Skip to content
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
4 changes: 2 additions & 2 deletions .github/workflows/deploy-pypi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
deploy:

runs-on: ubuntu-latest

#environment: release
Expand All @@ -27,7 +27,7 @@ jobs:
cache-dependency-path: '**/pyproject.toml'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade pip
pip install hatch
- name: Build package
run: hatch build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
pip install accelerator-toolbox
pip install matplotlib
pip install h5py
pip install accelerator-middle-layer
pip install "git+https://github.com/python-accelerator-middle-layer/pyaml.git"
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ coverage.xml
.pytest_cache/
cover/

# DS
# DS
**/.DS_Store

# Translations
Expand Down
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,12 @@ Python code:

```python
from tango.pyaml.attribute import Attribute
from tango.pyaml.tango_attribute import ConfigModel
import yaml

with open("attribute.yaml") as f:
cfg_dict = yaml.safe_load(f)

cfg = ConfigModel(**cfg_dict)
attr = Attribute(cfg)
attr = Attribute(**cfg_dict)

attr.set(10.0)
value = attr.get()
Expand Down Expand Up @@ -99,4 +97,3 @@ This project is licensed under the MIT License.
## Links

- 🧺 [Repository](https://github.com/python-accelerator-middle-layer/tango-pyaml)

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ dev = [
"pre-commit",
]

[project.entry-points."pyaml.schemas"]
tango_pyaml = "tango.pyaml"

[project.urls]
Homepage = "https://github.com/python-accelerator-middle-layer/tango-pyaml"
Documentation = "https://python-accelerator-middle-layer.github.io/tango-pyaml/"
Expand Down
131 changes: 77 additions & 54 deletions tango/pyaml/attribute.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import copy
import logging
from typing import Optional, Tuple

from pydantic import BaseModel

import pyaml
import tango
from pyaml.common.element import __pyaml_repr__
from pyaml.control.deviceaccess import DeviceAccess
from pyaml.control.readback_value import Value, Quality
from pyaml.control.readback_value import Quality, Value
from pyaml.validation import DynamicValidation, register_schema

from .initializable_element import InitializableElement
from .device_factory import DeviceFactory
from .tango_pyaml_utils import *
from .initializable_element import InitializableElement
from .tango_pyaml_utils import tango_to_PyAMLException, to_float_or_none

PYAMLCLASS: str = "Attribute"

logger = logging.getLogger(__name__)


class ConfigModel(BaseModel):
class AttributeConfig(BaseModel):
"""
Configuration model for Tango attributes.

Expand All @@ -36,29 +39,51 @@ class ConfigModel(BaseModel):

attribute: str
unit: str = ""
range: Optional[Tuple[Optional[float], Optional[float]]] = None
index: Optional[int] = None
range: tuple[float | None, float | None] | None = None
index: int | None = None


class Attribute(DeviceAccess, InitializableElement):
@register_schema
class Attribute(DeviceAccess, InitializableElement, DynamicValidation):
"""
Tango attribute that can be written to.

Parameters
----------
cfg : ConfigModel
Configuration object containing attribute path and units.
attribute : str
Full path of the Tango attribute (e.g., 'my/ps/device/current').
unit : str, optional
The unit of the attribute.
range : tuple(min, max), optional
Range of valid values. Use null for -∞ or +∞.
index : int, optional
Zero-based index into a SPECTRUM attribute. When set, the instance
behaves as a read-only scalar view of one vector element; writes are
always rejected and a SPECTRUM data_format is enforced on init.
writable : bool, optional
If the attribute should be writable. Default is True.

Raises
------
pyaml.PyAMLException
If the Tango attribute is not writable.
"""

def __init__(self, cfg: ConfigModel, writable=True):
def __init__(
self,
attribute: str,
unit: str = "",
range: tuple[float | None, float | None] | None = None,
index: int | None = None,
writable=True,
):
super().__init__()
self._cfg = cfg
self._index = cfg.index

self._attribute = attribute
self._unit = unit
self._range = range
self._index = index

# Indexed access never writes individual array elements.
self._writable = writable and self._index is None
self._attribute_dev: tango.DeviceProxy = None
Expand All @@ -69,9 +94,7 @@ def __init__(self, cfg: ConfigModel, writable=True):
def initialize(self):
super().initialize()
try:
self._attribute_dev_name, self._attr_name = self._cfg.attribute.rsplit(
"/", 1
)
self._attribute_dev_name, self._attr_name = self._attribute.rsplit("/", 1)
self._attribute_dev = DeviceFactory().get_device(self._attribute_dev_name)
except tango.DevFailed as df:
raise tango_to_PyAMLException(df)
Expand All @@ -80,22 +103,23 @@ def initialize(self):
self._attribute_dev.get_attribute_config(self._attr_name, wait=True)
)

if self._index is not None:
if self._attr_config.data_format != tango.AttrDataFormat.SPECTRUM:
raise pyaml.PyAMLException(
f"Tango attribute '{self._cfg.attribute}' is not a SPECTRUM; "
"indexed access requires a vector attribute."
)

if self._writable:
if self._attr_config.writable not in [
tango.AttrWriteType.READ_WRITE,
tango.AttrWriteType.WRITE,
tango.AttrWriteType.READ_WITH_WRITE,
]:
raise pyaml.PyAMLException(
f"Tango attribute {self._cfg.attribute} is not writable."
)
if (
self._index is not None
and self._attr_config.data_format != tango.AttrDataFormat.SPECTRUM
):
raise pyaml.PyAMLException(
f"Tango attribute '{self._attribute}' is not a SPECTRUM; "
"indexed access requires a vector attribute."
)

if self._writable and self._attr_config.writable not in [
tango.AttrWriteType.READ_WRITE,
tango.AttrWriteType.WRITE,
tango.AttrWriteType.READ_WITH_WRITE,
]:
raise pyaml.PyAMLException(
f"Tango attribute {self._attribute} is not writable."
)

def is_writable(self):
return self._writable
Expand All @@ -116,12 +140,12 @@ def set(self, value: float):
"""
if self._index is not None:
raise pyaml.PyAMLException(
f"Indexed attribute '{self._cfg.attribute}[{self._index}]' "
f"Indexed attribute '{self._attribute}[{self._index}]' "
"does not support individual element writes."
)
self._ensure_initialized()
logger.log(
logging.DEBUG, f"Setting asynchronously {self._cfg.attribute} to {value}"
logging.DEBUG, f"Setting asynchronously {self._attribute} to {value}"
)
try:
self._attribute_dev.write_attribute_asynch(self._attr_name, value)
Expand All @@ -144,11 +168,11 @@ def set_and_wait(self, value: float):
"""
if self._index is not None:
raise pyaml.PyAMLException(
f"Indexed attribute '{self._cfg.attribute}[{self._index}]' "
f"Indexed attribute '{self._attribute}[{self._index}]' "
"does not support individual element writes."
)
self._ensure_initialized()
logger.log(logging.DEBUG, f"Setting {self._cfg.attribute} to {value}")
logger.log(logging.DEBUG, f"Setting {self._attribute} to {value}")
try:
self._attribute_dev.write_attribute(self._attr_name, value)
except tango.DevFailed as df:
Expand All @@ -169,13 +193,17 @@ def readback(self) -> Value:
If the Tango read fails.
"""
self._ensure_initialized()
logger.log(logging.DEBUG, f"Reading {self._cfg.attribute}")
logger.log(logging.DEBUG, f"Reading {self._attribute}")
try:
attr_value = self._attribute_dev.read_attribute(self._attr_name)
quality = Quality[
attr_value.quality.name.rsplit("_", 1)[1]
] # AttrQuality.ATTR_VALID gives Quality.VALID
raw = attr_value.value[self._index] if self._index is not None else attr_value.value
raw = (
attr_value.value[self._index]
if self._index is not None
else attr_value.value
)
value = Value(raw, quality, attr_value.time.todatetime())
except tango.DevFailed as df:
raise tango_to_PyAMLException(df)
Expand All @@ -190,7 +218,7 @@ def unit(self) -> str:
str
The unit string.
"""
return self._cfg.unit
return self._unit

def name(self) -> str:
"""
Expand All @@ -203,8 +231,8 @@ def name(self) -> str:
notation when indexed (e.g., 'my/ps/device/current[2]').
"""
if self._index is not None:
return f"{self._cfg.attribute}[{self._index}]"
return self._cfg.attribute
return f"{self._attribute}[{self._index}]"
return self._attribute

def get_tango_attribute(self) -> str:
"""
Expand All @@ -215,7 +243,7 @@ def get_tango_attribute(self) -> str:
str
Tango attribute path stored in the configuration.
"""
return self._cfg.attribute
return self._attribute

def clone_with_tango_attribute(self, attribute: str) -> "Attribute":
"""
Expand All @@ -227,8 +255,7 @@ def clone_with_tango_attribute(self, attribute: str) -> "Attribute":
Tango attribute path to store in the cloned instance.
"""
new_obj = copy.copy(self)
new_obj._cfg = copy.copy(self._cfg)
new_obj._cfg.attribute = attribute
new_obj._attribute = attribute
return new_obj

def measure_name(self) -> str:
Expand All @@ -241,7 +268,7 @@ def measure_name(self) -> str:
The attribute name (e.g., 'current'), with index notation when
indexed (e.g., 'current[2]').
"""
short = self._cfg.attribute.rsplit("/", 1)[1]
short = self._attribute.rsplit("/", 1)[1]
if self._index is not None:
return f"{short}[{self._index}]"
return short
Expand Down Expand Up @@ -274,13 +301,9 @@ def get(self) -> float:

def get_range(self) -> list[float]:
attr_range: list[float] = [None, None]
if self._cfg.range is not None:
attr_range[0] = (
self._cfg.range[0] if self._cfg.range[0] is not None else None
)
attr_range[1] = (
self._cfg.range[1] if self._cfg.range[1] is not None else None
)
if self._range is not None:
attr_range[0] = self._range[0] if self._range[0] is not None else None
attr_range[1] = self._range[1] if self._range[1] is not None else None
else:
self._ensure_initialized()
min_value = self._attr_config.min_value
Expand All @@ -295,9 +318,9 @@ def check_device_availability(self) -> bool:
try:
self._ensure_initialized()
self._attribute_dev.ping()
except tango.DevFailed | pyaml.PyAMLException:
except (tango.DevFailed, pyaml.PyAMLException):
available = False
return available

def __repr__(self):
return repr(self._cfg).replace("ConfigModel", self.__class__.__name__)
return __pyaml_repr__(self)
Loading
Loading