hatch-fancy-pypi-readme is a Hatch metadata plugin for everyone who cares about the first impression of their project’s PyPI landing page. It allows you to define your PyPI project description in terms of concatenated fragments that are based on static strings, files, and most importantly: parts of files defined using cut-off points or regular expressions.
Once you’ve assembled your readme, you can additionally run regular expression-based substitutions over it. For instance to make relative links absolute or to linkify users and issue numbers in your changelog.
Do you want your PyPI readme to be the project readme, but without badges, followed by the license file, and the changelog section for only the last release? You’ve come to the right place!
Note
“PyPI project description”, “PyPI landing page”, and “PyPI readme” all refer to the same thing.
In setuptools it’s called long_description
and is the text shown on a project’s PyPI page.
We refer to it as “readme” because that’s how it’s called in PEP 621-based pyproject.toml
files.
- Anthropic SDK (
pyproject.toml
) - attrs (
pyproject.toml
) - Awkward Array (
pyproject.toml
) - Black (
pyproject.toml
) - doc2dash (
pyproject.toml
) - environ-config (
pyproject.toml
) - jsonschema (
pyproject.toml
) - Gradio (
pyproject.toml
) - httpx (
pyproject.toml
) - OpenAI SDK (
pyproject.toml
) - Pydantic (
pyproject.toml
) - pytermgui (
pyproject.toml
) - scikit-build (
pyproject.toml
) - stamina (
pyproject.toml
) - structlog (
pyproject.toml
) - Twisted (
pyproject.toml
)
hatch-fancy-pypi-readme doesn’t use itself to avoid a circular dependency that can be problematic in some cases. The shoemaker’s kids always go barefoot.
Please open a pull request to add your ✨fancy✨ project!
The main reason for my (past) hesitancy to move away from setup.py
files is that I like to make my PyPI readmes a lot more interesting, than what static strings or static files can offer me.
For example this is the code that gave me the PyPI readme for attrs 22.1.0. Especially having a summary of the latest changes is something I’ve found users to appreciate.
Hatch’s extensibility finally allowed me to build this plugin that allows you to switch away from setup.py
without compromising on the user experience.
Now you too can have fancy PyPI readmes – just by adding a few lines of configuration to your pyproject.toml
.
hatch-fancy-pypi-readme is, like Hatch, configured in your project’s pyproject.toml
1.
First you add hatch-fancy-pypi-readme to your [build-system]
:
[build-system]
requires = ["hatchling", "hatch-fancy-pypi-readme"]
build-backend = "hatchling.build"
Next, you tell the build system that your readme is dynamic by adding it to the project.dynamic
list:
[project]
# ...
dynamic = ["readme"]
Important
Don’t forget to remove the old readme
key!
Next, you add a [tool.hatch.metadata.hooks.fancy-pypi-readme]
section.
Here, you must supply a content-type
.
Currently, only text/markdown
and text/x-rst
are supported by PyPI.
[tool.hatch.metadata.hooks.fancy-pypi-readme]
content-type = "text/markdown"
Finally, you also must supply an array of fragments
.
A fragment is a piece of text that is appended to your readme in the order that it’s specified.
We recommend TOML’s syntactic sugar for arrays of wrapping the array name in double brackets and will use it throughout this documentation.
Text fragments consist of a single text
key and are appended to the readme exactly as you specify them:
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
text = "Fragment #1"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
text = "Fragment #2"
results in:
Fragment #1Fragment #2
Note that there’s no additional space or empty lines between fragments unless you specify them.
A file fragment reads a file specified by the path
key and appends it:
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
path = "AUTHORS.md"
Additionally it’s possible to cut away parts of the file before appending it:
-
start-after
cuts away everything before and including the string specified. -
start-at
cuts away everything before the string specified too, but the string itself is preserved. This is useful when you want to start at a heading without adding a marker before it.start-after
andstart-at
are mutually exclusive. -
end-before
cuts away everything after. -
pattern
takes a regular expression and returns the first group from it (you probably want to make your capture group non-greedy by appending a question mark:(.*?)
). Internally, it usesre.search(pattern, whatever_is_left_after_slicing, re.DOTALL).group(1)
to find it.
Both Markdown and reStructuredText (reST) have comments (<!-- this is a Markdown comment -->
and .. this is a reST comment
) that you can use for invisible markers:
# Boring Header
<!-- cut after this -->
This is the *interesting* body!
<!-- but before this -->
Uninteresting Footer
together with:
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
path = "path.md"
start-after = "<!-- cut after this -->\n\n"
end-before = "\n\n<!-- but before this -->"
pattern = "the (.*?) body"
would append:
*interesting*
to your readme.
Tip
-
You can insert the same file multiple times – each time a different part!
-
The order of the options in a fragment block does not matter. They’re always executed in the same order:
start-after
/start-at
end-before
pattern
For a complete example, please see our example configuration.
After a readme is assembled out of fragments, it’s possible to run an arbitrary number of regular expression-based substitutions over it:
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
pattern = "This is a (.*) that we'll replace later."
replacement = 'It was a "\1"!'
ignore-case = true # optional; false by default
Substitutions can be useful for replacing relative links with absolute ones:
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
# Literal TOML strings (single quotes) need no escaping of backslashes.
pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)'
replacement = '[\1](https://github.com/hynek/hatch-fancy-pypi-readme/tree/main\g<2>)'
Or, expanding GitHub issue/pull request IDs to links:
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
# Regular TOML strings (double quotes) do need escaping.
pattern = "#(\\d+)"
replacement = "[#\\1](https://github.com/hynek/hatch-fancy-pypi-readme/issues/\\1)"
Or, replacing GitHub-style callouts that aren't supported by PyPI with bolded text:
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
pattern = '\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]'
replacement = '**\1**:'
Again, please check out our example configuration for a complete example.
If the final readme contains the string $HFPR_VERSION
, it is replaced by the current package version.
When running hatch-fancy-pypi-readme in CLI mode (as described in the next section), packaging metadata is not available.
In that case $HFPR_VERSION
is hardcoded to 42.0
so you can still test your readme.
For faster feedback loops, hatch-fancy-pypi-readme comes with a CLI interface that takes a pyproject.toml
file as an argument and renders out the readme that would go into respective package.
Your can run it either as hatch-fancy-pypi-readme
or python -m hatch_fancy_pypi_readme
.
If you don’t pass an argument, it looks for a pyproject.toml
in the current directory.
You can optionally pass a -o
option to write the output into a file instead of to standard out.
Since hatch-fancy-pypi-readme is part of the isolated build system, it shouldn’t be installed along with your projects. Therefore we recommend running it using pipx:
$ pipx run hatch-fancy-pypi-readme
You can pipe the output into tools like rich-cli or bat to verify your markup.
For example, if you run
$ pipx run hatch-fancy-pypi-readme | pipx run rich-cli --markdown --hyperlinks -
with our example configuration, you will get the following output:
Warning
While the execution model is somewhat different from the Hatch-Python packaging pipeline, it uses the same configuration validator and text renderer, so the fidelity should be high.
It will not help you debug packaging issues, though.
To verify your PyPI readme using the full packaging pipeline, check out my build-and-inspect-python-package GitHub Action.
If you ensure that hatch-fancy-pypi-readme is installed in your Hatch environment (that means where the hatch
CLI command lives – not your development environment), you can also let Hatch render it for you:
hatch project metadata readme
gives you a rendered version of the readme.hatch project metadata | jq -r .readme.text
gives you the raw Markdown (needs jq).
Footnotes
-
As with Hatch, you can also use
hatch.toml
for configuration options that start withtool.hatch
and leave that prefix out. That meanspyprojects.toml
’s[tool.hatch.metadata.hooks.fancy-pypi-readme]
becomes[metadata.hooks.fancy-pypi-readme]
when inhatch.toml
. To keep the documentation simple, the more commonpyproject.toml
syntax is used throughout. ↩