Skip to content

Commit 0cb906c

Browse files
committed
refactor: clean up changes to CLI code
1 parent 824c683 commit 0cb906c

File tree

3 files changed

+127
-150
lines changed

3 files changed

+127
-150
lines changed

src/hatch_fancy_pypi_readme/__main__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pathlib import Path
1212
from typing import TextIO
1313

14-
from ._cli import cli_run
14+
from ._cli import Backend, cli_run
1515

1616

1717
if sys.version_info < (3, 11):
@@ -46,16 +46,23 @@ def main() -> None:
4646
help="Target file for output. Default: standard out.",
4747
metavar="TARGET-FILE-PATH",
4848
)
49+
parser.add_argument(
50+
"--backend",
51+
choices=[enum.value for enum in Backend],
52+
default=Backend.AUTO.value,
53+
help="Build backend in use. Default: auto-detect from pyproject.toml.",
54+
)
4955
args = parser.parse_args()
5056

5157
pyproject = tomllib.loads(Path(args.pyproject_path).read_text())
5258
hatch_toml = _maybe_load_hatch_toml(args.hatch_toml)
59+
backend = Backend(args.backend)
5360

5461
out: TextIO
5562
out = Path(args.o).open("w") if args.o else sys.stdout # noqa: SIM115
5663

5764
with closing(out):
58-
cli_run(pyproject, hatch_toml, out)
65+
cli_run(pyproject, hatch_toml, out, backend)
5966

6067

6168
def _maybe_load_hatch_toml(hatch_toml_arg: str | None) -> dict[str, object]:

src/hatch_fancy_pypi_readme/_cli.py

Lines changed: 74 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
import sys
88

9-
from contextlib import suppress
9+
from dataclasses import dataclass
10+
from enum import Enum
1011
from typing import Any, NoReturn, TextIO
1112

1213
from hatch_fancy_pypi_readme.exceptions import ConfigurationError
@@ -15,29 +16,70 @@
1516
from ._config import load_and_validate_config
1617

1718

19+
class Backend(Enum):
20+
AUTO = "auto"
21+
HATCHLING = "hatchling"
22+
PDM_BACKEND = "pdm.backend"
23+
24+
25+
@dataclass
26+
class BackendSettings:
27+
pyproject_key: str
28+
use_hatch_toml: bool
29+
30+
31+
_BACKEND_SETTINGS = {
32+
Backend.HATCHLING: BackendSettings(
33+
pyproject_key="tool.hatch.metadata.hooks.fancy-pypi-readme",
34+
use_hatch_toml=True,
35+
),
36+
Backend.PDM_BACKEND: BackendSettings(
37+
pyproject_key="tool.pdm.build.hooks.fancy-pypi-readme",
38+
use_hatch_toml=False,
39+
),
40+
}
41+
42+
43+
_HATCH_TOML_KEY = "metadata.hooks.fancy-pypi-readme"
44+
45+
1846
def cli_run(
19-
pyproject: dict[str, Any], hatch_toml: dict[str, Any], out: TextIO
47+
pyproject: dict[str, Any],
48+
hatch_toml: dict[str, Any],
49+
out: TextIO,
50+
backend: Backend = Backend.AUTO,
2051
) -> None:
2152
"""
2253
Best-effort verify config and print resulting PyPI readme.
2354
"""
24-
is_dynamic = False
25-
with suppress(KeyError):
26-
is_dynamic = "readme" in pyproject["project"]["dynamic"]
55+
if backend is Backend.AUTO:
56+
backend = _dwim_backend(pyproject)
57+
settings = _BACKEND_SETTINGS[backend]
2758

28-
if not is_dynamic:
59+
if "readme" not in _get_dotted(pyproject, "project.dynamic", ()):
2960
_fail("You must add 'readme' to 'project.dynamic'.")
3061

31-
find_config = _find_config
32-
with suppress(KeyError):
33-
build_backend = pyproject["build-system"]["build-backend"]
34-
if build_backend == "pdm.backend":
35-
find_config = _find_config_pdm
36-
37-
cfg = find_config(pyproject, hatch_toml)
62+
config_data = _get_dotted(pyproject, settings.pyproject_key)
63+
config_key = settings.pyproject_key
64+
config_source = f"`[{settings.pyproject_key}]` in pyproject.toml"
65+
66+
if settings.use_hatch_toml:
67+
hatch_toml_config = _get_dotted(hatch_toml, _HATCH_TOML_KEY)
68+
config_source += f" or `[{_HATCH_TOML_KEY}]` in hatch.toml"
69+
if hatch_toml_config and config_data:
70+
_fail(
71+
"Both pyproject.toml and hatch.toml contain "
72+
"hatch-fancy-pypi-readme configuration."
73+
)
74+
if hatch_toml_config is not None:
75+
config_data = hatch_toml_config
76+
config_key = _HATCH_TOML_KEY
77+
78+
if config_data is None:
79+
_fail(f"Missing configuration ({config_source})")
3880

3981
try:
40-
config = load_and_validate_config(cfg)
82+
config = load_and_validate_config(config_data, base=f"{config_key}.")
4183
except ConfigurationError as e:
4284
_fail(
4385
"Configuration has errors:\n\n"
@@ -47,73 +89,25 @@ def cli_run(
4789
print(build_text(config.fragments, config.substitutions), file=out)
4890

4991

50-
def _fail(msg: str) -> NoReturn:
51-
print(msg, file=sys.stderr)
52-
sys.exit(1)
53-
54-
55-
def _find_config(
56-
pyproject: dict[str, Any], hatch_toml: dict[str, Any]
57-
) -> dict[str, Any]:
58-
"""Find fancy-pypi-readme configuration table.
59-
60-
This find the configuration from either pyproject.toml or hatch.toml data,
61-
with table names as used when running under ``hatchling``.
62-
63-
"""
64-
pyproject_config = _get_table(
65-
pyproject, "tool.hatch.metadata.hooks.fancy-pypi-readme"
66-
)
67-
hatch_toml_config = _get_table(
68-
hatch_toml, "metadata.hooks.fancy-pypi-readme"
69-
)
70-
if hatch_toml_config and pyproject_config:
71-
_fail(
72-
"Both pyproject.toml and hatch.toml contain "
73-
"hatch-fancy-pypi-readme configuration."
74-
)
75-
if hatch_toml_config is not None:
76-
return hatch_toml_config
77-
if pyproject_config is None:
78-
_fail(
79-
"Missing configuration "
80-
"(`[tool.hatch.metadata.hooks.fancy-pypi-readme]` in"
81-
" pyproject.toml or `[metadata.hooks.fancy-pypi-readme]`"
82-
" in hatch.toml)"
83-
)
84-
return pyproject_config
85-
86-
87-
def _find_config_pdm(
88-
pyproject: dict[str, Any], hatch_toml: dict[str, Any]
89-
) -> dict[str, Any]:
90-
"""Find fancy-pypi-readme configuration table, PDM version.
92+
def _get_dotted(
93+
data: dict[str, Any], dotted_key: str, default: Any = None
94+
) -> Any:
95+
try:
96+
for key in dotted_key.split("."):
97+
data = data[key]
98+
except KeyError:
99+
return default
100+
return data
91101

92-
This finds the configuration from pyproject.toml assuming we are
93-
running under ``pdm-backend``.
94-
"""
95-
if _get_table(hatch_toml, "metadata.hooks.fancy-pypi-readme"):
96-
_fail(
97-
"Configuration in hatch.toml is ignored "
98-
"when using pdm-backend as the build backend."
99-
)
100-
cfg = _get_table(pyproject, "tool.pdm.build.hooks.fancy-pypi-readme")
101-
if cfg is None:
102-
_fail(
103-
"Missing configuration "
104-
"(`[tool.pdm.build.hooks.fancy-pypi-readme]` in pyproject.toml)"
105-
)
106-
return cfg
107102

103+
def _dwim_backend(pyproject: dict[str, Any]) -> Backend:
104+
"""Guess backend from pyproject.toml."""
105+
build_backend = _get_dotted(pyproject, "build-system.build-backend")
106+
if build_backend == "pdm.backend":
107+
return Backend.PDM_BACKEND
108+
return Backend.HATCHLING
108109

109-
def _get_table(data: dict[str, Any], key: str) -> dict[str, Any] | None:
110-
"""Get config data from dotted key.
111110

112-
Returns ``None`` if no data exists at the given dotted key.
113-
"""
114-
try:
115-
for part in key.split("."):
116-
data = data[part]
117-
except KeyError:
118-
return None
119-
return data
111+
def _fail(msg: str) -> NoReturn:
112+
print(msg, file=sys.stderr)
113+
sys.exit(1)

tests/test_cli.py

Lines changed: 44 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import pytest
1111

1212
from hatch_fancy_pypi_readme.__main__ import _maybe_load_hatch_toml, tomllib
13-
from hatch_fancy_pypi_readme._cli import cli_run
13+
from hatch_fancy_pypi_readme._cli import Backend, cli_run
1414

1515
from .utils import run
1616

@@ -29,12 +29,25 @@ def _pyproject(pyproject_toml_path):
2929
return tomllib.loads(pyproject_toml_path.read_text())
3030

3131

32+
@pytest.fixture(name="backend", params=Backend)
33+
def _backend(request):
34+
return request.param
35+
36+
3237
@pytest.fixture(name="empty_pyproject")
33-
def _empty_pyproject():
34-
return {
38+
def _empty_pyproject(backend):
39+
pyproject = {
3540
"project": {"dynamic": ["foo", "readme", "bar"]},
36-
"tool": {"hatch": {"metadata": {"hooks": {"fancy-pypi-readme": {}}}}},
3741
}
42+
if backend is Backend.PDM_BACKEND:
43+
pyproject["tool"] = {
44+
"pdm": {"build": {"hooks": {"fancy-pypi-readme": {}}}}
45+
}
46+
else:
47+
pyproject["tool"] = {
48+
"hatch": {"metadata": {"hooks": {"fancy-pypi-readme": {}}}}
49+
}
50+
return pyproject
3851

3952

4053
class TestCLIEndToEnd:
@@ -136,66 +149,52 @@ def test_config_in_hatch_toml(self, tmp_path, monkeypatch):
136149

137150

138151
class TestCLI:
139-
def test_cli_run_missing_dynamic(self, capfd):
152+
def test_cli_run_missing_dynamic(self, backend, capfd):
140153
"""
141154
Missing readme in dynamic is caught and gives helpful advice.
142155
"""
143156
with pytest.raises(SystemExit):
144-
cli_run({}, {}, sys.stdout)
157+
cli_run({}, {}, sys.stdout, backend)
145158

146159
out, err = capfd.readouterr()
147160

148161
assert "You must add 'readme' to 'project.dynamic'.\n" == err
149162
assert "" == out
150163

151-
def test_cli_run_missing_config(self, capfd):
164+
def test_cli_run_missing_config(self, backend, capfd):
152165
"""
153166
Missing configuration is caught and gives helpful advice.
154167
"""
155-
with pytest.raises(SystemExit):
156-
cli_run(
157-
{"project": {"dynamic": ["foo", "readme", "bar"]}},
158-
{},
159-
sys.stdout,
168+
if backend is Backend.PDM_BACKEND:
169+
source = (
170+
"`[tool.pdm.build.hooks.fancy-pypi-readme]` in pyproject.toml"
171+
)
172+
else:
173+
source = (
174+
"`[tool.hatch.metadata.hooks.fancy-pypi-readme]` in pyproject.toml"
175+
" or `[metadata.hooks.fancy-pypi-readme]` in hatch.toml"
160176
)
161177

162-
out, err = capfd.readouterr()
163-
164-
assert (
165-
"Missing configuration "
166-
"(`[tool.hatch.metadata.hooks.fancy-pypi-readme]` in"
167-
" pyproject.toml or `[metadata.hooks.fancy-pypi-readme]` in"
168-
" hatch.toml)\n" == err
169-
)
170-
assert "" == out
171-
172-
def test_cli_run_missing_pdm_config(self, capfd):
173-
"""
174-
Missing configuration is caught and gives helpful advice.
175-
"""
176178
with pytest.raises(SystemExit):
177179
cli_run(
178-
{
179-
"build-system": {"build-backend": "pdm.backend"},
180-
"project": {"dynamic": ["foo", "readme", "bar"]},
181-
},
180+
{"project": {"dynamic": ["foo", "readme", "bar"]}},
182181
{},
183182
sys.stdout,
183+
backend,
184184
)
185185

186186
out, err = capfd.readouterr()
187187

188-
assert (
189-
"Missing configuration "
190-
"(`[tool.pdm.build.hooks.fancy-pypi-readme]` in"
191-
" pyproject.toml)\n" == err
192-
)
188+
assert f"Missing configuration ({source})\n" == err
193189
assert "" == out
194190

195-
def test_cli_run_two_configs(self, capfd):
191+
def test_cli_run_two_configs(self, backend, capfd):
196192
"""
197193
Ambiguous two configs.
198194
"""
195+
if backend is Backend.PDM_BACKEND:
196+
return # hatch.toml is ignored under pdm.backend
197+
199198
meta = {
200199
"metadata": {
201200
"hooks": {
@@ -223,47 +222,24 @@ def test_cli_run_two_configs(self, capfd):
223222
)
224223
assert "" == out
225224

226-
def test_hatch_toml_config_with_pdm_backend(self, capfd):
227-
"""
228-
Config in hatch.toml is an error when using pdm-backend
229-
"""
230-
hooks = {
231-
"hooks": {"fancy-pypi-readme": {"content-type": "text/markdown"}}
232-
}
233-
with pytest.raises(SystemExit):
234-
cli_run(
235-
{
236-
"build-system": {"build-backend": "pdm.backend"},
237-
"project": {"dynamic": ["readme"]},
238-
"tool": {"pdm": {"build": hooks}},
239-
},
240-
{"metadata": hooks},
241-
sys.stdout,
242-
)
243-
244-
out, err = capfd.readouterr()
245-
246-
assert (
247-
"Configuration in hatch.toml is ignored when using"
248-
" pdm-backend as the build backend.\n" == err
249-
)
250-
assert "" == out
251-
252-
def test_cli_run_config_error(self, capfd, empty_pyproject):
225+
def test_cli_run_config_error(self, backend, capfd, empty_pyproject):
253226
"""
254227
Configuration errors are detected and give helpful advice.
255228
"""
229+
if backend is Backend.PDM_BACKEND:
230+
config_prefix = "tool.pdm.build.hooks.fancy-pypi-readme"
231+
else:
232+
config_prefix = "tool.hatch.metadata.hooks.fancy-pypi-readme"
233+
256234
with pytest.raises(SystemExit):
257-
cli_run(empty_pyproject, {}, sys.stdout)
235+
cli_run(empty_pyproject, {}, sys.stdout, backend)
258236

259237
out, err = capfd.readouterr()
260238

261239
assert (
262240
"Configuration has errors:\n\n"
263-
"- tool.hatch.metadata.hooks.fancy-pypi-readme."
264-
"content-type is missing.\n"
265-
"- tool.hatch.metadata.hooks.fancy-pypi-readme.fragments "
266-
"is missing.\n" == err
241+
f"- {config_prefix}.content-type is missing.\n"
242+
f"- {config_prefix}.fragments is missing.\n" == err
267243
)
268244
assert "" == out
269245

0 commit comments

Comments
 (0)