fix(typing): make SVDynamics members read-only; unblock linter bumps - #299
Merged
Conversation
#296 (dependabot, 16 packages) fails `quality` and all four `tests-and-type-check` legs. The runtime stack is fine — every leg reports 1111 passed before dying on `uv run ty check`. One package is responsible: ty 0.0.18 -> 0.0.65, which emits 16 diagnostics. Reproduced by running `uvx --from ty==0.0.65 ty check` against main. Fifteen were tooling artifacts. One was a real bug. `SVDynamics` declared `name: str` and `has_explicit_level: bool` as mutable protocol members. Mutable members are invariant, so `RandomWalk.name: Literal["random_walk"]` never actually satisfied the protocol, and `SV_DYNAMICS_REGISTRY: dict[str, type[SVDynamics]]` was unassignable. Declaring them read-only properties makes the members covariant and the narrowing legal. `@runtime_checkable` is unaffected: `isinstance` checks attribute presence, which pydantic fields still provide. Note `Mapping[str, type[SVDynamics]]` does *not* fix this — measured, still 16 diagnostics. Container covariance does not change the element check. The remaining 14 were `invalid-assignment` on lines like with pytest.raises(ValidationError): data.endog = np.zeros((100, 3)) where the assignment *is* the assertion. ty >= 0.0.65 models pydantic frozen semantics and rejects them statically. The rule is now ignored for `tests/**` only; `src/` keeps it, which is what surfaced the protocol bug. Four inline `# ty: ignore[invalid-assignment]` comments in test_sv_spec.py and test_volatility.py are now redundant and removed — they were earlier patches for this same pattern. `_check_finite` used `zip(frame.columns, finite, strict=True)`, which ty could not unpack. Rewritten as a boolean mask over the index: shorter, no suppression needed. Dependabot now groups ty/ruff/prek separately from runtime dependencies. Bundled together, one new lint rule blocks everything else in the PR — #296 stalled numpy 2.4.6's fix for `np.linalg.svd(hermitian=True)` returning non-unitary `vh`, which matters for a library factorising covariances. Verified on both halves of the pinned matrix — ty 0.0.18, 0.0.65 and 0.0.67 all clean, `make check` passes, and the suite passes on py3.11/PyMC 5/ArviZ 0 (1111 passed) and py3.12/PyMC 6/ArviZ 1 (1112 passed).
thomaspinder
force-pushed
the
fix/ty-protocol-variance
branch
from
August 7, 2026 07:47
99a8c33 to
14fd62a
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #299 +/- ##
=====================================
Coverage 96.1% 96.1%
=====================================
Files 55 55
Lines 3547 3548 +1
Branches 441 441
=====================================
+ Hits 3409 3410 +1
Misses 102 102
Partials 36 36 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Unblocks #296 and fixes the one real bug it surfaced.
Why #296 fails
Dependabot's 16-package bump fails
qualityand all fourtests-and-type-checklegs. The runtime stack is fine — every failing leg prints1111 passed, 1 skippedand then dies onuv run ty check.lowest-direct-deps,arviz-1-2-compat,wheel-install,build-docsanddocs-linkcheckall pass, which is the tell: the floors are fine, a newly locked version isn't.One package of the sixteen is responsible:
ty0.0.18 → 0.0.65, 47 releases of a pre-1.0 type checker, emitting 16 diagnostics. Reproduced againstmainwithuvx --from ty==0.0.65 ty check.Fifteen were tooling artifacts. One was a real bug.
The real bug:
SVDynamicsprotocol varianceA mutable protocol member is invariant, so
RandomWalk.name: Literal["random_walk"]never actually satisfiedname: str, andSV_DYNAMICS_REGISTRY: dict[str, type[SVDynamics]]was unassignable. ty 0.0.18 just didn't check it. Declaring both as read-only properties makes them covariant and the narrowing legal.@runtime_checkableis unaffected —isinstancechecks attribute presence, which the pydantic fields still provide. Confirmed:isinstance(RandomWalk(), SVDynamics)andisinstance(AR1(), SVDynamics)both stillTrue, registry resolves, 208 SV/volatility tests pass.Worth recording:
Mapping[str, type[SVDynamics]]does not fix this. I measured it — still 16 diagnostics.Mapping's covariance is in the container, not the element check; each value must still be assignable totype[SVDynamics].The 14 false positives
The assignment is the assertion. ty ≥ 0.0.65 models pydantic frozen semantics and rejects statically the exact line whose runtime rejection is being asserted.
invalid-assignmentis now ignored fortests/**only —src/keeps the rule, which is what surfaced the protocol bug above.Four inline
# ty: ignore[invalid-assignment]comments intest_sv_spec.pyandtest_volatility.pybecome redundant and are removed. They were earlier one-off patches for this same pattern, which is evidence the friction predates this bump.The
zipdiagnostic_check_finiteusedzip(frame.columns, finite, strict=True), which ty couldn't unpack (_T_co@zip is not iterable). Rewritten as a boolean mask over the index — shorter, faster, no suppression needed.Dependabot grouping
ty,ruffandpreknow group separately from runtime dependencies. Bundled underpatterns: ["*"], one new lint rule blocks everything else in the same PR — #296 stalled numpy 2.4.6's fix fornp.linalg.svd(..., hermitian=True)returning non-unitaryvh(numpy#31347) behind a type-checker bump, and this library factorises covariance matrices.Verification
Both halves of the pinned PyMC/ArviZ matrix, since the lock carries both families:
ty0.0.18 (locked)ty0.0.65 (#296)ty0.0.67 (latest)make checkpasses. The skip istest_arviz_compat.py:70, ArviZ-1-only, pre-existing.Then, decisively: overlaid #296's
uv.lockonto this branch and re-ran everything.ty0.0.65 →All checks passed,prek run --all-files→ all hooks pass, suite → 1111 passed. This branch is sufficient to unblock #296; that PR needs only a rebase afterwards.Two things found but deliberately not fixed here
Unbounded ty floor.
pyproject.tomlhas"ty>=0.0.14", so the lock is the only thing holding it at 0.0.18 — anyuv lockregeneration reintroduces these 16 diagnostics, not just a dependabot PR. This branch makes that harmless rather than papering over it.Two different ruffs.
.pre-commit-config.yamlpinsruff-pre-commitatv0.14.14(what prek and therefore CI enforce), while the lock's dev-dependency ruff is 0.15.2 → 0.16.1. They have already drifted:uv run ruff format --checkunder 0.16.1 wants to reformat 13 files that prek's 0.14.14 considers clean. Not a #296 blocker and not touched here, butdependabot.ymlhas nopre-commitecosystem entry, so that rev is never bumped automatically. Worth its own PR.