Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit ba567ee

Browse files
server-side proxy calls
When using a ref: that is internally used by another driver on the server side, forward the calls to the target driver. Needs to resolve the target device first, and for that we need the actual root device as a reference point to start the search from. Raise on improper use - trying to access report() or other driver call before the target is resolved. Also change report() to not require "root", as the function always returns a report of itself - or the proxy target in case of Proxy driver
1 parent ae62043 commit ba567ee

3 files changed

Lines changed: 105 additions & 11 deletions

File tree

packages/jumpstarter-driver-composite/jumpstarter_driver_composite/driver.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from dataclasses import field
12
from functools import reduce
23

34
from pydantic.dataclasses import dataclass
@@ -20,22 +21,33 @@ class Composite(CompositeInterface, Driver):
2021
@dataclass(kw_only=True)
2122
class Proxy(Driver):
2223
ref: str
24+
_proxy_target: Driver | None = field(default=None, init=False, repr=False)
2325

2426
@classmethod
2527
def client(cls) -> str:
26-
return "jumpstarter.client.DriverClient" # unused
28+
raise NotImplementedError("Proxy.client() should never be called; report() delegates to target")
2729

28-
def __target(self, root, name):
30+
def _resolve_proxy_target(self, root, name):
31+
if self._proxy_target:
32+
return self._proxy_target
2933
try:
3034
path = self.ref.split(".")
3135
if not path:
3236
raise ConfigurationError(f"Proxy driver {name} has empty path")
33-
return reduce(lambda instance, name: instance.children[name], path, root)
37+
self._proxy_target = reduce(lambda instance, name: instance.children[name], path, root)
38+
return self._proxy_target
3439
except KeyError:
3540
raise ConfigurationError(f"Proxy driver {name} references nonexistent driver {self.ref}") from None
3641

37-
def report(self, *, root=None, parent=None, name=None):
38-
return self.__target(root, name).report(root=root, parent=parent, name=name)
42+
def report(self, *, parent=None, name=None):
43+
if not self._proxy_target:
44+
raise RuntimeError("Proxy target not resolved. Call enumerate() before report()")
45+
return self._proxy_target.report(parent=parent, name=name)
3946

4047
def enumerate(self, *, root=None, parent=None, name=None):
41-
return self.__target(root, name).enumerate(root=root, parent=parent, name=name)
48+
return self._resolve_proxy_target(root or self, name).enumerate(root=root or self, parent=parent, name=name)
49+
50+
def __getattr__(self, name):
51+
if not self._proxy_target:
52+
raise RuntimeError(f"Proxy target not resolved. Call enumerate() before accessing '{name}'")
53+
return getattr(self._proxy_target, name)

packages/jumpstarter-driver-composite/jumpstarter_driver_composite/driver_test.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,42 @@
11
from jumpstarter_driver_power.driver import MockPower
2+
from pydantic.dataclasses import dataclass
23

34
from .driver import Composite, Proxy
45
from jumpstarter.common.utils import serve
6+
from jumpstarter.driver import Driver, export
7+
8+
9+
# Mock serial driver with a connect() method
10+
@dataclass(kw_only=True)
11+
class MockSerial(Driver):
12+
connected: bool = False
13+
14+
@classmethod
15+
def client(cls) -> str:
16+
return "jumpstarter.client.DriverClient"
17+
18+
@export
19+
def connect(self):
20+
self.connected = True
21+
return "connected"
22+
23+
@export
24+
def read(self):
25+
return "data"
26+
27+
28+
# Mock parent driver that accesses proxy child methods
29+
@dataclass(kw_only=True)
30+
class MockParent(Driver):
31+
@classmethod
32+
def client(cls) -> str:
33+
return "jumpstarter.client.DriverClient"
34+
35+
@export
36+
def initialize(self):
37+
# This simulates RideSX accessing self.children["serial"].connect()
38+
result = self.children["serial"].connect()
39+
return f"initialized with {result}"
540

641

742
def test_drivers_composite():
@@ -23,3 +58,54 @@ def test_drivers_composite():
2358
client.composite1.power1.on()
2459
client.proxy0.on()
2560
client.proxy1.power1.on()
61+
62+
63+
def test_proxy_method_forwarding():
64+
"""Test that Proxy forwards method calls to target driver"""
65+
# Server-side test: verify __getattr__ works on Proxy
66+
actual_serial = MockSerial()
67+
proxy = Proxy(ref="test")
68+
composite = Composite(
69+
children={
70+
"proxy_serial": proxy,
71+
"test": actual_serial,
72+
}
73+
)
74+
75+
# Simulate enumerate() being called (happens during serve())
76+
composite.enumerate()
77+
78+
# Now test that proxy forwards method calls to target
79+
result = proxy.connect()
80+
assert result == "connected"
81+
assert actual_serial.connected is True
82+
83+
data = proxy.read()
84+
assert data == "data"
85+
86+
87+
def test_proxy_in_parent_child():
88+
"""Test that parent driver can call methods on Proxy child (RideSX scenario)"""
89+
# Server-side test: verify parent accessing self.children["serial"].method()
90+
actual_serial = MockSerial()
91+
proxy = Proxy(ref="actual_serial")
92+
parent = MockParent(
93+
children={
94+
"serial": proxy,
95+
}
96+
)
97+
composite = Composite(
98+
children={
99+
"parent": parent,
100+
"actual_serial": actual_serial,
101+
}
102+
)
103+
104+
# Simulate enumerate() being called (happens during serve())
105+
composite.enumerate()
106+
107+
# Now test that parent.initialize() works, which internally calls
108+
# self.children["serial"].connect() on the Proxy
109+
result = parent.initialize()
110+
assert result == "initialized with connected"
111+
assert actual_serial.connected is True

packages/jumpstarter/jumpstarter/driver/base.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,12 @@ async def Stream(self, request, context):
195195
) as stream:
196196
yield stream
197197

198-
def report(self, *, root=None, parent=None, name=None):
198+
def report(self, *, parent=None, name=None):
199199
"""
200200
Create DriverInstanceReport
201201
202202
:meta private:
203203
"""
204-
205-
if root is None:
206-
root = self
207-
208204
return jumpstarter_pb2.DriverInstanceReport(
209205
uuid=str(self.uuid),
210206
parent_uuid=str(parent.uuid) if parent else None,

0 commit comments

Comments
 (0)