diff --git a/pm3py/core/client.py b/pm3py/core/client.py index ba100fa..e57cdb5 100644 --- a/pm3py/core/client.py +++ b/pm3py/core/client.py @@ -1,5 +1,6 @@ """Main Proxmark3 client with nested API.""" import asyncio +import functools import glob import inspect import logging @@ -161,13 +162,20 @@ class _SyncProxy: if hasattr(attr, '_t'): return _SyncProxy(attr, self._loop) if callable(attr) and inspect.iscoroutinefunction(attr): + # functools.wraps copies __doc__/__name__ AND sets __wrapped__, so + # inspect.signature() and help() report the real params (block, uid=None) + # instead of the wrapper's (*args, **kwargs). + @functools.wraps(attr) def sync_wrapper(*args, **kwargs): return self._loop.run_until_complete(attr(*args, **kwargs)) - sync_wrapper.__name__ = name - sync_wrapper.__doc__ = attr.__doc__ return sync_wrapper return attr + def __dir__(self): + # Surface the wrapped target's command methods and sub-modules so + # dir()/REPL tab-completion and help() discovery work through the proxy. + return sorted(set(object.__dir__(self)) | set(dir(self._target))) + def close(self): """Disconnect and close the event loop.""" self._loop.run_until_complete(self._target.disconnect()) diff --git a/tests/test_sync_proxy.py b/tests/test_sync_proxy.py new file mode 100644 index 0000000..d4d7300 --- /dev/null +++ b/tests/test_sync_proxy.py @@ -0,0 +1,31 @@ +"""_SyncProxy introspection: dir() and help()/signature must work through the proxy.""" +import asyncio +import inspect +from unittest.mock import AsyncMock + +from pm3py.core.client import _SyncProxy +from pm3py.core.hf_iso15 import HF15Commands + + +def _proxy(): + return _SyncProxy(HF15Commands(AsyncMock()), asyncio.new_event_loop()) + + +def test_dir_surfaces_target_methods(): + d = dir(_proxy()) + for name in ("scan", "rdbl", "wrbl", "raw_inventory", "sniff"): + assert name in d, f"{name} missing from dir(proxy)" + + +def test_wrapped_method_preserves_signature(): + proxy = _proxy() + # signature must be the real params, not the wrapper's (*args, **kwargs) + sig = inspect.signature(proxy.rdbl) + assert list(sig.parameters) == ["block", "uid"] + assert sig.parameters["uid"].default is None + + +def test_wrapped_method_preserves_doc_and_name(): + proxy = _proxy() + assert proxy.rdbl.__name__ == "rdbl" + assert proxy.rdbl.__doc__ == "Read a single block."