Report each warning once per channel; stop mutating global warning state (#362-#364) - #390
Conversation
There was a problem hiding this comment.
🟢 Approval recommended
The behavioral changes are well-scoped, consistent with the stated emission model, and are backed by targeted regression tests; remaining feedback is limited to minor documentation corrections.
Pull request overview
This PR fixes intertwined warning/exception emission defects by removing global warnings filter mutation, ensuring one emission per channel, and making quiet=True truly silent, with regression tests and documentation updates to codify the new contracts.
Changes:
- Stop mutating process-global
warnings.filtersduring warning construction; make warning construction side-effect free. - Ensure
warn()emits exactly once to logging and once to thewarningschannel (filtered normally) regardless of devmode. - Make
quiet=Trueerrors log nothing and avoid mutatingsys.tracebacklimit, with new focused regression coverage.
File summaries
| File | Description |
|---|---|
| tests/utilities/test_warning_filters.py | Adds regressions ensuring constructing/reporting pcapkit warnings does not mutate warnings.filters and does not invalidate unrelated warning registries. |
| tests/utilities/test_warning_emission.py | Pins the “one record per channel” emission model across devmode/verbose and category types. |
| tests/utilities/test_quiet_exceptions.py | Verifies quiet=True is silent and side-effect free, while loud errors still log and set sys.tracebacklimit in non-devmode. |
| tests/utilities/test_exceptions_warnings.py | Updates existing tests to reflect the new warning/exception behavior contracts. |
| tests/utilities/_unrelated_warning.py | Adds an unrelated warning category and emitter for control-arm testing of warning registry behavior. |
| tests/utilities/_harness.py | Introduces shared bootstrapping and log-capture scaffolding for the new regression tests. |
| pcapkit/utilities/warnings.py | Removes constructor side effects and documents the two-channel warning emission model; keeps filtering under caller control. |
| pcapkit/utilities/exceptions.py | Makes quiet=True exceptions silent and side-effect free; limits traceback only for loud errors outside devmode. |
| docs/source/pcapkit/utilities/warnings.rst | Documents the warning emission model and how to silence warnings via standard filters/CLI categories. |
| docs/source/pcapkit/utilities/exceptions.rst | Documents loud vs quiet errors and the behavioral change for quiet=True and traceback limiting. |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@copilot Fix the code for all comments in this review thread. When a review comment includes a suggested change, apply the suggestion exactly. Do not make changes beyond what is described in the linked review thread. |
…bal state
Three entangled defects in the warning and exception plumbing, fixed together
because they are one subsystem: how many times a warning is reported, on which
channel, and what quiet=True means.
The worst was BaseWarning.__init__ calling warnings.simplefilter('ignore',
type(self)) outside devmode. Constructing any pcapkit warning therefore inserted
an ignore filter at index 0 of the process-global filter list, overriding the
host application's own -W configuration and invalidating __warningregistry__ so
unrelated third-party warnings re-fired. It also meant pcapkit's own warnings
were invisible by default, which cannot have been the intent. The constructor is
gone; no filter is ever installed. Measured: constructing all 23 categories now
leaves warnings.filters byte-identical, an application's simplefilter('error')
now raises as it asked, and an unrelated warning no longer re-fires.
With the filter mutation gone, the emission counts settle: warn() logs exactly
one WARNING record and calls warnings.warn exactly once, in devmode and out. The
third emission was the constructor's own log call under devmode. Devmode now
changes the richness of the record - exc_info, stack_info - never the count.
Suppression is back where it belongs, with filterwarnings, catch_warnings or -W.
quiet=True meant "log at ERROR instead of CRITICAL", which is why
MultiDict.get() on an absent key logged an error per lookup - 12 of them on one
ipv6.pcap reassembly, 26 on http6.cap. It now means what it says: no record at
any level, and sys.tracebacklimit left alone, since a quiet error is pcapkit's
own control flow that it catches itself. A loud error is unchanged.
Consumers will notice three things. pcapkit warnings now reach the warnings
channel, so a project running -W error or pytest's filterwarnings=error will get
exceptions where they previously got silence - the sharpest edge, documented
under an attention block. Devmode drops from two log records per warning to one.
And a quiet exception no longer truncates tracebacks via sys.tracebacklimit.
20 tests added across three files, with a control arm on the re-fire probe so a
failure is attributable, and a devmode bootstrap that raises rather than
silently not taking effect. Before, against main's copies of both files: 25
failed, 31 passed. After: 42 passed.
Suite 508 -> 529 passed, 315 -> 332 subtests.
Closes #362, closes #363, closes #364.
The note shipped in #384 said the double-emission and the global filter mutation were deliberately left alone; this branch fixes both, so it now states the model and defers to docs/source/pcapkit/utilities/warnings.rst.
b6cb181 to
6528655
Compare
Co-authored-by: JarryShaw <15666417+JarryShaw@users.noreply.github.com>
Fixed all four issues in the review thread — committed in the latest push:
|
Three entangled defects, fixed together because they are one subsystem: how many times a warning is reported, on which channel, and what
quiet=Truemeans.The emission model
A warning is reported once per channel; construction is not a report.
warn()→ loggingWARNINGwarnings.filterslogging.getLogger('pcapkit')warn()→ warningsfilterwarnings,catch_warnings,-W, pytestSchemaWarning(...)Counts are now identical in devmode and out —
PCAPKIT_DEVMODEchanges the richness of the record (exc_info,stack_info), never the count.A logger-only model was rejected because suppression must remain available through the standard
warningsmachinery; a warnings-only model was rejected becauseexamples/generators/pcapng.pydeliberately listens to the logger, and it would depend on first fixingstacklevel()(see below).#364 — constructing a warning mutated
warnings.filtersBaseWarning.__init__calledwarnings.simplefilter('ignore', type(self))outside devmode, inserting an ignore filter at index 0 of the process-global list. So it overrode the host application's own-Wconfiguration, invalidated__warningregistry__so unrelated third-party warnings re-fired, and made pcapkit's own warnings invisible by default — which cannot have been the intent. The constructor is gone; no filter is ever installed.Measured, non-devmode:
filters == snapshotsimplefilter('always')thenwarn()simplefilter('error')thenwarn()extract()end-to-end on http6.capI confirmed the headline independently on this branch: 23 categories constructed,
filters unchanged: True, app filter wins with 1 record.#363 — each warning emitted more than once
Both
warn()and the constructor logged. Non-devmode was 1 rather than 2, because #364's ownsimplefilterswallowed thewarningsemission (CPython constructs the category before consulting the filters) — so the two issues were entangled and fixing one changed the other's observable behaviour. Devmode was exactly 3. Both are now 2: one log record, one warning.#362 —
quiet=Truestill loggedIt meant "log at
ERRORinstead ofCRITICAL", which is whyMultiDict.get()on an absent key logged an error per lookup — 12 on oneipv6.pcapreassembly, 26 onhttp6.cap. It now means what it says: no record at any level, andsys.tracebacklimitleft alone, since a quiet error is pcapkit's own control flow that it catches itself. A loud error is unchanged: oneCRITICAL, andtracebacklimit = 0outside devmode. Verified here: a quiet miss produces empty log output.Behaviour changes a consumer could notice
warningschannel — 43 new warning sites in pytest's summary. Restore withwarnings.filterwarnings('ignore', category=BaseWarning).-W error. A project running-W error::UserWarning, or pytest withfilterwarnings = error, will now get exceptions where pcapkit warnings previously vanished. The sharpest edge; documented under an.. attention::block.BaseWarningsubclass logs nothing (was 1 in devmode).quiet=Trueexceptions emit noERRORrecord — not restorable by configuration, so anything grepping logs forMissingKeyErrormust catch the exception instead.sys.tracebacklimitis no longer set by a quiet exception, so an uncaughtMultiDict()['missing']prints a normal traceback instead of one line.pcapkit.utilities.warnings.DEVMODEre-export removed (never in__all__; canonical home ispcapkit.utilities.logging.DEVMODE).BaseWarning.__init__no longer existing was checked for behavioural equivalence:args,str(), pickle round-trip, MRO, and even theTypeErrormessage for kwargs are unchanged.Verification
20 tests across three new files under
tests/utilities/, plus helpers. Two details worth noting: the re-fire probe carries a control arm asserting it can distinguish the two cases, so a failure is attributable to pcapkit rather than to the probe; and the devmode bootstrap raises ifPCAPKIT_DEVMODEdid not take effect, so a test cannot pass for the wrong reason.Before, against
main's copies of both files: 25 failed, 31 passed. After: 42 passed. Suite 508 → 529 passed, 315 → 332 subtests. isort clean; pylint 10.00/10 on the eight files authored here.tests/utilities/test_logging.pywas deliberately not touched — it belongs to #384.Merge note for #384
#384 (logging) also touches both files, and there are two reconciliations at merge time: its
logging.rstnote saying these three issues are "deliberately left alone for now" becomes stale, and itslogger = get_logger(__name__)conflicts textually — inwarnings.pykeepget_logger(__name__)and drop the now-unusedDEVMODEimport; inexceptions.pyBaseError.__init__is rewritten around the logger calls here.Reported, not fixed — a pre-existing defect this makes visible
stacklevel()(exceptions.py:55-73) returns an absolute stack index rather than a depth relative to the caller, sowarnings.warnreceives values like0and every pcapkit warning is attributed topcapkit/utilities/warnings.pyrather than the user's call site (it also keys the dedup registry there). It was invisible while the channel was swallowed. Left deliberately: it is shared by both channels and by everyBaseErrorlog record, so fixing it would relocate the reported source of every warning and error — a fourth behavioural change nobody asked for. Wants its own issue.Also corrected in the docs:
-W ignore::pcapkit.utilities.warnings.BaseWarningdoes not work — CPython rejects it, because-Wresolves the category beforesiteaddssite-packagestosys.path. Verified generic. The docs now recommend a standard base category on the CLI or a programmatic filter.Closes #362, closes #363, closes #364.