Skip to content
Open
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
1467fc6
Imrpove BlueapiClient to add plan parameter type hints
oliwenmandiamond Apr 1, 2026
d67adf8
Remove Pipfile
oliwenmandiamond Apr 1, 2026
ff42043
Remove unused code
oliwenmandiamond Apr 1, 2026
7345817
Clean up formatting
oliwenmandiamond Apr 1, 2026
689245f
Merge branch 'main' into Improve-BlueapiClient-plan-repr-to-add-param…
oliwenmandiamond Apr 1, 2026
e39a961
Merge branch 'Improve-BlueapiClient-plan-repr-to-add-parameter-types'…
oliwenmandiamond Apr 1, 2026
ec97173
Add single and multi line support
oliwenmandiamond Apr 1, 2026
a1ba5d4
Fix tests + add additional test
oliwenmandiamond Apr 1, 2026
a8aa805
Add more tests
oliwenmandiamond Apr 1, 2026
17e86b0
Improve type hints for plan signature to include defaults
oliwenmandiamond Apr 7, 2026
48c79e0
Merge branch 'main' into Improve-BlueapiClient-plan-repr-to-add-param…
oliwenmandiamond Apr 7, 2026
3ce0eff
Fix system tests
oliwenmandiamond Apr 7, 2026
47f036f
Merge branch 'Improve-BlueapiClient-plan-repr-to-add-parameter-types'…
oliwenmandiamond Apr 7, 2026
bd6ceae
Use null rather than "None"
oliwenmandiamond Apr 7, 2026
afd9841
Improve test to include optional parameter
oliwenmandiamond Apr 7, 2026
0d0c85e
Add missing defaults
oliwenmandiamond Apr 7, 2026
951eb7f
Add additional code coverage
oliwenmandiamond Apr 7, 2026
6272b42
Merge branch 'main' into Improve-BlueapiClient-plan-repr-to-add-param…
oliwenmandiamond Apr 22, 2026
ac57232
Merge branch 'main' into Improve-BlueapiClient-plan-repr-to-add-param…
oliwenmandiamond Jun 18, 2026
412b8b8
Merge branch 'main' into Improve-BlueapiClient-plan-repr-to-add-param…
oliwenmandiamond Jun 25, 2026
e694d46
Merge branch 'Improve-BlueapiClient-plan-repr-to-add-parameter-types'…
oliwenmandiamond Jun 25, 2026
fc13c02
Removed outdated test
oliwenmandiamond Jun 25, 2026
8760ac6
Remove ANY
oliwenmandiamond Jun 25, 2026
4f34f4c
rename tab to indent
oliwenmandiamond Jun 25, 2026
9938f2c
Update properties typing to KeysView
oliwenmandiamond Jun 25, 2026
8a90f92
Add _REPR_MAX_LENGTH and _REPR_MAX_ARGS_INLINE
oliwenmandiamond Jun 25, 2026
911c76f
Add fix for type | None = None
oliwenmandiamond Jun 25, 2026
2ad6299
Fix test
oliwenmandiamond Jun 25, 2026
0d777cb
try new format arg method
oliwenmandiamond Jun 25, 2026
a163bc5
Merge branch 'main' into Improve-BlueapiClient-plan-repr-to-add-param…
oliwenmandiamond Jun 25, 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
71 changes: 64 additions & 7 deletions src/blueapi/client/client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import itertools
import logging
import time
from collections.abc import Iterable
from collections.abc import Iterable, KeysView
from concurrent.futures import Future
from contextlib import suppress
from functools import cached_property
from itertools import chain
from pathlib import Path
from typing import Any, Self

Expand Down Expand Up @@ -56,6 +55,35 @@

log = logging.getLogger(__name__)

_REPR_MAX_LENGTH = 100
_REPR_MAX_ARGS_INLINE = 3


def _pretty_type(schema: dict[str, Any]) -> str:
if "$ref" in schema:
return schema["$ref"].split("/")[-1]

if schema.get("type") == "array":
item_schema = schema.get("items", {})
inner = _pretty_type(item_schema)
return f"list[{inner}]"

if "anyOf" in schema:
return " | ".join(_pretty_type(s) for s in schema["anyOf"])

json_type = schema.get("type")
type_map = {
"string": "str",
"integer": "int",
"boolean": "bool",
"number": "float",
"object": "dict",
}
Comment thread
Alexj9837 marked this conversation as resolved.
if isinstance(json_type, str):
return type_map.get(json_type, json_type.split(".")[-1])

return "Any"


class MissingInstrumentSessionError(Exception):
pass
Expand Down Expand Up @@ -164,7 +192,7 @@ def help_text(self) -> str:
return self.model.description or f"Plan {self!r}"

@property
def properties(self) -> set[str]:
def properties(self) -> KeysView[str]:
return self.model.parameter_schema.get("properties", {}).keys()

@property
Expand Down Expand Up @@ -201,10 +229,39 @@ def _build_args(self, *args, **kwargs):
raise TypeError(f"Missing argument(s) for {missing}")
return params

def __repr__(self):
opts = [p for p in self.properties if p not in self.required]
params = ", ".join(chain(self.required, (f"{opt}=None" for opt in opts)))
return f"{self.name}({params})"
def __repr__(self) -> str:
def _format_arg(name: str, info: dict[str, Any], required: set[str]) -> str:
typ = _pretty_type(info)

is_required = name in required
has_default = "default" in info
default = info.get("default")

if is_required:
return f"{name}: {typ}"

# optional with explicit default
if has_default:
if default is None:
return f"{name}: {typ} | None = None"
return f"{name}: {typ} = {repr(default)}"

# optional with no default
return f"{name}: {typ} | None = None"

props = self.model.parameter_schema.get("properties", {})
args = [
_format_arg(name, info, set(self.required)) for name, info in props.items()
]
single_line = f"{self.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)"


class BlueapiClient:
Expand Down
32 changes: 9 additions & 23 deletions src/blueapi/core/context.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to be careful that we're not making an invalid schema. With these changes the schema contains several fields such as

"group": {
    "title": "Group",
    "type": "string",
    "default": null
},

which in turn causes your repr to contain parameters such as metadata: dict = None which is not correctly typed.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from importlib import import_module, metadata
from inspect import Parameter, isclass, signature
from types import ModuleType, NoneType, UnionType
from typing import Any, Generic, TypeVar, Union, get_args, get_origin, get_type_hints
from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints

from bluesky.protocols import HasName
from bluesky.run_engine import RunEngine
Expand Down Expand Up @@ -459,14 +459,16 @@ def _type_spec_for_function(
):
default_factory = self._composite_factory(arg_type)
_type = SkipJsonSchema[self._convert_type(arg_type, no_default)]
field_info = FieldInfo(default_factory=default_factory)
else:
default_factory = DefaultFactory(para.default)
_type = self._convert_type(arg_type, no_default)
factory = None if no_default else default_factory
new_args[name] = (
_type,
FieldInfo(default_factory=factory),
)
if no_default:
field_info = FieldInfo()
else:
field_info = FieldInfo(default=para.default)

new_args[name] = (_type, field_info)

return new_args

def _convert_type(self, typ: Any, no_default: bool = True) -> type:
Expand Down Expand Up @@ -517,19 +519,3 @@ def _inject_composite():
return composite_class(**devices)

return _inject_composite


D = TypeVar("D")


class DefaultFactory(Generic[D]):
_value: D

def __init__(self, value: D):
self._value = value

def __call__(self) -> D:
return self._value

def __eq__(self, other) -> bool:
return other.__class__ == self.__class__ and self._value == other._value
34 changes: 23 additions & 11 deletions tests/system_tests/plans.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
},
"num": {
"title": "Num",
"type": "integer"
"type": "integer",
"default": 1
},
"delay": {
"anyOf": [
Expand All @@ -34,12 +35,14 @@
"type": "array"
}
],
"default": 0.0,
"title": "Delay"
},
"metadata": {
"additionalProperties": true,
"title": "Metadata",
"type": "object"
"type": "object",
"default": null
}
},
"required": [
Expand Down Expand Up @@ -681,7 +684,8 @@
"metadata": {
"additionalProperties": true,
"title": "Metadata",
"type": "object"
"type": "object",
"default": null
}
},
"required": [
Expand Down Expand Up @@ -711,11 +715,13 @@
},
"group": {
"title": "Group",
"type": "string"
"type": "string",
"default": null
},
"wait": {
"title": "Wait",
"type": "boolean"
"type": "boolean",
"default": false
}
},
"required": [
Expand Down Expand Up @@ -745,11 +751,13 @@
},
"group": {
"title": "Group",
"type": "string"
"type": "string",
"default": null
},
"wait": {
"title": "Wait",
"type": "boolean"
"type": "boolean",
"default": false
}
},
"required": [
Expand All @@ -773,7 +781,8 @@
},
"group": {
"title": "Group",
"type": "string"
"type": "string",
"default": null
}
},
"required": [
Expand All @@ -796,7 +805,8 @@
},
"group": {
"title": "Group",
"type": "string"
"type": "string",
"default": null
}
},
"required": [
Expand Down Expand Up @@ -832,11 +842,13 @@
"properties": {
"group": {
"title": "Group",
"type": "string"
"type": "string",
"default": null
},
"timeout": {
"title": "Timeout",
"type": "number"
"type": "number",
"default": null
}
},
"title": "wait",
Expand Down
67 changes: 66 additions & 1 deletion tests/unit_tests/client/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,72 @@ def test_plan_fallback_help_text(client):
),
client,
)
assert plan.help_text == "Plan foo(one, two=None)"
assert plan.help_text == "Plan foo(one: Any, two: Any | None = None)"


def test_plan_multi_parameter_fallback_help_text(client):
plan = Plan(
"foo",
PlanModel(
name="foo",
schema={
"properties": {
"one": {},
"two": {
"anyOf": [{"items": {}, "type": "array"}, {"type": "boolean"}],
},
"three": {"default": 3},
"four": {"default": None},
},
"required": ["one", "two"],
},
),
client,
)
assert (
plan.help_text == "Plan foo(\n"
" one: Any,\n"
" two: list[Any] | bool,\n"
" three: Any = 3,\n"
" four: Any | None = None\n"
")"
)


def test_plan_help_text_with_ref(client):
schema = {
"$defs": {
"Spec": {
"properties": {
"foo": {"type": "integer"},
"bar": {"$ref": "#/$defs/InnerSpec"},
},
"required": ["foo", "bar"],
},
"InnerSpec": {
"properties": {
"x": {"type": "number"},
"y": {"default": 10, "type": "number"},
},
"required": ["x"],
},
},
"properties": {
"spec": {"$ref": "#/$defs/Spec"},
"meta": {"type": "string", "default": "abc"},
},
"required": ["spec"],
}

plan = Plan(
"ref_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):
Expand Down
Loading
Loading