From 202d8515ca4bcd9ceb6c30265bb70f4d1cc0fe80 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sun, 20 Sep 2026 16:20:54 -0400 Subject: [PATCH] fix(foundation): rename Engine's registry keyword to `engine`, which works on Python 3.10 (#514) * `Engine.__init_subclass__`'s keyword is `engine` rather than `name`. `name` is one of four class keyword names -- `mcls`, `name`, `bases`, `namespace` -- that collide with `abc.ABCMeta.__new__`'s own parameters, which are positional-or-keyword before Python 3.11 and positional-only from 3.11. So on 3.10 `class MyEngine(Engine, name='x')` raised `TypeError` from the metaclass before the hook ran, making the documented registration path unusable there. Those four are the whole collision surface, measured; `engine`, `protocol` and `fmt` are all outside it. * no `name=` alias. A keyword that works on some interpreters and not others is the trap being removed, not a compatibility measure. * the `skipIf(sys.version_info < (3, 11))` on the inheritance test and the inline version branch in the opt-in test both come out, since neither needed a guard for any reason other than requiring `name=` at class-creation time. The engine tests now run in full on every supported version. * `name=` is an unrecognised keyword from here on, and its exception type is version-dependent -- `UnsupportedCall` from 3.11, `TypeError` from the metaclass on 3.10 -- so it is pinned per version rather than skipped. * corrects the #547 changelog entry, which named the old keyword, and `docs/source/ext.rst`'s engine example, which showed the crashing form. Unit tier 3.14: 1104 passed, 8 skipped, 2660 subtests, exit 0 read from a file. 3.10.21: engine + extraction + changelog tests 142 passed, 21 skipped, exit 0. Reverting the rename fails 3 tests on 3.14 and 2 on 3.10, exit 1 on both. mypy clean; pylint unchanged at its 3 pre-existing messages for this file. --- CHANGELOG.md | 2 +- docs/source/changelog/1.5.0.rst | 20 +++--- docs/source/ext.rst | 6 +- pcapkit/foundation/engines/engine.py | 58 ++++++++-------- tests/foundation/engines/test_engine_base.py | 69 +++++++++++++------- 5 files changed, 87 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42868f009..6f3e779ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/docs/source/changelog/1.5.0.rst b/docs/source/changelog/1.5.0.rst index 350a46fa4..3c96119a4 100644 --- a/docs/source/changelog/1.5.0.rst +++ b/docs/source/changelog/1.5.0.rst @@ -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 @@ -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 diff --git a/docs/source/ext.rst b/docs/source/ext.rst index 5ac47fce2..3af78adf4 100644 --- a/docs/source/ext.rst +++ b/docs/source/ext.rst @@ -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 @@ -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'): diff --git a/pcapkit/foundation/engines/engine.py b/pcapkit/foundation/engines/engine.py index 5f38a036e..822acf7a5 100644 --- a/pcapkit/foundation/engines/engine.py +++ b/pcapkit/foundation/engines/engine.py @@ -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 @@ -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. @@ -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__ `, which @@ -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 @@ -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 @@ -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__() diff --git a/tests/foundation/engines/test_engine_base.py b/tests/foundation/engines/test_engine_base.py index bebf42f97..4320a07eb 100644 --- a/tests/foundation/engines/test_engine_base.py +++ b/tests/foundation/engines/test_engine_base.py @@ -64,7 +64,7 @@ def read_frame(self) -> str: self.assertEqual(named.module, 'instance.module') def test_engine_subclass_registration_is_opt_in(self) -> None: - """Registration happens if and only if ``name`` is given. + """Registration happens if and only if ``engine`` is given. This is the #514 opt-in contract. Before it, ``__init_subclass__`` fell back to ``cls.name`` when the keyword was absent, so *every* subclass of @@ -77,30 +77,27 @@ def test_engine_subclass_registration_is_opt_in(self) -> None: ``'defaultengine'``. A class attribute is not a registry key, and now it registers nothing. - Note on the version guard below, which predates this change: it is - *not* about ``Engine`` being generic. ``Engine``'s registry keyword is - literally ``name``, and ``mcls``/``name``/``bases``/``namespace`` collide - with :meth:`abc.ABCMeta.__new__`'s own parameters, which are - positional-or-keyword on Python 3.10 and positional-only from 3.11. So on - 3.10 ``class MyEngine(Engine, name='x')`` -- the documented way to - register an engine -- raises :exc:`TypeError` from the metaclass before - ``__init_subclass__`` runs. Measured against this tree on 3.10.21. A - non-colliding keyword needs no guard, which is why the sibling test above - has none. + No version guard, and that is the point of the rename. The keyword was + ``name`` when opt-in registration landed, which made this half of the test + unrunnable on Python 3.10 -- ``mcls``/``name``/``bases``/``namespace`` + collide with :meth:`abc.ABCMeta.__new__`'s own parameters, which are + positional-or-keyword before 3.11 and positional-only from 3.11, so the + class statement failed in the metaclass before ``__init_subclass__`` ran. + ``engine`` is outside that set, so this now runs everywhere. Verified on + 3.10.21 and 3.14.7. """ from pcapkit.foundation.engines.engine import Engine - if sys.version_info >= (3, 11): - with mock.patch('pcapkit.foundation.extraction.Extractor.register_engine') as register: - class Explicit(Engine[str], name='ExplicitEngine'): - def run(self) -> None: - pass + with mock.patch('pcapkit.foundation.extraction.Extractor.register_engine') as register: + class Explicit(Engine[str], engine='ExplicitEngine'): + def run(self) -> None: + pass - def read_frame(self) -> str: - return 'frame' + def read_frame(self) -> str: + return 'frame' - register.assert_called_once_with('explicitengine', Explicit) + register.assert_called_once_with('explicitengine', Explicit) with mock.patch('pcapkit.foundation.extraction.Extractor.register_engine') as register: class Default(Engine[str]): @@ -121,7 +118,7 @@ def read_frame(self) -> str: def test_engine_subclass_rejects_unrecognised_keyword(self) -> None: """A misspelled class keyword raises instead of being swallowed. - ``Engine`` spells the registry key ``name`` while ``Reassembly`` and + ``Engine`` spells the registry key ``engine`` while ``Reassembly`` and ``TraceFlow`` spell the same idea ``protocol``, so guessing the wrong one is the expected mistake. It used to land in ``**kwargs``, get dropped by the bare ``super().__init_subclass__()``, and leave the class registered @@ -133,6 +130,13 @@ def test_engine_subclass_rejects_unrecognised_keyword(self) -> None: the guard is reached on every supported version. Verified against this tree on 3.10.21, where it raises ``UnsupportedCall`` as it does on 3.14. + ``name`` is now an unrecognised keyword here too, and is checked + separately below because it is the one typo whose exception *type* is + version-dependent: on 3.10 the metaclass collision fires before the + guard, so it is a :exc:`TypeError` rather than an + :exc:`~pcapkit.utilities.exceptions.UnsupportedCall`. Loud either way, + which is why the rename did not need to chase it. + """ from pcapkit.foundation.engines.engine import Engine from pcapkit.utilities.exceptions import UnsupportedCall @@ -149,9 +153,21 @@ def read_frame(self) -> str: register.assert_not_called() self.assertIn('protocol', str(caught.exception)) - @unittest.skipIf(sys.version_info < (3, 11), - "Engine's registry keyword is literally `name`, which collides " - 'with ABCMeta.__new__ on 3.10 -- see the note in the docstring') + # ``name=`` is no longer the registry keyword, so it is a typo now. It is + # the one whose exception type is version-dependent: the metaclass + # collision fires before the guard on 3.10. + expected = UnsupportedCall if sys.version_info >= (3, 11) else TypeError + with mock.patch('pcapkit.foundation.extraction.Extractor.register_engine') as register: + with self.assertRaises(expected): + class Collides(Engine[str], name='no-longer-the-keyword'): + def run(self) -> None: + pass + + def read_frame(self) -> str: + return 'frame' + + register.assert_not_called() + def test_registration_is_not_inherited_by_a_subclass(self) -> None: """A subclass of a *registered* class does not inherit its registration. @@ -169,11 +185,14 @@ def test_registration_is_not_inherited_by_a_subclass(self) -> None: ``HTTP``, ``L2TP`` and ``IP`` can be open to inheritance without the anti-re-registration guarantee being weakened. + Runs on every supported version since the keyword became ``engine``; + it was skipped below 3.11 while the keyword was ``name``. + """ from pcapkit.foundation.engines.engine import Engine with mock.patch('pcapkit.foundation.extraction.Extractor.register_engine') as register: - class Parent(Engine[str], name='ParentEngine'): + class Parent(Engine[str], engine='ParentEngine'): def run(self) -> None: pass @@ -192,7 +211,7 @@ class Derived(Parent): # ... and inheritance is still open to a subclass that asks to register with mock.patch('pcapkit.foundation.extraction.Extractor.register_engine') as register: - class DerivedOptIn(Parent, name='DerivedEngine'): + class DerivedOptIn(Parent, engine='DerivedEngine'): pass register.assert_called_once_with('derivedengine', DerivedOptIn)