Skip to content
This repository has been archived by the owner on Apr 7, 2022. It is now read-only.

Commit

Permalink
PR #194 Issue #188: Easier access to DevEnum attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
vxgmichel authored Jun 18, 2018
2 parents d86cf90 + b6119f0 commit 566c549
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 12 deletions.
8 changes: 8 additions & 0 deletions doc/data_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)**

Expand Down
2 changes: 2 additions & 0 deletions doc/utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion examples/Clock/ClockDS.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- time: read-only scalar float
- gmtime: read-only sequence (spectrum) of integers
- noon: read-only enumerated type
commands:
Expand All @@ -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):
Expand All @@ -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):
"""
Expand Down
8 changes: 4 additions & 4 deletions examples/Clock/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))




1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ def setup_args():

install_requires = [
'six',
'enum34;python_version<"3.4"',
]

setup_requires = []
Expand Down
32 changes: 26 additions & 6 deletions tango/device_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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


Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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()
Expand Down
28 changes: 28 additions & 0 deletions tango/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Test utilities"""

import enum

# Local imports
from . import DevState, GreenMode
from .server import Device
Expand All @@ -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 = {
Expand Down
48 changes: 48 additions & 0 deletions tango/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import types
import numbers
import collections
import enum

from ._tango import StdStringVector, StdDoubleVector, \
DbData, DbDevInfos, DbDevExportInfos, CmdArgType, AttrDataFormat, \
Expand Down Expand Up @@ -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.
Expand Down
57 changes: 56 additions & 1 deletion tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

0 comments on commit 566c549

Please sign in to comment.