fix: make the release publishable and the adapter type-visible - #152
Merged
Conversation
The release workflow was never updated for the two-distribution layout. It built the root package only and rewrote the root version from the tag, so publishing 3.0.0b1 would have shipped a bootstrap with no adapter on PyPI — every install connecting and then raising SpanPanelAdapterMissingError, with the package that fixes it not existing. A tag now selects a distribution rather than setting a version: vX.Y.Z -> span-panel-api schema-N-vX.Y.Z -> span-panel-api-schema-N and the job fails unless the tag matches the version committed in that distribution's pyproject.toml. Stamping the version at release time is not extensible to two packages and is now actively wrong: the adapter declares a dependency floor on the bootstrap, so both committed versions participate in resolution and cannot be treated as placeholders. The adapter distribution shipped without a py.typed marker — it did not travel with the code when the parser moved out of src/span_panel_api, which has one. Fully annotated, strict-clean code resolved as Any for every downstream consumer. Verified against installed wheels: strict mypy reported import-untyped and 'Revealed type is "Any"' before, real types after. Both CI and the release job now reject a wheel built without the marker. Two review findings alongside: - factory dispatch returned the literal "schema_0" while the transport's default path used DEFAULT_ADAPTER_KEY. Two sources of truth for the key the two callers of resolve_adapter must agree on, and a divergence is invisible in a dev workspace where every adapter is installed. - required-member derivation screened vars() for callable, which reads as equivalent to "every declared member" and is not: property and classmethod objects are not callable, so a protocol member of either kind would have stopped being required without anyone noticing. The derivation is extracted into a function so the rule is testable against a synthetic protocol rather than asserted in a comment. 432 tests pass, coverage 94%.
…ribution PyPI trusted publishing is configured per project, so span-panel-api-schema-0 needs a pending publisher created before its first release or the publish step fails on an otherwise correct build.
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.
Fixes the review findings that would have bitten on publish, plus the two
medium-severity ones. Targets
develop— prototype work, nothing onmain.Blocker: the release workflow could not publish this repo
release.ymlwas never updated for the two-distribution layout. It ranuv build(root only) andsed-ed the version into the rootpyproject.tomlonly. Publishing
3.0.0b1today would have putspan-panel-apion PyPI withno adapter package, so every install connects and then raises
SpanPanelAdapterMissingError— and the package that fixes it would not exist.The changelog's own
pip install span-panel-api span-panel-api-schema-0wouldhave failed on the second name.
A tag now selects a distribution instead of setting a version:
v3.0.0b1span-panel-apischema-0-v1.0.0b1span-panel-api-schema-0The job resolves the distribution from the tag, verifies the tagged version
against the one committed in that distribution's
pyproject.toml, and buildsonly that package so
dist/cannot pick up a sibling.Stamping the version at release time is not extensible to two packages, and
under this layout it is actively wrong: the adapter declares a floor on the
bootstrap (
span-panel-api>=3.0.0b1,<4.0), so both committed versionsparticipate in resolution and are no longer placeholders a release can
overwrite. This does change the maintainer ritual to bump-commit-then-tag, with
a mismatch failing loudly rather than publishing a surprise.
Tag parsing was exercised against
v3.0.0b1,v2.6.4,schema-0-v1.0.0b1,multi-digit majors, and malformed tags (
1.2.3,nightly), which are rejectedwith a message naming both accepted forms.
High: the adapter distribution shipped untyped
py.typeddid not travel with the parser when it moved out ofsrc/span_panel_api/, which has one. Confirmed against installed wheels in aclean venv, under
mypy --strict:Fully annotated,
--strict-clean code resolving asAnyfor every consumerdefeats the point of the typing work. Both CI and the release job now fail any
wheel built without the marker, and
tests/test_packaging.pyasserts it fromthe manifests so an adapter added under
packages/is covered the day itexists.
Medium: duplicated adapter key
factory.pyreturned the literal"schema_0"whileclient.pyusedDEFAULT_ADAPTER_KEY. Two sources of truth for the key that the only twocallers of
resolve_adaptermust agree on — and a divergence is invisible in adev workspace where every adapter is installed, surfacing only as an
unresolvable key in a real install.
Medium: conformance checking depended on member kind
_REQUIRED_MEMBERSscreenedvars()forcallable, which reads as "everydeclared member" and is not: a
propertyobject is not callable and neither isa
classmethodobject, so either would have silently stopped being required theday the protocol declared one. The comment claimed the stronger guarantee.
Kind filtering is dropped entirely — every public name in a Protocol's
vars()is a declared member, since Protocol's own machinery is uniformly
underscore-prefixed (verified by introspection). The derivation is extracted
into
_derive_required_members(protocol)so the rule is testable against asynthetic protocol declaring one of each kind, rather than asserted in prose.
Verification
py.typedand restoring thecallablefilter each fail their test.