feat(client): make dir()/help() work through _SyncProxy

The sync REPL proxy forwarded methods via __getattr__, so dir()/tab-completion never saw them and the sync wrapper reported (*args, **kwargs) instead of the real signature. Add __dir__ delegating to the wrapped target, and decorate the async wrapper with functools.wraps so inspect.signature()/help() report the real params + docstring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-05 00:20:03 -07:00
parent a90a01f2aa
commit 739e28e113
2 changed files with 41 additions and 2 deletions

View File

@@ -1,5 +1,6 @@
"""Main Proxmark3 client with nested API.""" """Main Proxmark3 client with nested API."""
import asyncio import asyncio
import functools
import glob import glob
import inspect import inspect
import logging import logging
@@ -161,13 +162,20 @@ class _SyncProxy:
if hasattr(attr, '_t'): if hasattr(attr, '_t'):
return _SyncProxy(attr, self._loop) return _SyncProxy(attr, self._loop)
if callable(attr) and inspect.iscoroutinefunction(attr): 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): def sync_wrapper(*args, **kwargs):
return self._loop.run_until_complete(attr(*args, **kwargs)) return self._loop.run_until_complete(attr(*args, **kwargs))
sync_wrapper.__name__ = name
sync_wrapper.__doc__ = attr.__doc__
return sync_wrapper return sync_wrapper
return attr 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): def close(self):
"""Disconnect and close the event loop.""" """Disconnect and close the event loop."""
self._loop.run_until_complete(self._target.disconnect()) self._loop.run_until_complete(self._target.disconnect())

31
tests/test_sync_proxy.py Normal file
View File

@@ -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."