Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,5 @@ repos:
name: Cog the pages
language: python
entry: cog -P -r -I ./helpers
files: "^docs/pages/guides/(packaging_compiled|docs|tasks|gha_basic).md|^copier.yml"
additional_dependencies: [cogapp, cookiecutter]
files: "^docs/pages/guides/(packaging_compiled|docs|tasks|gha_basic).md|^copier.yml|^docs/_includes/pyproject.md"
additional_dependencies: [cogapp, cookiecutter, tomlkit]
79 changes: 56 additions & 23 deletions docs/_includes/pyproject.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,67 @@
## pyproject.toml: project table

<!-- [[[cog
from cog_helpers import code_fence, render_cookie, TOMLMatcher
with render_cookie(backend="uv") as package:
pyproject = TOMLMatcher.from_file(package / "pyproject.toml")
]]] -->
<!-- [[[end]]] -->

The metadata is specified in a [standards-based][metadata] format:

<!-- [[[cog
with code_fence("toml"):
print(pyproject.get_source("project"))
]]] -->
<!-- prettier-ignore-start -->
```toml
[project]
name = "package"
version = "0.1.0"
authors = [
{ name = "My Name", email = "[email protected]" },
]
description = "A great package."
readme = "README.md"
license = "BSD-3-Clause"
license-files = ["LICENSE"]
authors = [
{ name = "My Name", email = "[email protected]" },
]
maintainers = [
{ name = "My Organization", email = "[email protected]" },
]
requires-python = ">=3.10"

dependencies = [
"typing_extensions",
]

classifiers = [
"Development Status :: 4 - Beta",
"Development Status :: 1 - Planning",
"Intended Audience :: Science/Research",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering :: Physics",
"Topic :: Scientific/Engineering",
"Typing :: Typed",
]
dependencies = []

[project.urls]
Homepage = "https://github.com/organization/package"
Documentation = "https://package.readthedocs.io/"
"Bug Tracker" = "https://github.com/organization/package/issues"
Discussions = "https://github.com/organization/package/discussions"
Changelog = "https://package.readthedocs.io/en/latest/changelog.html"
Homepage = "https://github.com/org/package"
"Bug Tracker" = "https://github.com/org/package/issues"
Discussions = "https://github.com/org/package/discussions"
Changelog = "https://github.com/org/package/releases"
```
<!-- prettier-ignore-end -->
<!-- [[[end]]] -->

You can read more about each field, and all allowed fields, in
[packaging.python.org][metadata],
[Flit](https://flit.readthedocs.io/en/latest/pyproject_toml.html#new-style-metadata)
or [Whey](https://whey.readthedocs.io/en/latest/configuration.html). Note that
"Homepage" is special, and replaces the old url setting.
or [Whey](https://whey.readthedocs.io/en/latest/configuration.html). Only the
`name` and `version` fields are strictly required. Note that "Homepage" is
special, and replaces the old url setting.

If you use the above configuration, you need `README.md` and `LICENSE` files,
since they are explicitly specified.

### License

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

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

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

<!-- [[[cog
with code_fence("toml"):
print(pyproject.get_source("dependency-groups"))
]]] -->
<!-- prettier-ignore-start -->
```toml
[dependency-groups]
test = [
"pytest >=6.0",
"pytest >=6",
"pytest-cov >=3",
]
dev = [
{ include-group = "test" },
]
docs = [
"sphinx>=7.0",
"myst_parser>=0.13",
"sphinx_copybutton",
"sphinx_autodoc_typehints",
"furo>=2023.08.17",
]
```
<!-- prettier-ignore-end -->
<!-- [[[end]]] -->

You can include one dependency group in another. Most tools allow you to install
groups using `--group`, like `pip` (25.1+), `uv pip`, and the high level `uv`
Expand Down
6 changes: 3 additions & 3 deletions docs/pages/guides/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ Ideally, software documentation should include:
> it if you are interested.

<!-- [[[cog
from cog_helpers import code_fence, render_cookie, Matcher
from cog_helpers import code_fence, render_cookie, PyMatcher
with render_cookie(backend="hatch", docs="sphinx") as package:
docs_conf_py = package.joinpath("docs/conf.py").read_text(encoding="utf-8").strip()
docs_index_md = package.joinpath("docs/index.md").read_text(encoding="utf-8").strip()
readthedocs_yaml = package.joinpath(".readthedocs.yaml").read_text(encoding="utf-8").strip()
noxfile = Matcher.from_file(package / "noxfile.py")
noxfile = PyMatcher.from_file(package / "noxfile.py")
with render_cookie(backend="hatch", docs="mkdocs") as package:
mkdocs_conf_yaml = package.joinpath("mkdocs.yml").read_text(encoding="utf-8").strip()
noxfile_mkdocs = Matcher.from_file(package / "noxfile.py")
noxfile_mkdocs = PyMatcher.from_file(package / "noxfile.py")
readthedocs_yaml_mkdocs = package.joinpath(".readthedocs.yaml").read_text(encoding="utf-8").strip()
]]] -->
<!-- [[[end]]] -->
Expand Down
7 changes: 3 additions & 4 deletions docs/pages/guides/packaging_compiled.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,9 @@ install_subdir('src/package', install_dir: py.get_install_dir() / 'package', str
<!-- prettier-ignore-end -->
<!-- [[[end]]] -->

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

{% endtab %} {% tab maturin Maturin %}

Expand Down
4 changes: 2 additions & 2 deletions docs/pages/guides/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ parent: Topical Guides
# Task runners

<!-- [[[cog
from cog_helpers import code_fence, render_cookie, Matcher
from cog_helpers import code_fence, render_cookie, PyMatcher
with render_cookie() as package:
noxfile = Matcher.from_file(package / "noxfile.py")
noxfile = PyMatcher.from_file(package / "noxfile.py")
]]] -->
<!-- [[[end]]] -->

Expand Down
31 changes: 26 additions & 5 deletions helpers/cog_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

import ast
import contextlib
import functools
import tempfile
import typing
from pathlib import Path
from types import SimpleNamespace

import tomlkit
from cookiecutter.main import cookiecutter

if typing.TYPE_CHECKING:
from collections.abc import Generator
from typing import Self

from cookiecutter.main import cookiecutter

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

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


class Matcher:
def __init__(self, txt: str) -> None:
class PyMatcher:
def __init__(self, txt: str, /) -> None:
self.ast = ast.parse(txt)
self.lines = txt.splitlines()

@classmethod
def from_file(cls, filename: Path) -> Self:
def from_file(cls, filename: Path, /) -> Self:
with filename.open(encoding="utf-8") as f:
return cls(f.read())

def get_source(self, name: str) -> str:
def get_source(self, name: str, /) -> str:
o = SimpleNamespace(name=name)
for item in self.ast.body:
match item:
Expand All @@ -53,6 +56,24 @@ def get_source(self, name: str) -> str:
raise RuntimeError(msg)


class TOMLMatcher:
def __init__(self, txt: str, /) -> None:
self.toml = tomlkit.loads(txt)

@classmethod
def from_file(cls, filename: Path, /) -> Self:
with filename.open(encoding="utf-8") as f:
return cls(f.read())

def get_source(self, dotted_name: str, /) -> str:
names = dotted_name.split(".")
toml_inner = functools.reduce(lambda d, k: d[k], names, self.toml)
toml = functools.reduce(
lambda d, k: tomlkit.table().add(k, d), reversed(names), toml_inner
)
return tomlkit.dumps(toml).strip()


@contextlib.contextmanager
def code_fence(lang: str, /, *, width: int = 3) -> Generator[None, None, None]:
tics = "`" * width
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ test = [
cog = [
"cogapp",
"cookiecutter",
"tomlkit",
]

[tool.hatch]
Expand Down