You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Design proposal, filed to iterate on rather than as a ready change. All figures measured against 27bb315d5.
Observation: the mechanism already exists and has zero users
Every one of the four ABCs already auto-registers its subclasses via __init_subclass__. Every built-in declines it by inheriting a parallel *Base class instead:
public class (auto-registers)
descendants
non-registering base
descendants
Protocol
0
ProtocolBase
43
Engine
0
EngineBase
9
Reassembly
0
ReassemblyBase
5
TraceFlow
0
TraceFlowBase
2
EnumSchema
402
—
—
So the auto-registering half of each pair is a 0-user API, while EnumSchema — same idea, different implementation — has 402 registered descendants.
Why this matters beyond tidiness: it has caused two real defects
#506's root cause.pcapkit/protocols/schema/schema.py:669 did isinstance(data, Protocol) where it meant ProtocolBase. Commit 680f84d4e ("revised Protocol metaclass") rewrote that import to ProtocolBase as Protocol in 20schema/** modules but missed this one, because it is a runtime import inside pack() rather than a module-level TYPE_CHECKING one. With Protocol having 0 descendants, the branch was unreachable for three years — IPv4.make(payload=<TCP instance>) raised ProtocolUnbound despite its own signature accepting Protocol.
#513.extraction.py:390/412/434 validate issubclass(x, Engine | Reassembly | TraceFlow), so all three public register_extractor_* functions reject pcapkit's own built-ins.
Two independent failures, one cause: a public class and a base class that differ only in whether they register, where the base is what everything actually uses.
# EnumSchema.__init_subclass__ pcapkit/protocols/schema/schema.py:1028-1033ifcodeisnotNone: # register ONLY when askedcls.__enum__[code] =cls
EnumSchemaguards. The four ABCs fall back. That inversion is the whole reason a parallel hierarchy is needed to decline registration — and the reason cls.name gets used as a registry key, which is itself surprising: class X(Reassembly, name='zzz') registers under 'x'-style lowercased class name, silently, because name= is swallowed by **kwargs (see the docstring fix for name vs protocol).
The pattern being proposed, as EnumMeta/EnumSchema implements it
Four parts, and the four ABCs already have three of them:
A metaclass over abc.ABCMeta.SchemaMeta(abc.ABCMeta) at schema.py:135, EnumMeta(SchemaMeta, Generic[_ET]) at :915. All four ABCs already have one — ProtocolMeta, EngineMeta, ReassemblyMeta, TraceFlowMeta, all ABCMeta subclasses.
A class-level registry property on the metaclass (schema.py:933-935), an immutable proxy to __enum__. This is the part that needs the metaclass — a property in the class body would be an instance property, so MySchema.registry would not work. None of the four ABCs has this.
Opt-in registration in __init_subclass__ — (cls, /, code=None, *args, **kwargs), register only when code is given. All four ABCs already take an optional keyword (schema=None, name=None, protocol=None, protocol=None); only the guard differs.
An explicit register() classmethod (schema.py:1038) coexisting with the automatic path. All four already have their register_* equivalents.
Plus a lazy per-root registry so each namespace owns its own mapping:
Invert the fallback into a guard: if protocol is None: super().__init_subclass__(); return. Built-ins then subclass the public class directly and simply omit the keyword — no *Base needed.
Add a registry property to each metaclass, mirroring EnumMeta.registry, so Reassembly.registry reads like MySchema.registry.
Keep *Base as an alias for backwards compatibility, but it stops being a mechanism.
What this buys: #513 disappears rather than needing its own patch; the EngineBase as Engine trick stops being load-bearing, so the next sweep like 680f84d4e's cannot half-miss it; and the four hierarchies come to match the one that already works at 402 subclasses.
The cost, stated plainly
This is a breaking change for external subclassers. Anyone who today writes class MyEngine(Engine) and relies on free registration under the class name would stop being registered unless they pass the keyword. In-tree the blast radius is zero — public-class descendants are 0 across all four — so only third-party code is affected. The 1.5.0 prerelease window is the right time to take it, with a changelog entry.
Open questions, which is why this is a design issue and not a PR
Why do the built-ins opt out today? The aliasing is clearly deliberate but the reasoning is documented nowhere. Plausibly: registering at class-definition time would populate the registry in import order, making duplicate keys and circular imports live concerns, and the built-ins are already wired in by explicit table entries. If that reasoning is sound it should be captured before it is discarded — and it may argue for keeping the explicit tables and only fixing register_extractor_engine/reassembly/traceflow reject pcapkit's own built-in classes #513.
Should cls.name remain a fallback key at all? Deriving a registry key from a class name is what makes the name=-swallowed bug silent instead of loud. Requiring the keyword makes it loud.
Does Protocol still need to exist once ProtocolBase registers on request? Its docstring describes it as the subclass for "external customised engines with auto registration", which the guard form makes unnecessary.
Ordering.EnumSchema registers into a per-root defaultdict owned by the schema tree; the four ABCs register into Extractor, a different module, via a deferred import inside __init_subclass__. Whether that indirection survives the change is worth settling before coding.
Design proposal, filed to iterate on rather than as a ready change. All figures measured against
27bb315d5.Observation: the mechanism already exists and has zero users
Every one of the four ABCs already auto-registers its subclasses via
__init_subclass__. Every built-in declines it by inheriting a parallel*Baseclass instead:ProtocolProtocolBaseEngineEngineBaseReassemblyReassemblyBaseTraceFlowTraceFlowBaseEnumSchemaSo the auto-registering half of each pair is a 0-user API, while
EnumSchema— same idea, different implementation — has 402 registered descendants.The opt-out is done by aliasing at import:
Why this matters beyond tidiness: it has caused two real defects
#506's root cause.
pcapkit/protocols/schema/schema.py:669didisinstance(data, Protocol)where it meantProtocolBase. Commit680f84d4e("revised Protocol metaclass") rewrote that import toProtocolBase as Protocolin 20schema/**modules but missed this one, because it is a runtime import insidepack()rather than a module-levelTYPE_CHECKINGone. WithProtocolhaving 0 descendants, the branch was unreachable for three years —IPv4.make(payload=<TCP instance>)raisedProtocolUnbounddespite its own signature acceptingProtocol.#513.
extraction.py:390/412/434validateissubclass(x, Engine | Reassembly | TraceFlow), so all three publicregister_extractor_*functions reject pcapkit's own built-ins.Two independent failures, one cause: a public class and a base class that differ only in whether they register, where the base is what everything actually uses.
The difference from
EnumSchemais two linesEnumSchemaguards. The four ABCs fall back. That inversion is the whole reason a parallel hierarchy is needed to decline registration — and the reasoncls.namegets used as a registry key, which is itself surprising:class X(Reassembly, name='zzz')registers under'x'-style lowercased class name, silently, becausename=is swallowed by**kwargs(see the docstring fix fornamevsprotocol).The pattern being proposed, as
EnumMeta/EnumSchemaimplements itFour parts, and the four ABCs already have three of them:
abc.ABCMeta.SchemaMeta(abc.ABCMeta)atschema.py:135,EnumMeta(SchemaMeta, Generic[_ET])at:915. All four ABCs already have one —ProtocolMeta,EngineMeta,ReassemblyMeta,TraceFlowMeta, allABCMetasubclasses.registryproperty on the metaclass (schema.py:933-935), an immutable proxy to__enum__. This is the part that needs the metaclass — apropertyin the class body would be an instance property, soMySchema.registrywould not work. None of the four ABCs has this.__init_subclass__—(cls, /, code=None, *args, **kwargs), register only whencodeis given. All four ABCs already take an optional keyword (schema=None,name=None,protocol=None,protocol=None); only the guard differs.register()classmethod (schema.py:1038) coexisting with the automatic path. All four already have theirregister_*equivalents.Plus a lazy per-root registry so each namespace owns its own mapping:
Proposed change, per class
if protocol is None: super().__init_subclass__(); return. Built-ins then subclass the public class directly and simply omit the keyword — no*Baseneeded.registryproperty to each metaclass, mirroringEnumMeta.registry, soReassembly.registryreads likeMySchema.registry.*Baseas an alias for backwards compatibility, but it stops being a mechanism.What this buys: #513 disappears rather than needing its own patch; the
EngineBase as Enginetrick stops being load-bearing, so the next sweep like680f84d4e's cannot half-miss it; and the four hierarchies come to match the one that already works at 402 subclasses.The cost, stated plainly
This is a breaking change for external subclassers. Anyone who today writes
class MyEngine(Engine)and relies on free registration under the class name would stop being registered unless they pass the keyword. In-tree the blast radius is zero — public-class descendants are 0 across all four — so only third-party code is affected. The1.5.0prerelease window is the right time to take it, with a changelog entry.Open questions, which is why this is a design issue and not a PR
cls.nameremain a fallback key at all? Deriving a registry key from a class name is what makes thename=-swallowed bug silent instead of loud. Requiring the keyword makes it loud.Protocolstill need to exist onceProtocolBaseregisters on request? Its docstring describes it as the subclass for "external customised engines with auto registration", which the guard form makes unnecessary.EnumSchemaregisters into a per-rootdefaultdictowned by the schema tree; the four ABCs register intoExtractor, a different module, via a deferred import inside__init_subclass__. Whether that indirection survives the change is worth settling before coding.