Skip to content

Commit 44070ba

Browse files
authored
Merge pull request #18 from python-accelerator-middle-layer/17-add-a-real-test-suite-for-pyaml-cs-oa
Test suite and ruff
2 parents d163ad1 + 9af82de commit 44070ba

32 files changed

Lines changed: 1057 additions & 210 deletions

.github/workflows/tests.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: run tests
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
test:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python 3.12
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.12"
23+
cache: pip
24+
cache-dependency-path: "**/pyproject.toml"
25+
26+
- name: Install dependencies
27+
run: |
28+
python -m pip install --upgrade pip
29+
python -m pip install "git+https://github.com/python-accelerator-middle-layer/pyaml.git"
30+
python -m pip install -e ".[test,epics,tango]"
31+
python -m pip install ruff
32+
33+
- name: Lint with Ruff
34+
run: ruff check --diff .
35+
36+
- name: Test with pytest
37+
run: python -m pytest -q

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
__pycache__/
22
*.egg-info/
3+
.idea
4+
build

pyaml_cs_oa/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import asyncio
2-
import contextlib
3-
from typing import Awaitable, Any
42
import atexit
3+
import contextlib
4+
from typing import Any, Awaitable
55

66
__version__ = "0.1.2"
77

88
# One persistent event loop
99
_loop = None
1010
_nest_asyncio_applied = False
1111

12+
1213
def loop() -> asyncio.AbstractEventLoop:
1314

1415
global _loop, _nest_asyncio_applied
@@ -20,6 +21,7 @@ def loop() -> asyncio.AbstractEventLoop:
2021
if not _nest_asyncio_applied:
2122
try:
2223
import nest_asyncio
24+
2325
nest_asyncio.apply(running_loop)
2426
_nest_asyncio_applied = True
2527
except ImportError:
@@ -37,14 +39,18 @@ def loop() -> asyncio.AbstractEventLoop:
3739
if not _nest_asyncio_applied:
3840
try:
3941
import nest_asyncio
42+
4043
nest_asyncio.apply(_loop)
4144
_nest_asyncio_applied = True
4245
except ImportError:
4346
pass
4447

4548
return _loop
49+
50+
4651
loop() # Make sure to initialize `_loop`
4752

53+
4854
def _reap_done_tasks(evloop: asyncio.AbstractEventLoop) -> None:
4955
"""Reap exceptions from tasks that are already DONE on this loop.
5056
Does not cancel or otherwise touch pending tasks.
@@ -76,4 +82,3 @@ def arun(coro: Awaitable[Any]) -> Any:
7682
# Clean up completed/cancelled tasks so residual CancelledError
7783
# doesn't leak to next run
7884
_reap_done_tasks(evloop)
79-

pyaml_cs_oa/container.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import inspect
33
from collections.abc import Awaitable, Callable
44
from typing import TypeVar
5-
from .signal import OASignal
65

76
from ophyd_async.core import (
87
SignalDatatypeT,
@@ -11,9 +10,11 @@
1110
set_and_wait_for_other_value,
1211
)
1312

13+
from . import arun
14+
from .signal import OASignal
15+
1416
T = TypeVar("T")
1517

16-
from . import arun
1718

1819
def _looks_disconnected(exc: BaseException) -> bool:
1920
# Keep it generic: ophyd-async wraps cancellations in TimeoutError;
@@ -46,7 +47,7 @@ async def _recover_once(
4647
raise
4748

4849

49-
class OAReadback():
50+
class OAReadback:
5051
"""A readback object."""
5152

5253
def __init__(self, r_signal: SignalR[SignalDatatypeT]):
@@ -80,15 +81,16 @@ def get(self) -> SignalDatatypeT:
8081
"""Synchronous wrapper around `async_get()`."""
8182
return arun(self.async_get())
8283

83-
class OASetpoint():
84+
85+
class OASetpoint:
8486
def __init__(
8587
self,
8688
w_signal: SignalW[SignalDatatypeT],
8789
r_signal: SignalR[SignalDatatypeT] | None = None,
8890
):
8991
self._w_sig = w_signal
90-
self._r_sig = r_signal # used only for `set_and_wait()`
91-
self._has_r_sig = (r_signal is not None)
92+
self._r_sig = r_signal # used only for `set_and_wait()`
93+
self._has_r_sig = r_signal is not None
9294

9395
async def _run_get(self) -> SignalDatatypeT:
9496
await self._w_sig.connect()
@@ -139,9 +141,7 @@ async def _rebuild_both(self) -> None:
139141

140142
async def _run_set_and_wait(self, value) -> None:
141143
if not self._has_r_sig:
142-
raise RuntimeError(
143-
"Cannot use set_and_wait() without a matching readback signal."
144-
)
144+
raise RuntimeError("Cannot use set_and_wait() without a matching readback signal.")
145145
await self._reconnect_both()
146146
await set_and_wait_for_other_value(self._w_sig, value, self._r_sig, value)
147147

@@ -153,9 +153,9 @@ async def async_set_and_wait(self, value) -> None:
153153
)
154154

155155
async def _complete_set(self, value):
156-
status = await self.async_set(value)
157-
await status # Wait for completion before returning
158-
return status
156+
status = await self.async_set(value)
157+
await status # Wait for completion before returning
158+
return status
159159

160160
def set(self, value):
161161
"""Synchronous wrapper around `async_set()`."""
@@ -167,4 +167,4 @@ def get(self) -> SignalDatatypeT:
167167

168168
def set_and_wait(self, value) -> None:
169169
"""Synchronous wrapper around `async_set_and_wait()`."""
170-
return arun(self.async_set_and_wait(value))
170+
return arun(self.async_set_and_wait(value))

pyaml_cs_oa/controlsystem.py

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,32 @@
1-
import os
2-
import logging
31
import copy
2+
import logging
3+
import os
44

5+
from pyaml.common.exception import PyAMLException
56
from pyaml.control.controlsystem import ControlSystem
67
from pydantic import BaseModel
7-
from pyaml.common.exception import PyAMLException
8-
9-
PYAMLCLASS : str = "OphydAsyncControlSystem"
10-
11-
logger = logging.getLogger(__name__)
128

9+
from . import __version__
10+
from .epicsR import EpicsR
11+
from .epicsRW import EpicsRW
12+
from .epicsW import EpicsW
13+
from .signal import OASignal
14+
from .tangoR import TangoR
15+
from .tangoRW import TangoRW
1316
from .types import (
1417
EpicsConfigR,
15-
EpicsConfigW,
1618
EpicsConfigRW,
19+
EpicsConfigW,
1720
TangoConfigR,
1821
TangoConfigRW,
1922
)
20-
from .signal import OASignal
21-
from .epicsR import EpicsR
22-
from .epicsW import EpicsW
23-
from .epicsRW import EpicsRW
24-
from .tangoR import TangoR
25-
from .tangoRW import TangoRW
2623

27-
from . import __version__
24+
PYAMLCLASS: str = "OphydAsyncControlSystem"
25+
26+
logger = logging.getLogger(__name__)
2827

29-
class ConfigModel(BaseModel):
3028

29+
class ConfigModel(BaseModel):
3130
"""
3231
Configuration model for an OA Control System.
3332
@@ -36,21 +35,21 @@ class ConfigModel(BaseModel):
3635
name : str
3736
Name of the control system.
3837
prefix : str
39-
Prefix added to the PV or attribute name. It can be a
38+
Prefix added to the PV or attribute name. It can be a
4039
for instance, TANGO_HOST, or a PV prefix.
4140
debug_level : int
4241
Debug verbosity level.
4342
scalar_aggregator : str
44-
Aggregator module for scalar values. If none specified, writings and
45-
readings of sclar value are serialized.
43+
Aggregator module for scalar values. If none specified, writings and
44+
readings of sclar value are serialized.
4645
vector_aggregator : str
4746
Aggregator module for vecrors. If none specified, writings and readings
4847
of vector are serialized,
4948
"""
5049

5150
name: str
5251
prefix: str = ""
53-
debug_level: str=None
52+
debug_level: str = None
5453
scalar_aggregator: str | None = "pyaml_cs_oa.scalar_aggregator"
5554
vector_aggregator: str | None = None
5655

@@ -61,57 +60,62 @@ class OphydAsyncControlSystem(ControlSystem):
6160
def __init__(self, cfg: ConfigModel):
6261
super().__init__()
6362
self._cfg = cfg
64-
self._devices = {} # Dict containing all attached DeviceAccess
63+
self._devices = {} # Dict containing all attached DeviceAccess
6564

6665
if self._cfg.debug_level:
67-
log_level = getattr(logging, self._cfg.debug_level, logging.WARNING)
68-
logger.parent.setLevel(log_level)
69-
logger.setLevel(log_level)
66+
log_level = getattr(logging, self._cfg.debug_level, logging.WARNING)
67+
logger.parent.setLevel(log_level)
68+
logger.setLevel(log_level)
69+
70+
logger.log(
71+
logging.WARNING,
72+
f"PyAML OA control system binding ({__version__}) initialized with name '{self._cfg.name}'"
73+
f" and prefix='{self._cfg.prefix}'",
74+
)
7075

71-
logger.log(logging.WARNING, f"PyAML OA control system binding ({__version__}) initialized with name '{self._cfg.name}'"
72-
f" and prefix='{self._cfg.prefix}'")
73-
7476
def attach(self, devs: list[OASignal]) -> list[OASignal]:
75-
return self._attach(devs,False)
77+
return self._attach(devs, False)
7678

7779
def attach_array(self, devs: list[OASignal]) -> list[OASignal]:
78-
return self._attach(devs,True)
80+
return self._attach(devs, True)
7981

80-
def _attach(self, devs: list[OASignal],is_array:bool) -> list[OASignal]:
82+
def _attach(self, devs: list[OASignal], is_array: bool) -> list[OASignal]:
8183
# Concatenate the prefix
8284
newDevs = []
83-
for d in devs:
85+
for d in devs:
8486
if d is not None:
85-
8687
sig_cfg = d._cfg
8788
sig_cfg_cls = sig_cfg.__class__
88-
89-
if isinstance(d._cfg,EpicsConfigR):
89+
90+
if isinstance(d._cfg, EpicsConfigR):
9091
key = self._cfg.prefix + d._cfg.read_pvname
9192
sig_cls = EpicsR
9293
config = dict(read_pvname=key)
93-
elif isinstance(d._cfg,EpicsConfigW):
94+
elif isinstance(d._cfg, EpicsConfigW):
9495
key = self._cfg.prefix + d._cfg.write_pvname
9596
sig_cls = EpicsW
9697
config = dict(write_pvname=key)
97-
elif isinstance(d._cfg,EpicsConfigRW):
98+
elif isinstance(d._cfg, EpicsConfigRW):
9899
key = self._cfg.prefix + d._cfg.read_pvname + d._cfg.write_pvname
99100
sig_cls = EpicsRW
100-
config = dict(read_pvname=self._cfg.prefix + d._cfg.read_pvname, write_pvname=self._cfg.prefix + d._cfg.write_pvname)
101-
elif isinstance(d._cfg,TangoConfigR):
101+
config = dict(
102+
read_pvname=self._cfg.prefix + d._cfg.read_pvname,
103+
write_pvname=self._cfg.prefix + d._cfg.write_pvname,
104+
)
105+
elif isinstance(d._cfg, TangoConfigR):
102106
key = self._cfg.prefix + d._cfg.attribute
103107
sig_cls = TangoR
104108
config = dict(attribute=key)
105-
elif isinstance(d._cfg,TangoConfigRW):
109+
elif isinstance(d._cfg, TangoConfigRW):
106110
key = self._cfg.prefix + d._cfg.attribute
107111
sig_cls = TangoRW
108112
config = dict(attribute=key)
109113
else:
110114
raise PyAMLException(f"OphydAsyncControlSystem: Unsupported type {type(sig_cfg)}")
111115

112116
if key not in self._devices:
113-
n_conf = dict(d._cfg) | config
114-
nr = sig_cls(sig_cfg_cls(**n_conf),is_array)
117+
n_conf = dict(d._cfg) | config
118+
nr = sig_cls(sig_cfg_cls(**n_conf), is_array)
115119
nr.build()
116120
self._devices[key] = nr
117121

@@ -130,7 +134,7 @@ def name(self) -> str:
130134
Name of the control system.
131135
"""
132136
return self._cfg.name
133-
137+
134138
def scalar_aggregator(self) -> str | None:
135139
"""
136140
Returns the module name used for handling aggregator of DeviceAccess
@@ -145,7 +149,7 @@ def scalar_aggregator(self) -> str | None:
145149
def vector_aggregator(self) -> str | None:
146150
"""
147151
Returns the module name used for handling aggregator of DeviceVectorAccess
148-
152+
149153
Returns
150154
-------
151155
str
@@ -154,4 +158,4 @@ def vector_aggregator(self) -> str | None:
154158
return self._cfg.vector_aggregator
155159

156160
def __repr__(self):
157-
return repr(self._cfg).replace("ConfigModel",self.__class__.__name__)
161+
return repr(self._cfg).replace("ConfigModel", self.__class__.__name__)

0 commit comments

Comments
 (0)