Skip to content

Commit a310c80

Browse files
authored
fix: match cookiecutter output with new TOMLMatcher (#714)
* chore: add TOMLMatcher helper Signed-off-by: Henry Schreiner <[email protected]> * fix: match example with cookiecutter output Signed-off-by: Henry Schreiner <[email protected]> * Apply suggestion from @henryiii --------- Signed-off-by: Henry Schreiner <[email protected]>
1 parent b01071f commit a310c80

File tree

7 files changed

+93
-39
lines changed

7 files changed

+93
-39
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,5 @@ repos:
9595
name: Cog the pages
9696
language: python
9797
entry: cog -P -r -I ./helpers
98-
files: "^docs/pages/guides/(packaging_compiled|docs|tasks|gha_basic).md|^copier.yml"
99-
additional_dependencies: [cogapp, cookiecutter]
98+
files: "^docs/pages/guides/(packaging_compiled|docs|tasks|gha_basic).md|^copier.yml|^docs/_includes/pyproject.md"
99+
additional_dependencies: [cogapp, cookiecutter, tomlkit]

docs/_includes/pyproject.md

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,67 @@
11
## pyproject.toml: project table
22

3+
<!-- [[[cog
4+
from cog_helpers import code_fence, render_cookie, TOMLMatcher
5+
with render_cookie(backend="uv") as package:
6+
pyproject = TOMLMatcher.from_file(package / "pyproject.toml")
7+
]]] -->
8+
<!-- [[[end]]] -->
9+
310
The metadata is specified in a [standards-based][metadata] format:
411

12+
<!-- [[[cog
13+
with code_fence("toml"):
14+
print(pyproject.get_source("project"))
15+
]]] -->
16+
<!-- prettier-ignore-start -->
517
```toml
618
[project]
719
name = "package"
820
version = "0.1.0"
21+
authors = [
22+
{ name = "My Name", email = "[email protected]" },
23+
]
924
description = "A great package."
1025
readme = "README.md"
1126
license = "BSD-3-Clause"
1227
license-files = ["LICENSE"]
13-
authors = [
14-
{ name = "My Name", email = "[email protected]" },
15-
]
16-
maintainers = [
17-
{ name = "My Organization", email = "[email protected]" },
18-
]
1928
requires-python = ">=3.10"
20-
21-
dependencies = [
22-
"typing_extensions",
23-
]
24-
2529
classifiers = [
26-
"Development Status :: 4 - Beta",
30+
"Development Status :: 1 - Planning",
31+
"Intended Audience :: Science/Research",
32+
"Intended Audience :: Developers",
33+
"Operating System :: OS Independent",
34+
"Programming Language :: Python",
35+
"Programming Language :: Python :: 3",
2736
"Programming Language :: Python :: 3 :: Only",
2837
"Programming Language :: Python :: 3.10",
2938
"Programming Language :: Python :: 3.11",
3039
"Programming Language :: Python :: 3.12",
3140
"Programming Language :: Python :: 3.13",
3241
"Programming Language :: Python :: 3.14",
33-
"Topic :: Scientific/Engineering :: Physics",
42+
"Topic :: Scientific/Engineering",
43+
"Typing :: Typed",
3444
]
45+
dependencies = []
3546

3647
[project.urls]
37-
Homepage = "https://github.com/organization/package"
38-
Documentation = "https://package.readthedocs.io/"
39-
"Bug Tracker" = "https://github.com/organization/package/issues"
40-
Discussions = "https://github.com/organization/package/discussions"
41-
Changelog = "https://package.readthedocs.io/en/latest/changelog.html"
48+
Homepage = "https://github.com/org/package"
49+
"Bug Tracker" = "https://github.com/org/package/issues"
50+
Discussions = "https://github.com/org/package/discussions"
51+
Changelog = "https://github.com/org/package/releases"
4252
```
53+
<!-- prettier-ignore-end -->
54+
<!-- [[[end]]] -->
4355

4456
You can read more about each field, and all allowed fields, in
4557
[packaging.python.org][metadata],
4658
[Flit](https://flit.readthedocs.io/en/latest/pyproject_toml.html#new-style-metadata)
47-
or [Whey](https://whey.readthedocs.io/en/latest/configuration.html). Note that
48-
"Homepage" is special, and replaces the old url setting.
59+
or [Whey](https://whey.readthedocs.io/en/latest/configuration.html). Only the
60+
`name` and `version` fields are strictly required. Note that "Homepage" is
61+
special, and replaces the old url setting.
62+
63+
If you use the above configuration, you need `README.md` and `LICENSE` files,
64+
since they are explicitly specified.
4965

5066
### License
5167

@@ -55,15 +71,17 @@ The modern way is to use the `license` field and an [SPDX identifier
5571
expression][spdx]. You can specify a list of files globs in `license-files`. You
5672
need `hatchling>=1.26`, `flit-core>=1.11` (1.12 for complex license statements),
5773
`pdm-backend>=2.4`, `setuptools>=77`, `meson-python>=0.18`, `maturin>=1.9.2`,
58-
`poetry-core>=2.2`, or `scikit-build-core>=0.12` to support this.
74+
`poetry-core>=2.2`, or `scikit-build-core>=0.12` to support this. You can also
75+
specify `license-files` as a list with globs for license files. If you don't,
76+
most backends will discover common license file names by default.
5977

6078
The classic convention uses one or more [Trove Classifiers][] to specify the
6179
license. There also was a `license.file` field, required by `meson-python`, but
6280
other tools often did the wrong thing (such as load the entire file into the
6381
metadata's free-form one line text field that was intended to describe
6482
deviations from the classifier license(s)).
6583

66-
```
84+
```toml
6785
classifiers = [
6886
"License :: OSI Approved :: BSD License",
6987
]
@@ -122,15 +140,30 @@ your package); the `dev` group is even installed, by default, when using `uv`'s
122140
high level commands like `uv run` and `uv sync`. {% rr PP0086 %} Here is an
123141
example:
124142

143+
<!-- [[[cog
144+
with code_fence("toml"):
145+
print(pyproject.get_source("dependency-groups"))
146+
]]] -->
147+
<!-- prettier-ignore-start -->
125148
```toml
126149
[dependency-groups]
127150
test = [
128-
"pytest >=6.0",
151+
"pytest >=6",
152+
"pytest-cov >=3",
129153
]
130154
dev = [
131155
{ include-group = "test" },
132156
]
157+
docs = [
158+
"sphinx>=7.0",
159+
"myst_parser>=0.13",
160+
"sphinx_copybutton",
161+
"sphinx_autodoc_typehints",
162+
"furo>=2023.08.17",
163+
]
133164
```
165+
<!-- prettier-ignore-end -->
166+
<!-- [[[end]]] -->
134167

135168
You can include one dependency group in another. Most tools allow you to install
136169
groups using `--group`, like `pip` (25.1+), `uv pip`, and the high level `uv`

docs/pages/guides/docs.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ Ideally, software documentation should include:
7575
> it if you are interested.
7676
7777
<!-- [[[cog
78-
from cog_helpers import code_fence, render_cookie, Matcher
78+
from cog_helpers import code_fence, render_cookie, PyMatcher
7979
with render_cookie(backend="hatch", docs="sphinx") as package:
8080
docs_conf_py = package.joinpath("docs/conf.py").read_text(encoding="utf-8").strip()
8181
docs_index_md = package.joinpath("docs/index.md").read_text(encoding="utf-8").strip()
8282
readthedocs_yaml = package.joinpath(".readthedocs.yaml").read_text(encoding="utf-8").strip()
83-
noxfile = Matcher.from_file(package / "noxfile.py")
83+
noxfile = PyMatcher.from_file(package / "noxfile.py")
8484
with render_cookie(backend="hatch", docs="mkdocs") as package:
8585
mkdocs_conf_yaml = package.joinpath("mkdocs.yml").read_text(encoding="utf-8").strip()
86-
noxfile_mkdocs = Matcher.from_file(package / "noxfile.py")
86+
noxfile_mkdocs = PyMatcher.from_file(package / "noxfile.py")
8787
readthedocs_yaml_mkdocs = package.joinpath(".readthedocs.yaml").read_text(encoding="utf-8").strip()
8888
]]] -->
8989
<!-- [[[end]]] -->

docs/pages/guides/packaging_compiled.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,9 @@ install_subdir('src/package', install_dir: py.get_install_dir() / 'package', str
163163
<!-- prettier-ignore-end -->
164164
<!-- [[[end]]] -->
165165

166-
Meson also requires that `LICENSE` and `README.md` exist, and that your source
167-
be tracked by version control. In a real project, you will likely be doing this,
168-
but when trying out a build backend you might not think to add these even though
169-
they are required.
166+
Meson requires that your source be tracked by version control. In a real
167+
project, you will likely be doing this, but when trying out a build backend you
168+
might not think to set up a git repo to build it.
170169

171170
{% endtab %} {% tab maturin Maturin %}
172171

docs/pages/guides/tasks.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ parent: Topical Guides
1111
# Task runners
1212

1313
<!-- [[[cog
14-
from cog_helpers import code_fence, render_cookie, Matcher
14+
from cog_helpers import code_fence, render_cookie, PyMatcher
1515
with render_cookie() as package:
16-
noxfile = Matcher.from_file(package / "noxfile.py")
16+
noxfile = PyMatcher.from_file(package / "noxfile.py")
1717
]]] -->
1818
<!-- [[[end]]] -->
1919

helpers/cog_helpers.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22

33
import ast
44
import contextlib
5+
import functools
56
import tempfile
67
import typing
78
from pathlib import Path
89
from types import SimpleNamespace
910

11+
import tomlkit
12+
from cookiecutter.main import cookiecutter
13+
1014
if typing.TYPE_CHECKING:
1115
from collections.abc import Generator
1216
from typing import Self
1317

14-
from cookiecutter.main import cookiecutter
1518

1619
DIR = Path(__file__).parent.resolve()
1720

@@ -29,17 +32,17 @@ def render_cookie(**context: str) -> Generator[Path, None, None]:
2932
yield Path(tmpdir).joinpath("package").resolve()
3033

3134

32-
class Matcher:
33-
def __init__(self, txt: str) -> None:
35+
class PyMatcher:
36+
def __init__(self, txt: str, /) -> None:
3437
self.ast = ast.parse(txt)
3538
self.lines = txt.splitlines()
3639

3740
@classmethod
38-
def from_file(cls, filename: Path) -> Self:
41+
def from_file(cls, filename: Path, /) -> Self:
3942
with filename.open(encoding="utf-8") as f:
4043
return cls(f.read())
4144

42-
def get_source(self, name: str) -> str:
45+
def get_source(self, name: str, /) -> str:
4346
o = SimpleNamespace(name=name)
4447
for item in self.ast.body:
4548
match item:
@@ -53,6 +56,24 @@ def get_source(self, name: str) -> str:
5356
raise RuntimeError(msg)
5457

5558

59+
class TOMLMatcher:
60+
def __init__(self, txt: str, /) -> None:
61+
self.toml = tomlkit.loads(txt)
62+
63+
@classmethod
64+
def from_file(cls, filename: Path, /) -> Self:
65+
with filename.open(encoding="utf-8") as f:
66+
return cls(f.read())
67+
68+
def get_source(self, dotted_name: str, /) -> str:
69+
names = dotted_name.split(".")
70+
toml_inner = functools.reduce(lambda d, k: d[k], names, self.toml)
71+
toml = functools.reduce(
72+
lambda d, k: tomlkit.table().add(k, d), reversed(names), toml_inner
73+
)
74+
return tomlkit.dumps(toml).strip()
75+
76+
5677
@contextlib.contextmanager
5778
def code_fence(lang: str, /, *, width: int = 3) -> Generator[None, None, None]:
5879
tics = "`" * width

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ test = [
9696
cog = [
9797
"cogapp",
9898
"cookiecutter",
99+
"tomlkit",
99100
]
100101

101102
[tool.hatch]

0 commit comments

Comments
 (0)