Skip to content

Commit 1f2207f

Browse files
ctruedenclaude
andcommitted
Add dir() support for ProxyObject via remote introspection
Implements __dir__() on ProxyObject to enable runtime introspection of remote objects. This allows users to call dir() on proxy objects to discover available attributes and methods. Key changes: - Added get_attributes() method to ScriptSyntax interface - PythonSyntax: Returns dir(obj) - delegates to object's __dir__ - GroovySyntax: Returns methods and properties from metaClass - ProxyObject.__dir__(): Executes get_attributes() script on worker and returns the result Implementation notes: - No caching - each dir() call performs a round-trip to the worker - Returns all attributes (including private ones) as determined by the remote object's own dir() implementation - Works for both Python and Groovy workers Test added: - test_proxy_dir: Verifies dir() works on custom classes and built-in objects like datetime 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 7cd313b commit 1f2207f

File tree

3 files changed

+87
-0
lines changed

3 files changed

+87
-0
lines changed

src/appose/syntax.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ def invoke_method(
108108
"""
109109
...
110110

111+
@abstractmethod
112+
def get_attributes(self, object_var_name: str) -> str:
113+
"""
114+
Generate a script expression to retrieve the list of attributes of an object.
115+
116+
This is used by proxy objects to implement __dir__() for runtime introspection.
117+
118+
Args:
119+
object_var_name: The name of the variable referencing the object.
120+
121+
Returns:
122+
A script expression that evaluates to a list of attribute names.
123+
"""
124+
...
125+
111126

112127
class PythonSyntax(ScriptSyntax):
113128
"""
@@ -139,6 +154,11 @@ def invoke_method(
139154
# Python method invocation: object.method(arg0, arg1, ...)
140155
return f"{object_var_name}.{method_name}({', '.join(arg_var_names)})"
141156

157+
def get_attributes(self, object_var_name: str) -> str:
158+
# Return all attributes from dir(), including private ones.
159+
# Let the object's __dir__ implementation decide what to expose.
160+
return f"dir({object_var_name})"
161+
142162

143163
class GroovySyntax(ScriptSyntax):
144164
"""
@@ -171,6 +191,11 @@ def invoke_method(
171191
# Groovy method invocation: object.method(arg0, arg1, ...)
172192
return f"{object_var_name}.{method_name}({', '.join(arg_var_names)})"
173193

194+
def get_attributes(self, object_var_name: str) -> str:
195+
# Return all method names and property names from the object's metaclass.
196+
# Groovy's metaClass provides runtime introspection.
197+
return f"({object_var_name}.metaClass.methods*.name + {object_var_name}.metaClass.properties*.name).unique()"
198+
174199

175200
# All known script syntax implementations.
176201
_SYNTAXES: list[ScriptSyntax] = [

src/appose/util/proxy.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,16 @@ def __call__(self, *args):
127127
except Exception as e:
128128
raise RuntimeError(str(e)) from e
129129

130+
def __dir__(self):
131+
# Query the remote object for its attributes via introspection.
132+
syntax.validate(self._service)
133+
script = self._service._syntax.get_attributes(self._var)
134+
135+
try:
136+
task = self._service.task(script, queue=self._queue)
137+
task.wait_for()
138+
return task.result()
139+
except Exception as e:
140+
raise RuntimeError(str(e)) from e
141+
130142
return ProxyObject(service, var, queue) # type: ignore

tests/test_syntax.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,53 @@ def __call__(self, x):
277277

278278
# Access a field on the callable object
279279
assert callable_obj.offset == 100
280+
281+
282+
def test_proxy_dir():
283+
"""Test that dir() works on proxy objects."""
284+
env = appose.system()
285+
with env.python() as service:
286+
maybe_debug(service)
287+
288+
# Create a custom class with known attributes
289+
obj = service.task("""
290+
class TestClass:
291+
def __init__(self):
292+
self.field1 = 42
293+
self.field2 = "hello"
294+
295+
def method1(self):
296+
return "method1"
297+
298+
def method2(self, x):
299+
return x * 2
300+
301+
TestClass()
302+
""").wait_for().result()
303+
304+
# Get dir() output
305+
attrs = dir(obj)
306+
307+
# Should be a list
308+
assert isinstance(attrs, list)
309+
310+
# Should contain our custom attributes
311+
assert "field1" in attrs
312+
assert "field2" in attrs
313+
assert "method1" in attrs
314+
assert "method2" in attrs
315+
316+
# Should also contain standard object attributes
317+
assert "__init__" in attrs
318+
assert "__class__" in attrs
319+
320+
# Test with a built-in object (datetime)
321+
dt = service.task("import datetime\ndatetime.datetime(2024, 1, 15)").wait_for().result()
322+
dt_attrs = dir(dt)
323+
324+
assert isinstance(dt_attrs, list)
325+
assert "year" in dt_attrs
326+
assert "month" in dt_attrs
327+
assert "day" in dt_attrs
328+
assert "isoformat" in dt_attrs
329+
assert "replace" in dt_attrs

0 commit comments

Comments
 (0)