Repository navigation
Close most of the mypy Optional-narrowing gap, fix bugs it surfaced - #10
Merged
Merged
Conversation
Reduces mypy errors on PGraph.py from 60 to 6 (the remainder is in plot()/highlight_vertex's visualization internals -- dozens of individual coord/x/y/name accesses on Optional types, left as debt since the per-site narrowing cost is high for comparatively low-value, non-core code). Most of the 54 fixed were narrowing-only (asserts/guards at points where an invariant already holds, e.g. an edge returned by self.edges() always has non-None v1/v2), but several were real, previously-undiscovered bugs, each verified empirically: - BaseVertex.label had no type annotation on its `self.label = None` assignment, so mypy inferred its type as bare `None` everywhere, not `int | None` -- the root cause of most of the _graphcolor- related noise. Same root cause for Edge.cost, whose type was inferred from the *first* branch of a multi-branch assignment (`self.cost = cost` inside `if cost is not None:`, where `cost` is narrowed to plain `float`) rather than the union of all branches. One-line annotation fixes at the actual assignment sites. - UGraph._graphcolor(): `self._ncomponents = lastlabel + 1` crashes with TypeError if the graph is empty (lastlabel stays None, since `range(self.n)` never executes) -- reproduced concretely by adding then removing a vertex and calling `.nc`. Fixed to treat an empty graph as 0 components. - _BaseGraph.closest(), BaseVertex.heuristic_distance()/distance()/ closest()/x/y/z: none of these guarded against a vertex with no coordinate (`coord is None`, a legitimate state for a non-embedded vertex) or, for closest()/heuristic_distance()/distance(), a vertex not connected to a graph (closest() was missing the same graph- membership guard already added elsewhere this cycle). All now raise a clear ValueError instead of crashing on a bare arithmetic/ indexing TypeError or AttributeError. - UVertex.connect()/DVertex.connect(): the documented "pass an existing Edge as `other`" code path was completely broken -- `v.connect(some_edge)` crashed unconditionally with "missing 1 required positional argument: 'dest'", confirmed on both UVertex and DVertex, and exercised by no test. Deleted rather than fixed -- the feature raises real ambiguity (does the edge already have both vertices, one, or none?) that a mechanical fix can't resolve without a design decision. This also happened to be one leg of the Liskov-incompatible connect() override already flagged as debt in the type-hints pass; with the ambiguous branch gone, the remaining mismatch (parameter name, and **kwargs vs named optional params) was cheap to close properly by matching BaseVertex.connect()'s signature exactly, resolving that debt item entirely rather than leaving it deferred. - Dict()/Adjacency(): return type corrected from the unprovable `UGraph | DGraph` to the honestly-provable `_BaseGraph` (matches copy()'s existing pattern on the same class). - average_degree(): added a fallback `else: raise TypeError(...)` for mypy's benefit, since the two isinstance branches aren't statically exhaustive even though they are in practice. Also removed one genuinely dead line: UVertex.connect() called self._graph._edgelist.add(e) after super().connect(), which already does that same add() internally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #10 +/- ##
==========================================
- Coverage 85.20% 84.82% -0.38%
==========================================
Files 2 2
Lines 750 771 +21
==========================================
+ Hits 639 654 +15
- Misses 111 117 +6 ☔ 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.
Summary
Reduces mypy errors on
PGraph.pyfrom 60 to 6 (the remainder is inplot()/highlight_vertex's visualization internals -- dozens of individual coord/x/y/name accesses onOptionaltypes, left as debt since the per-site narrowing cost is high for comparatively low-value, non-core code).Most of the 54 fixed were narrowing-only (asserts/guards at points where an invariant already holds, e.g. an edge returned by
self.edges()always has non-Nonev1/v2), but several were real, previously-undiscovered bugs, each verified empirically:BaseVertex.labelhad no type annotation on itsself.label = Noneassignment, so mypy inferred its type as bareNoneeverywhere, notint | None-- the root cause of most of the_graphcolor-related noise. Same root cause forEdge.cost, whose type was inferred from the first branch of a multi-branch assignment (self.cost = costinsideif cost is not None:, wherecostis narrowed to plainfloat) rather than the union of all branches. One-line annotation fixes at the actual assignment sites.UGraph._graphcolor():self._ncomponents = lastlabel + 1crashes withTypeErrorif the graph is empty (lastlabelstaysNone, sincerange(self.n)never executes) -- reproduced concretely by adding then removing a vertex and calling.nc. Fixed to treat an empty graph as 0 components._BaseGraph.closest(),BaseVertex.heuristic_distance()/distance()/closest()/x/y/z: none of these guarded against a vertex with no coordinate (coord is None, a legitimate state for a non-embedded vertex) or, forclosest()/heuristic_distance()/distance(), a vertex not connected to a graph (closest()was missing the same graph-membership guard already added elsewhere this cycle). All now raise a clearValueErrorinstead of crashing on a bare arithmetic/indexingTypeErrororAttributeError.UVertex.connect()/DVertex.connect(): the documented "pass an existingEdgeasother" code path was completely broken --v.connect(some_edge)crashed unconditionally with"missing 1 required positional argument: 'dest'", confirmed on bothUVertexandDVertex, and exercised by no test. Deleted rather than fixed -- the feature raises real ambiguity (does the edge already have both vertices, one, or none?) that a mechanical fix can't resolve without a design decision. This also happened to be one leg of the Liskov-incompatibleconnect()override already flagged as debt in the type-hints pass; with the ambiguous branch gone, the remaining mismatch (parameter name, and**kwargsvs named optional params) was cheap to close properly by matchingBaseVertex.connect()'s signature exactly, resolving that debt item entirely rather than leaving it deferred.Dict()/Adjacency(): return type corrected from the unprovableUGraph | DGraphto the honestly-provable_BaseGraph(matchescopy()'s existing pattern on the same class).average_degree(): added a fallbackelse: raise TypeError(...)for mypy's benefit, since the two isinstance branches aren't statically exhaustive even though they are in practice.Also removed one genuinely dead line:
UVertex.connect()calledself._graph._edgelist.add(e)aftersuper().connect(), which already does that sameadd()internally.Test plan
runblockregressions.nc, coordinate/graph-membership guards, and the deletedconnect(edge)path's new failure mode