From 832662c06f62f76c75965f66b366e51ddcda996b Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Thu, 15 Oct 2026 11:35:42 -0400 Subject: [PATCH] docs(foundation): add 1.5.0 version directives and fix stale prose (#1103) Version directives (38, all 1.5.0, citing issues rather than PRs): - versionchanged on Extractor, extract(), reassemble(), trace() and follow_tcp_stream() for the new parameters, timeout and tracing mode; on Engine, Reassembly, TraceFlow and Dumper for opt-in registration; on the IP, IPv6 and TCP reassemblers, make_dumper, PCAPIO, register_apptype, register_mh_extension, the dpkt reassembly adapters and pyshark tcp_traceflow. - logging, warnings and BaseError: import-time handler, one report per channel, silent quiet errors; three "up to v1.4.1" page admonitions become the matching directives. - versionadded on HoleDescriptor, Deferred, EnumKeyError and four logging functions; versionremoved for HoleDiscriptor, utilities.decorators and utilities.warnings.DEVMODE. Accuracy: traceflow TCP drops *args/**kwargs it does not take; return types and reassemble()'s strict match the code; follow_tcp_stream's PyShark note; a PR citation becomes its issue. Render: raw ``literals`` in two literal blocks, unresolved FileNotFound references, a duplicate BufferID entry. Prose: timed wording on the logging pages; grammar in the reassembly and IP docstrings. Refs #1103 --- .../foundation/reassembly/reassembly.rst | 6 ++-- .../pcapkit/foundation/reassembly/tcp.rst | 3 +- docs/source/pcapkit/utilities/exceptions.rst | 19 +++++----- docs/source/pcapkit/utilities/logging.rst | 16 ++++++--- docs/source/pcapkit/utilities/warnings.rst | 21 ++++++----- pcapkit/dumpkit/common.py | 20 +++++++++++ pcapkit/dumpkit/pcap.py | 7 ++++ pcapkit/foundation/engines/engine.py | 7 ++++ pcapkit/foundation/extraction.py | 16 +++++++-- pcapkit/foundation/reassembly/__init__.py | 2 +- pcapkit/foundation/reassembly/data/data.py | 2 ++ pcapkit/foundation/reassembly/data/tcp.py | 10 ++++++ pcapkit/foundation/reassembly/ip.py | 19 +++++++--- pcapkit/foundation/reassembly/ipv6.py | 7 ++++ pcapkit/foundation/reassembly/reassembly.py | 11 ++++-- pcapkit/foundation/reassembly/tcp.py | 8 +++++ pcapkit/foundation/registry/protocols.py | 10 ++++++ pcapkit/foundation/traceflow/tcp.py | 2 -- pcapkit/foundation/traceflow/traceflow.py | 8 +++++ pcapkit/interface/core.py | 30 +++++++++++++--- pcapkit/interface/misc.py | 22 ++++++++---- pcapkit/toolkit/dpkt.py | 15 ++++++++ pcapkit/toolkit/pyshark.py | 15 ++++++-- pcapkit/utilities/__init__.py | 6 ++++ pcapkit/utilities/exceptions.py | 11 ++++-- pcapkit/utilities/logging.py | 18 ++++++++-- pcapkit/utilities/warnings.py | 35 +++++++++++++------ 27 files changed, 278 insertions(+), 68 deletions(-) diff --git a/docs/source/pcapkit/foundation/reassembly/reassembly.rst b/docs/source/pcapkit/foundation/reassembly/reassembly.rst index 0984e084fc..aabb7e59a9 100644 --- a/docs/source/pcapkit/foundation/reassembly/reassembly.rst +++ b/docs/source/pcapkit/foundation/reassembly/reassembly.rst @@ -6,8 +6,8 @@ Base Class :mod:`pcapkit.foundation.reassembly.reassembly` contains :class:`~pcapkit.foundation.reassembly.reassembly.Reassembly` only, which is an abstract base class for all reassembly classes, -bases on algorithms described in :rfc:`791` and :rfc:`815`, -implements datagram reassembly of IP and TCP packets. +based on algorithms described in :rfc:`791` and :rfc:`815`, +that implement datagram reassembly of IP and TCP packets. .. autoclass:: pcapkit.foundation.reassembly.reassembly.Reassembly :no-members: @@ -30,7 +30,7 @@ implements datagram reassembly of IP and TCP packets. .. property:: protocol - :type: typing.Type[Protocol] + :type: typing.Type[pcapkit.protocols.protocol.ProtocolBase] Protocol of current reassembly class. diff --git a/docs/source/pcapkit/foundation/reassembly/tcp.rst b/docs/source/pcapkit/foundation/reassembly/tcp.rst index 0ed79aab30..fb20467ae4 100644 --- a/docs/source/pcapkit/foundation/reassembly/tcp.rst +++ b/docs/source/pcapkit/foundation/reassembly/tcp.rst @@ -123,7 +123,7 @@ Algorithm } give up until (no entry from HDL) } -The following algorithm implement is based on the **IP Datagram +The following algorithm implementation is based on the **IP Datagram Reassembly Algorithm** introduced in :rfc:`815`, which presents hole descriptors as an alternative to the ``RCVBT`` (fragment received bit table) approach used by the reassembly procedure in :rfc:`791`. And @@ -378,5 +378,6 @@ Type Variables .. data:: pcapkit.foundation.reassembly.data.tcp.BufferID :type: typing.Tuple[_AT, int, _AT, int] + :no-index: Buffer ID data structure. diff --git a/docs/source/pcapkit/utilities/exceptions.rst b/docs/source/pcapkit/utilities/exceptions.rst index 02cb357b91..1ad0943ad3 100644 --- a/docs/source/pcapkit/utilities/exceptions.rst +++ b/docs/source/pcapkit/utilities/exceptions.rst @@ -28,16 +28,15 @@ Such an error emits nothing on any channel and touches no process-global state. It is still an ordinary exception carrying its message, so ``except`` clauses and :func:`repr` are unaffected. -.. attention:: - - Up to and including v1.4.1, ``quiet=True`` logged at ``ERROR`` instead of - ``CRITICAL`` rather than not logging, and :data:`sys.tracebacklimit` was set - on both paths. A single ``MultiDict.get()`` miss therefore produced an - ``ERROR`` record -- one per frame when parsing unfragmented IPv6 with - reassembly enabled -- and truncated the tracebacks of unrelated exceptions - for the rest of the process. Those records never corresponded to a fault and - are gone; to observe internal lookup misses, catch the exception rather than - read the log. +.. versionchanged:: 1.5.0 + + In 1.4.x, ``quiet=True`` logged at ``ERROR`` instead of ``CRITICAL`` rather + than not logging, and :data:`sys.tracebacklimit` was set on both paths. A + single ``MultiDict.get()`` miss therefore produced an ``ERROR`` record -- one + per frame when parsing unfragmented IPv6 with reassembly enabled -- and + truncated the tracebacks of unrelated exceptions for the rest of the process. + Those records never corresponded to a fault; to observe internal lookup + misses, catch the exception rather than read the log (:issue:`362`). .. autoexception:: pcapkit.utilities.exceptions.BaseError :no-members: diff --git a/docs/source/pcapkit/utilities/logging.rst b/docs/source/pcapkit/utilities/logging.rst index e07abf971f..2041d88198 100644 --- a/docs/source/pcapkit/utilities/logging.rst +++ b/docs/source/pcapkit/utilities/logging.rst @@ -8,6 +8,12 @@ Logging System at :data:`~pcapkit.utilities.logging.logger`, and the API through which an application decides what, if anything, :mod:`pcapkit` emits. +.. versionchanged:: 1.5.0 + + Importing :mod:`pcapkit` attaches only a :class:`logging.NullHandler` and + sets no level; 1.4.x attached a :obj:`sys.stderr` handler at + :data:`logging.INFO`. + .. autodata:: pcapkit.utilities.logging.logger :no-value: @@ -142,9 +148,9 @@ Compatibility Note Before 1.5.0, importing :mod:`pcapkit` attached a :class:`logging.StreamHandler` on :obj:`sys.stderr` and forced the level to :data:`logging.INFO` (or :data:`logging.DEBUG` under - :envvar:`PCAPKIT_DEVMODE`). It no longer does, because that hijacked the - logging configuration of every application importing :mod:`pcapkit`. Two - consequences are visible to existing code: + :envvar:`PCAPKIT_DEVMODE`). That hijacked the logging configuration of every + application importing :mod:`pcapkit`, so 1.5.0 does it only under + :envvar:`PCAPKIT_DEVMODE`. Two consequences are visible to existing code: 1. **Nothing reaches stderr by default.** The ``registered ...`` bookkeeping is at :data:`logging.DEBUG`, not :data:`logging.INFO`. To restore the @@ -156,8 +162,8 @@ Compatibility Note from pcapkit.utilities.logging import configure configure(logging.INFO, stream=sys.stderr) - Equivalently, re-attach the module's own handler, which is still built - with the historical format: + Equivalently, re-attach the module's own handler, which is built with the + 1.4.x format: .. code-block:: python diff --git a/docs/source/pcapkit/utilities/warnings.rst b/docs/source/pcapkit/utilities/warnings.rst index 6d613ce05c..3cd14f4cd6 100644 --- a/docs/source/pcapkit/utilities/warnings.rst +++ b/docs/source/pcapkit/utilities/warnings.rst @@ -5,6 +5,12 @@ User Defined Warnings :mod:`pcapkit.utilities.warnings` refined built-in warnings. +.. versionremoved:: 1.5.0 + + ``pcapkit.utilities.warnings.DEVMODE``, which 1.4.x imported from + :mod:`pcapkit.utilities.logging`; read + :data:`pcapkit.utilities.logging.DEVMODE` instead. + How a Warning Is Reported ------------------------- @@ -79,16 +85,15 @@ configured separately, through the :mod:`logging` module:: logging.getLogger('pcapkit').setLevel(logging.ERROR) # drop WARNING records -.. attention:: +.. versionchanged:: 1.5.0 - Up to and including v1.4.1, constructing a + :mod:`pcapkit` warnings reach the :mod:`warnings` channel by default, and + the first snippet above restores the 1.4.x quiet. In 1.4.x, constructing a :class:`~pcapkit.utilities.warnings.BaseWarning` installed an ``ignore`` - filter for its own class, so :mod:`pcapkit` warnings were invisible on the - :mod:`warnings` channel by default and a caller could not re-enable them. - They are now delivered; the first snippet above restores the old quiet. The - filter went at the *front* of the process-global :data:`warnings.filters`, so - it also overrode the host application's own configuration for those - categories and made unrelated warnings re-fire. + filter for its own class, so a caller could not re-enable them. The filter + went at the *front* of the process-global :data:`warnings.filters`, so it + also overrode the host application's own configuration for those categories + and made unrelated warnings re-fire (:issue:`364`). .. autoexception:: pcapkit.utilities.warnings.BaseWarning :no-members: diff --git a/pcapkit/dumpkit/common.py b/pcapkit/dumpkit/common.py index 59a1d1637f..815ceaf614 100644 --- a/pcapkit/dumpkit/common.py +++ b/pcapkit/dumpkit/common.py @@ -115,6 +115,13 @@ class MyMixin(Dumper): # not registered register_dumper('my_mixin', MyMixin, '.mine') + .. versionchanged:: 1.5.0 + + Registration is opt-in, under the ``fmt`` class keyword; 1.4.x + registered every subclass, under its + :attr:`~dictdumper.dumper.Dumper.kind` when the keyword was omitted + (:issue:`514`). + """ def __init_subclass__(cls, /, fmt: 'Optional[str]' = None, @@ -307,6 +314,19 @@ def make_dumper(output: 'Type[ABCDumper]') -> 'Type[ABCDumper]': Returns: Customised :class:`~dictdumper.dumper.Dumper` object. + .. versionchanged:: 1.5.0 + + Several dumped shapes change. A nameless flag value renders its value + as the name, ``Flags::0 [0]`` rather than ``Flags::None [0]`` + (:issue:`648`). An :class:`~pcapkit.corekit.multidict.OrderedMultiDict` + dumps as a list of one-key mappings in wire order, not grouped by key + (:issue:`1263`). A timezone dumps as its offset in seconds, and + :data:`~pcapkit.corekit.sentinels.NULL` and + :data:`~pcapkit.corekit.sentinels.NO_VALUE` as ``''`` and + ``''`` (:issue:`1258`). In ``plist`` output, :data:`None` is + kept (:issue:`1257`), and a string carrying a character XML forbids + is written as ```` (:issue:`1262`). + """ # NOTE: :class:`~dictdumper.plist.PLIST` -- which is also what # :attr:`Extractor.__output__ ` diff --git a/pcapkit/dumpkit/pcap.py b/pcapkit/dumpkit/pcap.py index e0e80786e7..40e22d49b6 100644 --- a/pcapkit/dumpkit/pcap.py +++ b/pcapkit/dumpkit/pcap.py @@ -80,6 +80,13 @@ class PCAPIO(DumperBase): **kwargs: further global header fields, i.e. ``version``, ``thiszone``, ``sigfigs`` and ``snaplen``, and arbitrary keyword arguments + .. versionchanged:: 1.5.0 + + The global header takes ``version``, ``thiszone``, ``sigfigs`` and + ``snaplen`` from ``**kwargs``, so ``format='pcap'`` output keeps the + input's values; 1.4.x wrote the defaults (:issue:`1165`, + :issue:`1255`). + """ if TYPE_CHECKING: #: PCAP file global header. diff --git a/pcapkit/foundation/engines/engine.py b/pcapkit/foundation/engines/engine.py index ea23b28ef8..00a0db9ed2 100644 --- a/pcapkit/foundation/engines/engine.py +++ b/pcapkit/foundation/engines/engine.py @@ -240,6 +240,13 @@ class MyMixin(Engine): # not registered Args: extractor: :class:`~pcapkit.foundation.extraction.Extractor` instance. + .. versionchanged:: 1.5.0 + + Registration is opt-in, under the ``engine`` class keyword, which + replaces 1.4.x's ``name`` with no alias; 1.4.x registered every + subclass, under its class name when ``name`` was omitted + (:issue:`514`). + """ def __init_subclass__(cls, /, engine: 'Optional[str]' = None, *args: 'Any', **kwargs: 'Any') -> 'None': diff --git a/pcapkit/foundation/extraction.py b/pcapkit/foundation/extraction.py index 51e138e9a9..4efd5284e7 100644 --- a/pcapkit/foundation/extraction.py +++ b/pcapkit/foundation/extraction.py @@ -10,7 +10,7 @@ :class:`~pcapkit.foundation.extraction.Extractor` only, which synthesises file I/O and protocol analysis, coordinates information exchange in all network layers, -extracts parametres from a PCAP file. +extracts parameters from a PCAP file. """ import collections @@ -108,6 +108,18 @@ class Extractor(Generic[_P]): For supported engines, please refer to :meth:`~pcapkit.foundation.extraction.Extractor.run`. + .. versionchanged:: 1.5.0 + + ``layer`` and ``protocol`` stop the dissection where they say, which + 1.4.x ignored (:issue:`356`). A stream given as ``fin`` is left open: + only a file the extractor opened itself is closed (:issue:`610`). + ``reasm_timeout``, ``trace_bidirectional`` and ``trace_analyse`` are + inserted among the positional parameters and ``context`` is appended, + so a positional argument after ``reasm_store`` has to move to a + keyword. Flows are traced bidirectionally by default. :attr:`engine` + is typed :class:`~pcapkit.foundation.engines.engine.EngineBase`, the + base every engine derives from (:issue:`1022`). + """ if TYPE_CHECKING: #: Input file name. @@ -1075,7 +1087,7 @@ def __init__(self, Args: fin: file name to be read or a binary IO object; - if file not exist, raise :exc:`FileNotFound` + if file not exist, raise :exc:`~pcapkit.utilities.exceptions.FileNotFound` fout: file name to be written format: file format of output diff --git a/pcapkit/foundation/reassembly/__init__.py b/pcapkit/foundation/reassembly/__init__.py index 302edf2c91..4089d68e68 100644 --- a/pcapkit/foundation/reassembly/__init__.py +++ b/pcapkit/foundation/reassembly/__init__.py @@ -5,7 +5,7 @@ .. module:: pcapkit.foundation.reassembly -:mod:`pcapkit.foundation.reassembly` bases on algorithms described +:mod:`pcapkit.foundation.reassembly`, based on algorithms described in :rfc:`791` and :rfc:`815`, implements datagram reassembly of IP and TCP packets. diff --git a/pcapkit/foundation/reassembly/data/data.py b/pcapkit/foundation/reassembly/data/data.py index 89380cb665..cf60a3f122 100644 --- a/pcapkit/foundation/reassembly/data/data.py +++ b/pcapkit/foundation/reassembly/data/data.py @@ -135,6 +135,8 @@ class Deferred: proto: Payload protocol type. payload: Reassembled payload to parse. + .. versionadded:: 1.5.0 + """ __slots__ = ('analyze', 'proto', 'payload') diff --git a/pcapkit/foundation/reassembly/data/tcp.py b/pcapkit/foundation/reassembly/data/tcp.py index b4e1ee53f6..d84a8a23ee 100644 --- a/pcapkit/foundation/reassembly/data/tcp.py +++ b/pcapkit/foundation/reassembly/data/tcp.py @@ -149,6 +149,16 @@ class HoleDescriptor(Info): whole; a hole whose ``first`` is ``-sys.maxsize`` stands for whatever preceded a capture that missed the handshake, and does not count. + .. versionadded:: 1.5.0 + + Replaces the misspelt ``HoleDiscriptor``. + + .. versionremoved:: 1.5.0 + + ``HoleDiscriptor``, and its re-export + ``pcapkit.foundation.reassembly.data.TCP_HoleDiscriptor``, with no + alias; import ``HoleDescriptor`` and ``TCP_HoleDescriptor`` instead. + """ #: Sequence number of the first missing octet. diff --git a/pcapkit/foundation/reassembly/ip.py b/pcapkit/foundation/reassembly/ip.py index fb8a0874df..483972aaff 100644 --- a/pcapkit/foundation/reassembly/ip.py +++ b/pcapkit/foundation/reassembly/ip.py @@ -7,11 +7,11 @@ :mod:`pcapkit.foundation.reassembly.ip` contains :class:`~pcapkit.foundation.reassembly.ip.IP` only, which reconstructs fragmented IP packets back to -origin. The following algorithm implement is based on IP -reassembly procedure introduced in :rfc:`791`, using -``RCVBT`` (fragment received bit table). Though another -algorithm is explained in :rfc:`815`, replacing ``RCVBT``, -however, this implement still used the elder one. +origin. The implementation follows the IP reassembly +procedure introduced in :rfc:`791`, using ``RCVBT`` +(fragment received bit table). :rfc:`815` explains another +algorithm, replacing ``RCVBT``, but this implementation +uses the elder one. """ from typing import TYPE_CHECKING, Generic @@ -57,6 +57,15 @@ class IP(ReassemblyBase[Packet[_AT], Datagram[_AT], BufferID, Buffer[_AT]], Gene but rather used as a base class for the protocol-aware reassembly classes. + .. versionchanged:: 1.5.0 + + An overlapping fragment that disagrees with octets already received is + recorded in :attr:`Datagram.conflict + ` rather than + overwriting them unnoticed (:issue:`477`). An incomplete buffer expires + after ``timeout`` seconds of capture time, 60 by default for both IPv4 + and IPv6. + """ if TYPE_CHECKING: protocol: 'Type[IP_Protocol]' diff --git a/pcapkit/foundation/reassembly/ipv6.py b/pcapkit/foundation/reassembly/ipv6.py index bfa02d15e7..0fe93661bd 100644 --- a/pcapkit/foundation/reassembly/ipv6.py +++ b/pcapkit/foundation/reassembly/ipv6.py @@ -87,6 +87,13 @@ class IPv6(IP): # Fetch result: >>> result = ipv6_reassembly.datagram + .. versionchanged:: 1.5.0 + + The reassembled ``header`` leaves out the Fragment header and carries + its Next Header, per :rfc:`8200#section-4.5`, whichever engine fed it; + 1.4.x kept the Fragment header on the ``default`` and ``pcapng`` + engines (:issue:`415`). + """ ########################################################################## diff --git a/pcapkit/foundation/reassembly/reassembly.py b/pcapkit/foundation/reassembly/reassembly.py index 35ef8de0d4..9336211b03 100644 --- a/pcapkit/foundation/reassembly/reassembly.py +++ b/pcapkit/foundation/reassembly/reassembly.py @@ -7,8 +7,8 @@ :mod:`pcapkit.foundation.reassembly.reassembly` contains :class:`~pcapkit.foundation.reassembly.reassembly.Reassembly` only, which is an abstract base class for all reassembly classes, -bases on algorithms described in :rfc:`791` and :rfc:`815`, -implements datagram reassembly of IP and TCP packets. +based on algorithms described in :rfc:`791` and :rfc:`815`, +that implement datagram reassembly of IP and TCP packets. """ import abc @@ -541,6 +541,13 @@ class MyMixin(Reassembly): # not registered timeout: reassembly timeout in seconds, on the capture's own clock; :data:`None` selects the protocol's :attr:`__timeout__` default + .. versionchanged:: 1.5.0 + + Registration is opt-in, under the ``protocol`` class keyword; 1.4.x + registered every subclass, under its :attr:`name` when the keyword was + omitted (:issue:`514`). The ``timeout`` argument is added: a buffer + older than it on the capture's clock is abandoned. + """ def __init_subclass__(cls, /, protocol: 'Optional[str]' = None, *args: 'Any', **kwargs: 'Any') -> 'None': diff --git a/pcapkit/foundation/reassembly/tcp.py b/pcapkit/foundation/reassembly/tcp.py index 1611bdb0ff..2f9e4e618f 100644 --- a/pcapkit/foundation/reassembly/tcp.py +++ b/pcapkit/foundation/reassembly/tcp.py @@ -89,6 +89,14 @@ class TCP(ReassemblyBase[Packet, Datagram, BufferID, Buffer]): start is the SYN's own sequence number, and a FIN completes the stream only when every octet from there up to the FIN has arrived. + .. versionchanged:: 1.5.0 + + Of two overlapping segments that disagree, the octets received first + are kept, per :rfc:`9293#section-3.10`, and the contested range is + recorded in :attr:`Datagram.conflict + `; 1.4.x + kept the later octets without a record (:issue:`443`). + """ if TYPE_CHECKING: protocol: 'Type[TCP_Protocol]' diff --git a/pcapkit/foundation/registry/protocols.py b/pcapkit/foundation/registry/protocols.py index 81054a50c4..cddbb41f87 100644 --- a/pcapkit/foundation/registry/protocols.py +++ b/pcapkit/foundation/registry/protocols.py @@ -931,6 +931,11 @@ def register_mh_extension(code: 'MH_CGAExtension', meth: 'str | tuple[MH_Extensi schema: :class:`~pcapkit.protocols.schema.schema.Schema` class for the message type. It should be a subclass of :class:`pcapkit.protocols.schema.internet.mh.CGAExtension`. + .. versionchanged:: 1.5.0 + + ``schema`` is keyword-only, as in the other ``register_*`` functions + that take one (:issue:`516`). + """ if isinstance(meth, str) and not hasattr(MH, f'_read_ext_{meth}'): raise RegistryError('method must be a valid MH CGA extension function') @@ -1031,6 +1036,11 @@ def register_apptype(code: 'int | Enum_AppType', module: 'str | ModuleDescriptor * :func:`pcapkit.foundation.registry.register_udp` * :func:`pcapkit.foundation.registry.register_sctp` + .. versionchanged:: 1.5.0 + + The keyword-only ``proto`` is replaced by ``*transport``, one transport + protocol per argument, with no alias (:issue:`806`). + """ # NOTE: ``class_`` sits at the sibling position -- positional, ahead of # ``*transport`` -- per maintainer ruling, so a third positional that is diff --git a/pcapkit/foundation/traceflow/tcp.py b/pcapkit/foundation/traceflow/tcp.py index 103ba81acd..9a1dd7bce2 100644 --- a/pcapkit/foundation/traceflow/tcp.py +++ b/pcapkit/foundation/traceflow/tcp.py @@ -44,8 +44,6 @@ class TCP(TraceFlowBase[BufferID, Buffer[_AT], Index, Packet[_AT]], Generic[_AT] analyse: reassemble each flow's application layer, so that :attr:`Index.packet ` can be read - *args: Arbitrary positional arguments. - **kwargs: Arbitrary keyword arguments. Note: A TCP connection has two halves, and by default they are traced as **one diff --git a/pcapkit/foundation/traceflow/traceflow.py b/pcapkit/foundation/traceflow/traceflow.py index 181c9e78e0..40868a559a 100644 --- a/pcapkit/foundation/traceflow/traceflow.py +++ b/pcapkit/foundation/traceflow/traceflow.py @@ -509,6 +509,14 @@ class MyMixin(TraceFlow): # not registered bidirectional: trace both halves of a conversation as one flow analyse: reassemble each flow's application layer + .. versionchanged:: 1.5.0 + + Registration is opt-in, under the ``protocol`` class keyword; 1.4.x + registered every subclass, under its :attr:`name` when the keyword was + omitted (:issue:`514`). The ``bidirectional`` and ``analyse`` arguments + are added, and a flow covers both directions of a conversation unless + ``bidirectional=False`` is passed. + """ def __init_subclass__(cls, /, protocol: 'Optional[str]' = None, *args: 'Any', **kwargs: 'Any') -> 'None': diff --git a/pcapkit/interface/core.py b/pcapkit/interface/core.py index 425e0bf43d..05d87920b7 100644 --- a/pcapkit/interface/core.py +++ b/pcapkit/interface/core.py @@ -88,7 +88,7 @@ def extract(fin: 'Optional[str | IO[bytes]]' = None, fout: 'Optional[str]' = Non Arguments: fin: file name to be read or a binary IO object; - if file not exist, raise :exc:`FileNotFound` + if file not exist, raise :exc:`~pcapkit.utilities.exceptions.FileNotFound` fout: file name to be written format: file format of output @@ -156,6 +156,14 @@ def extract(fin: 'Optional[str | IO[bytes]]' = None, fout: 'Optional[str]' = Non Returns: An :class:`~pcapkit.foundation.extraction.Extractor` object. + .. versionchanged:: 1.5.0 + + ``reasm_timeout``, ``trace_bidirectional`` and ``trace_analyse`` are + inserted among the positional parameters and ``context`` is appended, + so a positional argument after ``reasm_store`` has to move to a + keyword. Flows are traced bidirectionally by default, and IPv4 and IPv6 + reassembly buffers expire after 60 seconds of capture time. + """ if isinstance(layer, type) and issubclass(layer, ProtocolBase): layer = (layer.__layer__ or 'none').lower() # type: ignore[assignment] @@ -180,16 +188,24 @@ def reassemble(protocol: 'str | Type[ProtocolBase]', strict: 'bool' = False, Arguments: protocol: protocol to be reassembled - strict: if return all datagrams (including those not implemented) when submit + strict: if :data:`True`, report a datagram that is not completely + reassembled as the tuple of its received runs; otherwise as one + contiguous payload timeout: reassembly timeout in seconds, on the capture's own clock; :data:`None` selects the protocol's own default Returns: - A :class:`~pcapkit.foundation.reassembly.reassembly.Reassembly` object of corresponding protocol. + A :class:`~pcapkit.foundation.reassembly.reassembly.ReassemblyBase` object of corresponding protocol. Raises: FormatError: If ``protocol`` is **NOT** any of IPv4, IPv6 or TCP. + .. versionchanged:: 1.5.0 + + ``timeout`` is added. IPv4 and IPv6 buffers expire after 60 seconds of + capture time by default, where 1.4.x held them for the life of the + object. + """ if isinstance(protocol, type) and issubclass(protocol, ProtocolBase): protocol = protocol.id()[0] @@ -219,11 +235,17 @@ def trace(protocol: 'str | Type[ProtocolBase]', fout: 'Optional[str]', flow, which is the default Returns: - A :class:`~pcapkit.foundation.traceflow.traceflow.TraceFlow` object. + A :class:`~pcapkit.foundation.traceflow.traceflow.TraceFlowBase` object. Raises: FormatError: If ``protocol`` is **NOT** TCP. + .. versionchanged:: 1.5.0 + + ``bidirectional`` is added, and by default both directions of a + conversation are traced as one flow; 1.4.x traced each direction as a + flow of its own. + """ if isinstance(protocol, type) and issubclass(protocol, ProtocolBase): protocol = protocol.id()[0] diff --git a/pcapkit/interface/misc.py b/pcapkit/interface/misc.py index f8d8ffc3a0..2b1f0d828e 100644 --- a/pcapkit/interface/misc.py +++ b/pcapkit/interface/misc.py @@ -81,7 +81,8 @@ def follow_tcp_stream(fin: 'Optional[str]' = None, verbose: 'bool' = False, """Follow TCP streams. Arguments: - fin: file name to be read; if file not exist, raise :exc:`FileNotFound` + fin: file name to be read; if file not exist, raise + :exc:`~pcapkit.utilities.exceptions.FileNotFound` extension: if check and append extensions to output file verbose: if print verbose output information engine: extraction engine to be used @@ -103,13 +104,20 @@ def follow_tcp_stream(fin: 'Optional[str]' = None, verbose: 'bool' = False, either way. Returns: - List of extracted TCP streams. + Tuple of extracted TCP streams. + + .. versionchanged:: 1.5.0 + + ``trace_bidirectional`` and ``trace_analyse`` are added. By default a + stream holds both directions of a conversation; 1.4.x returned one + stream per direction. """ - # NOTE: all of these engines disable TCP flow tracing outright -- PyShark - # because ``pcapkit`` has no reassembly adapter for it, PyPCAP and PCAP_CT - # because they perform no protocol dissection at all -- so - # ``extraction.trace`` below would raise instead of yielding streams. + # NOTE: none of these engines can follow a stream. PyShark traces flows, but + # ``pcapkit`` has no reassembly adapter for it, so the conversations below + # could not be rebuilt; PyPCAP and PCAP_CT perform no protocol dissection at + # all and disable TCP flow tracing outright, so ``extraction.trace`` below + # would raise instead of yielding streams. if engine is not None and engine.lower() in ('pyshark', 'pypcap', 'pcap_ct'): warn(f'unsupported extraction engine: {engine}; fallback to default engine', EngineWarning, stacklevel=stacklevel()) @@ -199,7 +207,7 @@ def follow_tcp_stream(fin: 'Optional[str]' = None, verbose: 'bool' = False, filename=stream.fpout, packets=tuple(packets), conversations=tuple(datagram.payload for datagram in sorted( - reassembly.datagram, key=lambda datagram: datagram.index # make sure the converstations are in order + reassembly.datagram, key=lambda datagram: datagram.index # make sure the conversations are in order )), )) return tuple(streams) diff --git a/pcapkit/toolkit/dpkt.py b/pcapkit/toolkit/dpkt.py index cf77ad4155..d1fbf2994d 100644 --- a/pcapkit/toolkit/dpkt.py +++ b/pcapkit/toolkit/dpkt.py @@ -603,6 +603,11 @@ def ipv4_reassembly(packet: 'Packet', timestamp: 'float', *, See Also: :class:`pcapkit.foundation.reassembly.ipv4.IPv4` + .. versionchanged:: 1.5.0 + + ``timestamp`` is a new second positional parameter, so a 1.4.x call + with ``packet`` alone fails. + """ network = _network(packet) # an IPv6 layer is the one with extension headers, as in ``_wire_hdr_len`` @@ -682,6 +687,11 @@ def ipv6_reassembly(packet: 'Packet', timestamp: 'float', *, See Also: :class:`pcapkit.foundation.reassembly.ipv6.IPv6` + .. versionchanged:: 1.5.0 + + ``timestamp`` is a new second positional parameter, so a 1.4.x call + with ``packet`` alone fails. + """ network = _network(packet) ipv6 = cast('IP6', network) if hasattr(network, 'extension_hdrs') else None @@ -791,6 +801,11 @@ def tcp_reassembly(packet: 'Packet', timestamp: 'float', *, See Also: :class:`pcapkit.foundation.reassembly.tcp.TCP` + .. versionchanged:: 1.5.0 + + ``timestamp`` is a new second positional parameter, so a 1.4.x call + with ``packet`` alone fails. + """ ip = _network(packet) if ip is None: diff --git a/pcapkit/toolkit/pyshark.py b/pcapkit/toolkit/pyshark.py index a425e95c6c..8319890272 100644 --- a/pcapkit/toolkit/pyshark.py +++ b/pcapkit/toolkit/pyshark.py @@ -47,8 +47,8 @@ #: Every entry was measured, not transcribed, with ``editcap``/``tshark`` 4.6.9:: #: #: editcap -F pcap -T examples/captures/in.pcap out.pcap -#: # value := the ``network`` field at offset 20 of out.pcap's pcap file header -#: # key := the ``frame.encap_type`` that ``tshark -r out.pcap -c 1 -T pdml`` then reports +#: # value := the network field at offset 20 of out.pcap's pcap file header +#: # key := the frame.encap_type that `tshark -r out.pcap -c 1 -T pdml` then reports #: #: so each pair is one round trip through Wireshark's own ``wiretap/pcap-common.c``, in the read #: direction :mod:`pyshark` actually exercises. The trailing comment on each line names the @@ -404,7 +404,7 @@ def _frame_layer(packet: 'Packet') -> 'Any': PyShark takes the second PDML ```` as ``frame_info``. On a packet with a comment that is ``pkt_comment``, and the frame layer is ``layers[0]`` (measured on ``test.pcapng`` frames 1 and 6 with tshark - 4.6.9), so the layer is looked up by name, as the engine does (#1531). + 4.6.9), so the layer is looked up by name, as the engine does (#1515). """ for layer in (packet.frame_info, *packet.layers): @@ -556,6 +556,15 @@ def tcp_traceflow(packet: 'Packet', *, count: 'int' = -1) -> 'TF_TCP_Packet | No See Also: :class:`pcapkit.foundation.traceflow.tcp.TCP` + .. versionchanged:: 1.5.0 + + The link type is resolved from ``frame.encap_type`` through + :data:`~pcapkit.toolkit.pyshark.ENCAP_TYPE_TO_LINKTYPE`, and one that + cannot be resolved raises + :exc:`~pcapkit.utilities.exceptions.MissingKeyError`; 1.4.x looked up + the root layer's name, which gave the wrong DLT for ``ppp``, ``null``, + ``lapd`` and ``raw`` (:issue:`843`). + """ ip, tcp = _carrier(packet) if ip is not None and tcp is not None and _has_fixed_header(tcp) and _has_addresses(ip): diff --git a/pcapkit/utilities/__init__.py b/pcapkit/utilities/__init__.py index bac5dfc5bf..2c377a6f20 100644 --- a/pcapkit/utilities/__init__.py +++ b/pcapkit/utilities/__init__.py @@ -11,6 +11,12 @@ third-party libraries. It imports nothing from :mod:`pcapkit` outside itself (:issue:`1712`). +.. versionremoved:: 1.5.0 + + ``pcapkit.utilities.decorators``, and the ``seekset``, ``beholder`` and + ``prepare`` it bound on :mod:`pcapkit.utilities`, with no re-export; import + them from :mod:`pcapkit.corekit.decorators` (:issue:`1735`). + """ from pcapkit.utilities.chardet import detect from pcapkit.utilities.exceptions import stacklevel diff --git a/pcapkit/utilities/exceptions.py b/pcapkit/utilities/exceptions.py index ed275758e1..fe3658fc92 100644 --- a/pcapkit/utilities/exceptions.py +++ b/pcapkit/utilities/exceptions.py @@ -82,9 +82,9 @@ def stacklevel() -> 'int': level 0 stacklevel() itself level 1 whoever called stacklevel() ... - level ``boundary`` the outermost frame inside pcapkit + level boundary the outermost frame inside pcapkit ... - level ``outermost`` the interpreter entry point + level outermost the interpreter entry point The frame to name is the first one *past* the boundary, hence ``boundary + 1``. ``boundary`` is measured from the *inside* out: it depends @@ -205,6 +205,11 @@ class BaseError(Exception): See Also: :func:`pcapkit.utilities.exceptions.stacklevel` + .. versionchanged:: 1.5.0 + + A quiet error logs nothing, where 1.4.x logged it at ``ERROR``, and no + error sets :data:`sys.tracebacklimit` (:issue:`362`). + """ def __init__(self, *args: 'Any', quiet: 'bool' = False, **kwargs: 'Any') -> 'None': @@ -722,6 +727,8 @@ class EnumKeyError(BaseError, KeyError): would leave a caller unable to tell a registry that has no such member from a packet dict that has no such field. + .. versionadded:: 1.5.0 + """ diff --git a/pcapkit/utilities/logging.py b/pcapkit/utilities/logging.py index 1bb86cecba..48d65f06f2 100644 --- a/pcapkit/utilities/logging.py +++ b/pcapkit/utilities/logging.py @@ -32,10 +32,16 @@ .. seealso:: - :envvar:`PCAPKIT_DEVMODE` still bootstraps the historical :obj:`sys.stderr` - handler at :data:`logging.DEBUG`, so a development session needs no explicit + :envvar:`PCAPKIT_DEVMODE` attaches the 1.4.x :obj:`sys.stderr` handler at + :data:`logging.DEBUG` on import, so a development session needs no explicit :func:`configure` call. +.. versionchanged:: 1.5.0 + + Importing :mod:`pcapkit` attaches only a :class:`logging.NullHandler` and + sets no level; 1.4.x attached a :obj:`sys.stderr` handler at + :data:`logging.INFO`. + """ import logging import os @@ -150,6 +156,8 @@ def get_logger(name: 'Optional[str]' = None) -> 'logging.Logger': Returns: The requested logger. + .. versionadded:: 1.5.0 + """ if not name or name == ROOT_LOGGER_NAME: return logger @@ -225,6 +233,8 @@ def ensure_output(level: 'Union[int, str]' = logging.DEBUG, *, :data:`True` if a handler was added, :data:`False` if output was already going somewhere and the existing configuration was left alone. + .. versionadded:: 1.5.0 + """ if _has_output(logger): return False @@ -252,6 +262,8 @@ def reset(name: 'Optional[str]' = None) -> 'logging.Logger': everything else. To reinstate it, call ``configure(logging.DEBUG, stream=sys.stderr)``. + .. versionadded:: 1.5.0 + """ target = get_logger(name) @@ -327,6 +339,8 @@ def configure(level: 'Optional[Union[int, str]]' = None, *, configure(logging.DEBUG, handler=logging.FileHandler('pcapkit.log')) configure(logging.INFO, name='pcapkit.foundation.registry') + .. versionadded:: 1.5.0 + """ if stream is not None and handler is not None: raise ValueError("configure() accepts 'stream' or 'handler', not both") diff --git a/pcapkit/utilities/warnings.py b/pcapkit/utilities/warnings.py index f6665ad064..be96fbb5ef 100644 --- a/pcapkit/utilities/warnings.py +++ b/pcapkit/utilities/warnings.py @@ -55,6 +55,12 @@ holds back its warnings, on both channels, until the parse is kept, and drops them if it is discarded (:issue:`1580`). +.. versionremoved:: 1.5.0 + + ``pcapkit.utilities.warnings.DEVMODE``, which 1.4.x imported from + :mod:`pcapkit.utilities.logging`; read + :data:`pcapkit.utilities.logging.DEVMODE` instead. + """ import contextvars import inspect @@ -137,6 +143,12 @@ def warn(message: 'Union[str, Warning]', category: 'Type[Warning]', with the frame it is attributed to, and reported from there only if the block keeps it. + .. versionchanged:: 1.5.0 + + A warning is reported exactly once on each channel, in development mode + as outside it (:issue:`363`), and the :mod:`warnings` channel answers to + the application's filters alone (:issue:`364`). + """ if stacklevel is None: # Computed here, so the level is already relative to *this* frame, which @@ -319,17 +331,18 @@ class BaseWarning(UserWarning): of :func:`warn`, which is called once per complaint; a warning object may be built without ever being reported, and the two must not be confused. - Note: - Up to and including v1.4.1, this constructor called - ``warnings.simplefilter('ignore', type(self))`` outside development - mode, which inserted an entry at the front of the process-global - :data:`warnings.filters` -- overriding whatever the host application, - :option:`-W` or the test runner had configured, and invalidating every - module's ``__warningregistry__`` so that unrelated warnings re-fired. - It also logged the warning a second time under - :data:`~pcapkit.utilities.logging.DEVMODE`. Both are gone; filtering is - the application's business, and is done through the standard - :mod:`warnings` machinery. + .. versionchanged:: 1.5.0 + + The constructor has no side effects (:issue:`364`). In 1.4.x it called + ``warnings.simplefilter('ignore', type(self))`` outside development + mode, which inserted an entry at the front of the process-global + :data:`warnings.filters` -- overriding whatever the host application, + :option:`-W` or the test runner had configured, and invalidating every + module's ``__warningregistry__`` so that unrelated warnings re-fired. + It also logged the warning a second time under + :data:`~pcapkit.utilities.logging.DEVMODE`. Filtering is the + application's business, and is done through the standard + :mod:`warnings` machinery. """