readmeta
Check that your README will actually render on PyPI — by inspecting the built artifact, not the repo. And with --fix, rewrite the broken references at build time.
The problem
PyPI renders the long_description from your built artifact (the body of METADATA in a wheel / PKG-INFO in an sdist). It does not render your repo's README.md. Things that look perfect on GitHub silently break on PyPI:
- Relative images (
) → 404, PyPI has nodocs/folder - Relative links (
[usage](docs/usage.md)) → resolve againstpypi.org, broken - Raw SVG references (
assets/logo.svg) → 404; PyPI doesn't serve repo files - In-page anchors (
[setup](#instalation)) → go nowhere when the heading id doesn't exist
twine check validates that the description renders — it does not check that any link or image actually resolves.
Real cases this catches:
armsmithv1.2.1 shipped a relative SVG badge that 404'd on its PyPI page while rendering fine on GitHub.aicertifymaintains a separateREADME-pypi.mdwith rewritten absolute URLs, precisely because the GitHub README breaks on PyPI.- An audit of the
modern-pythonGitHub org found 23 of 25 packages shipping relative asset references that can't resolve on PyPI.
Install
pip install readmeta
Requires Python 3.9+. No dependencies — stdlib only.
Usage
Check your built artifacts (build first, then check what PyPI will actually see):
python -m build
readmeta check dist/*
Or check what's currently hosted on PyPI:
readmeta check --pypi requests
Example output:
fakepkg 0.1.0 [text/markdown] <- dist/fakepkg-0.1.0-py3-none-any.whl
Found 2 issue(s):
[relative-image] dist/fakepkg-0.1.0-py3-none-any.whl:12
docs/shot.png
-> PyPI renders the description standalone; relative image paths 404. Use an absolute https:// URL (e.g. raw.githubusercontent.com).
[broken-anchor] dist/fakepkg-0.1.0-py3-none-any.whl:20
#instalation
-> No heading with a matching id was found; the link goes nowhere on PyPI.
Exit codes are CI-friendly: 0 = clean, 1 = issues found, 2 = error (unreadable artifact, PyPI unreachable, bad usage).
--fix: rewrite at build time (v0.2)
Checking tells you what's broken; --fix rewrites it. This is a build-pipeline
step, not a source edit — your source README keeps its GitHub-friendly relative
paths, and the built artifacts get absolute URLs:
python -m build
readmeta check dist/* --fix --repo octo/demo
twine upload dist/*.fixed.*
Why build-time and not source-time? Two constraints:
- The artifact is derived output — hand-editing
dist/*.whlis not a repeatable step. - Baking absolute URLs into the source README is wrong for GitHub: a
tag-pinned
raw.githubusercontent.comURL 404s until the tag exists (chicken-and-egg), and a branch-pinned URL drifts. Keep relative paths in the source; let the pipeline rewrite them for PyPI.
What --fix does:
- Rewrites every
relative-image/relative-link/relative-svgfinding to an absolute URL. Images (incl..svg) resolve tohttps://raw.githubusercontent.com/<repo>/<ref>/<path>; other relative links tohttps://github.com/<repo>/blob/<ref>/<path>. Use--refto pin something other thanmain. - Or resolve everything against your own base with
--base-url https://cdn.example.com/static/(mutually exclusive with--repo). - Writes
*.fixed.whl/*.fixed.tar.gznext to the inputs (or into--out-dir). Inputs are never modified. - Never touches fenced code blocks, inline code spans, or
<pre>/<code>— documentation about a bad pattern is not rewritten. - Cannot rewrite broken in-page anchors (
#anchorhas no sensible absolute form); those are reported as remaining issues for you to fix by hand.
Example:
$ readmeta check dist/* --fix --repo octo/demo
...
--fix: rewrote 3 reference(s) -> dist/fakepkg-0.1.0-py3-none-any.fixed.whl
[relative-image] dist/fakepkg-0.1.0-py3-none-any.whl:12
docs/shot.png
-> https://raw.githubusercontent.com/octo/demo/main/docs/shot.png
--fix: 1 issue(s) could not be rewritten (manual fix needed):
[broken-anchor] dist/fakepkg-0.1.0-py3-none-any.fixed.whl:20
#instalation
-> No heading with a matching id was found; the link goes nowhere on PyPI.
With --fix, the exit code is 0 when everything was clean or fully rewritten,
1 when unrewritable issues (anchors) remain, 2 on error.
CI example
- name: Build
run: python -m build
- name: Check PyPI rendering
run: |
pip install readmeta
readmeta check dist/*
- name: Rewrite for PyPI and upload the fixed artifacts
run: |
readmeta check dist/* --fix --repo ${{ github.repository }}
twine upload dist/*.fixed.*
Differentiation
- Inspects the built artifact, not the repo. Other README linters scan your
README.md; readmeta reads thelong_descriptionout of the wheel/sdist — the exact bytes PyPI will render. A separateREADME-pypi.mdworkflow can't drift from what you ship, because the check runs on the artifact itself. twine checkdoesn't check links.twine checkvalidates that the description renders as valid RST/Markdown. It never follows a URL, so a relative image that 404s on every PyPI page passestwine checkcleanly.--fixrewrites at build time. No other zero-dependency tool both detects PyPI-broken references in the artifact and emits fixed artifacts in the same pipeline step — without touching your source files.- Zero dependencies. Stdlib only (
zipfile,tarfile,html.parser,urllib) — safe topip installin any CI job.
How it works
- Reads
long_descriptionfrom.whl(zipfile→.dist-info/METADATA) or.tar.gz(tarfile→PKG-INFO), plus theDescription-Content-Typeheader. - For
text/html, parses withhtml.parserand validates everyimg[src],a[href], and#anchoragainst collected element ids (explicit ids plus GitHub-style heading slugs). - For Markdown/RST (what artifacts actually carry — the raw source, not rendered HTML), scans with regexes: inline and reference-style images/links, embedded
<img>tags, RSTimage::/figure::directives, and#anchorlinks against# Headingslugs. --fixlocates the same matches on code-masked text (positions preserved) and splices absolute URLs into the original, then rewrites theMETADATA/PKG-INFObody inside a copy of the artifact.--pypimode fetcheshttps://pypi.org/pypi/<name>/jsonand checks the hosted description.
Limitations
- Anchors can't be auto-fixed —
--fixreports them; fix the heading or the link by hand. - Anchor validation for RST is limited (Markdown headings and HTML ids are covered; RST
.. _target:definitions are not yet resolved). - Code spans and fenced code blocks are ignored (documenting a bad pattern doesn't flag it); indented code blocks are still scanned.
- Heading-slug generation approximates GitHub/PyPI's algorithm; exotic headings could produce false positives — explicit
idattributes always win. --pypiuses the rawdescriptionfrom the JSON API (the API doesn't expose rendered HTML); findings are identical to checking a fresh local build.
License
MIT — see LICENSE.
Metadata
Release files for readmeta 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| readmeta-0.2.0.tar.gz | 23.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| readmeta-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.0 kB
Release files / readmeta-0.2.0.tar.gz
| Download URL | readmeta-0.2.0.tar.gz |
|---|---|
| Size | 23.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
38d4a98fa59cd631d12e59dbb5740624a4810e304f58940e65a8a7b19e902a6f
|
|
BLAKE2b-256 checksum How to use checksums |
40c40ec9192efb7f29bd6b32b4d0fe81265dd50745fd415232d7dbd2fe4684c5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / readmeta-0.2.0-py3-none-any.whl
| Download URL | readmeta-0.2.0-py3-none-any.whl |
|---|---|
| Size | 18.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
127fcff30bc6e430d3388d4b587779fc80de35345c5301571a62789068433e81
|
|
BLAKE2b-256 checksum How to use checksums |
0d6329e3359e6b25a31a8e080cb56f5e65c96f03c8d844586ed7fecd5ccee2aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|