Publish the supported API as a generated reference - #86
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 anh4. No heading survives that, and the anchor becomes an unusable slug. Signatures moved into fenced blocks under bare-name headings.DEADis'dead'. One table per module now.dtls_probe's ten constants above its five functions. Classes lead, then functions, then the table.h2toh4and 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.pyholds the mapping, moved out of the test so the generator and the test read one source.tests/test_share_safety.pyalready imports fromtools/, 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_handshakewas missing from the contract. LocalThings imports it atconfig_flow.py:537, and I extended it withauth=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.probestays undeclared. LocalThings imports that too, atconfig_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.getdocfalls back to an instance's type docstring, so every outcome constant rendered asstr(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str. Constants now show their value, which is what a reader wants fromDEADanyway.from __future__ import annotationsleaves stringized annotations, so signatures readidentity: 'bytes'untileval_str=Trueresolved them..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:
xis 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
inspectrenders the same annotations differently by version and I had committed a 3.14 render. Two causes, both now normalised:typing.Optional[X]renders asX | Nonefrom 3.14 and asOptional[X]before it. Twelve lines differed. Signatures canonicalise it by bracket matching, since the inner type nests as inOptional[Callable[[], bool]]and a regex mis-slices that.inspect.signatureon an Enum class returnsEnumMeta.__call__:(*values)on 3.14, and the whole functional API on 3.11.SamsungServerRolenow 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.
--checkprints 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
h3child of "BLE OCF framing": the list of names downstream may import, parented under a codec section. It is anh2directly after the library quick start now, and a Documentation block near the top linksdocs/api.mdalongside the two device-research files.The
h3sections 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 andtools/does not, so a test importing fromtools/cannot be collected from a bare sdist.tests/test_share_safety.pyalready 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. Shippingtools/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.pyis clean, and the build plus distribution check pass.docs/api.mdandtools/stay out of the sdist, matching the existing policy.