From ccaf567f196b080b62113b37c96c83e03db84ee1 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sun, 25 Oct 2026 00:43:17 -0400 Subject: [PATCH] docs(changelog): fifth whole-repo pass over the changelog (#1103) - 1.5.0: add entries for the library, packaging, docs-rendering and release-pipeline changes merged since #1896, filed under their module sections; skip the prose passes, test-only and CI-internal changes and bot dependency bumps, as the file's precedent does. - 1.5.0: correct claims later changes made stale: the prerelease range (b14), the cited PR range, the typehints pin (3.17.2 since #1944), to_dict carrying __exthdr__ (#2014), and setup.py reading the README (static readme since #1994). - 1.3.3/1.3.1: the #180 fix, the base data class's packet and Info's excluded-field iteration first shipped in 1.3.1.post8 (#1933). - Regenerate CHANGELOG.md. tests/project/test_changelog_md.py, test_conventions_doc_claims.py and test_docs_*: pass. --- CHANGELOG.md | 38 ++++++-- docs/source/changelog.rst | 8 +- docs/source/changelog/1.3.1.rst | 3 + docs/source/changelog/1.3.3.rst | 8 +- docs/source/changelog/1.5.0.rst | 168 +++++++++++++++++++++++++++++--- 5 files changed, 198 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69fbd08a0a..e916cb5984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ ## 1.5.0 -- unreleased -The largest release since 1.0, and the first recorded here as it happened rather than reconstructed. New extraction engines, ESP with payload decryption, SCTP and NGAP over SCTP, a library logger that no longer hijacks the consumer's, and a defect programme run through the issue tracker, citing issues and pull requests from [#340](https://github.com/JarryShaw/PyPCAPKit/pull/340) to [#1895](https://github.com/JarryShaw/PyPCAPKit/pull/1895). +The largest release since 1.0, and the first recorded here as it happened rather than reconstructed. New extraction engines, ESP with payload decryption, SCTP and NGAP over SCTP, a library logger that no longer hijacks the consumer's, and a defect programme run through the issue tracker, citing issues and pull requests from [#340](https://github.com/JarryShaw/PyPCAPKit/pull/340) to [#2151](https://github.com/JarryShaw/PyPCAPKit/pull/2151). -Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and `1.5.0b4` to `1.5.0b13` (2026-09-20 to 2026-10-16), dated in UTC and all published as prereleases and so resolved only by `pip install --pre`. `1.5.0b1` half-shipped: the tag, the GitHub release and the Conda deployments landed, but PyPI rejected the wheel because `twine check` found a Sphinx-only `:mod:` role in `README.rst`, which `pyproject.toml` declares as the dynamic long description. `1.5.0b2` reshipped it -- an existing version cannot republish -- and `1.5.0b3` followed the CI change that stops a TestPyPI outage from costing a release its wheels ([#497](https://github.com/JarryShaw/PyPCAPKit/pull/497), [#498](https://github.com/JarryShaw/PyPCAPKit/pull/498)). +Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and `1.5.0b4` to `1.5.0b14` (2026-09-20 to 2026-10-22), dated in UTC and all published as prereleases and so resolved only by `pip install --pre`. `1.5.0b1` half-shipped: the tag, the GitHub release and the Conda deployments landed, but PyPI rejected the wheel because `twine check` found a Sphinx-only `:mod:` role in `README.rst`, which `pyproject.toml` declares as the dynamic long description. `1.5.0b2` reshipped it -- an existing version cannot republish -- and `1.5.0b3` followed the CI change that stops a TestPyPI outage from costing a release its wheels ([#497](https://github.com/JarryShaw/PyPCAPKit/pull/497), [#498](https://github.com/JarryShaw/PyPCAPKit/pull/498)). ### pcapkit.const @@ -43,7 +43,7 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and - the `AppType` registry's dict union is defined only on interpreters whose `dict` has one ([#1694](https://github.com/JarryShaw/PyPCAPKit/issues/1694), [#1703](https://github.com/JarryShaw/PyPCAPKit/pull/1703)). - NGAP's `ProcedureCode` and `ProtocolIE` are generated into `pcapkit.const.ngap` from `pycrate`'s compiled NGAP specification, with no network fetch. An unseen value gets a non-registering member, where every unknown key used to mint a member at the shared value `-1` and so aliased every other ([#880](https://github.com/JarryShaw/PyPCAPKit/issues/880), [#899](https://github.com/JarryShaw/PyPCAPKit/pull/899)). - `FEATCode.get()` matches case-insensitively, as [RFC 5797 Section 2](https://datatracker.ietf.org/doc/html/rfc5797#section-2) defines FEAT codes; the registry-wide case-sensitivity audit found no other registry that needed to fold ([#903](https://github.com/JarryShaw/PyPCAPKit/issues/903), [#913](https://github.com/JarryShaw/PyPCAPKit/pull/913)). -- `register` refuses what `get` refuses. `TCP.register(70000, 'x')` and `Hardware.register(70000, 'x')` minted a member that `get` then returned, because no registry overrode the `_validate_value` hook `register` calls. Each generated registry, `AppType` included, now overrides it with the check its `_missing_` makes, raising `EnumValueError`. `get(value, default)` on a value the hook refuses still returns `default` and logs nothing ([#1792](https://github.com/JarryShaw/PyPCAPKit/issues/1792), [#1818](https://github.com/JarryShaw/PyPCAPKit/pull/1818), [#1816](https://github.com/JarryShaw/PyPCAPKit/issues/1816), [#1823](https://github.com/JarryShaw/PyPCAPKit/pull/1823); the quiet miss is [#1845](https://github.com/JarryShaw/PyPCAPKit/issues/1845), [#1873](https://github.com/JarryShaw/PyPCAPKit/pull/1873), under `pcapkit.corekit`). +- `register` refuses what `get` refuses. `TCP.register(70000, 'x')` and `Hardware.register(70000, 'x')` minted a member that `get` then returned, because no registry overrode the `_validate_value` hook `register` calls. Each generated registry, `AppType` included, now overrides it with the check its `_missing_` makes, raising `EnumValueError`. `get(value, default)` on a value the hook refuses still returns `default` and logs nothing ([#1792](https://github.com/JarryShaw/PyPCAPKit/issues/1792), [#1818](https://github.com/JarryShaw/PyPCAPKit/pull/1818), [#1816](https://github.com/JarryShaw/PyPCAPKit/issues/1816), [#1823](https://github.com/JarryShaw/PyPCAPKit/pull/1823); the quiet miss is [#1845](https://github.com/JarryShaw/PyPCAPKit/issues/1845), [#1873](https://github.com/JarryShaw/PyPCAPKit/pull/1873), under `pcapkit.corekit`). `AppType.register()` and `OptionType.register()` keep that rejection loud inside a quiet `get`, as `EnumRegistry.register()` does ([#1916](https://github.com/JarryShaw/PyPCAPKit/issues/1916), [#1927](https://github.com/JarryShaw/PyPCAPKit/pull/1927)). - `Command.get()` refuses a key that is not a `str` with `EnumValueError`, as `Method.get()` does. `get(b'\x01')` minted a member keyed by `bytes`, and `get(-1)` raised `AttributeError` whatever its `default` ([#1824](https://github.com/JarryShaw/PyPCAPKit/issues/1824), [#1826](https://github.com/JarryShaw/PyPCAPKit/pull/1826)). - IANA's port spans that name no transport answer `AppType.get()` with the asking registry's transport rather than `TransportProtocol.undefined`, as its `unknown` fallback for a port in no span already did: `TCP.get(8012)` is `unassigned [tcp]`. A port IANA lists on other transports only stays `unknown`: 8011 is `reserved [tcp]` and `bier-echo [udp]`, and `unknown [dccp]` and `unknown [sctp]`. Lookups still mint nothing ([#860](https://github.com/JarryShaw/PyPCAPKit/issues/860)) and an out-of-range port still raises ([#2048](https://github.com/JarryShaw/PyPCAPKit/issues/2048), [#2052](https://github.com/JarryShaw/PyPCAPKit/pull/2052)). @@ -56,6 +56,7 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and - `EnumLookup`, the base carrying `get` and `get_all` that `EnumRegistry` and the non-registry enumerations share ([#877](https://github.com/JarryShaw/PyPCAPKit/issues/877), [#906](https://github.com/JarryShaw/PyPCAPKit/pull/906), [#921](https://github.com/JarryShaw/PyPCAPKit/pull/921), [#932](https://github.com/JarryShaw/PyPCAPKit/pull/932)). Its `get` raises pcapkit exceptions in the shape of the stdlib `Enum`: a name miss raises `EnumKeyError`, a `KeyError` ([#923](https://github.com/JarryShaw/PyPCAPKit/issues/923), [#927](https://github.com/JarryShaw/PyPCAPKit/pull/927)). - `MultiInfo` and `OrderedMultiInfo`, immutable `Info` subclasses that are also a `MultiDict` and an `OrderedMultiDict`, so a repeated key keeps every value. The data models' option lists use them ([#1484](https://github.com/JarryShaw/PyPCAPKit/issues/1484), [#1523](https://github.com/JarryShaw/PyPCAPKit/pull/1523)). - `pcapkit.corekit.fields.byteorder`, holding `packet_byteorder` and `byteorder_callback`, the byte-order helpers the PCAP and PCAP-NG schemas share ([#1528](https://github.com/JarryShaw/PyPCAPKit/pull/1528), [#1711](https://github.com/JarryShaw/PyPCAPKit/issues/1711), [#1715](https://github.com/JarryShaw/PyPCAPKit/pull/1715)). +- `can_seek()`, whether a stream really seeks: on Windows a pipe answers `seekable()` with `True` while its `seek` does nothing. It trusts `seekable()` only for a regular file or a stream with no descriptor. `Extractor` wraps any other stream in `SeekableReader`, so `pcapkit -` reads a Windows pipe ([#2097](https://github.com/JarryShaw/PyPCAPKit/issues/2097), [#2104](https://github.com/JarryShaw/PyPCAPKit/pull/2104)), and `advance()` skips a record on one by reading ([#2108](https://github.com/JarryShaw/PyPCAPKit/issues/2108), [#2118](https://github.com/JarryShaw/PyPCAPKit/pull/2118), `93a6173ae`). #### Changed @@ -101,6 +102,7 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and - `PortEnumField` given a `proto` with no `.name` -- a string such as `'tcp'`, or a composite such as `TransportProtocol.tcp | TransportProtocol.udp`, which is a bare `int` -- raised `AttributeError` on a port the registry does not hold. The miss takes its transport from the registry's `__transport__`, as `AppType.get()` does ([#1774](https://github.com/JarryShaw/PyPCAPKit/issues/1774), [#1781](https://github.com/JarryShaw/PyPCAPKit/pull/1781)). - `EnumLookup.get()` answers a value miss quietly, whether `default` answers it or the miss is raised: a registry's `_validate_value` rejection logs nothing and installs no exception hook there, while `EnumRegistry.register()` still reports the same rejection loudly ([#1845](https://github.com/JarryShaw/PyPCAPKit/issues/1845), [#1873](https://github.com/JarryShaw/PyPCAPKit/pull/1873)). - each sentinel is a singleton, as `None` is: calling its type, `copy`, `deepcopy` and `pickle` at every protocol give back the one object, and `NO_VALUE` reprs as its own name rather than as an object address. Their new common base, `Sentinel`, is public; a sentinel's type cannot be subclassed, and neither it nor the sentinel takes an attribute write. A pickle written before this change at protocol 0 or 1 still loads, though not as the singleton ([#1841](https://github.com/JarryShaw/PyPCAPKit/issues/1841), [#1842](https://github.com/JarryShaw/PyPCAPKit/issues/1842), [#1876](https://github.com/JarryShaw/PyPCAPKit/pull/1876)). +- defects off CPython on Linux. On GraalPy, `SeekableReader`'s reads aliased a buffer a later slide rewrote, so they copy out of it, and a `MultiDict` nested each value list, so it is built from a `dict` ([#1963](https://github.com/JarryShaw/PyPCAPKit/issues/1963), [#1974](https://github.com/JarryShaw/PyPCAPKit/pull/1974)); and a sentinel type refused the cache writes `_py_abc` makes, so `isinstance` against it raised `TypeError` ([#1960](https://github.com/JarryShaw/PyPCAPKit/issues/1960), [#1966](https://github.com/JarryShaw/PyPCAPKit/pull/1966)). `Field.pack` hands `struct` a `bytearray` as `bytes` for the `s`, `p` and `c` formats, which PyPy refuses for `s` and `p` and GraalPy accepts for `c` where CPython does not, so it packs alike on every interpreter ([#2004](https://github.com/JarryShaw/PyPCAPKit/issues/2004), [#2007](https://github.com/JarryShaw/PyPCAPKit/pull/2007), [#2015](https://github.com/JarryShaw/PyPCAPKit/issues/2015), [#2018](https://github.com/JarryShaw/PyPCAPKit/pull/2018)). On Windows, `SeekableReader(buffer_save=True)` with no `buffer_path` could not reopen its temporary file; it creates one it can, and deletes it on `close()` or collection ([#2098](https://github.com/JarryShaw/PyPCAPKit/issues/2098), [#2105](https://github.com/JarryShaw/PyPCAPKit/pull/2105)). ### pcapkit.dumpkit @@ -114,6 +116,7 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and - tree output raised on a `complex` value, and is now documented as lossy ([#1266](https://github.com/JarryShaw/PyPCAPKit/issues/1266), [#1366](https://github.com/JarryShaw/PyPCAPKit/pull/1366)). - plist dates are written to whole seconds, so `plistlib` reads them back ([#1448](https://github.com/JarryShaw/PyPCAPKit/issues/1448), [#1450](https://github.com/JarryShaw/PyPCAPKit/pull/1450)). - a PCAP trace record scales its fraction to the trace's resolution ([#1500](https://github.com/JarryShaw/PyPCAPKit/issues/1500), [#1529](https://github.com/JarryShaw/PyPCAPKit/pull/1529)), and writes the octets it has rather than a truncated record's claimed length ([#1541](https://github.com/JarryShaw/PyPCAPKit/issues/1541), [#1543](https://github.com/JarryShaw/PyPCAPKit/pull/1543)). +- a subclass of a dumper `make_dumper` builds recursed without end in its `object_hook` fallback, which called `super(type(self), self)` ([#2144](https://github.com/JarryShaw/PyPCAPKit/issues/2144), [#2146](https://github.com/JarryShaw/PyPCAPKit/pull/2146)). ### pcapkit.foundation @@ -135,12 +138,13 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and - **a breaking change to** `Extractor.engine` and `Extractor.record_header()`: both are now declared to return `EngineBase[_P]` rather than `Engine`, and the private `_exeng` attribute follows. The built-in engines subclass `EngineBase` directly, so none was ever an `Engine`, which adds only the `__init_subclass__` registration hook. A type checker sees the change: code passing the result to an `Engine`-typed parameter must now accept `EngineBase`; and because the old declarations were *unparameterised*, `engine.read_frame()` and `record_header().read_frame()` used to be `Any` and are now the extractor's own frame type, so an assignment that relied on `Any` there no longer type-checks ([#1022](https://github.com/JarryShaw/PyPCAPKit/issues/1022)). - the documentation of the engine base classes: the member stubs (`name`, `module`, `registry`, `extractor`, `unsupported_reason`, `run`, `read_frame`, `close` and the rest) moved from `Engine` to `EngineBase`, where they are defined, and `Engine` now documents only the `__init_subclass__` registration hook it adds, so the `EngineBase[_P]` that `Extractor.engine` returns leads to an API. Cross-references to `Engine.run`, `Engine.read_frame`, `Engine.close` and `Engine.unsupported_reason` now point at `EngineBase`, which stays out of `__all__`, as [#1016](https://github.com/JarryShaw/PyPCAPKit/issues/1016) settled ([#1024](https://github.com/JarryShaw/PyPCAPKit/issues/1024)). - **a breaking change to** `register_mh_extension`: `schema` is keyword-only, as in the other schema-taking registrars ([#516](https://github.com/JarryShaw/PyPCAPKit/issues/516), [#526](https://github.com/JarryShaw/PyPCAPKit/pull/526)). +- the `pypcap` and `pcap_ct` engines probe the `pcap` backend once per extraction rather than three times, and find its distribution without parsing each candidate's `METADATA`, which `importlib.metadata` does on Python 3.15 and which made `pcap_ct` extraction about 2.4x slower there ([#2110](https://github.com/JarryShaw/PyPCAPKit/issues/2110), [#2112](https://github.com/JarryShaw/PyPCAPKit/pull/2112), [#2113](https://github.com/JarryShaw/PyPCAPKit/issues/2113), [#2121](https://github.com/JarryShaw/PyPCAPKit/pull/2121)). #### Fixed - TCP reassembly mixed absolute sequence numbers with buffer-relative slicing, so on any capture carrying a SYN with a realistic initial sequence number incomplete datagrams were dropped silently, and the `completed=False` branch of the public API was unreachable ([#349](https://github.com/JarryShaw/PyPCAPKit/issues/349), [#376](https://github.com/JarryShaw/PyPCAPKit/pull/376)). - `format='text'` raised `AttributeError` before writing anything, naming a `dictdumper.Text` that has never existed. It now points at `Tree`, as the `'txt'` alias beside it already did. -- `Extractor` had the ownership of its input stream inverted: a handle it opened itself, from `fin` given as a path, was **never closed**, while a stream the *caller* supplied and still needed **was**. `_cleanup` closed under `not self._flag_s` (the flag that gated the `open()`), and the `SeekableReader` wrapping a non-seekable input asked for `stream_closing=not self._flag_s` in the same wrong direction. The leak was one descriptor per extraction, the root cause of the `ResourceWarning` that reddened [#577](https://github.com/JarryShaw/PyPCAPKit/pull/577), [#596](https://github.com/JarryShaw/PyPCAPKit/pull/596) and [#600](https://github.com/JarryShaw/PyPCAPKit/pull/600) ([#606](https://github.com/JarryShaw/PyPCAPKit/issues/606) had only made the test immune). Both conditions now follow ownership via a single `Extractor._owns_input`, and a finaliser releases the handle of an extraction *abandoned* before end of file (`auto=False`, iterated part way and dropped); the files [#610](https://github.com/JarryShaw/PyPCAPKit/issues/610) names emit no such warning. Leaving a `with` block follows the same rule, so a caller's stream stays open after it ([#1999](https://github.com/JarryShaw/PyPCAPKit/issues/1999)). +- `Extractor` had the ownership of its input stream inverted: a handle it opened itself, from `fin` given as a path, was **never closed**, while a stream the *caller* supplied and still needed **was**. `_cleanup` closed under `not self._flag_s` (the flag that gated the `open()`), and the `SeekableReader` wrapping a non-seekable input asked for `stream_closing=not self._flag_s` in the same wrong direction. The leak was one descriptor per extraction, the root cause of the `ResourceWarning` that reddened [#577](https://github.com/JarryShaw/PyPCAPKit/pull/577), [#596](https://github.com/JarryShaw/PyPCAPKit/pull/596) and [#600](https://github.com/JarryShaw/PyPCAPKit/pull/600) ([#606](https://github.com/JarryShaw/PyPCAPKit/issues/606) had only made the test immune). Both conditions now follow ownership via a single `Extractor._owns_input`, and a finaliser releases the handle of an extraction *abandoned* before end of file (`auto=False`, iterated part way and dropped); the files [#610](https://github.com/JarryShaw/PyPCAPKit/issues/610) names emit no such warning. Leaving a `with` block follows the same rule, so a caller's stream stays open after it ([#1999](https://github.com/JarryShaw/PyPCAPKit/issues/1999), [#2000](https://github.com/JarryShaw/PyPCAPKit/pull/2000)). The finaliser is a `weakref.finalize`, so it also runs where `__del__` never does, as on GraalPy ([#1997](https://github.com/JarryShaw/PyPCAPKit/issues/1997), [#1998](https://github.com/JarryShaw/PyPCAPKit/pull/1998)). - `extract(..., no_eof=True)` never returned. End of stream was detected and `ExtractionWarning: EOF reached` fired, but every loop that handles it (`record_frames`, `__next__`, `__call__`) read `if self._flag_n: continue`, so the flag suppressed the error that *ended* the loop without supplying another ending and an exhausted input was retried forever. The flag is not pointless -- `__main__` sets `no_eof` for `fin='-'`, and the end of what has arrived on a live capture is not the end of the capture -- so this is a termination condition rather than a deletion. The input's own position tells a paused capture from a finished one: `prepare` raises end of stream when zero bytes remain and restores the position first, so two consecutive ends of stream at the same position mean nothing arrived between them (`Extractor._note_eof_progress`). A pipe is unaffected, since a read there *blocks* while the writer is open but idle. This also fixes `pcapkit -`, which hung on **any** finished stdin. **One deliberate narrowing**: a *seekable* input does not block, so a file still being appended to ends at the data present when the extraction reached it; a timed grace period would make the cut-off intermittent rather than absent, so following a growing file is left to a policy of its own. Pre-existing and untouched: an append landing *mid-record* raises `ValueError: read length must be non-negative or -1`. The end-to-end tests are bounded by a **child process** rather than `tests._support.time_limit`, because the in-process deadline proved intermittent on this loop, and an intermittent guard against a hang leaves a wedged suite rather than a red test. Guesses at the cause are ruled out: the parse path's `except Exception` handlers are never entered during the spin, and the memory is the retry loop's `EOF reached` records, which the runner retains ([#620](https://github.com/JarryShaw/PyPCAPKit/issues/620)). - `register_protocol` displaced one protocol class with another and said nothing. The registry behind `pcapkit.protocols.__proto__` is keyed on `cls.__name__.upper()`, and several dispatchable classes are named `HTTP` (the base `pcapkit.protocols.application.http.HTTP` and the `httpv1` and `httpv2` implementations), so a bare assignment decided the winner: `register_protocol(httpv2.HTTP)` moved `__proto__['HTTP']` to the `httpv2` class with no exception, log line or `RegistryWarning`, and `ProtocolBase.expand_comp('HTTP')` then resolved to whichever registered last. The overwrite now raises a `RegistryWarning` naming the displaced and the replacing class. The sharpest path is not a call to the registrar: `Protocol.__init_subclass__()` registers unconditionally, so merely *defining* a subclass of the public `Protocol` under a name a built-in holds displaced that built-in. A plain `import pcapkit` emits zero new warnings: the built-in `HTTP` classes derive from `ProtocolBase`, so `__init_subclass__` never fires for them, and import-time seeding keys off the distinct names `HTTP`, `HTTPv1` and `HTTPv2`. The guard deliberately reads "key present and the incumbent is a different class", not mere presence ([#726](https://github.com/JarryShaw/PyPCAPKit/pull/726) later gave its sibling registrars the same identity guard): this function is the funnel every wrapper registrar ends in, so registering one class under two codes (`register_tcp` then `register_udp`, a documented path) reaches it twice with nothing displaced. The collision is reported, not resolved: re-keying is a registry-format change that none of the bare-name readers can absorb (each degrades *silently* to a string or `Raw` on a miss), so it belongs to the registry redesign in [#514](https://github.com/JarryShaw/PyPCAPKit/issues/514). The issue called this the one registrar with no warning; in fact none warned by itself, the siblings only by delegating to a guarded classmethod ([#675](https://github.com/JarryShaw/PyPCAPKit/issues/675)). - the `#:` autodoc comments in `pcapkit/foundation/traceflow/traceflow.py` that named a bare `Type`, which Sphinx resolves against every class named `Type` in the project rather than `typing.Type`, and silently linked to `pcapkit.const.l2tp.type.Type`, an L2TP field-type enum; once [#684](https://github.com/JarryShaw/PyPCAPKit/issues/684) rendered `TraceFlow._foutio`, the built docs pointed at the wrong class with no warning. These sites now spell it `~typing.Type[Dumper]`, among the sites [#709](https://github.com/JarryShaw/PyPCAPKit/issues/709) lists. The other bare `Type` sites, in hand-written `:type:` fields under `docs/source/pcapkit/foundation/`, are fixed separately by [#714](https://github.com/JarryShaw/PyPCAPKit/pull/714) ([#709](https://github.com/JarryShaw/PyPCAPKit/issues/709)). @@ -160,6 +164,9 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and - `extract()` accepts a file object with no `name` as `fin` ([#1506](https://github.com/JarryShaw/PyPCAPKit/issues/1506), [#1509](https://github.com/JarryShaw/PyPCAPKit/pull/1509)), and reads every frame from a pipe or other non-seekable stream ([#1586](https://github.com/JarryShaw/PyPCAPKit/issues/1586), [#1595](https://github.com/JarryShaw/PyPCAPKit/pull/1595)). - the third-party engines read what the capture holds. The dpkt engine keeps a frame dpkt cannot parse rather than leaking `AttributeError` ([#1351](https://github.com/JarryShaw/PyPCAPKit/issues/1351), [#1377](https://github.com/JarryShaw/PyPCAPKit/pull/1377)), applies each PCAP-NG interface's link type and timestamp resolution rather than the first one's ([#1379](https://github.com/JarryShaw/PyPCAPKit/issues/1379), [#1396](https://github.com/JarryShaw/PyPCAPKit/pull/1396)), and reads float timestamps, Simple Packet Blocks and raw-IP frames ([#1550](https://github.com/JarryShaw/PyPCAPKit/pull/1550)). The scapy engine resets its interfaces at each PCAP-NG section ([#1522](https://github.com/JarryShaw/PyPCAPKit/issues/1522), [#1546](https://github.com/JarryShaw/PyPCAPKit/pull/1546)), takes a packet's link type and clock from its interface ([#1563](https://github.com/JarryShaw/PyPCAPKit/pull/1563)), and reads each record whole ([#1717](https://github.com/JarryShaw/PyPCAPKit/issues/1717), [#1733](https://github.com/JarryShaw/PyPCAPKit/pull/1733)). The pyshark engine skips non-packet PCAP-NG blocks ([#1515](https://github.com/JarryShaw/PyPCAPKit/issues/1515), [#1531](https://github.com/JarryShaw/PyPCAPKit/pull/1531)), and the pypcapfile engine reads the PCAP records pypcapfile drops ([#1512](https://github.com/JarryShaw/PyPCAPKit/issues/1512), [#1569](https://github.com/JarryShaw/PyPCAPKit/pull/1569)). dpkt and scapy write real PCAP traces ([#1507](https://github.com/JarryShaw/PyPCAPKit/issues/1507), [#1511](https://github.com/JarryShaw/PyPCAPKit/pull/1511)). - a callable passed as `verbose=` is called on every engine. The scapy, dpkt, pyshark, pypcap, pcap-ct and pypcapfile engines replaced it with their own printer, which now runs only for `verbose=True`, its output unchanged ([#1874](https://github.com/JarryShaw/PyPCAPKit/issues/1874), [#1880](https://github.com/JarryShaw/PyPCAPKit/pull/1880)). +- an `Extractor` whose constructor raises closes what it owns, the input it opened and then the engine it chose, so a PyShark capture's `tshark` does not outlive it; the original exception still propagates, and a caller's stream stays open ([#2100](https://github.com/JarryShaw/PyPCAPKit/issues/2100), [#2106](https://github.com/JarryShaw/PyPCAPKit/pull/2106), [#2107](https://github.com/JarryShaw/PyPCAPKit/issues/2107), [#2119](https://github.com/JarryShaw/PyPCAPKit/pull/2119)). +- `engine='pypcap'` warns and falls back to the default engine when the `pcap` module is installed but cannot load, as `engine='pcap_ct'` already did, where it raised the loader's `OSError` ([#2132](https://github.com/JarryShaw/PyPCAPKit/issues/2132), [#2133](https://github.com/JarryShaw/PyPCAPKit/pull/2133)). +- TCP reassembly bounded its holes with `sys.maxsize`, which on GraalPy and 32-bit builds is `2**31 - 1`, inside TCP's sequence space, so a stream past it never completed. The bound is `HOLE_OPEN_BOUND`, `2**63 - 1`, the value 64-bit builds already used ([#1959](https://github.com/JarryShaw/PyPCAPKit/issues/1959), [#1965](https://github.com/JarryShaw/PyPCAPKit/pull/1965)). ### pcapkit.interface @@ -213,6 +220,7 @@ Layer-limited extraction: `layer='link'` no longer sets the termination flag on - **a breaking change to** `pack()`: it no longer keeps the header it builds on the instance it is called on, which in 1.4.1 too left `schema` disagreeing with `info`: `UDP(srcport=1234).pack()` reset `schema.srcport` to 0 while `info.srcport` kept 1234. Once an instance has been parsed or constructed, `pack()` builds on a shallow copy, so `schema`, `info` and what `make()` sets, such as TCP's `connection`, stay as they were. The `Frame`, `PCAPNG` and `HTTPv2` overrides do the same, and re-packing a PCAP-NG block that carries an option allowed only once no longer raises. Code that relied on `pack()` to re-sync `schema` calls `make()` or constructs a new instance ([#1769](https://github.com/JarryShaw/PyPCAPKit/issues/1769), [#1791](https://github.com/JarryShaw/PyPCAPKit/pull/1791)). - **a breaking change to** the HOPOPT, IPv6-Opts and Mobility Header padding: two octets are one `PadN` with no data (`01 00`) rather than two `Pad1` options, as [RFC 8200 Section 4.2](https://datatracker.ietf.org/doc/html/rfc8200#section-4.2) and [RFC 6275 Section 6.2.2](https://datatracker.ietf.org/doc/html/rfc6275#section-6.2.2) ask. Only a freshly built header with exactly two octets of padding changes; a parsed one that carried two `Pad1` options still rebuilds byte for byte ([#1793](https://github.com/JarryShaw/PyPCAPKit/issues/1793), [#1812](https://github.com/JarryShaw/PyPCAPKit/pull/1812)). - **a breaking change to** `NTPTimestamp`: it and `PMIPv6Timestamp` are `Info` classes in `pcapkit.protocols.data.internet.mh` rather than tuples, and `NTPTimestamp` keeps no alias at its 1.4.x home in `pcapkit.protocols.internet.mh`. As Mappings they no longer index, unpack, compare or hash as a tuple does; `.seconds` and `.fraction` still work, and the dumpers write both as keys ([#1886](https://github.com/JarryShaw/PyPCAPKit/issues/1886), [#1887](https://github.com/JarryShaw/PyPCAPKit/pull/1887)). +- **a breaking change to** rebuilding from `to_dict()`: the payload is dispatched only from the `__next_name__` and `__next_type__` keys, and the IPv6 extension-header chain only from `__exthdr__`, never from key order, which on PyPy follows an earlier instance's attribute layout and so picked the wrong layer. `to_dict()`, iteration and `len()` carry the dunder keys a new `Info.__included__` names, and `Info.__excluded__` only keeps a key out of `repr`, `str` and the dumps. A hand-written dict needs those keys, `__short_read__` among them, to rebuild, and a custom `Info` lists its own dunder keys in `__included__` ([#2003](https://github.com/JarryShaw/PyPCAPKit/issues/2003), [#2014](https://github.com/JarryShaw/PyPCAPKit/pull/2014)). #### Fixed @@ -283,7 +291,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - **a breaking change to** `from_data` on a capture truncated by snaplen: `IPv4`, `IPv6` and `UDP` rebuilt their length field from the payload at hand, writing the recorded length, so `big_endian.pcap` frame 3 came back as `45000052` where `450004a2` was on the wire. `_make_data` now passes the parsed length through to `make()`'s new `total_length` (IPv4, UDP) or `payload_length` (IPv6) keyword, which computes the length as before when omitted, so `IPv4(payload=...)` is unchanged ([#1155](https://github.com/JarryShaw/PyPCAPKit/issues/1155)). - **a breaking change to** the layer chain of a capture cut inside a header: a next layer parsed from too few captured octets was zero-filled, and `from_data` rebuilt the fabricated octets. `IPv4.read()` handed UDP the declared payload length when nothing past the IPv4 header was captured, so `test.pcapng` frame 3 rebuilt with 8 invented UDP octets ([#1167](https://github.com/JarryShaw/PyPCAPKit/issues/1167)). Every `_import_next_layer` now goes through one guard: no captured octets gives `NoPayload`, and fewer than the next layer's `__length_hint__` gives `Raw` unless the parsed header fits anyway, as a 12-octet L2TPv2 ZLB does. A header-only IPv6 packet is `IPv6` rather than `IPv6:UDP:Raw`, and frame 3 is `Ethernet:Raw`, rebuilt byte for byte ([#1170](https://github.com/JarryShaw/PyPCAPKit/issues/1170)). The guard needed honest hints, so `IPv6_Route` (4), `MH` (6), `HOPOPT` and `IPv6_Opts` (2) now hint their 8-octet minimum header ([#1173](https://github.com/JarryShaw/PyPCAPKit/issues/1173)), and so does the `IPv6_Ext` fallback, which hinted 2 ([#1178](https://github.com/JarryShaw/PyPCAPKit/issues/1178)). - **a breaking change to** `L2TPv2`, against [RFC 2661](https://datatracker.ietf.org/doc/html/rfc2661) section 3.1. `Type` was inverted; it is now `Data = 0` and `Control = 1`, regenerated in `pcapkit.const` from the fixed `pcapkit.vendor` table, so the same bytes parse to the other `flags.type`, and a default `make()` writes T=0 where it wrote T=1. A control message now gets `L` and a Length field automatically, and the offset pad, which `from_data` zero-filled, is kept as `info.padding` -- a new key in `info` and in dumped output ([#1175](https://github.com/JarryShaw/PyPCAPKit/issues/1175)). `from_data` also cleared `L` and dropped the Length field: `_make_data` returned it as `length`, which `ProtocolBase` takes as the parse length. `make()`'s `length` keyword is now `total_length`, written as given; a call still passing `length` loses `L` silently ([#1172](https://github.com/JarryShaw/PyPCAPKit/issues/1172)). `make()` also gains `length_flag` and `padding`, and raises `ProtocolError` for a `total_length` with `length_flag=False`. -- **a breaking change to** `IPv6.from_data`: it dropped the extension-header chain, so a packet carrying a Routing header rebuilt 48 octets as 40. The chain is now kept in wire order under a hidden `__exthdr__` key, absent from `to_dict` and dumped output, and re-emitted ahead of the upper layer. Sample frames that rebuilt wrong now rebuild exactly, though some that had rebuilt with wrong bytes raised `AttributeError` instead ([#1177](https://github.com/JarryShaw/PyPCAPKit/issues/1177)), from older defects fixed with it: `HIP.alias` read `_info` before `read` had set it, which also broke a standalone HIP parse and `HIP.from_data` ([#1184](https://github.com/JarryShaw/PyPCAPKit/issues/1184)), and the MPL option's maker called `bit_length()` on the `IPv6Address` seed of an S=0 option. MPL in `HOPOPT` and `IPv6_Opts` now takes `S` from the parsed option or a new `seed_type` keyword, and a missing or oversized seed raises `ProtocolError` ([#1185](https://github.com/JarryShaw/PyPCAPKit/issues/1185)). With both fixed, every IPv6 frame of the `options-*.pcap` captures rebuilds byte for byte. +- **a breaking change to** `IPv6.from_data`: it dropped the extension-header chain, so a packet carrying a Routing header rebuilt 48 octets as 40. The chain is now kept in wire order under a hidden `__exthdr__` key, absent from dumped output (`to_dict` carries it since [#2014](https://github.com/JarryShaw/PyPCAPKit/pull/2014), below), and re-emitted ahead of the upper layer. Sample frames that rebuilt wrong now rebuild exactly, though some that had rebuilt with wrong bytes raised `AttributeError` instead ([#1177](https://github.com/JarryShaw/PyPCAPKit/issues/1177)), from older defects fixed with it: `HIP.alias` read `_info` before `read` had set it, which also broke a standalone HIP parse and `HIP.from_data` ([#1184](https://github.com/JarryShaw/PyPCAPKit/issues/1184)), and the MPL option's maker called `bit_length()` on the `IPv6Address` seed of an S=0 option. MPL in `HOPOPT` and `IPv6_Opts` now takes `S` from the parsed option or a new `seed_type` keyword, and a missing or oversized seed raises `ProtocolError` ([#1185](https://github.com/JarryShaw/PyPCAPKit/issues/1185)). With both fixed, every IPv6 frame of the `options-*.pcap` captures rebuilds byte for byte. - **a breaking change to** the SMF_DPD option's maker in `HOPOPT` and `IPv6_Opts`: it overwrote the parsed DPD mode, so H-DPD rebuilt as I-DPD, and inferred the TaggerID type from the Tagger ID's length, so a 4-octet `DEFAULT` Tagger ID rebuilt as IPv4. Both now come from the parsed option, or from `mode` and a new `tid_type` keyword. A Tagger ID that does not fit its type raises `ProtocolError`, so a 17-octet one does where it raised `FieldValueError`, and `tid=b''` builds a NULL Tagger ID where it raised `ValueError` ([#1190](https://github.com/JarryShaw/PyPCAPKit/issues/1190)). An H-DPD option with an empty Hash Assist Value raises `ProtocolError` where it raised a bare `IndexError` ([#1193](https://github.com/JarryShaw/PyPCAPKit/issues/1193)). - a `ForwardMatchField` that read short at the end of the stream rewound by its declared width, not by the octets it read, so the fields after it re-read the preceding octet. An SMF_DPD option in the last two octets of a `HOPOPT` or `IPv6_Opts` header was re-read from the padding and raised a bare `TypeError`; it now raises `FieldValueError`, as CALIPSO and MPL do. A full read rewinds as before ([#1195](https://github.com/JarryShaw/PyPCAPKit/issues/1195)). - `HIP()` with no parameters wrote Header Length 0, below [RFC 7401](https://datatracker.ietf.org/doc/html/rfc7401) section 5.1's minimum of 4 for the two HITs, so it failed to read back with `ProtocolError`; it now writes 4. `read` names a Header Length below 4 in a new `invalid header length` message. Lengths 0 to 3 already raised `ProtocolError`, so only the message changes ([#1189](https://github.com/JarryShaw/PyPCAPKit/issues/1189)). @@ -325,6 +333,9 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `pcapkit.protocols.protocol.__all__` lists `Protocol` beside `ProtocolBase`, so `from pcapkit.protocols.protocol import *` binds the module's public class ([#1770](https://github.com/JarryShaw/PyPCAPKit/issues/1770), [#1778](https://github.com/JarryShaw/PyPCAPKit/pull/1778)). - the data and schema packages re-export what their submodules list in `__all__`, under the prefixes their siblings use: `pcapkit.protocols.data.link` exports the L2TP data model as `L2TP` and `L2TP_Flags` ([#1797](https://github.com/JarryShaw/PyPCAPKit/issues/1797), [#1806](https://github.com/JarryShaw/PyPCAPKit/pull/1806)), and the Mobility Header models, the HOPOPT and IPv6-Opts `SMFDPDOption` and `QuickStartOption`, PCAP-NG's `PacketBlock` and `PACK_*` options and HTTP/2's `Settings` reach their packages and the aggregators ([#1807](https://github.com/JarryShaw/PyPCAPKit/issues/1807), [#1815](https://github.com/JarryShaw/PyPCAPKit/pull/1815)). - the MPL option's `seed` in the HOPOPT and IPv6-Opts schemas is annotated `int | IPv6Address`, since an S=0 option holds the source address ([#1840](https://github.com/JarryShaw/PyPCAPKit/issues/1840), [#1866](https://github.com/JarryShaw/PyPCAPKit/pull/1866)). +- a declared length above `sys.maxsize`, such as a PCAP `incl_len` of `2**31 - 16` on GraalPy or 32-bit CPython, raised `OverflowError` from a read or seek. Every length-driven read in `Schema.unpack()`, `Frame.read()` and `PCAPNG.read()` is bounded at `sys.maxsize`, so such a claim gets the overrun handling it gets on 64-bit CPython ([#1975](https://github.com/JarryShaw/PyPCAPKit/issues/1975), [#1979](https://github.com/JarryShaw/PyPCAPKit/pull/1979)). +- PCAP-NG blocks cut short by the file or by their own framing no longer abort an extraction. A Decryption Secrets Block whose `secrets_length` runs past the block keeps the declared length and warns, its secrets capped at what the block holds ([#1980](https://github.com/JarryShaw/PyPCAPKit/issues/1980), [#1983](https://github.com/JarryShaw/PyPCAPKit/pull/1983)); an unknown, Custom or systemd journal export block the file truncates gets the "block truncated" warning the other block types give ([#1984](https://github.com/JarryShaw/PyPCAPKit/issues/1984), [#1988](https://github.com/JarryShaw/PyPCAPKit/pull/1988)); and a malformed TLS or WireGuard key-log line warns with `ProtocolWarning` and is left out of `entries`, where it raised `FieldValueError` ([#1985](https://github.com/JarryShaw/PyPCAPKit/issues/1985), [#1989](https://github.com/JarryShaw/PyPCAPKit/pull/1989)). A ZigBee NWK or APS key off its fixed width rebuilds as captured, where it was zero-padded or cut to that width ([#1986](https://github.com/JarryShaw/PyPCAPKit/issues/1986), [#2001](https://github.com/JarryShaw/PyPCAPKit/pull/2001)). +- capture timestamps convert without the platform's `fromtimestamp` limits. Windows' C runtime covers only 1970 to 3000, so a PCAP-NG timestamp outside that range fell back to 1970 with `invalid timestamp`, and the Mobility Header's NTP and PMIPv6 timestamps had the same gap. PCAP, PCAP-NG and MH convert through `pcapkit.utilities.datetime.fromtimestamp()`, which gives the stdlib's result on Linux ([#2099](https://github.com/JarryShaw/PyPCAPKit/issues/2099), [#2111](https://github.com/JarryShaw/PyPCAPKit/pull/2111)), and encode through `pcapkit.utilities.datetime.totimestamp()`, since GraalPy's `timestamp()` put a pre-1970 instant with a fraction one second late ([#2134](https://github.com/JarryShaw/PyPCAPKit/issues/2134), [#2136](https://github.com/JarryShaw/PyPCAPKit/pull/2136)). ### pcapkit.toolkit @@ -351,6 +362,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `pcapkit.utilities.logging` as a real interface: `get_logger()` for per-module children, `configure()` to set level, handler, stream, format or propagation at runtime, `reset()` to return to library-neutral, and `ensure_output()`. Each module that logs now does so under its own `__name__`, so a consumer can silence `pcapkit.foundation.registry` while keeping `pcapkit.foundation.extraction` ([#384](https://github.com/JarryShaw/PyPCAPKit/pull/384)). - `pcapkit.utilities.config`, one module for every tunable limit, each read once from a `PCAPKIT_*` environment variable: the frame-layer and extension-header limits, the pypcapfile engine's TCP-over-IPv6 warning limit, the dumpkit fallback, the charset-detection cache, the read chunk and the zero-padding guards. A bad value warns with `ConfigWarning` and falls back to the default; `-1` lifts a limit. The homes these limits had in the prereleases keep no re-export ([#1726](https://github.com/JarryShaw/PyPCAPKit/issues/1726), [#1743](https://github.com/JarryShaw/PyPCAPKit/pull/1743)). +- `pcapkit.utilities.datetime`, holding `fromtimestamp()` and `totimestamp()`, the timestamp conversions of the capture-timestamp entry under `pcapkit.protocols` ([#2111](https://github.com/JarryShaw/PyPCAPKit/pull/2111), [#2136](https://github.com/JarryShaw/PyPCAPKit/pull/2136)). Its first name, `pcapkit.utilities.timestamp`, shipped in no release. #### Changed @@ -366,6 +378,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `stacklevel()` returns a level relative to its caller, as `warnings.warn` and `logging` count it, so a warning or a `BaseError` log record names the caller's frame at any stack depth rather than a frame inside `logging` or pcapkit ([#397](https://github.com/JarryShaw/PyPCAPKit/pull/397)). - re-executing `pcapkit.utilities.logging` under `PCAPKIT_DEVMODE` keeps one stderr handler, where each `importlib.reload` added another ([#391](https://github.com/JarryShaw/PyPCAPKit/pull/391)). - a loud `BaseError` no longer sets the process-global `sys.tracebacklimit`, which truncated every later traceback, pcapkit's or not; lazily installed `sys.excepthook` and `threading.excepthook` keep its output terse instead ([#983](https://github.com/JarryShaw/PyPCAPKit/pull/983)). +- an import-time warning names the importing module on GraalPy too, where the importer's stack level counted the bootstrap frames GraalPy's `warnings.warn` skips, attributing the warning to `:0` ([#1963](https://github.com/JarryShaw/PyPCAPKit/issues/1963), [#1996](https://github.com/JarryShaw/PyPCAPKit/pull/1996)). ### pcapkit.vendor @@ -396,13 +409,16 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `CITATION.cff`, citation metadata in Citation File Format 1.2.0, which GitHub renders as the "Cite this repository" button and which citation managers and dependency inventories read directly. It is the machine-readable half of the attribution BSD-3-Clause already asks for. Validated with `cffconvert --validate` and against the published 1.2.0 schema; `doi` and `orcid` are omitted rather than invented, since neither exists for this project and both are checked formats, so a wrong value would still validate. The licence is unchanged (BSD-3-Clause, no `NOTICE`). Alongside it the copyright line moves from `2018-2023` to `2018-2026` (`LICENSE` was its only occurrence; `docs/source/conf.py` derives its own from the current year), and a stray `s` after the closing `DAMAGE.` of the licence text, present since the Mozilla-to-BSD relicence, is removed so the wording matches canonical BSD-3-Clause ([#615](https://github.com/JarryShaw/PyPCAPKit/pull/615)). - `examples/generators/endian.py`, and the byte-order tests that read what it writes. There was no big-endian `.pcap` in the repository, which is why [#605](https://github.com/JarryShaw/PyPCAPKit/issues/605) survived its own code review: the fix to a misspelled key left the corrected path as untested as the broken one. The generator writes `big_endian.pcap` (magic `a1 b2 c3 d4`), `big_endian_nanosecond.pcap` (`a1 b2 3c 4d`, the first fixture to take that branch of the magic-number table) and `little_endian.pcap` (`d4 c3 b2 a1`), carrying the *same records* in each, so tests can assert that byte order makes no difference to what is read rather than only matching numbers written in a test. Frame 3 is captured short (1200 octets on the wire, 96-octet `snaplen`), so `incl_len` and `orig_len` differ and cannot both be satisfied by one byte-swapped value. `test_frame_endian_runtime.py` drives each file through `extract()` and derives expectations by walking its record chain with `struct`; a unit-tier case in `test_header_frame_unit.py` builds a big-endian capture in memory, so the fixture-free selection CI runs on every push catches it too. Each fails on the unfixed tree (the fixture-backed ones by that `ValueError`, the in-memory one by `AssertionError: 3106905 != 1500000000`) while the little-endian twin passes on both, showing the records themselves are not the variable ([#605](https://github.com/JarryShaw/PyPCAPKit/issues/605)). - local type stubs under `typings/` for dpkt, pyshark, `pcap` and pypcapfile, declaring what pcapkit uses of each, so mypy checks the engines against them. `mypy.ini` reads them through `mypy_path`, and neither the sdist nor the wheel ships them ([#1878](https://github.com/JarryShaw/PyPCAPKit/issues/1878), [#1882](https://github.com/JarryShaw/PyPCAPKit/pull/1882)). +- a glossary page for the terms with several meanings -- registry, schema, data model, info and default engine -- each sense linked to the page that explains it ([#1949](https://github.com/JarryShaw/PyPCAPKit/issues/1949), [#1955](https://github.com/JarryShaw/PyPCAPKit/pull/1955)). +- the engine benchmark on GitHub Actions: `benchmark.yml`, weekly and on dispatch, measures every engine on the runner's own interpreters through `examples/benchmark/native.sh` and posts the report to the *CI reports* discussions ([#2125](https://github.com/JarryShaw/PyPCAPKit/issues/2125), [#2128](https://github.com/JarryShaw/PyPCAPKit/pull/2128)). A `pcap` engine whose module cannot load is an explained row rather than a fatal error ([#2129](https://github.com/JarryShaw/PyPCAPKit/issues/2129), [#2131](https://github.com/JarryShaw/PyPCAPKit/pull/2131)), and the report is reST with footnotes, so it pastes unedited onto `docs/source/engines.rst` ([#2142](https://github.com/JarryShaw/PyPCAPKit/pull/2142)), the page the engine comparison and prerequisites move to from `index.rst`, carrying the first run's figures (`f9de043d6`, `d7aa41535`). The tables had first been regenerated with Python 3.15 measured ([#2109](https://github.com/JarryShaw/PyPCAPKit/pull/2109)). +- a win-64 Conda package ([#2060](https://github.com/JarryShaw/PyPCAPKit/issues/2060)). Conda Update builds and smoke-tests it ([#2067](https://github.com/JarryShaw/PyPCAPKit/pull/2067)), reading the environment without `os.environb` ([#2120](https://github.com/JarryShaw/PyPCAPKit/issues/2120), [#2122](https://github.com/JarryShaw/PyPCAPKit/pull/2122)) and conda-forge's CFEP-27 site-packages layout ([#2126](https://github.com/JarryShaw/PyPCAPKit/issues/2126), [#2127](https://github.com/JarryShaw/PyPCAPKit/pull/2127)); then it publishes from Conda Update and Create Release, and a release is complete only once the platforms its tagged commit lists, win-64 among them, are on Anaconda ([#2138](https://github.com/JarryShaw/PyPCAPKit/pull/2138)). A manual Conda Update run builds only (`c5081fbfe`) unless dispatched on `main` with `publish`, which forces the bump that ships a release's missing Anaconda files ([#2150](https://github.com/JarryShaw/PyPCAPKit/pull/2150)), and Create Release's warning about an unfinished release names that remedy when Anaconda files are what is missing ([#2140](https://github.com/JarryShaw/PyPCAPKit/issues/2140), [#2151](https://github.com/JarryShaw/PyPCAPKit/pull/2151)). #### Changed - **a breaking change to** `requires-python`: it is `>=3.8`, so pip refuses an older interpreter at install, as "requires a different Python". The sources use 3.8 syntax, and the `bpc-*` backport tools that converted it for 3.6 and 3.7 are no longer maintained, so the conversion goes: the `bpc-*` requirements, `refactor()` and the `setup.py` build commands that ran it. The 3.7 KTLO (*keep the lights on*) legs go with it, and `coverage-comment.yml` lists no install failure as by design any more, which closes [#2040](https://github.com/JarryShaw/PyPCAPKit/issues/2040) ([#2051](https://github.com/JarryShaw/PyPCAPKit/issues/2051), [#2053](https://github.com/JarryShaw/PyPCAPKit/pull/2053)). - `setup.py` is gone, and `pyproject.toml` is the only build configuration. With the 3.8 floor it was a Python 2 guard and a bare `setup()`: the wheel's files are identical without it, `pip install -e .` goes through PEP 660, pip still refuses 3.7 as "requires a different Python", and on 2.7 it fails on the `setuptools` build requirement before it would reach `setup.py`. `examples/benchmark/Dockerfile` stops copying it, and a test fails should `setup.py` return or a `COPY` there name a file the repository does not track. Dependabot now fetches `pyproject.toml` in `setup.py`'s place and passes it through unstubbed; the static `readme` stays, as it does not depend on what a stub would supply ([#2061](https://github.com/JarryShaw/PyPCAPKit/issues/2061), [#2066](https://github.com/JarryShaw/PyPCAPKit/pull/2066)). - renames with no compatibility alias left behind: `examples/sample` and `examples/samples` -- one letter apart, holding different things -- are now `examples/captures` and `examples/generators`. -- the README is a landing page now, and Markdown rather than reStructuredText. `README.rst` became a much shorter `README.md`, keeping what a reader arriving from PyPI or a search result needs (what the library is, why it exists rather than Scapy or DPKT, how to install it, a worked example, where the documentation lives). **Module Structure**, **Engine Comparison**, **Engine support by Python version**, **Test Environment**, **Test Results** and **Installation Notes** were already duplicated in fuller form in `docs/source/index.rst`, so they are linked rather than restated; the blocks that existed nowhere else moved: **Testing** is now `docs/source/contributing/testing.rst`, and the `pipenv` and `make setup` block joined the Installation section of `docs/source/index.rst`. Requested by the project owner, and a deliberate exception to the reStructuredText convention, for the README only, since its renderers are GitHub and PyPI rather than Sphinx. `setup.py` reads `README.md` and declares `text/markdown`, and `MANIFEST.in` gains `include README.md`, which is belt-and-braces rather than load-bearing: setuptools' `sdist` ships a `README.md` regardless. Of the `include` lines only `CHANGELOG.md` is load-bearing, measured in the [#631](https://github.com/JarryShaw/PyPCAPKit/pull/631) entry below. `examples/benchmark/Dockerfile` copies `README.md`, while the `README.md` that `examples/benchmark/benchmark.py` names is the benchmark suite's own ([#619](https://github.com/JarryShaw/PyPCAPKit/pull/619)). +- the README is a landing page now, and Markdown rather than reStructuredText. `README.rst` became a much shorter `README.md`, keeping what a reader arriving from PyPI or a search result needs (what the library is, why it exists rather than Scapy or DPKT, how to install it, a worked example, where the documentation lives). **Module Structure**, **Engine Comparison**, **Engine support by Python version**, **Test Environment**, **Test Results** and **Installation Notes** were already duplicated in fuller form in `docs/source/index.rst`, so they are linked rather than restated; the blocks that existed nowhere else moved: **Testing** is now `docs/source/contributing/testing.rst`, and the `pipenv` and `make setup` block joined the Installation section of `docs/source/index.rst`. Requested by the project owner, and a deliberate exception to the reStructuredText convention, for the README only, since its renderers are GitHub and PyPI rather than Sphinx. `setup.py` read `README.md` and declared `text/markdown` (`pyproject.toml` declares the `readme` statically since [#1994](https://github.com/JarryShaw/PyPCAPKit/pull/1994)), and `MANIFEST.in` gains `include README.md`, which is belt-and-braces rather than load-bearing: setuptools' `sdist` ships a `README.md` regardless. Of the `include` lines only `CHANGELOG.md` is load-bearing, measured in the [#631](https://github.com/JarryShaw/PyPCAPKit/pull/631) entry below. `examples/benchmark/Dockerfile` copies `README.md`, while the `README.md` that `examples/benchmark/benchmark.py` names is the benchmark suite's own ([#619](https://github.com/JarryShaw/PyPCAPKit/pull/619)). - `CODE_OF_CONDUCT.md` moves from Contributor Covenant 1.4 to 3.0, at the owner's request. The text is the canonical 3.0 Markdown fetched from https://www.contributor-covenant.org/version/3/0/code_of_conduct/code_of_conduct.md rather than transcribed. Some things needed deciding. 3.0's `[NOTE` placeholders are filled or removed: the reporting channel names `jarryshaw@icloud.com` (the contact 1.4 carried and `SECURITY.md` uses as email fallback) plus GitHub's report-abuse form for a report about the maintainer, which a single-maintainer project cannot otherwise cover; the enforcement placeholder is an instruction to the adopter and is removed. 3.0's plural "Community Moderators" become the singular maintainer, this repository having no moderators. The enforcement ladder (Warning, Temporarily Limited Activities, Temporary Suspension, Permanent Ban) is a suggestion and is **kept**, each rung mapping to a lever one person holds on GitHub. 3.0 is CC BY-SA 4.0 where 1.4 carried no licence notice, so the attribution names version 3.0, links the permanent `version/3/0/` URL, carries the CC BY-SA 4.0 notice, indicates changes were made as BY requires, and says the share-alike term covers this document only (the code remains BSD-3-Clause, `LICENSE` untouched). The ladder renders as nested list items, checked against GitHub's Markdown API; [#613](https://github.com/JarryShaw/PyPCAPKit/pull/613) had to repair the 1.4 file when a stray list marker collapsed it into one nested item ([#624](https://github.com/JarryShaw/PyPCAPKit/pull/624)). - reconciled which private (`_xxx`) attributes and methods the Sphinx build documents, replacing an ad hoc mix with one rule: document the contract, hide the recipe. A member every subclass must implement, or whose shape a caller depends on, stays documented despite its underscore; a private helper that only keeps one method short does not. Module-level directives for pure implementation detail (`esp._resolve`, `esp._CRYPTO`, `ngap._convert`, `ngap._revert`, `ngap._PYCRATE`, `ngap._PDU_LOCK`, `pypcapfile._NamedStream`) were dropped, while `autoattribute` directives were added for class-private state that *is* contract, among them `Extractor._flag_f`, the `PyPCAP` and `PCAP_CT` engines' `_backend` (the only third-party engines with one), `TraceFlow`'s internal fields, `FieldBase`/`Field` internals, the built-in `PCAP` engine's `_gbhdr`, `_vinfo`, `_nnsec` and `_dlink` (shared with `PyPCAP`, `PCAP_CT` and `PyPCAPFile`) and `PCAPNG`'s `_ctx`/`_ctx_list`. The runtime definitions of `_missing_` (under `pcapkit.const`, and inline in `pcapkit.protocols`) gained one write-up in place of a directive per class, a new "Unrecognised Values" section in `docs/source/pcapkit/const/index.rst` cross-referenced from `registry.rst` (`conf.py` already excludes `_missing_` from every `autoclass`). `CONTRIBUTING.md` gained the rule as a named section. The new Sphinx warning, a bare `Type` newly rendered by `TraceFlow._foutio`'s directive, is [#709](https://github.com/JarryShaw/PyPCAPKit/issues/709). No line under `pcapkit/` changed ([#684](https://github.com/JarryShaw/PyPCAPKit/issues/684)). - `examples/captures/out.json`, `out.plist`, `out.txt` and `pcapng.txt` are no longer tracked in git; they are build output, not fixtures, and a tracked rendering with no reader goes stale silently. `pcapng.txt` is exactly that: it still recorded `packet -> NIL` for the Enhanced Packet Blocks of `dhcp.pcapng` long after [#683](https://github.com/JarryShaw/PyPCAPKit/pull/683) gave those blocks their captured octets back. Rather than regenerate it again and leave the drift free to recur, these files are removed from the index and folded into `examples/captures/`'s blanket `.gitignore` rule; the genuine inputs `in.pcap` and `dhcp.pcapng` stay tracked. `examples/legacy_smoke/Makefile` and its `README.md` now describe regenerating these reports via `make fixtures`, and `tests/project/test_capture_tracking.py` pins the invariant ([#685](https://github.com/JarryShaw/PyPCAPKit/issues/685)). @@ -413,9 +429,11 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - Ruleset `23497679`'s `required_status_checks` names exact check contexts, and GitHub rulesets match literally with no wildcard, so the list needs hand-editing on every matrix change. The old `Engines Python ` names in it are dead since [#849](https://github.com/JarryShaw/PyPCAPKit/pull/849); the `Compat Python 3.10`-`3.14` names stay live, emitted by `python-compatibility.yml`, and are untouched. Adds a job, `required-checks` (context `Required checks passed`), depending on `[test, integration, engine-tests, pypcap-parity]` with `if: always() && inputs.gate-only != true` and an explicit per-dependency check that fails loudly, naming the job, on anything but `success`: a bare `needs:` list would leave it *skipped* rather than failed when a dependency failed, and GitHub's docs list a skipped required check as passing. That context is meant to replace the ruleset's `test`, `integration`, `Engines` and `pypcap-parity` entries once the ruleset is updated separately, as a repository setting outside the change; the `Compat` names are excluded from the replacement. Only `.github/workflows/unit-tests.yml` changes; no library or test code ([#856](https://github.com/JarryShaw/PyPCAPKit/pull/856)). - the prose the post-[#719](https://github.com/JarryShaw/PyPCAPKit/issues/719) defect wave added or touched, checked against the code; no code changes. `MesgIDOption.seconds` in the Mobility Header schema said seconds since 1970 where it holds an [RFC 1305](https://datatracker.ietf.org/doc/html/rfc1305) NTP count from 1900; `HTTP._guess_version()` comments predated the HTTP/2 gate; and the `_parse_next_layer` note miscounted the captured octets. `Extractor._open_output()` now says only the PCAP engine passes header fields, `Extractor.run()` and the PCAP-NG engine's `run` list the `FormatError` for PCAP output from a PCAP-NG input, and the extraction and engine pages document the PCAP-output path ([#1179](https://github.com/JarryShaw/PyPCAPKit/pull/1179), [#1180](https://github.com/JarryShaw/PyPCAPKit/pull/1180), [#1186](https://github.com/JarryShaw/PyPCAPKit/pull/1186), [#1103](https://github.com/JarryShaw/PyPCAPKit/issues/1103)). - **a breaking change to** the `all` extra: it carries the core addons only -- `cli`, `crypto` and `NGAP`. The third-party engines 1.4.x bundled in it (`dpkt`, `scapy` and `pyshark`) and the vendor crawler's `requests` and `beautifulsoup4` are no longer in it; the engines keep their own `DPKT`, `Scapy` and `PyShark` extras and the crawler its `vendor` extra, and `pypcapfile` has a new `PyPCAPFile` extra of its own. A new `dev` extra carries what CI needs ([#910](https://github.com/JarryShaw/PyPCAPKit/issues/910), [#914](https://github.com/JarryShaw/PyPCAPKit/pull/914)). -- the `Scapy` and `dev` extras require `scapy>=2.6.0` ([#1572](https://github.com/JarryShaw/PyPCAPKit/issues/1572), [#1578](https://github.com/JarryShaw/PyPCAPKit/pull/1578)), and the `docs` extra `Sphinx>=7.3`, the first release with `versionremoved` ([#1743](https://github.com/JarryShaw/PyPCAPKit/pull/1743)), and `sphinx-autodoc-typehints==3.15.0` ([#1856](https://github.com/JarryShaw/PyPCAPKit/issues/1856), [#1877](https://github.com/JarryShaw/PyPCAPKit/pull/1877)). The `test` extra gains `psutil`, `markdown-it-py` and `lxml`, which the scripts under `util/` use ([#1894](https://github.com/JarryShaw/PyPCAPKit/issues/1894), [#1896](https://github.com/JarryShaw/PyPCAPKit/pull/1896)). +- the `Scapy` and `dev` extras require `scapy>=2.6.0` ([#1572](https://github.com/JarryShaw/PyPCAPKit/issues/1572), [#1578](https://github.com/JarryShaw/PyPCAPKit/pull/1578)), and the `docs` extra `Sphinx>=7.3`, the first release with `versionremoved` ([#1743](https://github.com/JarryShaw/PyPCAPKit/pull/1743)), and an exact `sphinx-autodoc-typehints` pin ([#1856](https://github.com/JarryShaw/PyPCAPKit/issues/1856), [#1877](https://github.com/JarryShaw/PyPCAPKit/pull/1877)), 3.17.2 since the weekly dependency update ([#1944](https://github.com/JarryShaw/PyPCAPKit/pull/1944)). The `test` extra gains `psutil`, `markdown-it-py` and `lxml`, which the scripts under `util/` use ([#1894](https://github.com/JarryShaw/PyPCAPKit/issues/1894), [#1896](https://github.com/JarryShaw/PyPCAPKit/pull/1896)). The `crypto` and `all` extras cap `cryptography` below 46 on PyPy older than 3.11, for which no later release has a Linux wheel ([#2040](https://github.com/JarryShaw/PyPCAPKit/issues/2040), [#2046](https://github.com/JarryShaw/PyPCAPKit/pull/2046)). - the READMEs of `examples/benchmark` and `examples/legacy_smoke` are Markdown, `README.md` rather than `README.rst`, and `MANIFEST.in` ships them through `recursive-include examples *.md`. Stale build leftovers go: `setup.py`'s `bdist_wheel` override, since `build_py` and `install` already convert the tree the wheel archives, and `MANIFEST.in`'s `prune test`, which named no directory; `make profile` runs in `examples/legacy_smoke` ([#1773](https://github.com/JarryShaw/PyPCAPKit/issues/1773), [#1780](https://github.com/JarryShaw/PyPCAPKit/pull/1780)). `MANIFEST.in` also drops the lines that matched nothing -- `global-include *.rst`, `include pcapkit/_extern` and `exclude pcapkit/protocols/*/NotImplemented` -- and the source distribution and wheel are unchanged ([#1794](https://github.com/JarryShaw/PyPCAPKit/issues/1794), [#1813](https://github.com/JarryShaw/PyPCAPKit/pull/1813)). - the contributor pages state no counts that a test or the Python version updater pinned, and each pin checks the fact it stood for instead ([#1811](https://github.com/JarryShaw/PyPCAPKit/issues/1811), [#1817](https://github.com/JarryShaw/PyPCAPKit/pull/1817)). +- the workflows' supply chain ([#1968](https://github.com/JarryShaw/PyPCAPKit/issues/1968)): every third-party action is pinned to a commit SHA that Dependabot updates by pull request ([#1981](https://github.com/JarryShaw/PyPCAPKit/pull/1981)); no third-party step runs after the deploy key is loaded, and every workflow declares its top-level permissions, the GitHub Pages build read-only beside a separate publishing job ([#1970](https://github.com/JarryShaw/PyPCAPKit/pull/1970)); and the cron jobs name the environments that hold their secrets ([#1993](https://github.com/JarryShaw/PyPCAPKit/pull/1993)). `pyproject.toml` declares the `readme` statically, so Dependabot can build the editable install ([#1994](https://github.com/JarryShaw/PyPCAPKit/pull/1994)). +- CodeQL skips `tests/`, where the recurring clear-text-logging alerts in `pcapkit.foundation.extraction` and `pcapkit.corekit.io` traced to a test fixture's `secrets` names; `pcapkit/` is still scanned by every query ([#2130](https://github.com/JarryShaw/PyPCAPKit/issues/2130), [#2139](https://github.com/JarryShaw/PyPCAPKit/pull/2139)). The advisory linters drop their long-standing false positives with no behaviour change: vermin honours the `# novermin` markers and confirms the 3.8 floor, bandit reports nothing, and the unused `type: ignore` comments go, from the vendor templates too ([#2145](https://github.com/JarryShaw/PyPCAPKit/issues/2145), [#2147](https://github.com/JarryShaw/PyPCAPKit/pull/2147)). #### Fixed @@ -431,12 +449,14 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `test_every_tracked_name_exists_and_matches_git` in `tests/test_tier_guard.py` checked neither the git index nor the filesystem despite its name: it asserted only that the tracked-capture list was non-empty and no name contained a `/`, so a hardcoded list passed whether or not it matched reality. Sibling `test_capture_suggestions_are_captures` had the same gap. Fixed in [#703](https://github.com/JarryShaw/PyPCAPKit/pull/703); [#703](https://github.com/JarryShaw/PyPCAPKit/pull/703)'s description names both [#700](https://github.com/JarryShaw/PyPCAPKit/issues/700) and [#708](https://github.com/JarryShaw/PyPCAPKit/issues/708). The first test now re-derives the expected set from `git ls-files -z` under `examples/captures/`, asserts equality against it, and checks each name with `Path.is_file()`; its sibling re-implements `committed_capture_names()`'s filter inline, which catches a wrong filter but still trusts `committed_captures()` for the tracked set. A hardcoded literal that happens to match the tracked names still passes; the test catches a set that is wrong when it runs ([#708](https://github.com/JarryShaw/PyPCAPKit/issues/708)). - `pcapkit.all.__all__` listed `OSPF`, `RARP` and `DRARP` on its Link Layer line after [#719](https://github.com/JarryShaw/PyPCAPKit/issues/719) moved them ([#1124](https://github.com/JarryShaw/PyPCAPKit/issues/1124)), and omitted names its packages export, among them `ESP`, `HTTPv1`, `HTTPv2`, `PCAPNG` and `EnumRegistry`, so `from pcapkit.all import *` did not bind them. `pcapkit.utilities` stays unlisted deliberately, so generic helpers such as `warn` do not arrive through `import *` ([#1136](https://github.com/JarryShaw/PyPCAPKit/issues/1136)). - `examples/generators/options.py` wrote every HTTP/2 sample frame on stream 0, which [RFC 9113](https://datatracker.ietf.org/doc/html/rfc9113) forbids for `DATA`, `HEADERS` and the other stream-level types, so those frames in `options-transport.pcap` decoded as `TCP:Raw`. `_httpv2_build` now puts `SETTINGS`, `PING` and `GOAWAY` on stream 0 and every other type on stream 1, with `PUSH_PROMISE` promising stream 2, and every HTTP/2 sample frame decodes as `TCP:HTTP/2`; no other generated capture changes ([#1156](https://github.com/JarryShaw/PyPCAPKit/issues/1156)). -- `make_samples.py` builds fixtures from the tree it runs in, not the shared checkout ([#1343](https://github.com/JarryShaw/PyPCAPKit/issues/1343), [#1388](https://github.com/JarryShaw/PyPCAPKit/pull/1388)), and retries the Wireshark sample downloads on 429, 5xx and timeouts ([#1658](https://github.com/JarryShaw/PyPCAPKit/issues/1658), [#1659](https://github.com/JarryShaw/PyPCAPKit/pull/1659)). +- `make_samples.py` builds fixtures from the tree it runs in, not the shared checkout ([#1343](https://github.com/JarryShaw/PyPCAPKit/issues/1343), [#1388](https://github.com/JarryShaw/PyPCAPKit/pull/1388)), and retries the Wireshark sample downloads on 429, 5xx and timeouts ([#1658](https://github.com/JarryShaw/PyPCAPKit/issues/1658), [#1659](https://github.com/JarryShaw/PyPCAPKit/pull/1659)). `examples/generators/options.py`'s deadline discards a `SIGALRM` still pending as it puts the old handler back, which on GraalPy could kill the process ([#2090](https://github.com/JarryShaw/PyPCAPKit/issues/2090), [#2091](https://github.com/JarryShaw/PyPCAPKit/pull/2091)). - the Conda Update workflow tags and uploads each bump exactly once ([#1632](https://github.com/JarryShaw/PyPCAPKit/issues/1632), [#1641](https://github.com/JarryShaw/PyPCAPKit/pull/1641)), stays green when a merge races its push ([#1649](https://github.com/JarryShaw/PyPCAPKit/issues/1649), [#1669](https://github.com/JarryShaw/PyPCAPKit/pull/1669)), and keeps the release's name and notes when it uploads ([#1657](https://github.com/JarryShaw/PyPCAPKit/issues/1657), [#1678](https://github.com/JarryShaw/PyPCAPKit/pull/1678)). Vendor Update commits the new modules a crawl adds ([#1685](https://github.com/JarryShaw/PyPCAPKit/issues/1685), [#1688](https://github.com/JarryShaw/PyPCAPKit/pull/1688)), and runs `tests/const` and `tests/vendor` on the regenerated tree first, keeping only the modules they pass with ([#1853](https://github.com/JarryShaw/PyPCAPKit/issues/1853), [#1879](https://github.com/JarryShaw/PyPCAPKit/pull/1879)). - package `__all__` lists match what each package holds: several disagreed with it, and `from pcapkit.protocols.data import *` raised `AttributeError` on the unbound `HOPOPT_QuickStartOption` ([#515](https://github.com/JarryShaw/PyPCAPKit/issues/515), [#527](https://github.com/JarryShaw/PyPCAPKit/pull/527)); the public attributes several packages left out are listed ([#533](https://github.com/JarryShaw/PyPCAPKit/issues/533), [#544](https://github.com/JarryShaw/PyPCAPKit/pull/544)). - the documentation build's "more than one target found" warnings, fixed where they arise: an implicit reference to `dict`, `type` or `set` no longer matches a registry member such as `TCP.dict`, an enum member's docstring holding a URL no longer gains a spurious type field, and the hand-written `TypeVar` directives take the bare name under their `.. module::` ([#1775](https://github.com/JarryShaw/PyPCAPKit/issues/1775), [#1790](https://github.com/JarryShaw/PyPCAPKit/pull/1790)). - the Mermaid diagrams render at their natural size, shrunk only to the page width, where each was fitted into a fixed box, and their `click` targets are relative to the page, so they resolve under the site's `/PyPCAPKit/` prefix ([#1103](https://github.com/JarryShaw/PyPCAPKit/issues/1103), [#1819](https://github.com/JarryShaw/PyPCAPKit/pull/1819)). -- the API reference renders what the code declares. `Extractor._vfunc` keeps its annotation ([#1839](https://github.com/JarryShaw/PyPCAPKit/issues/1839), [#1867](https://github.com/JarryShaw/PyPCAPKit/pull/1867)), and the HTTP/2 frame schemas' and the engine classes' annotations resolve ([#1857](https://github.com/JarryShaw/PyPCAPKit/issues/1857), [#1870](https://github.com/JarryShaw/PyPCAPKit/pull/1870)). Under the pinned sphinx-autodoc-typehints 3.15.0 ([#1856](https://github.com/JarryShaw/PyPCAPKit/issues/1856), [#1877](https://github.com/JarryShaw/PyPCAPKit/pull/1877)), `conf.py` renders an `aenum` base as such, keeps a default as written (`byteorder=sys.byteorder` rather than the build host's value), gives a class the parameters of the `__init__` its `__new__` hides and an enumeration `(*values)`, keeps table ids unique, and stops the source index linking a missing page ([#1858](https://github.com/JarryShaw/PyPCAPKit/issues/1858) to [#1863](https://github.com/JarryShaw/PyPCAPKit/issues/1863), [#1881](https://github.com/JarryShaw/PyPCAPKit/pull/1881)). The generated const tables carry no version directives; each note is on the module's hand-written page instead ([#1884](https://github.com/JarryShaw/PyPCAPKit/issues/1884), [#1895](https://github.com/JarryShaw/PyPCAPKit/pull/1895)). +- the API reference renders what the code declares. `Extractor._vfunc` keeps its annotation ([#1839](https://github.com/JarryShaw/PyPCAPKit/issues/1839), [#1867](https://github.com/JarryShaw/PyPCAPKit/pull/1867)), and the HTTP/2 frame schemas' and the engine classes' annotations resolve ([#1857](https://github.com/JarryShaw/PyPCAPKit/issues/1857), [#1870](https://github.com/JarryShaw/PyPCAPKit/pull/1870)). Under the pinned sphinx-autodoc-typehints ([#1856](https://github.com/JarryShaw/PyPCAPKit/issues/1856), [#1877](https://github.com/JarryShaw/PyPCAPKit/pull/1877)), `conf.py` renders an `aenum` base as such, keeps a default as written (`byteorder=sys.byteorder` rather than the build host's value), gives a class the parameters of the `__init__` its `__new__` hides and an enumeration `(*values)`, keeps table ids unique, and stops the source index linking a missing page ([#1858](https://github.com/JarryShaw/PyPCAPKit/issues/1858) to [#1863](https://github.com/JarryShaw/PyPCAPKit/issues/1863), [#1881](https://github.com/JarryShaw/PyPCAPKit/pull/1881)). The generated const tables carry no version directives; each note is on the module's hand-written page instead ([#1884](https://github.com/JarryShaw/PyPCAPKit/issues/1884), [#1895](https://github.com/JarryShaw/PyPCAPKit/pull/1895)). +- cross-references resolve or render as literals. A role into a package with no inventory is a literal or an `rst_epilog` link ([#1919](https://github.com/JarryShaw/PyPCAPKit/issues/1919), [#1928](https://github.com/JarryShaw/PyPCAPKit/pull/1928), [#1936](https://github.com/JarryShaw/PyPCAPKit/issues/1936), [#1939](https://github.com/JarryShaw/PyPCAPKit/pull/1939), [#1920](https://github.com/JarryShaw/PyPCAPKit/issues/1920), [#1945](https://github.com/JarryShaw/PyPCAPKit/pull/1945)); a `pcap` module mention links the note on PyPCAP and pcap-ct ([#1948](https://github.com/JarryShaw/PyPCAPKit/issues/1948), [#1952](https://github.com/JarryShaw/PyPCAPKit/pull/1952)); library-internal names link, and the classes and special methods they name are documented ([#1940](https://github.com/JarryShaw/PyPCAPKit/issues/1940), [#1946](https://github.com/JarryShaw/PyPCAPKit/pull/1946), [#1947](https://github.com/JarryShaw/PyPCAPKit/issues/1947), [#1953](https://github.com/JarryShaw/PyPCAPKit/pull/1953)), the const registries' `get` overrides among them ([#1921](https://github.com/JarryShaw/PyPCAPKit/issues/1921), [#1935](https://github.com/JarryShaw/PyPCAPKit/pull/1935)); and a `Warns:` section's warning classes link as `Raises:` entries do ([#2094](https://github.com/JarryShaw/PyPCAPKit/issues/2094), [#2096](https://github.com/JarryShaw/PyPCAPKit/pull/2096)). `conf.py` resolves a decorated function's overloads and qualifies a third-party class whose bare name collides ([#1917](https://github.com/JarryShaw/PyPCAPKit/issues/1917), [#1918](https://github.com/JarryShaw/PyPCAPKit/issues/1918), [#1929](https://github.com/JarryShaw/PyPCAPKit/pull/1929)). The `versionchanged` directives on `Info` iteration and `Raw` name `1.3.1.post8`, the release that shipped their change ([#1933](https://github.com/JarryShaw/PyPCAPKit/issues/1933), [#1934](https://github.com/JarryShaw/PyPCAPKit/pull/1934)). +- the Conda release legs: Create Release's Anaconda completeness check counts osx-arm64 ([#1922](https://github.com/JarryShaw/PyPCAPKit/issues/1922), [#1932](https://github.com/JarryShaw/PyPCAPKit/pull/1932)); the `macos-26-intel` legs install Miniforge, upload with the `anaconda` CLI where the upload action's download returned 404, and build again past upstream conda and conda-build breakage ([#2044](https://github.com/JarryShaw/PyPCAPKit/issues/2044), [#2045](https://github.com/JarryShaw/PyPCAPKit/pull/2045), [#2049](https://github.com/JarryShaw/PyPCAPKit/pull/2049), [#2123](https://github.com/JarryShaw/PyPCAPKit/issues/2123), [#2124](https://github.com/JarryShaw/PyPCAPKit/pull/2124)); and `util/conda_vendor.py`'s pin refresh warns when it cannot move a platform pin, or when a pinned release has lost a wheel ([#2025](https://github.com/JarryShaw/PyPCAPKit/issues/2025), [#2026](https://github.com/JarryShaw/PyPCAPKit/pull/2026), [#2028](https://github.com/JarryShaw/PyPCAPKit/issues/2028), [#2035](https://github.com/JarryShaw/PyPCAPKit/pull/2035)). - `pcapkit-cli`'s help strings say what each option does: `-o` describes the output rather than the input, an extension is appended only under `--auto-extension`, and each format flag names the flags that take precedence over it ([#1796](https://github.com/JarryShaw/PyPCAPKit/issues/1796), [#1810](https://github.com/JarryShaw/PyPCAPKit/pull/1810)). - the Conda package: its vendored dependencies are the package's `dependencies` and its `all` extra, with their requirements, shipped with their data files rather than as `.py` files only ([#1814](https://github.com/JarryShaw/PyPCAPKit/issues/1814), [#1821](https://github.com/JarryShaw/PyPCAPKit/pull/1821)); the `osx-64` package is built on an Intel runner, where converting the arm64 build shipped arm64 binaries ([#1822](https://github.com/JarryShaw/PyPCAPKit/issues/1822), [#1825](https://github.com/JarryShaw/PyPCAPKit/pull/1825)); and on Intel macOS it pins `cryptography` 48.0.1, which has a wheel for that platform where the newer pin has none ([#1827](https://github.com/JarryShaw/PyPCAPKit/issues/1827), [#1828](https://github.com/JarryShaw/PyPCAPKit/pull/1828)). `util/conda_vendor.py --check` reports a package pinned differently under one marker ([#1852](https://github.com/JarryShaw/PyPCAPKit/issues/1852), [#1865](https://github.com/JarryShaw/PyPCAPKit/pull/1865)); the platform pins move to `conda/platform-pins.toml`, each with a review-by date, and Conda Update moves them up ([#1851](https://github.com/JarryShaw/PyPCAPKit/issues/1851), [#1885](https://github.com/JarryShaw/PyPCAPKit/pull/1885)); and the release's conda leg installs none of pcapkit's extras and smoke-tests the package before uploading it ([#1854](https://github.com/JarryShaw/PyPCAPKit/issues/1854), [#1855](https://github.com/JarryShaw/PyPCAPKit/issues/1855), [#1868](https://github.com/JarryShaw/PyPCAPKit/pull/1868)). - a wheel built with the oldest setuptools `[build-system] requires` allowed, without build isolation, left out `pcapkit/py.typed`, so type checkers read the installed package as untyped. Nothing declares the file, and setuptools ships it unasked only from 69.0.0, so that is the floor now, in `pyproject.toml` and the Conda recipe alike ([#2062](https://github.com/JarryShaw/PyPCAPKit/issues/2062), [#2064](https://github.com/JarryShaw/PyPCAPKit/pull/2064)). diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 157bb78243..d83428308b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -66,13 +66,13 @@ carries only the version being released; this page is the whole recorded history transport-layer regression fixed in 1.3.5 -- upgrade past this version rather than to it. :doc:`Full changelog `. * **1.3.3** (2024-11-03) -- Fixed PCAP reading from the CLI and library - (:issue:`240`), a source install broken by ``setup.py`` (:issue:`221`), and PCAP output - during flow tracing (:issue:`180`); 1.3.2 was never released. :doc:`Full changelog - `. + (:issue:`240`) and a source install broken by ``setup.py`` (:issue:`221`); 1.3.2 was + never released. :doc:`Full changelog `. * **1.3.1** (2023-12-21) -- Renamed ``register_port()`` to ``register_apptype()`` with no alias left behind, and revised the documentation across ``pcapkit.protocols``, ``pcapkit.corekit`` and - ``pcapkit.dumpkit``. :doc:`Full changelog `. + ``pcapkit.dumpkit``; ``1.3.1.post8`` fixed PCAP output during flow tracing + (:issue:`180`). :doc:`Full changelog `. * **1.3.0** (2023-10-04) -- A redesign of the extension points onto metaclasses, adding ``ModuleDescriptor`` so a registry entry can name a module and class imported lazily on first use. :doc:`Full changelog diff --git a/docs/source/changelog/1.3.1.rst b/docs/source/changelog/1.3.1.rst index 364d1d8225..7e61c40594 100644 --- a/docs/source/changelog/1.3.1.rst +++ b/docs/source/changelog/1.3.1.rst @@ -9,3 +9,6 @@ particular. *Also released:* ``1.3.1.post1`` through ``1.3.1.post28`` (through 2024-10-26). +``post8`` carried library changes: it fixed PCAP output during flow tracing +(:issue:`180`), gave the base protocol data class ``packet``, and made ``Info`` +iteration skip excluded fields. diff --git a/docs/source/changelog/1.3.3.rst b/docs/source/changelog/1.3.3.rst index e629b9ffcf..678123bcf3 100644 --- a/docs/source/changelog/1.3.3.rst +++ b/docs/source/changelog/1.3.3.rst @@ -4,9 +4,11 @@ * **Fixed** -- errors reading PCAP files from both the CLI and the library (:issue:`240`): missing optional CLI dependencies are handled gracefully and console output is far less verbose; installation from source, which ``setup.py`` had - broken (:issue:`221`); PCAP output during flow tracing (:issue:`180`). -* **Changed** -- the base protocol data class carries ``packet``; ``Info`` - iteration skips excluded fields. + broken (:issue:`221`). + +The PCAP-output fix for flow tracing (:issue:`180`), the base protocol data class's +``packet`` and ``Info`` iteration skipping excluded fields first shipped in +``1.3.1.post8``. 1.3.2 was never released. The version string existed in-tree for a few hours on 2024-11-03 and was superseded by 1.3.3 the same day, so there is no ``v1.3.2`` diff --git a/docs/source/changelog/1.5.0.rst b/docs/source/changelog/1.5.0.rst index 3910436000..69ab6d469e 100644 --- a/docs/source/changelog/1.5.0.rst +++ b/docs/source/changelog/1.5.0.rst @@ -5,10 +5,10 @@ The largest release since 1.0, and the first recorded here as it happened rather than reconstructed. New extraction engines, ESP with payload decryption, SCTP and NGAP over SCTP, a library logger that no longer hijacks the consumer's, and a defect programme run through the issue tracker, citing issues and pull -requests from :pr:`340` to :pr:`1895`. +requests from :pr:`340` to :pr:`2151`. Preceded by ``1.5.0a1`` (2026-09-15), ``1.5.0b1`` to ``1.5.0b3`` (all 2026-09-19) -and ``1.5.0b4`` to ``1.5.0b13`` (2026-09-20 to 2026-10-16), dated in UTC and all +and ``1.5.0b4`` to ``1.5.0b14`` (2026-09-20 to 2026-10-22), dated in UTC and all published as prereleases and so resolved only by ``pip install --pre``. ``1.5.0b1`` half-shipped: the tag, the GitHub release and the Conda deployments landed, but PyPI rejected the wheel because ``twine check`` found a Sphinx-only @@ -362,7 +362,9 @@ Fixed overrides it with the check its ``_missing_`` makes, raising :exc:`.EnumValueError`. ``get(value, default)`` on a value the hook refuses still returns ``default`` and logs nothing (:issue:`1792`, :pr:`1818`, :issue:`1816`, :pr:`1823`; the quiet miss is - :issue:`1845`, :pr:`1873`, under :mod:`pcapkit.corekit`). + :issue:`1845`, :pr:`1873`, under :mod:`pcapkit.corekit`). :meth:`.AppType.register` + and :meth:`.OptionType.register` keep that rejection loud inside a quiet ``get``, as + :meth:`.EnumRegistry.register` does (:issue:`1916`, :pr:`1927`). * :meth:`.Command.get` refuses a key that is not a ``str`` with :exc:`.EnumValueError`, as :meth:`.Method.get` does. ``get(b'\x01')`` minted a member keyed by ``bytes``, and ``get(-1)`` raised ``AttributeError`` whatever its ``default`` (:issue:`1824`, @@ -402,6 +404,13 @@ Added * :mod:`pcapkit.corekit.fields.byteorder`, holding ``packet_byteorder`` and ``byteorder_callback``, the byte-order helpers the PCAP and PCAP-NG schemas share (:pr:`1528`, :issue:`1711`, :pr:`1715`). +* :func:`~pcapkit.corekit.io.can_seek`, whether a stream really seeks: on Windows a pipe + answers ``seekable()`` with ``True`` while its ``seek`` does nothing. It trusts + ``seekable()`` only for a regular file or a stream with no descriptor. + :class:`.Extractor` wraps any other stream in :class:`.SeekableReader`, so ``pcapkit -`` + reads a Windows pipe (:issue:`2097`, :pr:`2104`), and + :func:`~pcapkit.corekit.io.advance` skips a record on one by reading + (:issue:`2108`, :pr:`2118`, ``93a6173ae``). Changed ~~~~~~~ @@ -762,6 +771,17 @@ Fixed and neither it nor the sentinel takes an attribute write. A pickle written before this change at protocol 0 or 1 still loads, though not as the singleton (:issue:`1841`, :issue:`1842`, :pr:`1876`). +* defects off CPython on Linux. On GraalPy, :class:`.SeekableReader`'s reads aliased a + buffer a later slide rewrote, so they copy out of it, and a :class:`.MultiDict` nested + each value list, so it is built from a ``dict`` (:issue:`1963`, :pr:`1974`); and a + sentinel type refused the cache writes ``_py_abc`` makes, so ``isinstance`` against it + raised ``TypeError`` (:issue:`1960`, :pr:`1966`). ``Field.pack`` hands ``struct`` a + ``bytearray`` as ``bytes`` for the ``s``, ``p`` and ``c`` formats, which PyPy refuses + for ``s`` and ``p`` and GraalPy accepts for ``c`` where CPython does not, so it packs + alike on every interpreter (:issue:`2004`, :pr:`2007`, :issue:`2015`, :pr:`2018`). On + Windows, ``SeekableReader(buffer_save=True)`` with no ``buffer_path`` could not reopen + its temporary file; it creates one it can, and deletes it on ``close()`` or collection + (:issue:`2098`, :pr:`2105`). pcapkit.dumpkit --------------- @@ -832,6 +852,9 @@ Fixed * a PCAP trace record scales its fraction to the trace's resolution (:issue:`1500`, :pr:`1529`), and writes the octets it has rather than a truncated record's claimed length (:issue:`1541`, :pr:`1543`). +* a subclass of a dumper ``make_dumper`` builds recursed without end in its + ``object_hook`` fallback, which called ``super(type(self), self)`` (:issue:`2144`, + :pr:`2146`). pcapkit.foundation ------------------ @@ -962,6 +985,11 @@ Changed (:issue:`1024`). * **a breaking change to** ``register_mh_extension``: ``schema`` is keyword-only, as in the other schema-taking registrars (:issue:`516`, :pr:`526`). +* the ``pypcap`` and ``pcap_ct`` engines probe the ``pcap`` backend once per extraction + rather than three times, and find its distribution without parsing each candidate's + ``METADATA``, which ``importlib.metadata`` does on Python 3.15 and which made + ``pcap_ct`` extraction about 2.4x slower there (:issue:`2110`, :pr:`2112`, + :issue:`2113`, :pr:`2121`). Fixed ~~~~~ @@ -985,7 +1013,9 @@ Fixed finaliser releases the handle of an extraction *abandoned* before end of file (``auto=False``, iterated part way and dropped); the files :issue:`610` names emit no such warning. Leaving a ``with`` block follows the same rule, so a caller's - stream stays open after it (:issue:`1999`). + stream stays open after it (:issue:`1999`, :pr:`2000`). The finaliser is a + ``weakref.finalize``, so it also runs where ``__del__`` never does, as on GraalPy + (:issue:`1997`, :pr:`1998`). * ``extract(..., no_eof=True)`` never returned. End of stream was detected and ``ExtractionWarning: EOF reached`` fired, but every loop that handles it (``record_frames``, ``__next__``, ``__call__``) read ``if self._flag_n: continue``, @@ -1150,6 +1180,18 @@ Fixed pyshark, pypcap, pcap-ct and pypcapfile engines replaced it with their own printer, which now runs only for ``verbose=True``, its output unchanged (:issue:`1874`, :pr:`1880`). +* an :class:`.Extractor` whose constructor raises closes what it owns, the input it + opened and then the engine it chose, so a PyShark capture's ``tshark`` does not + outlive it; the original exception still propagates, and a caller's stream stays open + (:issue:`2100`, :pr:`2106`, :issue:`2107`, :pr:`2119`). +* ``engine='pypcap'`` warns and falls back to the default engine when the ``pcap`` module + is installed but cannot load, as ``engine='pcap_ct'`` already did, where it raised the + loader's ``OSError`` (:issue:`2132`, :pr:`2133`). +* TCP reassembly bounded its holes with ``sys.maxsize``, which on GraalPy and 32-bit + builds is ``2**31 - 1``, inside TCP's sequence space, so a stream past it never + completed. The bound is + :data:`~pcapkit.foundation.reassembly.data.tcp.HOLE_OPEN_BOUND`, ``2**63 - 1``, the + value 64-bit builds already used (:issue:`1959`, :pr:`1965`). pcapkit.interface ----------------- @@ -1416,6 +1458,15 @@ Changed :mod:`pcapkit.protocols.internet.mh`. As Mappings they no longer index, unpack, compare or hash as a tuple does; ``.seconds`` and ``.fraction`` still work, and the dumpers write both as keys (:issue:`1886`, :pr:`1887`). +* **a breaking change to** rebuilding from ``to_dict()``: the payload is dispatched only + from the ``__next_name__`` and ``__next_type__`` keys, and the IPv6 extension-header + chain only from ``__exthdr__``, never from key order, which on PyPy follows an earlier + instance's attribute layout and so picked the wrong layer. ``to_dict()``, iteration + and ``len()`` carry the dunder keys a new :attr:`.Info.__included__` names, and + :attr:`.Info.__excluded__` only keeps a key out of ``repr``, ``str`` and the dumps. A + hand-written dict needs those keys, ``__short_read__`` among them, to rebuild, and a + custom :class:`.Info` lists its own dunder keys in ``__included__`` (:issue:`2003`, + :pr:`2014`). Fixed ~~~~~ @@ -2443,8 +2494,9 @@ Fixed :exc:`.ProtocolError` for a ``total_length`` with ``length_flag=False``. * **a breaking change to** ``IPv6.from_data``: it dropped the extension-header chain, so a packet carrying a Routing header rebuilt 48 octets as 40. The chain is - now kept in wire order under a hidden ``__exthdr__`` key, absent from ``to_dict`` - and dumped output, and re-emitted ahead of the upper layer. Sample frames that + now kept in wire order under a hidden ``__exthdr__`` key, absent from dumped output + (``to_dict`` carries it since :pr:`2014`, below), and re-emitted ahead of the upper + layer. Sample frames that rebuilt wrong now rebuild exactly, though some that had rebuilt with wrong bytes raised ``AttributeError`` instead (:issue:`1177`), from older defects fixed with it: :attr:`.HIP.alias` read ``_info`` before ``read`` had set it, which also broke a @@ -2604,6 +2656,29 @@ Fixed * the MPL option's ``seed`` in the HOPOPT and IPv6-Opts schemas is annotated ``int | IPv6Address``, since an S=0 option holds the source address (:issue:`1840`, :pr:`1866`). +* a declared length above ``sys.maxsize``, such as a PCAP ``incl_len`` of + ``2**31 - 16`` on GraalPy or 32-bit CPython, raised ``OverflowError`` from a read or + seek. Every length-driven read in :meth:`.Schema.unpack`, :meth:`.Frame.read` and + :meth:`.PCAPNG.read` is bounded at ``sys.maxsize``, so such a claim gets the overrun + handling it gets on 64-bit CPython (:issue:`1975`, :pr:`1979`). +* PCAP-NG blocks cut short by the file or by their own framing no longer abort an + extraction. A Decryption Secrets Block whose ``secrets_length`` runs past the block + keeps the declared length and warns, its secrets capped at what the block holds + (:issue:`1980`, :pr:`1983`); an unknown, Custom or systemd journal export block the + file truncates gets the "block truncated" warning the other block types give + (:issue:`1984`, :pr:`1988`); and a malformed TLS or WireGuard key-log line warns with + :exc:`.ProtocolWarning` and is left out of ``entries``, where it raised + :exc:`.FieldValueError` (:issue:`1985`, :pr:`1989`). A ZigBee NWK or APS key off its + fixed width rebuilds as captured, where it was zero-padded or cut to that width + (:issue:`1986`, :pr:`2001`). +* capture timestamps convert without the platform's ``fromtimestamp`` limits. Windows' + C runtime covers only 1970 to 3000, so a PCAP-NG timestamp outside that range fell + back to 1970 with ``invalid timestamp``, and the Mobility Header's NTP and PMIPv6 + timestamps had the same gap. PCAP, PCAP-NG and MH convert through + :func:`pcapkit.utilities.datetime.fromtimestamp`, which gives the stdlib's result on + Linux (:issue:`2099`, :pr:`2111`), and encode through + :func:`pcapkit.utilities.datetime.totimestamp`, since GraalPy's ``timestamp()`` put a + pre-1970 instant with a fraction one second late (:issue:`2134`, :pr:`2136`). pcapkit.toolkit --------------- @@ -2727,6 +2802,11 @@ Added bad value warns with :exc:`.ConfigWarning` and falls back to the default; ``-1`` lifts a limit. The homes these limits had in the prereleases keep no re-export (:issue:`1726`, :pr:`1743`). +* :mod:`pcapkit.utilities.datetime`, holding + :func:`~pcapkit.utilities.datetime.fromtimestamp` and + :func:`~pcapkit.utilities.datetime.totimestamp`, the timestamp conversions of the + capture-timestamp entry under :mod:`pcapkit.protocols` (:pr:`2111`, :pr:`2136`). Its + first name, ``pcapkit.utilities.timestamp``, shipped in no release. Changed ~~~~~~~ @@ -2778,6 +2858,9 @@ Fixed which truncated every later traceback, pcapkit's or not; lazily installed ``sys.excepthook`` and ``threading.excepthook`` keep its output terse instead (:pr:`983`). +* an import-time warning names the importing module on GraalPy too, where the + importer's stack level counted the bootstrap frames GraalPy's ``warnings.warn`` skips, + attributing the warning to ``:0`` (:issue:`1963`, :pr:`1996`). pcapkit.vendor -------------- @@ -2959,6 +3042,28 @@ Added declaring what pcapkit uses of each, so mypy checks the engines against them. ``mypy.ini`` reads them through ``mypy_path``, and neither the sdist nor the wheel ships them (:issue:`1878`, :pr:`1882`). +* a glossary page for the terms with several meanings -- registry, schema, data model, + info and default engine -- each sense linked to the page that explains it + (:issue:`1949`, :pr:`1955`). +* the engine benchmark on GitHub Actions: ``benchmark.yml``, weekly and on dispatch, + measures every engine on the runner's own interpreters through + ``examples/benchmark/native.sh`` and posts the report to the *CI reports* discussions + (:issue:`2125`, :pr:`2128`). A ``pcap`` engine whose module cannot load is an explained + row rather than a fatal error (:issue:`2129`, :pr:`2131`), and the report is reST with + footnotes, so it pastes unedited onto ``docs/source/engines.rst`` (:pr:`2142`), the + page the engine comparison and prerequisites move to from ``index.rst``, carrying the + first run's figures (``f9de043d6``, ``d7aa41535``). The tables had first been + regenerated with Python 3.15 measured (:pr:`2109`). +* a win-64 Conda package (:issue:`2060`). Conda Update builds and smoke-tests it + (:pr:`2067`), reading the environment without ``os.environb`` (:issue:`2120`, + :pr:`2122`) and conda-forge's CFEP-27 site-packages layout (:issue:`2126`, + :pr:`2127`); then it publishes from Conda Update and Create Release, and a release is + complete only once the platforms its tagged commit lists, win-64 among them, are on + Anaconda (:pr:`2138`). A manual Conda Update run builds only (``c5081fbfe``) unless + dispatched on ``main`` with ``publish``, which forces the bump that ships a release's + missing Anaconda files (:pr:`2150`), and Create Release's warning about an unfinished + release names that remedy when Anaconda files are what is missing (:issue:`2140`, + :pr:`2151`). Changed ~~~~~~~ @@ -2996,7 +3101,8 @@ Changed joined the Installation section of ``docs/source/index.rst``. Requested by the project owner, and a deliberate exception to the reStructuredText convention, for the README only, since its renderers are GitHub and PyPI rather than Sphinx. - ``setup.py`` reads ``README.md`` and declares ``text/markdown``, and ``MANIFEST.in`` + ``setup.py`` read ``README.md`` and declared ``text/markdown`` (``pyproject.toml`` + declares the ``readme`` statically since :pr:`1994`), and ``MANIFEST.in`` gains ``include README.md``, which is belt-and-braces rather than load-bearing: setuptools' ``sdist`` ships a ``README.md`` regardless. Of the ``include`` lines only ``CHANGELOG.md`` is load-bearing, measured in the :pr:`631` entry below. @@ -3138,10 +3244,12 @@ Changed carries what CI needs (:issue:`910`, :pr:`914`). * the ``Scapy`` and ``dev`` extras require ``scapy>=2.6.0`` (:issue:`1572`, :pr:`1578`), and the ``docs`` extra ``Sphinx>=7.3``, the first release with - ``versionremoved`` (:pr:`1743`), and ``sphinx-autodoc-typehints==3.15.0`` - (:issue:`1856`, :pr:`1877`). The ``test`` extra gains ``psutil``, + ``versionremoved`` (:pr:`1743`), and an exact ``sphinx-autodoc-typehints`` pin + (:issue:`1856`, :pr:`1877`), 3.17.2 since the weekly dependency update (:pr:`1944`). The ``test`` extra gains ``psutil``, ``markdown-it-py`` and ``lxml``, which the scripts under ``util/`` use - (:issue:`1894`, :pr:`1896`). + (:issue:`1894`, :pr:`1896`). The ``crypto`` and ``all`` extras cap ``cryptography`` + below 46 on PyPy older than 3.11, for which no later release has a Linux wheel + (:issue:`2040`, :pr:`2046`). * the READMEs of ``examples/benchmark`` and ``examples/legacy_smoke`` are Markdown, ``README.md`` rather than ``README.rst``, and ``MANIFEST.in`` ships them through ``recursive-include examples *.md``. Stale build leftovers go: ``setup.py``'s @@ -3155,6 +3263,20 @@ Changed * the contributor pages state no counts that a test or the Python version updater pinned, and each pin checks the fact it stood for instead (:issue:`1811`, :pr:`1817`). +* the workflows' supply chain (:issue:`1968`): every third-party action is pinned to a + commit SHA that Dependabot updates by pull request (:pr:`1981`); no third-party step + runs after the deploy key is loaded, and every workflow declares its top-level + permissions, the GitHub Pages build read-only beside a separate publishing job + (:pr:`1970`); and the cron jobs name the environments that hold their secrets + (:pr:`1993`). ``pyproject.toml`` declares the ``readme`` statically, so Dependabot can + build the editable install (:pr:`1994`). +* CodeQL skips ``tests/``, where the recurring clear-text-logging alerts in + :mod:`pcapkit.foundation.extraction` and :mod:`pcapkit.corekit.io` traced to a test + fixture's ``secrets`` names; ``pcapkit/`` is still scanned by every query + (:issue:`2130`, :pr:`2139`). The advisory linters drop their long-standing false + positives with no behaviour change: vermin honours the ``# novermin`` markers and + confirms the 3.8 floor, bandit reports nothing, and the unused ``type: ignore`` + comments go, from the vendor templates too (:issue:`2145`, :pr:`2147`). Fixed ~~~~~ @@ -3341,6 +3463,9 @@ Fixed * ``make_samples.py`` builds fixtures from the tree it runs in, not the shared checkout (:issue:`1343`, :pr:`1388`), and retries the Wireshark sample downloads on 429, 5xx and timeouts (:issue:`1658`, :pr:`1659`). + ``examples/generators/options.py``'s deadline discards a ``SIGALRM`` still pending as + it puts the old handler back, which on GraalPy could kill the process + (:issue:`2090`, :pr:`2091`). * the Conda Update workflow tags and uploads each bump exactly once (:issue:`1632`, :pr:`1641`), stays green when a merge races its push (:issue:`1649`, :pr:`1669`), and keeps the release's name and notes when it @@ -3365,7 +3490,7 @@ Fixed * the API reference renders what the code declares. :attr:`.Extractor._vfunc` keeps its annotation (:issue:`1839`, :pr:`1867`), and the HTTP/2 frame schemas' and the engine classes' annotations resolve (:issue:`1857`, :pr:`1870`). Under the pinned - sphinx-autodoc-typehints 3.15.0 (:issue:`1856`, :pr:`1877`), ``conf.py`` renders an + sphinx-autodoc-typehints (:issue:`1856`, :pr:`1877`), ``conf.py`` renders an ``aenum`` base as such, keeps a default as written (``byteorder=sys.byteorder`` rather than the build host's value), gives a class the parameters of the ``__init__`` its ``__new__`` hides and an enumeration ``(*values)``, keeps table ids @@ -3373,6 +3498,27 @@ Fixed :issue:`1863`, :pr:`1881`). The generated const tables carry no version directives; each note is on the module's hand-written page instead (:issue:`1884`, :pr:`1895`). +* cross-references resolve or render as literals. A role into a package with no + inventory is a literal or an ``rst_epilog`` link (:issue:`1919`, :pr:`1928`, + :issue:`1936`, :pr:`1939`, :issue:`1920`, :pr:`1945`); a ``pcap`` module mention + links the note on PyPCAP and pcap-ct (:issue:`1948`, :pr:`1952`); library-internal + names link, and the classes and special methods they name are documented + (:issue:`1940`, :pr:`1946`, :issue:`1947`, :pr:`1953`), the const registries' ``get`` + overrides among them (:issue:`1921`, :pr:`1935`); and a ``Warns:`` section's warning + classes link as ``Raises:`` entries do (:issue:`2094`, :pr:`2096`). ``conf.py`` + resolves a decorated function's overloads and qualifies a third-party class whose + bare name collides (:issue:`1917`, :issue:`1918`, :pr:`1929`). The ``versionchanged`` + directives on :class:`.Info` iteration and :class:`~pcapkit.protocols.misc.raw.Raw` + name ``1.3.1.post8``, the release that shipped their change (:issue:`1933`, + :pr:`1934`). +* the Conda release legs: Create Release's Anaconda completeness check counts + osx-arm64 (:issue:`1922`, :pr:`1932`); the ``macos-26-intel`` legs install + Miniforge, upload with the ``anaconda`` CLI where the upload action's download + returned 404, and build again past upstream conda and conda-build breakage + (:issue:`2044`, :pr:`2045`, :pr:`2049`, :issue:`2123`, :pr:`2124`); and + ``util/conda_vendor.py``'s pin refresh warns when it cannot move a platform pin, or + when a pinned release has lost a wheel (:issue:`2025`, :pr:`2026`, :issue:`2028`, + :pr:`2035`). * ``pcapkit-cli``'s help strings say what each option does: ``-o`` describes the output rather than the input, an extension is appended only under ``--auto-extension``, and each format flag names the flags that take precedence