nfcscript 0.0.16__tar.gz → 0.0.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {nfcscript-0.0.16 → nfcscript-0.0.17}/PKG-INFO +1 -1
  2. {nfcscript-0.0.16 → nfcscript-0.0.17}/pyproject.toml +2 -5
  3. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/nfc_cli.py +3 -3
  4. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/reader.py +197 -64
  5. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/PKG-INFO +1 -1
  6. {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_reader.py +139 -83
  7. {nfcscript-0.0.16 → nfcscript-0.0.17}/README.md +0 -0
  8. {nfcscript-0.0.16 → nfcscript-0.0.17}/setup.cfg +0 -0
  9. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/__init__.py +0 -0
  10. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/assertions.py +0 -0
  11. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/bits.py +0 -0
  12. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/checksum.py +0 -0
  13. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/cli.py +0 -0
  14. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/delay.py +0 -0
  15. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/hex_util.py +0 -0
  16. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/trace.py +0 -0
  17. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/SOURCES.txt +0 -0
  18. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/dependency_links.txt +0 -0
  19. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/entry_points.txt +0 -0
  20. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/requires.txt +0 -0
  21. {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/top_level.txt +0 -0
  22. {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_assertions.py +0 -0
  23. {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_bits.py +0 -0
  24. {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_checksum.py +0 -0
  25. {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_cli.py +0 -0
  26. {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_cli_collect.py +0 -0
  27. {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_delay.py +0 -0
  28. {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_hex_util.py +0 -0
  29. {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_trace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nfcscript
3
- Version: 0.0.16
3
+ Version: 0.0.17
4
4
  Summary: A plugin-based core engine for DSL-style NFC testing automation.
5
5
  Author: crthu
6
6
  Requires-Python: >=3.14
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nfcscript"
3
- version = "0.0.16"
3
+ version = "0.0.17"
4
4
  description = "A plugin-based core engine for DSL-style NFC testing automation."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -24,9 +24,6 @@ testpaths = ["tests"]
24
24
  [tool.uv]
25
25
  package = true
26
26
 
27
- [tool.uv.sources]
28
- nfctester = { workspace = true }
29
-
30
27
  [dependency-groups]
31
28
  dev = [
32
29
  "pytest",
@@ -34,7 +31,7 @@ dev = [
34
31
  ]
35
32
 
36
33
  [tool.bumpversion]
37
- current_version = "0.0.16"
34
+ current_version = "0.0.17"
38
35
  commit = true
39
36
  tag = true
40
37
  allow_dirty = true
@@ -172,13 +172,13 @@ def main():
172
172
  parser.error("需要指定卡片类型 (-c/--card) 或使用 --list-cards 查看可用类型")
173
173
 
174
174
  try:
175
- connect(port=args.port, reader_type=args.reader)
175
+ reader.connect(port=args.port, reader_type=args.reader)
176
176
  except Exception as e:
177
177
  print(f"连接读卡器失败: {e}")
178
178
  sys.exit(1)
179
179
 
180
180
  try:
181
- card = CardRegistry.create(args.card, reader=get_reader())
181
+ card = CardRegistry.create(args.card, reader=reader.target)
182
182
  methods = _get_methods(card)
183
183
 
184
184
  print(f"已连接卡片: {args.card}")
@@ -220,7 +220,7 @@ def main():
220
220
  print(f"错误: {e}")
221
221
 
222
222
  finally:
223
- close()
223
+ reader.close()
224
224
  print("已断开连接")
225
225
 
226
226
 
@@ -1,5 +1,10 @@
1
+ import atexit
1
2
  import os
3
+ import sys
2
4
  from contextlib import contextmanager
5
+ from typing import Any
6
+
7
+ from dotenv import load_dotenv
3
8
  from nfctester.drivers.card_reader import CardInfo, TransceiveBits
4
9
  from nfctester.registry import CardReaderRegistry
5
10
  from nfctester.trace import trace as _trace
@@ -7,44 +12,104 @@ from .assertions import ASSERT_EQUAL, ASSERT_IS_NOT_NONE, ASSERT_LEN
7
12
  from .checksum import GET_BCC
8
13
  from .hex_util import FORMAT_HEX
9
14
 
15
+ _DOTENV_LOADED = False
16
+
17
+
18
+ def _find_dotenv_all(start_dir: str) -> list[str]:
19
+ """从起始目录逐级向上搜索所有 .env 文件"""
20
+ chain = []
21
+ path = start_dir
22
+ while True:
23
+ env_path = os.path.join(path, ".env")
24
+ if os.path.exists(env_path):
25
+ chain.append(env_path)
26
+ parent = os.path.dirname(path)
27
+ if parent == path:
28
+ break
29
+ path = parent
30
+ chain.reverse()
31
+ return chain
32
+
33
+
34
+ def _load_env_configs() -> None:
35
+ """按需加载当前目录及上级目录的 .env 文件"""
36
+ global _DOTENV_LOADED
37
+ if not _DOTENV_LOADED:
38
+ try:
39
+ cwd = os.getcwd()
40
+ for env_path in _find_dotenv_all(cwd):
41
+ load_dotenv(env_path, override=False)
42
+ except Exception:
43
+ pass
44
+ _DOTENV_LOADED = True
45
+
46
+
47
+ # ─── 轻量读卡器门面单例 ───────────────────────────────────────────────────
10
48
 
11
- class _NFCState:
12
- """单例状态管理,封装读卡器生命周期"""
49
+ class _ReaderFacade:
50
+ """
51
+ 读卡器单例门面对象 (`reader`)。
52
+
53
+ 统一维护读卡器连接状态,提供 `reader.rf = True / False` 属性控制,
54
+ 并将所有属性与方法调用优先路由到当前模块顶层函数,或透传到底层 CardReader 实例。
55
+ """
56
+
57
+ def __init__(self) -> None:
58
+ self._target: Any = None
59
+ self._connected: bool = False
60
+
61
+ @property
62
+ def rf(self) -> bool:
63
+ """获取射频场状态。"""
64
+ return getattr(get_reader(), "rf_field", False)
65
+
66
+ @rf.setter
67
+ def rf(self, value: bool) -> None:
68
+ """设置射频场开关 (True / False)。"""
69
+ get_reader().rf_field = bool(value)
13
70
 
14
- _instance = None
71
+ @property
72
+ def rf_field(self) -> bool:
73
+ """获取射频场状态 (rf 别名)。"""
74
+ return self.rf
15
75
 
16
- def __new__(cls):
17
- if cls._instance is None:
18
- cls._instance = super().__new__(cls)
19
- cls._instance._reader = None
20
- cls._instance._connected = False
21
- return cls._instance
76
+ @rf_field.setter
77
+ def rf_field(self, value: bool) -> None:
78
+ """设置射频场开关 (rf 别名)。"""
79
+ self.rf = value
22
80
 
23
81
  @property
24
- def reader(self):
25
- return self._reader
82
+ def mf_crypto(self) -> bool:
83
+ """获取 Mifare 硬件加密状态。"""
84
+ return getattr(get_reader(), "mf_crypto", False)
26
85
 
27
- def connect(self, port: str = "COM20", reader_type: str = "pn532"):
28
- self._reader = CardReaderRegistry.create(reader_type, transport="serial", port=port)
29
- self._reader.open()
30
- self._connected = True
31
- _trace.log(f"Connected to {reader_type.upper()} on port {port}", layer="app")
86
+ @property
87
+ def target(self) -> Any:
88
+ """获取当前连接的底层 CardReader 实例。"""
89
+ return get_reader()
32
90
 
33
- def disconnect(self):
34
- if self._connected and self._reader:
35
- self._reader.close()
36
- self._connected = False
37
- self._reader = None
38
- _trace.log("Disconnected", layer="app")
91
+ def __getattr__(self, name: str) -> Any:
92
+ # 1. 优先路由到当前模块的顶层函数(如 reader.active -> active)
93
+ mod = sys.modules[__name__]
94
+ if hasattr(mod, name) and not name.startswith("_"):
95
+ return getattr(mod, name)
96
+ # 2. 其次透传给底层 CardReader 驱动实例(如 reader.mf_auth -> _target.mf_auth)
97
+ return getattr(self.target, name)
39
98
 
40
- def ensure_connected(self):
41
- ASSERT_IS_NOT_NONE(self._reader, msg="NFC driver not connected. Call connect() first.")
99
+ def __enter__(self):
100
+ get_reader()
101
+ return self
42
102
 
103
+ def __exit__(self, exc_type, exc_val, exc_tb):
104
+ pass
43
105
 
44
- _state = _NFCState()
45
106
 
107
+ reader = _ReaderFacade()
46
108
 
47
- def connect(port: str = None, reader_type: str = None):
109
+
110
+ # ─── 模块级顶层函数(单一事实来源) ──────────────────────────────────────────
111
+
112
+ def connect(port: str = None, reader_type: str = None) -> None:
48
113
  """
49
114
  连接并初始化读卡器。
50
115
 
@@ -55,6 +120,7 @@ def connect(port: str = None, reader_type: str = None):
55
120
  Raises:
56
121
  ValueError: 未提供 port 或 reader_type 且环境变量未设置。
57
122
  """
123
+ _load_env_configs()
58
124
  if port is None:
59
125
  port = os.environ.get("NFC_PORT")
60
126
  if reader_type is None:
@@ -63,18 +129,53 @@ def connect(port: str = None, reader_type: str = None):
63
129
  raise ValueError("未指定串口,请通过 -p 参数、connect(port=...) 或 NFC_PORT 环境变量配置")
64
130
  if reader_type is None:
65
131
  raise ValueError("未指定读卡器类型,请通过 -r 参数、connect(reader_type=...) 或 NFC_READER 环境变量配置")
66
- _state.connect(port, reader_type)
132
+
133
+ if reader._connected and reader._target is not None:
134
+ disconnect()
135
+
136
+ reader._target = CardReaderRegistry.create(reader_type, transport="serial", port=port)
137
+ reader._target.open()
138
+ reader._connected = True
139
+ _trace.log(f"Connected to {reader_type.upper()} on port {port}", layer="app")
140
+
141
+
142
+ def disconnect() -> None:
143
+ """断开与读卡器的连接并释放资源。"""
144
+ if reader._connected and reader._target is not None:
145
+ try:
146
+ reader._target.rf_field = False
147
+ except Exception:
148
+ pass
149
+ try:
150
+ reader._target.close()
151
+ except Exception:
152
+ pass
153
+ reader._connected = False
154
+ reader._target = None
155
+ _trace.log("Disconnected", layer="app")
156
+
157
+
158
+ def close() -> None:
159
+ """断开与读卡器的连接 (disconnect 别名)。"""
160
+ disconnect()
67
161
 
68
162
 
69
- def get_reader():
163
+ def get_reader() -> Any:
70
164
  """
71
- 获取当前连接的读卡器实例,供 Card 类使用。
165
+ 获取当前连接的读卡器实例,若未连接则尝试从环境变量自动连接。
72
166
 
73
167
  Returns:
74
168
  reader: 当前连接的读卡器实例。
75
169
  """
76
- _state.ensure_connected()
77
- return _state.reader
170
+ if not reader._connected or reader._target is None:
171
+ _load_env_configs()
172
+ port = os.environ.get("NFC_PORT")
173
+ reader_type = os.environ.get("NFC_READER")
174
+ if port and reader_type:
175
+ connect(port, reader_type)
176
+ else:
177
+ ASSERT_IS_NOT_NONE(reader._target, msg="NFC driver not connected. Call connect() first.")
178
+ return reader._target
78
179
 
79
180
 
80
181
  def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int = 0x26) -> CardInfo | None:
@@ -94,11 +195,10 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
94
195
  - 'atq' (list[int]): 卡片的 ATQ (Answer To Request)。
95
196
  - 'sak' (int): 卡片的 SAK (Select Acknowledge)。
96
197
  """
97
- _state.ensure_connected()
98
- reader = _state.reader
198
+ rdr = get_reader()
99
199
 
100
200
  if not low_layer:
101
- return reader.active()
201
+ return rdr.active()
102
202
 
103
203
  # 手动底层寻卡流程
104
204
  # 1. REQA
@@ -109,6 +209,7 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
109
209
 
110
210
  # 2. 抗冲突与选择
111
211
  full_uid = []
212
+ sak = 0
112
213
  for cl in [1, 2, 3]:
113
214
  res = anticoll(cl_level=cl, nvb=0x20)
114
215
  if not res.data:
@@ -153,30 +254,35 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
153
254
  return info
154
255
 
155
256
 
156
- def transceive(data: list[int], tx_crc: bool = True, rx_crc: bool = True) -> list[int]:
257
+ def transceive(data: list[int] | bytes, tx_crc: bool = True, rx_crc: bool = True) -> list[int]:
157
258
  """
158
259
  使用底层帧交互方式发送数据。
159
260
 
160
261
  Args:
161
- data: 要发送的字节列表。
262
+ data: 要发送的字节列表或 bytes。
162
263
  tx_crc: 发送时是否自动添加 CRC。False 则原样发送。
163
264
  rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
164
265
 
165
266
  Returns:
166
267
  list[int]: 接收到的数据字节列表。
167
268
  """
168
- _state.ensure_connected()
169
- reader = _state.reader
170
- res = reader.transceive(data, tx_crc=tx_crc, rx_crc=rx_crc)
269
+ rdr = get_reader()
270
+ raw_list = list(data) if isinstance(data, (bytes, bytearray)) else data
271
+ res = rdr.transceive(raw_list, tx_crc=tx_crc, rx_crc=rx_crc)
171
272
  return res.data if res and res.data else []
172
273
 
173
274
 
174
- def transceive_bits(data: list[int], last_tx_bits: int = 0, tx_crc: bool = True, rx_crc: bool = True) -> TransceiveBits:
275
+ def transceive_bits(
276
+ data: list[int] | bytes,
277
+ last_tx_bits: int = 0,
278
+ tx_crc: bool = True,
279
+ rx_crc: bool = True,
280
+ ) -> TransceiveBits:
175
281
  """
176
282
  使用底层帧交互方式发送数据,并支持对最后一个字节进行位控制。
177
283
 
178
284
  Args:
179
- data: 要发送的字节列表。
285
+ data: 要发送的字节列表或 bytes。
180
286
  last_tx_bits: 最后一个字节实际发送的有效位数 (1-7),0 表示发送完整字节。
181
287
  tx_crc: 发送时是否自动添加 CRC。False 则原样发送。
182
288
  rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
@@ -184,9 +290,9 @@ def transceive_bits(data: list[int], last_tx_bits: int = 0, tx_crc: bool = True,
184
290
  Returns:
185
291
  TransceiveBits: 包含 data 和 bits 属性,失败返回空的 TransceiveBits(data=[], bits=0)。
186
292
  """
187
- _state.ensure_connected()
188
- reader = _state.reader
189
- res = reader.transceive(data, last_tx_bits=last_tx_bits, tx_crc=tx_crc, rx_crc=rx_crc)
293
+ rdr = get_reader()
294
+ raw_list = list(data) if isinstance(data, (bytes, bytearray)) else data
295
+ res = rdr.transceive(raw_list, last_tx_bits=last_tx_bits, tx_crc=tx_crc, rx_crc=rx_crc)
190
296
  return res if res else TransceiveBits(data=[], bits=0)
191
297
 
192
298
 
@@ -273,33 +379,24 @@ def select(cl_level: int, uid: list[int]) -> list[int]:
273
379
  return transceive(cmd, tx_crc=True, rx_crc=True)
274
380
 
275
381
 
276
- def field_on():
382
+ def field_on() -> None:
277
383
  """
278
- 开启 PN532 的 RF 场。
384
+ 开启读卡器的 RF 场。
279
385
 
280
386
  Raises:
281
387
  AssertionError: 未连接读卡器时抛出。
282
388
  """
283
- _state.ensure_connected()
284
- _state.reader.rf_field = True
389
+ get_reader().rf_field = True
285
390
 
286
391
 
287
- def field_off():
392
+ def field_off() -> None:
288
393
  """
289
- 关闭 PN532 的 RF 场。
394
+ 关闭读卡器的 RF 场。
290
395
 
291
396
  Raises:
292
397
  AssertionError: 未连接读卡器时抛出。
293
398
  """
294
- _state.ensure_connected()
295
- _state.reader.rf_field = False
296
-
297
-
298
- def close():
299
- """
300
- 断开与 PN532 读卡器的连接。
301
- """
302
- _state.disconnect()
399
+ get_reader().rf_field = False
303
400
 
304
401
 
305
402
  @contextmanager
@@ -308,8 +405,8 @@ def session(port: str = None, reader_type: str = None):
308
405
  上下文管理器,自动管理连接生命周期。
309
406
 
310
407
  用法:
311
- with session("COM20") as reader:
312
- card_info = reader.active()
408
+ with session("COM20") as r:
409
+ card_info = r.active()
313
410
  """
314
411
  if port is None:
315
412
  port = os.environ.get("NFC_PORT")
@@ -319,8 +416,44 @@ def session(port: str = None, reader_type: str = None):
319
416
  raise ValueError("未指定串口,请通过 session(port=...) 或 NFC_PORT 环境变量配置")
320
417
  if reader_type is None:
321
418
  raise ValueError("未指定读卡器类型,请通过 session(reader_type=...) 或 NFC_READER 环境变量配置")
322
- _state.connect(port, reader_type)
419
+ connect(port, reader_type)
323
420
  try:
324
- yield _state.reader
421
+ yield reader
325
422
  finally:
326
- _state.disconnect()
423
+ disconnect()
424
+
425
+
426
+ def _atexit_cleanup() -> None:
427
+ """进程退出时的 atexit 安全清理钩子:先关闭物理 RF 场,再关闭串口。"""
428
+ if reader._connected and reader._target is not None:
429
+ try:
430
+ reader._target.rf_field = False
431
+ except Exception:
432
+ pass
433
+ try:
434
+ reader._target.close()
435
+ except Exception:
436
+ pass
437
+
438
+
439
+ atexit.register(_atexit_cleanup)
440
+
441
+
442
+ __all__ = [
443
+ "reader",
444
+ "connect",
445
+ "disconnect",
446
+ "get_reader",
447
+ "active",
448
+ "transceive",
449
+ "transceive_bits",
450
+ "reqa",
451
+ "wupa",
452
+ "halt",
453
+ "anticoll",
454
+ "select",
455
+ "field_on",
456
+ "field_off",
457
+ "close",
458
+ "session",
459
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nfcscript
3
- Version: 0.0.16
3
+ Version: 0.0.17
4
4
  Summary: A plugin-based core engine for DSL-style NFC testing automation.
5
5
  Author: crthu
6
6
  Requires-Python: >=3.14
@@ -1,60 +1,70 @@
1
1
  """reader 模块测试 (connect / get_reader / active / transceive / session 等)"""
2
+ import os
3
+ import sys
2
4
  import pytest
3
- from unittest.mock import MagicMock, patch, PropertyMock
5
+ from unittest.mock import MagicMock, patch
6
+ import nfc
4
7
  from nfc import reader
5
8
  from nfctester.drivers.card_reader import CardInfo, TransceiveBits
9
+ from nfctester.registry import CardReaderRegistry
6
10
 
7
11
 
8
12
  @pytest.fixture(autouse=True)
9
13
  def reset_state():
10
14
  """每个测试前重置全局状态"""
11
- reader._state._reader = None
12
- reader._state._connected = False
15
+ reader._target = None
16
+ reader._connected = False
13
17
  yield
14
- reader._state._reader = None
15
- reader._state._connected = False
18
+ reader._target = None
19
+ reader._connected = False
16
20
 
17
21
 
18
22
  def _mock_reader():
19
23
  mock = MagicMock()
20
24
  mock.transceive.return_value = TransceiveBits(data=[0xAA, 0xBB], bits=8)
21
25
  mock.active.return_value = CardInfo(uid=[0x01, 0x02, 0x03, 0x04], atq=[0x44, 0x00], sak=0x00)
26
+ mock.rf_field = False
27
+ mock.mf_crypto = False
22
28
  return mock
23
29
 
24
30
 
25
31
  class TestConnect:
26
32
 
27
- @patch.object(reader.CardReaderRegistry, "create")
33
+ @patch.object(CardReaderRegistry, "create")
28
34
  def test_connect_with_params(self, mock_create):
29
- mock_reader = _mock_reader()
30
- mock_create.return_value = mock_reader
31
- reader.connect(port="COM99", reader_type="pn532")
35
+ mock_rdr = _mock_reader()
36
+ mock_create.return_value = mock_rdr
37
+ nfc.connect(port="COM99", reader_type="pn532")
32
38
  mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
33
- mock_reader.open.assert_called_once()
34
- assert reader._state._connected is True
39
+ mock_rdr.open.assert_called_once()
40
+ assert reader._connected is True
35
41
 
36
- @patch.object(reader.CardReaderRegistry, "create")
42
+ @patch.object(CardReaderRegistry, "create")
37
43
  def test_connect_from_env(self, mock_create, monkeypatch):
38
44
  monkeypatch.setenv("NFC_PORT", "COM10")
39
45
  monkeypatch.setenv("NFC_READER", "pn532")
40
46
  mock_create.return_value = _mock_reader()
41
- reader.connect()
47
+ nfc.connect()
42
48
  mock_create.assert_called_once_with("pn532", transport="serial", port="COM10")
43
49
 
44
- def test_connect_no_port_raises(self):
50
+ def test_connect_no_port_raises(self, monkeypatch):
51
+ monkeypatch.delenv("NFC_PORT", raising=False)
52
+ monkeypatch.delenv("NFC_READER", raising=False)
45
53
  with pytest.raises(ValueError, match="未指定串口"):
46
- reader.connect(reader_type="pn532")
54
+ nfc.connect(reader_type="pn532")
47
55
 
48
- def test_connect_no_reader_type_raises(self):
56
+ def test_connect_no_reader_type_raises(self, monkeypatch):
57
+ monkeypatch.delenv("NFC_PORT", raising=False)
58
+ monkeypatch.delenv("NFC_READER", raising=False)
49
59
  with pytest.raises(ValueError, match="未指定读卡器类型"):
50
- reader.connect(port="COM99")
60
+ nfc.connect(port="COM99")
51
61
 
52
- @patch.object(reader.CardReaderRegistry, "create")
62
+ @patch.object(CardReaderRegistry, "create")
53
63
  def test_connect_overrides_env(self, mock_create, monkeypatch):
54
64
  monkeypatch.setenv("NFC_PORT", "COM10")
55
65
  monkeypatch.setenv("NFC_READER", "clrc663")
56
66
  mock_create.return_value = _mock_reader()
57
- reader.connect(port="COM99", reader_type="pn532")
67
+ nfc.connect(port="COM99", reader_type="pn532")
58
68
  mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
59
69
 
60
70
 
@@ -62,123 +72,128 @@ class TestGetReader:
62
72
 
63
73
  def test_get_reader_when_connected(self):
64
74
  mock = _mock_reader()
65
- reader._state._reader = mock
66
- reader._state._connected = True
67
- assert reader.get_reader() is mock
68
-
69
- def test_get_reader_when_not_connected(self):
75
+ reader._target = mock
76
+ reader._connected = True
77
+ assert nfc.get_reader() is mock
78
+ assert reader.target is mock
79
+
80
+ def test_get_reader_when_not_connected(self, monkeypatch):
81
+ monkeypatch.delenv("NFC_PORT", raising=False)
82
+ monkeypatch.delenv("NFC_READER", raising=False)
70
83
  with pytest.raises(AssertionError):
71
- reader.get_reader()
84
+ nfc.get_reader()
72
85
 
73
86
 
74
87
  class TestTransceive:
75
88
 
76
89
  def test_transceive_returns_data(self):
77
90
  mock = _mock_reader()
78
- reader._state._reader = mock
79
- reader._state._connected = True
80
- result = reader.transceive([0x30, 0x00])
91
+ reader._target = mock
92
+ reader._connected = True
93
+ result = nfc.transceive([0x30, 0x00])
81
94
  assert result == [0xAA, 0xBB]
82
95
  mock.transceive.assert_called_once_with([0x30, 0x00], tx_crc=True, rx_crc=True)
83
96
 
84
97
  def test_transceive_no_crc(self):
85
98
  mock = _mock_reader()
86
- reader._state._reader = mock
87
- reader._state._connected = True
88
- reader.transceive([0x30], tx_crc=False, rx_crc=False)
99
+ reader._target = mock
100
+ reader._connected = True
101
+ nfc.transceive([0x30], tx_crc=False, rx_crc=False)
89
102
  mock.transceive.assert_called_once_with([0x30], tx_crc=False, rx_crc=False)
90
103
 
91
104
  def test_transceive_empty_result(self):
92
105
  mock = _mock_reader()
93
106
  mock.transceive.return_value = None
94
- reader._state._reader = mock
95
- reader._state._connected = True
96
- result = reader.transceive([0x30])
107
+ reader._target = mock
108
+ reader._connected = True
109
+ result = nfc.transceive([0x30])
97
110
  assert result == []
98
111
 
99
- def test_transceive_not_connected(self):
112
+ def test_transceive_not_connected(self, monkeypatch):
113
+ monkeypatch.delenv("NFC_PORT", raising=False)
114
+ monkeypatch.delenv("NFC_READER", raising=False)
100
115
  with pytest.raises(AssertionError):
101
- reader.transceive([0x30])
116
+ nfc.transceive([0x30])
102
117
 
103
118
 
104
119
  class TestTransceiveBits:
105
120
 
106
121
  def test_transceive_bits_returns_result(self):
107
122
  mock = _mock_reader()
108
- reader._state._reader = mock
109
- reader._state._connected = True
110
- result = reader.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
123
+ reader._target = mock
124
+ reader._connected = True
125
+ result = nfc.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
111
126
  assert result is not None
112
127
  assert result.data == [0xAA, 0xBB]
113
128
 
114
129
  def test_transceive_bits_passes_params(self):
115
130
  mock = _mock_reader()
116
- reader._state._reader = mock
117
- reader._state._connected = True
118
- reader.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
131
+ reader._target = mock
132
+ reader._connected = True
133
+ nfc.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
119
134
  mock.transceive.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
120
135
 
121
136
 
122
137
  class TestReqa:
123
138
 
124
- @patch.object(reader, "transceive_bits")
139
+ @patch("nfc.reader.transceive_bits")
125
140
  def test_reqa_default(self, mock_tb):
126
141
  mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
127
- result = reader.reqa()
142
+ result = nfc.reqa()
128
143
  mock_tb.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
129
144
  assert result is not None
130
145
 
131
- @patch.object(reader, "transceive_bits")
146
+ @patch("nfc.reader.transceive_bits")
132
147
  def test_reqa_custom_cmd(self, mock_tb):
133
148
  mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
134
- reader.reqa(cmd=0x20)
149
+ nfc.reqa(cmd=0x20)
135
150
  mock_tb.assert_called_once_with([0x20], last_tx_bits=7, tx_crc=False, rx_crc=False)
136
151
 
137
152
 
138
153
  class TestWupa:
139
154
 
140
- @patch.object(reader, "transceive_bits")
155
+ @patch("nfc.reader.transceive_bits")
141
156
  def test_wupa(self, mock_tb):
142
157
  mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
143
- result = reader.wupa()
158
+ result = nfc.wupa()
144
159
  mock_tb.assert_called_once_with([0x52], last_tx_bits=7, tx_crc=False, rx_crc=False)
145
160
  assert result is not None
146
161
 
147
162
 
148
163
  class TestHalt:
149
164
 
150
- @patch.object(reader, "transceive")
165
+ @patch("nfc.reader.transceive")
151
166
  def test_halt(self, mock_ts):
152
167
  mock_ts.return_value = []
153
- result = reader.halt()
168
+ result = nfc.halt()
154
169
  mock_ts.assert_called_once_with([0x50, 0x00], tx_crc=True, rx_crc=True)
155
170
 
156
171
 
157
172
  class TestSelect:
158
173
 
159
- @patch.object(reader, "transceive")
174
+ @patch("nfc.reader.transceive")
160
175
  def test_select_cl1(self, mock_ts):
161
176
  mock_ts.return_value = [0x08]
162
- result = reader.select(cl_level=1, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
177
+ result = nfc.select(cl_level=1, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
163
178
  mock_ts.assert_called_once_with(
164
179
  [0x93, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
165
180
  tx_crc=True, rx_crc=True
166
181
  )
167
182
  assert result == [0x08]
168
183
 
169
- @patch.object(reader, "transceive")
184
+ @patch("nfc.reader.transceive")
170
185
  def test_select_cl2(self, mock_ts):
171
186
  mock_ts.return_value = [0x08]
172
- reader.select(cl_level=2, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
187
+ nfc.select(cl_level=2, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
173
188
  mock_ts.assert_called_once_with(
174
189
  [0x95, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
175
190
  tx_crc=True, rx_crc=True
176
191
  )
177
192
 
178
- @patch.object(reader, "transceive")
193
+ @patch("nfc.reader.transceive")
179
194
  def test_select_cl3(self, mock_ts):
180
195
  mock_ts.return_value = [0x08]
181
- reader.select(cl_level=3, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
196
+ nfc.select(cl_level=3, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
182
197
  mock_ts.assert_called_once_with(
183
198
  [0x97, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
184
199
  tx_crc=True, rx_crc=True
@@ -186,28 +201,28 @@ class TestSelect:
186
201
 
187
202
  def test_select_invalid_cl(self):
188
203
  with pytest.raises(ValueError, match="Invalid CL level"):
189
- reader.select(cl_level=4, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
204
+ nfc.select(cl_level=4, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
190
205
 
191
206
 
192
207
  class TestAnticoll:
193
208
 
194
- @patch.object(reader, "transceive_bits")
209
+ @patch("nfc.reader.transceive_bits")
195
210
  def test_anticoll_cl1(self, mock_tb):
196
211
  mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
197
- result = reader.anticoll(cl_level=1)
212
+ result = nfc.anticoll(cl_level=1)
198
213
  mock_tb.assert_called_once_with([0x93, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
199
214
  assert result is not None
200
215
 
201
- @patch.object(reader, "transceive_bits")
216
+ @patch("nfc.reader.transceive_bits")
202
217
  def test_anticoll_cl2(self, mock_tb):
203
218
  mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
204
- reader.anticoll(cl_level=2)
219
+ nfc.anticoll(cl_level=2)
205
220
  mock_tb.assert_called_once_with([0x95, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
206
221
 
207
- @patch.object(reader, "transceive_bits")
222
+ @patch("nfc.reader.transceive_bits")
208
223
  def test_anticoll_with_uid_prefix(self, mock_tb):
209
224
  mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
210
- reader.anticoll(cl_level=1, uid_prefix=[0x04, 0x12, 0x34, 0x56])
225
+ nfc.anticoll(cl_level=1, uid_prefix=[0x04, 0x12, 0x34, 0x56])
211
226
  mock_tb.assert_called_once_with(
212
227
  [0x93, 0x20, 0x04, 0x12, 0x34, 0x56],
213
228
  last_tx_bits=0, tx_crc=False, rx_crc=False
@@ -218,58 +233,99 @@ class TestFieldControl:
218
233
 
219
234
  def test_field_on(self):
220
235
  mock = _mock_reader()
221
- reader._state._reader = mock
222
- reader._state._connected = True
223
- reader.field_on()
236
+ reader._target = mock
237
+ reader._connected = True
238
+ nfc.field_on()
224
239
  assert mock.rf_field is True
225
240
 
226
241
  def test_field_off(self):
227
242
  mock = _mock_reader()
228
- reader._state._reader = mock
229
- reader._state._connected = True
230
- reader.field_off()
243
+ reader._target = mock
244
+ reader._connected = True
245
+ nfc.field_off()
231
246
  assert mock.rf_field is False
232
247
 
233
248
 
234
249
  class TestSession:
235
250
 
236
- @patch.object(reader._state, "disconnect")
237
- @patch.object(reader._state, "connect")
251
+ @patch("nfc.reader.disconnect")
252
+ @patch("nfc.reader.connect")
238
253
  def test_session_yields_reader(self, mock_connect, mock_disconnect):
239
254
  mock_rdr = _mock_reader()
240
255
 
241
256
  def fake_connect(port="COM20", reader_type="pn532"):
242
- reader._state._reader = mock_rdr
243
- reader._state._connected = True
257
+ reader._target = mock_rdr
258
+ reader._connected = True
244
259
 
245
260
  mock_connect.side_effect = fake_connect
246
261
 
247
- with reader.session(port="COM99", reader_type="pn532") as r:
248
- assert r is mock_rdr
262
+ with nfc.session(port="COM99", reader_type="pn532") as r:
263
+ assert r is reader
249
264
  mock_disconnect.assert_called_once()
250
265
 
251
- def test_session_no_port_raises(self):
266
+ def test_session_no_port_raises(self, monkeypatch):
267
+ monkeypatch.delenv("NFC_PORT", raising=False)
268
+ monkeypatch.delenv("NFC_READER", raising=False)
252
269
  with pytest.raises(ValueError, match="未指定串口"):
253
- with reader.session(reader_type="pn532"):
270
+ with nfc.session(reader_type="pn532"):
254
271
  pass
255
272
 
256
- def test_session_no_reader_type_raises(self):
273
+ def test_session_no_reader_type_raises(self, monkeypatch):
274
+ monkeypatch.delenv("NFC_PORT", raising=False)
275
+ monkeypatch.delenv("NFC_READER", raising=False)
257
276
  with pytest.raises(ValueError, match="未指定读卡器类型"):
258
- with reader.session(port="COM99"):
277
+ with nfc.session(port="COM99"):
259
278
  pass
260
279
 
261
280
 
262
281
  class TestGetSelCmd:
263
282
 
264
283
  def test_cl1(self):
265
- assert reader._get_sel_cmd(1) == 0x93
284
+ from nfc.reader import _get_sel_cmd
285
+ assert _get_sel_cmd(1) == 0x93
266
286
 
267
287
  def test_cl2(self):
268
- assert reader._get_sel_cmd(2) == 0x95
288
+ from nfc.reader import _get_sel_cmd
289
+ assert _get_sel_cmd(2) == 0x95
269
290
 
270
291
  def test_cl3(self):
271
- assert reader._get_sel_cmd(3) == 0x97
292
+ from nfc.reader import _get_sel_cmd
293
+ assert _get_sel_cmd(3) == 0x97
272
294
 
273
295
  def test_invalid(self):
296
+ from nfc.reader import _get_sel_cmd
274
297
  with pytest.raises(ValueError):
275
- reader._get_sel_cmd(4)
298
+ _get_sel_cmd(4)
299
+
300
+
301
+ class TestReaderSingletonProxy:
302
+
303
+ @patch.object(CardReaderRegistry, "create")
304
+ def test_reader_rf_property_and_lazy_connect(self, mock_create, monkeypatch):
305
+ monkeypatch.setenv("NFC_PORT", "COM10")
306
+ monkeypatch.setenv("NFC_READER", "pn532")
307
+ mock_rdr = _mock_reader()
308
+ mock_create.return_value = mock_rdr
309
+
310
+ # reader.rf triggers auto-connect
311
+ reader.rf = True
312
+ assert mock_rdr.rf_field is True
313
+ assert reader.rf is True
314
+ assert reader.rf_field is True
315
+ assert reader.mf_crypto is False
316
+
317
+ @patch.object(CardReaderRegistry, "create")
318
+ def test_reader_delegates_to_functions_and_target(self, mock_create, monkeypatch):
319
+ monkeypatch.setenv("NFC_PORT", "COM10")
320
+ monkeypatch.setenv("NFC_READER", "pn532")
321
+ mock_rdr = _mock_reader()
322
+ mock_create.return_value = mock_rdr
323
+
324
+ # reader.active calls active()
325
+ card = reader.active()
326
+ assert card is not None
327
+ assert card.uid == [0x01, 0x02, 0x03, 0x04]
328
+
329
+ # reader.custom_method delegates to mock_rdr
330
+ mock_rdr.custom_method = MagicMock(return_value="OK")
331
+ assert reader.custom_method() == "OK"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes