From 1be810b73d9d057f9b0013cc53611dbd98d97ec1 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 16 Oct 2024 15:21:39 +0100 Subject: [PATCH 01/10] alias the record with pascal case if the `pv_name` is not pascal case already --- src/fastcs/backends/epics/ioc.py | 6 +++++- tests/backends/epics/test_ioc.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/fastcs/backends/epics/ioc.py b/src/fastcs/backends/epics/ioc.py index 7aa0e892..c5694b79 100644 --- a/src/fastcs/backends/epics/ioc.py +++ b/src/fastcs/backends/epics/ioc.py @@ -118,7 +118,7 @@ def _create_and_link_attribute_pvs(pv_prefix: str, mapping: Mapping) -> None: for single_mapping in mapping.get_controller_mappings(): path = single_mapping.controller.path for attr_name, attribute in single_mapping.attributes.items(): - pv_name = attr_name.title().replace("_", "") + pv_name = attr_name.replace("_", "") _pv_prefix = ":".join([pv_prefix] + path) full_pv_name_length = len(f"{_pv_prefix}:{pv_name}") @@ -218,6 +218,10 @@ async def async_write_display(value: T): record = _get_output_record( f"{pv_prefix}:{pv_name}", attribute, on_update=on_update ) + pascal_case_pv_name = pv_name.title() + if pascal_case_pv_name != pv_name: + record.add_alias(f"{pv_prefix}:{pascal_case_pv_name}") + _add_attr_pvi_info(record, pv_prefix, attr_name, "w") attribute.set_write_display_callback(async_write_display) diff --git a/tests/backends/epics/test_ioc.py b/tests/backends/epics/test_ioc.py index c1a5b321..b469d64e 100644 --- a/tests/backends/epics/test_ioc.py +++ b/tests/backends/epics/test_ioc.py @@ -357,7 +357,8 @@ def test_add_attr_pvi_info(mocker: MockerFixture): ) -async def do_nothing(arg): ... +async def do_nothing(arg): + ... class NothingCommand: From 975f72aa5bac0382a88b52447d0bb3ae1ad50cf7 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 16 Oct 2024 16:02:18 +0100 Subject: [PATCH 02/10] moved options to be an attribute on the IOC Also added options for formatting the pv names. --- src/fastcs/backends/epics/backend.py | 14 ++++++--- src/fastcs/backends/epics/ioc.py | 43 ++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/fastcs/backends/epics/backend.py b/src/fastcs/backends/epics/backend.py index 20373183..5efa8b51 100644 --- a/src/fastcs/backends/epics/backend.py +++ b/src/fastcs/backends/epics/backend.py @@ -7,11 +7,17 @@ class EpicsBackend(Backend): - def __init__(self, controller: Controller, pv_prefix: str = "MY-DEVICE-PREFIX"): + def __init__( + self, + controller: Controller, + pv_prefix: str = "MY-DEVICE-PREFIX", + options: EpicsIOCOptions | None = None, + ): super().__init__(controller) self._pv_prefix = pv_prefix - self._ioc = EpicsIOC(pv_prefix, self._mapping) + options = options or EpicsIOCOptions() + self._ioc = EpicsIOC(pv_prefix, self._mapping, options=options) def create_docs(self, options: EpicsDocsOptions | None = None) -> None: EpicsDocs(self._mapping).create_docs(options) @@ -19,5 +25,5 @@ def create_docs(self, options: EpicsDocsOptions | None = None) -> None: def create_gui(self, options: EpicsGUIOptions | None = None) -> None: EpicsGUI(self._mapping, self._pv_prefix).create_gui(options) - def _run(self, options: EpicsIOCOptions | None = None): - self._ioc.run(self._dispatcher, self._context, options) + def _run(self): + self._ioc.run(self._dispatcher, self._context) diff --git a/src/fastcs/backends/epics/ioc.py b/src/fastcs/backends/epics/ioc.py index c5694b79..82317a23 100644 --- a/src/fastcs/backends/epics/ioc.py +++ b/src/fastcs/backends/epics/ioc.py @@ -1,5 +1,6 @@ from collections.abc import Callable from dataclasses import dataclass +from enum import Enum from types import MethodType from typing import Any, Literal @@ -22,32 +23,41 @@ EPICS_MAX_NAME_LENGTH = 60 +class PvNamingConvention(Enum): + NO_CONVERSION = 0 + PASCAL = 1 + CAPITALIZED = 2 + + @dataclass class EpicsIOCOptions: terminal: bool = True + pv_naming_convention: PvNamingConvention = PvNamingConvention.PASCAL class EpicsIOC: - def __init__(self, pv_prefix: str, mapping: Mapping): + def __init__( + self, pv_prefix: str, mapping: Mapping, options: EpicsIOCOptions | None = None + ): + self.options = options or EpicsIOCOptions() _add_pvi_info(f"{pv_prefix}:PVI") _add_sub_controller_pvi_info(pv_prefix, mapping.controller) _create_and_link_attribute_pvs(pv_prefix, mapping) - _create_and_link_command_pvs(pv_prefix, mapping) + _create_and_link_command_pvs( + pv_prefix, mapping, self.options.pv_naming_convention + ) def run( self, dispatcher: AsyncioDispatcher, context: dict[str, Any], - options: EpicsIOCOptions | None = None, ) -> None: - if options is None: - options = EpicsIOCOptions() - builder.LoadDatabase() softioc.iocInit(dispatcher) - softioc.interactive_ioc(context) + if self.options.terminal: + softioc.interactive_ioc(context) def _add_pvi_info( @@ -218,9 +228,6 @@ async def async_write_display(value: T): record = _get_output_record( f"{pv_prefix}:{pv_name}", attribute, on_update=on_update ) - pascal_case_pv_name = pv_name.title() - if pascal_case_pv_name != pv_name: - record.add_alias(f"{pv_prefix}:{pascal_case_pv_name}") _add_attr_pvi_info(record, pv_prefix, attr_name, "w") @@ -256,11 +263,23 @@ def _get_output_record(pv: str, attribute: AttrW, on_update: Callable) -> Any: ) -def _create_and_link_command_pvs(pv_prefix: str, mapping: Mapping) -> None: +def _convert_attr_name_to_pv_name( + attr_name: str, naming_convention: PvNamingConvention +) -> str: + if naming_convention == PvNamingConvention.PASCAL: + return attr_name.title().replace("_", "") + elif naming_convention == PvNamingConvention.CAPITALIZED: + return attr_name.upper().replace("_", "-") + return attr_name + + +def _create_and_link_command_pvs( + pv_prefix: str, mapping: Mapping, naming_convention: PvNamingConvention +) -> None: for single_mapping in mapping.get_controller_mappings(): path = single_mapping.controller.path for attr_name, method in single_mapping.command_methods.items(): - pv_name = attr_name.title().replace("_", "") + pv_name = _convert_attr_name_to_pv_name(attr_name, naming_convention) _pv_prefix = ":".join([pv_prefix] + path) if len(f"{_pv_prefix}:{pv_name}") > EPICS_MAX_NAME_LENGTH: print( From 9de39a204ccc34e2594feb5cc5bdae825eaecdc2 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Thu, 17 Oct 2024 11:51:29 +0100 Subject: [PATCH 03/10] made the pv naming convention extend to all blocks --- src/fastcs/backends/epics/ioc.py | 417 +++++++++++++++++-------------- tests/backends/epics/test_ioc.py | 3 +- 2 files changed, 225 insertions(+), 195 deletions(-) diff --git a/src/fastcs/backends/epics/ioc.py b/src/fastcs/backends/epics/ioc.py index 82317a23..f276632e 100644 --- a/src/fastcs/backends/epics/ioc.py +++ b/src/fastcs/backends/epics/ioc.py @@ -24,15 +24,29 @@ class PvNamingConvention(Enum): - NO_CONVERSION = 0 - PASCAL = 1 - CAPITALIZED = 2 + NO_CONVERSION = "NO_CONVERSION" + PASCAL = "PASCAL" + CAPITALIZED = "CAPITALIZED" + + +DEFAULT_PV_SEPARATOR = ":" @dataclass class EpicsIOCOptions: terminal: bool = True pv_naming_convention: PvNamingConvention = PvNamingConvention.PASCAL + pv_separator: str = DEFAULT_PV_SEPARATOR + + +def _convert_attr_name_to_pv_name( + attr_name: str, naming_convention: PvNamingConvention +) -> str: + if naming_convention == PvNamingConvention.PASCAL: + return attr_name.title().replace("_", "") + elif naming_convention == PvNamingConvention.CAPITALIZED: + return attr_name.upper().replace("_", "-") + return attr_name class EpicsIOC: @@ -40,13 +54,11 @@ def __init__( self, pv_prefix: str, mapping: Mapping, options: EpicsIOCOptions | None = None ): self.options = options or EpicsIOCOptions() - _add_pvi_info(f"{pv_prefix}:PVI") - _add_sub_controller_pvi_info(pv_prefix, mapping.controller) + _add_pvi_info(f"{pv_prefix}{self.options.pv_separator}PVI") + self._add_sub_controller_pvi_info(pv_prefix, mapping.controller) - _create_and_link_attribute_pvs(pv_prefix, mapping) - _create_and_link_command_pvs( - pv_prefix, mapping, self.options.pv_naming_convention - ) + self._create_and_link_attribute_pvs(pv_prefix, mapping) + self._create_and_link_command_pvs(pv_prefix, mapping) def run( self, @@ -59,6 +71,209 @@ def run( if self.options.terminal: softioc.interactive_ioc(context) + def _add_sub_controller_pvi_info(self, pv_prefix: str, parent: BaseController): + """Add PVI references from controller to its sub controllers, recursively. + + Args: + pv_prefix: PV Prefix of IOC + parent: Controller to add PVI refs for + + """ + parent_pvi = self.options.pv_separator.join([pv_prefix] + parent.path + ["PVI"]) + + for child in parent.get_sub_controllers().values(): + child_pvi = self.options.pv_separator.join( + [pv_prefix] + + [ + _convert_attr_name_to_pv_name( + path, self.options.pv_naming_convention + ) + for path in child.path + ] + + ["PVI"] + ) + child_name = child.path[-1].lower() + + _add_pvi_info(child_pvi, parent_pvi, child_name) + + self._add_sub_controller_pvi_info(pv_prefix, child) + + def _create_and_link_attribute_pvs(self, pv_prefix: str, mapping: Mapping) -> None: + for single_mapping in mapping.get_controller_mappings(): + formatted_path = [ + _convert_attr_name_to_pv_name(p, self.options.pv_naming_convention) + for p in single_mapping.controller.path + ] + for attr_name, attribute in single_mapping.attributes.items(): + pv_name = _convert_attr_name_to_pv_name( + attr_name, self.options.pv_naming_convention + ) + _pv_prefix = self.options.pv_separator.join( + [pv_prefix] + formatted_path + ) + full_pv_name_length = len( + f"{_pv_prefix}{self.options.pv_separator}{pv_name}" + ) + + if full_pv_name_length > EPICS_MAX_NAME_LENGTH: + attribute.enabled = False + print( + f"Not creating PV for {attr_name} for controller" + f" {single_mapping.controller.path} as full name would exceed" + f" {EPICS_MAX_NAME_LENGTH} characters" + ) + continue + + match attribute: + case AttrRW(): + if full_pv_name_length > (EPICS_MAX_NAME_LENGTH - 4): + print( + f"Not creating PVs for {attr_name} as _RBV PV" + f" name would exceed {EPICS_MAX_NAME_LENGTH}" + " characters" + ) + attribute.enabled = False + else: + self._create_and_link_read_pv( + _pv_prefix, f"{pv_name}_RBV", attr_name, attribute + ) + self._create_and_link_write_pv( + _pv_prefix, pv_name, attr_name, attribute + ) + case AttrR(): + self._create_and_link_read_pv( + _pv_prefix, pv_name, attr_name, attribute + ) + case AttrW(): + self._create_and_link_write_pv( + _pv_prefix, pv_name, attr_name, attribute + ) + + def _create_and_link_read_pv( + self, pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrR[T] + ) -> None: + if attr_is_enum(attribute): + + async def async_record_set(value: T): + record.set(enum_value_to_index(attribute, value)) + + else: + + async def async_record_set(value: T): + record.set(value) + + record = _get_input_record( + f"{pv_prefix}{self.options.pv_separator}{pv_name}", attribute + ) + self._add_attr_pvi_info(record, pv_prefix, attr_name, "r") + + attribute.set_update_callback(async_record_set) + + def _create_and_link_command_pvs(self, pv_prefix: str, mapping: Mapping) -> None: + for single_mapping in mapping.get_controller_mappings(): + formatted_path = [ + _convert_attr_name_to_pv_name(p, self.options.pv_naming_convention) + for p in single_mapping.controller.path + ] + for attr_name, method in single_mapping.command_methods.items(): + pv_name = _convert_attr_name_to_pv_name( + attr_name, self.options.pv_naming_convention + ) + _pv_prefix = self.options.pv_separator.join( + [pv_prefix] + formatted_path + ) + if ( + len(f"{_pv_prefix}{self.options.pv_separator}{pv_name}") + > EPICS_MAX_NAME_LENGTH + ): + print( + f"Not creating PV for {attr_name} as full name would exceed" + f" {EPICS_MAX_NAME_LENGTH} characters" + ) + method.enabled = False + else: + self._create_and_link_command_pv( + _pv_prefix, + pv_name, + attr_name, + MethodType(method.fn, single_mapping.controller), + ) + + def _create_and_link_write_pv( + self, pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrW[T] + ) -> None: + if attr_is_enum(attribute): + + async def on_update(value): + await attribute.process_without_display_update( + enum_index_to_value(attribute, value) + ) + + async def async_write_display(value: T): + record.set(enum_value_to_index(attribute, value), process=False) + + else: + + async def on_update(value): + await attribute.process_without_display_update(value) + + async def async_write_display(value: T): + record.set(value, process=False) + + record = _get_output_record( + f"{pv_prefix}{self.options.pv_separator}{pv_name}", + attribute, + on_update=on_update, + ) + + self._add_attr_pvi_info(record, pv_prefix, attr_name, "w") + + attribute.set_write_display_callback(async_write_display) + + def _create_and_link_command_pv( + self, pv_prefix: str, pv_name: str, attr_name: str, method: Callable + ) -> None: + async def wrapped_method(_: Any): + await method() + + record = builder.aOut( + f"{pv_prefix}{self.options.pv_separator}{pv_name}", + initial_value=0, + always_update=True, + on_update=wrapped_method, + ) + + self._add_attr_pvi_info(record, pv_prefix, attr_name, "x") + + def _add_attr_pvi_info( + self, + record: RecordWrapper, + prefix: str, + name: str, + access_mode: Literal["r", "w", "rw", "x"], + ): + """Add an info tag to a record to include it in the PVI for the controller. + + Args: + record: Record to add info tag to + prefix: PV prefix of controller + name: Name of parameter to add to PVI + access_mode: Access mode of parameter + + """ + record.add_info( + "Q:group", + { + f"{prefix}{self.options.pv_separator}PVI": { + f"value.{name}.{access_mode}": { + "+channel": "NAME", + "+type": "plain", + "+trigger": f"value.{name}.{access_mode}", + } + } + }, + ) + def _add_pvi_info( pvi: str, @@ -105,82 +320,6 @@ def _add_pvi_info( record.add_info("Q:group", q_group) -def _add_sub_controller_pvi_info(pv_prefix: str, parent: BaseController): - """Add PVI references from controller to its sub controllers, recursively. - - Args: - pv_prefix: PV Prefix of IOC - parent: Controller to add PVI refs for - - """ - parent_pvi = ":".join([pv_prefix] + parent.path + ["PVI"]) - - for child in parent.get_sub_controllers().values(): - child_pvi = ":".join([pv_prefix] + child.path + ["PVI"]) - child_name = child.path[-1].lower() - - _add_pvi_info(child_pvi, parent_pvi, child_name) - - _add_sub_controller_pvi_info(pv_prefix, child) - - -def _create_and_link_attribute_pvs(pv_prefix: str, mapping: Mapping) -> None: - for single_mapping in mapping.get_controller_mappings(): - path = single_mapping.controller.path - for attr_name, attribute in single_mapping.attributes.items(): - pv_name = attr_name.replace("_", "") - _pv_prefix = ":".join([pv_prefix] + path) - full_pv_name_length = len(f"{_pv_prefix}:{pv_name}") - - if full_pv_name_length > EPICS_MAX_NAME_LENGTH: - attribute.enabled = False - print( - f"Not creating PV for {attr_name} for controller" - f" {single_mapping.controller.path} as full name would exceed" - f" {EPICS_MAX_NAME_LENGTH} characters" - ) - continue - - match attribute: - case AttrRW(): - if full_pv_name_length > (EPICS_MAX_NAME_LENGTH - 4): - print( - f"Not creating PVs for {attr_name} as _RBV PV" - f" name would exceed {EPICS_MAX_NAME_LENGTH}" - " characters" - ) - attribute.enabled = False - else: - _create_and_link_read_pv( - _pv_prefix, f"{pv_name}_RBV", attr_name, attribute - ) - _create_and_link_write_pv( - _pv_prefix, pv_name, attr_name, attribute - ) - case AttrR(): - _create_and_link_read_pv(_pv_prefix, pv_name, attr_name, attribute) - case AttrW(): - _create_and_link_write_pv(_pv_prefix, pv_name, attr_name, attribute) - - -def _create_and_link_read_pv( - pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrR[T] -) -> None: - if attr_is_enum(attribute): - - async def async_record_set(value: T): - record.set(enum_value_to_index(attribute, value)) - else: - - async def async_record_set(value: T): - record.set(value) - - record = _get_input_record(f"{pv_prefix}:{pv_name}", attribute) - _add_attr_pvi_info(record, pv_prefix, attr_name, "r") - - attribute.set_update_callback(async_record_set) - - def _get_input_record(pv: str, attribute: AttrR) -> RecordWrapper: if attr_is_enum(attribute): assert attribute.allowed_values is not None and all( @@ -204,36 +343,6 @@ def _get_input_record(pv: str, attribute: AttrR) -> RecordWrapper: ) -def _create_and_link_write_pv( - pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrW[T] -) -> None: - if attr_is_enum(attribute): - - async def on_update(value): - await attribute.process_without_display_update( - enum_index_to_value(attribute, value) - ) - - async def async_write_display(value: T): - record.set(enum_value_to_index(attribute, value), process=False) - - else: - - async def on_update(value): - await attribute.process_without_display_update(value) - - async def async_write_display(value: T): - record.set(value, process=False) - - record = _get_output_record( - f"{pv_prefix}:{pv_name}", attribute, on_update=on_update - ) - - _add_attr_pvi_info(record, pv_prefix, attr_name, "w") - - attribute.set_write_display_callback(async_write_display) - - def _get_output_record(pv: str, attribute: AttrW, on_update: Callable) -> Any: if attr_is_enum(attribute): assert attribute.allowed_values is not None and all( @@ -261,81 +370,3 @@ def _get_output_record(pv: str, attribute: AttrW, on_update: Callable) -> Any: raise FastCSException( f"Unsupported type {type(attribute.datatype)}: {attribute.datatype}" ) - - -def _convert_attr_name_to_pv_name( - attr_name: str, naming_convention: PvNamingConvention -) -> str: - if naming_convention == PvNamingConvention.PASCAL: - return attr_name.title().replace("_", "") - elif naming_convention == PvNamingConvention.CAPITALIZED: - return attr_name.upper().replace("_", "-") - return attr_name - - -def _create_and_link_command_pvs( - pv_prefix: str, mapping: Mapping, naming_convention: PvNamingConvention -) -> None: - for single_mapping in mapping.get_controller_mappings(): - path = single_mapping.controller.path - for attr_name, method in single_mapping.command_methods.items(): - pv_name = _convert_attr_name_to_pv_name(attr_name, naming_convention) - _pv_prefix = ":".join([pv_prefix] + path) - if len(f"{_pv_prefix}:{pv_name}") > EPICS_MAX_NAME_LENGTH: - print( - f"Not creating PV for {attr_name} as full name would exceed" - f" {EPICS_MAX_NAME_LENGTH} characters" - ) - method.enabled = False - else: - _create_and_link_command_pv( - _pv_prefix, - pv_name, - attr_name, - MethodType(method.fn, single_mapping.controller), - ) - - -def _create_and_link_command_pv( - pv_prefix: str, pv_name: str, attr_name: str, method: Callable -) -> None: - async def wrapped_method(_: Any): - await method() - - record = builder.aOut( - f"{pv_prefix}:{pv_name}", - initial_value=0, - always_update=True, - on_update=wrapped_method, - ) - - _add_attr_pvi_info(record, pv_prefix, attr_name, "x") - - -def _add_attr_pvi_info( - record: RecordWrapper, - prefix: str, - name: str, - access_mode: Literal["r", "w", "rw", "x"], -): - """Add an info tag to a record to include it in the PVI for the controller. - - Args: - record: Record to add info tag to - prefix: PV prefix of controller - name: Name of parameter to add to PVI - access_mode: Access mode of parameter - - """ - record.add_info( - "Q:group", - { - f"{prefix}:PVI": { - f"value.{name}.{access_mode}": { - "+channel": "NAME", - "+type": "plain", - "+trigger": f"value.{name}.{access_mode}", - } - } - }, - ) diff --git a/tests/backends/epics/test_ioc.py b/tests/backends/epics/test_ioc.py index b469d64e..c1a5b321 100644 --- a/tests/backends/epics/test_ioc.py +++ b/tests/backends/epics/test_ioc.py @@ -357,8 +357,7 @@ def test_add_attr_pvi_info(mocker: MockerFixture): ) -async def do_nothing(arg): - ... +async def do_nothing(arg): ... class NothingCommand: From 62db783e2caf8561d9591cbc9e84a397752bb6aa Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Thu, 17 Oct 2024 14:13:35 +0100 Subject: [PATCH 04/10] also added epics name options to the gui --- src/fastcs/backends/epics/backend.py | 17 ++++---- src/fastcs/backends/epics/gui.py | 29 +++++++++++-- src/fastcs/backends/epics/ioc.py | 61 ++++++++++++++-------------- src/fastcs/backends/epics/util.py | 34 ++++++++++++++-- 4 files changed, 96 insertions(+), 45 deletions(-) diff --git a/src/fastcs/backends/epics/backend.py b/src/fastcs/backends/epics/backend.py index 5efa8b51..b34eff8f 100644 --- a/src/fastcs/backends/epics/backend.py +++ b/src/fastcs/backends/epics/backend.py @@ -11,19 +11,22 @@ def __init__( self, controller: Controller, pv_prefix: str = "MY-DEVICE-PREFIX", - options: EpicsIOCOptions | None = None, + ioc_options: EpicsIOCOptions | None = None, ): super().__init__(controller) self._pv_prefix = pv_prefix - options = options or EpicsIOCOptions() - self._ioc = EpicsIOC(pv_prefix, self._mapping, options=options) + self.ioc_options = ioc_options or EpicsIOCOptions() + self._ioc = EpicsIOC(pv_prefix, self._mapping, options=ioc_options) - def create_docs(self, options: EpicsDocsOptions | None = None) -> None: - EpicsDocs(self._mapping).create_docs(options) + def create_docs(self, docs_options: EpicsDocsOptions | None = None) -> None: + EpicsDocs(self._mapping).create_docs(docs_options) - def create_gui(self, options: EpicsGUIOptions | None = None) -> None: - EpicsGUI(self._mapping, self._pv_prefix).create_gui(options) + def create_gui(self, gui_options: EpicsGUIOptions | None = None) -> None: + assert self.ioc_options.name_options is not None + EpicsGUI( + self._mapping, self._pv_prefix, self.ioc_options.name_options + ).create_gui(gui_options) def _run(self): self._ioc.run(self._dispatcher, self._context) diff --git a/src/fastcs/backends/epics/gui.py b/src/fastcs/backends/epics/gui.py index b9c48751..ef64921c 100644 --- a/src/fastcs/backends/epics/gui.py +++ b/src/fastcs/backends/epics/gui.py @@ -27,6 +27,7 @@ from pydantic import ValidationError from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW +from fastcs.backends.epics.util import EpicsNameOptions, _convert_attr_name_to_pv_name from fastcs.cs_methods import Command from fastcs.datatypes import Bool, Float, Int, String from fastcs.exceptions import FastCSException @@ -39,7 +40,7 @@ class EpicsGUIFormat(Enum): edl = ".edl" -@dataclass +@dataclass(frozen=True) class EpicsGUIOptions: output_path: Path = Path.cwd() / "output.bob" file_format: EpicsGUIFormat = EpicsGUIFormat.bob @@ -47,13 +48,33 @@ class EpicsGUIOptions: class EpicsGUI: - def __init__(self, mapping: Mapping, pv_prefix: str) -> None: + def __init__( + self, + mapping: Mapping, + pv_prefix: str, + epics_name_options: EpicsNameOptions | None = None, + ) -> None: self._mapping = mapping self._pv_prefix = pv_prefix + self.epics_name_options = epics_name_options or EpicsNameOptions() def _get_pv(self, attr_path: list[str], name: str): - attr_prefix = ":".join([self._pv_prefix] + attr_path) - return f"{attr_prefix}:{name.title().replace('_', '')}" + return self.epics_name_options.pv_separator.join( + [ + self._pv_prefix, + ] + + [ + _convert_attr_name_to_pv_name( + attr_name, self.epics_name_options.pv_naming_convention + ) + for attr_name in attr_path + ] + + [ + _convert_attr_name_to_pv_name( + name, self.epics_name_options.pv_naming_convention + ), + ], + ) @staticmethod def _get_read_widget(attribute: AttrR) -> ReadWidgetUnion: diff --git a/src/fastcs/backends/epics/ioc.py b/src/fastcs/backends/epics/ioc.py index f276632e..f4457f84 100644 --- a/src/fastcs/backends/epics/ioc.py +++ b/src/fastcs/backends/epics/ioc.py @@ -1,6 +1,5 @@ from collections.abc import Callable from dataclasses import dataclass -from enum import Enum from types import MethodType from typing import Any, Literal @@ -11,6 +10,8 @@ from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.backends.epics.util import ( MBB_STATE_FIELDS, + EpicsNameOptions, + PvNamingConvention, attr_is_enum, enum_index_to_value, enum_value_to_index, @@ -23,20 +24,10 @@ EPICS_MAX_NAME_LENGTH = 60 -class PvNamingConvention(Enum): - NO_CONVERSION = "NO_CONVERSION" - PASCAL = "PASCAL" - CAPITALIZED = "CAPITALIZED" - - -DEFAULT_PV_SEPARATOR = ":" - - -@dataclass +@dataclass(frozen=True) class EpicsIOCOptions: terminal: bool = True - pv_naming_convention: PvNamingConvention = PvNamingConvention.PASCAL - pv_separator: str = DEFAULT_PV_SEPARATOR + name_options: EpicsNameOptions = EpicsNameOptions() def _convert_attr_name_to_pv_name( @@ -53,8 +44,10 @@ class EpicsIOC: def __init__( self, pv_prefix: str, mapping: Mapping, options: EpicsIOCOptions | None = None ): - self.options = options or EpicsIOCOptions() - _add_pvi_info(f"{pv_prefix}{self.options.pv_separator}PVI") + self._options = options or EpicsIOCOptions() + self._name_options = self._options.name_options + + _add_pvi_info(f"{pv_prefix}{self._name_options.pv_separator}PVI") self._add_sub_controller_pvi_info(pv_prefix, mapping.controller) self._create_and_link_attribute_pvs(pv_prefix, mapping) @@ -68,7 +61,7 @@ def run( builder.LoadDatabase() softioc.iocInit(dispatcher) - if self.options.terminal: + if self._options.terminal: softioc.interactive_ioc(context) def _add_sub_controller_pvi_info(self, pv_prefix: str, parent: BaseController): @@ -79,14 +72,16 @@ def _add_sub_controller_pvi_info(self, pv_prefix: str, parent: BaseController): parent: Controller to add PVI refs for """ - parent_pvi = self.options.pv_separator.join([pv_prefix] + parent.path + ["PVI"]) + parent_pvi = self._name_options.pv_separator.join( + [pv_prefix] + parent.path + ["PVI"] + ) for child in parent.get_sub_controllers().values(): - child_pvi = self.options.pv_separator.join( + child_pvi = self._name_options.pv_separator.join( [pv_prefix] + [ _convert_attr_name_to_pv_name( - path, self.options.pv_naming_convention + path, self._name_options.pv_naming_convention ) for path in child.path ] @@ -101,18 +96,20 @@ def _add_sub_controller_pvi_info(self, pv_prefix: str, parent: BaseController): def _create_and_link_attribute_pvs(self, pv_prefix: str, mapping: Mapping) -> None: for single_mapping in mapping.get_controller_mappings(): formatted_path = [ - _convert_attr_name_to_pv_name(p, self.options.pv_naming_convention) + _convert_attr_name_to_pv_name( + p, self._name_options.pv_naming_convention + ) for p in single_mapping.controller.path ] for attr_name, attribute in single_mapping.attributes.items(): pv_name = _convert_attr_name_to_pv_name( - attr_name, self.options.pv_naming_convention + attr_name, self._name_options.pv_naming_convention ) - _pv_prefix = self.options.pv_separator.join( + _pv_prefix = self._name_options.pv_separator.join( [pv_prefix] + formatted_path ) full_pv_name_length = len( - f"{_pv_prefix}{self.options.pv_separator}{pv_name}" + f"{_pv_prefix}{self._name_options.pv_separator}{pv_name}" ) if full_pv_name_length > EPICS_MAX_NAME_LENGTH: @@ -163,7 +160,7 @@ async def async_record_set(value: T): record.set(value) record = _get_input_record( - f"{pv_prefix}{self.options.pv_separator}{pv_name}", attribute + f"{pv_prefix}{self._name_options.pv_separator}{pv_name}", attribute ) self._add_attr_pvi_info(record, pv_prefix, attr_name, "r") @@ -172,18 +169,20 @@ async def async_record_set(value: T): def _create_and_link_command_pvs(self, pv_prefix: str, mapping: Mapping) -> None: for single_mapping in mapping.get_controller_mappings(): formatted_path = [ - _convert_attr_name_to_pv_name(p, self.options.pv_naming_convention) + _convert_attr_name_to_pv_name( + p, self._name_options.pv_naming_convention + ) for p in single_mapping.controller.path ] for attr_name, method in single_mapping.command_methods.items(): pv_name = _convert_attr_name_to_pv_name( - attr_name, self.options.pv_naming_convention + attr_name, self._name_options.pv_naming_convention ) - _pv_prefix = self.options.pv_separator.join( + _pv_prefix = self._name_options.pv_separator.join( [pv_prefix] + formatted_path ) if ( - len(f"{_pv_prefix}{self.options.pv_separator}{pv_name}") + len(f"{_pv_prefix}{self._name_options.pv_separator}{pv_name}") > EPICS_MAX_NAME_LENGTH ): print( @@ -221,7 +220,7 @@ async def async_write_display(value: T): record.set(value, process=False) record = _get_output_record( - f"{pv_prefix}{self.options.pv_separator}{pv_name}", + f"{pv_prefix}{self._name_options.pv_separator}{pv_name}", attribute, on_update=on_update, ) @@ -237,7 +236,7 @@ async def wrapped_method(_: Any): await method() record = builder.aOut( - f"{pv_prefix}{self.options.pv_separator}{pv_name}", + f"{pv_prefix}{self._name_options.pv_separator}{pv_name}", initial_value=0, always_update=True, on_update=wrapped_method, @@ -264,7 +263,7 @@ def _add_attr_pvi_info( record.add_info( "Q:group", { - f"{prefix}{self.options.pv_separator}PVI": { + f"{prefix}{self._name_options.pv_separator}PVI": { f"value.{name}.{access_mode}": { "+channel": "NAME", "+type": "plain", diff --git a/src/fastcs/backends/epics/util.py b/src/fastcs/backends/epics/util.py index b1ffa608..3f149d2d 100644 --- a/src/fastcs/backends/epics/util.py +++ b/src/fastcs/backends/epics/util.py @@ -1,3 +1,6 @@ +from dataclasses import dataclass +from enum import Enum + from fastcs.attributes import Attribute from fastcs.datatypes import String, T @@ -25,6 +28,31 @@ MBB_MAX_CHOICES = len(_MBB_FIELD_PREFIXES) +class PvNamingConvention(Enum): + NO_CONVERSION = "NO_CONVERSION" + PASCAL = "PASCAL" + CAPITALIZED = "CAPITALIZED" + + +DEFAULT_PV_SEPARATOR = ":" + + +@dataclass(frozen=True) +class EpicsNameOptions: + pv_naming_convention: PvNamingConvention = PvNamingConvention.PASCAL + pv_separator: str = DEFAULT_PV_SEPARATOR + + +def _convert_attr_name_to_pv_name( + attr_name: str, naming_convention: PvNamingConvention +) -> str: + if naming_convention == PvNamingConvention.PASCAL: + return attr_name.title().replace("_", "") + elif naming_convention == PvNamingConvention.CAPITALIZED: + return attr_name.upper().replace("_", "-") + return attr_name + + def attr_is_enum(attribute: Attribute) -> bool: """Check if the `Attribute` has a `String` datatype and has `allowed_values` set. @@ -36,9 +64,9 @@ def attr_is_enum(attribute: Attribute) -> bool: """ match attribute: - case Attribute( - datatype=String(), allowed_values=allowed_values - ) if allowed_values is not None and len(allowed_values) <= MBB_MAX_CHOICES: + case Attribute(datatype=String(), allowed_values=allowed_values) if ( + allowed_values is not None and len(allowed_values) <= MBB_MAX_CHOICES + ): return True case _: return False From 0f4ab57e079fcdd82ea163990dfcde4c3dc45a05 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Thu, 24 Oct 2024 08:26:03 +0100 Subject: [PATCH 05/10] added option for attributes being pascal and controllers being capitalized --- src/fastcs/backends/epics/gui.py | 10 +++++----- src/fastcs/backends/epics/ioc.py | 33 ++++++++++++------------------- src/fastcs/backends/epics/util.py | 9 +++++++-- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/fastcs/backends/epics/gui.py b/src/fastcs/backends/epics/gui.py index ef64921c..042ac7ab 100644 --- a/src/fastcs/backends/epics/gui.py +++ b/src/fastcs/backends/epics/gui.py @@ -27,7 +27,7 @@ from pydantic import ValidationError from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW -from fastcs.backends.epics.util import EpicsNameOptions, _convert_attr_name_to_pv_name +from fastcs.backends.epics.util import EpicsNameOptions, _convert_attribute_name_to_pv_name from fastcs.cs_methods import Command from fastcs.datatypes import Bool, Float, Int, String from fastcs.exceptions import FastCSException @@ -64,14 +64,14 @@ def _get_pv(self, attr_path: list[str], name: str): self._pv_prefix, ] + [ - _convert_attr_name_to_pv_name( - attr_name, self.epics_name_options.pv_naming_convention + _convert_attribute_name_to_pv_name( + attr_name, self.epics_name_options.pv_naming_convention, is_attribute=False ) for attr_name in attr_path ] + [ - _convert_attr_name_to_pv_name( - name, self.epics_name_options.pv_naming_convention + _convert_attribute_name_to_pv_name( + name, self.epics_name_options.pv_naming_convention, is_attribute=True ), ], ) diff --git a/src/fastcs/backends/epics/ioc.py b/src/fastcs/backends/epics/ioc.py index f4457f84..c6c1f7c2 100644 --- a/src/fastcs/backends/epics/ioc.py +++ b/src/fastcs/backends/epics/ioc.py @@ -11,7 +11,7 @@ from fastcs.backends.epics.util import ( MBB_STATE_FIELDS, EpicsNameOptions, - PvNamingConvention, + _convert_attribute_name_to_pv_name, attr_is_enum, enum_index_to_value, enum_value_to_index, @@ -30,15 +30,6 @@ class EpicsIOCOptions: name_options: EpicsNameOptions = EpicsNameOptions() -def _convert_attr_name_to_pv_name( - attr_name: str, naming_convention: PvNamingConvention -) -> str: - if naming_convention == PvNamingConvention.PASCAL: - return attr_name.title().replace("_", "") - elif naming_convention == PvNamingConvention.CAPITALIZED: - return attr_name.upper().replace("_", "-") - return attr_name - class EpicsIOC: def __init__( @@ -80,8 +71,10 @@ def _add_sub_controller_pvi_info(self, pv_prefix: str, parent: BaseController): child_pvi = self._name_options.pv_separator.join( [pv_prefix] + [ - _convert_attr_name_to_pv_name( - path, self._name_options.pv_naming_convention + _convert_attribute_name_to_pv_name( + path, + self._name_options.pv_naming_convention, + is_attribute=False ) for path in child.path ] @@ -96,14 +89,14 @@ def _add_sub_controller_pvi_info(self, pv_prefix: str, parent: BaseController): def _create_and_link_attribute_pvs(self, pv_prefix: str, mapping: Mapping) -> None: for single_mapping in mapping.get_controller_mappings(): formatted_path = [ - _convert_attr_name_to_pv_name( - p, self._name_options.pv_naming_convention + _convert_attribute_name_to_pv_name( + p, self._name_options.pv_naming_convention, is_attribute=False ) for p in single_mapping.controller.path ] for attr_name, attribute in single_mapping.attributes.items(): - pv_name = _convert_attr_name_to_pv_name( - attr_name, self._name_options.pv_naming_convention + pv_name = _convert_attribute_name_to_pv_name( + attr_name, self._name_options.pv_naming_convention, is_attribute=True ) _pv_prefix = self._name_options.pv_separator.join( [pv_prefix] + formatted_path @@ -169,14 +162,14 @@ async def async_record_set(value: T): def _create_and_link_command_pvs(self, pv_prefix: str, mapping: Mapping) -> None: for single_mapping in mapping.get_controller_mappings(): formatted_path = [ - _convert_attr_name_to_pv_name( - p, self._name_options.pv_naming_convention + _convert_attribute_name_to_pv_name( + p, self._name_options.pv_naming_convention, is_attribute=False ) for p in single_mapping.controller.path ] for attr_name, method in single_mapping.command_methods.items(): - pv_name = _convert_attr_name_to_pv_name( - attr_name, self._name_options.pv_naming_convention + pv_name = _convert_attribute_name_to_pv_name( + attr_name, self._name_options.pv_naming_convention, is_attribute=True ) _pv_prefix = self._name_options.pv_separator.join( [pv_prefix] + formatted_path diff --git a/src/fastcs/backends/epics/util.py b/src/fastcs/backends/epics/util.py index 3f149d2d..579fae21 100644 --- a/src/fastcs/backends/epics/util.py +++ b/src/fastcs/backends/epics/util.py @@ -32,6 +32,7 @@ class PvNamingConvention(Enum): NO_CONVERSION = "NO_CONVERSION" PASCAL = "PASCAL" CAPITALIZED = "CAPITALIZED" + CAPITALIZED_CONTROLLER_PASCAL_ATTRIBUTE = "CAPITALIZED_CONTROLLER_PASCAL_ATTRIBUTE" DEFAULT_PV_SEPARATOR = ":" @@ -43,13 +44,17 @@ class EpicsNameOptions: pv_separator: str = DEFAULT_PV_SEPARATOR -def _convert_attr_name_to_pv_name( - attr_name: str, naming_convention: PvNamingConvention +def _convert_attribute_name_to_pv_name( + attr_name: str, naming_convention: PvNamingConvention, is_attribute: bool = False ) -> str: if naming_convention == PvNamingConvention.PASCAL: return attr_name.title().replace("_", "") elif naming_convention == PvNamingConvention.CAPITALIZED: return attr_name.upper().replace("_", "-") + elif naming_convention == PvNamingConvention.CAPITALIZED_CONTROLLER_PASCAL_ATTRIBUTE: + if is_attribute: + return _convert_attribute_name_to_pv_name(attr_name, PvNamingConvention.PASCAL, is_attribute) + return _convert_attribute_name_to_pv_name(attr_name, PvNamingConvention.CAPITALIZED) return attr_name From 8b1e3bbe601357d7abdf6fb4ee550da2a8de674e Mon Sep 17 00:00:00 2001 From: Eva Date: Tue, 12 Nov 2024 09:38:13 +0000 Subject: [PATCH 06/10] fixed tests Also made it so that scan tasks are explicitly torn down when the backend is deleted. --- src/fastcs/backends/epics/ioc.py | 2 +- tests/backends/epics/test_ioc.py | 94 ++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 35 deletions(-) diff --git a/src/fastcs/backends/epics/ioc.py b/src/fastcs/backends/epics/ioc.py index c6c1f7c2..e456b647 100644 --- a/src/fastcs/backends/epics/ioc.py +++ b/src/fastcs/backends/epics/ioc.py @@ -338,7 +338,7 @@ def _get_input_record(pv: str, attribute: AttrR) -> RecordWrapper: def _get_output_record(pv: str, attribute: AttrW, on_update: Callable) -> Any: if attr_is_enum(attribute): assert attribute.allowed_values is not None and all( - isinstance(v, str) for v in attribute.allowed_values + isinstance(v, str) or isinstance(v, int) for v in attribute.allowed_values ) state_keys = dict(zip(MBB_STATE_FIELDS, attribute.allowed_values, strict=False)) return builder.mbbOut(pv, always_update=True, on_update=on_update, **state_keys) diff --git a/tests/backends/epics/test_ioc.py b/tests/backends/epics/test_ioc.py index c1a5b321..6fdf200e 100644 --- a/tests/backends/epics/test_ioc.py +++ b/tests/backends/epics/test_ioc.py @@ -7,11 +7,7 @@ from fastcs.backends.epics.ioc import ( EPICS_MAX_NAME_LENGTH, EpicsIOC, - _add_attr_pvi_info, _add_pvi_info, - _add_sub_controller_pvi_info, - _create_and_link_read_pv, - _create_and_link_write_pv, _get_input_record, _get_output_record, ) @@ -27,17 +23,31 @@ ONOFF_STATES = {"ZRST": "disabled", "ONST": "enabled"} +@pytest.fixture +def ioc_without_mapping(mocker: MockerFixture, mapping: Mapping): + mocker.patch("fastcs.backends.epics.ioc.builder") + mocker.patch("fastcs.backends.epics.ioc.EpicsIOC._create_and_link_attribute_pvs") + mocker.patch("fastcs.backends.epics.ioc.EpicsIOC._create_and_link_command_pvs") + + return EpicsIOC(DEVICE, mapping) + + @pytest.mark.asyncio -async def test_create_and_link_read_pv(mocker: MockerFixture): +async def test_create_and_link_read_pv( + mocker: MockerFixture, ioc_without_mapping: EpicsIOC +): get_input_record = mocker.patch("fastcs.backends.epics.ioc._get_input_record") - add_attr_pvi_info = mocker.patch("fastcs.backends.epics.ioc._add_attr_pvi_info") attr_is_enum = mocker.patch("fastcs.backends.epics.ioc.attr_is_enum") + mocker.patch("fastcs.backends.epics.ioc._add_pvi_info") + add_attr_pvi_info = mocker.patch( + "fastcs.backends.epics.ioc.EpicsIOC._add_attr_pvi_info" + ) record = get_input_record.return_value attribute = mocker.MagicMock() - attr_is_enum.return_value = False - _create_and_link_read_pv("PREFIX", "PV", "attr", attribute) + + ioc_without_mapping._create_and_link_read_pv("PREFIX", "PV", "attr", attribute) get_input_record.assert_called_once_with("PREFIX:PV", attribute) add_attr_pvi_info.assert_called_once_with(record, "PREFIX", "attr", "r") @@ -51,9 +61,13 @@ async def test_create_and_link_read_pv(mocker: MockerFixture): @pytest.mark.asyncio -async def test_create_and_link_read_pv_enum(mocker: MockerFixture): +async def test_create_and_link_read_pv_enum( + mocker: MockerFixture, ioc_without_mapping: EpicsIOC +): get_input_record = mocker.patch("fastcs.backends.epics.ioc._get_input_record") - add_attr_pvi_info = mocker.patch("fastcs.backends.epics.ioc._add_attr_pvi_info") + add_attr_pvi_info = mocker.patch( + "fastcs.backends.epics.ioc.EpicsIOC._add_attr_pvi_info" + ) attr_is_enum = mocker.patch("fastcs.backends.epics.ioc.attr_is_enum") record = get_input_record.return_value enum_value_to_index = mocker.patch("fastcs.backends.epics.ioc.enum_value_to_index") @@ -61,7 +75,7 @@ async def test_create_and_link_read_pv_enum(mocker: MockerFixture): attribute = mocker.MagicMock() attr_is_enum.return_value = True - _create_and_link_read_pv("PREFIX", "PV", "attr", attribute) + ioc_without_mapping._create_and_link_read_pv("PREFIX", "PV", "attr", attribute) get_input_record.assert_called_once_with("PREFIX:PV", attribute) add_attr_pvi_info.assert_called_once_with(record, "PREFIX", "attr", "r") @@ -108,9 +122,13 @@ def test_get_input_record_raises(mocker: MockerFixture): @pytest.mark.asyncio -async def test_create_and_link_write_pv(mocker: MockerFixture): +async def test_create_and_link_write_pv( + mocker: MockerFixture, ioc_without_mapping: EpicsIOC +): get_output_record = mocker.patch("fastcs.backends.epics.ioc._get_output_record") - add_attr_pvi_info = mocker.patch("fastcs.backends.epics.ioc._add_attr_pvi_info") + add_attr_pvi_info = mocker.patch( + "fastcs.backends.epics.ioc.EpicsIOC._add_attr_pvi_info" + ) attr_is_enum = mocker.patch("fastcs.backends.epics.ioc.attr_is_enum") record = get_output_record.return_value @@ -118,7 +136,7 @@ async def test_create_and_link_write_pv(mocker: MockerFixture): attribute.process_without_display_update = mocker.AsyncMock() attr_is_enum.return_value = False - _create_and_link_write_pv("PREFIX", "PV", "attr", attribute) + ioc_without_mapping._create_and_link_write_pv("PREFIX", "PV", "attr", attribute) get_output_record.assert_called_once_with( "PREFIX:PV", attribute, on_update=mocker.ANY @@ -140,9 +158,13 @@ async def test_create_and_link_write_pv(mocker: MockerFixture): @pytest.mark.asyncio -async def test_create_and_link_write_pv_enum(mocker: MockerFixture): +async def test_create_and_link_write_pv_enum( + mocker: MockerFixture, ioc_without_mapping: EpicsIOC +): get_output_record = mocker.patch("fastcs.backends.epics.ioc._get_output_record") - add_attr_pvi_info = mocker.patch("fastcs.backends.epics.ioc._add_attr_pvi_info") + add_attr_pvi_info = mocker.patch( + "fastcs.backends.epics.ioc.EpicsIOC._add_attr_pvi_info" + ) attr_is_enum = mocker.patch("fastcs.backends.epics.ioc.attr_is_enum") enum_value_to_index = mocker.patch("fastcs.backends.epics.ioc.enum_value_to_index") enum_index_to_value = mocker.patch("fastcs.backends.epics.ioc.enum_index_to_value") @@ -152,7 +174,7 @@ async def test_create_and_link_write_pv_enum(mocker: MockerFixture): attribute.process_without_display_update = mocker.AsyncMock() attr_is_enum.return_value = True - _create_and_link_write_pv("PREFIX", "PV", "attr", attribute) + ioc_without_mapping._create_and_link_write_pv("PREFIX", "PV", "attr", attribute) get_output_record.assert_called_once_with( "PREFIX:PV", attribute, on_update=mocker.ANY @@ -215,22 +237,28 @@ def test_ioc(mocker: MockerFixture, mapping: Mapping): builder = mocker.patch("fastcs.backends.epics.ioc.builder") add_pvi_info = mocker.patch("fastcs.backends.epics.ioc._add_pvi_info") add_sub_controller_pvi_info = mocker.patch( - "fastcs.backends.epics.ioc._add_sub_controller_pvi_info" + "fastcs.backends.epics.ioc.EpicsIOC._add_sub_controller_pvi_info" ) EpicsIOC(DEVICE, mapping) # Check records are created builder.boolIn.assert_called_once_with(f"{DEVICE}:ReadBool", ZNAM="OFF", ONAM="ON") - builder.longIn.assert_any_call(f"{DEVICE}:ReadInt") - builder.aIn.assert_called_once_with(f"{DEVICE}:ReadWriteFloat_RBV", PREC=2) + builder.longIn.assert_any_call(f"{DEVICE}:ReadInt", EGU=None) + builder.aIn.assert_called_once_with( + f"{DEVICE}:ReadWriteFloat_RBV", EGU=None, PREC=2 + ) builder.aOut.assert_any_call( - f"{DEVICE}:ReadWriteFloat", always_update=True, on_update=mocker.ANY, PREC=2 + f"{DEVICE}:ReadWriteFloat", + always_update=True, + on_update=mocker.ANY, + EGU=None, + PREC=2, ) - builder.longIn.assert_any_call(f"{DEVICE}:BigEnum") - builder.longIn.assert_any_call(f"{DEVICE}:ReadWriteInt_RBV") + builder.longIn.assert_any_call(f"{DEVICE}:BigEnum", EGU=None) + builder.longIn.assert_any_call(f"{DEVICE}:ReadWriteInt_RBV", EGU=None) builder.longOut.assert_called_with( - f"{DEVICE}:ReadWriteInt", always_update=True, on_update=mocker.ANY + f"{DEVICE}:ReadWriteInt", always_update=True, on_update=mocker.ANY, EGU=None ) builder.mbbIn.assert_called_once_with( f"{DEVICE}:StringEnum_RBV", ZRST="red", ONST="green", TWST="blue" @@ -323,7 +351,9 @@ def test_add_pvi_info_with_parent(mocker: MockerFixture): ) -def test_add_sub_controller_pvi_info(mocker: MockerFixture): +def test_add_sub_controller_pvi_info( + mocker: MockerFixture, ioc_without_mapping: EpicsIOC +): add_pvi_info = mocker.patch("fastcs.backends.epics.ioc._add_pvi_info") controller = mocker.MagicMock() controller.path = [] @@ -331,17 +361,17 @@ def test_add_sub_controller_pvi_info(mocker: MockerFixture): child.path = ["Child"] controller.get_sub_controllers.return_value = {"d": child} - _add_sub_controller_pvi_info(DEVICE, controller) + ioc_without_mapping._add_sub_controller_pvi_info(DEVICE, controller) add_pvi_info.assert_called_once_with( f"{DEVICE}:Child:PVI", f"{DEVICE}:PVI", "child" ) -def test_add_attr_pvi_info(mocker: MockerFixture): +def test_add_attr_pvi_info(mocker: MockerFixture, ioc_without_mapping: EpicsIOC): record = mocker.MagicMock() - _add_attr_pvi_info(record, DEVICE, "attr", "r") + ioc_without_mapping._add_attr_pvi_info(record, DEVICE, "attr", "r") record.add_info.assert_called_once_with( "Q:group", @@ -387,13 +417,9 @@ def test_long_pv_names_discarded(mocker: MockerFixture): short_pv_name = "attr_rw_short_name".title().replace("_", "") builder.longOut.assert_called_once_with( - f"{DEVICE}:{short_pv_name}", - always_update=True, - on_update=mocker.ANY, - ) - builder.longIn.assert_called_once_with( - f"{DEVICE}:{short_pv_name}_RBV", + f"{DEVICE}:{short_pv_name}", always_update=True, on_update=mocker.ANY, EGU=None ) + builder.longIn.assert_called_once_with(f"{DEVICE}:{short_pv_name}_RBV", EGU=None) long_pv_name = long_attr_name.title().replace("_", "") with pytest.raises(AssertionError): From 0df5cf459d495c89424cc61e4020964c855e823f Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Mon, 18 Nov 2024 12:10:51 +0000 Subject: [PATCH 07/10] linting --- src/fastcs/backends/epics/gui.py | 13 ++++++++++--- src/fastcs/backends/epics/ioc.py | 11 +++++++---- src/fastcs/backends/epics/util.py | 12 +++++++++--- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/fastcs/backends/epics/gui.py b/src/fastcs/backends/epics/gui.py index 042ac7ab..4a040ec6 100644 --- a/src/fastcs/backends/epics/gui.py +++ b/src/fastcs/backends/epics/gui.py @@ -27,7 +27,10 @@ from pydantic import ValidationError from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW -from fastcs.backends.epics.util import EpicsNameOptions, _convert_attribute_name_to_pv_name +from fastcs.backends.epics.util import ( + EpicsNameOptions, + _convert_attribute_name_to_pv_name, +) from fastcs.cs_methods import Command from fastcs.datatypes import Bool, Float, Int, String from fastcs.exceptions import FastCSException @@ -65,13 +68,17 @@ def _get_pv(self, attr_path: list[str], name: str): ] + [ _convert_attribute_name_to_pv_name( - attr_name, self.epics_name_options.pv_naming_convention, is_attribute=False + attr_name, + self.epics_name_options.pv_naming_convention, + is_attribute=False, ) for attr_name in attr_path ] + [ _convert_attribute_name_to_pv_name( - name, self.epics_name_options.pv_naming_convention, is_attribute=True + name, + self.epics_name_options.pv_naming_convention, + is_attribute=True, ), ], ) diff --git a/src/fastcs/backends/epics/ioc.py b/src/fastcs/backends/epics/ioc.py index e456b647..f2aea9e1 100644 --- a/src/fastcs/backends/epics/ioc.py +++ b/src/fastcs/backends/epics/ioc.py @@ -30,7 +30,6 @@ class EpicsIOCOptions: name_options: EpicsNameOptions = EpicsNameOptions() - class EpicsIOC: def __init__( self, pv_prefix: str, mapping: Mapping, options: EpicsIOCOptions | None = None @@ -74,7 +73,7 @@ def _add_sub_controller_pvi_info(self, pv_prefix: str, parent: BaseController): _convert_attribute_name_to_pv_name( path, self._name_options.pv_naming_convention, - is_attribute=False + is_attribute=False, ) for path in child.path ] @@ -96,7 +95,9 @@ def _create_and_link_attribute_pvs(self, pv_prefix: str, mapping: Mapping) -> No ] for attr_name, attribute in single_mapping.attributes.items(): pv_name = _convert_attribute_name_to_pv_name( - attr_name, self._name_options.pv_naming_convention, is_attribute=True + attr_name, + self._name_options.pv_naming_convention, + is_attribute=True, ) _pv_prefix = self._name_options.pv_separator.join( [pv_prefix] + formatted_path @@ -169,7 +170,9 @@ def _create_and_link_command_pvs(self, pv_prefix: str, mapping: Mapping) -> None ] for attr_name, method in single_mapping.command_methods.items(): pv_name = _convert_attribute_name_to_pv_name( - attr_name, self._name_options.pv_naming_convention, is_attribute=True + attr_name, + self._name_options.pv_naming_convention, + is_attribute=True, ) _pv_prefix = self._name_options.pv_separator.join( [pv_prefix] + formatted_path diff --git a/src/fastcs/backends/epics/util.py b/src/fastcs/backends/epics/util.py index 579fae21..973cef43 100644 --- a/src/fastcs/backends/epics/util.py +++ b/src/fastcs/backends/epics/util.py @@ -51,10 +51,16 @@ def _convert_attribute_name_to_pv_name( return attr_name.title().replace("_", "") elif naming_convention == PvNamingConvention.CAPITALIZED: return attr_name.upper().replace("_", "-") - elif naming_convention == PvNamingConvention.CAPITALIZED_CONTROLLER_PASCAL_ATTRIBUTE: + elif ( + naming_convention == PvNamingConvention.CAPITALIZED_CONTROLLER_PASCAL_ATTRIBUTE + ): if is_attribute: - return _convert_attribute_name_to_pv_name(attr_name, PvNamingConvention.PASCAL, is_attribute) - return _convert_attribute_name_to_pv_name(attr_name, PvNamingConvention.CAPITALIZED) + return _convert_attribute_name_to_pv_name( + attr_name, PvNamingConvention.PASCAL, is_attribute + ) + return _convert_attribute_name_to_pv_name( + attr_name, PvNamingConvention.CAPITALIZED + ) return attr_name From f229d5de71d9dd130903739aa35c331829a9af19 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Tue, 19 Nov 2024 08:21:18 +0000 Subject: [PATCH 08/10] removed change to test from other branch --- tests/backends/epics/test_ioc.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/backends/epics/test_ioc.py b/tests/backends/epics/test_ioc.py index 6fdf200e..710f12ca 100644 --- a/tests/backends/epics/test_ioc.py +++ b/tests/backends/epics/test_ioc.py @@ -244,21 +244,18 @@ def test_ioc(mocker: MockerFixture, mapping: Mapping): # Check records are created builder.boolIn.assert_called_once_with(f"{DEVICE}:ReadBool", ZNAM="OFF", ONAM="ON") - builder.longIn.assert_any_call(f"{DEVICE}:ReadInt", EGU=None) - builder.aIn.assert_called_once_with( - f"{DEVICE}:ReadWriteFloat_RBV", EGU=None, PREC=2 - ) + builder.longIn.assert_any_call(f"{DEVICE}:ReadInt") + builder.aIn.assert_called_once_with(f"{DEVICE}:ReadWriteFloat_RBV", PREC=2) builder.aOut.assert_any_call( f"{DEVICE}:ReadWriteFloat", always_update=True, on_update=mocker.ANY, - EGU=None, PREC=2, ) - builder.longIn.assert_any_call(f"{DEVICE}:BigEnum", EGU=None) - builder.longIn.assert_any_call(f"{DEVICE}:ReadWriteInt_RBV", EGU=None) + builder.longIn.assert_any_call(f"{DEVICE}:BigEnum") + builder.longIn.assert_any_call(f"{DEVICE}:ReadWriteInt_RBV") builder.longOut.assert_called_with( - f"{DEVICE}:ReadWriteInt", always_update=True, on_update=mocker.ANY, EGU=None + f"{DEVICE}:ReadWriteInt", always_update=True, on_update=mocker.ANY ) builder.mbbIn.assert_called_once_with( f"{DEVICE}:StringEnum_RBV", ZRST="red", ONST="green", TWST="blue" @@ -417,9 +414,9 @@ def test_long_pv_names_discarded(mocker: MockerFixture): short_pv_name = "attr_rw_short_name".title().replace("_", "") builder.longOut.assert_called_once_with( - f"{DEVICE}:{short_pv_name}", always_update=True, on_update=mocker.ANY, EGU=None + f"{DEVICE}:{short_pv_name}", always_update=True, on_update=mocker.ANY ) - builder.longIn.assert_called_once_with(f"{DEVICE}:{short_pv_name}_RBV", EGU=None) + builder.longIn.assert_called_once_with(f"{DEVICE}:{short_pv_name}_RBV") long_pv_name = long_attr_name.title().replace("_", "") with pytest.raises(AssertionError): From c1d80c8d61106d117eb8a18a449244c41fb0ab05 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Tue, 19 Nov 2024 10:53:32 +0000 Subject: [PATCH 09/10] make test for naming convention --- tests/backends/epics/test_ioc.py | 77 +++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/tests/backends/epics/test_ioc.py b/tests/backends/epics/test_ioc.py index 710f12ca..84655fec 100644 --- a/tests/backends/epics/test_ioc.py +++ b/tests/backends/epics/test_ioc.py @@ -7,11 +7,13 @@ from fastcs.backends.epics.ioc import ( EPICS_MAX_NAME_LENGTH, EpicsIOC, + EpicsIOCOptions, _add_pvi_info, _get_input_record, _get_output_record, ) -from fastcs.controller import Controller +from fastcs.backends.epics.util import EpicsNameOptions, PvNamingConvention +from fastcs.controller import Controller, SubController from fastcs.cs_methods import Command from fastcs.datatypes import Int, String from fastcs.exceptions import FastCSException @@ -461,3 +463,76 @@ def test_long_pv_names_discarded(mocker: MockerFixture): always_update=True, on_update=mocker.ANY, ) + + +class FooSubController(SubController): + def __init__(self): + self.foo_bar_2 = AttrR(Int()) + super().__init__() + + +class FooController(Controller): + def __init__(self): + self.foo_bar_1 = AttrR(Int()) + self.sub_controller = FooSubController() + super().__init__() + self.register_sub_controller("sub_controller", self.sub_controller) + + +@pytest.mark.parametrize( + ("naming_convention", "separator", "foo_bar_1", "sub_controller", "foo_bar_2"), + [ + ( + PvNamingConvention.NO_CONVERSION, + "&", + f"{DEVICE}&foo_bar_1", + f"{DEVICE}&sub_controller&PVI", + f"{DEVICE}&sub_controller&foo_bar_2", + ), + ( + PvNamingConvention.PASCAL, + ":", + f"{DEVICE}:FooBar1", + f"{DEVICE}:SubController:PVI", + f"{DEVICE}:SubController:FooBar2", + ), + ( + PvNamingConvention.CAPITALIZED, + ":", + f"{DEVICE}:FOO-BAR-1", + f"{DEVICE}:SUB-CONTROLLER:PVI", + f"{DEVICE}:SUB-CONTROLLER:FOO-BAR-2", + ), + ( + PvNamingConvention.CAPITALIZED_CONTROLLER_PASCAL_ATTRIBUTE, + ":", + f"{DEVICE}:FooBar1", + f"{DEVICE}:SUB-CONTROLLER:PVI", + f"{DEVICE}:SUB-CONTROLLER:FooBar2", + ), + ], +) +def test_pv_naming_conventions( + mocker: MockerFixture, + naming_convention: PvNamingConvention, + separator: str, + foo_bar_1: str, + sub_controller: str, + foo_bar_2: str, +): + options = EpicsIOCOptions( + name_options=EpicsNameOptions( + pv_naming_convention=naming_convention, pv_separator=separator + ) + ) + builder = mocker.patch("fastcs.backends.epics.ioc.builder") + controller = FooController() + mapping = Mapping(controller) + EpicsIOC(DEVICE, mapping, options=options) + builder.longIn.assert_any_call(foo_bar_1) + builder.longIn.assert_any_call(foo_bar_2) + builder.longStringIn.assert_any_call( + f"{sub_controller}_PV", + initial_value=f"{sub_controller}", + DESC="The records in this controller", + ) From 722cc37b775a43805b03e60e04401f11aa06aa88 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Fri, 25 Oct 2024 12:03:22 +0100 Subject: [PATCH 10/10] made `update_period` optional on `Updater` --- src/fastcs/attributes.py | 3 ++- src/fastcs/backend.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/fastcs/attributes.py b/src/fastcs/attributes.py index 1bcb5638..742e11c5 100644 --- a/src/fastcs/attributes.py +++ b/src/fastcs/attributes.py @@ -26,7 +26,8 @@ async def put(self, controller: Any, attr: AttrW, value: Any) -> None: class Updater(Protocol): """Protocol for updating the cached readback value of an ``Attribute``.""" - update_period: float + # If update period is None then the attribute will not be updated as a task. + update_period: float | None async def update(self, controller: Any, attr: AttrR) -> None: pass diff --git a/src/fastcs/backend.py b/src/fastcs/backend.py index 10f7bc3e..dde3df7b 100644 --- a/src/fastcs/backend.py +++ b/src/fastcs/backend.py @@ -134,6 +134,8 @@ def _add_attribute_updater_tasks( for attribute in single_mapping.attributes.values(): match attribute: case AttrR(updater=Updater(update_period=update_period)) as attribute: + if update_period is None: + continue callback = _create_updater_callback( attribute, single_mapping.controller )