From d0def08822c65e97d6373c182067ff0c2879e25a Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 14 Sep 2026 17:45:36 -0400 Subject: [PATCH] tests: clear the stdlib ABC caches when purging pcapkit modules purge_modules() drops pcapkit from sys.modules so the next test re-imports it fresh, but the collections.abc ABCs are never purged. Every re-import rebuilds pcapkit's Mapping subclasses (Info, Schema, ContextRegistry, ProtocolContext, EnumSchema) as new class objects, and that churn corrupts the C-level _abc_impl instance-check caches on the shared ABCs. Those caches then give stale answers for immortal built-ins: isinstance({}, collections.abc.Mapping) returns False, or isinstance({}, Schema) returns True, until the cache token happens to advance. The effect is order-dependent and self-heals by teardown, so every test file passes in isolation while the full suite fails intermittently on Python 3.10 - ContextRegistry.make() taking a dict down its Iterable branch and registering the dict's keys, or Schema.to_dict() being called on a plain dict. The maintainer had already worked around one instance of this at pcapkit/corekit/infoclass.py:270 with a belt-and-braces isinstance check. purge_modules now resets the ABC caches (abc._reset_caches, guarded so a future runtime without it degrades rather than errors) after the purge, which fixes the root rather than another symptom. 3.10 CI selection: 347 passed, 14 skipped, 156 subtests passed, 0 failed. --- tests/_support.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/_support.py b/tests/_support.py index b461bd1e6b..842a9c0c22 100644 --- a/tests/_support.py +++ b/tests/_support.py @@ -1,5 +1,7 @@ from __future__ import annotations +import abc +import collections.abc import importlib.util import pathlib import sys @@ -131,10 +133,38 @@ def install_fake_payload_protocols(raw_cls: type, null_cls: type) -> None: sys.modules['pcapkit.protocols.misc.null'] = null_module +def _reset_abc_caches() -> None: + """Clear the stdlib ABC instance-check caches. + + :func:`purge_modules` drops :mod:`pcapkit` from :data:`sys.modules` so the + next test re-imports it fresh, but the :mod:`collections.abc` ABCs are + never purged. Each re-import rebuilds pcapkit's ``Mapping`` subclasses + (``Info``, ``Schema``, ``ContextRegistry``, ``ProtocolContext``, …) as new + class objects, and their creation churns the C-level ``_abc_impl`` caches + on the shared ABCs. Those caches then hold stale answers keyed on immortal + built-ins -- so ``isinstance({}, collections.abc.Mapping)`` can return + :data:`False`, or ``isinstance({}, Schema)`` :data:`True`, until the cache + token happens to advance. The effect is order-dependent and invisible when + a test file runs alone, which is why it only ever bit the full suite. + + :func:`abc._reset_caches` is a CPython internal (present on both the C + ``_abc`` and pure-python ``_py_abc`` backends); if a future runtime drops + it this degrades to the previous, occasionally-flaky behaviour rather than + erroring. + """ + reset = getattr(abc, '_reset_caches', None) + if reset is None: # pragma: no cover + return + for obj in vars(collections.abc).values(): + if isinstance(obj, type) and hasattr(obj, '_abc_impl'): + reset(obj) + + def purge_modules(prefixes: Iterable[str]) -> None: for name in list(sys.modules): if any(name == prefix or name.startswith(prefix + '.') for prefix in prefixes): sys.modules.pop(name, None) + _reset_abc_caches() def close_extractor(extractor: object) -> None: