From 6c8116e49ff011336b3b73a749f9980c03fa7a45 Mon Sep 17 00:00:00 2001 From: Jack Nagy Date: Sat, 12 Sep 2026 20:35:53 +0100 Subject: [PATCH] Keep every README link working on the package page pyproject sets `readme = "README.md"`, so the README is also the PyPI description. PyPI renders it standalone with no base URL, so a relative path resolves against pypi.org and 404s, while GitHub resolves the same path against the repository. A relative link therefore looks correct in review and is dead on the package page. Four were relative: the three docs links I added with the API reference yesterday, and LICENSE, which predates them. The intro's ocf-pki-laundry link was already absolute, which is the trace of someone hitting this before. All four are absolute now. Nothing was broken in public: v0.1.17 predates the docs commit, so the three new links have never been published. They would have shipped with the next release. A test walks every link in the README and holds three rules: no relative paths, an absolute link into this repository must point at a file that exists, and an intra-document anchor must match a heading. Verified by reintroducing each fault in turn. The shipped sdist's PKG-INFO, which is what PyPI renders, now carries 28 links and no relative ones. --- README.md | 10 ++++----- tests/test_public_api_contract.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d7f3294..6e5e4ed 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ The repo also ships a self-contained **reference bridge demo** (`mqtt_demo/`) th ## Documentation -- **[`docs/api.md`](docs/api.md)** — the supported API: every name downstream code may import, with its signature. Generated from the code, so it cannot drift. -- [`docs/ocf-pki-laundry.md`](docs/ocf-pki-laundry.md) — the newer OCF-PKI appliance generation, and why an AC14K_M certificate is refused there. -- [`docs/ocf-vd-devices.md`](docs/ocf-vd-devices.md) — server-authenticated findings from Samsung VD hardware. +- **[`docs/api.md`](https://github.com/QuiteYellow/SmartThings-Local/blob/main/docs/api.md)** — the supported API: every name downstream code may import, with its signature. Generated from the code, so it cannot drift. +- [`docs/ocf-pki-laundry.md`](https://github.com/QuiteYellow/SmartThings-Local/blob/main/docs/ocf-pki-laundry.md) — the newer OCF-PKI appliance generation, and why an AC14K_M certificate is refused there. +- [`docs/ocf-vd-devices.md`](https://github.com/QuiteYellow/SmartThings-Local/blob/main/docs/ocf-vd-devices.md) — server-authenticated findings from Samsung VD hardware. ## Quick start (library) @@ -217,7 +217,7 @@ The public API is organized by responsibility rather than re-exported through one large root namespace. Explicit imports from these modules are intentional and covered by the downstream compatibility contract. -**[`docs/api.md`](docs/api.md) is the full reference**: every supported name, with its signature read off the code. It is generated from `tools/api_contract.py` by `tools/generate_api_docs.py`, and the test suite fails if the page and the code disagree. A public name absent from it is reachable but incidental, and may move. The table below is the map; that page is the inventory. +**[`docs/api.md`](https://github.com/QuiteYellow/SmartThings-Local/blob/main/docs/api.md) is the full reference**: every supported name, with its signature read off the code. It is generated from `tools/api_contract.py` by `tools/generate_api_docs.py`, and the test suite fails if the page and the code disagree. A public name absent from it is reachable but incidental, and may move. The table below is the map; that page is the inventory. | Module | Supported responsibility | | --- | --- | @@ -1185,4 +1185,4 @@ This is an independent, unofficial project. It is **not affiliated with, authori "Samsung", "SmartThings", and any related names, marks, and logos are trademarks of Samsung Electronics Co., Ltd. They are used in this project **only nominatively** — to identify the hardware and protocols this software interoperates with — and no claim is made to any right in them. Use of these marks does not imply any affiliation with or endorsement by their owner. -The software is provided under the [MIT License](LICENSE) for interoperability with hardware you own, without warranty of any kind. +The software is provided under the [MIT License](https://github.com/QuiteYellow/SmartThings-Local/blob/main/LICENSE) for interoperability with hardware you own, without warranty of any kind. diff --git a/tests/test_public_api_contract.py b/tests/test_public_api_contract.py index cf77563..7dae12f 100644 --- a/tests/test_public_api_contract.py +++ b/tests/test_public_api_contract.py @@ -472,3 +472,37 @@ def test_contents_links_resolve_to_a_heading_on_the_page(): assert anchors, "the page should carry a table of contents" for anchor in anchors: assert anchor in headings, anchor + + +_REPO_ROOT = Path(__file__).resolve().parent.parent +_REPO_BLOB = "https://github.com/QuiteYellow/SmartThings-Local/blob/main/" + + +def test_readme_links_render_on_pypi_and_on_github(): + """pyproject sets `readme = "README.md"`, so this file is also the + package's PyPI description. + + PyPI renders it standalone with no base URL, so a relative path resolves + against pypi.org and 404s. GitHub resolves the same path against the + repository, which is why a relative link looks fine in review and is + dead on the package page. Links are therefore either absolute or an + intra-document anchor. + """ + readme = (_REPO_ROOT / "README.md").read_text() + headings = { + generate_api_docs._slug(line) + for line in readme.splitlines() + if line.startswith("#") + } + + for target in re.findall(r"\]\(([^)]+)\)", readme): + if target.startswith("#"): + assert target[1:] in headings, f"README anchor goes nowhere: {target}" + continue + assert target.startswith(("https://", "http://")), ( + f"relative README link will not render on PyPI: {target}. " + f"Use {_REPO_BLOB}{target}" + ) + if target.startswith(_REPO_BLOB): + path = _REPO_ROOT / target[len(_REPO_BLOB):].split("#", 1)[0] + assert path.exists(), f"README links to a missing file: {target}"