refactor(backend): accept any peer sequence in RelationshipManager.update - #10079
refactor(backend): accept any peer sequence in RelationshipManager.update#10079saltas888 wants to merge 6 commits into
Conversation
…r.update (INBOX-8)
…date The data parameter's collection member was an invariant list, so a caller holding a narrower peer list could not pass it without a type: ignore at the call site. Typing it as a covariant Sequence lets the suppression go and restores arg-type checking on that path. Widen the runtime narrowing in step with the annotation: a non-str Sequence is now treated as a collection, so a tuple of peers is iterated rather than mistaken for one opaque peer. A str stays on the single-value branch because a peer id must never be iterated character by character. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would auto-approve. Type parameter widened from list to Sequence, runtime narrowing adjusted, and type suppression removed, with added tests preserving existing behavior.
Re-trigger cubic
CI triage:
|
ajtmccarty
left a comment
There was a problem hiding this comment.
nice fix. out of scope for this PR, but PeerWithRelationshipMetadata is overused as both the write and read model for some relationship logic. so it is used to encode both responses from database queries AND updates for a Relationship. it should be split into 2 models: one for reading and one for writing
There was a problem hiding this comment.
not sure if we need all this spec stuff for ~10 lines of production code change
There was a problem hiding this comment.
Fair point, and worth flagging up the chain rather than me deciding it on this PR.
These artifacts are the output of the speckit-opsmill-auto pipeline that the automated platform-health agent is required to run end-to-end, and .agents/rules/code-doc-style.md names dev/specs/ as their home (it's where the rule says ticket/spec IDs are allowed to live, as opposed to in code comments). So they're landing where the repo says they should — but you're right that seven process files for ~10 lines of production code is out of proportion.
That's a pipeline-policy call, not a this-PR call, so I've left them in place and raised it on INBOX-8 for a human to decide whether the agent should scale the speckit ceremony down (or skip the spec artifacts entirely) for Effort-S cards. Happy to strip them from this PR if you'd rather not carry them — just say so and I'll push the removal.
Leaving this thread unresolved so it stays visible for that decision.
Per review: an internal type-annotation fix with no user-visible behaviour change does not warrant a changelog entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #10079 widened RelationshipManager.update()'s data parameter from an invariant list to a covariant Sequence to drop a call-site type: ignore. The reviewer's "definitely would have missed this one" was for the carve-out that makes it safe: str satisfies Sequence, so isinstance(data, str) must be checked before isinstance(data, Sequence), or a bare peer id gets iterated character-by-character. Nothing in the guidelines named this gotcha, so document it next to the neighboring isinstance-narrowing and list/Sequence-variance sections. The PR's other two review threads (skip-changelog for no-user-visible- effect changes, and proportional dev/specs/ ceremony) restate lessons already codified on this branch by c1b89c4 (INBOX-20) — that commit just hasn't reached develop yet, so PR #10079's branch never saw it. No doc gap, no edit needed there. The approving review's PeerWithRelationshipMetadata read/write-split suggestion is a real, confirmed observation (manager.py and node/create.py populate it asymmetrically) but is scoped to that one class, not a recurring authoring convention — discarded from this harvest as PR-local tech debt rather than encoded as a rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgpvpBsruPYD3fYpyfzL7R
CI triage (run at
|
| Job | Reported | Actual cause |
|---|---|---|
E2E-testing-pytest-playwright (foundation, branches_repo, sites_a) |
fail | ERROR at setup — docker compose --project-name infrahub-test-… up --wait returned non-zero → RuntimeError: Failed to start docker compose. 201 deselected, 5 errors — the environment never came up, so no test ever ran. |
backend-tests-component (graphql) |
fail | OOM. [gw0] node down: Not properly terminated followed by Process completed with exit code 137 (SIGKILL). The xdist worker was killed; test_diff_tree_query.py::test_diff_tree_one_attr_change was merely the test in flight and got reported as collateral. No assertion output exists. |
backend-tests-functional, backend-tests-component (core-diff), backend-benchmark |
fail | Not failures — cancelled. The GitHub API reports "conclusion": "cancelled" for all three; they were killed by fail-fast when the jobs above went red. gh pr checks flattens cancelled into fail, which is what makes this run look far worse than it is. |
Zero genuine test failures. Nothing here is reachable from a list → Sequence parameter annotation and an isinstance branch — a typing change cannot stop containers from starting or cause a runner OOM.
Corroborating evidence that the change itself is sound:
- The previous run on
98a1694had all of these same suites green — unit (3.12/3.13/3.14), component (core-diff,core-schema,graphql,other), functional, docker-integration — with only a separatebackend-tests-integrationflake, already triaged above. - CodSpeed on this PR: "Merging this PR will not alter performance", 12 untouched benchmarks.
- Locally on this branch: full-backend
mypyclean (1597 files),backend.test-unit2151 passed,test_relationship_manager.py36 passed,component/core/diff/merge27 passed,functional/ipamreconcile 4 passed.
On the branch being 4 commits behind develop
Noting it rather than acting on it. I deliberately did not rebase: the diagnosed causes are container startup and memory, neither of which staleness explains, and a force-push would risk dismissing @ajtmccarty's approval on a PR that is otherwise ready to go. mergeable is currently MERGEABLE, so there's no conflict. Happy to rebase if you'd rather have it fresh before merging — say the word.
🤖 platform-health-agent · automated reconcile pass
Why
RelationshipManager.update()'sdataparameter declared its collection member as an invariantlist[str | Node | dict | PeerWithRelationshipMetadata]. Becauselistis invariant in its elementtype, a caller holding a narrower
list[PeerWithRelationshipMetadata]could not pass it, whichforced a scoped suppression at the call site in
core/manager.py:That suppression was not cosmetic:
arg-typeis not ininfrahub.core.manager'sdisable_error_codelist, so the ignore was the only thing blinding mypy to that whole class oferror on a live code path. As the reviewer put it on #9977: "Since we are removing the warning, it's
always better to properly fix the warning."
Goal: type the parameter as a covariant, non-mutating
Sequenceso the suppression can go andarg-typechecking is restored there.Non-goals: the unrelated
# type: ignore[arg-type]on the**dictsplat incore/manager.py,and the one at
menu/repository.py:105(different root cause — a singleCoreMenuItem | None, not alist). Both deliberately untouched.
Tracking: INBOX-8 · originating review comment: #9977 (comment)
What changed
Behavioral changes — one, and it fixes a latent bug rather than altering existing behaviour:
listsequence (e.g. atuple) of peers is now correctly iterated as acollection. Previously such a value would have been wrapped as a single opaque "peer" and raised
ValidationError. No current caller passes a tuple, so nothing observable changes today.Implementation notes:
data's collection member:list[...]→Sequence[...](covariant, exposes no mutators).The runtime narrowing was widened in step with the annotation. This is the part worth
reviewing: widening only the annotation would make
tuplestatically valid whileisinstance(data, list)still routed it down the single-value branch — an annotation that acceptsan input the runtime mishandles is a worse contract than the invariant
listit replaced.strmust be excluded first:strsatisfiesSequencebut is a legitimate single-valuemember of the union (a bare peer id). Without the carve-out, a peer id would be shredded into one
peer per character — silently, since
_process_update_itemacceptsstritems.core/ipam/reconciler.py:168is a live caller that passes exactly that, so this is not hypothetical.
dictneeds no carve-out (aMappingis not aSequence), nor doNode,PeerWithRelationshipMetadata, orNone.Sequencemoved fromtypingtocollections.abc(it appeared in only two places in the file),since
typing.Sequenceis deprecated and this change uses it as anisinstancetarget. Theneighbouring
Iterable/Iterator/Mappingimports were left alone on purpose — migratingthem would be an unrelated drive-by refactor.
The
Sequenceannotation is only sound becauseupdate()genuinely never mutates the collection —verified against the body, which iterates
list_dataonce and never appends/assigns. (The nearbyself._relationships.clear()acts on the manager's own collection, not the caller's argument.)What stayed the same: no schema changes, no migrations, no GraphQL/REST contract changes, no auth
changes, no new dependencies, no CI changes, no generated files. Behaviour is unchanged for all seven
existing call sites.
How to review
Focus on one line — the widened narrowing in
backend/infrahub/core/relationship/model.py. Thestr-first ordering is what keeps peer ids from being iterated; everything else is mechanical.The behaviour matrix this must preserve:
datalist[...]strdict/Node/PeerWithRelationshipMetadata/NoneSequence)tuple[...]How to test
Evidence from this branch:
mypyon both changed modulesmypy --show-error-codes backend(full, as CI runs it)ruff check/ruff format --checkinvoke format,invoke main.lint,uv lock --checkinvoke backend.test-unittest_relationship_manager.pycomponent/core/diff/mergefunctional/ipamrebase+merge reconcilebackend.validate-generated,schema.validate-graphqlschema,schema.validate-jsonschema,docs.validateTwo checks confirm the change is meaningfully tested rather than vacuously passing:
itself:
"List" is invariant … Consider using "Sequence" instead, which is covariant.Sequenceannotation) makes the new tuple test failwith
ValidationError: Invalid data provided to form a relationship, proving the latent bug isreal and the test catches it.
Not run locally:
invoke backend.lint(thetystep) fails with 107unresolved-importdiagnostics, but it fails identically on a pristine
origin/developtree in this environment andnone of the diagnostics touch the changed files — pre-existing and environment-caused, left to CI.
Frontend checks and
docs.formatwere skipped (no frontend files changed;markdownlint-cli2notinstalled locally).
Impact & rollout
still type-checks and behaves identically. No migration steps.
isinstanceperupdate()call.Checklist
changelog/+relationship-update-sequence-param.housekeeping.md)dev/specs/005-manager-update-sequence-type/🤖 Generated with Claude Code