Skip to content

fix(corekit): copying or pickling a stream proxy no longer recurses (#1524) - #1544

Merged
JarryShaw merged 1 commit into
mainfrom
fix/1524-peekable-stream-copy
Oct 10, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/1524-peekable-stream-copy

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Please follow the guide below

  • You will be asked some questions, please read them carefully and answer honestly

  • Put an x into all the boxes [ ] relevant to your pull request (like that [x])

  • Use Preview tab to see how your pull request will actually look like

  • Searched for similar pull requests

  • Followed the coding style (make pylint, make mypy, make isort)

  • make test passes, and a test case covers the change — tests added; ran tests/corekit, tests/foundation/test_extraction*.py, the PyPCAPFile engine, tests/project, the docstring contract and the tier guard, not the full suite

  • Added a changelog entry under docs/source/changelog/ and regenerated CHANGELOG.md, if the change is user-visible — N/A — centralised in docs(changelog): shared 1.5.0 changelog — long-lived, merges last (#610, #616, #617, #618, #620) #657

What is the purpose of your pull request?

Tick the commit type your subject line carries.

  • fix — corrects a defect
  • feat — adds a feature
  • perf — changes performance, not behaviour
  • refactor — changes neither behaviour nor performance
  • test — tests only
  • docs — documentation only
  • ci — workflows or build tooling
  • release — bumps the version or rolls up a distribution
  • chore — anything else

Description of your pull request and other information

copy and pickle build a PeekableStream without __init__. __getattr__ then forwarded __setstate__ to an unset _stream, whose own lookup re-entered __getattr__ until RecursionError. It now refuses _stream and every dunder, so the proxy's protocols are never the stream's.

Semantics: copy.copy shares the stream (one position); copy.deepcopy and pickle copy it, so they succeed exactly when the stream's own do — BytesIO from protocol 2, an open file never. NamedStream and SeekableReader were already sound and are unchanged.

tests/corekit/test_io_peekable_stream_copy_1524_unit.py: 7 tests; 9 of 11 results fail on 945c3c3d8 (8 RecursionError, 1 forwarded __setstate__).

Closes #1524

…1524)

* `PeekableStream.__getattr__` forwarded every missing name to
  `self._stream`. `copy` and `pickle` build the instance without
  `__init__`, so `_stream` was unset and its lookup re-entered
  `__getattr__` until `RecursionError`. It now raises `AttributeError`
  for `_stream` and for any dunder, so the proxy's own protocols are
  never replaced by the stream's.
* The default protocols then apply: `copy.copy` shares the stream,
  `copy.deepcopy` and `pickle` copy it, and so work exactly when the
  stream's own do. The class docstring states this.
* `NamedStream` has no `__getattr__` and `SeekableReader` refuses
  copying as `io.BufferedReader` does; neither changes.

New tests in `tests/corekit/test_io_peekable_stream_copy_1524_unit.py`
cover copy, deepcopy and pickle protocols 0-5 over a `BytesIO`.
@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) review: running A cross-review is in flight against the current head - no verdict yet labels Oct 10, 2026
@JarryShaw JarryShaw moved this to In review in PyPCAPKit Oct 10, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Verdict: GOOD TO GO at 696cf1a26. Cross-reviewed on Sonnet; the author ran Opus.

  • Fail-before: with main's corekit/io.py, the new test file gives 9 failed, 2 passed; at the head, 7 passed, 12 subtests passed. I re-ran both myself. Each guard clause (dunder, _stream) has its own failing test under mutation.
  • No forwarding lost: implicit dunder lookups never went through __getattr__, and no code in pcapkit/, tests/, docs/ or examples/ calls a forwarded dunder explicitly. extract(fin=BytesIO) matches extract(fin=path) on 8 captures (default, dpkt) and 6 (pypcapfile, 3.11).
  • Semantics: copy shares the stream and position; deepcopy gives an independent stream at the same position; pickle 2–5 round-trips; 0/1 fail with the stream's own TypeError.
  • Based on current main (b6b95bc8e), no conflict; CI 27 ok / 0 fail, 7 still running.

Nits, not blocking: the __getattr__ comment could name __getstate__ (3.10 and older) and __deepcopy__ as the reachable cases rather than __setstate__; and test_pickle_below_protocol_2_fails_as_the_stream_does passes on main too, so it is a guard, not a regression test.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: running A cross-review is in flight against the current head - no verdict yet labels Oct 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Coverage: 90.09% (unit tier, Python 3.14, 696cf1a26, Unit Tests run success)

Package Statements Missed Branches Partial Cover
pcapkit (top level) 104 4 20 4 93.55%
pcapkit/const 18795 842 2356 756 92.09%
pcapkit/corekit 2204 67 712 34 96.26%
pcapkit/dumpkit 258 2 90 2 98.85%
pcapkit/foundation 2650 98 934 42 94.98%
pcapkit/interface 112 7 40 5 92.11%
pcapkit/protocols 17009 229 4574 189 98.04%
pcapkit/toolkit 539 74 168 3 84.58%
pcapkit/utilities 431 4 124 4 98.56%
pcapkit/vendor 4409 2342 1006 157 43.25%

Per-file detail: the coverage-html artifact of this run.

@JarryShaw
JarryShaw merged commit 16fbea0 into main Oct 10, 2026
41 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in PyPCAPKit Oct 10, 2026
@JarryShaw
JarryShaw deleted the fix/1524-peekable-stream-copy branch October 10, 2026 01:59
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Oct 10, 2026
@JarryShaw JarryShaw added this to the PyPCAPKit v1.5 milestone Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

fix(corekit): copying or pickling PeekableStream recurses until RecursionError

1 participant