Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
da06b44
Split BlueapiClient, Devices, and plans
oliwenmandiamond Sep 10, 2026
def367c
Break circular dependencies, make tests mirror src
oliwenmandiamond Sep 11, 2026
7659a6b
Add doc strings to plan
oliwenmandiamond Sep 11, 2026
9f607d1
Add missing name typing
oliwenmandiamond Sep 11, 2026
f78dbe3
Merge branch 'main' into Split_client_devices_and_plans
oliwenmandiamond Sep 21, 2026
97b3312
Merge branch 'main' into Split_client_devices_and_plans
oliwenmandiamond Sep 28, 2026
a063615
Merge branch 'main' into Split_client_devices_and_plans
oliwenmandiamond Oct 8, 2026
ebec348
Merge branch 'main' into Split_client_devices_and_plans
oliwenmandiamond Oct 9, 2026
bec1209
Add back name for device and plans, remove in different PR
oliwenmandiamond Oct 13, 2026
505a11d
Add comment on why the protocols are needed
oliwenmandiamond Oct 13, 2026
c7548cf
Merge branch 'main' into Split_client_devices_and_plans
oliwenmandiamond Oct 13, 2026
206219a
Update grammar
oliwenmandiamond Oct 13, 2026
5821bd3
Merge branch 'Split_client_devices_and_plans' of github.com:DiamondLi…
oliwenmandiamond Oct 13, 2026
3d0d15a
Update device and plans to use model name
oliwenmandiamond Oct 13, 2026
eea6ef4
Apply suggestion from @ZohebShaikh
oliwenmandiamond Oct 13, 2026
ead5e93
Apply suggestion from @ZohebShaikh
oliwenmandiamond Oct 13, 2026
73102b5
Apply suggestion from @ZohebShaikh
oliwenmandiamond Oct 13, 2026
6314ac0
Update device and plans to use model name
oliwenmandiamond Oct 13, 2026
b63be0f
Merge branch 'Remove_outdated_name_from_client_plans_and_devices' of …
oliwenmandiamond Oct 13, 2026
92ec084
Formatting
oliwenmandiamond Oct 13, 2026
f6553fd
Update device and plans to use model name
oliwenmandiamond Oct 13, 2026
2e9c241
Merge branch 'Remove_outdated_name_from_client_plans_and_devices' of …
oliwenmandiamond Oct 13, 2026
6586ab6
Update ClientObjectRef to use model name
oliwenmandiamond Oct 13, 2026
8492bdd
Update device and plans to use model name
oliwenmandiamond Oct 13, 2026
6be2823
Update ClientObjectRef to use model name
oliwenmandiamond Oct 13, 2026
560ef5d
Merge branch 'Remove_outdated_name_from_client_plans_and_devices' of …
oliwenmandiamond Oct 13, 2026
87bc349
Update device and plans to use model name
oliwenmandiamond Oct 13, 2026
e6b19b4
Update ClientObjectRef to use model name
oliwenmandiamond Oct 13, 2026
0960694
Merge branch 'Remove_outdated_name_from_client_plans_and_devices' of …
oliwenmandiamond Oct 21, 2026
76d43ca
Update tests to use model name
oliwenmandiamond Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions src/blueapi/client/device_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class DeviceCache:
def __init__(self, rest: RestClientProtocol):
self._rest = rest
self._cache = {
model.name: DeviceRef(name=model.name, cache=self, model=model)
model.name: DeviceRef(cache=self, model=model)
for model in rest.get_devices().devices
}
for name, device in self._cache.items():
Expand All @@ -37,7 +37,7 @@ def __getitem__(self, name: str) -> "DeviceRef":
return dev
try:
model = self._rest.get_device(name)
device = DeviceRef(name=name, cache=self, model=model)
device = DeviceRef(cache=self, model=model)
self._cache[name] = device
setattr(self, model.name, device)
return device
Expand All @@ -63,19 +63,17 @@ class DeviceRef(ClientObjectRef):
``devices.my_device.child``.
"""

name: str
model: DeviceModel
_cache: DeviceCache

def __init__(self, name: str, cache: DeviceCache, model: DeviceModel):
self.name = name
def __init__(self, cache: DeviceCache, model: DeviceModel):
self.model = model
self._cache = cache

def __getattr__(self, name: str) -> "DeviceRef":
if name.startswith("_"):
raise AttributeError(f"No child device named {name}")
return self._cache[f"{self.name}.{name}"]
return self._cache[f"{self.model.name}.{name}"]

def __repr__(self):
return f"Device({self.name})"
return f"Device({self.model.name})"
20 changes: 8 additions & 12 deletions src/blueapi/client/plan_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ class PlanCache:
"""Collection of plans that can be accessed by name or attribute."""

def __init__(self, client: ClientProtocol, plans: list[PlanModel]):
self._cache = {
model.name: Plan(name=model.name, model=model, client=client)
for model in plans
}
self._cache = {model.name: Plan(model=model, client=client) for model in plans}
for name, plan in self._cache.items():
if name.startswith("_"):
continue
Expand All @@ -54,8 +51,7 @@ def __repr__(self) -> str:
class Plan:
"""Callable client-side reference to a registered BlueAPI plan."""

def __init__(self, name: str, model: PlanModel, client: ClientProtocol):
self.name = name
def __init__(self, model: PlanModel, client: ClientProtocol):
self.model = model
self._client = client
self.__doc__ = model.description
Expand All @@ -75,7 +71,7 @@ def __call__(self, *args, **kwargs) -> Any:
parameter schema.
"""
req = TaskRequest(
name=self.name,
name=self.model.name,
params=self._build_args(*args, **kwargs),
instrument_session=self._client.instrument_session,
)
Expand Down Expand Up @@ -117,9 +113,9 @@ def _build_args(self, *args, **kwargs) -> dict[str, Any]:
)

if len(args) > len(self.properties):
raise TypeError(f"{self.name} got too many arguments")
raise TypeError(f"{self.model.name} got too many arguments")
if extra := {k for k in kwargs if k not in self.properties}:
raise TypeError(f"{self.name} got unexpected arguments: {extra}")
raise TypeError(f"{self.model.name} got unexpected arguments: {extra}")

params = {}
# Initially fill parameters using positional args assuming the order
Expand All @@ -131,7 +127,7 @@ def _build_args(self, *args, **kwargs) -> dict[str, Any]:
for key, value in kwargs.items():
# If we've already assumed a positional arg was this value, bail out
if key in params:
raise TypeError(f"{self.name} got multiple values for {key}")
raise TypeError(f"{self.model.name} got multiple values for {key}")
params[key] = value

if missing := {k for k in self.required if k not in params}:
Expand All @@ -158,15 +154,15 @@ def _format_arg(name: str, info: dict[str, Any]) -> str:
return f"{name}: {typ} | None = None"

args = [_format_arg(name, info) for name, info in self.properties.items()]
single_line = f"{self.name}({', '.join(args)})"
single_line = f"{self.model.name}({', '.join(args)})"

if len(single_line) <= _REPR_MAX_LENGTH and len(args) <= _REPR_MAX_ARGS_INLINE:
return single_line

indent = " "
# Fall back to multiline if too many arguments or too long.
multiline_args = ",\n".join(f"{indent}{arg}" for arg in args)
return f"{self.name}(\n{multiline_args}\n)"
return f"{self.model.name}(\n{multiline_args}\n)"


def _pretty_type(schema: dict[str, Any]) -> str:
Expand Down
1 change: 0 additions & 1 deletion src/blueapi/client/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,4 @@ def get_device(self, name: str) -> DeviceModel: ...


class ClientObjectRef:
name: str
model: DeviceModel
2 changes: 1 addition & 1 deletion src/blueapi/client/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,5 +432,5 @@ class ServiceUnavailableError(Exception):
def _task_model_fallback(obj: Any) -> Any:
"""Fallback method for serializing TaskRequests"""
if isinstance(obj, ClientObjectRef):
return obj.name
return obj.model.name
raise PydanticSerializationError(f"Object of type {type(obj)} not serializable")
4 changes: 2 additions & 2 deletions tests/system_tests/test_blueapi_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def test_missing_sub_device(bc: BlueapiClient):


def test_plans_iterable(bc: BlueapiClient):
names = {plan.name for plan in bc.plans}
names = {plan.model.name for plan in bc.plans}
assert {"count", "sleep", "set_absolute"} <= names
assert all(isinstance(plan, Plan) for plan in bc.plans)

Expand Down Expand Up @@ -307,7 +307,7 @@ def test_reload_environment_clears_caches(bc: BlueapiClient):
assert new_env.environment_id != old_env.environment_id
assert bc.plans is not plans
assert bc.devices is not devices
assert {p.name for p in bc.plans} == {p.name for p in plans}
assert {p.model.name for p in bc.plans} == {p.model.name for p in plans}


def test_callbacks_receive_worker_events(bc: BlueapiClient):
Expand Down
2 changes: 0 additions & 2 deletions tests/unit_tests/client/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,6 @@ def test_scripting_interface_returns_result():
result=TaskResult(result=42, type="int"),
)
demo_plan = Plan(
name="demo",
client=client,
model=PlanModel(name="demo", description="Demo plan", schema={}),
)
Expand All @@ -521,7 +520,6 @@ def test_scripting_interface_raises_exceptions():
result=TaskError(type="ValueError", message="Plan failed"),
)
demo_plan = Plan(
name="demo",
client=client,
model=PlanModel(name="demo", description="Demo plan", schema={}),
)
Expand Down
4 changes: 2 additions & 2 deletions tests/unit_tests/client/test_device_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ def test_device_repr():
cache = Mock()
model = Mock()
model.name = "foo"
dev = DeviceRef(name="foo", cache=cache, model=model)
dev = DeviceRef(cache=cache, model=model)
assert repr(dev) == "Device(foo)"


def test_device_ignores_underscores():
cache = MagicMock()
model = Mock()
model.name = "foo"
dev = DeviceRef(name="foo", cache=cache, model=model)
dev = DeviceRef(cache=cache, model=model)
with pytest.raises(AttributeError, match="_underscore"):
_ = dev._underscore
cache.__getitem__.assert_not_called()
13 changes: 5 additions & 8 deletions tests/unit_tests/client/test_plan_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,12 @@ def test_plan_cache_repr(client):


def test_plan_help_text(client):
plan = Plan("foo", PlanModel(name="foo", description="help for foo"), client)
plan = Plan(PlanModel(name="foo", description="help for foo"), client)
assert plan.help_text == "help for foo"


def test_plan_fallback_help_text(client):
plan = Plan(
"foo",
PlanModel(
name="foo",
schema={"properties": {"one": {}, "two": {}}, "required": ["one"]},
Expand All @@ -51,7 +50,6 @@ def test_plan_fallback_help_text(client):

def test_plan_multi_parameter_fallback_help_text(client):
plan = Plan(
"foo",
PlanModel(
name="foo",
schema={
Expand Down Expand Up @@ -102,15 +100,14 @@ def test_plan_help_text_with_ref(client):
"required": ["spec"],
}

plan = Plan("ref_plan", PlanModel(name="ref_plan", schema=schema), client)
plan = Plan(PlanModel(name="ref_plan", schema=schema), client)
expected = "Plan ref_plan(spec: Spec, meta: str = 'abc')"

assert plan.help_text == expected


def test_plan_properties(client):
plan = Plan(
"foo",
PlanModel(
name="foo",
schema={"properties": {"one": {}, "two": {}}, "required": ["one"]},
Expand All @@ -123,7 +120,7 @@ def test_plan_properties(client):

def test_plan_empty_fallback_help_text(client):
plan = Plan(
"foo", PlanModel(name="foo", schema={"properties": {}, "required": []}), client
PlanModel(name="foo", schema={"properties": {}, "required": []}), client
)
assert plan.help_text == "Plan foo()"

Expand All @@ -144,7 +141,7 @@ def test_plan_empty_fallback_help_text(client):
def test_plan_param_mapping(args, kwargs, params):
client = Mock()
client.instrument_session = "cm12345-1"
plan = Plan("foobar", FULL_PLAN, client)
plan = Plan(FULL_PLAN, client)

plan(*args, **kwargs)
client.run_task.assert_called_once_with(
Expand All @@ -170,7 +167,7 @@ def test_plan_param_mapping(args, kwargs, params):
def test_plan_invalid_param_mapping(args, kwargs, msg):
client = Mock()
client.instrument_session = "cm12345-1"
plan = Plan("foobar", FULL_PLAN, client)
plan = Plan(FULL_PLAN, client)

with pytest.raises(TypeError, match=msg):
plan(*args, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/client/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def test_create_task_serialization():
request = TaskRequest(
name="demo",
instrument_session="cm12345-1",
params={"devices": [DeviceRef(name="foo", cache=Mock(), model=model)]},
params={"devices": [DeviceRef(cache=Mock(), model=model)]},
)

BlueapiRestClient.create_task(rest, request)
Expand Down
Loading