Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4548aa0
Adapt OA control system to new pyaml API
gupichon-soleil Apr 22, 2026
1d4956f
.gitignore update
gupichon-soleil Apr 22, 2026
e8e6d44
Refactor catalog and signal index support: merge OAReadbackIndexed/OA…
gupichon-soleil Apr 24, 2026
052ab00
Correction of errors in previous commit.
gupichon-soleil Apr 24, 2026
c380e1f
Restored container, added a trace for debugging
JeanLucPons Apr 29, 2026
d76ff9d
Adde index in key
JeanLucPons Apr 29, 2026
d6e4500
Handling index (no factoring)
JeanLucPons Apr 29, 2026
fd6834c
Removed confusing index managenet
JeanLucPons Apr 29, 2026
6c0716f
Removed confusing index managenet
JeanLucPons Apr 29, 2026
eb7a8b0
Validate indexed readback values before element access
gupichon-soleil Apr 29, 2026
faa5ff9
Removed no needed file
JeanLucPons Apr 29, 2026
6800220
Fix comment
JeanLucPons Apr 29, 2026
dd48aef
Simplify indexed readback validation
gupichon-soleil Apr 29, 2026
7932dac
Retored original OASeptoint contructor
JeanLucPons Apr 29, 2026
6457626
Avoid duplication of Ophyd signal
JeanLucPons Apr 29, 2026
e14b4f3
Removed trace
JeanLucPons Apr 29, 2026
49f8e3d
restored container.py
JeanLucPons Apr 29, 2026
482d2fc
Moved index management to DeviceAccess sub class
JeanLucPons Apr 29, 2026
ab26f8c
Update for aggregator
JeanLucPons Apr 29, 2026
614b9c3
Fix
JeanLucPons Apr 29, 2026
1271e13
Merge branch 'main' into 13-adding-a-field-for-catalog-name-for-the-c…
JeanLucPons Apr 30, 2026
ddc61f4
Fix
JeanLucPons Apr 30, 2026
fc90f51
Adapt BPM tests to PyAML catalog-based device resolution
gupichon-soleil Apr 30, 2026
87497dc
Fix EPICS catalog PV parsing and cover scalar/indexed keys
gupichon-soleil Apr 30, 2026
3f17418
Fix EPICS catalog parsing for scalar and whitespace-indexed keys
gupichon-soleil Apr 30, 2026
9996302
Adapt OA catalogs to PyAML backend-owned device resolution.
gupichon-soleil May 4, 2026
799e412
Updated tests, moved bach catalog from pyaml core
JeanLucPons Jun 2, 2026
bfb4c73
Remove unrlevant tests, New management for Tango attribute and dynami…
JeanLucPons Jun 3, 2026
767ef1e
Added missing files
JeanLucPons Jun 3, 2026
eb18bc2
Fix
JeanLucPons Jun 3, 2026
e775bdb
Ruff formating
JeanLucPons Jun 3, 2026
1b7ccad
Removed trace
JeanLucPons Jun 3, 2026
450484e
Removed unecessary type check
JeanLucPons Jun 3, 2026
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
24 changes: 24 additions & 0 deletions .github/workflows/ruff_formatting.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Ruff Format Check

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
ruff-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11" # or your preferred version

- name: Install Ruff
run: pip install ruff

- name: Run Ruff Check
run: ruff check --diff .
10 changes: 7 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ jobs:
python -m pip install --upgrade pip
python -m pip install "git+https://github.com/python-accelerator-middle-layer/pyaml.git"
python -m pip install -e ".[test,epics,tango]"
python -m pip install ruff
pip install flake8

- name: Lint with Ruff
run: ruff check --diff .
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Test with pytest
run: python -m pytest -q
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ repos:
args: [--fix]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
hooks:
- id: mypy
additional_dependencies: [
"pydantic>=2.0",
]
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.18.2
# hooks:
# - id: mypy
# additional_dependencies: [
# "pydantic>=2.0",
# ]
24 changes: 24 additions & 0 deletions pyaml_cs_oa/catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Configuration helpers for backend-provided catalogs."""

from abc import ABCMeta, abstractmethod

from pydantic import BaseModel


class Catalog(metaclass=ABCMeta):
r"""
Abstract class for backend catalog configuration objects.

Notes
-----
Concrete catalogs live in each control-system package. They may expose
backend-specific resolution APIs, but those APIs are not called by the
PyAML core.
"""

@abstractmethod
def resolve(self, key: str) -> BaseModel:
"""
Return a configuration model for a DeviceAccess
"""
pass
6 changes: 4 additions & 2 deletions pyaml_cs_oa/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def _looks_disconnected(exc: BaseException) -> bool:
async def _recover_once(
run: Callable[[], Awaitable[T]],
reconnect: Callable[[], Awaitable[None]],
peer: OASignal,
peer,
) -> T:
try:
return await run()
Expand Down Expand Up @@ -56,6 +56,7 @@ def __init__(self, r_signal: SignalR[SignalDatatypeT]):
async def _run_get(self) -> SignalDatatypeT:
await self._r_sig.connect()
backend = self._r_sig._connector.backend
print(f"Read {self._r_sig.name}")
return await backend.get_value()

async def async_get(self) -> SignalDatatypeT:
Expand Down Expand Up @@ -129,7 +130,8 @@ async def async_set(self, value):
)

async def _reconnect_both(self) -> None:
await asyncio.gather(self._w_sig.connect(), self._r_sig.connect())
if self._r_sig:
await asyncio.gather(self._w_sig.connect(), self._r_sig.connect())

async def _rebuild_both(self) -> None:
w_rebuild = getattr(self._w_sig, "__peer__", None)
Expand Down
110 changes: 65 additions & 45 deletions pyaml_cs_oa/controlsystem.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import copy
import logging
import os

from pyaml.common.exception import PyAMLException
from pyaml.control.controlsystem import ControlSystem
from pydantic import BaseModel
from pyaml.control.deviceaccess import DeviceAccess
from pydantic import BaseModel, ConfigDict

from . import __version__
from .catalog import Catalog
from .epicsR import EpicsR
from .epicsRW import EpicsRW
from .epicsW import EpicsW
from .signal import OASignal
from .tangoR import TangoR
from .tangoRW import TangoRW
from .types import (
EpicsConfigR,
EpicsConfigRW,
EpicsConfigW,
TangoConfigR,
TangoConfigRW,
)
from .tangoAtt import TangoAtt
from .types import ControlSysConfig, EpicsConfigR, EpicsConfigRW, EpicsConfigW, TangoConfigAtt

PYAMLCLASS: str = "OphydAsyncControlSystem"

Expand All @@ -37,7 +30,10 @@ class ConfigModel(BaseModel):
prefix : str
Prefix added to the PV or attribute name. It can be a
for instance, TANGO_HOST, or a PV prefix.
debug_level : int
catalog : Catalog | None
Catalog instance or catalog name used to resolve PyAML device keys.
If None specified a dynamic catalog is used.
debug_level : str
Debug verbosity level.
scalar_aggregator : str
Aggregator module for scalar values. If none specified, writings and
Expand All @@ -47,9 +43,12 @@ class ConfigModel(BaseModel):
of vector are serialized,
"""

model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")

name: str
prefix: str = ""
debug_level: str = None
catalog: Catalog | None = None
debug_level: str | None = None
scalar_aggregator: str | None = "pyaml_cs_oa.scalar_aggregator"
vector_aggregator: str | None = None

Expand All @@ -60,11 +59,10 @@ class OphydAsyncControlSystem(ControlSystem):
def __init__(self, cfg: ConfigModel):
super().__init__()
self._cfg = cfg
self._devices = {} # Dict containing all attached DeviceAccess
self._devices: dict[str, DeviceAccess] = {} # Dict containing all attached DeviceAccess

if self._cfg.debug_level:
log_level = getattr(logging, self._cfg.debug_level, logging.WARNING)
logger.parent.setLevel(log_level)
logger.setLevel(log_level)

logger.log(
Expand All @@ -73,48 +71,70 @@ def __init__(self, cfg: ConfigModel):
f" and prefix='{self._cfg.prefix}'",
)

def attach(self, devs: list[OASignal]) -> list[OASignal]:
return self._attach(devs, False)

def attach_array(self, devs: list[OASignal]) -> list[OASignal]:
return self._attach(devs, True)

def _attach(self, devs: list[OASignal], is_array: bool) -> list[OASignal]:
def attach(self, devs: list[OASignal | None]) -> list[OASignal | None]:
# Deprecated function
return self._attach([d._cfg if d is not None else None for d in devs], False)

def attach_array(self, devs: list[OASignal | None]) -> list[OASignal | None]:
# Deprecated function
return self._attach([d._cfg if d is not None else None for d in devs], True)

def get_device(self, ref: str | BaseModel | None) -> DeviceAccess | None:
if ref is None:
return None

if isinstance(ref, str):
# Retrieve a config from a key using using a Catalog
if self._cfg.catalog is None:
raise PyAMLException(f"Control system '{self.name()}' has no catalog when trying to resolve '{ref}'")
try:
ref = self._cfg.catalog.resolve(ref)
except AttributeError as exc:
raise PyAMLException(f"Control system '{self.name()}' catalog cannot resolve key '{ref}'") from exc

if isinstance(ref, EpicsConfigR):
return self._attach([ref], ref.index is not None)[0]
if isinstance(ref, EpicsConfigW):
return self._attach([ref], ref.index is not None)[0]
if isinstance(ref, EpicsConfigRW):
return self._attach([ref], ref.index is not None)[0]
if isinstance(ref, TangoConfigAtt):
return self._attach([ref], ref.index is not None)[0]

raise PyAMLException(f"Control system '{self.name()}' cannot build a device from {type(ref).__name__}")

def _attach(self, configs: list[ControlSysConfig | None], is_array: bool) -> list[OASignal | None]:
# Concatenate the prefix
newDevs = []
for d in devs:
if d is not None:
sig_cfg = d._cfg
for sig_cfg in configs:
if sig_cfg is not None:
sig_cfg_cls = sig_cfg.__class__
index_str = "" if sig_cfg.index is None else str(sig_cfg.index)

if isinstance(d._cfg, EpicsConfigR):
key = self._cfg.prefix + d._cfg.read_pvname
if isinstance(sig_cfg, EpicsConfigR):
key = self._cfg.prefix + sig_cfg.read_pvname + index_str
sig_cls = EpicsR
config = dict(read_pvname=key)
elif isinstance(d._cfg, EpicsConfigW):
key = self._cfg.prefix + d._cfg.write_pvname
config = dict(read_pvname=self._cfg.prefix + sig_cfg.read_pvname)
elif isinstance(sig_cfg, EpicsConfigW):
key = self._cfg.prefix + sig_cfg.write_pvname + index_str
sig_cls = EpicsW
config = dict(write_pvname=key)
elif isinstance(d._cfg, EpicsConfigRW):
key = self._cfg.prefix + d._cfg.read_pvname + d._cfg.write_pvname
config = dict(write_pvname=self._cfg.prefix + sig_cfg.write_pvname)
elif isinstance(sig_cfg, EpicsConfigRW):
key = self._cfg.prefix + sig_cfg.read_pvname + sig_cfg.write_pvname + index_str
sig_cls = EpicsRW
config = dict(
read_pvname=self._cfg.prefix + d._cfg.read_pvname,
write_pvname=self._cfg.prefix + d._cfg.write_pvname,
read_pvname=self._cfg.prefix + sig_cfg.read_pvname,
write_pvname=self._cfg.prefix + sig_cfg.write_pvname,
)
elif isinstance(d._cfg, TangoConfigR):
key = self._cfg.prefix + d._cfg.attribute
sig_cls = TangoR
config = dict(attribute=key)
elif isinstance(d._cfg, TangoConfigRW):
key = self._cfg.prefix + d._cfg.attribute
sig_cls = TangoRW
config = dict(attribute=key)
elif isinstance(sig_cfg, TangoConfigAtt):
key = self._cfg.prefix + sig_cfg.attribute + index_str
sig_cls = TangoAtt
config = dict(attribute=self._cfg.prefix + sig_cfg.attribute)
else:
raise PyAMLException(f"OphydAsyncControlSystem: Unsupported type {type(sig_cfg)}")

if key not in self._devices:
n_conf = dict(d._cfg) | config
n_conf = dict(sig_cfg) | config
nr = sig_cls(sig_cfg_cls(**n_conf), is_array)
nr.build()
self._devices[key] = nr
Expand Down
Loading
Loading