diff --git a/docs/source/contributing/conventions/index.rst b/docs/source/contributing/conventions/index.rst index b20777e2bd..20b33b1804 100644 --- a/docs/source/contributing/conventions/index.rst +++ b/docs/source/contributing/conventions/index.rst @@ -13,6 +13,8 @@ House Conventions arisen. :ref:`sentinel-convention` governs :mod:`pcapkit.corekit`, :ref:`extension-header-subclassing` a protocol class hierarchy, :ref:`protocol-layer-placement` which subpackage a dissector belongs in, + :ref:`utilities-or-corekit` which of :mod:`pcapkit.utilities` and + :mod:`pcapkit.corekit` a support module belongs in, :ref:`process` the repository rather than any of its code, :ref:`documentation` the prose itself -- on these pages and in the API reference -- rather than any code at all, and :ref:`test-coverage` what the @@ -37,3 +39,4 @@ House Conventions process documentation test-coverage + utilities-or-corekit diff --git a/docs/source/contributing/conventions/utilities-or-corekit.rst b/docs/source/contributing/conventions/utilities-or-corekit.rst new file mode 100644 index 0000000000..c74385c1ef --- /dev/null +++ b/docs/source/contributing/conventions/utilities-or-corekit.rst @@ -0,0 +1,27 @@ +.. _utilities-or-corekit: + +Choosing Between ``utilities`` and ``corekit`` +---------------------------------------------- + +Which of the two support packages a module belongs in. Settled by the owner on +:issue:`1712`, over two alternatives that sorted modules by packet knowledge or by +who uses them. This rule can be checked from the import graph alone, and it needed +the fewest moves: one. + +:mod:`pcapkit.utilities` **is for cross-cutting services**: exceptions, warnings, +logging, the compatibility layer, configuration, and wrappers around third-party +libraries such as :mod:`~pcapkit.utilities.chardet`. The question to ask of a module +is *could any code use this service, and does it import nothing from* :mod:`pcapkit` +*outside* ``utilities``? Both halves have to hold, and *nothing* covers a top-level +import, one inside a function and one under ``TYPE_CHECKING`` alike. +:file:`tests/project/test_utilities_import_boundary_1735_unit.py` checks every module +under :file:`pcapkit/utilities/` for it. + +:mod:`pcapkit.corekit` **is for the types the packet model is built from**: what +:mod:`pcapkit.protocols`, :mod:`pcapkit.const` and :mod:`pcapkit.foundation` +subclass, instantiate or decorate with. The question is *is this part of what a +protocol or its schema is made of?* A support module that fails the ``utilities`` +question belongs here instead. :mod:`pcapkit.corekit.decorators` is the worked example: it acts on a +protocol's file and falls back to protocol classes, so it moved out of ``utilities`` +on :issue:`1735`. ``corekit`` may import ``utilities``; the reverse is what the rule +forbids. diff --git a/docs/source/pcapkit/corekit/decorators.rst b/docs/source/pcapkit/corekit/decorators.rst new file mode 100644 index 0000000000..5fceeb4101 --- /dev/null +++ b/docs/source/pcapkit/corekit/decorators.rst @@ -0,0 +1,39 @@ +Decorator Functions +=================== + +.. module:: pcapkit.corekit.decorators + +:mod:`pcapkit.corekit.decorators` contains several useful +decorators, including :func:`~pcapkit.corekit.decorators.seekset`, +:func:`~pcapkit.corekit.decorators.beholder` and +:func:`~pcapkit.corekit.decorators.prepare`. + +.. note:: + + This module was ``pcapkit.utilities.decorators`` until :issue:`1735` moved it + here, and :mod:`pcapkit.utilities` no longer exports the three decorators. + Nothing is left at the old path, so import them from this module. + +.. autodecorator:: pcapkit.corekit.decorators.seekset + +.. autodecorator:: pcapkit.corekit.decorators.beholder + +.. autodecorator:: pcapkit.corekit.decorators.prepare + +.. important:: + + All three decorators above are designed for decorating *class methods*. + For more information, please refer to the documentation of each + decorator function. + +Type Variables +-------------- + +.. data:: pcapkit.corekit.decorators.R_seekset + :type: typing.Any + +.. data:: pcapkit.corekit.decorators.R_beholder + :type: pcapkit.protocols.protocol.ProtocolBase + +.. data:: pcapkit.corekit.decorators.R_prepare + :type: pcapkit.protocols.schema.schema.Schema diff --git a/docs/source/pcapkit/corekit/index.rst b/docs/source/pcapkit/corekit/index.rst index 6ccbad3b7e..efb2a6f53d 100644 --- a/docs/source/pcapkit/corekit/index.rst +++ b/docs/source/pcapkit/corekit/index.rst @@ -10,7 +10,8 @@ collection :class:`~pcapkit.corekit.protochain.ProtoChain`, the :class:`~pcapkit.corekit.multidict.MultiDict` family for multi-entry mappings, the :class:`~pcapkit.corekit.fields.field.Field` family for data parsing, the :class:`~pcapkit.corekit.context.ContextRegistry` channel for -caller-supplied information a protocol needs but the wire does not carry, and +caller-supplied information a protocol needs but the wire does not carry, the +:mod:`~pcapkit.corekit.decorators` that protocols and schemas parse through, and the :class:`~pcapkit.corekit.enum.EnumLookup`/ :class:`~pcapkit.corekit.enum.EnumRegistry` bases every constant enumeration inherits from. @@ -20,6 +21,7 @@ inherits from. fields/index context + decorators enum infoclass io diff --git a/docs/source/pcapkit/protocols/application/ngap.rst b/docs/source/pcapkit/protocols/application/ngap.rst index c8be61e1fd..b0c98bd80d 100644 --- a/docs/source/pcapkit/protocols/application/ngap.rst +++ b/docs/source/pcapkit/protocols/application/ngap.rst @@ -32,7 +32,7 @@ it; an NGAP payload simply degrades to the opaque payload path, exactly as an unregistered PPID would, because :meth:`~pcapkit.protocols.protocol.Protocol._import_next_layer`, which :class:`~pcapkit.protocols.transport.sctp.SCTP` inherits, is wrapped in -:func:`~pcapkit.utilities.decorators.beholder` and falls back to +:func:`~pcapkit.corekit.decorators.beholder` and falls back to :class:`~pcapkit.protocols.misc.raw.Raw`. Decoder Dependency diff --git a/docs/source/pcapkit/utilities/decorators.rst b/docs/source/pcapkit/utilities/decorators.rst deleted file mode 100644 index 6d767d1b12..0000000000 --- a/docs/source/pcapkit/utilities/decorators.rst +++ /dev/null @@ -1,33 +0,0 @@ -Decorator Functions -=================== - -.. module:: pcapkit.utilities.decorators - -:mod:`pcapkit.utilities.decorators` contains several useful -decorators, including :func:`~pcapkit.utilities.decorators.seekset`, -:func:`~pcapkit.utilities.decorators.beholder` and -:func:`~pcapkit.utilities.decorators.prepare`. - -.. autodecorator:: pcapkit.utilities.decorators.seekset - -.. autodecorator:: pcapkit.utilities.decorators.beholder - -.. autodecorator:: pcapkit.utilities.decorators.prepare - -.. important:: - - All three decorators above are designed for decorating *class methods*. - For more information, please refer to the documentation of each - decorator function. - -Type Variables --------------- - -.. data:: pcapkit.utilities.decorators.R_seekset - :type: typing.Any - -.. data:: pcapkit.utilities.decorators.R_beholder - :type: pcapkit.protocols.protocol.ProtocolBase - -.. data:: pcapkit.utilities.decorators.R_prepare - :type: pcapkit.protocols.schema.schema.Schema diff --git a/docs/source/pcapkit/utilities/index.rst b/docs/source/pcapkit/utilities/index.rst index 3b5e141990..7d70aee097 100644 --- a/docs/source/pcapkit/utilities/index.rst +++ b/docs/source/pcapkit/utilities/index.rst @@ -4,16 +4,14 @@ Module Utilities .. module:: pcapkit.utilities -:mod:`pcapkit.utilities` contains several useful functions -and classes which are foundations of :mod:`pcapkit`, including -decorator function :func:`~pcapkit.utilities.decorators.seekset` -and :func:`~pcapkit.utilities.decorators.beholder`, and -several user-refined exceptions and warnings. +:mod:`pcapkit.utilities` holds the cross-cutting services :mod:`pcapkit` is +built with: user-refined exceptions and warnings, logging, a compatibility +layer, and wrappers around third-party libraries. It imports nothing from +:mod:`pcapkit` outside itself; see :ref:`utilities-or-corekit`. .. toctree:: :maxdepth: 2 - decorators exceptions warnings logging diff --git a/examples/generators/dispatch.py b/examples/generators/dispatch.py index 3fcae09be3..e955c0a47a 100644 --- a/examples/generators/dispatch.py +++ b/examples/generators/dispatch.py @@ -786,7 +786,7 @@ class Outcome(NamedTuple): detail: 'str' #: The first ``'error'`` value found anywhere in the parsed frame's nested #: info mapping, or ``''`` if none is present. This is what - #: :func:`pcapkit.utilities.decorators.beholder` records when a dissector + #: :func:`pcapkit.corekit.decorators.beholder` records when a dissector #: raises and gets replaced with :class:`~pcapkit.protocols.misc.raw.Raw`, #: so it is the fragment :data:`KNOWN_DEGRADED` pins against. error: 'str' diff --git a/pcapkit/all.py b/pcapkit/all.py index 8575a83ea1..7ebc733b98 100644 --- a/pcapkit/all.py +++ b/pcapkit/all.py @@ -145,5 +145,4 @@ # pcapkit.utilities -- unlisted: its names stay reachable as attributes, but # ``import *`` leaves out generic helpers such as ``warn``, ``reset``, # ``configure`` and ``detect`` - #'beholder_ng', 'seekset_ng', # Decorators ] diff --git a/pcapkit/utilities/decorators.py b/pcapkit/corekit/decorators.py similarity index 96% rename from pcapkit/utilities/decorators.py rename to pcapkit/corekit/decorators.py index 07974caa51..f17b407d76 100644 --- a/pcapkit/utilities/decorators.py +++ b/pcapkit/corekit/decorators.py @@ -3,12 +3,17 @@ """Decorator Functions ========================= -.. module:: pcapkit.utilities.decorators +.. module:: pcapkit.corekit.decorators -:mod:`pcapkit.utilities.decorators` contains several useful -decorators, including :func:`~pcapkit.utilities.decorators.seekset`, -:func:`~pcapkit.utilities.decorators.beholder` and -:func:`~pcapkit.utilities.decorators.prepare`. +:mod:`pcapkit.corekit.decorators` contains several useful +decorators, including :func:`~pcapkit.corekit.decorators.seekset`, +:func:`~pcapkit.corekit.decorators.beholder` and +:func:`~pcapkit.corekit.decorators.prepare`. + +They act on a protocol's file and on the schema it unpacks, and +:func:`~pcapkit.corekit.decorators.beholder` falls back to protocol classes, +so they live with the packet model rather than in :mod:`pcapkit.utilities` +(:issue:`1735`). """ import functools diff --git a/pcapkit/foundation/extraction.py b/pcapkit/foundation/extraction.py index a0cd5052b3..be433d2488 100644 --- a/pcapkit/foundation/extraction.py +++ b/pcapkit/foundation/extraction.py @@ -1571,7 +1571,7 @@ def _note_eof_progress(self) -> 'bool': :issue:`620` reported. The signal is the input's own position. End of stream is raised by - :func:`~pcapkit.utilities.decorators.prepare` when the bytes remaining in + :func:`~pcapkit.corekit.decorators.prepare` when the bytes remaining in the stream measure zero, and it restores the position before raising, so the position at end of stream is stable. Two consecutive ends of stream at the *same* position therefore mean nothing arrived between them. diff --git a/pcapkit/protocols/application/ngap.py b/pcapkit/protocols/application/ngap.py index 5153c37bce..010aee0058 100644 --- a/pcapkit/protocols/application/ngap.py +++ b/pcapkit/protocols/application/ngap.py @@ -30,7 +30,7 @@ unregistered PPID would, because :meth:`~pcapkit.protocols.protocol.Protocol._import_next_layer`, which :class:`~pcapkit.protocols.transport.sctp.SCTP` inherits, is wrapped in -:func:`~pcapkit.utilities.decorators.beholder` and falls back to +:func:`~pcapkit.corekit.decorators.beholder` and falls back to :class:`~pcapkit.protocols.misc.raw.Raw`. Decoder Dependency diff --git a/pcapkit/protocols/data/internet/ipv6_ext.py b/pcapkit/protocols/data/internet/ipv6_ext.py index b0968b3520..8e55365b8b 100644 --- a/pcapkit/protocols/data/internet/ipv6_ext.py +++ b/pcapkit/protocols/data/internet/ipv6_ext.py @@ -48,6 +48,6 @@ class IPv6_Ext(Protocol): #: Header-specific content after the two fixed octets, as consumed. data: 'bytes' #: Original parsing error, if this instance was reached as a - #: :func:`~pcapkit.utilities.decorators.beholder` fallback rather than by + #: :func:`~pcapkit.corekit.decorators.beholder` fallback rather than by #: direct dispatch. error: 'Optional[Exception]' diff --git a/pcapkit/protocols/internet/internet.py b/pcapkit/protocols/internet/internet.py index 47e0e4453f..9d63d99970 100644 --- a/pcapkit/protocols/internet/internet.py +++ b/pcapkit/protocols/internet/internet.py @@ -18,11 +18,11 @@ from pcapkit.const.ipv6.extension_header import ExtensionHeader as Enum_ExtensionHeader from pcapkit.const.reg.transtype import TransType as Enum_TransType +from pcapkit.corekit.decorators import beholder from pcapkit.corekit.module import ModuleDescriptor from pcapkit.corekit.protochain import ProtoChain from pcapkit.protocols.protocol import _PT, _ST, ProtocolBase, _rebuilding from pcapkit.protocols.schema.schema import keep_short_read, replay_short_read -from pcapkit.utilities.decorators import beholder from pcapkit.utilities.exceptions import stacklevel from pcapkit.utilities.warnings import ProtocolWarning, RegistryWarning, warn diff --git a/pcapkit/protocols/internet/ipv6.py b/pcapkit/protocols/internet/ipv6.py index d60edbc84c..44d343457d 100644 --- a/pcapkit/protocols/internet/ipv6.py +++ b/pcapkit/protocols/internet/ipv6.py @@ -31,6 +31,7 @@ from pcapkit.const.ipv6.extension_header import ExtensionHeader as Enum_ExtensionHeader from pcapkit.const.reg.transtype import TransType as Enum_TransType +from pcapkit.corekit.decorators import beholder from pcapkit.corekit.infoclass import Info from pcapkit.corekit.multidict import MultiDict, OrderedMultiDict from pcapkit.corekit.protochain import ProtoChain @@ -40,7 +41,6 @@ from pcapkit.protocols.protocol import FRAME_LAYER_LIMIT, _frame_depth from pcapkit.protocols.schema.internet.ipv6 import IPv6 as Schema_IPv6 from pcapkit.protocols.schema.internet.ipv6 import jumbo_payload_length, jumbogram_error -from pcapkit.utilities.decorators import beholder from pcapkit.utilities.exceptions import ProtocolError if TYPE_CHECKING: @@ -72,7 +72,7 @@ class IPv6(IP[Data_IPv6, Schema_IPv6], #: When that dedicated parser raises, :meth:`_import_next_layer` #: substitutes :class:`~pcapkit.protocols.internet.ipv6_ext.IPv6_Ext` #: instead of letting the generic - #: :func:`~pcapkit.utilities.decorators.beholder` fall back to plain + #: :func:`~pcapkit.corekit.decorators.beholder` fall back to plain #: :class:`~pcapkit.protocols.misc.raw.Raw`, which has no ``next`` field #: and so cannot continue the walk in :meth:`_decode_next_layer`. #: @@ -606,7 +606,7 @@ def _decode_next_layer(self, ipv6: 'Data_IPv6', proto: 'Optional[int]' = None, # :class:`~pcapkit.protocols.misc.raw.Raw`, which does not (see # :attr:`__generic_ext_codes__`). Reading ``info.next`` on that # would raise ``AttributeError`` and let a further-out - # :func:`~pcapkit.utilities.decorators.beholder` degrade the + # :func:`~pcapkit.corekit.decorators.beholder` degrade the # *whole* packet. # # Such a layer is not recorded as an extension header: it consumed @@ -725,7 +725,7 @@ def _import_next_layer(self, proto: 'int', length: 'Optional[int]' = None, *, # raises, this substitutes :class:`~pcapkit.protocols.internet.ipv6_ext.IPv6_Ext` for it rather than letting the exception reach the - :func:`~pcapkit.utilities.decorators.beholder` decorating this + :func:`~pcapkit.corekit.decorators.beholder` decorating this method, which would substitute plain :class:`~pcapkit.protocols.misc.raw.Raw` -- and ``Raw`` has no ``next`` field, so it could not continue diff --git a/pcapkit/protocols/internet/ipv6_ext.py b/pcapkit/protocols/internet/ipv6_ext.py index 34f32e2c16..ace7e6464c 100644 --- a/pcapkit/protocols/internet/ipv6_ext.py +++ b/pcapkit/protocols/internet/ipv6_ext.py @@ -107,7 +107,7 @@ 2. **A recognised header whose own parser raises.** ``HOPOPT``, ``IPv6-Route``, ``IPv6-Opts``, ``MH``, ``HIP``, ``IPv6-Frag`` and ``AH`` all have dedicated classes. When one of *those* raises, - :func:`~pcapkit.utilities.decorators.beholder` + :func:`~pcapkit.corekit.decorators.beholder` (:meth:`Protocol._import_next_layer `) would substitute plain :class:`~pcapkit.protocols.misc.raw.Raw`, which loses ``next`` and crashes the walk as in path 1. @@ -381,7 +381,7 @@ def read(self, length: 'Optional[int]' = None, *, # pylint: disable=arguments-d stands in for, e.g. ``0`` for ``HOPOPT`` or ``140`` for ``Shim6``. * ``error`` -- the parsing error, when this instance was - reached as a :func:`~pcapkit.utilities.decorators.beholder` + reached as a :func:`~pcapkit.corekit.decorators.beholder` fallback rather than by direct dispatch. Returns: @@ -578,7 +578,7 @@ def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None, length: 'Opt reading ``IPv4:IPv6-Ext:...`` instead of the ``IPv4:Shim6`` that a plain, non-continuing ``Raw`` gives. Rejecting here sends construction back through - :func:`~pcapkit.utilities.decorators.beholder` at the *caller's* + :func:`~pcapkit.corekit.decorators.beholder` at the *caller's* layer, which substitutes that same ``Raw``: the version-agnostic behaviour, with no IPv4-specific code. diff --git a/pcapkit/protocols/misc/pcapng.py b/pcapkit/protocols/misc/pcapng.py index 463366d830..9b68b742cf 100644 --- a/pcapkit/protocols/misc/pcapng.py +++ b/pcapkit/protocols/misc/pcapng.py @@ -1316,7 +1316,7 @@ def read(self, length: 'Optional[int]' = None, *, _read: 'bool' = True, # copy and never against the file, so on a capture cut short it runs # past the real end -- and seeking past the end is legal and silent. # The *next* block read would then measure a negative remainder, - # since ``pcapkit.utilities.decorators.prepare`` derives it as the + # since ``pcapkit.corekit.decorators.prepare`` derives it as the # end of the stream less the current position, and a negative # ``__length__`` is what ``pcapng_block_selector`` hands to # :class:`~pcapkit.corekit.fields.misc.SchemaField`, which fails with @@ -1530,7 +1530,7 @@ def _check_block_floor(self, length: 'Optional[int]') -> 'None': :meth:`FieldBase.unpack `, which has no catch point above it and would abort the whole extraction -- this costs no frame that was actually in the file. It is the same signal - :func:`~pcapkit.utilities.decorators.prepare` raises for a *derived* + :func:`~pcapkit.corekit.decorators.prepare` raises for a *derived* remainder of exactly zero; a remainder of one, two or three octets passes that check and is caught here instead. diff --git a/pcapkit/protocols/misc/raw.py b/pcapkit/protocols/misc/raw.py index be3b602b29..ee599f024a 100644 --- a/pcapkit/protocols/misc/raw.py +++ b/pcapkit/protocols/misc/raw.py @@ -139,7 +139,7 @@ def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None, self._file = io.BytesIO(self._data) # NOTE: The buffer above is all there is to parse, so its size is - # declared rather than left for :func:`~pcapkit.utilities.decorators.prepare` + # declared rather than left for :func:`~pcapkit.corekit.decorators.prepare` # to derive: a derived zero is read as the end of the stream, whereas # an empty raw payload is a payload all the same (:issue:`1285`). if length is None: diff --git a/pcapkit/protocols/protocol.py b/pcapkit/protocols/protocol.py index 3bc58837b9..4313511b9f 100644 --- a/pcapkit/protocols/protocol.py +++ b/pcapkit/protocols/protocol.py @@ -35,6 +35,7 @@ import aenum from pcapkit.corekit.context import ContextRegistry +from pcapkit.corekit.decorators import beholder, seekset from pcapkit.corekit.fields.misc import ConditionalField, SchemaField, SwitchField from pcapkit.corekit.module import ModuleDescriptor from pcapkit.corekit.protochain import ProtoChain @@ -48,7 +49,6 @@ from pcapkit.protocols.schema.schema import Schema from pcapkit.utilities.chardet import detect from pcapkit.utilities.compat import cached_property, get_annotations -from pcapkit.utilities.decorators import beholder, seekset from pcapkit.utilities.exceptions import (BaseError, ProtocolError, ProtocolNotFound, ProtocolNotImplemented, RegistryError, StructError, UnsupportedCall, stacklevel) diff --git a/pcapkit/protocols/schema/schema.py b/pcapkit/protocols/schema/schema.py index fd0bf6aa36..42b108d53a 100644 --- a/pcapkit/protocols/schema/schema.py +++ b/pcapkit/protocols/schema/schema.py @@ -8,6 +8,7 @@ import itertools from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast +from pcapkit.corekit.decorators import prepare from pcapkit.corekit.fields.collections import ListField, OptionField from pcapkit.corekit.fields.field import NO_VALUE, FieldBase from pcapkit.corekit.fields.misc import (ConditionalField, ForwardMatchField, PayloadField, @@ -15,7 +16,6 @@ from pcapkit.corekit.fields.strings import PaddingField from pcapkit.corekit.infoclass import FinalisedState from pcapkit.utilities.compat import Mapping, final -from pcapkit.utilities.decorators import prepare from pcapkit.utilities.exceptions import NoDefaultValue, ProtocolUnbound, SchemaError, stacklevel from pcapkit.utilities.warnings import RegistryWarning, SchemaWarning, UnknownFieldWarning, warn diff --git a/pcapkit/utilities/__init__.py b/pcapkit/utilities/__init__.py index bd1169d2a2..bac5dfc5bf 100644 --- a/pcapkit/utilities/__init__.py +++ b/pcapkit/utilities/__init__.py @@ -5,19 +5,17 @@ .. module:: pcapkit.utilities -:mod:`pcapkit.utilities` contains several useful functions -and classes which are foundations of :mod:`pcapkit`, including -decorator function :func:`~pcapkit.utilities.decorators.seekset` -and :func:`~pcapkit.utilities.decorators.beholder`, etc., and -several user-refined exceptions and warnings. +:mod:`pcapkit.utilities` holds the cross-cutting services +:mod:`pcapkit` is built with: user-refined exceptions and +warnings, logging, a compatibility layer, and wrappers around +third-party libraries. It imports nothing from :mod:`pcapkit` +outside itself (:issue:`1712`). """ from pcapkit.utilities.chardet import detect -from pcapkit.utilities.decorators import beholder, prepare, seekset from pcapkit.utilities.exceptions import stacklevel from pcapkit.utilities.logging import configure, ensure_output, get_logger, logger, reset from pcapkit.utilities.warnings import warn __all__ = ['logger', 'get_logger', 'configure', 'reset', 'ensure_output', - 'warn', 'stacklevel', 'detect', - 'beholder', 'prepare', 'seekset'] + 'warn', 'stacklevel', 'detect'] diff --git a/pcapkit/utilities/exceptions.py b/pcapkit/utilities/exceptions.py index 3f627f0626..ed275758e1 100644 --- a/pcapkit/utilities/exceptions.py +++ b/pcapkit/utilities/exceptions.py @@ -654,7 +654,7 @@ def __init__(self, *args: 'Any', eof: 'bool' = False, **kwargs: 'Any') -> 'None' class StreamEOFError(BaseError, EOFError): """Underlying stream exhausted; no data left to read. - Raised by :func:`~pcapkit.utilities.decorators.prepare` when the *length* + Raised by :func:`~pcapkit.corekit.decorators.prepare` when the *length* of a schema's read was derived by measuring what is actually left in the stream -- rather than declared by the caller -- and that measurement came back zero. This is the frame reader's "no more packets" signal, so it @@ -666,7 +666,7 @@ class StreamEOFError(BaseError, EOFError): nothing to read -- is a different situation and does not raise this. Note: - :func:`~pcapkit.utilities.decorators.prepare` always raises this with + :func:`~pcapkit.corekit.decorators.prepare` always raises this with ``quiet=True``: reaching end of stream is the frame reader's ordinary way of finding out there is nothing left to parse, not a fault to log -- the same convention diff --git a/tests/_support.py b/tests/_support.py index 65266432f0..6973c991d3 100644 --- a/tests/_support.py +++ b/tests/_support.py @@ -434,8 +434,8 @@ def bootstrap_core_modules(test: 'Optional[unittest.TestCase]' = None) -> dict[s 'pcapkit/utilities/exceptions.py', test) warnings = load_module('pcapkit.utilities.warnings', 'pcapkit/utilities/warnings.py', test) multidict = load_module('pcapkit.corekit.multidict', 'pcapkit/corekit/multidict.py', test) - decorators = load_module('pcapkit.utilities.decorators', - 'pcapkit/utilities/decorators.py', test) + decorators = load_module('pcapkit.corekit.decorators', + 'pcapkit/corekit/decorators.py', test) protochain = load_module('pcapkit.corekit.protochain', 'pcapkit/corekit/protochain.py', test) return { diff --git a/tests/utilities/test_decorators.py b/tests/corekit/test_decorators.py similarity index 100% rename from tests/utilities/test_decorators.py rename to tests/corekit/test_decorators.py diff --git a/tests/corekit/test_decorators_path_1735_unit.py b/tests/corekit/test_decorators_path_1735_unit.py new file mode 100644 index 0000000000..e9a8b784b4 --- /dev/null +++ b/tests/corekit/test_decorators_path_1735_unit.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +"""The decorators live at :mod:`pcapkit.corekit.decorators`, and only there. + +GitHub issue #1735, applying #1712's placement rule. ``seekset``, ``beholder`` +and ``prepare`` act on a protocol's file and the schema it unpacks, and +``beholder`` falls back to protocol classes, so they moved from +``pcapkit.utilities.decorators`` to :mod:`pcapkit.corekit.decorators`. Per the +ruling on #1715 nothing is left at the old path, and :mod:`pcapkit.utilities` +no longer exports the three names. This pins both halves of that: the old path +and the old exports are gone, and every importer uses the new module. + +""" +import importlib +import importlib.util +import unittest + +from tests._support import reimport_once_per_class + +#: Where the decorators live. +NEW_PATH = 'pcapkit.corekit.decorators' + +#: Where they lived before #1735; removed, not re-exported. +OLD_PATH = 'pcapkit.utilities.decorators' + +#: The three decorators. +NAMES = ('seekset', 'beholder', 'prepare') + +#: ``(importing module, decorator it imports)``, for every module that does. +IMPORTERS = ( + ('pcapkit.protocols.protocol', 'seekset'), + ('pcapkit.protocols.protocol', 'beholder'), + ('pcapkit.protocols.internet.internet', 'beholder'), + ('pcapkit.protocols.internet.ipv6', 'beholder'), + ('pcapkit.protocols.schema.schema', 'prepare'), +) + + +class DecoratorsPathTests(unittest.TestCase): + """Pin the move of the decorators into :mod:`pcapkit.corekit`.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_the_old_module_path_no_longer_imports(self) -> None: + with self.assertRaises(ModuleNotFoundError) as ctx: + importlib.import_module(OLD_PATH) + self.assertEqual(ctx.exception.name, OLD_PATH) + self.assertIsNone(importlib.util.find_spec(OLD_PATH)) + + def test_utilities_no_longer_exports_the_decorators(self) -> None: + utilities = importlib.import_module('pcapkit.utilities') + self.assertFalse(hasattr(utilities, 'decorators')) + for name in NAMES: + with self.subTest(name=name): + self.assertNotIn(name, utilities.__all__) + self.assertFalse(hasattr(utilities, name)) + + def test_the_new_module_holds_the_decorators(self) -> None: + decorators = importlib.import_module(NEW_PATH) + self.assertEqual(decorators.__all__, list(NAMES)) + self.assertEqual(decorators.logger.name, NEW_PATH) + for name in NAMES: + with self.subTest(name=name): + self.assertEqual(getattr(decorators, name).__module__, NEW_PATH) + + def test_every_importer_uses_the_new_module(self) -> None: + decorators = importlib.import_module(NEW_PATH) + for module, name in IMPORTERS: + with self.subTest(module=module, name=name): + self.assertIs(getattr(importlib.import_module(module), name), + getattr(decorators, name)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/corekit/test_fields_misc.py b/tests/corekit/test_fields_misc.py index f74f34e997..70fce943f7 100644 --- a/tests/corekit/test_fields_misc.py +++ b/tests/corekit/test_fields_misc.py @@ -135,7 +135,7 @@ class SchemaFieldDefaultTests(unittest.TestCase): typed ``default: bytes`` constructor argument and unpacks it eagerly, via ``schema.unpack(default)`` -- a single positional argument. That call shape reached - :func:`~pcapkit.utilities.decorators.prepare`, which read ``length`` and + :func:`~pcapkit.corekit.decorators.prepare`, which read ``length`` and ``packet`` out of ``args[2]``/``args[3]`` unconditionally, so any call shorter than three positional arguments raised ``IndexError`` instead of falling back to the documented ``None`` default -- making diff --git a/tests/corekit/test_fields_misc_schema_length_1148_unit.py b/tests/corekit/test_fields_misc_schema_length_1148_unit.py index 786a16284b..e6192898c6 100644 --- a/tests/corekit/test_fields_misc_schema_length_1148_unit.py +++ b/tests/corekit/test_fields_misc_schema_length_1148_unit.py @@ -12,7 +12,7 @@ The nested schema is now told the true remainder of the buffer. A remainder of zero is passed as a declared length, so a truncated item is still reported as malformed data rather than as the quiet end-of-stream signal that -:func:`~pcapkit.utilities.decorators.prepare` raises for a derived zero. +:func:`~pcapkit.corekit.decorators.prepare` raises for a derived zero. """ import unittest diff --git a/tests/foundation/test_extraction_no_eof.py b/tests/foundation/test_extraction_no_eof.py index b0f3043809..6b66bbd08f 100644 --- a/tests/foundation/test_extraction_no_eof.py +++ b/tests/foundation/test_extraction_no_eof.py @@ -74,11 +74,11 @@ * It is not the parse path's ``except Exception`` handlers absorbing the ``SIGALRM``-raised :exc:`TimeoutError`. Those handlers exist -- in - :mod:`pcapkit.utilities.decorators` and + :mod:`pcapkit.corekit.decorators` and :meth:`ProtocolBase._decode_next_layer ` -- and :exc:`TimeoutError` is indeed an :exc:`Exception`, but neither is *entered* - during the spin: :func:`~pcapkit.utilities.decorators.prepare` raises end of + during the spin: :func:`~pcapkit.corekit.decorators.prepare` raises end of stream before any next-layer decode is reached, and a logger tap over the retries recorded nothing from either module. * The memory is not those handlers logging. It is the retry loop emitting tens of diff --git a/tests/project/test_all_exports_unit.py b/tests/project/test_all_exports_unit.py index d34a488c62..d1c269bef5 100644 --- a/tests/project/test_all_exports_unit.py +++ b/tests/project/test_all_exports_unit.py @@ -29,7 +29,7 @@ #: an API decision. DELIBERATE_NON_EXPORTS = { 'pcapkit.utilities': ('logger', 'get_logger', 'configure', 'reset', 'ensure_output', - 'warn', 'stacklevel', 'detect', 'beholder', 'prepare', 'seekset'), + 'warn', 'stacklevel', 'detect'), } diff --git a/tests/project/test_conventions_doc_claims.py b/tests/project/test_conventions_doc_claims.py index 36cd5ae7e0..e2b90ea467 100644 --- a/tests/project/test_conventions_doc_claims.py +++ b/tests/project/test_conventions_doc_claims.py @@ -108,9 +108,10 @@ #: lived only in that thread. ``protocol-layer-placement`` carries the other ruling #: that thread settled -- which subpackage a dissector belongs in, decided by designed #: function rather than by encapsulation -- and sits beside the other class-hierarchy -#: page. ``test-coverage`` came last, carrying GitHub issue #1203's rules for what the -#: test suite has to prove. Every anchor is the bare file stem, which is what -#: :data:`PAGES` below depends on. +#: page. ``test-coverage`` came next, carrying GitHub issue #1203's rules for what the +#: test suite has to prove, and ``utilities-or-corekit`` came last, carrying GitHub +#: issue #1712's rule for which of those two packages a module belongs in. Every +#: anchor is the bare file stem, which is what :data:`PAGES` below depends on. ANCHORS = ( 'mint-criterion', 'sentinel-convention', @@ -120,6 +121,7 @@ 'process', 'documentation', 'test-coverage', + 'utilities-or-corekit', ) #: Each anchor's own file, one-to-one since the split -- there is no longer a single diff --git a/tests/project/test_public_api.py b/tests/project/test_public_api.py index 1e1cc04b2a..60d3cb42f4 100644 --- a/tests/project/test_public_api.py +++ b/tests/project/test_public_api.py @@ -79,8 +79,9 @@ :mod:`pcapkit.foundation` withheld while listing the other 27 registry functions, ``ReassemblyManager`` and ``TraceFlowManager``, which are defined *below* the :attr:`__all__` literal that forgot them and imported by name from six modules, and -the three decorators :mod:`pcapkit.utilities` advertises in its own module -docstring. The 22 that remain are listed with their reasons, and +the three decorators :mod:`pcapkit.utilities` then advertised in its own module +docstring (since moved to :mod:`pcapkit.corekit.decorators` and dropped from that +package by #1735). The 22 that remain are listed with their reasons, and :meth:`~PublicAPISurfaceTests.test_the_non_export_allowlist_is_tight` stops the list rotting into a silencer: every entry has to name a package that exists, an attribute that package really holds, and a name its :attr:`__all__` really omits, diff --git a/tests/project/test_utilities_import_boundary_1735_unit.py b/tests/project/test_utilities_import_boundary_1735_unit.py new file mode 100644 index 0000000000..b2c2638df0 --- /dev/null +++ b/tests/project/test_utilities_import_boundary_1735_unit.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +"""Nothing under :mod:`pcapkit.utilities` imports :mod:`pcapkit` outside it. + +GitHub issue #1712 settled which of :mod:`pcapkit.utilities` and +:mod:`pcapkit.corekit` a module belongs in: ``utilities`` holds cross-cutting +services, and a service imports nothing from :mod:`pcapkit` outside +``utilities`` -- not at top level, not inside a function, and not under +``TYPE_CHECKING``. ``pcapkit.utilities.decorators`` broke that, reaching +:mod:`pcapkit.protocols` both under ``TYPE_CHECKING`` and lazily inside +``beholder``, and #1735 moved it to :mod:`pcapkit.corekit.decorators`. + +The check reads each module with :mod:`ast` rather than importing it, so a lazy +import is seen whether or not anything ever calls the function that holds it. +Relative imports are resolved against the module's own package, and a literal +:func:`importlib.import_module` or :func:`__import__` call counts as an import. + +The rule is written down on +:file:`docs/source/contributing/conventions/utilities-or-corekit.rst`. + +This module is unit-tier and reads no capture. + +""" +from __future__ import annotations + +import ast +import pathlib +import unittest + +#: Repository root. +ROOT = pathlib.Path(__file__).resolve().parents[2] + +#: The package the rule governs. +PACKAGE = 'pcapkit.utilities' + +#: Its directory. +UTILITIES = ROOT / 'pcapkit' / 'utilities' + +#: Call names that import a module named by their first argument. +DYNAMIC_IMPORTERS = frozenset({'import_module', '__import__'}) + + +def _module_name(path: 'pathlib.Path') -> 'str': + """Dotted name of ``path``, a source file under :data:`ROOT`.""" + parts = list(path.relative_to(ROOT).with_suffix('').parts) + if parts[-1] == '__init__': + parts.pop() + return '.'.join(parts) + + +def _relative_base(module: 'str', is_package: 'bool', level: 'int') -> 'str': + """The package a ``from`` import with ``level`` leading dots names.""" + package = module.split('.') if is_package else module.split('.')[:-1] + return '.'.join(package[:len(package) - (level - 1)]) + + +def imported_names(source: 'str', module: 'str', is_package: 'bool' = False + ) -> 'list[tuple[int, str]]': + """Every module ``source`` imports, as ``(line, dotted name)`` in line order. + + ``from X import a, b`` yields ``X``, except that ``from pcapkit import a`` + yields ``pcapkit.a``, so ``from pcapkit import utilities`` is told apart from + ``from pcapkit import protocols``. + + Args: + source: Python source to read. + module: Dotted name of the module ``source`` belongs to, for resolving + relative imports. + is_package: Whether ``source`` is that package's :file:`__init__.py`. + + """ + found = [] # type: list[tuple[int, str]] + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.Import): + found.extend((node.lineno, alias.name) for alias in node.names) + elif isinstance(node, ast.ImportFrom): + if node.level: + base = _relative_base(module, is_package, node.level) + target = f'{base}.{node.module}' if node.module else base + else: + target = node.module or '' + if target == 'pcapkit' or not node.module: + found.extend((node.lineno, f'{target}.{alias.name}') for alias in node.names) + else: + found.append((node.lineno, target)) + elif isinstance(node, ast.Call) and node.args: + func = node.func + name = func.attr if isinstance(func, ast.Attribute) else getattr(func, 'id', None) + first = node.args[0] + if (name in DYNAMIC_IMPORTERS and isinstance(first, ast.Constant) + and isinstance(first.value, str)): + found.append((node.lineno, first.value)) + # ``ast.walk`` is breadth-first, so a nested import would otherwise be + # reported after every top-level one. + return sorted(found) + + +def crosses_the_boundary(name: 'str') -> 'bool': + """Whether importing ``name`` reaches :mod:`pcapkit` outside :data:`PACKAGE`.""" + if name == PACKAGE or name.startswith(f'{PACKAGE}.'): + return False + return name == 'pcapkit' or name.startswith('pcapkit.') + + +def violations(root: 'pathlib.Path' = UTILITIES) -> 'list[str]': + """Every import under ``root`` that crosses the boundary, as ``path:line: name``.""" + found = [] # type: list[str] + for path in sorted(root.rglob('*.py')): + source = path.read_text(encoding='utf-8') + for line, name in imported_names(source, _module_name(path), + is_package=path.name == '__init__.py'): + if crosses_the_boundary(name): + found.append(f'{path.relative_to(ROOT).as_posix()}:{line}: {name}') + return found + + +class UtilitiesImportBoundaryTests(unittest.TestCase): + """:mod:`pcapkit.utilities` stays below every other :mod:`pcapkit` package.""" + + def test_the_walk_found_the_package(self) -> None: + names = {path.name for path in UTILITIES.rglob('*.py')} + self.assertIn('__init__.py', names) + self.assertIn('exceptions.py', names) + + def test_no_module_imports_pcapkit_outside_utilities(self) -> None: + found = violations() + self.assertEqual(found, [], + 'these modules under pcapkit/utilities/ import pcapkit from ' + 'outside pcapkit.utilities, which #1712 rules out at top level, ' + 'inside a function and under TYPE_CHECKING alike; a module that ' + 'needs them belongs in pcapkit.corekit:\n' + '\n'.join(found)) + + def test_the_scanner_sees_every_form_of_import(self) -> None: + source = '\n'.join([ + 'from typing import TYPE_CHECKING', # 1 + 'import pcapkit.utilities.logging', # 2 + 'from pcapkit.utilities.compat import final', # 3 + 'from . import warnings', # 4 + 'from pcapkit import utilities', # 5 + 'if TYPE_CHECKING:', # 6 + ' from pcapkit.protocols.protocol import P', # 7 + 'def f():', # 8 + ' from pcapkit.protocols.misc.raw import Raw', # 9 + ' import pcapkit.const', # 10 + ' from ..corekit import fields', # 11 + ' from .. import protocols', # 12 + ' from pcapkit import __version__', # 13 + ' importlib.import_module("pcapkit.foundation")', # 14 + ' __import__("pcapkit.dumpkit")', # 15 + 'import pcapkit', # 16 + ]) + crossing = [(line, name) + for line, name in imported_names(source, 'pcapkit.utilities.demo') + if crosses_the_boundary(name)] + self.assertEqual(crossing, [ + (7, 'pcapkit.protocols.protocol'), + (9, 'pcapkit.protocols.misc.raw'), + (10, 'pcapkit.const'), + (11, 'pcapkit.corekit'), + (12, 'pcapkit.protocols'), + (13, 'pcapkit.__version__'), + (14, 'pcapkit.foundation'), + (15, 'pcapkit.dumpkit'), + (16, 'pcapkit'), + ]) + + def test_relative_imports_resolve_from_the_package_itself(self) -> None: + source = 'from . import compat\nfrom .. import protocols\n' + self.assertEqual(imported_names(source, PACKAGE, is_package=True), + [(1, 'pcapkit.utilities.compat'), (2, 'pcapkit.protocols')]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/application/test_ngap_unit.py b/tests/protocols/application/test_ngap_unit.py index c9dd508d08..bd23415655 100644 --- a/tests/protocols/application/test_ngap_unit.py +++ b/tests/protocols/application/test_ngap_unit.py @@ -308,7 +308,7 @@ def test_sctp_payload_degrades_to_raw_without_pycrate(self) -> None: This is the behaviour the optional dependency buys its optionality with: the payload reaches :class:`~pcapkit.protocols.misc.raw.Raw` through - :func:`~pcapkit.utilities.decorators.beholder` rather than aborting the + :func:`~pcapkit.corekit.decorators.beholder` rather than aborting the frame, and keeps the PPID's name while doing so. """ diff --git a/tests/protocols/internet/test_ipv6_ext_chain_limit_1604_unit.py b/tests/protocols/internet/test_ipv6_ext_chain_limit_1604_unit.py index c9c6f08fd2..d57dd8b28b 100644 --- a/tests/protocols/internet/test_ipv6_ext_chain_limit_1604_unit.py +++ b/tests/protocols/internet/test_ipv6_ext_chain_limit_1604_unit.py @@ -3,7 +3,7 @@ GitHub issue #1604: inside IPv4, each IPv6 extension header dissects the next one itself, so a long chain ran into Python's recursion limit, and -:func:`~pcapkit.utilities.decorators.beholder` caught the ``RecursionError`` +:func:`~pcapkit.corekit.decorators.beholder` caught the ``RecursionError`` and left the rest raw, silently. Where that happened depended on the interpreter and the stack -- after 79 headers on Python 3.11, and between 85 and 90 on 3.14, from a script at the default limit -- and IPv6, which walks its diff --git a/tests/protocols/internet/test_ipv6_exthdr_cut_rebuild_unit.py b/tests/protocols/internet/test_ipv6_exthdr_cut_rebuild_unit.py index 7b268426c5..f1f169a6d8 100644 --- a/tests/protocols/internet/test_ipv6_exthdr_cut_rebuild_unit.py +++ b/tests/protocols/internet/test_ipv6_exthdr_cut_rebuild_unit.py @@ -3,7 +3,7 @@ GitHub issue #1471: an upper layer cut short after an extension header fell back to :class:`~pcapkit.protocols.misc.raw.Raw` over the *whole* IPv6 payload, -since :func:`~pcapkit.utilities.decorators.beholder` ignored the ``payload`` it +since :func:`~pcapkit.corekit.decorators.beholder` ignored the ``payload`` it was handed, so the rebuild wrote the extension headers twice -- Destination Options, TCP cut at 57 octets rebuilt 65. diff --git a/tests/protocols/link/test_l2tp_version_unit.py b/tests/protocols/link/test_l2tp_version_unit.py index c5a7078f2b..d124959328 100644 --- a/tests/protocols/link/test_l2tp_version_unit.py +++ b/tests/protocols/link/test_l2tp_version_unit.py @@ -227,7 +227,7 @@ def test_l2tpv3_over_ip_degrades_to_raw_rather_than_a_fabricated_header(self) -> self.assertNotIn('l2tp', ipv4_info) self.assertNotIn('L2TPv2', str(frame.protochain)) - # It degrades through :func:`~pcapkit.utilities.decorators.beholder`, + # It degrades through :func:`~pcapkit.corekit.decorators.beholder`, # which re-parses as ``Raw`` and passes ``alias=proto`` -- so an # enumeration key renders under its own name rather than as ``Raw``, # per that function's own note. The payload is preserved verbatim and diff --git a/tests/protocols/misc/test_raw_empty_bytes_1285_unit.py b/tests/protocols/misc/test_raw_empty_bytes_1285_unit.py index 28f3ac27d8..cf3683c676 100644 --- a/tests/protocols/misc/test_raw_empty_bytes_1285_unit.py +++ b/tests/protocols/misc/test_raw_empty_bytes_1285_unit.py @@ -2,7 +2,7 @@ """``Raw`` accepts an empty byte string on the parsing path too. GitHub issue #1285: ``Raw(b"")`` left the length for -:func:`~pcapkit.utilities.decorators.prepare` to derive, and a derived zero +:func:`~pcapkit.corekit.decorators.prepare` to derive, and a derived zero is the end-of-stream signal, so it raised :exc:`~pcapkit.utilities.exceptions.StreamEOFError` while ``Raw(packet=b"")`` worked. diff --git a/tests/protocols/test_frame_layer_limit_1610_unit.py b/tests/protocols/test_frame_layer_limit_1610_unit.py index af403f7be0..af05a6832a 100644 --- a/tests/protocols/test_frame_layer_limit_1610_unit.py +++ b/tests/protocols/test_frame_layer_limit_1610_unit.py @@ -3,7 +3,7 @@ GitHub issue #1610: each layer dissects the next one from inside its own parse, so deep nesting of any protocol ran into Python's recursion limit, and -:func:`~pcapkit.utilities.decorators.beholder` caught the ``RecursionError`` and +:func:`~pcapkit.corekit.decorators.beholder` caught the ``RecursionError`` and left the rest raw, silently. From a script on Python 3.14, TCP was lost behind 108 IPv4 headers in IPv4, or behind 200 stacked 802.1Q tags. #1604 bounded one chain of extension headers, but its count restarts at every tunnel. diff --git a/tests/protocols/transport/test_sctp_unit.py b/tests/protocols/transport/test_sctp_unit.py index 45d40dee2c..2c3ee5cc27 100644 --- a/tests/protocols/transport/test_sctp_unit.py +++ b/tests/protocols/transport/test_sctp_unit.py @@ -898,7 +898,7 @@ def test_ppid_dispatch_hook(self) -> None: ``b'ngap-pdu'`` is not an aligned PER ``NGAP-PDU``, which is the point: the failure has to reach :class:`Raw` through - :func:`~pcapkit.utilities.decorators.beholder` rather than escape, and + :func:`~pcapkit.corekit.decorators.beholder` rather than escape, and the payload has to keep the PPID's name while doing so. For PPID 60 that path is also what a capture parsed *without* ``pycrate`` installed takes on every NGAP packet. diff --git a/tests/utilities/test_logging.py b/tests/utilities/test_logging.py index bfe1dc3a37..4daf8e395d 100644 --- a/tests/utilities/test_logging.py +++ b/tests/utilities/test_logging.py @@ -224,7 +224,7 @@ def test_modules_log_through_their_own_child_logger(self) -> None: 'pcapkit.foundation.engines.pcapng': 'pcapkit.foundation.engines.pcapng', 'pcapkit.utilities.exceptions': 'pcapkit.utilities.exceptions', 'pcapkit.utilities.warnings': 'pcapkit.utilities.warnings', - 'pcapkit.utilities.decorators': 'pcapkit.utilities.decorators', + 'pcapkit.corekit.decorators': 'pcapkit.corekit.decorators', 'pcapkit.dumpkit.common': 'pcapkit.dumpkit.common', } for module_name, expected in cases.items(): diff --git a/tests/utilities/test_quiet_exceptions.py b/tests/utilities/test_quiet_exceptions.py index f199a6e8a2..2b37353710 100644 --- a/tests/utilities/test_quiet_exceptions.py +++ b/tests/utilities/test_quiet_exceptions.py @@ -149,7 +149,7 @@ def test_quiet_error_leaves_tracebacklimit_alone(self) -> None: def test_prepare_raises_stream_eof_error_quietly(self) -> None: """End of stream is control flow, so ``prepare`` raises it silently. - :func:`~pcapkit.utilities.decorators.prepare` passes ``quiet=True`` + :func:`~pcapkit.corekit.decorators.prepare` passes ``quiet=True`` when a *measured* -- as opposed to caller-declared -- read length comes back zero, because that is how the frame reader learns a capture is exhausted rather than a fault worth a log record. It is the same