|
3 | 3 | # SPDX-License-Identifier: BSD-2-Clause |
4 | 4 |
|
5 | 5 | """ |
6 | | -Utility functions for creating local proxy objects that provide strongly typed |
7 | | -access to remote objects living in Appose worker processes. |
| 6 | +Utility functions for creating local proxy objects that provide access to |
| 7 | +remote objects living in Appose worker processes. |
8 | 8 |
|
9 | | -A proxy object forwards method calls to a corresponding object in the worker |
10 | | -process by generating and executing scripts via Tasks. This provides a more |
11 | | -natural, object-oriented API compared to manually constructing script strings |
12 | | -for each method invocation. |
| 9 | +A proxy object forwards attribute accesses and method calls to a corresponding |
| 10 | +object in the worker process by generating and executing scripts via Tasks. |
| 11 | +This provides a natural, object-oriented API compared to manually constructing |
| 12 | +script strings for each operation. |
13 | 13 |
|
14 | | -Type safety is honor-system based: The interface you provide must match the |
15 | | -actual methods and signatures of the remote object. If there's a mismatch, |
16 | | -you'll get runtime errors from the worker process. |
| 14 | +Proxy objects support: |
| 15 | +- Attribute access: proxy.field returns the field value (or another proxy) |
| 16 | +- Method calls: proxy.method(args) invokes the method remotely |
| 17 | +- Chaining: proxy.obj.method(args) works naturally |
| 18 | +- Callables: proxy() invokes the proxied object if it's callable |
17 | 19 |
|
18 | 20 | Usage pattern: First, create and export the remote object via a task, |
19 | 21 | then create a proxy to interact with it: |
20 | 22 |
|
21 | 23 | service = env.python() |
22 | 24 | service.task("task.export(my_obj=MyClass())").wait_for() |
23 | | - proxy = service.proxy("my_obj", MyInterface) |
| 25 | + proxy = service.proxy("my_obj") |
24 | 26 | result = proxy.some_method(42) # Executes remotely |
25 | 27 |
|
| 28 | +Automatic proxying: When a task returns a non-JSON-serializable object, it's |
| 29 | +automatically exported and returned as a proxy object: |
| 30 | +
|
| 31 | + counter = service.task("import collections; collections.Counter('abbc')").wait_for().result() |
| 32 | + # counter is now a ProxyObject wrapping the remote Counter |
| 33 | + total = counter.total() # Access the total method and call it |
| 34 | +
|
26 | 35 | Important: Variables must be explicitly exported using task.export(varName=value) |
27 | 36 | in a previous task before they can be proxied. Exported variables persist across |
28 | 37 | tasks within the same service. |
@@ -77,34 +86,45 @@ def create(service: Service, var: str, queue: str | None = None) -> Any: |
77 | 86 | RuntimeError: If a proxied method call fails in the worker process. |
78 | 87 | """ |
79 | 88 |
|
80 | | - class ProxyHandler: |
| 89 | + class ProxyObject: |
81 | 90 | def __init__(self, service: Service, var: str, queue: str | None): |
82 | 91 | self._service = service |
83 | 92 | self._var = var |
84 | 93 | self._queue = queue |
85 | 94 |
|
86 | 95 | def __getattr__(self, name: str): |
87 | | - def method(*args): |
88 | | - # Construct map of input arguments. |
89 | | - inputs = {} |
90 | | - arg_names = [] |
91 | | - for i, arg in enumerate(args): |
92 | | - arg_name = f"arg{i}" |
93 | | - inputs[arg_name] = arg |
94 | | - arg_names.append(arg_name) |
95 | | - |
96 | | - # Use the service's ScriptSyntax to generate the method invocation script. |
97 | | - # This allows support for different languages with varying syntax. |
98 | | - syntax.validate(self._service) |
99 | | - script = self._service._syntax.invoke_method(self._var, name, arg_names) |
100 | | - |
101 | | - try: |
102 | | - task = self._service.task(script, inputs, self._queue) |
103 | | - task.wait_for() |
104 | | - return task.result() |
105 | | - except Exception as e: |
106 | | - raise RuntimeError(str(e)) from e |
107 | | - |
108 | | - return method |
109 | | - |
110 | | - return ProxyHandler(service, var, queue) # type: ignore |
| 96 | + # Immediately evaluate the attribute access on the worker. |
| 97 | + attr_expr = f"{self._var}.{name}" |
| 98 | + |
| 99 | + try: |
| 100 | + task = self._service.task(attr_expr, queue=self._queue) |
| 101 | + task.wait_for() |
| 102 | + result = task.result() |
| 103 | + # If result is a worker_object, it will already be a ProxyObject |
| 104 | + # thanks to proxify_worker_objects() in Task._handle(). |
| 105 | + return result |
| 106 | + except Exception as e: |
| 107 | + raise RuntimeError(str(e)) from e |
| 108 | + |
| 109 | + def __call__(self, *args): |
| 110 | + # Invoke the proxied object as a callable. |
| 111 | + # Construct map of input arguments. |
| 112 | + inputs = {} |
| 113 | + arg_names = [] |
| 114 | + for i, arg in enumerate(args): |
| 115 | + arg_name = f"arg{i}" |
| 116 | + inputs[arg_name] = arg |
| 117 | + arg_names.append(arg_name) |
| 118 | + |
| 119 | + # Use the service's ScriptSyntax to generate the call script. |
| 120 | + syntax.validate(self._service) |
| 121 | + script = self._service._syntax.call(self._var, arg_names) |
| 122 | + |
| 123 | + try: |
| 124 | + task = self._service.task(script, inputs, self._queue) |
| 125 | + task.wait_for() |
| 126 | + return task.result() |
| 127 | + except Exception as e: |
| 128 | + raise RuntimeError(str(e)) from e |
| 129 | + |
| 130 | + return ProxyObject(service, var, queue) # type: ignore |
0 commit comments