Skip to content

fix: make the release publishable and the adapter type-visible - #152

Merged
cayossarian merged 2 commits into
developfrom
fix/release-packaging-and-typing
Aug 4, 2026
Merged

fix: make the release publishable and the adapter type-visible#152
cayossarian merged 2 commits into
developfrom
fix/release-packaging-and-typing

Conversation

@cayossarian

Copy link
Copy Markdown
Member

Fixes the review findings that would have bitten on publish, plus the two
medium-severity ones. Targets develop — prototype work, nothing on main.

Blocker: the release workflow could not publish this repo

release.yml was never updated for the two-distribution layout. It ran
uv build (root only) and sed-ed the version into the root pyproject.toml
only. Publishing 3.0.0b1 today would have put span-panel-api on PyPI with
no 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-0 would
have failed on the second name.

A tag now selects a distribution instead of setting a version:

Tag Publishes
v3.0.0b1 span-panel-api
schema-0-v1.0.0b1 span-panel-api-schema-0

The job resolves the distribution from the tag, verifies the tagged version
against the one committed in that distribution's pyproject.toml, and builds
only 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 versions
participate 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 rejected
with a message naming both accepted forms.

High: the adapter distribution shipped untyped

py.typed did not travel with the parser when it moved out of
src/span_panel_api/, which has one. Confirmed against installed wheels in a
clean venv, under mypy --strict:

before: error: Skipping analyzing "span_panel_api_schema_0": module is installed,
               but missing library stubs or py.typed marker  [import-untyped]
        note: Revealed type is "Any"

after:  note: Revealed type is "span_panel_api_schema_0.adapter.SchemaZeroAdapter"
        note: Revealed type is "list[str]"
        Success: no issues found in 1 source file

Fully annotated, --strict-clean code resolving as Any for every consumer
defeats the point of the typing work. Both CI and the release job now fail any
wheel built without the marker, and tests/test_packaging.py asserts it from
the manifests so an adapter added under packages/ is covered the day it
exists.

Medium: duplicated adapter key

factory.py returned the literal "schema_0" while client.py used
DEFAULT_ADAPTER_KEY. Two sources of truth for the key that the only two
callers of resolve_adapter must agree on — and a divergence is invisible in a
dev workspace where every adapter is installed, surfacing only as an
unresolvable key in a real install.

Medium: conformance checking depended on member kind

_REQUIRED_MEMBERS screened vars() for callable, which reads as "every
declared member" and is not: a property object is not callable and neither is
a classmethod object, so either would have silently stopped being required the
day 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 a
synthetic protocol declaring one of each kind, rather than asserted in prose.

Verification

  • 432 tests pass (+6), coverage 94%, all pre-commit hooks green.
  • Both new guards were mutation-checked: removing py.typed and restoring the
    callable filter each fail their test.

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.
@cayossarian
cayossarian merged commit e85cec7 into develop Aug 4, 2026
5 checks passed
@cayossarian
cayossarian deleted the fix/release-packaging-and-typing branch August 4, 2026 21:23
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