chore: mark vermin's guarded-import false positives, close the engine handle - #411
Merged
Merged
Conversation
… handle Two small fixes and one measurement. **`close_extractor` leaked the engine handle.** It closed `_ifile` only, while both `Extractor._cleanup` and `Extractor.__exit__` close `_ifile` *then* `_exeng`. That matters because `Engine.close()` is a no-op on the base class but not on the subclasses: `pcap_ct` and `pypcap` close a live `pcap.pcap` handle and `pyshark` closes a temp file, so an extractor abandoned mid-file in a test teardown leaked a real OS handle. Now closes both, in the library's order, each guarded separately and in a `try/finally` so a failing stream close cannot strand the engine. `getattr` probing keeps it tolerant of an extractor that died before `_exeng` was assigned, which is the state teardown most often sees. No test depended on the leak: all ~30 call sites use it purely as teardown and never touch the extractor afterwards. New `tests/test_support_helpers.py` pins the contract, following the `tests/test_tier_guard.py` precedent for covering test-support code. **`# novermin` on three guarded imports in `compat.py`.** Vermin reported a 3.11 minimum from `enum.StrEnum` and `enum.show_flag_values`, and 3.10 from `typing.TypeAlias`, all three in the `else:` branch of a `sys.version_info` guard -- so they never execute on an older interpreter and the report was a false positive. `# novermin` is Vermin's documented escape hatch for exactly this. Verified the combined `# type: ignore[attr-defined] # novermin` still works: zero mypy errors in the file, and mypy does report unused ignores elsewhere in the same run, so the check has teeth. `targets = 3.6` is left as it is, per the maintainer's decision to keep the 3.6 claim. Recording what that costs, since it was measured and should not have to be re-discovered: after these three markers the report lands on **3.9**, and the remaining floor is real code rather than false positives -- **65 named-expression (walrus) sites across 57 files**, unguarded and at module level, plus a positional-only `/` parameter in `foundation/engines/engine.py`. Walrus is 3.8+, so no amount of marking reaches 3.6. Vermin is not wired into any workflow, so nothing is red either way. Verified: `tests/test_support_helpers.py` plus `tests/utilities` 96 passed, 100 subtests.
There was a problem hiding this comment.
🟡 Changes recommended
_close_quietly can currently raise from getattr(target, "close", ...), which is a teardown-safety regression compared to the previous hasattr(...) behavior and should be guarded.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR tightens test teardown correctness by ensuring tests._support.close_extractor releases both the extractor’s input stream and its engine, and it reduces Vermin noise by marking version-guarded imports as false positives.
Changes:
- Extend
tests._support.close_extractorto close_ifileand_exengin library order, with independent failure isolation. - Add a dedicated unit test module to pin teardown-helper behavior and prevent regressions.
- Annotate
pcapkit.utilities.compatguarded imports with# noverminto avoid static false positives.
File summaries
| File | Description |
|---|---|
| tests/test_support_helpers.py | Adds contract tests covering teardown helper tolerance and close-order/independence. |
| tests/_support.py | Updates teardown helper to close both stream and engine with guarding. |
| pcapkit/utilities/compat.py | Marks guarded imports as Vermin false positives using # novermin. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…allowed Addresses two review findings on #411: - ``getattr(target, 'close', None)`` sat outside the ``try``, so a resource whose ``close`` is a property or resolved through ``__getattr__`` raised from the *lookup* and escaped -- the failure the helper exists to prevent, since it runs from teardown and would replace the real test failure. Measured against the previous code: it raised ``RuntimeError``. The lookup now happens inside the ``try``, and one unreachable resource still does not strand the other. - The docstring said "swallowing anything it raises" of a bare ``except Exception``. It now says :exc:`Exception`, and says why :exc:`BaseException` is left to propagate. Two tests added; 10 pass in the file, 659 in the fixture-free tier.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two small fixes and one measurement worth recording.
close_extractorleaked the engine handletests/_support.close_extractorclosed_ifileonly, while bothExtractor._cleanupandExtractor.__exit__close_ifilethen_exeng.That matters because
Engine.close()is a no-op on the base class but not on the subclasses —pcap_ctandpypcapclose a livepcap.pcaphandle,pysharkcloses a temp file. So an extractor abandoned mid-file in a test teardown leaked a real OS handle.Now closes both, in the library's own order, each guarded separately and inside a
try/finallyso a failing stream close cannot strand the engine.getattrprobing keeps it tolerant of an extractor that died before_exengwas assigned — which is the state teardown most often sees.No test depended on the leak: all ~30 call sites use it purely as teardown and never touch the extractor afterwards. New
tests/test_support_helpers.pypins the contract, following thetests/test_tier_guard.pyprecedent for covering test-support code.# noverminon three guarded importsVermin reported a 3.11 minimum from
enum.StrEnumandenum.show_flag_values, and 3.10 fromtyping.TypeAlias— all three in theelse:branch of asys.version_infoguard, so they never execute on an older interpreter. False positives, and# noverminis Vermin's documented escape hatch for exactly this shape.Verified the combined
# type: ignore[attr-defined] # noverminstill works: zero mypy errors in the file, and mypy does report unused ignores elsewhere in the same run, so the check has teeth.targets = 3.6left alone — and what that costsPer your decision to keep the 3.6 claim. Recording the measurement so it doesn't have to be rediscovered:
After these three markers the report lands on 3.9, and what remains is real code, not false positives — 65 named-expression (walrus) sites across 57 files, unguarded and at module level, plus a positional-only
/parameter infoundation/engines/engine.py. Walrus is 3.8+, so no amount of marking reaches 3.6.Vermin isn't wired into any workflow, so nothing is red either way. Flagging it only because the gap between the declared floor and the actual one is now quantified.
Also measured, not changed
The packaging question, settled by building both artefacts: the sdist carries all 91 test modules, the wheel carries none. That is the right split — the suite could not run from an installed package anyway, since the generated captures aren't shipped and
tests/_tiers.pyresolves paths from a repository root.MANIFEST.in'sprune test(singular) does not matchtests, which is why the sdist includes them; changing it toprune testswould remove the suite from the sdist, the opposite of what was documented. Left as a deliberate decision, with the prose corrected on #408 instead.Verification
tests/test_support_helpers.py+tests/utilities: 96 passed, 100 subtests.