diff --git a/doc/data_types.rst b/doc/data_types.rst index 4b2b2e47..bd12f3ec 100644 --- a/doc/data_types.rst +++ b/doc/data_types.rst @@ -50,6 +50,14 @@ much more efficient. | | | In this case it is :py:obj:`str` (decoded with default python | | | | encoding *utf-8*)) | +-------------------------+---------------------------------------------------------------------------+---------------------------------------------------------------------------+ +| | * :py:obj:`int` (for value) | * :py:obj:`int` (for value) | +| | * :py:class:`list` <:py:obj:`str`> (for enum_labels) | * :py:class:`list` <:py:obj:`str`> (for enum_labels) | +| DEV_ENUM | | | +| (*New in PyTango 9.0*) | Note: direct attribute access via DeviceProxy will return enumerated | Note: direct attribute access via DeviceProxy will return enumerated | +| | type :py:obj:`enum.IntEnum`. | type :py:obj:`enum.IntEnum`. | +| | This type uses the package enum34. | Python < 3.4, uses the package enum34. | +| | | Python >= 3.4, uses standard package enum. | ++-------------------------+---------------------------------------------------------------------------+---------------------------------------------------------------------------+ **For array types (SPECTRUM/IMAGE)** diff --git a/doc/utilities.rst b/doc/utilities.rst index 8d61c172..2081ae95 100644 --- a/doc/utilities.rst +++ b/doc/utilities.rst @@ -9,6 +9,8 @@ The Utilities API :members: :undoc-members: +.. autofunction:: tango.utils.get_enum_labels + .. autofunction:: tango.utils.is_pure_str .. autofunction:: tango.utils.is_seq diff --git a/examples/Clock/ClockDS.py b/examples/Clock/ClockDS.py index 5e1ecde3..0c5b4b79 100644 --- a/examples/Clock/ClockDS.py +++ b/examples/Clock/ClockDS.py @@ -6,6 +6,7 @@ - time: read-only scalar float - gmtime: read-only sequence (spectrum) of integers + - noon: read-only enumerated type commands: @@ -14,7 +15,14 @@ """ import time -from PyTango.server import Device, attribute, command +from enum import IntEnum +from tango.server import Device, attribute, command +from tango.utils import get_enum_labels + + +class Noon(IntEnum): + AM = 0 # DevEnum's must start at 0 + PM = 1 # and increment by 1 class Clock(Device): @@ -28,6 +36,11 @@ def time(self): def read_gmtime(self): return time.gmtime() + @attribute(dtype='DevEnum', enum_labels=get_enum_labels(Noon)) + def noon(self): + time_struct = time.gmtime(time.time()) + return Noon.AM if time_struct.tm_hour < 12 else Noon.PM + @command(dtype_in=float, dtype_out=str) def ctime(self, seconds): """ diff --git a/examples/Clock/client.py b/examples/Clock/client.py index 137119b2..aff09dd9 100644 --- a/examples/Clock/client.py +++ b/examples/Clock/client.py @@ -26,11 +26,11 @@ clock = PyTango.DeviceProxy(sys.argv[1]) t = clock.time gmt = clock.gmtime +noon = clock.noon print(t) print(gmt) +print(noon, noon.name, noon.value) +if noon == noon.AM: + print('Good morning!') print(clock.ctime(t)) print(clock.mktime(gmt)) - - - - diff --git a/setup.py b/setup.py index d87d3917..d5258d0c 100644 --- a/setup.py +++ b/setup.py @@ -428,6 +428,7 @@ def setup_args(): install_requires = [ 'six', + 'enum34;python_version<"3.4"', ] setup_requires = [] diff --git a/tango/device_proxy.py b/tango/device_proxy.py index c8a05b15..f86dba2f 100644 --- a/tango/device_proxy.py +++ b/tango/device_proxy.py @@ -15,6 +15,7 @@ import textwrap import threading import collections +import enum from ._tango import StdStringVector, DbData, DbDatum, AttributeInfo from ._tango import AttributeInfoEx, AttributeInfoList, AttributeInfoListEx @@ -149,7 +150,7 @@ def __DeviceProxy__get_attr_cache(self): try: ret = self.__dict__['__attr_cache'] except KeyError: - self.__dict__['__attr_cache'] = ret = () + self.__dict__['__attr_cache'] = ret = {} return ret @@ -214,7 +215,15 @@ def __DeviceProxy__refresh_cmd_cache(self): def __DeviceProxy__refresh_attr_cache(self): - attr_cache = [attr_name.lower() for attr_name in self.get_attribute_list()] + attr_list = self.attribute_list_query_ex() + attr_cache = {} + for attr in attr_list: + name = attr.name.lower() + enum_class = None + if attr.data_type == CmdArgType.DevEnum and attr.enum_labels: + labels = StdStringVector_2_seq(attr.enum_labels) + enum_class = enum.IntEnum(attr.name, labels, start=0) + attr_cache[name] = (attr.name, enum_class, ) self.__dict__['__attr_cache'] = attr_cache @@ -233,6 +242,15 @@ def f(*args, **kwds): return f +def __get_attribute_value(self, attr_info, name): + _, enum_class = attr_info + attr_value = self.read_attribute(name).value + if enum_class: + return enum_class(attr_value) + else: + return attr_value + + def __DeviceProxy__getattr(self, name): # trait_names is a feature of IPython. Hopefully they will solve # ticket http://ipython.scipy.org/ipython/ipython/ticket/229 someday @@ -246,8 +264,9 @@ def __DeviceProxy__getattr(self, name): if cmd_info: return __get_command_func(self, cmd_info, name) - if name_l in self.__get_attr_cache(): - return self.read_attribute(name).value + attr_info = self.__get_attr_cache().get(name_l) + if attr_info: + return __get_attribute_value(self, attr_info, name) if name_l in self.__get_pipe_cache(): return self.read_pipe(name) @@ -266,8 +285,9 @@ def __DeviceProxy__getattr(self, name): except: pass - if name_l in self.__get_attr_cache(): - return self.read_attribute(name).value + attr_info = self.__get_attr_cache().get(name_l) + if attr_info: + return __get_attribute_value(self, attr_info, name) try: self.__refresh_pipe_cache() diff --git a/tango/test_utils.py b/tango/test_utils.py index 3f439332..d0471088 100644 --- a/tango/test_utils.py +++ b/tango/test_utils.py @@ -1,5 +1,7 @@ """Test utilities""" +import enum + # Local imports from . import DevState, GreenMode from .server import Device @@ -26,6 +28,32 @@ def init_device(self): self.set_state(DevState.ON) +# Test enums + +class GoodEnum(enum.IntEnum): + START = 0 + MIDDLE = 1 + END = 2 + + +class BadEnumNonZero(enum.IntEnum): + START = 1 + MIDDLE = 2 + END = 3 + + +class BadEnumSkipValues(enum.IntEnum): + START = 0 + MIDDLE = 2 + END = 4 + + +class BadEnumDuplicates(enum.IntEnum): + START = 0 + MIDDLE = 1 + END = 1 + + # Helpers TYPED_VALUES = { diff --git a/tango/utils.py b/tango/utils.py index bae8b6f2..7feb1fc5 100644 --- a/tango/utils.py +++ b/tango/utils.py @@ -22,6 +22,7 @@ import types import numbers import collections +import enum from ._tango import StdStringVector, StdDoubleVector, \ DbData, DbDevInfos, DbDevExportInfos, CmdArgType, AttrDataFormat, \ @@ -417,6 +418,53 @@ def get_tango_type(obj): return __get_tango_type(obj) +class EnumTypeError(Exception): + """Invalid Enum class for use with DEV_ENUM.""" + + +def get_enum_labels(enum_cls): + """ + Return list of enumeration labels from Enum class. + + The list is useful when creating an attribute, for the + `enum_labels` parameter. The enumeration values are checked + to ensure they are unique, start at zero, and increment by one. + + :param enum_cls: the Enum class to be inspected + :type enum_cls: :py:obj:`enum.Enum` + + :return: List of label strings + :rtype: :py:obj:`list` + + :raises EnumTypeError: in case the given class is invalid + """ + if not issubclass(enum_cls, enum.Enum): + raise EnumTypeError("Input class '%s' must be derived from enum.Enum" + % enum_cls) + + # Check there are no duplicate labels + try: + enum.unique(enum_cls) + except ValueError as exc: + raise EnumTypeError("Input class '%s' must be unique - %s" + % (enum_cls, exc)) + + # Check the values start at 0, and increment by 1, since that is + # assumed by tango's DEV_ENUM implementation. + values = [member.value for member in enum_cls] + if not values: + raise EnumTypeError("Input class '%s' has no members!" % enum_cls) + expected_value = 0 + for value in values: + if value != expected_value: + raise EnumTypeError("Enum values for '%s' must start at 0 and " + "increment by 1. Values: %s" + % (enum_cls, values)) + expected_value += 1 + + return [member.name for member in enum_cls] + + def is_pure_str(obj): """ Tells if the given object is a python string. diff --git a/tests/test_server.py b/tests/test_server.py index 42d96055..d8a3dc92 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -3,11 +3,15 @@ import sys import textwrap import pytest +import enum from tango import DevState, AttrWriteType, GreenMode, DevFailed from tango.server import Device from tango.server import command, attribute, device_property -from tango.test_utils import DeviceTestContext, assert_close +from tango.test_utils import DeviceTestContext, assert_close, \ + GoodEnum, BadEnumNonZero, BadEnumSkipValues, BadEnumDuplicates +from tango.utils import get_enum_labels, EnumTypeError + # Asyncio imports try: @@ -140,6 +144,33 @@ def attr(self, value): assert_close(proxy.attr, value) +def test_read_write_attribute_enum(server_green_mode): + dtype = 'DevEnum' + values = (member.value for member in GoodEnum) + enum_labels = get_enum_labels(GoodEnum) + + class TestDevice(Device): + green_mode = server_green_mode + + @attribute(dtype=dtype, enum_labels=enum_labels, + access=AttrWriteType.READ_WRITE) + def attr(self): + return self.attr_value + + @attr.write + def attr(self, value): + self.attr_value = value + + with DeviceTestContext(TestDevice) as proxy: + for value, label in zip(values, enum_labels): + proxy.attr = value + read_attr = proxy.attr + assert read_attr == value + assert isinstance(read_attr, enum.IntEnum) + assert read_attr.value == value + assert read_attr.name == label + + # Test properties def test_device_property_no_default(typed_values, server_green_mode): @@ -303,3 +334,27 @@ def get_prop(self): with DeviceTestContext(TestDevice, process=True) as proxy: pass assert 'Device property prop is mandatory' in str(context.value) + + +# fixtures + +@pytest.fixture(params=[GoodEnum]) +def good_enum(request): + return request.param + + +@pytest.fixture(params=[BadEnumNonZero, BadEnumSkipValues, BadEnumDuplicates]) +def bad_enum(request): + return request.param + + +# test utilities for servers + +def test_get_enum_labels_success(good_enum): + expected_labels = ['START', 'MIDDLE', 'END'] + assert get_enum_labels(good_enum) == expected_labels + + +def test_get_enum_labels_fail(bad_enum): + with pytest.raises(EnumTypeError): + get_enum_labels(bad_enum)