Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to new attributes API and use pyright #51

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@

@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
GDYendell marked this conversation as resolved.
Show resolved Hide resolved
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 @@
"""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"),
}:
GDYendell marked this conversation as resolved.
Show resolved Hide resolved
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
Loading