Module Descriptor

pcapkit.corekit.module contains tuple like class ModuleDescriptor, which is originally designed as tuple[str, str].

class pcapkit.corekit.module.ModuleDescriptor(module, name)[source]

Bases: ModuleDescriptor, Generic[_T]

Module descriptor contains module name and class name, the actual class can be imported by from module import name.

property klass: Type[_T]

Import class from module.

Raises:

pcapkit.utilities.exceptions.ProtocolError – If name is NULL – the caller building this descriptor omitted the class name rather than naming one that turned out wrong – or if module has no attribute named name.

Important

The module is read from sys.modules first, and importlib.import_module() is entered only when it is not loaded yet. That matters because this property is on a per-frame dispatch path: a next layer code nobody registered falls back to a ModuleDescriptor for Raw which ProtocolBase._lookup_next_layer deliberately does not write back, so every unrecognised frame resolves the same descriptor again – 48 of the 52 resolutions an extraction of many_interfaces.pcapng performs. import_module() keeps real per-call work for an already-imported module (locks, ModuleSpec checks, the fromlist walk), so each repeat cost ~436 ns where this property now costs ~117 ns.

The class is still read off the module with getattr() on every access, and nothing is memoised here. That is the point: sys.modules is the module cache, and it is the only one whose invalidation the interpreter maintains – importlib.reload() rebinds the class in place and sys.modules.pop() drops the entry, both of which this sees immediately. Holding the resolved class instead would serve the pre-reload class forever, and an instance of it fails isinstance() against the live one.

property module: str

Module name.

property name: str | pcapkit.corekit.module.NullType

Class name, or NULL when whatever built this descriptor never got one – see klass.

Auxiliaries

class pcapkit.corekit.module.NullType[source]

Bases: object

Type of NULL, the omitted-class_/module sentinel.

A distinct class rather than a plain str – which is what the registry helpers in pcapkit.foundation.registry.protocols and pcapkit.foundation.registry.foundation used to define, independently of each other – so that is comparisons against it mean what they say: no str a caller passes, including one that happens to spell '(null)' itself, can compare equal to this sentinel by identity. See GitHub issue #833.

Genuinely a singleton, not merely a class this module happens to instantiate once: __new__() always hands back the one instance that already exists, rather than building a new one, so no caller – direct, or copy/pickle reconstructing an instance behind the scenes – can end up holding a second object that fails an is NULL check downstream. ModuleDescriptor.klass() makes exactly that check, and a stricter guard that raises on a second call would be truer to “singleton” in the abstract, but it would also mean the module’s own NULL = NullType() below is the only call that is ever allowed to succeed – fragile for no real benefit, since nothing here needs rejecting a second construction, only preventing it from producing a distinct object.

That still leaves copy.deepcopy(), copy.copy() and pickle unhandled: none of them constructs a new instance by calling NullType() themselves, so the guard above never runs for them. Each is therefore given its own override below, rather than left to fall back to the default behaviour for a plain object:

  • copy.copy() and copy.deepcopy() check for __copy__()/__deepcopy__() before ever falling back to reduction, so __deepcopy__() in particular has to be defined – its absence is the actual defect this class used to have: deepcopying a ModuleDescriptor recursed into this sentinel, reduced it, and rebuilt a second, non-identical NullType that then read as an ordinary attribute name to getattr(), downgrading a clean ProtocolError into a bare TypeError (attribute name must be string, not 'NullType').

  • pickle protocols 2 and up reconstruct through cls.__new__(cls), which the guarded __new__() already keeps to one instance – but protocols 0 and 1 reconstruct through copyreg._reconstructor(), which calls object.__new__() directly, bypassing __new__() entirely. __reduce__() is defined so that every protocol, not only the ones that happen to go through this class’s own __new__(), is routed through the same module-level getter instead of through reconstruction at all.

A caveat rather than a defect: importlib.reload() on this module re-executes NULL = NullType() below, producing a second singleton that the reloaded code compares against correctly but that every module which already imported the pre-reload NULL still holds – so a comparison spanning the reload sees two “singletons” that are not each other. ModuleDescriptor.klass() faces exactly this class of problem for the class it resolves, which is why it re-reads sys.modules on every call rather than memoising; nothing equivalent is possible here, because unlike a resolved class there is no live registry this sentinel could be re-read from. The pre-#833 str sentinel had the same fragility for the same reason – it is a property of sharing one module-level binding across a reload, not something this class’s singleton guarantees claim to solve – and nothing in this package reloads pcapkit.corekit.module after import.

pcapkit.corekit.module.NULL = <NULL>

Sentinel for an omitted class_ argument to the register_* helpers in pcapkit.foundation.registry.protocols and pcapkit.foundation.registry.foundation. Defined once, here, rather than once per module: both already import ModuleDescriptor from this module, so it is the shared home that needs no new module and creates no import cycle.

Type:

NullType

Type Variables

pcapkit.corekit.module._T: Any