Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The largest release since 1.0, and the first recorded here as it happened rather
- **Changed** -- conflicting TCP overlaps resolve first-write-wins, per [RFC 9293](https://datatracker.ietf.org/doc/html/rfc9293) section 3.10, where they had silently resolved last-write-wins (#443, #478). A deliberate behaviour break, and a narrow one: a conforming retransmission carries identical bytes, so nothing changes for it. IP fragment reassembly keeps last-write-wins, because [RFC 791](https://datatracker.ietf.org/doc/html/rfc791) specifies the opposite resolution, and records the disagreement instead (#482).
- **Changed** -- `Probe`, `CipherSuite` and `IntegritySuite` are `Info` subclasses rather than `typing.NamedTuple`, and no `NamedTuple` remains in the package. They are Mappings now, so `len()` and iteration yield field names rather than values.
- **Changed** -- renames with no compatibility alias left behind: `HoleDiscriptor` is spelled `HoleDescriptor` and its package alias `TCP_HoleDiscriptor` is `TCP_HoleDescriptor` (#350); PCAP-NG `Option` subclasses spell the namespace class keyword `ns=` instead of `namespace=` (#439); and `examples/sample` and `examples/samples` -- one letter apart, holding different things -- are now `examples/captures` and `examples/generators`.
- **Changed** -- subclass registration is **opt-in** for `Engine`, `Reassembly`, `TraceFlow` and `dumpkit`'s `Dumper` (#514). Each registers if and only if its registry keyword is given -- `name=` for `Engine`, `protocol=` for `Reassembly` and `TraceFlow`, `fmt=` for `Dumper`. Previously an absent keyword fell back to the class' own name, so *every* subclass of the public class was registered, and declining meant subclassing the parallel `*Base` class under an alias -- which is what every built-in does, and why the public classes had **0** subclasses between them against the `*Base` classes' 9, 5, 2 and 3. **This breaks out-of-tree code that subclasses one of the four and relies on the derived key**; pass the keyword, or call the matching `register_*` function. Nothing the library ships is affected, and the `*Base` classes remain importable. Two things that were silent are now loud: an unrecognised class keyword raises `UnsupportedCall` instead of being swallowed by `**kwargs` -- which used to register the class under its own name, so passing `name=` to a `Reassembly` subclass silently ignored the key it was given, `protocol=` being the real one -- and `Dumper`'s `ext=` without `fmt=` likewise. A class attribute is not an opt-in: `__engine_name__` and `__protocol_name__` still set the name a class reports, registered or not. Each metaclass also gained a class-level `registry` property mirroring `EnumSchema.registry`. As a side effect a `Dumper` subclass no longer touches the filesystem while its `class` statement runs: inferring `fmt` from the `kind` property meant instantiating the class against a `NamedTemporaryFile` mid-definition. **One constraint specific to Python 3.10**, where this matters because the keyword is now the only class-definition path: `Engine`'s keyword is literally `name`, and `mcls`, `name`, `bases` and `namespace` collide with `abc.ABCMeta.__new__`'s own parameters, which are positional-or-keyword before 3.11 and positional-only from 3.11. So on 3.10 a class statement passing `name=` raises `TypeError` from the metaclass before the hook is reached, and an engine has to be registered with `Extractor.register_engine` instead -- which works on every version. `protocol=` and `fmt=` do not collide and are unaffected. Measured on 3.10.21, 3.11.15 and 3.14.7.
- **Changed** -- subclass registration is **opt-in** for `Engine`, `Reassembly`, `TraceFlow` and `dumpkit`'s `Dumper` (#514). Each registers if and only if its registry keyword is given -- `engine=` for `Engine`, `protocol=` for `Reassembly` and `TraceFlow`, `fmt=` for `Dumper`. Previously an absent keyword fell back to the class' own name, so *every* subclass of the public class was registered, and declining meant subclassing the parallel `*Base` class under an alias -- which is what every built-in does, and why the public classes had **0** subclasses between them against the `*Base` classes' 9, 5, 2 and 3. **This breaks out-of-tree code that subclasses one of the four and relies on the derived key**; pass the keyword, or call the matching `register_*` function. Nothing the library ships is affected, and the `*Base` classes remain importable. Two things that were silent are now loud: an unrecognised class keyword raises `UnsupportedCall` instead of being swallowed by `**kwargs` -- which used to register the class under its own name, so passing `name=` to a `Reassembly` subclass silently ignored the key it was given, `protocol=` being the real one -- and `Dumper`'s `ext=` without `fmt=` likewise. A class attribute is not an opt-in: `__engine_name__` and `__protocol_name__` still set the name a class reports, registered or not. Each metaclass also gained a class-level `registry` property mirroring `EnumSchema.registry`. As a side effect a `Dumper` subclass no longer touches the filesystem while its `class` statement runs: inferring `fmt` from the `kind` property meant instantiating the class against a `NamedTemporaryFile` mid-definition. `Engine`'s keyword is `engine=` rather than the `name=` this first shipped with, because `name` cannot be passed as a class keyword at all on Python 3.10: `mcls`, `name`, `bases` and `namespace` collide with `abc.ABCMeta.__new__`'s own parameters, which are positional-or-keyword before 3.11 and positional-only from 3.11, so a class statement naming any of the four raises `TypeError` from the metaclass before the hook is reached. Those four are the whole of the `ABCMeta.__new__` collision surface, measured on 3.10.21, 3.11.15 and 3.14.7; `engine=`, `protocol=` and `fmt=` are all outside it, so the documented registration path works on every supported version. There is no `name=` alias -- a keyword that worked on some interpreters and not others is the trap being removed, not a compatibility measure.
- **Changed** -- extraction is around 46% faster on a 1,117-frame HTTP capture, with byte-identical output (#420). A reassembled datagram's payload is now analysed on first read rather than eagerly, which cuts IP reassembly's own cost by 90.7% and TCP's by 23.7% -- IP reassembly submits a datagram for every frame, fragmented or not (#424). Flow tracing over the same capture went from 1416.6 ms to 744.0 ms, because the flow dumper had been handing each record to a `Frame` constructor that re-dissected the whole protocol stack to return bytes it had just been given; options are no longer parsed twice either (#427). All output compared byte-for-byte across the sample captures in each case.
- **Fixed** -- next-layer, option, chunk, block and parameter dispatch all read `defaultdict` registries, so a lookup miss inserted the key into class-level state shared by every later instance, after which a legitimate `register_*` call warned that the code was already registered. Every read now goes through a lookup that does not grow the table, and `IPv4.__option__` and `HIP.__parameter__` became inspectable class attributes rather than names assembled at call time (#426, #428, #429, #434). One break comes with it: a tuple-registered handler pair written to the documented `OptionParser`/`OptionConstructor` signature now works where it could previously never be called at all, and a pair written with an explicit leading `self` -- the only shape that used to work -- now does not.
- **Fixed** -- on Python 3.10 and older, no `Schema` subclass got its own `_abc_impl`: all of them fell through to `collections.abc.Mapping`'s, so a single `isinstance` or `issubclass` answer poisoned every later question about that class for the rest of the process. A terminating PCAP-NG `EndRecord` tested `True` as an `IPv4Record` (#439).
Expand Down
20 changes: 11 additions & 9 deletions docs/source/changelog/1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ pull requests between #326 and #509.
``examples/generators``.
* **Changed** -- subclass registration is **opt-in** for ``Engine``,
``Reassembly``, ``TraceFlow`` and ``dumpkit``'s ``Dumper`` (#514). Each
registers if and only if its registry keyword is given -- ``name=`` for
registers if and only if its registry keyword is given -- ``engine=`` for
``Engine``, ``protocol=`` for ``Reassembly`` and ``TraceFlow``, ``fmt=`` for
``Dumper``. Previously an absent keyword fell back to the class' own name, so
*every* subclass of the public class was registered, and declining meant
Expand All @@ -160,15 +160,17 @@ pull requests between #326 and #509.
effect a ``Dumper`` subclass no longer touches the filesystem while its
``class`` statement runs: inferring ``fmt`` from the ``kind`` property meant
instantiating the class against a ``NamedTemporaryFile`` mid-definition.
**One constraint specific to Python 3.10**, where this matters because the
keyword is now the only class-definition path: ``Engine``'s keyword is literally
``name``, and ``mcls``, ``name``, ``bases`` and ``namespace`` collide with
``Engine``'s keyword is ``engine=`` rather than the ``name=`` this first
shipped with, because ``name`` cannot be passed as a class keyword at all on
Python 3.10: ``mcls``, ``name``, ``bases`` and ``namespace`` collide with
``abc.ABCMeta.__new__``'s own parameters, which are positional-or-keyword
before 3.11 and positional-only from 3.11. So on 3.10 a class statement passing
``name=`` raises ``TypeError`` from the metaclass before the hook is reached,
and an engine has to be registered with ``Extractor.register_engine`` instead
-- which works on every version. ``protocol=`` and ``fmt=`` do not collide and
are unaffected. Measured on 3.10.21, 3.11.15 and 3.14.7.
before 3.11 and positional-only from 3.11, so a class statement naming any of
the four raises ``TypeError`` from the metaclass before the hook is reached.
Those four are the whole of the ``ABCMeta.__new__`` collision surface, measured
on 3.10.21, 3.11.15 and 3.14.7; ``engine=``, ``protocol=`` and ``fmt=`` are all outside it,
so the documented registration path works on every supported version. There is
no ``name=`` alias -- a keyword that worked on some interpreters and not others
is the trap being removed, not a compatibility measure.
* **Changed** -- extraction is around 46% faster on a 1,117-frame HTTP capture,
with byte-identical output (#420). A reassembled datagram's payload is now
analysed on first read rather than eagerly, which cuts IP reassembly's own
Expand Down
6 changes: 3 additions & 3 deletions docs/source/ext.rst
Original file line number Diff line number Diff line change
Expand Up @@ -386,13 +386,13 @@ The following code snippet shows how to create a new engine class:
from scapy.packet import Packet


# NOTE: The ``name`` keyword is what registers the engine with the Extractor,
# NOTE: The ``engine`` keyword is what registers the engine with the Extractor,
# and it is the key ``Extractor(engine=...)`` will look it up under. It is
# required: registration is opt-in, so omitting it defines a perfectly usable
# class that is simply not selectable by name. Note that __engine_name__ is
# *not* an opt-in -- it sets the name the engine reports about itself, which
# it does whether or not the engine is registered.
class MyScapy(Engine['Packet'], name='scapy'):
class MyScapy(Engine['Packet'], engine='scapy'):

__engine_name__ = 'Scapy' # friendly name of the engine
__engine_module__ = 'scapy' # module name that the engine is based on
Expand Down Expand Up @@ -751,7 +751,7 @@ The following code snippet shows how to create a new reassembly class:
# it defines a perfectly usable class that is simply not selectable by name.
# Note that __protocol_name__ is *not* an opt-in -- it sets the name the class
# reports about itself, registered or not. Note also that the keyword is
# spelled ``protocol`` here and ``name`` on Engine above.
# spelled ``protocol`` here and ``engine`` on Engine above.
class MyReassembly(Reassembly[Packet, Datagram, BufferID, Buffer],
protocol='ipv4'):

Expand Down
58 changes: 28 additions & 30 deletions pcapkit/foundation/engines/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,12 @@ class Engine(EngineBase[_T], Generic[_T]):

Example:

Registration is opt-in. Pass keyword argument ``name`` at class
Registration is opt-in. Pass keyword argument ``engine`` at class
definition to register the engine under that name:

.. code-block:: python

class MyEngine(Engine, name='my_engine'):
class MyEngine(Engine, engine='my_engine'):
...

Omit it and the subclass is *not* registered, which is how a class
Expand All @@ -242,14 +242,14 @@ class MyMixin(Engine): # not registered

"""

def __init_subclass__(cls, /, name: 'Optional[str]' = None, *args: 'Any', **kwargs: 'Any') -> 'None':
def __init_subclass__(cls, /, engine: 'Optional[str]' = None, *args: 'Any', **kwargs: 'Any') -> 'None':
"""Initialise subclass.

This method is to be used for registering the engine class to
:class:`~pcapkit.foundation.extraction.Extractor` class.

Args:
name: Engine name to register the subclass under, lowercased.
engine: Engine name to register the subclass under, lowercased.
:data:`None` (the default) skips registration entirely.
*args: Arbitrary positional arguments.
**kwargs: Arbitrary keyword arguments.
Expand All @@ -258,7 +258,7 @@ def __init_subclass__(cls, /, name: 'Optional[str]' = None, *args: 'Any', **kwar
UnsupportedCall: If any unrecognised class keyword is given.

Registration is **opt-in**: the subclass is registered if and only if
``name`` is given. This is what lets a subclass decline registration
``engine`` is given. This is what lets a subclass decline registration
rather than having to inherit :class:`EngineBase` to avoid it, and it
matches :meth:`EnumSchema.__init_subclass__
<pcapkit.protocols.schema.schema.EnumSchema.__init_subclass__>`, which
Expand All @@ -270,28 +270,21 @@ def __init_subclass__(cls, /, name: 'Optional[str]' = None, *args: 'Any', **kwar
the engine reports, which it does whether or not the engine is
registered; only the keyword decides registration.

Warning:
**On Python 3.10 the ``name`` keyword cannot be passed at all.**
:meth:`abc.ABCMeta.__new__` takes ``mcls``, ``name``, ``bases`` and
``namespace`` as positional-*or-keyword* parameters before 3.11, so a
class keyword named ``name`` collides with one of them and the class
statement raises :exc:`TypeError` -- ``ABCMeta.__new__() got multiple
values for argument 'name'`` -- from the metaclass, before this method
is reached. From 3.11 those parameters are positional-only and the
keyword arrives here normally. Measured on 3.10.21, 3.11.15 and
3.14.7.

The consequence on 3.10 is that an engine cannot be registered at
class definition; register it explicitly instead, which works on every
version::

class MyEngine(Engine): # no keyword, so not registered
...

Extractor.register_engine('my_engine', MyEngine)

The sibling hooks are unaffected, their keywords being ``protocol``
and ``fmt``.
Note:
This keyword was ``name`` when opt-in registration landed, and was
renamed because ``name`` cannot be passed as a class keyword at all
on Python 3.10: :meth:`abc.ABCMeta.__new__` takes ``mcls``, ``name``,
``bases`` and ``namespace`` as positional-*or-keyword* parameters
before 3.11, so a class keyword by any of those four names collides
with one of them and the class statement raises :exc:`TypeError` from
the metaclass before this method is reached. ``engine`` is outside
that set, so the documented registration path now works on every
supported version. Measured on 3.10.21, 3.11.15 and 3.14.7; those
four are the whole of the :meth:`abc.ABCMeta.__new__` collision
surface. Separately, and for an unrelated reason that holds on every
version, ``metaclass`` is not usable as a class keyword either: a
``class`` statement consumes it to choose the metaclass, so it never
reaches this method at all.

See Also:
For more details, please refer to
Expand All @@ -301,11 +294,16 @@ class MyEngine(Engine): # no keyword, so not registered
# NOTE: an unrecognised class keyword lands in ``**kwargs`` and is then
# dropped by the bare ``super().__init_subclass__()`` below, since
# ``object.__init_subclass__`` takes none. Silently swallowing it is how
# ``class MyEngine(Engine, nmae='x')`` used to register under its class
# ``class MyEngine(Engine, engnie='x')`` used to register under its class
# name instead -- no exception, no warning. Now that a missing keyword
# means "do not register", the same typo would silently skip
# registration altogether, which is quieter still. So reject it.
#
# One typo this cannot catch is ``name=``, and only on Python 3.10: it is
# one of the four names that collide with ``ABCMeta.__new__``, so it fails
# in the metaclass before reaching here. It is still loud, just as a
# ``TypeError`` rather than an ``UnsupportedCall``.
#
# ``args`` is checked alongside ``kwargs`` for completeness rather than
# because a ``class`` statement can fill it -- class creation passes
# keywords only. It is reachable through a direct
Expand All @@ -314,10 +312,10 @@ class MyEngine(Engine): # no keyword, so not registered
unexpected = ', '.join([*map(repr, args), *sorted(kwargs)])
raise UnsupportedCall(f'{cls.__name__}: unexpected class keyword(s): {unexpected}')

if name is not None:
if engine is not None:
from pcapkit.foundation.extraction import \
Extractor # pylint: disable=import-outside-toplevel

Extractor.register_engine(name.lower(), cls)
Extractor.register_engine(engine.lower(), cls)

return super().__init_subclass__()
Loading
Loading