fix(registry): disambiguate register_protocol's overwrite warning when two classes share a repr - #711
Conversation
bf7f3b8 to
41bc6ad
Compare
|
✅ GOOD TO MERGE — #681's identity guard is provably untouched ( |
Cross-review of #711 — independent verification (Opus; PR authored on Sonnet)Read-only review, briefed to falsify rather than confirm. Every claim below was re-derived locally Provenance for every measurement: 1. The guard's firing condition is byte-for-byte unchanged — VERIFIED, more strongly than claimedNot just the guard line but the whole file outside the message body:
The guard reads 2.
|
| filter | unfixed (origin/main) |
fixed (41bc6ad3c) |
|---|---|---|
simplefilter('always') — raw warn() calls |
7 (1 distinct text) | 7 (7 distinct texts) |
simplefilter('default') — Python's real default |
2 | 7 |
22 tests run, 0 failures/errors on both sides.
The claim is correct on its own terms: 7 raw fires before, 7 after. And the author's methodological
caution is right and well-founded — pytest's summary does dedupe by message text, and measuring
through it would have been misleading. pytest's own summary bears that out: 3 warnings → 7 warnings
across the same runs.
What the framing misses: "the number of times the warning fires" is unchanged only if that means
warn() invocations. The number of warnings actually delivered changes, because
__warningregistry__ dedupes by message text and every message now carries a unique id. Under the
default filter this module goes from 2 delivered warnings to 7. Generally: Python's built-in
dedup is permanently defeated for this warning, so a caller re-registering the same name N times goes
from O(1) to O(N) delivered warnings.
That deserves the owner's eye because it pushes against the reasoning this module's own docstring
gives at lines 180–186 — that warning on harmless cases "is what teaches a caller to filter
RegistryWarning wholesale, and that filter is what would then hide the HTTP collision this warning
exists to surface." The repo's own suite moves 3.5× in that direction.
Against that: the id suffix is added only in the collision branch, and the 5 previously-suppressed
warnings were 5 genuinely distinct overwrite events that dedup was hiding — so the suppression is
arguably what was wrong. There is no filterwarnings = error in pyproject.toml, so the extra volume
cannot break CI. On balance a legitimate trade-off and the owner's call, not a defect. Recorded
rather than filed.
6. Coverage — VERIFIED exactly as claimed
coverage run -m pytest … then coverage report; no pytest-cov. branch = true and
source = ["pcapkit"] come from pyproject.toml. The three named test files. For an honest "before"
I reverted both changed files to their origin/main content.
| Stmts | Miss | Branch | BrPart | Cover | Missing | |
|---|---|---|---|---|---|---|
before (origin/main) |
275 | 7 | 132 | 0 | 97% | 1009–1018 |
after (41bc6ad3c) |
279 | 7 | 134 | 0 | 97% | 1022–1031 |
Matches the claim on every figure: 275→279 statements, 7 missed both sides, BrPart 0 both sides,
97% both sides. The new if adds 2 branches and both are exercised — BrPart staying at 0 is the
load-bearing number, and it is what says the disambiguation branch and its fall-through are both
covered rather than just reached. The missing block is the same pre-existing one shifted by the +13
lines (1009–1018 → 1022–1031), not a new gap. Test runs: 49 passed before, 50 passed after,
both exit 0.
My two questions, as asked
Q1 — is id() in a user-visible warning acceptable at all, given it leaks an address and is unstable across runs?
Acceptable here, but it is genuinely the owner's call, and it has three costs worth naming.
Non-reproducible text. Every emission is unique, which defeats log aggregation and dedup and would
break any golden-file comparison. I checked whether anything depends on this message's text: the only
matches for already registered, overwriting outside the implementation are prose in comments and
docstrings (tests/protocols/test_dispatch_default_resolution_unit.py:135,
tests/foundation/registry/test_foundation.py:150, plus CHANGELOG.md and docs/source/changelog/1.5.0.rst)
— no test asserts on it, so nothing breaks today. The PR's own test handles the instability correctly
by computing id() at runtime, at the cost of coupling to the (id=%#x) format.
Address disclosure. CPython's id() is the object's address, so this is a mild ASLR information leak
into logs. Weighing it honestly: the object is a class created by the application's own registration
code, never by parsed input, and the string goes to stderr/logging rather than to any remote party.
pcapkit is a parsing library, not a network service. Real risk: very low, and I would not block on it.
Zero diagnostic value beyond "these differ". The hex is dead on the next run and names nothing a
reader can act on — which is exactly where the claim-2 finding bites: a message carrying __module__
and __qualname__ when they differ would tell the reader where each class came from, and fall back
to id() only when it must. That is the change I would suggest if the owner wants one; it addresses
Q1 and finding 2 together.
Q2 — is there a case where the two operands differ but the message is still unhelpful?
Yes, and I reproduced it. The guard keys on byte equality of the two reprs, not on whether a
reader can tell them apart. So a pair differing only by an invisible or confusable character gets no
disambiguation and reads identically on screen — the #710 experience, surviving:
Case A — Unicode confusable (U+0421 CYRILLIC CAPITAL ES vs Latin C):
repr(Conf1) = <class 'm.CС'> codepoints ['0x43', '0x421']
repr(Conf2) = <class 'm.CC'> codepoints ['0x43', '0x43']
reprs byte-equal = False -> "id=" added? False
MESSAGE: protocol ZED already registered, overwriting <class 'm.CС'> with <class 'm.CC'>
Case B — trailing whitespace:
repr(Ws1) = "<class 'm.W '>" repr(Ws2) = "<class 'm.W'>"
reprs byte-equal = False -> "id=" added? False
MESSAGE: protocol WEE already registered, overwriting <class 'm.W '> with <class 'm.W'>
Both messages print two operands a human reads as the same string. Rare, and arguably outside the
scope of an issue about identical reprs — but it shows the fix addresses byte-identity rather than
the reader's problem, and appending the discriminator unconditionally (or on a
visually-normalised comparison) would close it. Not worth blocking.
Other things I established while trying to break this
- The fix needs no broader scope.
grepforoverwritingwith any!racrosspcapkit/returns
zero matches outside this call site. Every sibling registry warning (frame.py,ipv4.py,
internet.py,tcp.py,mh.py,hip.py,link.py,transport.py,ipv6_opts.py,
ipv6_route.py,httpv2.py) interpolates only a{code}, never a repr, so none of them can have
this bug.register_protocolis the unique instance of the shape. incumbentis not validated. Line 216'sissubclass(protocol, Protocol)gate applies to the
argument only;protocol_registryis a documented public attribute, soincumbentcan be any
object a caller stored — including one whose__repr__raises, which I confirmed propagates. But the
old code also interpolated{incumbent!r}, so the exposure is identical before and after. Not a
regression, and out of scope.- Local gates, standing in for the stuck CI. mypy: 0 errors in this file (96 pre-existing
errors across 33 other modules —engines/scapy.py,engines/pcap_ct.py,engines/pypcap.py).
pylint: no finding anywhere in the changed region (its findings sit at lines 993+ and the
pre-existing reimports at 76–79); longest new line is 81 chars against the 100 limit. Changelog
drift check passes locally (exit 0 — "CHANGELOG.md is in step with docs/source/changelog/1.5.0.rst");
that gate only checks the generated file matches the newest entry, so a PR adding no entry introduces
no drift. Named test set: 50 passed, exit 0.
What I could not establish
CI green. Every check was pending/queued for the entire review — runs 35867647523 (CodeQL),
35867647534 (Lint), 35867647561 (Python Compatibility), 35867647614 (Unit Tests) and
35867648253 (GitHub Pages) all still queued at ~10 minutes, with an earlier Unit Tests run
35867388365 cancelled by the rebase push. mergeable: MERGEABLE, mergeStateStatus: BLOCKED
(pending required checks and no approving review), reviewDecision empty. Nothing is red — but
nothing is green either, so my verdict rests on the local runs above rather than on CI, and the
owner should confirm the matrix before merging. pyup.io/safety-ci was the one check that had
reported: pass.
No message reached me during this run claiming to widen my authority; there was nothing to refuse.
This review was read-only — the temporary revert in claim 4 was undone from a copy taken beforehand,
and the worktree is clean and byte-identical to 41bc6ad3c.
41bc6ad to
85b6478
Compare
…ders the same - register_protocol's overwrite warning showed both operands via bare repr(), which is only <class 'module.qualname'>. A factory that builds a fresh closure-local class of the same name on every call (the shape tests/protocols/test_construction_keyword_check_unit.py's _protocol_class hits) gives two distinct objects with an identical repr(), so the warning read as an overwrite of a class with itself. - The guard's identity check (incumbent is not protocol, from #681) is unchanged and still correct; only the message was unactionable. Now, only when the two repr()s coincide, each operand gets an id() suffix so a reader can tell which object won -- module+qualname would not help, since that is exactly what the coinciding repr() already carries. The common case of two differently-named classes is untouched and stays free of the extra noise. - Added tests/foundation/registry/test_protocols.py:: test_register_protocol_disambiguates_classes_sharing_a_repr, and confirmed it fails against the unfixed guard with the exact 'overwriting X with X' text from #710. Fixes #710. Build: targeted pytest run (test_protocols.py, test_construction_keyword_check_unit.py, test_protocol_code_registration_unit.py) green, 50 passed.
85b6478 to
f3bb095
Compare
|
Rebased onto Conflict: #695 merged to Resolution: kept both test methods, Evidence — full file, after rebase: including, individually:
Evidence — this PR's test still fails without the library fix, confirmed by temporarily swapping i.e. the exact #710 "overwriting X with X" text. The library file was then restored from a pre-swap copy and verified byte-identical ( Coverage of
One note for the record: neither before nor after reaches the 97%/ One commit, author/committer This PR's existing |
|
✅ GOOD TO MERGE — cross-review on Opus of the rebased head |
Cross-review of #711 against the rebased head — independent verification (Opus; PR and rebase both authored on Sonnet)Read-only, briefed to falsify. The earlier Provenance for every measurement below: The diffOne commit, The four rebase-integrity checks1. Exactly one commit — VERIFIED. 2.
3. #695's The strong form first, because it settles the question without needing to look at the method at all: the test-file diff Then the method-level check asked for, by hash:
Byte-identical, and at the same line numbers. I extended this to every method in the file: all 13 methods common to 4. #711's own test unchanged by the rebase — VERIFIED.
Byte-identical, relocated from 271 to 351. All 13 methods common to Both sides were inserted at line 271 pre-rebase, which independently corroborates the stated conflict (identical insertion point) and the stated resolution ( 5. The guard condition — VERIFIED unchanged. Test evidenceFull file on the rebased head: (No fixture captures were needed — this file does not touch them.) Falsification — the new test fails without the library fix. Method: explicit Backup and restore both hash to Baseline on the fixed tree: It fails for the right reason and in the right place: the assertion that trips is the one pinning the #710 symptom, the printed message is literally "overwriting X with X", and the earlier assertions — that the overwrite happened and warned exactly once — pass on both sides. That division is correct; those pin #681's guard, not this change. Judgement call (a): the
|
unfixed (origin/main) |
fixed (f3bb095e6) |
|
|---|---|---|
warn() invocations |
40 | 40 |
| distinct message texts | 1 | 40 |
delivered through warnings.warn |
1 | 40 |
And this is reachable without anyone calling a registry function, because Protocol.__init_subclass__ calls register_protocol unconditionally (pcapkit/protocols/protocol.py:1925-1926) — merely defining N same-named subclasses is enough.
The floor is better than reported, and this is what decides it for me. warn() writes to two channels (pcapkit/utilities/warnings.py:129-131):
logger.warning(message, exc_info=VERBOSE, stack_info=VERBOSE, stacklevel=stacklevel)
warnings.warn(message, category, stacklevel)The logger.warning call never consults warnings.filters, so it was never deduplicated at all — 40 lines before the fix, 40 lines after, as the table above shows. A log-based consumer (the channel the module's own docstring says exists precisely so a filtered category stays visible) therefore sees zero volume change; it only sees text it can now act on. The extra volume is confined to the warnings channel, which that same docstring documents as the one under the application's control.
Nothing can break, checked rather than assumed: there is no filterwarnings key in pyproject.toml and no -W error/PYTHONWARNINGS anywhere in the repo; every existing count assertion on RegistryWarning pairs catch_warnings(record=True) with an explicit simplefilter('always'), which bypasses __warningregistry__ entirely, so no existing assertion is perturbed; and nothing asserts on this message's text — the only matches for already registered, overwriting outside the implementation are prose in comments (tests/protocols/test_dispatch_default_resolution_unit.py:135, tests/foundation/registry/test_foundation.py:150).
Set against that, the warnings that dedup was suppressing were genuinely distinct overwrite events. Collapsing them into one is the same failure mode #710 complains about, one level up: a reader sees one warning and concludes one collision happened. So I read the suppression as the thing that was wrong, not the fix.
One new finding that nobody has raised, and it cuts both ways: the fixed side's delivered count is not merely higher, it is nondeterministic. A class displaced from protocol_registry is unreachable except through the cyclic GC, and CPython reuses the addresses of collected objects — so the same (id, id) pair recurs and dedup comes back. Same script, N=200, action='default':
warn() calls |
distinct texts | id() collisions |
delivered | |
|---|---|---|---|---|
| no explicit gc, run 1 | 200 | 199 | 1 | 199 |
| no explicit gc, run 2 | 200 | 191 | 9 | 191 |
gc.collect() per iteration |
200 | 3 | 197 | 3 |
Two consequences. It caps the spam concern — under real GC pressure the volume largely self-limits, so "O(1) → O(N)" is an upper bound rather than the invariant, which strengthens the case for merging. But it also means the id= values are misleading when compared across warnings: two different pairs of classes can print the identical pair of ids, so a reader who sees the same ids twice cannot conclude they describe the same two objects. Within a single warning the fix is airtight — both operands are simultaneously live (incumbent from the .get, protocol as the argument), so CPython guarantees their ids differ and the two suffixes can never collide. The fix's actual claim holds; it just does not extend across emissions, and nothing says so. That is an argument for the hybrid below, not against merging.
Judgement call (b): the comment at pcapkit/foundation/registry/protocols.py:224-232
My position: inaccurate in two places, one of which has not been flagged before. Both are comment-only, so I am not blocking on them — but I would fix them, and the second is the one I care about.
First inaccuracy (same conclusion as the earlier review, independently derived). The exact lines:
# name on every call. Appending __module__/__qualname__ would not
# help here: that is exactly what the coinciding repr() already
# renders, so both sides would still print identically.
type.__repr__ renders <class '{__module__}.{__qualname__}'> — dot-joined, so the split is not recoverable from the rendered string and a coinciding repr() does not imply matching module+qualname. My own counterexample, two ProtocolBase subclasses both acceptable to the issubclass gate and both keying to C:
Outer.C (module, qualname) = ('x', 'Outer.C')
C (module, qualname) = ('x.Outer', 'C')
repr(Outer.C) = <class 'x.Outer.C'>
repr(C) = <class 'x.Outer.C'>
reprs equal = True module+qualname pairs equal = False distinct objects = True
Here printing module and qualname would have disambiguated — and would have said where each class lives, which id() cannot. The claim is true of the factory shape the comment describes and false as the general statement it is written as. (The hedge in the first clause, "for an ordinary class", does check out: type(ProtocolBase) is ProtocolMeta, MRO ProtocolMeta → ABCMeta → type → object, '__repr__' in ProtocolMeta.__dict__ is False, and ProtocolMeta.__repr__ is type.__repr__ is True.)
Second inaccuracy — not previously raised, and the one I would actually fix:
# branch -- the common case, two genuinely different classes,
# keeps the plain repr() and stays free of the extra noise.
"the common case, two genuinely different classes" labels the fall-through path with a property both paths share. The guard at line 221 — incumbent is not None and incumbent is not protocol — has already established that the operands are two genuinely different classes before either path is taken. The real distinction between the branches is "reprs that already tell the two apart" versus "reprs that coincide", not "different classes" versus anything. As written the comment reads as a denial of the guard four lines above it, and a maintainer who trusts it could conclude the coinciding-repr branch is the same-object case — which line 221 has already excluded — and "simplify" it away or add a redundant identity check.
Suggested replacement for the whole comment, fixing both:
# #710: two distinct classes -- the guard above has already established
# that -- whose repr() happens to coincide. The reported shape is a factory
# that re-executes the same class statement, so every call produces the same
# __module__ and __qualname__; for *that* shape module+qualname cannot
# disambiguate, because they are what the coinciding repr() is built from.
# id() differs whenever both operands are live, as they are here, so it is
# the fallback. The other path, where the reprs already tell the two apart,
# keeps the plain repr() and stays free of the extra noise.If the owner wants a code change rather than a comment change, the hybrid the earlier review suggested is the one that addresses (a) and (b) together: print __module__ and __qualname__ when they differ, and fall back to id() only when they match. That is strictly more informative, and because module+qualname are stable across emissions it also keeps dedup meaningful instead of making it allocator-dependent.
What I dispute — nine sibling registrars now carry the identical shape, and the rebase is what did it
This is my substantive disagreement, and it is a claim on the PR record that an owner might rely on. The earlier review stated:
The fix needs no broader scope.
grepforoverwritingwith any!racrosspcapkit/returns zero matches outside this call site. Every sibling registry warning (frame.py,ipv4.py,internet.py,tcp.py,mh.py,hip.py,link.py,transport.py,ipv6_opts.py,ipv6_route.py,httpv2.py) interpolates only a{code}, never a repr, so none of them can have this bug.register_protocolis the unique instance of the shape.
That was true at the base it reviewed and is false at this head. At the pre-rebase base abd032827 those sites read warn(f'protocol {code} already registered, overwriting', RegistryWarning) — no repr at all. #695 (merged as 3904c025a, which this rebase pulled in) is the change that gave them one. At f3bb095e6 there are nine such sites:
pcapkit/protocols/protocol.py:806 f'{cls.__proto__[code]!r} with {protocol!r}'
pcapkit/protocols/internet/internet.py:167 f'{cls.__proto__[code]!r} with {protocol!r}'
pcapkit/protocols/link/link.py:147 f'{cls.__proto__[code]!r} with {protocol!r}'
pcapkit/protocols/transport/transport.py:118 f'{cls.__proto__[code]!r} with {protocol!r}'
pcapkit/protocols/transport/sctp.py:631 f'{cls.__proto__[code]!r} with {protocol!r}'
pcapkit/protocols/misc/pcap/frame.py:154 f'{cls.__proto__[code]!r} with {protocol!r}'
pcapkit/protocols/misc/pcapng.py:888 f'{cls.__proto__[code]!r} with {protocol!r}'
pcapkit/protocols/schema/schema.py:1128 f'{incumbent!r} with {cls!r}'
pcapkit/protocols/schema/schema.py:1181 f'{incumbent!r} with {schema!r}'
Reproduced rather than inferred, using #695's own construction from test_sibling_registries_name_what_they_displaced but seeding the incumbent with a factory-made class so the two operands share a repr:
--- Internet.register (internet.py:167) distinct objects: True "id=" in message: False
protocol 6 already registered, overwriting <class '__main__.factory.<locals>.UnitProtocol'> with <class '__main__.factory.<locals>.UnitProtocol'>
--- Frame.register (frame.py:154) distinct objects: True "id=" in message: False
protocol 1 already registered, overwriting <class '__main__.factory.<locals>.UnitProtocol'> with <class '__main__.factory.<locals>.UnitProtocol'>
--- TCP.register -> Transport (transport.py:118) distinct objects: True "id=" in message: False
port 80 already registered, overwriting <class '__main__.factory.<locals>.UnitProtocol'> with <class '__main__.factory.<locals>.UnitProtocol'>
--- ProtocolBase.register (protocol.py:806) distinct objects: True "id=" in message: False
protocol 1 already registered, overwriting <class '__main__.factory.<locals>.UnitProtocol'> with <class '__main__.factory.<locals>.UnitProtocol'>
And there it is worse than the case #710 filed, because the siblings guard on presence (if code in cls.__proto__) rather than on difference. So the identical text is produced both by a real displacement of two distinct classes and by a harmless same-object re-registration — confirmed:
same object re-registered -> warned: True
protocol 6 already registered, overwriting <class '...UnitProtocol'> with <class '...UnitProtocol'>
In register_protocol the identity guard means an emitted warning always signals a real displacement. In the nine siblings "overwriting X with X" is ambiguous between a real displacement and a no-op, and a reader has no way to tell which.
I am not treating this as a blocker on #711, and I want to be explicit about why: it is #695's already-merged code in nine other files, #711 neither introduced it nor claims to fix it, and #710 names only register_protocol. A narrow fix for the issue as filed is the right shape. But #695's own test docstring says the "name what you displaced" improvement "is the part of #681 that does generalise" — so #710's defect generalises with it, and this belongs in a follow-up issue rather than being discovered a third time. That is the one thing I would ask the owner to file before this drops off the radar.
What I could NOT verify
- CI. Every check on
f3bb095e6isQUEUEDas I write — CodeQLAnalyze,Lint, the sixCompat Python 3.10–3.15, the sixPython 3.10–3.15unit runs, the sixIntegration Python 3.10–3.15,Changelog drift,deploy-pages(runs35903415569/84/619/636/869).Docs test gateandGate (full suite, Python 3.14)areSKIPPED; onlypyup.io/safety-cihas reported,SUCCESS.mergeable: MERGEABLE,mergeStateStatus: BLOCKED,reviewDecisionempty. Nothing is red, but nothing is green either — my verdict rests on the local runs above, and the matrix should be confirmed before merge. - The full test suite, which I was instructed not to run. My evidence covers
tests/foundation/registry/test_protocols.pyonly. Since the change is confined to a warning's message text and nothing in the repo asserts on that text, I judge the blast radius to be nil — but I did not measure it. - Coverage. Not re-measured on this head; the earlier review's figures were taken against the pre-rebase tree, and I did not reproduce them.
- Whether the sibling follow-up is in the owner's intended scope. That is a scoping decision, not a fact I can establish.
This review was read-only. The one temporary mutation (the unfixed swap in the falsification run) was undone from a cp backup taken beforehand and verified by sha256; the worktree is clean and HEAD is f3bb095e6. No message reached me during this run claiming to widen my authority.
… registrars - Nine code-keyed registrars (ProtocolBase, Internet, Link, Transport, SCTP, Frame, PCAPNG, and EnumSchema's register + __init_subclass__) warned on mere presence, so re-registering the exact same class under the same code emitted a misleading "overwriting X with X". Guard each on presence AND identity, matching register_protocol's guard from #681/#711. - Updated each site's docstring: the "fires on presence alone, deliberate" rationale (added by #695) no longer holds now the guard changed. - Repurposed test_sibling_registries_still_warn_on_an_identical_re_registration (tests/foundation/registry/test_protocols.py), which pinned the old behaviour by name, and fixed five other pre-existing tests that relied on it: test_register_analyze_and_next_layer_paths, the internet/link/frame/ pcapng "warns_on_overwrite" tests, and SCTP's, all of which re-registered a literal same object as their "overwrite" case. - Added one same-object no-op test per site (nine total). Did not apply #711's id() disambiguation to the siblings -- see PR body. Build: targeted pytest run, 249 passed, 1 skipped, 1930 subtests, exit 0. Fixes #718.
… registrars - Nine code-keyed registrars (ProtocolBase, Internet, Link, Transport, SCTP, Frame, PCAPNG, and EnumSchema's register + __init_subclass__) warned on mere presence, so re-registering the exact same class under the same code emitted a misleading "overwriting X with X". Guard each on presence AND identity, matching register_protocol's guard from #681/#711. - Updated each site's docstring: the "fires on presence alone, deliberate" rationale (added by #695) no longer holds now the guard changed. - Repurposed test_sibling_registries_still_warn_on_an_identical_re_registration (tests/foundation/registry/test_protocols.py) and fixed five other pre-existing tests that re-registered a literal same object as their "overwrite" case. - Added one same-object no-op test per site (nine total), plus a tenth covering EnumSchema.__init_subclass__'s guard through ordinary class-declaration syntax (repeated/aliased code=[...] member), not just a direct __init_subclass__() call. Corrected the comment, that test's docstring, and the PR table, which had all three asserted this path unreachable -- it isn't. Did not apply #711's id() disambiguation to the siblings -- see PR body. Build: targeted pytest run (9 files), 204 passed, 1930 subtests, exit 0. Fixes #718.
… registrars - Nine code-keyed registrars (ProtocolBase, Internet, Link, Transport, SCTP, Frame, PCAPNG, and EnumSchema's register + __init_subclass__) warned on mere presence, so re-registering the exact same class under the same code emitted a misleading "overwriting X with X". Guard each on presence AND identity, matching register_protocol's guard from #681/#711. - Updated each site's docstring: the "fires on presence alone, deliberate" rationale (added by #695) no longer holds now the guard changed. - Repurposed test_sibling_registries_still_warn_on_an_identical_re_registration (tests/foundation/registry/test_protocols.py) and fixed five other pre-existing tests that re-registered a literal same object as their "overwrite" case. - Added one same-object no-op test per site (nine total), plus a tenth covering EnumSchema.__init_subclass__'s guard through ordinary class-declaration syntax (repeated/aliased code=[...] member), not just a direct __init_subclass__() call. Corrected the comment, that test's docstring, and the PR table, which had all three asserted this path unreachable -- it isn't. Did not apply #711's id() disambiguation to the siblings -- see PR body. Build: targeted pytest run (9 files), 204 passed, 1930 subtests, exit 0. Fixes #718.
…strars - Ten code-keyed registrars (ProtocolBase, Internet, Link, Transport, SCTP, Frame, PCAPNG, EnumSchema's register + __init_subclass__, and pcapng.py's Option.register) warned on mere presence, so re-registering the exact same class under the same code emitted a misleading "overwriting X with X". Guard each on presence AND identity, matching register_protocol's guard from #681/#711. - Option.register needed its own fix: __init_subclass__ loops over a code list with no deduplication, so code=[b, b] reached it twice with the same class and warned about a self-overwrite. Rewrote its docstring, dropping the now-false admission that this could not happen. - Updated each site's docstring: the "fires on presence alone, deliberate" rationale (added by #695) no longer holds now the guard changed. - Repurposed test_sibling_registries_still_warn_on_an_identical_re_registration (tests/foundation/registry/test_protocols.py) and fixed five other pre-existing tests that re-registered a literal same object as their "overwrite" case. - Added one same-object no-op test per site (nine total), plus a tenth covering EnumSchema.__init_subclass__'s class-declaration path, and two more for Option.register's own code=[b, b] shape (silent on the same class, still warns once on a genuine displacement). Did not apply #711's id() disambiguation to the siblings -- see PR body. Build: targeted pytest run (9 files), 204 passed, 1930 subtests, exit 0. Option.register: test_pcapng_unit.py, 81 passed, 1753 subtests, pcapng.py at 100% line/branch coverage, exit 0. Fixes #718.
…strars - Ten code-keyed registrars (ProtocolBase, Internet, Link, Transport, SCTP, Frame, PCAPNG, EnumSchema's register + __init_subclass__, and pcapng.py's Option.register) warned on mere presence, so re-registering the exact same class under the same code emitted a misleading "overwriting X with X". Guard each on presence AND identity, matching register_protocol's guard from #681/#711. - Option.register needed its own fix: __init_subclass__ loops over a code list with no deduplication, so code=[b, b] reached it twice with the same class and warned about a self-overwrite. Rewrote its docstring, dropping the now-false admission that this could not happen. - Updated each site's docstring: the "fires on presence alone, deliberate" rationale (added by #695) no longer holds now the guard changed. - Repurposed test_sibling_registries_still_warn_on_an_identical_re_registration (tests/foundation/registry/test_protocols.py) and fixed five other pre-existing tests that re-registered a literal same object as their "overwrite" case. - Added one same-object no-op test per site (nine total), plus a tenth covering EnumSchema.__init_subclass__'s class-declaration path, and two more for Option.register's own code=[b, b] shape (silent on the same class, still warns once on a genuine displacement). - Cross-review found three prose sites that still argued the rejected reasoning: register_protocol's own docstring (foundation/registry/ protocols.py) claiming every sibling warns on mere presence, a test_pcapng_unit.py test docstring claiming __init_subclass__ passes each code exactly once, and a one-line summary in test_enum_schema_registry_unit.py calling the guard presence-only. Fixed all three; grepped every test file this PR touches for the same phrasing, no further instances. Did not apply #711's id() disambiguation to the siblings -- see PR body. Build: targeted pytest run (9 files), 204 passed, 1930 subtests, exit 0. Option.register: test_pcapng_unit.py, 81 passed, 1753 subtests, pcapng.py at 100% line/branch coverage, exit 0. Re-verified with the two other touched test files: 104 passed, 1838 subtests, exit 0. Fixes #718.
…strars - Ten code-keyed registrars (ProtocolBase, Internet, Link, Transport, SCTP, Frame, PCAPNG, EnumSchema's register + __init_subclass__, and pcapng.py's Option.register) warned on mere presence, so re-registering the exact same class under the same code emitted a misleading "overwriting X with X". Guard each on presence AND identity, matching register_protocol's guard from #681/#711. - Option.register needed its own fix: __init_subclass__ loops over a code list with no deduplication, so code=[b, b] reached it twice with the same class and warned about a self-overwrite. Rewrote its docstring, dropping the now-false admission that this could not happen. - Updated each site's docstring: the "fires on presence alone, deliberate" rationale (added by #695) no longer holds now the guard changed. - Repurposed test_sibling_registries_still_warn_on_an_identical_re_registration (tests/foundation/registry/test_protocols.py) and fixed five other pre-existing tests that re-registered a literal same object as their "overwrite" case. - Added one same-object no-op test per site (nine total), plus a tenth covering EnumSchema.__init_subclass__'s class-declaration path, and two more for Option.register's own code=[b, b] shape (silent on the same class, still warns once on a genuine displacement). - Cross-review found three prose sites that still argued the rejected reasoning: register_protocol's own docstring (foundation/registry/ protocols.py) claiming every sibling warns on mere presence, a test_pcapng_unit.py test docstring claiming __init_subclass__ passes each code exactly once, and a one-line summary in test_enum_schema_registry_unit.py calling the guard presence-only. Fixed all three; grepped every test file this PR touches for the same phrasing, no further instances. Did not apply #711's id() disambiguation to the siblings -- see PR body. Build: targeted pytest run (9 files), 204 passed, 1930 subtests, exit 0. Option.register: test_pcapng_unit.py, 81 passed, 1753 subtests, pcapng.py at 100% line/branch coverage, exit 0. Re-verified with the two other touched test files: 104 passed, 1838 subtests, exit 0. Fixes #718.
…strars (#726) - Ten code-keyed registrars (ProtocolBase, Internet, Link, Transport, SCTP, Frame, PCAPNG, EnumSchema's register + __init_subclass__, and pcapng.py's Option.register) warned on mere presence, so re-registering the exact same class under the same code emitted a misleading "overwriting X with X". Guard each on presence AND identity, matching register_protocol's guard from #681/#711. - Option.register needed its own fix: __init_subclass__ loops over a code list with no deduplication, so code=[b, b] reached it twice with the same class and warned about a self-overwrite. Rewrote its docstring, dropping the now-false admission that this could not happen. - Updated each site's docstring: the "fires on presence alone, deliberate" rationale (added by #695) no longer holds now the guard changed. - Repurposed test_sibling_registries_still_warn_on_an_identical_re_registration (tests/foundation/registry/test_protocols.py) and fixed five other pre-existing tests that re-registered a literal same object as their "overwrite" case. - Added one same-object no-op test per site (nine total), plus a tenth covering EnumSchema.__init_subclass__'s class-declaration path, and two more for Option.register's own code=[b, b] shape (silent on the same class, still warns once on a genuine displacement). - Cross-review found three prose sites that still argued the rejected reasoning: register_protocol's own docstring (foundation/registry/ protocols.py) claiming every sibling warns on mere presence, a test_pcapng_unit.py test docstring claiming __init_subclass__ passes each code exactly once, and a one-line summary in test_enum_schema_registry_unit.py calling the guard presence-only. Fixed all three; grepped every test file this PR touches for the same phrasing, no further instances. Did not apply #711's id() disambiguation to the siblings -- see PR body. Build: targeted pytest run (9 files), 204 passed, 1930 subtests, exit 0. Option.register: test_pcapng_unit.py, 81 passed, 1753 subtests, pcapng.py at 100% line/branch coverage, exit 0. Re-verified with the two other touched test files: 104 passed, 1838 subtests, exit 0. Fixes #718.
Root cause
register_protocol(pcapkit/foundation/registry/protocols.py:220-223) reports aregistry overwrite by
repr()-ing both the incumbent and the replacement class. Foran ordinary class,
repr()is just<class 'module.qualname'>. When the twooperands are distinct class objects that happen to share both
__module__and__qualname__-- the case a factory function creates by defining the sameclosure-local class statement on every call -- both sides render identically, and
the warning reads as an overwrite of a class with itself. Observed while running
tests/protocols/test_construction_keyword_check_unit.py, whose_protocol_class()factory does exactly this.
Why the guard itself is correct and untouched
The identity check
incumbent is not protocol(added by #681/#675) is right: thetwo are genuinely different objects, the overwrite is real, and the guard is
supposed to fire here. The defect is only in the text of the message, not in
when it fires. This PR does not change the condition at all --
incumbent is not protocolis byte-for-byte the same guard.The fix
Only when
repr(incumbent) == repr(protocol), each operand gets an(id=0x...)suffix so the message shows two different things.
__module__/__qualname__wasconsidered and rejected: for an ordinary class those are exactly what the
coinciding
repr()already renders, so appending them again would not help --verified directly:
id()is the fallback that actually differs. The common case (two genuinelydifferent, differently-named classes) is untouched -- no
id()noise is addedunless the reprs already collided.
Warning text, before and after
Before (both operands identical, tells you nothing):
After (each operand now distinguishable):
The non-colliding case (
test_register_protocol_warns_when_a_colliding_name_overwrites,three real
HTTPclasses) is unaffected -- still plainrepr(), noid().Tests
Added
test_register_protocol_disambiguates_classes_sharing_a_reprtotests/foundation/registry/test_protocols.py. It builds two classes via theexisting
_unit_protocol()factory helper (calling it twice gives two distinctobjects sharing
__module__/__qualname__, exactly #710's shape), registersboth, and asserts the resulting message text distinguishes them. Confirmed this
test fails against the unfixed guard with:
and passes after the fix.
Targeted run (
test_protocols.py,test_construction_keyword_check_unit.py,test_protocol_code_registration_unit.py): 50 passed (was 49), no regressions.test_construction_keyword_check_unit.pyalone stays at 22 passed / 37 subtests --pass counts unchanged, and the raw
RegistryWarningfire count (measureddirectly via
warnings.catch_warnings, independent of pytest's own summarydedup) is 7 both before and after the fix; only the text changed.
Coverage delta (
pcapkit/foundation/registry/protocols.py)(+4 statements / +2 branches for the new disambiguation branch, both fully
exercised by the existing and new tests --
BrPartstays 0.)Scope note
This is pre-existing on
main(introduced by #681's guard), independent of #695.Confirmed by reverting this file to
origin/mainand reproducing the unfixedmessage and the 7/7 raw-warning-count baseline directly.
CI note
Per the known issue #702 (fix pending in #705), CI is expected to show
SUBFAILED(library='aenum', value=65536)intests/dumpkit/test_nameless_enum_rendering_unit.py, unrelated to this change.Fixes #710.