From 1467fc644ecf37c4dba3afaf3d18d21d91906f9e Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Wed, 1 Apr 2026 15:28:58 +0100 Subject: [PATCH 1/7] Imrpove BlueapiClient to add plan parameter type hints --- Pipfile | 11 ++++++ src/blueapi/client/client.py | 74 +++++++++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 Pipfile diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000000..0757494bb3 --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/src/blueapi/client/client.py b/src/blueapi/client/client.py index c5b41ff45e..ff16c68a65 100644 --- a/src/blueapi/client/client.py +++ b/src/blueapi/client/client.py @@ -6,7 +6,8 @@ from functools import cached_property from itertools import chain from pathlib import Path -from typing import Self +from textwrap import dedent, indent +from typing import Any, Self from bluesky_stomp.messaging import MessageContext, StompClient from bluesky_stomp.models import Broker @@ -50,6 +51,37 @@ log = logging.getLogger(__name__) +def _pretty_type(schema: dict[str, Any]) -> str: + # refs first + if "$ref" in schema: + return schema["$ref"].split("/")[-1] + + # arrays preserve inner type + if schema.get("type") == "array": + item_schema = schema.get("items", {}) + inner = _pretty_type(item_schema) + return f"list[{inner}]" + + # unions + 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", + } + + if isinstance(json_type, str): + return type_map.get(json_type, json_type.split(".")[-1]) + + return "Any" + + class MissingInstrumentSessionError(Exception): pass @@ -154,7 +186,7 @@ def help_text(self) -> str: return self.model.description or f"Plan {self!r}" @property - def properties(self) -> set[str]: + def properties(self) -> dict[str, Any]: return self.model.parameter_schema.get("properties", {}).keys() @property @@ -192,9 +224,18 @@ def _build_args(self, *args, **kwargs): 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})" + props = self.model.parameter_schema.get("properties", {}) + tab = " " + args = [] + for name, info in props.items(): + typ = _pretty_type(info) + arg = f"{tab}{name}: {typ}" + if name not in self.required: + arg = f"{arg} | None = None" + args.append(arg) + + joined = ",\n".join(args) + return f"{self.name}(\n{joined}\n)" class BlueapiClient: @@ -281,6 +322,29 @@ def get_plans(self) -> PlanResponse: """ return self._rest.get_plans() + @property + def ls_plans(self): + for plan in self.plans: + print(plan) + print(plan.help_text) + # print("\n") + # tab = " " + + # for plan in self.get_plans().plans: + # self.ls_plan(plan.name) + + # def ls_plan(self, name: str): + # tab = " " + # plan = self.get_plan(name) + # name_with_signature = schema_to_signature(plan.parameter_schema, plan.name) + # print(name_with_signature) + # # print(schema_to_signature(plan.model_json_schema())) + # if plan.description is not None: + # print(indent(dedent(plan.description).strip(), tab)) + # # print(plan.parameter_schema) + # else: + # print(indent("No documentation provided.", tab)) + @start_as_current_span(TRACER, "name") @deprecated("plans[name]") def get_plan(self, name: str) -> PlanModel: From d67adf820a24f3f4ae7b420ef6fd61d2e368aa77 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Wed, 1 Apr 2026 15:30:09 +0100 Subject: [PATCH 2/7] Remove Pipfile --- Pipfile | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 Pipfile diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 0757494bb3..0000000000 --- a/Pipfile +++ /dev/null @@ -1,11 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] - -[dev-packages] - -[requires] -python_version = "3.11" From ff420433c883b996598d47e160c35d14aba6132d Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Wed, 1 Apr 2026 15:30:46 +0100 Subject: [PATCH 3/7] Remove unused code --- src/blueapi/client/client.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/blueapi/client/client.py b/src/blueapi/client/client.py index ff16c68a65..eb1f0bfefa 100644 --- a/src/blueapi/client/client.py +++ b/src/blueapi/client/client.py @@ -4,9 +4,7 @@ from collections.abc import Iterable from concurrent.futures import Future from functools import cached_property -from itertools import chain from pathlib import Path -from textwrap import dedent, indent from typing import Any, Self from bluesky_stomp.messaging import MessageContext, StompClient @@ -322,29 +320,6 @@ def get_plans(self) -> PlanResponse: """ return self._rest.get_plans() - @property - def ls_plans(self): - for plan in self.plans: - print(plan) - print(plan.help_text) - # print("\n") - # tab = " " - - # for plan in self.get_plans().plans: - # self.ls_plan(plan.name) - - # def ls_plan(self, name: str): - # tab = " " - # plan = self.get_plan(name) - # name_with_signature = schema_to_signature(plan.parameter_schema, plan.name) - # print(name_with_signature) - # # print(schema_to_signature(plan.model_json_schema())) - # if plan.description is not None: - # print(indent(dedent(plan.description).strip(), tab)) - # # print(plan.parameter_schema) - # else: - # print(indent("No documentation provided.", tab)) - @start_as_current_span(TRACER, "name") @deprecated("plans[name]") def get_plan(self, name: str) -> PlanModel: From 73458170501e0210e7ed7d2acffe24b09d40ef8d Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Wed, 1 Apr 2026 15:32:00 +0100 Subject: [PATCH 4/7] Clean up formatting --- src/blueapi/client/client.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/blueapi/client/client.py b/src/blueapi/client/client.py index eb1f0bfefa..6564f20989 100644 --- a/src/blueapi/client/client.py +++ b/src/blueapi/client/client.py @@ -50,22 +50,18 @@ def _pretty_type(schema: dict[str, Any]) -> str: - # refs first if "$ref" in schema: return schema["$ref"].split("/")[-1] - # arrays preserve inner type if schema.get("type") == "array": item_schema = schema.get("items", {}) inner = _pretty_type(item_schema) return f"list[{inner}]" - # unions 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", @@ -73,7 +69,6 @@ def _pretty_type(schema: dict[str, Any]) -> str: "number": "float", "object": "dict", } - if isinstance(json_type, str): return type_map.get(json_type, json_type.split(".")[-1]) From ec97173d2ba9ac9e7d787b1d17776c4cffae42ab Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Wed, 1 Apr 2026 15:50:57 +0100 Subject: [PATCH 5/7] Add single and multi line support --- src/blueapi/client/client.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/blueapi/client/client.py b/src/blueapi/client/client.py index 6564f20989..7ef94ba764 100644 --- a/src/blueapi/client/client.py +++ b/src/blueapi/client/client.py @@ -222,13 +222,21 @@ def __repr__(self): args = [] for name, info in props.items(): typ = _pretty_type(info) - arg = f"{tab}{name}: {typ}" + arg = f"{name}: {typ}" if name not in self.required: arg = f"{arg} | None = None" args.append(arg) - joined = ",\n".join(args) - return f"{self.name}(\n{joined}\n)" + single_line = f"{self.name}({', '.join(args)})" + max_length = 100 + max_args_inline = 4 + if len(single_line) <= max_length and len(args) <= max_args_inline: + return single_line + + # Fall back to multiline if too many arguments or too long. + multiline_args = ",\n".join(f"{tab}{arg}" for arg in args) + + return f"{self.name}(\n{multiline_args}\n)" class BlueapiClient: From a1ba5d44487074bfee158f63103e547465752f6e Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Wed, 1 Apr 2026 16:04:57 +0100 Subject: [PATCH 6/7] Fix tests + add additional test --- src/blueapi/client/client.py | 2 +- tests/unit_tests/client/test_client.py | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/blueapi/client/client.py b/src/blueapi/client/client.py index 7ef94ba764..d62b97e1ae 100644 --- a/src/blueapi/client/client.py +++ b/src/blueapi/client/client.py @@ -229,7 +229,7 @@ def __repr__(self): single_line = f"{self.name}({', '.join(args)})" max_length = 100 - max_args_inline = 4 + max_args_inline = 3 if len(single_line) <= max_length and len(args) <= max_args_inline: return single_line diff --git a/tests/unit_tests/client/test_client.py b/tests/unit_tests/client/test_client.py index a96f428e8c..cd28c74203 100644 --- a/tests/unit_tests/client/test_client.py +++ b/tests/unit_tests/client/test_client.py @@ -716,7 +716,29 @@ 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": {}, "three": {}, "four": {}}, + "required": ["one"], + }, + ), + client, + ) + assert ( + plan.help_text == "Plan foo(\n" + " one: Any,\n" + " two: Any | None = None,\n" + " three: Any | None = None,\n" + " four: Any | None = None\n" + ")" + ) def test_plan_properties(client): From a8aa80533eeac7ce22ee5d22261b1eca8b6bebcf Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Wed, 1 Apr 2026 16:11:34 +0100 Subject: [PATCH 7/7] Add more tests --- tests/unit_tests/client/test_client.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/client/test_client.py b/tests/unit_tests/client/test_client.py index cd28c74203..f79b8aaefa 100644 --- a/tests/unit_tests/client/test_client.py +++ b/tests/unit_tests/client/test_client.py @@ -725,8 +725,15 @@ def test_plan_multi_parameter_fallback_help_text(client): PlanModel( name="foo", schema={ - "properties": {"one": {}, "two": {}, "three": {}, "four": {}}, - "required": ["one"], + "properties": { + "one": {}, + "two": { + "anyOf": [{"items": {}, "type": "array"}, {"type": "boolean"}], + }, + "three": {}, + "four": {}, + }, + "required": ["one", "two"], }, ), client, @@ -734,7 +741,7 @@ def test_plan_multi_parameter_fallback_help_text(client): assert ( plan.help_text == "Plan foo(\n" " one: Any,\n" - " two: Any | None = None,\n" + " two: list[Any] | bool,\n" " three: Any | None = None,\n" " four: Any | None = None\n" ")"