Summary
BaseWarning.__init__ calls warnings.simplefilter('ignore', type(self)) when not in devmode. warnings.simplefilter mutates the process-global warnings.filters list and inserts at index 0, so merely constructing a pcapkit warning — or parsing one packet — permanently overrides whatever warning configuration the host application, the test runner, or python -W had set for that category. It also invalidates every module's __warningregistry__, which makes unrelated non-pcapkit warnings re-fire.
A side effect is that pcapkit's own warnings are invisible by default, and there is no way for a caller to re-enable them: any filter they install is overtaken by the next warning construction.
file:line
pcapkit/utilities/warnings.py:74, in BaseWarning.__init__ (lines 65-77):
65 def __init__(self, *args: 'Any', **kwargs: 'Any') -> 'None': # pylint: disable=useless-super-delegation
66 # log warning
67 if DEVMODE:
68 if VERBOSE:
69 logger.warning(str(self), exc_info=self, stack_info=True,
70 stacklevel=stacklevel_calculator())
71 else:
72 logger.warning(str(self))
73 else:
74 warnings.simplefilter('ignore', type(self))
75
76 # warnings.simplefilter('default')
77 super().__init__(*args, **kwargs)
The offending expression, executed from a constructor:
warnings.simplefilter('ignore', type(self))
Observable symptom 1 — the global filter list changes
Constructing one instance. No warn() call, no parse. Python 3.14.7:
warnings.filters BEFORE any pcapkit warning: (5 entries)
[0] ('default', None, <class 'DeprecationWarning'>, '__main__', 0)
[1] ('ignore', None, <class 'DeprecationWarning'>, None, 0)
[2] ('ignore', None, <class 'PendingDeprecationWarning'>, None, 0)
[3] ('ignore', None, <class 'ImportWarning'>, None, 0)
[4] ('ignore', None, <class 'ResourceWarning'>, None, 0)
--- constructing ONE pcapkit warning instance, nothing else ---
constructed: SchemaWarning('just constructing the object')
warnings.filters AFTER: (6 entries)
[0] ('ignore', None, <class 'pcapkit.utilities.warnings.SchemaWarning'>, None, 0)
[1] ('default', None, <class 'DeprecationWarning'>, '__main__', 0)
[2] ('ignore', None, <class 'DeprecationWarning'>, None, 0)
[3] ('ignore', None, <class 'PendingDeprecationWarning'>, None, 0)
[4] ('ignore', None, <class 'ImportWarning'>, None, 0)
[5] ('ignore', None, <class 'ResourceWarning'>, None, 0)
NEW entries introduced by merely constructing SchemaWarning():
+ ('ignore', None, <class 'pcapkit.utilities.warnings.SchemaWarning'>, None, 0)
The entry lands at index 0, ahead of everything the interpreter and the application put there, and is never removed or restored.
Observable symptom 2 — the caller's configuration is overridden
Run under python -W error::UserWarning. A plain UserWarning from user code correctly raises; pcapkit's ProtocolWarning, which is a UserWarning subclass, does not:
sys.warnoptions = ['error::UserWarning']
warnings.filters at start:
[0] ('error', None, <class 'UserWarning'>, None, 0)
...
--- a NON-pcapkit warning of the same base category, for comparison ---
RESULT: raised UserWarning: plain UserWarning from user code (i.e. -W error DID apply)
--- a pcapkit warning ---
RESULT: no exception (-W error was defeated; warning silently swallowed)
warnings.filters at end:
[0] ('ignore', None, <class 'pcapkit.utilities.warnings.ProtocolWarning'>, None, 0)
[1] ('error', None, <class 'UserWarning'>, None, 0)
...
The same holds for a filter the application sets programmatically before the pcapkit warning — pcapkit's entry jumps ahead of it:
--- host app calls warnings.simplefilter('always') ---
warnings recorded by the host app : 0 []
exception raised : None
filters len before/after warn() : 7 -> 8
filters[0] is now : ('ignore', None, <class 'pcapkit.utilities.warnings.ProtocolWarning'>, None, 0)
--- host app calls warnings.simplefilter('error') ---
warnings recorded by the host app : 0 []
exception raised : None
filters len before/after warn() : 7 -> 8
filters[0] is now : ('ignore', None, <class 'pcapkit.utilities.warnings.ProtocolWarning'>, None, 0)
Observable symptom 3 — unrelated, non-pcapkit warnings re-fire
warnings.simplefilter calls warnings._filters_mutated(), which invalidates every module's __warningregistry__. A once-per-location warning raised by code that never heard of pcapkit gets reported a second time. With a control arm, so the effect is attributed correctly — the helper module does not import pcapkit and owns its own category:
############ CONTROL: middle step does nothing pcapkit-related ############
--- CONTROL: middle = a plain stdlib warnings.warn() of an unrelated category ---
host-app warnings shown after 2 identical calls : 1
host-app warnings shown after the 3rd call : 1
the 3rd call re-fired : False
############ TEST: middle step is ONE pcapkit warning ############
--- TEST: middle = pcapkit warn("...", SchemaWarning) ---
host-app warnings shown after 2 identical calls : 1
host-app warnings shown after the 3rd call : 2
the 3rd call re-fired : True
############ TEST: middle step is a real capture parse ############
(parsed 26 frames)
--- TEST: middle = pcapkit.extract() on http6.cap ---
host-app warnings shown after 2 identical calls : 1
host-app warnings shown after the 3rd call : 2
the 3rd call re-fired : True
This is the effect that reaches categories pcapkit has never heard of.
Growth is bounded, not unbounded
warnings._add_filter removes an identical tuple before re-inserting, so the list grows by at most one entry per distinct warning class:
filters len before parsing a whole capture: 6
frames parsed: 26
filters len after parsing a whole capture: 7 (delta +1)
--- 5000 constructions of the SAME category ---
filters len 7 -> 7 (delta +0)
--- one construction of EVERY BaseWarning subclass ---
23 BaseWarning subclasses; filters len 7 -> 28 (delta +21)
The ceiling is roughly the 23 BaseWarning subclasses in pcapkit/utilities/warnings.py's __all__. A full parse of examples/captures/http6.cap adds exactly one entry. Recording this because it would be easy to overstate the defect as unbounded growth, and it is not.
A precision point on the blast radius
"Silences a warning category for the entire host application, including code unrelated to pcapkit" is half right and worth splitting. The inserted filter is keyed on type(self), a pcapkit class, so the silencing directly reaches only pcapkit's own categories and their subclasses. What genuinely reaches unrelated code is (i) the filter-list mutation itself, which overrides the application's and the interpreter's own -W/simplefilter configuration at index 0 for those categories, and (ii) the __warningregistry__ invalidation in symptom 3, which makes any unrelated 'default'/'once' warning re-fire.
Minimal reproduction
hostapp.py — stands in for unrelated application code, and does not import pcapkit:
import warnings
class HostAppWarning(UserWarning):
pass
def emit():
# stacklevel=1 keeps the (message, category, location) registry key identical
warnings.warn('host app warning A', HostAppWarning, stacklevel=1)
repro.py:
import warnings
from pcapkit.utilities.warnings import SchemaWarning, warn
import hostapp
# 1. the filter list changes from merely constructing the object
before = list(warnings.filters)
SchemaWarning('just constructing the object')
print('new filter entries:', [f for f in warnings.filters if f not in before])
# 2. unrelated host-app warnings re-fire
for label, middle in (('CONTROL', lambda: warnings.warn('x', DeprecationWarning)),
('pcapkit', lambda: warn('y', SchemaWarning))):
with warnings.catch_warnings(record=True) as rec:
warnings.resetwarnings()
warnings.simplefilter('default')
hostapp.emit()
hostapp.emit()
n = len([r for r in rec if r.category is hostapp.HostAppWarning])
middle()
hostapp.emit()
m = len([r for r in rec if r.category is hostapp.HostAppWarning])
print('%-8s deduplicated=%d after=%d re-fired=%s' % (label, n, m, m > n))
Output:
new filter entries: [('ignore', None, <class 'pcapkit.utilities.warnings.SchemaWarning'>, None, 0)]
CONTROL deduplicated=1 after=1 re-fired=False
pcapkit deduplicated=1 after=2 re-fired=True
For symptom 2, run a script that calls warn('...', ProtocolWarning) under python -W error::UserWarning. Note that plain python -W error cannot be used: importing pcapkit trips an unrelated DeprecationWarning: VueJS is deprecated from a dependency at import time.
Blast radius, including this project's own tests
tests/utilities/test_exceptions_warnings.py:49-55 currently asserts the suppression as intended behaviour:
49 def test_warn_suppresses_base_warning_categories_outside_dev_mode(self) -> None:
50 with mock.patch.object(self.warnings, 'DEVMODE', False):
51 with pywarnings.catch_warnings(record=True) as records:
52 pywarnings.simplefilter('always')
53 self.warnings.warn('careful', self.warnings.FormatWarning, stacklevel=1)
54
55 self.assertEqual(records, [])
That test asserts the warning is swallowed even though it explicitly set simplefilter('always') — which is exactly the override described above, encoded as an expectation. So the suppression may well be deliberate; what this issue is about is the mechanism: an unscoped, unrestorable, process-global mutation performed from inside a constructor at arbitrary points during parsing. tests/toolkit/test_dpkt_unit.py:129-130 also uses catch_warnings + simplefilter('ignore') and should be reviewed.
Downstream applications may unknowingly depend on pcapkit warnings being silent today, so a fix will make previously invisible warnings appear. That belongs in the release notes.
What a fix would need to touch
pcapkit/utilities/warnings.py:74, in BaseWarning.__init__ (65-77) — remove the global mutation from the constructor. The dead line 76, # warnings.simplefilter('default'), should go with it.
- If "pcapkit warnings are off by default" is the intent, express it somewhere a caller can see and override: once at import in
pcapkit/__init__.py, behind a documented opt-in, or left to the application (which is what the warnings module is for). Wrapping warn() in warnings.catch_warnings() would scope it but would also mean pcapkit never emits through that channel at all, which folds into the separate design question about which channel owns a warning (pcapkit/utilities/warnings.py:52-54).
tests/utilities/test_exceptions_warnings.py:49-55 will fail with any fix and needs rewriting to state the intended contract.
tests/toolkit/test_dpkt_unit.py:129-130 — check for accidental dependence on the current suppression.
Observed vs inferred
- Observed by running code: the exact filter-list diff from constructing one instance; the inserted entry at index 0; the
-W error::UserWarning defeat with a working control on a non-pcapkit UserWarning; the defeat of programmatic simplefilter('always') and simplefilter('error'); unrelated host-app warnings re-firing, with a control arm that does not re-fire; +1 filter entry for a whole http6.cap parse, +0 for 5000 same-category constructions, +21 for 23 distinct subclasses.
- Inferred by reading code: that
warnings._add_filter's remove-then-insert is what bounds the growth (the bound was measured, the CPython path was read); that _filters_mutated() clearing __warningregistry__ is the mechanism behind the re-fire (the effect is measured and controlled, the mechanism is read from the stdlib); that test_warn_suppresses_base_warning_categories_outside_dev_mode reflects deliberate intent rather than an accident — the suite was not run.
Related
Same file and same family, filed separately because no single change fixes both: the double/triple emission at pcapkit/utilities/warnings.py:52-54. The two interact — the emission counts measured there depend on this filter mutation — so decide them together, but they are two changes in two functions. Also related in shape: #362, the ERROR-level logging of quiet=True exceptions, whose constructor has a comparable unscoped global side effect (sys.tracebacklimit = 0).
Summary
BaseWarning.__init__callswarnings.simplefilter('ignore', type(self))when not in devmode.warnings.simplefiltermutates the process-globalwarnings.filterslist and inserts at index 0, so merely constructing a pcapkit warning — or parsing one packet — permanently overrides whatever warning configuration the host application, the test runner, orpython -Whad set for that category. It also invalidates every module's__warningregistry__, which makes unrelated non-pcapkit warnings re-fire.A side effect is that pcapkit's own warnings are invisible by default, and there is no way for a caller to re-enable them: any filter they install is overtaken by the next warning construction.
file:line
pcapkit/utilities/warnings.py:74, inBaseWarning.__init__(lines 65-77):The offending expression, executed from a constructor:
Observable symptom 1 — the global filter list changes
Constructing one instance. No
warn()call, no parse. Python 3.14.7:The entry lands at index 0, ahead of everything the interpreter and the application put there, and is never removed or restored.
Observable symptom 2 — the caller's configuration is overridden
Run under
python -W error::UserWarning. A plainUserWarningfrom user code correctly raises; pcapkit'sProtocolWarning, which is aUserWarningsubclass, does not:The same holds for a filter the application sets programmatically before the pcapkit warning — pcapkit's entry jumps ahead of it:
Observable symptom 3 — unrelated, non-pcapkit warnings re-fire
warnings.simplefiltercallswarnings._filters_mutated(), which invalidates every module's__warningregistry__. A once-per-location warning raised by code that never heard of pcapkit gets reported a second time. With a control arm, so the effect is attributed correctly — the helper module does not import pcapkit and owns its own category:This is the effect that reaches categories pcapkit has never heard of.
Growth is bounded, not unbounded
warnings._add_filterremoves an identical tuple before re-inserting, so the list grows by at most one entry per distinct warning class:The ceiling is roughly the 23
BaseWarningsubclasses inpcapkit/utilities/warnings.py's__all__. A full parse ofexamples/captures/http6.capadds exactly one entry. Recording this because it would be easy to overstate the defect as unbounded growth, and it is not.A precision point on the blast radius
"Silences a warning category for the entire host application, including code unrelated to pcapkit" is half right and worth splitting. The inserted filter is keyed on
type(self), a pcapkit class, so the silencing directly reaches only pcapkit's own categories and their subclasses. What genuinely reaches unrelated code is (i) the filter-list mutation itself, which overrides the application's and the interpreter's own-W/simplefilterconfiguration at index 0 for those categories, and (ii) the__warningregistry__invalidation in symptom 3, which makes any unrelated'default'/'once'warning re-fire.Minimal reproduction
hostapp.py— stands in for unrelated application code, and does not import pcapkit:repro.py:Output:
For symptom 2, run a script that calls
warn('...', ProtocolWarning)underpython -W error::UserWarning. Note that plainpython -W errorcannot be used: importing pcapkit trips an unrelatedDeprecationWarning: VueJS is deprecatedfrom a dependency at import time.Blast radius, including this project's own tests
tests/utilities/test_exceptions_warnings.py:49-55currently asserts the suppression as intended behaviour:That test asserts the warning is swallowed even though it explicitly set
simplefilter('always')— which is exactly the override described above, encoded as an expectation. So the suppression may well be deliberate; what this issue is about is the mechanism: an unscoped, unrestorable, process-global mutation performed from inside a constructor at arbitrary points during parsing.tests/toolkit/test_dpkt_unit.py:129-130also usescatch_warnings+simplefilter('ignore')and should be reviewed.Downstream applications may unknowingly depend on pcapkit warnings being silent today, so a fix will make previously invisible warnings appear. That belongs in the release notes.
What a fix would need to touch
pcapkit/utilities/warnings.py:74, inBaseWarning.__init__(65-77) — remove the global mutation from the constructor. The dead line 76,# warnings.simplefilter('default'), should go with it.pcapkit/__init__.py, behind a documented opt-in, or left to the application (which is what thewarningsmodule is for). Wrappingwarn()inwarnings.catch_warnings()would scope it but would also mean pcapkit never emits through that channel at all, which folds into the separate design question about which channel owns a warning (pcapkit/utilities/warnings.py:52-54).tests/utilities/test_exceptions_warnings.py:49-55will fail with any fix and needs rewriting to state the intended contract.tests/toolkit/test_dpkt_unit.py:129-130— check for accidental dependence on the current suppression.Observed vs inferred
-W error::UserWarningdefeat with a working control on a non-pcapkitUserWarning; the defeat of programmaticsimplefilter('always')andsimplefilter('error'); unrelated host-app warnings re-firing, with a control arm that does not re-fire;+1filter entry for a wholehttp6.capparse,+0for 5000 same-category constructions,+21for 23 distinct subclasses.warnings._add_filter's remove-then-insert is what bounds the growth (the bound was measured, the CPython path was read); that_filters_mutated()clearing__warningregistry__is the mechanism behind the re-fire (the effect is measured and controlled, the mechanism is read from the stdlib); thattest_warn_suppresses_base_warning_categories_outside_dev_modereflects deliberate intent rather than an accident — the suite was not run.Related
Same file and same family, filed separately because no single change fixes both: the double/triple emission at
pcapkit/utilities/warnings.py:52-54. The two interact — the emission counts measured there depend on this filter mutation — so decide them together, but they are two changes in two functions. Also related in shape: #362, the ERROR-level logging ofquiet=Trueexceptions, whose constructor has a comparable unscoped global side effect (sys.tracebacklimit = 0).