Skip to content
Snippets Groups Projects
Commit 95128caa authored by Corné Lukken's avatar Corné Lukken
Browse files

Merge branch 'L2SS-964' into 'main'

L2SS-964: Address several review comments

Closes L2SS-964

See merge request !1
parents 11db548f d6fae792
Branches
Tags
1 merge request!1L2SS-964: Address several review comments
Pipeline #48524 passed with warnings
...@@ -79,8 +79,8 @@ run_unit_tests_py37: ...@@ -79,8 +79,8 @@ run_unit_tests_py37:
# images with different python versions. # images with different python versions.
stage: test stage: test
before_script: before_script:
- apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y libboost-python-dev libtango-dev # Needed to install pytango - apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y git python3-pip libboost-python-dev libtango-dev # Needed to install pytango
- python -m pip install --upgrade pip - python3 -m pip install --upgrade pip
- pip install --upgrade tox - pip install --upgrade tox
run_unit_tests_py38: run_unit_tests_py38:
...@@ -97,15 +97,12 @@ run_unit_tests_py39: ...@@ -97,15 +97,12 @@ run_unit_tests_py39:
- echo "run python3.9 unit tests /w coverage" - echo "run python3.9 unit tests /w coverage"
- tox -e py39 - tox -e py39
#run_unit_tests_py310: run_unit_tests_py310:
# extends: .run_unit_tests_pyXX extends: .run_unit_tests_pyXX
# image: python:3.10-bullseye image: ubuntu:jammy
# script: script:
# # Debian Bullseye ships with libboost-python linked to Python 3.9. Use the one from Debian Sid instead. - echo "run python3.10 unit tests /w coverage"
# - echo 'deb http://deb.debian.org/debian sid main' >> /etc/apt/sources.list - tox -e py310
# - apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y libboost-python1.74-dev
# - echo "run python3.10 unit tests /w coverage"
# - tox -e py310
run_unit_tests_py311: run_unit_tests_py311:
extends: .run_unit_tests_pyXX extends: .run_unit_tests_pyXX
......
# Example Python Package [![Pipeline Status](https://git.astron.nl/lofar2.0/attributewrapper/badges/main/pipeline.svg)](https://git.astron.nl/lofar2.0/attributewrapper/-/pipelines)
[![Coverage Status](https://git.astron.nl/lofar2.0/attributewrapper/badges/main/coverage.svg)](https://git.astron.nl/lofar2.0/attributewrapper/-/jobs/artifacts/main/download?job=coverage)
[![Python Versions](https://img.shields.io/badge/python-3.7%20|%203.8%20|%203.9%20|%203.10%20|%203.11-informational)](https://git.astron.nl/lofar2.0/attributewrapper)
[![Latest Documentation](https://img.shields.io/badge/docs-download-informational)](https://git.astron.nl/lofar2.0/attributewrapper/-/jobs/artifacts/main/download?job=package_docs)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Latest Release](https://git.astron.nl/lofar2.0/attributewrapper/-/badges/release.svg)](https://git.astron.nl/lofar2.0/attributewrapper/-/releases)
An example repository of an CI/CD pipeline for building, testing and publishing a python package. # PyTango Attribute Wrapper
If you find some missing functionality with regards to CI/CD, testing, linting or something else, feel free to make a merge request with the proposed changes. The PyTango attribute wrapper reduces boilerplate code for pytango devices
by generating attribute read and write functions dynamically.
## Example of README.md contents below:
## Installation ## Installation
``` ```
...@@ -13,11 +16,7 @@ pip install . ...@@ -13,11 +16,7 @@ pip install .
``` ```
## Usage ## Usage
```
from my_awesome_app import cool_module
cool_module.greeter() # prints "Hello World"
```
## Contributing ## Contributing
...@@ -41,3 +40,7 @@ To automatically apply most suggested linting changes execute: ...@@ -41,3 +40,7 @@ To automatically apply most suggested linting changes execute:
## License ## License
This project is licensed under the Apache License Version 2.0 This project is licensed under the Apache License Version 2.0
## Releases
- 0.1 - Initial release from separating into own repository
\ No newline at end of file
0.1
# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
# SPDX-License-Identifier: Apache-2.0
import logging
import numpy
__all__ = ["AttributeIO"]
logger = logging.getLogger()
class AttributeIO(object):
"""Holds the I/O functionality for an attribute for a specific device."""
def __init__(self, device, attribute_wrapper):
# Link to the associated device
self.device = device
# Link to the associated attribute wrapper
self.attribute_wrapper = attribute_wrapper
# Link to last (written) value
self.cached_value = None
# Specific read and write functions for this attribute on this device
self.read_function = lambda: None
self.write_function = lambda value: None
def cached_read_function(self):
"""Return the last (written) value, if available. Otherwise, read
from the device."""
if self.cached_value is not None:
return self.cached_value
self.cached_value = self.read_function()
return self.cached_value
def cached_write_function(self, value):
"""Writes the given value to the device, and updates the cache."""
# flexible array sizes are not supported by all clients. make sure we only
# write arrays of maximum size.
if self.attribute_wrapper.shape != ():
if isinstance(value, numpy.ndarray):
value_shape = value.shape
else:
if len(value) > 0 and isinstance(value[0], list):
# nested list
value_shape = (len(value), len(value[0]))
else:
# straight list
value_shape = (len(value),)
if value_shape != self.attribute_wrapper.shape:
raise ValueError(
f"Tried writing an array of shape {value_shape} into an attribute "
f"of shape {self.attribute_wrapper.shape}"
)
self.write_function(value)
self.cached_value = value
...@@ -4,88 +4,16 @@ ...@@ -4,88 +4,16 @@
import logging import logging
from functools import reduce from functools import reduce
from operator import mul from operator import mul
from functools import wraps
import numpy
from tango import AttrWriteType, AttReqType from tango import AttrWriteType, AttReqType
from tango.server import attribute from tango.server import attribute
logger = logging.getLogger() from attribute_wrapper.attribute_io import AttributeIO
from attribute_wrapper.decorators import fault_on_error
__all__ = ["AttributeWrapper"] __all__ = ["AttributeWrapper"]
logger = logging.getLogger()
def fault_on_error():
"""
Wrapper to catch exceptions. Sets the device in a FAULT state if any occurs.
"""
def inner(func):
@wraps(func)
def error_wrapper(device, *args, **kwargs):
try:
return func(device, *args, **kwargs)
except Exception as e:
logger.exception("Function failed.")
device.Fault(f"FAULT in {func.__name__}: {e.__class__.__name__}: {e}")
raise
return error_wrapper
return inner
class AttributeIO(object):
"""Holds the I/O functionality for an attribute for a specific device."""
def __init__(self, device, attribute_wrapper):
# Link to the associated device
self.device = device
# Link to the associated attribute wrapper
self.attribute_wrapper = attribute_wrapper
# Link to last (written) value
self.cached_value = None
# Specific read and write functions for this attribute on this device
self.read_function = lambda: None
self.write_function = lambda value: None
def cached_read_function(self):
"""Return the last (written) value, if available. Otherwise, read
from the device."""
if self.cached_value is not None:
return self.cached_value
self.cached_value = self.read_function()
return self.cached_value
def cached_write_function(self, value):
"""Writes the given value to the device, and updates the cache."""
# flexible array sizes are not supported by all clients. make sure we only
# write arrays of maximum size.
if self.attribute_wrapper.shape != ():
if isinstance(value, numpy.ndarray):
value_shape = value.shape
else:
if len(value) > 0 and isinstance(value[0], list):
# nested list
value_shape = (len(value), len(value[0]))
else:
# straight list
value_shape = (len(value),)
if value_shape != self.attribute_wrapper.shape:
raise ValueError(
f"Tried writing an array of shape {value_shape} into an attribute "
f"of shape {self.attribute_wrapper.shape}"
)
self.write_function(value)
self.cached_value = value
class AttributeWrapper(attribute): class AttributeWrapper(attribute):
......
# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
# SPDX-License-Identifier: Apache-2.0
import logging
from functools import wraps
__all__ = ["fault_on_error"]
logger = logging.getLogger()
def fault_on_error():
"""Wrapper to catch exceptions. Sets the device in a FAULT state if any occurs."""
def inner(func):
@wraps(func)
def error_wrapper(device, *args, **kwargs):
try:
return func(device, *args, **kwargs)
except Exception as e:
logger.exception("Function failed.")
device.Fault(f"FAULT in {func.__name__}: {e.__class__.__name__}: {e}")
raise
return error_wrapper
return inner
...@@ -2,7 +2,7 @@ FROM python:3.7-buster ...@@ -2,7 +2,7 @@ FROM python:3.7-buster
# Install PyTango dependencies # Install PyTango dependencies
RUN apt-get update -y RUN apt-get update -y
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y libboost-python-dev libtango-dev RUN DEBIAN_FRONTEND=noninteractive apt-get install -y git python3-pip libboost-python-dev libtango-dev
# Make sure we have the latest tooling for our tests # Make sure we have the latest tooling for our tests
RUN python -m pip install --upgrade pip RUN python -m pip install --upgrade pip
......
...@@ -9,10 +9,10 @@ import numpy ...@@ -9,10 +9,10 @@ import numpy
logger = logging.getLogger() logger = logging.getLogger()
class TestClient: class ExampleClient:
"""Example comms_client implementation """Example comms_client implementation
During initialisation it creates a correctly shaped zero filled value. on read that During initialisation, it creates a correctly shaped zero filled value. on read that
value is returned and on write its modified. value is returned and on write its modified.
""" """
......
...@@ -21,7 +21,7 @@ from tango.test_context import DeviceTestContext ...@@ -21,7 +21,7 @@ from tango.test_context import DeviceTestContext
# Internal imports # Internal imports
from attribute_wrapper.attribute_wrapper import AttributeWrapper from attribute_wrapper.attribute_wrapper import AttributeWrapper
from attribute_wrapper.states import INITIALISED_STATES from attribute_wrapper.states import INITIALISED_STATES
from test_client import TestClient from example_client import ExampleClient
SCALAR_DIMS = (1,) SCALAR_DIMS = (1,)
SPECTRUM_DIMS = (4,) SPECTRUM_DIMS = (4,)
...@@ -32,14 +32,6 @@ STR_SPECTRUM_VAL = ["1", "1", "1", "1"] ...@@ -32,14 +32,6 @@ STR_SPECTRUM_VAL = ["1", "1", "1", "1"]
STR_IMAGE_VAL = [["1", "1"], ["1", "1"], ["1", "1"]] STR_IMAGE_VAL = [["1", "1"], ["1", "1"], ["1", "1"]]
# def dev_init(device):
# device.set_state(DevState.INIT)
# device.test_client = TestClient(device.Fault)
# for i in device.attr_list():
# asyncio.run(i.async_set_comm_client(device, device.test_client))
# device.test_client.start()
class DeviceWrapper(Device, metaclass=DeviceMeta): class DeviceWrapper(Device, metaclass=DeviceMeta):
def __init__(self, cl, name): def __init__(self, cl, name):
super().__init__(cl, name) super().__init__(cl, name)
...@@ -70,11 +62,11 @@ class DeviceWrapper(Device, metaclass=DeviceMeta): ...@@ -70,11 +62,11 @@ class DeviceWrapper(Device, metaclass=DeviceMeta):
def configure_for_initialise(self): def configure_for_initialise(self):
self.set_state(DevState.INIT) self.set_state(DevState.INIT)
self.test_client = TestClient(self.Fault) self.example_client = ExampleClient(self.Fault)
for i in self.attr_list(): for i in self.attr_list():
asyncio.run(i.async_set_comm_client(self, self.test_client)) asyncio.run(i.async_set_comm_client(self, self.example_client))
self.test_client.start() self.example_client.start()
@command() @command()
def initialise(self): def initialise(self):
...@@ -82,12 +74,6 @@ class DeviceWrapper(Device, metaclass=DeviceMeta): ...@@ -82,12 +74,6 @@ class DeviceWrapper(Device, metaclass=DeviceMeta):
class TestAttributeTypes(testscenarios.WithScenarios, unittest.TestCase): class TestAttributeTypes(testscenarios.WithScenarios, unittest.TestCase):
# def setUp(self):
# # Avoid the device trying to access itself as a client
# self.deviceproxy_patch = mock.patch.object(DeviceWrapper, "DeviceProxy")
# self.deviceproxy_patch.start()
# self.addCleanup(self.deviceproxy_patch.stop)
class StrScalarDevice(DeviceWrapper): class StrScalarDevice(DeviceWrapper):
scalar_R = AttributeWrapper(comms_annotation="str_scalar_R", datatype=str) scalar_R = AttributeWrapper(comms_annotation="str_scalar_R", datatype=str)
scalar_RW = AttributeWrapper( scalar_RW = AttributeWrapper(
...@@ -934,12 +920,6 @@ class TestAttributeTypes(testscenarios.WithScenarios, unittest.TestCase): ...@@ -934,12 +920,6 @@ class TestAttributeTypes(testscenarios.WithScenarios, unittest.TestCase):
class TestAttributeAccess(testscenarios.WithScenarios, TestCase): class TestAttributeAccess(testscenarios.WithScenarios, TestCase):
# def setUp(self):
# # Avoid the device trying to access itself as a client
# self.deviceproxy_patch = mock.patch.object(DeviceWrapper, "DeviceProxy")
# self.deviceproxy_patch.start()
# self.addCleanup(self.deviceproxy_patch.stop)
class float32_scalar_device(DeviceWrapper): class float32_scalar_device(DeviceWrapper):
scalar_R = AttributeWrapper( scalar_R = AttributeWrapper(
comms_annotation="float32_scalar_R", datatype=numpy.float32 comms_annotation="float32_scalar_R", datatype=numpy.float32
......
...@@ -25,7 +25,7 @@ commands = ...@@ -25,7 +25,7 @@ commands =
[testenv:coverage] [testenv:coverage]
commands = commands =
{envpython} --version {envpython} --version
{envpython} -m pytest --cov-report xml --cov-report html --cov=map {envpython} -m pytest --cov-report xml --cov-report html --cov-report xml:coverage.xml --cov=attribute_wrapper
# Use generative name and command prefixes to reuse the same virtualenv # Use generative name and command prefixes to reuse the same virtualenv
# for all linting jobs. # for all linting jobs.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment