Skip to content

Publish the supported API as a generated reference - #86

Merged
QuiteYellow merged 5 commits into
mainfrom
docs/publish-supported-api
Sep 12, 2026
Merged

QuiteYellow merged 5 commits into
mainfrom
docs/publish-supported-api

Conversation

@QuiteYellow

@QuiteYellow QuiteYellow commented Sep 12, 2026

Copy link
Copy Markdown
Owner

localthings#435 contains a section headed "What the library provides", verified there against released wheels and carrying its own list of gaps. @mbillow unzipped wheels to establish what this library exposes, and kept the result in his issue tracker. That is the gap this closes.

The README offered an eleven-row table of module responsibilities. 209 public names sit across those modules, of which 100 are supported, and that distinction lived only in tests/test_public_api_contract.py, where no consumer reads it.

What is here

docs/api.md: every supported name, with the signature and summary read off the object. Classes list their methods, constants show their values.

The layout took a second pass, because the first cut was a flat list of 106 equal-weight entries. Four things about it were measurably hard to use:

  • DtlsCoapSession's signature is 413 characters and it was inside an h4. No heading survives that, and the anchor becomes an unusable slug. Signatures moved into fenced blocks under bare-name headings.
  • 26 constants were 26 headings, each spending a heading and a detail line to say DEAD is 'dead'. One table per module now.
  • Alphabetical order put dtls_probe's ten constants above its five functions. Classes lead, then functions, then the table.
  • Headings skipped h2 to h4 and there was no contents list, so fifteen flat module names were the only way in. Six layers and a contents list now.

The layer grouping is the one piece of hand-maintained judgement, so a test asserts every supported module appears in exactly one layer. Another checks the contents anchors resolve to headings, with the slugs verified against GitHub's own renderer.

tools/api_contract.py holds the mapping, moved out of the test so the generator and the test read one source. tests/test_share_safety.py already imports from tools/, so the direction is established.

Four tests keep the page honest. One re-renders and compares, so a renamed argument or a new supported name fails the suite, which is where a stale page gets caught. The others assert every declared name reaches the page, every module is laid out once, and every contents anchor resolves. I checked the drift and anchor tests bite by breaking the file each way and watching them fail.

What writing it found

The two discovery functions now cross-reference each other in their docstrings, which is what help() and an IDE tooltip show. #20 spent five weeks dialling a plaintext response port as a DTLS target, and I said there that the similar names were mine to make clearer. The page separates them through the module and function summaries; the docstrings now say it outright.

diagnose_dtls_handshake was missing from the contract. LocalThings imports it at config_flow.py:537, and I extended it with auth= and documented it in 0.1.17 this week, all while it sat undeclared. It is declared now. I audited the other sixteen names I have pointed anyone at, publicly or in release notes, and the rest were already there.

probe stays undeclared. LocalThings imports that too, at config_flow.py:405, but it is the deprecated adapter, probe(stateless=False) already warns, and this list is a promise to keep a name working. That leaves LocalThings importing a name outside the contract, which is a conversation for #435 and not this PR.

Three rendering faults, each of which made the page wrong

Naming them because they are the kind of thing a generated page hides:

  • inspect.getdoc falls back to an instance's type docstring, so every outcome constant rendered as str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str. Constants now show their value, which is what a reader wants from DEAD anyway.
  • from __future__ import annotations leaves stringized annotations, so signatures read identity: 'bytes' until eval_str=True resolved them.
  • Cutting a summary at the first . turned "exits (i.e. socket dies)" into "exits (i." and "RFC 7252 §4.4 CoAP Ping" into "RFC 7252 §4.". Summaries take the first paragraph whole, capped at 200 characters.

RST double backticks are also normalised to single ones, purely as house style. I had this written up as a fourth fault until I checked it against GitHub's renderer: x is already a code span in Markdown, so it rendered correctly all along.

A generated page has to render the same everywhere

The first push passed on 3.14 and failed on 3.11, 3.12 and 3.13, because inspect renders the same annotations differently by version and I had committed a 3.14 render. Two causes, both now normalised:

  • typing.Optional[X] renders as X | None from 3.14 and as Optional[X] before it. Twelve lines differed. Signatures canonicalise it by bracket matching, since the inner type nests as in Optional[Callable[[], bool]] and a regex mis-slices that.
  • inspect.signature on an Enum class returns EnumMeta.__call__: (*values) on 3.14, and the whole functional API on 3.11. SamsungServerRole now lists its members and values, which is what an enum is for, and carries no signature to differ over.

Output is byte-identical on 3.11, 3.12, 3.13 and 3.14, each checked against a real interpreter, and the suite passes on all four plus the dependency floor.

--check prints a unified diff now. It reported only "stale" before, which turned an interpreter-specific render into a guess-and-push loop against CI when the cause was one line each time.

The reference is reachable now

"Supported library imports" sat at line 474 of 1180, as an h3 child of "BLE OCF framing": the list of names downstream may import, parented under a codec section. It is an h2 directly after the library quick start now, and a Documentation block near the top links docs/api.md alongside the two device-research files.

The h3 sections that followed it are still under "BLE OCF framing", which is the same misparenting one level along. Splitting this README by audience is its own change.

One pre-existing wrinkle, now slightly wider

tests/ ships in the sdist and tools/ does not, so a test importing from tools/ cannot be collected from a bare sdist. tests/test_share_safety.py already had this, and I verified both files now fail to collect from an extracted sdist. CI does not catch it, because the sdist job imports the package without running the tests. Shipping tools/ alongside the tests it supports would fix it, and that belongs in its own change.

Validation

796 tests pass on Python 3.11, 3.12, 3.13 and 3.14, and on both CI dependency sets. check_share_safety.py is clean, and the build plus distribution check pass. docs/api.md and tools/ stay out of the sdist, matching the existing policy.

localthings#435 carries a section titled "What the library provides",
verified there against released wheels and carrying its own list of gaps.
A downstream maintainer unzipped wheels to find out what this library
exposes, because the README offered an eleven-row table of module
responsibilities and nothing else. 209 public names sit across those
modules; only 100 are actually supported, and that distinction lived in a
test.

docs/api.md now publishes it: every supported name grouped by module, with
its signature and summary read off the object. tools/api_contract.py holds
the mapping, moved out of the test so the generator and the test read one
source, following the direction tests/test_share_safety.py already takes.
Two tests keep the page honest, so a renamed argument fails the suite
rather than leaving the page quietly wrong.

Writing it found diagnose_dtls_handshake missing from the contract, which
LocalThings imports at config_flow.py:537 and which I extended and
documented this week. Declared.

Three rendering faults worth naming, since each made the page wrong rather
than ugly: inspect.getdoc falls back to an instance's *type* docstring, so
every outcome constant read "str(object='') -> str" instead of its value;
stringized annotations from `from __future__ import annotations` rendered
as quoted strings until eval_str resolved them; and cutting a summary at
the first "." turned "exits (i.e. socket dies)" into "exits (i." and
"RFC 7252 §4.4" into "RFC 7252 §4.", so summaries take the first paragraph
whole.

RST double backticks are also normalised to single, which is house style
and not a fix: checked against GitHub's renderer, ``x`` is already a code
span in Markdown.

`probe` stays undeclared. LocalThings imports it too, but it is the
deprecated adapter and this list is a promise to keep a name working.
The first cut was a flat list: fifteen module headings, then 106 entries of
equal weight, alphabetical. Four things made it hard to use, each measured
rather than a matter of taste.

DtlsCoapSession's signature is 413 characters, and it was inside an h4
heading. No heading survives that, and it turns the anchor into an unusable
slug. A signature now sits in a fenced block under a bare-name heading,
which also makes every entry linkable.

Twenty-six constants were twenty-six headings, each spending a heading and
a detail line to say DEAD is 'dead'. They collapse into one table per
module.

Alphabetical order put those ten constants above the five functions a
reader of dtls_probe came for. Classes lead now, then functions, then the
constants table.

Headings skipped h2 to h4 and the page had no contents, so fifteen flat
module names were the only way in. Modules are grouped into six layers in
tools/api_contract.py, and the page opens with a contents list. The layer
grouping is the one piece of hand-maintained judgement here, so a test
asserts every supported module appears in exactly one layer.

Exceptions no longer carry a redundant `()` block.

Two more tests: one that the contents anchors resolve to headings on the
page, checked against GitHub's own slug algorithm, and the existing
name-coverage test taught that a constant is now a table row.
The drift check passed on 3.14 and failed on 3.11, 3.12 and 3.13. inspect
renders typing.Optional[X] as `X | None` from 3.14 and as `Optional[X]`
before it, so the committed page depended on the interpreter that generated
it. Twelve lines differed between 3.13 and 3.14, all of them that.

Signatures now canonicalise Optional[X] to X | None, by bracket matching
rather than a regex, since the inner type nests as in
Optional[Callable[[], bool]]. Rendering on 3.13 and on 3.14 now produces
byte-identical output.

My local floor environment pinned the dependency floor while still running
3.14, which is why the version matrix caught this and I did not.
"Supported library imports" sat at line 474 of 1180, as an h3 child of
"BLE OCF framing". The list of names downstream code may import was
parented under a codec section, 40% of the way down the file.

It is now an h2 directly after the library quick start, and a Documentation
block near the top links docs/api.md alongside the two device-research
files, so the reference is reachable without scrolling.

The h3 sections that followed it — classified errors, resolved endpoints,
plaintext reads — are still parented under BLE OCF framing, which is the
same misparenting one level along. Splitting this README by audience is its
own change; this moves the piece someone asked for.
3.11 was the last version still failing the drift check. inspect.signature
on an Enum class returns EnumMeta.__call__, which renders as `(*values)` on
3.14 and as the full functional API, `(value, names=None, *, module=None,
...)`, on 3.11. Neither is something a caller would write.

SamsungServerRole now lists its members and values, which is the content a
consumer wants from an enum, and carries no signature to differ over.

--check also prints a unified diff now. It said only "stale" before, which
turned an interpreter-specific render into a guess-and-push loop against
CI. The cause was one line each time and the diff would have named it.

Verified byte-identical output on 3.11, 3.12, 3.13 and 3.14, and the full
suite passes on all four plus the dependency floor.
@QuiteYellow
QuiteYellow merged commit 7734312 into main Sep 12, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant