From 2aee79a739a38f7aaa016ca586ff409dced7aaf4 Mon Sep 17 00:00:00 2001 From: James Souter Date: Tue, 3 Dec 2024 08:44:27 +0000 Subject: [PATCH] Update for new FastCS attributes API Use pyright instead of mypy --- .copier-answers.yml | 2 +- pyproject.toml | 13 +++-- src/fastcs_odin/__main__.py | 32 +++++----- src/fastcs_odin/odin_adapter_controller.py | 19 +++--- src/fastcs_odin/odin_data.py | 68 ++++++++++------------ src/fastcs_odin/util.py | 18 +++++- tests/test_controllers.py | 20 +++---- 7 files changed, 87 insertions(+), 85 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 523031f..058748a 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -15,4 +15,4 @@ github_org: DiamondLightSource package_name: fastcs_odin pypi: true repo_name: fastcs-odin -type_checker: mypy +type_checker: pyright diff --git a/pyproject.toml b/pyproject.toml index 4d77958..e984f1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ description = "FastCS support for the Odin detector software framework" dependencies = [ "aiohttp", - "fastcs~=0.7.0", + "fastcs @ git+https://github.com/DiamondLightSource/FastCS.git@main", ] dynamic = ["version"] license.file = "LICENSE" @@ -27,11 +27,11 @@ authors = [ [project.optional-dependencies] dev = [ "copier", - "mypy", "myst-parser", "pipdeptree", "pre-commit", "pydata-sphinx-theme>=0.12", + "pyright", "pytest", "pytest-asyncio", "pytest-cov", @@ -58,8 +58,9 @@ GitHub = "https://github.com/DiamondLightSource/fastcs-odin" [tool.setuptools_scm] version_file = "src/fastcs_odin/_version.py" -[tool.mypy] -ignore_missing_imports = true # Ignore missing stubs in imported modules +[tool.pyright] +typeCheckingMode = "standard" +reportMissingImports = false # Ignore missing stubs in imported modules [tool.pytest.ini_options] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error @@ -92,12 +93,12 @@ passenv = * allowlist_externals = pytest pre-commit - mypy + pyright sphinx-build sphinx-autobuild commands = pre-commit: pre-commit run --all-files --show-diff-on-failure {posargs} - type-checking: mypy src tests {posargs} + type-checking: pyright src tests {posargs} tests: pytest --cov=fastcs_odin --cov-report term --cov-report xml:cov.xml {posargs} docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html """ diff --git a/src/fastcs_odin/__main__.py b/src/fastcs_odin/__main__.py index 3413e8f..5260730 100644 --- a/src/fastcs_odin/__main__.py +++ b/src/fastcs_odin/__main__.py @@ -2,9 +2,13 @@ from typing import Optional import typer -from fastcs.backends.asyncio_backend import AsyncioBackend -from fastcs.backends.epics.gui import EpicsGUIOptions from fastcs.connections.ip_connection import IPConnectionSettings +from fastcs.launch import FastCS +from fastcs.transport.epics.options import ( + EpicsGUIOptions, + EpicsIOCOptions, + EpicsOptions, +) from fastcs_odin.odin_controller import OdinController @@ -43,25 +47,17 @@ def main( @app.command() def ioc(pv_prefix: str = typer.Argument(), ip: str = OdinIp, port: int = OdinPort): - from fastcs.backends.epics.backend import EpicsBackend - controller = OdinController(IPConnectionSettings(ip, port)) - - backend = EpicsBackend(controller, pv_prefix) - backend.create_gui( - options=EpicsGUIOptions( + options = EpicsOptions( + ioc=EpicsIOCOptions(pv_prefix=pv_prefix), + gui=EpicsGUIOptions( output_path=Path.cwd() / "odin.bob", title=f"Odin - {pv_prefix}" - ) + ), ) - backend.run() - - -@app.command() -def asyncio(ip: str = OdinIp, port: int = OdinPort): - controller = OdinController(IPConnectionSettings(ip, port)) - - backend = AsyncioBackend(controller) - backend.run() + launcher = FastCS(controller, options) + launcher.create_docs() + launcher.create_gui() + launcher.run() # test with: python -m fastcs_odin diff --git a/src/fastcs_odin/odin_adapter_controller.py b/src/fastcs_odin/odin_adapter_controller.py index 9fb837a..82e4cd0 100644 --- a/src/fastcs_odin/odin_adapter_controller.py +++ b/src/fastcs_odin/odin_adapter_controller.py @@ -2,7 +2,7 @@ import re from collections.abc import Callable, Iterable, Sequence from dataclasses import dataclass -from typing import Any, TypeVar +from typing import Any from fastcs.attributes import AttrR, AttrRW, AttrW, Handler, Sender, Updater from fastcs.controller import BaseController, SubController @@ -23,7 +23,7 @@ class AdapterResponseError(Exception): ... @dataclass class ParamTreeHandler(Handler): path: str - update_period: float = 0.2 + update_period: float | None = 0.2 allowed_values: dict[int, str] | None = None async def put( @@ -59,9 +59,6 @@ async def update( logging.error("Update loop failed for %s:\n%s", self.path, e) -T = TypeVar("T") - - @dataclass class StatusSummaryUpdater(Updater): """Updater to accumulate underlying attributes into a high-level summary. @@ -79,13 +76,13 @@ class StatusSummaryUpdater(Updater): path_filter: list[str | tuple[str] | re.Pattern] attribute_name: str - accumulator: Callable[[Iterable[T]], T] - update_period: float = 0.2 + accumulator: Callable[[Iterable[Any]], float | int | bool | str] + update_period: float | None = 0.2 async def update(self, controller: "OdinAdapterController", attr: AttrR): values = [] for sub_controller in _filter_sub_controllers(controller, self.path_filter): - sub_attribute: AttrR = getattr(sub_controller, self.attribute_name) + sub_attribute: AttrR = sub_controller.attributes[self.attribute_name] # type: ignore values.append(sub_attribute.get()) await attr.set(self.accumulator(values)) @@ -101,7 +98,7 @@ class ConfigFanSender(Sender): attributes: list[AttrW] - async def put(self, _controller: "OdinAdapterController", attr: AttrW, value: Any): + async def put(self, controller: "OdinAdapterController", attr: AttrW, value: Any): for attribute in self.attributes: await attribute.process(value) @@ -115,6 +112,7 @@ def _filter_sub_controllers( sub_controller_map = controller.get_sub_controllers() if len(path_filter) == 1: + assert isinstance(path_filter[0], str) yield sub_controller_map[path_filter[0]] return @@ -212,5 +210,4 @@ def _create_attributes(self): ), group=group, ) - - setattr(self, parameter.name.replace(".", ""), attr) + self.attributes[parameter.name] = attr diff --git a/src/fastcs_odin/odin_data.py b/src/fastcs_odin/odin_data.py index fa1de17..69b56e3 100644 --- a/src/fastcs_odin/odin_data.py +++ b/src/fastcs_odin/odin_data.py @@ -1,14 +1,12 @@ import logging -from collections.abc import Iterable from fastcs.attributes import AttrW -from fastcs.controller import BaseController, SubController from fastcs_odin.odin_adapter_controller import ( ConfigFanSender, OdinAdapterController, ) -from fastcs_odin.util import OdinParameter, partition +from fastcs_odin.util import OdinParameter, get_all_sub_controllers, partition class OdinDataController(OdinAdapterController): @@ -65,41 +63,35 @@ def _create_config_fan_attributes(self): """Search for config attributes in sub controllers to create fan out PVs.""" parameter_attribute_map: dict[str, tuple[OdinParameter, list[AttrW]]] = {} for sub_controller in get_all_sub_controllers(self): - for parameter in sub_controller.parameters: - mode, key = parameter.uri[0], parameter.uri[-1] - if mode == "config" and key not in self._unique_config: - try: - attr = getattr(sub_controller, parameter.name) - except AttributeError: - logging.warning( - f"Controller has parameter {parameter}, " - f"but no corresponding attribute {parameter.name}" - ) - - if parameter.name not in parameter_attribute_map: - parameter_attribute_map[parameter.name] = (parameter, [attr]) - else: - parameter_attribute_map[parameter.name][1].append(attr) + match sub_controller: + case OdinAdapterController(): + for parameter in sub_controller.parameters: + mode, key = parameter.uri[0], parameter.uri[-1] + if mode == "config" and key not in self._unique_config: + try: + attr: AttrW = sub_controller.attributes[parameter.name] # type: ignore + if parameter.name not in parameter_attribute_map: + parameter_attribute_map[parameter.name] = ( + parameter, + [attr], + ) + else: + parameter_attribute_map[parameter.name][1].append( + attr + ) + except KeyError: + logging.warning( + f"Controller has parameter {parameter}, " + f"but no corresponding attribute {parameter.name}" + ) + case _: + logging.warning( + f"Subcontroller {sub_controller} not an OdinAdapterController" + ) for parameter, sub_attributes in parameter_attribute_map.values(): - setattr( - self, - parameter.name, - sub_attributes[0].__class__( - sub_attributes[0].datatype, - group=sub_attributes[0].group, - handler=ConfigFanSender(sub_attributes), - ), + self.attributes[parameter.name] = sub_attributes[0].__class__( + sub_attributes[0].datatype, + group=sub_attributes[0].group, + handler=ConfigFanSender(sub_attributes), ) - - -def get_all_sub_controllers( - controller: "OdinAdapterController", -) -> list["OdinAdapterController"]: - return list(_walk_sub_controllers(controller)) - - -def _walk_sub_controllers(controller: BaseController) -> Iterable[SubController]: - for sub_controller in controller.get_sub_controllers().values(): - yield sub_controller - yield from _walk_sub_controllers(sub_controller) diff --git a/src/fastcs_odin/util.py b/src/fastcs_odin/util.py index 310e2f1..ef51870 100644 --- a/src/fastcs_odin/util.py +++ b/src/fastcs_odin/util.py @@ -1,8 +1,10 @@ -from collections.abc import Callable, Iterator, Mapping +from collections.abc import Callable, Iterable, Iterator, Mapping from dataclasses import dataclass, field from enum import Enum from typing import Any, TypeVar +from fastcs.controller import BaseController, SubController + def is_metadata_object(v: Any) -> bool: return isinstance(v, dict) and "writeable" in v and "type" in v @@ -146,3 +148,17 @@ def partition( falsy.append(parameter) return truthy, falsy + + +def get_all_sub_controllers( + controller: BaseController, +) -> list[SubController]: + return list(_walk_sub_controllers(controller)) + + +def _walk_sub_controllers( + controller: BaseController, +) -> Iterable[SubController]: + for sub_controller in controller.get_sub_controllers().values(): + yield sub_controller + yield from _walk_sub_controllers(sub_controller) diff --git a/tests/test_controllers.py b/tests/test_controllers.py index bc8efc7..0fd94e1 100644 --- a/tests/test_controllers.py +++ b/tests/test_controllers.py @@ -41,12 +41,12 @@ def test_create_attributes(): controller._create_attributes() - match controller: - case OdinAdapterController( - read_int=AttrR(datatype=Int()), - write_bool=AttrRW(datatype=Bool()), - group_float=AttrR(datatype=Float(), group="Group"), - ): + match controller.attributes: + case { + "read_int": AttrR(datatype=Int()), + "write_bool": AttrRW(datatype=Bool()), + "group_float": AttrR(datatype=Float(), group="Group"), + }: pass case _: pytest.fail("Controller Attributes not as expected") @@ -151,6 +151,7 @@ async def test_fp_create_plugin_sub_controllers(): }: sub_controllers = controllers["HDF"].get_sub_controllers() assert "DS" in sub_controllers + assert isinstance(sub_controllers["DS"], OdinAdapterController) assert sub_controllers["DS"].parameters == [ OdinParameter( uri=["status", "hdf", "dataset", "compressed_size", "compression"], @@ -233,12 +234,11 @@ async def test_status_summary_updater(mocker: MockerFixture): } fpx_controller.get_sub_controllers.return_value = {"HDF": hdf_controller} - hdf_controller.frames_written.get.return_value = 50 + hdf_controller.attributes["frames_written"].get.return_value = 50 handler = StatusSummaryUpdater( ["OD", ("FP",), re.compile("FP*"), "HDF"], "frames_written", sum ) - hdf_controller.frames_written.get.return_value = 50 await handler.update(controller, attr) attr.set.assert_called_once_with(100) @@ -246,11 +246,11 @@ async def test_status_summary_updater(mocker: MockerFixture): ["OD", ("FP",), re.compile("FP*"), "HDF"], "writing", any ) - hdf_controller.writing.get.side_effect = [True, False] + hdf_controller.attributes["writing"].get.side_effect = [True, False] await handler.update(controller, attr) attr.set.assert_called_with(True) - hdf_controller.writing.get.side_effect = [False, False] + hdf_controller.attributes["writing"].get.side_effect = [False, False] await handler.update(controller, attr) attr.set.assert_called_with(False)