Skip to content

Commit

Permalink
Use Enum types for DevEnum attributes
Browse files Browse the repository at this point in the history
TANGO implements DevEnum attributes as an integer value combined
with a list label strings.  The current PyTango implementation
requires the user to fetch the labels separately from the values,
and match them up manually.  This makes it difficult to use.  This new
implementation combines the information to create proper enumerated
types.

#### Client-side changes
When the DeviceProxy reads a DevEnum attribute, a Python
`enum.IntEnum` object is returned instead of just an integer.  This
object can be compared to integers directly, so minimal changes are
expected for old code.  The benefit is that new code using the
enumerations directly will be more readable.

#### Server-side changes
Not much was required here.  Added a utility function that extracts the
labels from an `enum.Enum` class and verifies that the values will work
with the core TANGO implementation.

#### Warning
The `DeviceProxy` maintains a cache of the attributes, which
includes the Enumeration class.  If the device server changes the
enum_labels for an attribute that the DeviceProxy has already cached,
then the DeviceProxy will not know about it.  A new instance of the
DeviceProxy will have to be created after any significant interface
change on the server.

Addresses issue tango-controls#188
  • Loading branch information
ajoubertza committed Jun 15, 2018
1 parent d86cf90 commit 5f3a4fd
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 11 deletions.
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
16 changes: 15 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,12 @@ 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())
result = Noon.AM if time_struct.tm_hour < 12 else Noon.PM
return int(result)

@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
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

0 comments on commit 5f3a4fd

Please sign in to comment.