fix(foundation): declare the engine-typed slots as EngineBase, not Engine - #1023
Conversation
|
Cross-review verdict: NEEDS CHANGES (ran on Opus; the change was authored on Sonnet). One substantive finding, which I reproduced myself before acting on it. Bare
It is avoidable at zero cost. Keeping the parameter and retargeting the casts instead of deleting them measures at 305 errors in 33 files, 0 in _exeng: 'EngineBase[_P]'
self._exeng = cast('EngineBase[_P]', PCAP_Engine(self))That is strictly better than both this head and Everything else in the review reproduced, several claims more strongly than the description states them — notably that |
c194639 to
11cde1a
Compare
…gine Three declarations in `pcapkit/foundation/extraction.py` named a class the attribute never holds. Closes #1022. * `_exeng` (`:183`), `Extractor.engine` (`:354`) and `record_header` (`:738`) said `Engine`, and the latter two said it unparameterised. Both built-in engines subclass `EngineBase` directly -- `PCAP` is `EngineBase[Frame]` and `PCAPNG` is `EngineBase[PCAPNG]` -- so neither has ever been an `Engine` at run time. All three now say `EngineBase[_P]`. * The two `cast` calls at `:609`/`:612` are **retargeted**, not deleted. They were laundering the class *and* the frame type parameter; they now launder only the parameter, which is a narrower and honest claim. * Corrected `record_header`'s docstring, which named a class the attributes do not live on, had lost its verb, and said "also ... as well". Keeping the parameter is what makes this safe rather than merely more accurate. Declaring the slots bare `EngineBase` type-checks identically -- 305 errors either way -- but degrades `_exeng.read_frame()` from `Frame` to `Any`, and that expression is the whole body of `Extractor.__next__` and `__call__`, both declared `-> '_P'`. Bare would have silently dropped the only type-level guarantee that the two public iteration entry points return the frame type they advertise, on a `py.typed` package, with no error-count change to show it. `Engine` adds no instance API over `EngineBase` -- measured, its only added member is `__init_subclass__`, the `dir()` difference is empty in both directions, the metaclass object is the same, and no `__slots__`, `__getattr__` or `__dir__` override appears anywhere in the MRO. A sweep of every read of `_exeng`, `.engine` and `.record_header` across `pcapkit/`, `tests/`, `examples/` and `docs/` found none touching an `Engine`-only member. Breaking, in two ways a type checker sees. Code passing `Extractor.engine` to an `Engine`-typed parameter must now accept `EngineBase`; and since the old declarations were unparameterised, `engine.read_frame()` and `record_header().read_frame()` were `Any` and are now the extractor's frame type, so an assignment relying on `Any` there stops type-checking. New test pins both halves -- that each declaration names a class both built-in engines satisfy, and that it stays parameterised. Eight subtests fail on main, six for the class and two because `engine` and `record_header` are bare there; dropping `[_P]` from any one declaration fails that declaration's own subtest. tests/foundation + tests/interface + tests/project: 555 passed, 13 skipped, 1291 subtests passed. mypy on extraction.py: 305 errors, byte-identical to main's set, none in the file.
11cde1a to
da431d1
Compare
|
Merge this before #1020. They turn out to be complementary, and I have measured it rather than inferred it. #1020 implements the ruled design from #1016: the public Widening the store then collides with Measured on a scratch tree with this PR's commit cherry-picked onto Nothing changes here as a result; this is a note about ordering, not a request. Worth recording because #1020's own CI will carry that one error until this merges, and it should not be mistaken for a defect in #1020. |
Implements the maintainer's ruling on #1016: the public `register_*` door names the public class, and the base check moves to an internal path, so neither that ruling nor #513 has to concede. * `register_engine`, `register_reassembly` and `register_traceflow` now guard `issubclass(x, Engine / Reassembly / TraceFlow)` instead of the `*Base` classes. Their messages are unchanged -- `main` already named the public class, which is what made the mismatch #1016 reported. * Three private classmethods take the base check: `Extractor._register_internal_engine` and its two siblings. They unwrap a `ModuleDescriptor`, check the base, keep the overwrite warning, and write the registry. None is in any `__all__`, none is autodocumented, and no `register_extractor_*` wrapper reaches them. * The three registry stores and the three metaclass `registry` properties widen from `Type[Engine]` to `Type[EngineBase]` and friends. The internal path writes base-only classes by design, so the narrow declaration was false by construction rather than by accident; `dict` is invariant in its value type, so the stores could not widen without the properties that return them. * The `#:` prose above each store said the values were "a tuple representing the module name and class name" and an `Engine` subclass. They are `ModuleDescriptor`s and base-only classes -- wrong on both counts before this change, not because of it. The built-ins are not affected by the narrowing and do not use the internal path at runtime: they are `ModuleDescriptor` literals in `Extractor`'s class body and never pass through a registrar. The internal path exists for programmatic internal registration, and is what #513's regression test now exercises. `register_protocol` and `register_dumper` are untouched. The former cannot narrow: `ProtocolBase` has 44 descendants including `Protocol` itself, while the public `Protocol` has none, so narrowing would reject every in-house protocol class. The latter already guards the public `Dumper`. Breaking: a third party subclassing `EngineBase` directly and calling `register_engine` is now rejected where it was accepted. #513's regression test is retargeted, not weakened. It keeps its real subject classes, its unmocked gate, its registry read-back and its anti-no-op comment, and now exercises the internal path -- with a comment citing #1016 so a reader arriving from #513 sees why the assertion moved, plus `mock.patch.dict` rollback it did not have before. What it guaranteed still holds: pcapkit's own base-only classes are registrable through a real gate. Two new tests pin the other half, that the public door rejects a base-only class both as a class and as a `ModuleDescriptor`. mypy, single-file entry point `mypy pcapkit/foundation/extraction.py`: 305 errors, 0 attributed to the file, sorted error set identical to main. The whole-package `mypy pcapkit` form reads 321 on both trees. The store widening draws a `_exeng` mismatch on its own, which #1023 resolved. tests/foundation + tests/interface + tests/project: 557 passed, 13 skipped, 1300 subtests passed.
…ne (#1024) `Extractor.engine` is declared `EngineBase[_P]` since #1023, but `EngineBase` was documented `:no-members:` under Internal Definitions while `Engine` carried every member stub, so the type a reader followed exposed no API. * `engine.rst`: the member stubs move to `EngineBase`, where they are defined; `Engine` documents only `__init_subclass__` and keeps the customisation `seealso`. `EngineBase` is not added to `__all__` -- that is left to #1016. * Cross-references to `Engine.run`, `read_frame`, `close` and `unsupported_reason` (`pep.rst`, `engines/index.rst`, four `_exeng` docstrings in `extraction.py`) now point at `EngineBase`, where they resolve. * `Engine.__init_subclass__`: qualify the bare `__engine_name__` reference, which stopped resolving once the attribute moved to `EngineBase`. * `PCAP.run`: repair the verbless "will also be save the current `PCAP` engine instance", as #1023 did in `extraction.py`. * Changelog entry added; the entry-count pin in `process.rst` is 157. Also corrected `docs/source/contributing/pep.rst`, which said subclassing an engine registers it automatically. Registration has been opt-in since #514: `__init_subclass__` registers only when the `engine` class keyword is given, and defaults to skipping. Measured -- a subclass without the keyword adds nothing to `Extractor.__engine__`; with it, the name appears. Nitpicky Sphinx build: 1239 unresolved references on fa3e861, 1237 here. The two `EngineBase.unsupported_reason` references in `docs/source/index.rst` newly resolve; no new miss. tests/project passes.
…cks (#1020) Implements the maintainer's ruling on #1016: the public `register_*` door names the public class, and the base check moves to an internal path, so neither that ruling nor #513 has to concede. * `register_engine`, `register_reassembly` and `register_traceflow` now guard `issubclass(x, Engine / Reassembly / TraceFlow)` instead of the `*Base` classes. Their messages are unchanged -- `main` already named the public class, which is what made the mismatch #1016 reported. * Three private classmethods take the base check: `Extractor._register_internal_engine` and its two siblings. They unwrap a `ModuleDescriptor`, check the base, keep the overwrite warning, and write the registry. None is in any `__all__`, none is autodocumented, and no `register_extractor_*` wrapper reaches them. * The three registry stores and the three metaclass `registry` properties widen from `Type[Engine]` to `Type[EngineBase]` and friends. The internal path writes base-only classes by design, so the narrow declaration was false by construction rather than by accident; `dict` is invariant in its value type, so the stores could not widen without the properties that return them. * The `#:` prose above each store said the values were "a tuple representing the module name and class name" and an `Engine` subclass. They are `ModuleDescriptor`s and base-only classes -- wrong on both counts before this change, not because of it. The built-ins are not affected by the narrowing and do not use the internal path at runtime: they are `ModuleDescriptor` literals in `Extractor`'s class body and never pass through a registrar. The internal path exists for programmatic internal registration, and is what #513's regression test now exercises. `register_protocol` and `register_dumper` are untouched. The former cannot narrow: `ProtocolBase` has 44 descendants including `Protocol` itself, while the public `Protocol` has none, so narrowing would reject every in-house protocol class. The latter already guards the public `Dumper`. Breaking: a third party subclassing `EngineBase` directly and calling `register_engine` is now rejected where it was accepted. #513's regression test is retargeted, not weakened. It keeps its real subject classes, its unmocked gate, its registry read-back and its anti-no-op comment, and now exercises the internal path -- with a comment citing #1016 so a reader arriving from #513 sees why the assertion moved, plus `mock.patch.dict` rollback it did not have before. What it guaranteed still holds: pcapkit's own base-only classes are registrable through a real gate. Two new tests pin the other half, that the public door rejects a base-only class both as a class and as a `ModuleDescriptor`. mypy, single-file entry point `mypy pcapkit/foundation/extraction.py`: 305 errors, 0 attributed to the file, sorted error set identical to main. The whole-package `mypy pcapkit` form reads 321 on both trees. The store widening draws a `_exeng` mismatch on its own, which #1023 resolved. tests/foundation + tests/interface + tests/project: 557 passed, 13 skipped, 1300 subtests passed.
…ne (#1024) (#1027) `Extractor.engine` is declared `EngineBase[_P]` since #1023, but `EngineBase` was documented `:no-members:` under Internal Definitions while `Engine` carried every member stub, so the type a reader followed exposed no API. * `engine.rst`: the member stubs move to `EngineBase`, where they are defined; `Engine` documents only `__init_subclass__` and keeps the customisation `seealso`. `EngineBase` is not added to `__all__` -- that is left to #1016. * Cross-references to `Engine.run`, `read_frame`, `close` and `unsupported_reason` (`pep.rst`, `engines/index.rst`, four `_exeng` docstrings in `extraction.py`) now point at `EngineBase`, where they resolve. * `Engine.__init_subclass__`: qualify the bare `__engine_name__` reference, which stopped resolving once the attribute moved to `EngineBase`. * `PCAP.run`: repair the verbless "will also be save the current `PCAP` engine instance", as #1023 did in `extraction.py`. * Changelog entry added; the entry-count pin in `process.rst` is 157. Also corrected `docs/source/contributing/pep.rst`, which said subclassing an engine registers it automatically. Registration has been opt-in since #514: `__init_subclass__` registers only when the `engine` class keyword is given, and defaults to skipping. Measured -- a subclass without the keyword adds nothing to `Extractor.__engine__`; with it, the name appears. Nitpicky Sphinx build: 1239 unresolved references on fa3e861, 1237 here. The two `EngineBase.unsupported_reason` references in `docs/source/index.rst` newly resolve; no new miss. tests/project passes.
Please follow the guide below
make pylint,make mypy,make isort)make testpasses, and a test case covers the changedocs/source/changelog/and regeneratedCHANGELOG.md, if the change is user-visibleWhat 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 #1022. Three declarations in
pcapkit/foundation/extraction.pynamed a class the attribute never holds:_exeng(:183),Extractor.engine(:354) andrecord_header(:738) all now sayEngineBase[_P]. The twocastcalls at:609/:612are retargeted, not deleted — they were laundering the class and the frame type parameter, and now launder only the parameter.Keeping the type parameter is the point, and it is why the first revision of this PR was wrong. Declaring the slots bare
EngineBasetype-checks identically — 305 errors either way, none in the file — but degrades_exeng.read_frame()fromFrametoAny. That expression is the whole body ofExtractor.__next__(:1121) and__call__(:1147), both declared-> '_P', so bare would silently drop the only type-level guarantee that the two public iteration entry points return the frame type they advertise, on apy.typedpackage, with no error-count change to show it. Measured withreveal_type:maine._exengEngine[Frame]EngineBase[Frame]e._exeng.read_frame()FrameFramee.engineEngine[Any]EngineBase[Frame]e.engine.read_frame()AnyFrameWhy it is safe.
Engineadds no instance API overEngineBase— its only added member is__init_subclass__, thedir()difference is empty in both directions, the metaclass object is literally the same soEngineMeta's properties are reachable identically, and there is no__slots__,__getattr__or__dir__override anywhere in the MRO. A sweep of every read of_exeng,.engineand.record_headeracrosspcapkit/,tests/,examples/anddocs/found none touching anEngine-only member.Breaking in two ways, hence the label and the 1.5.0 entry. Code passing
Extractor.engineto anEngine-typed parameter must now acceptEngineBase; and because the old declarations onengine/record_headerwere unparameterised,read_frame()through them wasAnyand is now the extractor's frame type, so an assignment relying onAnythere stops type-checking. Run-time behaviour is unchanged — the only non-annotation edits are twotyping.castcalls, which return their argument untouched.The new test pins both halves: that each declaration names a class both built-in engines satisfy, and that it stays parameterised. Eight subtests fail on
main— six for the class, two becauseengineandrecord_headerare bare there — and dropping[_P]from any one declaration fails that declaration's own subtest by name.tests/foundation+tests/interface+tests/project: 555 passed, 13 skipped, 1291 subtests passed. mypy onextraction.py: 305 errors, the sorted set byte-identical tomain's, none in the file.util/changelog_md.py --checkexits 0.Deliberately not done, though measured as viable. Casting at
record_header's two local assignments instead of keeping the two# type: ignore[return-value]at:759/:766also gives 305 errors and would retire three suppressions including the[assignment]one. Declined here because those two ignores already exist onmain, so retaining them keepsrecord_header's body byte-identical tomainand this PR to declarations only; the alternative edits the function body for a suppression-style preference. Worth revisiting separately.Out of scope and untouched: the registry
# type:comments at:242/:251/:259, the#:attribute docs at:227/:244/:253, and the wholeregister_*region — PR #1020 owns that. #1024 tracks the consequence thatEngineBaseis excluded from__all__and documented:no-members:, so this public return type currently has no documented API.