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>
This commit is contained in:
46
tests/test_import_hygiene.py
Normal file
46
tests/test_import_hygiene.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user