Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ dependencies = [
[project.entry-points.hatch]
fancy-pypi-readme = "hatch_fancy_pypi_readme.hooks"

[project.entry-points."setuptools.finalize_distribution_options"]
fancy-pypi-readme = "hatch_fancy_pypi_readme._setuptools:update_metadata"

[project.scripts]
hatch-fancy-pypi-readme = "hatch_fancy_pypi_readme.__main__:main"

Expand Down
20 changes: 12 additions & 8 deletions src/hatch_fancy_pypi_readme/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,30 @@ class Config:
_BASE = "tool.hatch.metadata.hooks.fancy-pypi-readme."


def load_and_validate_config(config: dict[str, Any]) -> Config:
def load_and_validate_config(
config: dict[str, Any], base: str = _BASE
) -> Config:
errs = []

ct = config.get("content-type")
if ct is None:
errs.append(f"{_BASE}content-type is missing.")
errs.append(f"{base}content-type is missing.")
elif ct not in ("text/markdown", "text/x-rst"):
errs.append(
f"{_BASE}content-type: '{ct}' is not one of "
f"{base}content-type: '{ct}' is not one of "
"['text/markdown', 'text/x-rst']"
)

try:
fragments = _load_fragments(config.get("fragments"))
fragments = _load_fragments(config.get("fragments"), base)
except ConfigurationError as e:
errs.extend(e.errors)

try:
subs_cfg = config.get("substitutions", [])
if not isinstance(subs_cfg, list):
raise ConfigurationError(
[f"{_BASE}substitutions must be an array."]
[f"{base}substitutions must be an array."]
)

substitutions = [
Expand All @@ -62,14 +64,16 @@ def load_and_validate_config(config: dict[str, Any]) -> Config:
)


def _load_fragments(config: list[dict[str, str]] | None) -> list[Fragment]:
def _load_fragments(
config: list[dict[str, str]] | None, base: str
) -> list[Fragment]:
"""
Load fragments from *config*.
"""
if config is None:
raise ConfigurationError([f"{_BASE}fragments is missing."])
raise ConfigurationError([f"{base}fragments is missing."])
if not config:
raise ConfigurationError([f"{_BASE}fragments must not be empty."])
raise ConfigurationError([f"{base}fragments must not be empty."])

frags = []
errs = []
Expand Down
45 changes: 45 additions & 0 deletions src/hatch_fancy_pypi_readme/_setuptools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

import sys
from typing import TYPE_CHECKING

from ._builder import build_text
from ._config import Config, load_and_validate_config

if sys.version_info < (3, 11):
import tomli as tomllib
else:
import tomllib

if TYPE_CHECKING:
from setuptools import Distribution


_NAME = "fancy-pypi-readme"


def get_config() -> Config | None:
with open("pyproject.toml", "rb") as f:
tools = tomllib.load(f).get("tool") or {}

return (
load_and_validate_config(tools[_NAME], f"tool.{_NAME}.")
if _NAME in tools
else None
)


def update_metadata(dist: Distribution) -> None:
"""Update the distribution's core metadata description"""
config = get_config()
if config:
dist.metadata.long_description = build_text(
config.fragments,
config.substitutions,
version=dist.metadata.get_version(),
)
dist.metadata.long_description_content_type = config.content_type


# Delay hook so that plugins like setuptools-scm can run first:
update_metadata.order = 100 # type: ignore[attr-defined]
53 changes: 32 additions & 21 deletions tests/test_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import pytest

from .utils import append, run
from .utils import append, replace, run


def build_project(*args, check=True):
Expand All @@ -16,13 +16,29 @@ def build_project(*args, check=True):
return run("build", *args, check=check)


def pyproject(backend, new_project, additional_text):
append(new_project / "pyproject.toml", additional_text)

if backend == "setuptools":
replace(
new_project / "pyproject.toml",
{
"tool.hatch.metadata.hooks.": "tool.",
"hatchling.build": "setuptools.build_meta",
"hatchling": "setuptools",
},
)


@pytest.mark.slow()
def test_build(new_project):
@pytest.mark.parametrize("backend", ("hatchling", "setuptools"))
def test_build(new_project, backend):
"""
Build a fake project end-to-end and verify wheel contents.
"""
append(
new_project / "pyproject.toml",
pyproject(
backend,
new_project,
"""
[tool.hatch.metadata.hooks.fancy-pypi-readme]
content-type = "text/markdown"
Expand All @@ -40,8 +56,7 @@ def test_build(new_project):

build_project()

whl = new_project / "dist" / "my_app-1.0-py2.py3-none-any.whl"

whl = next(new_project.glob("dist/my_app-1.0-*.whl"))
assert whl.exists()

run("wheel", "unpack", whl)
Expand All @@ -54,30 +69,26 @@ def test_build(new_project):

assert "text/markdown" == metadata["Description-Content-Type"]
assert (
"# Level 1\n\nFancy *Markdown*.\n---\nFooter" == metadata.get_payload()
"# Level 1\n\nFancy *Markdown*.\n---\nFooter"
== metadata.get_payload().strip()
)


@pytest.mark.slow()
def test_invalid_config(new_project):
@pytest.mark.parametrize("backend", ("hatchling", "setuptools"))
def test_invalid_config(new_project, backend):
"""
Missing config makes the build fail with a meaningful error message.
"""
pyp = new_project / "pyproject.toml"

# If we leave out the config for good, the plugin doesn't get activated.
pyp.write_text(
pyp.read_text() + "[tool.hatch.metadata.hooks.fancy-pypi-readme]"
pyproject(
backend,
new_project,
# If we leave out the config for good, the plugin doesn't get activated.
"[tool.hatch.metadata.hooks.fancy-pypi-readme]",
)

out = build_project(check=False)

assert "hatch_fancy_pypi_readme.exceptions.ConfigurationError:" in out
assert (
"tool.hatch.metadata.hooks.fancy-pypi-readme.content-type "
"is missing." in out
)
assert (
"tool.hatch.metadata.hooks.fancy-pypi-readme.fragments "
"is missing." in out
)
assert ".fancy-pypi-readme.content-type is missing." in out
assert ".fancy-pypi-readme.fragments is missing." in out
7 changes: 7 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import subprocess
import sys
from functools import reduce

import pytest

Expand All @@ -23,3 +24,9 @@ def run(*args, check=True):

def append(file, text):
file.write_text(file.read_text() + text)


def replace(file, subs):
file.write_text(
reduce(lambda acc, x: acc.replace(*x), subs.items(), file.read_text())
)