Repository navigation
tests/utilities/test_compat.py poisons aenum._common.pyver for the whole process, breaking a later import pcapkit #687
Description
Activity
- addedtestPull requests that add or correct tests (test: subject prefix)Pull requests that add or correct tests (test: subject prefix)
on Sep 22, 2026 Reproduced on
f0999858ebefore fixing it, and one part of the premise here needs
correcting — in a direction that makes the defect a little worse than filed.The reproduction stands, verbatim.
python -m unittest tests.utilities.test_compat tests.project.test_public_apion CPython 3.14.7 givesRan 14 tests/
FAILED (errors=10), all ten beingAttributeError: 'TransportProtocol' object has no attribute '__set_name__'raised ataenum/_enum.py:1640from
pcapkit/const/reg/apptype.py:26.pytest -p no:randomly --noconftest -qover the same
two files gives10 failed, 4 passed. Each file alone passes."On Python ≥ 3.11" is true of this test file, but not of the hazard. Below 3.11 the
real branch ofcompat.pyis itselffrom aenum import StrEnum, andsetUpexecutes
the real module before every test, so on 3.10 aenum is always already warm when the fake
goes in and the file passes. But faking the version around aenum's first ever import in
a fresh 3.10.21 process does not quietly poison a cache — it takes the import down:ImportError: cannot import name 'FlagBoundary' from 'enum' AttributeError: 'FlagBoundary' object has no attribute '__set_name__'from aenum's own fallback definition of
FlagBoundary. So what protects 3.10 today is
an incidental ordering insidesetUp, not a structural guarantee, and pre-importing
aenumis load-bearing there too.Suggested direction 3 is dead on measurement, which is worth recording since the
issue offers it:aenum/_enum.py:2isfrom ._common import *, sopyveris copied
into every aenum module's globals and_enum.py:1640branches on its own copy.
importlib.reload(aenum._common)gives_common.pyver == (3, 14)and leaves
_enum.pyver == (3, 5)— it repairs the reading nobody reads.Direction 2 is rejected on coverage, also measured:
[tool.coverage.run]here is
source = ["pcapkit"]with noparallel/concurrencyand noCOVERAGE_PROCESS_START
.pth, and coverage keys on file path, so the in-processload_module(…, 'pcapkit/utilities/compat.py')is what credits that file's< 3.6,< 3.8,< 3.9
and< 3.11branches today. A subprocess would drop the measurement.Fixed in #693 by direction 1, with the ordering asserted rather than assumed — the
window between installing and removing the fake is measured, and any name first-imported
inside it fails the test at the line that caused it. Theaenumpyverassertion you
asked for is there, swept across every aenum module that holds one rather than just
_common, and runs as a cleanup after every test in the file.- added 4 commits that reference this issue
on Sep 23, 2026
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
tests/utilities/test_compat.py::test_python35_fallback_implementationsfakessys.version_info = (3, 5)and then executespcapkit/utilities/compat.py:143,from aenum import StrEnum. On Python ≥ 3.11 that is the first-everimport aenumin the process, soaenum/_common.py:17cachespyver = (3, 5)permanently. Every later use ofaenumin that process is then operating under a false Python version.The consequence is not subtle: a later
import pcapkitdies ataenum/_enum.py:1640withbecause
aenumskips the descriptor protocol it believes the running interpreter lacks.Why it is normally invisible
Masked under pytest by
tests/conftest.py'spytest_sessionstart, which importspcapkiteagerly — soaenumis already imported with the true version before any test runs, and the fake assignment caches nothing new.So this only bites when
aenumhas not already been imported: the stdlibunittestrunner, a bare script, or any future change to that session hook. Confirmed with a standalone script using neitherunittestnormock.Why the test cannot simply be left alone
The test is legitimate — it exercises the
< 3.6fallback branch ofcompat.py. The problem is that fakingsys.version_infoaround a realimportleaks state into a third-party module's import-time cache, and no amount of restoringsys.version_infoafterwards undoes it, because the damage is a value already memoised inaenum._common.Suggested directions, not prescribed
aenumbefore the fake so the cache is already warm with the true version — smallest change, but relies on ordering that nothing enforces.aenum._commonafterwards — fragile, since other modules hold references to the cached value.Whichever is chosen, a regression test should assert
aenum._common.pyvermatches the real interpreter after the suite, since that is the invariant being broken.Provenance
Found by #686's worker while auditing
sys.modulesleakage for #674, and deliberately not fixed there — it is a different mechanism (a third-party import-time cache, notsys.modules) andtests/utilities/test_compat.pywas outside that PR's scope. Reproduction is its measurement; I have not independently re-run it.Related: #674 (the
sys.modulesrestoration gap, fixed in #686) and thetests/cli/test_main.pyleak filed alongside this.