Files
pm3py/tests/test_import_hygiene.py
michael 0586e2a41a fix(sim): break the transponders<->sim circular import via lazy re-exports
Importing a transponder model first (e.g. iso15693.type5, or iso14443a via the fuzzer) triggered pm3py.sim's __init__, which eagerly re-imported that same transponder before it finished initialising — ImportError on a partially-initialised module. Masked in the full suite by import order; failed whenever a transponder was the first import.

Move sim/__init__'s ~90 transponder re-exports into sim/_models.py and load them lazily via a PEP 562 __getattr__ (public API unchanged: attribute access, from-import, __all__, dir(), and 'import *' all still work). Defer fuzzer.py's iso14443a.base import into the one method that uses it. Add tests/test_import_hygiene.py — subprocess fresh-import guards per transponder family.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 21:23:12 -07:00

47 lines
1.8 KiB
Python

"""Guards against the transponders<->sim circular import.
Transponder models import ``pm3py.sim`` primitives (Transponder/Reader/RFFrame/Medium), so
``pm3py.sim`` must NOT eagerly re-import transponder models at package-init time — its model
re-exports are lazy (see ``pm3py/sim/_models.py`` + the package ``__getattr__``), and ``fuzzer.py``
defers its ``iso14443a.base`` import. Each family must therefore import as a *fresh* first import.
These run in subprocesses because import-order bugs only show in a clean interpreter.
"""
import subprocess
import sys
import pytest
# each of these, imported first in a clean interpreter, used to trigger a circular import
_FRESH_IMPORTS = [
"pm3py.transponders.hf.iso15693.type5", # the reported failure
"pm3py.transponders.hf.iso14443a.base", # the fuzzer-side failure
"pm3py.transponders.hf.iso14443b.base",
"pm3py.transponders.lf.atmel.t5577",
"pm3py.transponders.implants",
"pm3py.sim.fuzzer",
"pm3py.sim",
]
@pytest.mark.parametrize("module", _FRESH_IMPORTS)
def test_fresh_import_has_no_cycle(module):
r = subprocess.run([sys.executable, "-c", f"import {module}"],
capture_output=True, text=True)
assert r.returncode == 0, f"importing {module} first failed:\n{r.stderr}"
def test_sim_lazy_reexports_resolve():
import pm3py.sim as sim
from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag
assert sim.NfcType5Tag is NfcType5Tag # lazy re-export == canonical class
assert "NfcType5Tag" in dir(sim)
assert all(getattr(sim, n, None) is not None for n in sim.__all__)
def test_fuzzer_deferred_import_works():
from pm3py.sim.fuzzer import GrammarFuzzer
frame = GrammarFuzzer("14443a").generate("SELECT", uid=b"\x04\x11\x22\x33")
assert frame.data.hex() == "93700411223304" # CL1, NVB_SELECT, uid, bcc