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:
@@ -16,68 +16,27 @@ from .mcu_bridge import McuBridge
|
|||||||
from .dual_session import DualInterfaceSession
|
from .dual_session import DualInterfaceSession
|
||||||
|
|
||||||
# --- Transponder models (canonical home: pm3py.transponders) ---
|
# --- Transponder models (canonical home: pm3py.transponders) ---
|
||||||
from pm3py.transponders.hf.iso14443a.base import (
|
# Loaded lazily (PEP 562) from ._models. Those models import back into pm3py.sim for the
|
||||||
Tag14443A, Tag14443A_3, Tag14443A_4, Reader14443A, State14443A,
|
# Transponder/Reader/RFFrame/Medium primitives, so importing them at package-init time would
|
||||||
)
|
# create a transponders<->sim circular import (breaking e.g. `from pm3py.transponders...` as a
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1
|
# first import). On first access they load from ._models and cache into this module's globals.
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import (
|
def __getattr__(name):
|
||||||
MifareClassicTag, MifareClassicReader, MifareAccessConditions,
|
if name.startswith("__") and name.endswith("__"):
|
||||||
)
|
raise AttributeError(name) # don't trigger a model import for dunder probes
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader
|
import importlib
|
||||||
from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag, NfcType4Tag
|
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.type2 import NxpType2Tag, Type2Config
|
models = importlib.import_module(f"{__name__}._models")
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.ntag21x import (
|
try:
|
||||||
Ntag21x, NTAG210, NTAG212, NTAG213, NTAG215, NTAG216, Ntag21xConfig,
|
value = getattr(models, name)
|
||||||
)
|
except AttributeError:
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.ultralight import (
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
||||||
MifareUltralight, MifareUltralightC, MifareUltralightEV1, MF0UL11, MF0UL21,
|
globals()[name] = value # cache: subsequent access is a direct lookup
|
||||||
UL_C_DEFAULT_KEY, UltralightEV1Config, UltralightCConfig,
|
return value
|
||||||
)
|
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.ntag_i2c import (
|
|
||||||
NtagI2C, NT3H2111, NT3H2211, NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C,
|
def __dir__():
|
||||||
)
|
return sorted(set(globals()) | set(__all__))
|
||||||
from pm3py.transponders.hf.iso14443a.st.st25tn import ST25TN, ST25TN512, ST25TN01K
|
|
||||||
from pm3py.transponders.hf.iso14443a.infineon.optiga_nbt import OptigaAuthenticateNBT
|
|
||||||
from pm3py.transponders.hf.iso14443a.st.st25ta import (
|
|
||||||
ST25TA, ST25TA512B, ST25TA02KB, ST25TA16K, ST25TA64K,
|
|
||||||
)
|
|
||||||
from pm3py.transponders.hf.iso15693.base import Tag15693, Reader15693, State15693
|
|
||||||
from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag, ndef_text, ndef_uri, ndef_mime
|
|
||||||
from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
|
|
||||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
|
||||||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
|
||||||
from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag, IcodeFeatureFlags
|
|
||||||
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
|
||||||
from pm3py.transponders.hf.iso15693.nxp.auth_aes import NxpKeyPrivileges
|
|
||||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import (
|
|
||||||
Ntag5PlatformTag, Ntag5StatusReg, Ntag5ConfigReg,
|
|
||||||
)
|
|
||||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
|
|
||||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
|
||||||
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
|
||||||
from pm3py.transponders.hf.iso15693.st.st25tv import ST25TV, ST25TV512C, ST25TV02KC
|
|
||||||
from pm3py.transponders.hf.iso15693.st.st25dv import (
|
|
||||||
ST25DV, ST25DV04K, ST25DV16K, ST25DV64K, St25dvRfAreaSS,
|
|
||||||
)
|
|
||||||
from pm3py.transponders.hf.iso15693.infineon.myd_vicinity import (
|
|
||||||
MydVicinity, SRF55V02P, SRF55V10P,
|
|
||||||
)
|
|
||||||
from pm3py.transponders.hf.iso14443b.base import Tag14443B, StateB, Tag14443B_4, StateB4
|
|
||||||
from pm3py.transponders.hf.iso14443b.st.st25tb import (
|
|
||||||
ST25TB, ST25TB512AC, ST25TB512AT, ST25TB02K, ST25TB04K,
|
|
||||||
)
|
|
||||||
from pm3py.transponders.hf.iso14443b.ti.rf430cl330h import RF430CL330H
|
|
||||||
from pm3py.transponders.hf.iso15693.ti.tagit import TagItHFIPlus
|
|
||||||
from pm3py.transponders.hf.iso15693.ti.rf430frl import (
|
|
||||||
RF430FRL15xH, RF430FRL152H, RF430FRL153H, RF430FRL154H,
|
|
||||||
)
|
|
||||||
from pm3py.transponders.lf.base import TagLF, ReaderLF, Modulation
|
|
||||||
from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader, EM4100Code
|
|
||||||
from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader
|
|
||||||
from pm3py.transponders.bitfield import BitField, Register
|
|
||||||
from pm3py.transponders.lf.atmel.t5577 import T5577Tag, T5577Reader, T5577Config
|
|
||||||
from pm3py.transponders.lf.nxp.hitag2 import Hitag2Tag, Hitag2Reader
|
|
||||||
from pm3py.transponders.implants import xEM, xNT, xM1, FlexDF, NExT, MagicMifareClassicTag
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"RFFrame",
|
"RFFrame",
|
||||||
|
|||||||
71
pm3py/sim/_models.py
Normal file
71
pm3py/sim/_models.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"""Transponder-model re-exports for :mod:`pm3py.sim`.
|
||||||
|
|
||||||
|
Loaded lazily by the package's PEP 562 ``__getattr__`` — these models import back into
|
||||||
|
``pm3py.sim`` (for the ``Transponder`` / ``Reader`` / ``RFFrame`` / ``Medium`` primitives), so
|
||||||
|
importing them at ``sim`` package-init time creates a circular import. Their canonical home is
|
||||||
|
``pm3py.transponders``; this module just re-exposes them under ``pm3py.sim``.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pm3py.transponders.hf.iso14443a.base import (
|
||||||
|
Tag14443A, Tag14443A_3, Tag14443A_4, Reader14443A, State14443A,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1
|
||||||
|
from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import (
|
||||||
|
MifareClassicTag, MifareClassicReader, MifareAccessConditions,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader
|
||||||
|
from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag, NfcType4Tag
|
||||||
|
from pm3py.transponders.hf.iso14443a.nxp.type2 import NxpType2Tag, Type2Config
|
||||||
|
from pm3py.transponders.hf.iso14443a.nxp.ntag21x import (
|
||||||
|
Ntag21x, NTAG210, NTAG212, NTAG213, NTAG215, NTAG216, Ntag21xConfig,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.hf.iso14443a.nxp.ultralight import (
|
||||||
|
MifareUltralight, MifareUltralightC, MifareUltralightEV1, MF0UL11, MF0UL21,
|
||||||
|
UL_C_DEFAULT_KEY, UltralightEV1Config, UltralightCConfig,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.hf.iso14443a.nxp.ntag_i2c import (
|
||||||
|
NtagI2C, NT3H2111, NT3H2211, NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.hf.iso14443a.st.st25tn import ST25TN, ST25TN512, ST25TN01K
|
||||||
|
from pm3py.transponders.hf.iso14443a.infineon.optiga_nbt import OptigaAuthenticateNBT
|
||||||
|
from pm3py.transponders.hf.iso14443a.st.st25ta import (
|
||||||
|
ST25TA, ST25TA512B, ST25TA02KB, ST25TA16K, ST25TA64K,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.hf.iso15693.base import Tag15693, Reader15693, State15693
|
||||||
|
from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag, ndef_text, ndef_uri, ndef_mime
|
||||||
|
from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
|
||||||
|
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||||||
|
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||||||
|
from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag, IcodeFeatureFlags
|
||||||
|
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
|
||||||
|
from pm3py.transponders.hf.iso15693.nxp.auth_aes import NxpKeyPrivileges
|
||||||
|
from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import (
|
||||||
|
Ntag5PlatformTag, Ntag5StatusReg, Ntag5ConfigReg,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
|
||||||
|
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
|
||||||
|
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
|
||||||
|
from pm3py.transponders.hf.iso15693.st.st25tv import ST25TV, ST25TV512C, ST25TV02KC
|
||||||
|
from pm3py.transponders.hf.iso15693.st.st25dv import (
|
||||||
|
ST25DV, ST25DV04K, ST25DV16K, ST25DV64K, St25dvRfAreaSS,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.hf.iso15693.infineon.myd_vicinity import (
|
||||||
|
MydVicinity, SRF55V02P, SRF55V10P,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.hf.iso14443b.base import Tag14443B, StateB, Tag14443B_4, StateB4
|
||||||
|
from pm3py.transponders.hf.iso14443b.st.st25tb import (
|
||||||
|
ST25TB, ST25TB512AC, ST25TB512AT, ST25TB02K, ST25TB04K,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.hf.iso14443b.ti.rf430cl330h import RF430CL330H
|
||||||
|
from pm3py.transponders.hf.iso15693.ti.tagit import TagItHFIPlus
|
||||||
|
from pm3py.transponders.hf.iso15693.ti.rf430frl import (
|
||||||
|
RF430FRL15xH, RF430FRL152H, RF430FRL153H, RF430FRL154H,
|
||||||
|
)
|
||||||
|
from pm3py.transponders.lf.base import TagLF, ReaderLF, Modulation
|
||||||
|
from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader, EM4100Code
|
||||||
|
from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader
|
||||||
|
from pm3py.transponders.bitfield import BitField, Register
|
||||||
|
from pm3py.transponders.lf.atmel.t5577 import T5577Tag, T5577Reader, T5577Config
|
||||||
|
from pm3py.transponders.lf.nxp.hitag2 import Hitag2Tag, Hitag2Reader
|
||||||
|
from pm3py.transponders.implants import xEM, xNT, xM1, FlexDF, NExT, MagicMifareClassicTag
|
||||||
@@ -6,7 +6,6 @@ from typing import Iterator
|
|||||||
|
|
||||||
from bitarray import bitarray
|
from bitarray import bitarray
|
||||||
from .frame import RFFrame
|
from .frame import RFFrame
|
||||||
from pm3py.transponders.hf.iso14443a.base import CL1, NVB_SELECT, _compute_bcc
|
|
||||||
|
|
||||||
|
|
||||||
class MutationFuzzer:
|
class MutationFuzzer:
|
||||||
@@ -76,6 +75,9 @@ class GrammarFuzzer:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _gen_14443a(self, command: str, **overrides) -> RFFrame | None:
|
def _gen_14443a(self, command: str, **overrides) -> RFFrame | None:
|
||||||
|
# deferred: iso14443a.base imports back into pm3py.sim, so a module-level import here would
|
||||||
|
# create a transponders<->sim circular import when 14443a is the first thing imported
|
||||||
|
from pm3py.transponders.hf.iso14443a.base import CL1, NVB_SELECT, _compute_bcc
|
||||||
match command:
|
match command:
|
||||||
case "REQA":
|
case "REQA":
|
||||||
return RFFrame.from_bytes(b"\x26")
|
return RFFrame.from_bytes(b"\x26")
|
||||||
|
|||||||
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