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:
@@ -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
31
tests/test_sync_proxy.py
Normal 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."
|
||||||
Reference in New Issue
Block a user