Skip to content

Report each warning once per channel; stop mutating global warning state (#362-#364) - #390

Merged
JarryShaw merged 3 commits into
mainfrom
fix/warnings-and-exceptions
Sep 15, 2026
Merged

JarryShaw merged 3 commits into
mainfrom
fix/warnings-and-exceptions

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Three entangled defects, fixed together because they are one subsystem: how many times a warning is reported, on which channel, and what quiet=True means.

The emission model

A warning is reported once per channel; construction is not a report.

records channel governed by
warn() → logging exactly 1, WARNING unconditional, ignores warnings.filters logging.getLogger('pcapkit')
warn() → warnings exactly 1 filtered normally filterwarnings, catch_warnings, -W, pytest
constructing SchemaWarning(...) 0 — —

Counts are now identical in devmode and out — PCAPKIT_DEVMODE changes 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 warnings machinery; a warnings-only model was rejected because examples/generators/pcapng.py deliberately listens to the logger, and it would depend on first fixing stacklevel() (see below).

#364 — constructing a warning mutated warnings.filters

BaseWarning.__init__ called warnings.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 -W configuration, 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:

probe before after
filters after constructing all 23 categories +22 entries +0, filters == snapshot
app's simplefilter('always') then warn() 0 recorded 1 recorded
app's simplefilter('error') then warn() no exception raises
unrelated third-party warning re-fires True False
extract() end-to-end on http6.cap re-fired, filters +1 not re-fired, filters unchanged

I 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 own simplefilter swallowed the warnings emission (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=True still logged

It meant "log at ERROR instead of CRITICAL", which is why MultiDict.get() on an absent key logged an error per lookup — 12 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: one CRITICAL, and tracebacklimit = 0 outside devmode. Verified here: a quiet miss produces empty log output.

Behaviour changes a consumer could notice

  1. pcapkit warnings now reach the warnings channel — 43 new warning sites in pytest's summary. Restore with warnings.filterwarnings('ignore', category=BaseWarning).
  2. The application's filter now wins, including -W error. A project running -W error::UserWarning, or pytest with filterwarnings = error, will now get exceptions where pcapkit warnings previously vanished. The sharpest edge; documented under an .. attention:: block.
  3. Devmode: 2 log records per warning → 1.
  4. Constructing a BaseWarning subclass logs nothing (was 1 in devmode).
  5. quiet=True exceptions emit no ERROR record — not restorable by configuration, so anything grepping logs for MissingKeyError must catch the exception instead.
  6. sys.tracebacklimit is no longer set by a quiet exception, so an uncaught MultiDict()['missing'] prints a normal traceback instead of one line.
  7. pcapkit.utilities.warnings.DEVMODE re-export removed (never in __all__; canonical home is pcapkit.utilities.logging.DEVMODE).

BaseWarning.__init__ no longer existing was checked for behavioural equivalence: args, str(), pickle round-trip, MRO, and even the TypeError message 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 if PCAPKIT_DEVMODE did 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.py was 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.rst note saying these three issues are "deliberately left alone for now" becomes stale, and its logger = get_logger(__name__) conflicts textually — in warnings.py keep get_logger(__name__) and drop the now-unused DEVMODE import; in exceptions.py BaseError.__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, so warnings.warn receives values like 0 and every pcapkit warning is attributed to pcapkit/utilities/warnings.py rather 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 every BaseError log 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.BaseWarning does not work — CPython rejects it, because -W resolves the category before site adds site-packages to sys.path. Verified generic. The docs now recommend a standard base category on the CLI or a programmatic filter.

Closes #362, closes #363, closes #364.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.filters during warning construction; make warning construction side-effect free.
  • Ensure warn() emits exactly once to logging and once to the warnings channel (filtered normally) regardless of devmode.
  • Make quiet=True errors log nothing and avoid mutating sys.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.

Comment thread docs/source/pcapkit/utilities/exceptions.rst Outdated
Comment thread docs/source/pcapkit/utilities/warnings.rst Outdated
Comment thread pcapkit/utilities/exceptions.py Outdated
Comment thread pcapkit/utilities/warnings.py Outdated
@JarryShaw

Copy link
Copy Markdown
Owner Author

@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.
Co-authored-by: JarryShaw <15666417+JarryShaw@users.noreply.github.com>

Copilot AI commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

@copilot Fix the code for all comments in this review thread.

When a review comment includes a suggested change, apply ...

Fixed all four issues in the review thread — committed in the latest push:

  • exceptions.rst: :mod:pcapkit.exceptions → `:mod:`pcapkit.utilities.exceptions, infomation → information
  • warnings.rst: :mod:pcapkit.warnings → `:mod:`pcapkit.utilities.warnings
  • exceptions.py:109: dose → does
  • warnings.py:7: :mod:pcapkit.warnings → `:mod:`pcapkit.utilities.warnings

@JarryShaw
JarryShaw merged commit 487d3da into main Sep 15, 2026
49 checks passed
@JarryShaw
JarryShaw deleted the fix/warnings-and-exceptions branch September 17, 2026 01:08
@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) breaking Breaks public-facing behaviour or API (apply alongside the type label) labels Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaks public-facing behaviour or API (apply alongside the type label) fix Pull requests that fix a defect (fix: subject prefix)

Projects

None yet

3 participants