Skip to content

Commit

Permalink
Update for new FastCS attributes API
Browse files Browse the repository at this point in the history
Use pyright instead of mypy
  • Loading branch information
jsouter committed Dec 9, 2024
1 parent 833015c commit 2aee79a
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 85 deletions.
2 changes: 1 addition & 1 deletion .copier-answers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ github_org: DiamondLightSource
package_name: fastcs_odin
pypi: true
repo_name: fastcs-odin
type_checker: mypy
type_checker: pyright
13 changes: 7 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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
"""
Expand Down
32 changes: 14 additions & 18 deletions src/fastcs_odin/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(

Check warning on line 51 in src/fastcs_odin/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs_odin/__main__.py#L51

Added line #L51 was not covered by tests
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()

Check warning on line 60 in src/fastcs_odin/__main__.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs_odin/__main__.py#L57-L60

Added lines #L57 - L60 were not covered by tests


# test with: python -m fastcs_odin
Expand Down
19 changes: 8 additions & 11 deletions src/fastcs_odin/odin_adapter_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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))
Expand All @@ -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)

Expand All @@ -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

Expand Down Expand Up @@ -212,5 +210,4 @@ def _create_attributes(self):
),
group=group,
)

setattr(self, parameter.name.replace(".", ""), attr)
self.attributes[parameter.name] = attr
68 changes: 30 additions & 38 deletions src/fastcs_odin/odin_data.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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(

Check warning on line 83 in src/fastcs_odin/odin_data.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs_odin/odin_data.py#L82-L83

Added lines #L82 - L83 were not covered by tests
f"Controller has parameter {parameter}, "
f"but no corresponding attribute {parameter.name}"
)
case _:
logging.warning(

Check warning on line 88 in src/fastcs_odin/odin_data.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs_odin/odin_data.py#L87-L88

Added lines #L87 - L88 were not covered by tests
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)
18 changes: 17 additions & 1 deletion src/fastcs_odin/util.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
20 changes: 10 additions & 10 deletions tests/test_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -233,24 +234,23 @@ 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)

handler = StatusSummaryUpdater(
["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)

Expand Down

0 comments on commit 2aee79a

Please sign in to comment.