fix(reg,corekit): retype the registry NULL sentinel and raise ProtocolError from ModuleDescriptor.klass - #835
Conversation
|
NEEDS CHANGES — cross-review (opus) plus my own re-derivation. The substance is sound; two fixes in 1. The consequence, with a module that really imports ( On stock, deepcopy was no worse than the original. On this head it degrades to a bare stdlib 2. Also worth a line in the body, not a blocker: Confirmed unchanged: no import cycle (6 fresh interpreters, each importing a different module first, |
375b50c to
67b45ad
Compare
|
Fix pushed as Fail-before proof holds. On the prior head Only Back to |
|
NEEDS CHANGES on
Both names are now in Everything else confirmed on this head: Two non-blocking notes: the per-protocol assertions sit inside |
|
Reviewer's final verdict on It reclassified the One correction to my own earlier note, for the record: the reviewer's deepcopy finding was measured correctly. The probe it quoted in its first report was a discarded one that died on Two notes carried forward as non-blocking: the per-protocol assertions sit inside |
…lError from ModuleDescriptor.klass (#832) (#833) `NULL` was a plain `str` (`'(null)'`) compared by identity, defined independently in `protocols.py` (13 uses) and `foundation.py` (7 uses), so an equal-but-distinct `'(null)'` from a caller took a different branch than the sentinel itself depending on string interning. That let an omitted `class_` reach `ModuleDescriptor.klass`'s bare `getattr` and surface as `AttributeError: module 'X' has no attribute '(null)'`, and the same bare `AttributeError` leaked for any bad class name across all nine `register_*` call sites that build a descriptor. Add `NullType`/`NULL` to `pcapkit.corekit.module`, the module both registries already import `ModuleDescriptor` from, giving the sentinel a type no caller-supplied string can collide with, and retype every `class_` parameter accordingly. `ModuleDescriptor.klass` now raises `ProtocolError` for a class name that resolves to nothing, and, ahead of `getattr` entirely, for a `name` still `NULL` -- an omitted argument rather than a request for a class literally named `'(null)'`. `NullType` is now a genuine singleton rather than a class this module merely instantiated once: `__new__` always hands back the existing instance, and `__copy__`/`__deepcopy__`/`__reduce__` keep `copy.copy`, `copy.deepcopy` and every `pickle` protocol (0 through 5) on that same object too. Without this, deepcopying a `ModuleDescriptor` minted a second, non-identical `NullType` that reached `getattr` as a non-`str` name and downgraded the clean `ProtocolError` above into a bare `TypeError`. Also added `NULL`/`NullType` to `__all__`, fixed an over-indented continuation line, and extended `test_null_sentinel_is_not_a_string` to assert the singleton claim its docstring made rather than only describing it. Updated #815's `AttributeError` assertion in `test_protocols.py` to `ProtocolError`, keeping its `assertIn("'tcp'", ...)` check that a `str` third argument is resolved as a class name, never sniffed as a transport. Added coverage for the omitted-class-name, explicit-`'(null)'`, sentinel-still-means-absent, and singleton-identity cases; `coverage run` shows 100% on `module.py` and `foundation.py`, and no drop in `protocols.py`. Corrected `docs/source/pcapkit/corekit/module.rst`, a hand-written page `autodoc`/`nitpicky` never regenerates or gates: the `name` property's `:type:` still said `str`, and the page had no entry for `NullType`/`NULL` despite eight cross-references into it from `module.py`'s own docstrings. Added an "Auxiliaries" section documenting both, following the `NoValueType`/`NoValue` precedent in `fields/field.rst`. Verified by building the full site locally with `PYTHONPATH` pointed at this tree -- the venv's editable install otherwise shadows it with the unmodified main checkout -- 56 pre-existing warnings, none from this page or naming `NullType`/`NULL`. Also: the pickle-identity test now asserts outside `subTest` too, since this repo's `pytest-subtests` reports the parent test as passed when only a `subTest` failed inside it (reproduced directly to confirm); and `NullType`'s docstring now notes that `importlib.reload` desyncs the sentinel across modules that already imported it -- structural to sharing one module-level binding, true of the old `str` sentinel too, and unreached in-tree.
67b45ad to
37c2143
Compare
|
GOOD TO GO — amended to The doc page is fixed: Both non-blocking notes were taken rather than deferred. Fail-before re-proven by me on the pre-fix head The test rework was verified by me, not by the cross-reviewer, which saw only the previous head. |
|
Merged-together integration check — clean. CI only ever proved each PR alone: #834, #835 and #836 were all cut from Merged all three into a scratch worktree at That last one is not this change. Verified with a stock control — a clean So 356 tests pass on the combined tree with zero failures attributable to the merge, and the three may be merged in any order. |
…poses Closes #808. Blocked on #806 (merged as #815), which retyped every member's `proto` to a single transport, leaving nothing that builds or relies on a composite `TransportProtocol` value. - `TransportProtocol` becomes a plain `aenum.IntEnum`. The four transports keep their exact values (tcp=1, udp=2, sctp=4, dccp=8, undefined=0) via `cast(...)` rather than `auto()`, since IntEnum's `auto()` numbers sequentially and would renumber them. - Removed `_missing_`'s composing fallback/range guard (a plain IntEnum's default `_missing_` already rejects anything undeclared). - `.get()` refuses an unrecognised name outright rather than minting one -- maintainer ruling: "Do not allow extension of TransportProtocol at all." It used to mint at `max_val + 1` (an intermediate revision of this PR; stock still doubles, `max_val * 2`); there is nothing left to walk now. Two earlier rounds of this PR gave a `'|'`-containing string, e.g. `'tcp|udp'`, its own distinct message on the theory that it names a composite rather than merely an unknown name; the owner's final ruling drops that distinction outright rather than refining it -- "since it's no longer a Flag, `|` joined values are no longer parsed and accepted, we will treat it as a whole, instead of splitting" -- so `'|'` gets the identical generic refusal any other unrecognised name does. `.get()` still only case-folds, never strips whitespace, per the same ruling: the owner pointed at engine selection (`extraction.py:922`, lower-only, no `.strip()` anywhere in `pcapkit/foundation/`) as the convention to match, and normalising is the only part of that convention adopted -- engine selection warns and falls back to a default on a miss, `.get()` still raises. - `_dispatch` no longer decodes a bare-int `proto`'s bits at all, matching the same ruling: a composite built by hand, e.g. `TransportProtocol.tcp | TransportProtocol.udp` (a bare `int` since `|` falls through to `int.__or__` now), used to be split via `show_flag_values` into a `ProtocolError` naming every transport whose bit was set -- the GitHub issue #759 fix, present on stock and refined once more in an intermediate round of this PR to tell a clean composite (`3`, real bits only) apart from a stray bit (`17`, one real bit plus one nothing declares). The owner's ruling retires that decoding entirely rather than refining it further: a bare-int composite is now refused exactly like any other value naming no registry -- one plain `ValueError`, whether the int is `3`, `17`, or `TransportProtocol.undefined`. This removes the last use of `show_flag_values` and of `ProtocolError` from this module, so both imports are dropped along with the docstring `Raises:` entries naming `ProtocolError` on `_dispatch`/`get`/`get_all`. User-visible consequence: `AppType.get(80, proto=17)` and `AppType.get(80, proto=3)` were both `ProtocolError` on stock `ad4805f5f` and are both `ValueError` now. Neither is a regression on a *supported* input -- a bare `int` was off-contract until this PR widened `proto`'s annotation to include it -- but the exception type a caller now sees for that input has changed. - Widened `proto`'s type annotation to include `int` across `_dispatch`/`get`/`get_all`, and cast at the one dict-key site mypy cannot infer, to match the type mypy actually needs to stay clean. - Applied identically to the vendor generator template; verified the generated `TransportProtocol` class and `_dispatch`/`get` bodies are byte-identical between the two by rendering the template's `BASE` lambda directly against text extracted from the committed const file, rather than running the network-dependent vendor crawl. - `pcapkit/foundation/registry/protocols.py` (in scope once #835 merged into `ad4805f5f`): corrected `register_apptype`'s own NOTE, which justified resolving a string transport via `__members__` rather than `TransportProtocol[name]` with two claims this PR made false -- that `Flag.__getitem__` parses `'tcp|udp'` into the value `3`, and that `TransportProtocol` is an `IntFlag`. Neither holds once `|` is retired: `TransportProtocol['tcp|udp']` now raises a bare `KeyError`, same as `TransportProtocol['bogus']`, which is the corrected reason `__members__.get(...)` is still used -- this function's own contract is `RegistryError` on a miss, not `KeyError`. The `registries.get(1)` conclusion right after it is unchanged and stays: `hash(TransportProtocol.tcp) == hash(1)` regardless of the base, so an `int` key still hits a `TransportProtocol`-keyed dict entry. No behaviour changed here, only the comment explaining it. - `tests/dumpkit/test_nameless_enum_rendering_unit.py`'s flag-registry sweep drops from 7 to 6 registries (TransportProtocol no longer matches `issubclass(_, aenum.Flag)`) and from 4 to 3 distinct `_missing_` field widths; re-measured and re-pinned rather than assumed, prose updated to match. - Updated tests pinning removed Flag mechanics and two enum-sweep size pins (Flag count 7->6, IntEnum count 111->112). Converted every test whose premise the rulings above removed: the `.get('bogus')` minting probe and its misread-as-composite regression now assert refusal instead; the composite-string test lost its distinct-message assertions; the bare-int composite test (`test_a_bare_int_composite_is_refused_as_a_whole`, renamed from `test_a_proto_naming_two_transports_is_refused_rather_than_resolved`) now asserts the identical plain `ValueError` for `3` that a stray bit and `undefined` already got, through all three entry points; and `test_transport_protocol_can_no_longer_be_extended_at_runtime` pins the registration's removal rather than its shape. Corrected two rounds of stale narration a cross-review caught along the way: three comments/docstrings citing a `TransportProtocol.__getitem__` contrast that no longer has anything to contrast (there is no composite-specific branch left to justify), and two docstrings attributing the `max_val + 1` minting scheme to stock rather than to this PR's own now-superseded intermediate revision -- stock mints at `max_val * 2` (`16` for `'quic'`/`'bogus'`), measured on `ad4805f5f`. Also normalised three `3118ed796` "stock" references to `ad4805f5f` for consistency with the rebased base, since the claims hold at either commit. - Corrected an earlier claim: `list(TransportProtocol)` now yields all five members (4 on stock) since `Flag` hid the zero-valued `undefined` from iteration and plain `IntEnum` does not. Per-member repr/str/name/value are still byte-identical; nothing in-tree iterates the class bare, only through `__members__` (5 either way). `register_apptype` and its own tests needed no change beyond the NOTE above: they already reject anything that is not `isinstance(proto, TransportProtocol)`, which a bare int (what `|` now produces) satisfies identically, and its own no-strip case-fold resolution was already the model `.get()`'s normalisation follows. Built and tested against current `main` (`ad4805f5f`): `tests/const/` (77), `tests/foundation/registry/` (19), `tests/vendor/test_vendor_reg_apptype_generator_unit.py` (6) and `tests/dumpkit/test_nameless_enum_rendering_unit.py` (6) all pass via plain unittest, 108 total. mypy (114 errors/38 files) is identical before and after this change once line-number drift from the new `protocols.py` comment is accounted for -- zero new errors -- and isort is clean on all three touched source files.
Please follow the guide below
make pylint,make mypy,make isort)make testpasses, and a test case covers the change — did not run the full suite (it OOMs on this host); ran the scopedtests/foundation/registry/,tests/corekit/test_module.pyandtests/const/test_const_apptype_split_unit.pyinstead, and new test cases cover the changedocs/source/changelog/and regeneratedCHANGELOG.md, if the change is user-visible — N/A, changelog centralised in docs(changelog): shared 1.5.0 changelog — long-lived, merges last (#610, #616, #617, #618, #620) #657What is the purpose of your pull request?
fix— corrects a defectfeat— adds a featureperf— changes performance, not behaviourrefactor— changes neither behaviour nor performancetest— tests onlydocs— documentation onlyci— workflows or build toolingchore— anything elseDescription of your pull request and other information
Closes #832
Closes #833
NULLwas a plainstrcompared by identity, defined independently inprotocols.pyandfoundation.py; an equal-but-distinct'(null)'could take a different branch than the sentinel itself (#833), and let an omittedclass_reachModuleDescriptor.klass's baregetattr, surfacing as a stdlibAttributeErrornaming'(null)'— the same bare error any bad class name leaks across all nineregister_*sites (#832).Adds one
NullType/NULLinpcapkit.corekit.module, shared by both registry modules.ModuleDescriptor.klassnow raisesProtocolErrorfor a class name that resolves to nothing, and, ahead ofgetattr, for aclass_that was never given. Updates #815'sAttributeErrorassertion toProtocolError, keeping its class-name-not-transport check.