rework EnumType to use better Enum's

unfortunately IntEnum can't be bent like we would need it (extensible).
So we had to write our own....

The members of the Enum still behave like ints, but also have
.name and .value attributes, should they be needed.

needed adoptions to correctly use (and test) the EnumType are included.

Change-Id: Ie019d2f449a244c4fab00554b6c6daaac8948b59
Reviewed-on: https://forge.frm2.tum.de/review/17843
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
Enrico Faulhaber 2018-04-26 16:29:09 +02:00
parent 927ca854a2
commit 574a66c65b
15 changed files with 644 additions and 298 deletions

View File

@ -41,8 +41,8 @@ except ImportError:
import mlzlog
from secop.datatypes import get_datatype
from secop.lib import mkthread, formatException
from secop.datatypes import get_datatype, EnumType
from secop.lib import mkthread, formatException, formatExtendedStack
from secop.lib.parsing import parse_time, format_time
#from secop.protocol.encoding import ENCODERS
#from secop.protocol.framing import FRAMERS
@ -226,6 +226,7 @@ class Client(object):
try:
self._inner_run()
except Exception as err:
print(formatExtendedStack())
self.log.exception(err)
raise
@ -383,6 +384,11 @@ class Client(object):
for module, moduleData in self.describing_data['modules'].items():
for parameter, parameterData in moduleData['parameters'].items():
datatype = get_datatype(parameterData['datatype'])
# *sigh* special handling for 'some' parameters....
if isinstance(datatype, EnumType):
datatype._enum.name = parameter
if parameter == 'status':
datatype.subtypes[0]._enum.name = 'status'
self.describing_data['modules'][module]['parameters'] \
[parameter]['datatype'] = datatype
for _cmdname, cmdData in moduleData['commands'].items():

View File

@ -21,17 +21,21 @@
# *****************************************************************************
"""Define validated data types."""
from __future__ import print_function
try:
# py2
unicode(u'')
unicode
except NameError:
# py3
unicode = str # pylint: disable=redefined-builtin
from base64 import b64encode, b64decode
from .errors import ProgrammingError, ParsingError
from .parse import Parser
from secop.lib.enum import Enum
from secop.errors import ProgrammingError, ParsingError
from secop.parse import Parser
Parser = Parser()
@ -181,64 +185,33 @@ class IntRange(DataType):
class EnumType(DataType):
as_json = [u'enum']
def __init__(self, enum_or_name='', **kwds):
self._enum = Enum(enum_or_name, **kwds)
def __init__(self, *args, **kwds):
# enum keys are ints! remember mapping from intvalue to 'name'
self.entries = {} # maps ints to strings
num = 0
for arg in args:
if not isinstance(arg, (str, unicode)):
raise ValueError(u'EnumType entries MUST be strings!')
self.entries[num] = arg
num += 1
for k, v in list(kwds.items()):
v = int(v)
if v in self.entries:
raise ValueError(
u'keyword argument %r=%d is already assigned %r' %
(k, v, self.entries[v]))
self.entries[v] = unicode(k)
# if len(self.entries) == 0:
# raise ValueError('Empty enums ae not allowed!')
# also keep a mapping from name strings to numbers
self.reversed = {} # maps Strings to ints
for k, v in self.entries.items():
if v in self.reversed:
raise ValueError(u'Mapping for %r=%r is not Unique!' % (v, k))
self.reversed[v] = k
self.as_json = [u'enum', self.reversed.copy()]
@property
def as_json(self):
return [u'enum'] + [dict((m.name, m.value) for m in self._enum.members)]
def __repr__(self):
return u'EnumType(%s)' % u', '.join(
[u'%s=%d' % (v, k) for k, v in list(self.entries.items())])
return "EnumType(%r, %s" % (self._enum.name, ', '.join('%s=%d' %(m.name, m.value) for m in self._enum.members))
def export_value(self, value):
"""returns a python object fit for serialisation"""
if value in self.reversed:
return self.reversed[value]
if int(value) in self.entries:
return int(value)
raise ValueError(u'%r is not one of %s' %
(unicode(value), u', '.join(list(self.reversed.keys()))))
return int(self.validate(value))
def import_value(self, value):
"""returns a python object from serialisation"""
# internally we store the key (which is a string)
return self.entries[int(value)]
return self.validate(value)
def validate(self, value):
"""return the validated (internal) value or raise"""
if value in self.reversed:
return self.reversed[value]
if int(value) in self.entries:
return int(value)
raise ValueError(u'%r is not one of %s' %
(unicode(value), u', '.join(map(unicode, self.entries))))
try:
return self._enum[value]
except KeyError:
raise ValueError('%r is not a member of enum %r' % (value, self._enum))
def from_string(self, text):
value = text
return self.validate(value)
return self.validate(text)
class BLOBType(DataType):
@ -606,7 +579,7 @@ DATATYPES = dict(
string=lambda _max=None, _min=0: StringType(_max, _min),
array=lambda subtype, _max=None, _min=0: ArrayOf(get_datatype(subtype), _max, _min),
tuple=lambda subtypes: TupleOf(*map(get_datatype, subtypes)),
enum=lambda kwds: EnumType(**kwds),
enum=lambda kwds: EnumType('', **kwds),
struct=lambda named_subtypes: StructOf(
**dict((n, get_datatype(t)) for n, t in list(named_subtypes.items()))),
command=Command,

View File

@ -211,12 +211,11 @@ class ReadableWidget(QWidget):
if self._is_enum:
self._map = {} # maps QT-idx to name/value
self._revmap = {} # maps value/name to QT-idx
for idx, (val, name) in enumerate(
sorted(datatype.entries.items())):
self._map[idx] = (name, val)
self._revmap[name] = idx
self._revmap[val] = idx
self.targetComboBox.addItem(name, val)
for idx, member in enumerate(datatype._enum.members):
self._map[idx] = member
self._revmap[member.name] = idx
self._revmap[member.value] = idx
self.targetComboBox.addItem(member.name, member.value)
self._init_status_widgets()
self._init_current_widgets()
@ -298,7 +297,8 @@ class DrivableWidget(ReadableWidget):
def update_current(self, value, qualifiers=None):
if self._is_enum:
self.currentLineEdit.setText(self._map[self._revmap[value]][0])
member = self._map[self._revmap[value]]
self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value))
else:
self.currentLineEdit.setText(str(value))
@ -333,5 +333,5 @@ class DrivableWidget(ReadableWidget):
self.target_go(self.targetLineEdit.text())
@pyqtSlot(unicode)
def on_targetComboBox_activated(self, stuff):
self.target_go(stuff)
def on_targetComboBox_activated(self, selection):
self.target_go(selection)

View File

@ -21,6 +21,8 @@
#
# *****************************************************************************
from __future__ import print_function
try:
# py2
unicode(u'')
@ -32,6 +34,7 @@ from secop.gui.qt import QWidget, QLabel, QPushButton as QButton, QLineEdit, \
from secop.gui.util import loadUi
from secop.datatypes import EnumType
from secop.lib import formatExtendedStack
class ParameterWidget(QWidget):
@ -94,15 +97,12 @@ class EnumParameterWidget(GenericParameterWidget):
loadUi(self, 'parambuttons_select.ui')
# transfer allowed settings from datatype to comboBoxes
self._map = {} # maps index to enumstring
self._revmap = {} # maps enumstring to index
index = 0
for enumval, enumname in sorted(self._datatype.entries.items()):
self.setComboBox.addItem(enumname, enumval)
self._map[index] = (enumval, enumname)
self._revmap[enumname] = index
self._revmap[enumval] = index
index += 1
self._map = {} # maps index to EnumMember
self._revmap = {} # maps Enum.name + Enum.value to index
for index, member in enumerate(self._datatype._enum.members):
self.setComboBox.addItem(member.name, member.value)
self._map[index] = member
self._revmap[member.name] = self._revmap[member.value] = index
if self._readonly:
self.setLabel.setEnabled(False)
self.setComboBox.setEnabled(False)
@ -115,19 +115,16 @@ class EnumParameterWidget(GenericParameterWidget):
@pyqtSlot()
def on_setPushButton_clicked(self):
_enumval, enumname = self._map[self.setComboBox.currentIndex()]
self.setRequested.emit(self._module, self._paramcmd, enumname)
member = self._map[self.setComboBox.currentIndex()]
self.setRequested.emit(self._module, self._paramcmd, member)
def updateValue(self, value):
try:
value = int(value)
except ValueError:
pass
if value in self._revmap:
index = self._revmap[value]
self.currentLineEdit.setText('(%d): %s' % self._map[index])
else:
member = self._map[self._revmap[int(value)]]
self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value))
except Exception:
self.currentLineEdit.setText('undefined Value: %r' % value)
print(formatExtendedStack())
class GenericCmdWidget(ParameterWidget):

View File

@ -62,16 +62,14 @@ class EnumWidget(QComboBox):
self._map = {}
self._revmap = {}
for idx, (val, name) in enumerate(datatype.entries.items()):
self._map[idx] = (name, val)
self._revmap[name] = idx
self._revmap[val] = idx
self.addItem(name, val)
# XXX: fill Combobox from datatype
for idx, member in enumerate(datatype._enum.members):
self._map[idx] = member
self._revmap[member.name] = idx
self._revmap[member.value] = idx
self.addItem(member.name, member.value)
def get_value(self):
# XXX: return integer corresponding to the selected item
return self._map[self.currentIndex()][1]
return self._map[self.currentIndex()].value
def set_value(self, value):
self.setCurrentIndex(self._revmap[value])

View File

@ -49,6 +49,8 @@ CONFIG = {
}
unset_value = object()
class lazy_property(object):
"""A property that calculates its value only once."""

306
secop/lib/enum.py Executable file
View File

@ -0,0 +1,306 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
# Copyright (c) 2015-2016 by the authors, see LICENSE
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Enum class"""
__ALL__ = ['Enum']
try:
text_type = unicode # Py2
except NameError:
text_type = str # Py3
class EnumMember(object):
"""represents one member of an Enum
has an int-type value and attributes 'name' and 'value'
"""
__slots__ = ['name', 'value', 'enum']
def __init__(self, enum, name, value):
if not isinstance(enum, Enum):
raise TypeError('1st Argument must be an instance of class Enum()')
self.value = int(value)
self.enum = enum
self.name = name or 'unnamed'
# to behave like an int for comparisons
def __cmp__(self, other):
if isinstance(other, EnumMember):
other = other.value
if isinstance(other, (str, unicode)):
if other in self.enum:
other = self.enum[other].value
try:
other = int(other)
except Exception:
#raise TypeError('%r can not be compared to %r!' %(other, self))
return -1 # XXX:!
if self.value < other:
return -1
elif self.value > other:
return 1
return 0
def __lt__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == -1
def __le__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) < 1
def __eq__(self, other):
if isinstance(other, (EnumMember)):
return other.value == self.value
if isinstance(other, (int, long)):
return other == self.value
# compare by name (for (in)equality only)
if isinstance(other, (str, unicode)):
if other in self.enum:
return self.name == other
return False
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 0
def __ne__(self, other):
return not self.__eq__(other)
def __ge__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) > -1
def __gt__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 1
# to be useful in indexing
def __hash__(self):
return self.value.__hash__()
# be read-only (except during initialization)
def __setattr__(self, key, value):
if key in self.__slots__ and not getattr(self, 'name', None):
return object.__setattr__(self, key, value)
raise TypeError('Modifying EnumMember\'s is not allowed!')
# allow access to other EnumMembers (via the Enum)
def __getattr__(self, key):
enum = object.__getattribute__(self, 'enum')
if key in enum:
return enum[key]
return object.__getattribute__(self, key)
# be human readable (for debugging)
def __repr__(self):
return '<%s.%s (%d)>' % (self.enum.name, self.name, self.value)
# numeric operations: delegate to int. Do we really need any of those?
def __add__(self, other):
return self.value.__add__(other.value if isinstance(other, EnumMember) else other)
def __sub__(self, other):
return self.value.__sub__(other.value if isinstance(other, EnumMember) else other)
def __mul__(self, other):
return self.value.__mul__(other.value if isinstance(other, EnumMember) else other)
def __div__(self, other):
return self.value.__div__(other.value if isinstance(other, EnumMember) else other)
def __truediv__(self, other):
return self.value.__truediv__(other.value if isinstance(other, EnumMember) else other)
def __floordiv__(self, other):
return self.value.__floordiv__(other.value if isinstance(other, EnumMember) else other)
def __mod__(self, other):
return self.value.__mod__(other.value if isinstance(other, EnumMember) else other)
def __divmod__(self, other):
return self.value.__divmod__(other.value if isinstance(other, EnumMember) else other)
def __pow__(self, other, *args):
return self.value.__pow__(other, *args)
def __lshift__(self, other):
return self.value.__lshift__(other.value if isinstance(other, EnumMember) else other)
def __rshift__(self, other):
return self.value.__rshift__(other.value if isinstance(other, EnumMember) else other)
def __radd__(self, other):
return self.value.__radd__(other.value if isinstance(other, EnumMember) else other)
def __rsub__(self, other):
return self.value.__rsub__(other.value if isinstance(other, EnumMember) else other)
def __rmul__(self, other):
return self.value.__rmul__(other.value if isinstance(other, EnumMember) else other)
def __rdiv__(self, other):
return self.value.__rdiv__(other.value if isinstance(other, EnumMember) else other)
def __rtruediv__(self, other):
return self.value.__rtruediv__(other.value if isinstance(other, EnumMember) else other)
def __rfloordiv__(self, other):
return self.value.__rfloordiv__(other.value if isinstance(other, EnumMember) else other)
def __rmod__(self, other):
return self.value.__rmod__(other.value if isinstance(other, EnumMember) else other)
def __rdivmod__(self, other):
return self.value.__rdivmod__(other.value if isinstance(other, EnumMember) else other)
def __rpow__(self, other, *args):
return self.value.__rpow__(other, *args)
def __rlshift__(self, other):
return self.value.__rlshift__(other.value if isinstance(other, EnumMember) else other)
def __rrshift__(self, other):
return self.value.__rrshift__(other.value if isinstance(other, EnumMember) else other)
# logical operations
def __and__(self, other):
return self.value.__and__(other.value if isinstance(other, EnumMember) else other)
def __xor__(self, other):
return self.value.__xor__(other.value if isinstance(other, EnumMember) else other)
def __or__(self, other):
return self.value.__or__(other.value if isinstance(other, EnumMember) else other)
def __rand__(self, other):
return self.value.__rand__(other.value if isinstance(other, EnumMember) else other)
def __rxor__(self, other):
return self.value.__rxor__(other.value if isinstance(other, EnumMember) else other)
def __ror__(self, other):
return self.value.__ror__(other.value if isinstance(other, EnumMember) else other)
# other stuff
def __neg__(self):
return self.value.__neg__()
def __pos__(self):
return self.value.__pos__()
def __abs__(self):
return self.value.__abs__()
def __invert__(self):
return self.value.__invert__()
def __int__(self):
return self.value.__int__()
def __long__(self):
return self.value.__long__()
def __float__(self):
return self.value.__float__()
#return NotImplemented # makes no sense
def __oct__(self):
return self.value.__oct__()
def __hex__(self):
return self.value.__hex__()
def __index__(self):
return self.value.__index__()
# note: we do not implement the __i*__ methods as they modify our value
# inplace and we want to have a const
def __forbidden__(self, *args):
raise TypeError('Operation is forbidden!')
__iadd__ = __isub__ = __imul__ = __idiv__ = __itruediv__ = __ifloordiv__ = \
__imod__ = __ipow__ = __ilshift__ = __irshift__ = __iand__ = \
__ixor__ = __ior__ = __forbidden__
class Enum(dict):
"""The Enum class
use instance of this like this:
>>> status = Enum('status', idle=1, busy=2, error=3)
you may create an extended Enum:
>>> moveable_status = Enum(status, alarm=5)
>>> yet_another_enum = Enum('X', dict(a=1, b=2), c=3)
last example 'extends' the definition given by the dict with c=3.
accessing the members:
>>> status['idle'] == status.idle == status('idle')
>>> status[1] == status.idle == status(1)
Each member can be used like an int, so:
>>> status.idle == 1 is True
>>> status.error +5
You can neither modify members nor Enums.
You only can create an extended Enum.
"""
name = ''
def __init__(self, name='', parent=None, **kwds):
super(Enum, self).__init__()
if isinstance(name, (dict, Enum)) and parent is None:
# swap if only parent is given as positional argument
name, parent = '', name
# parent may be dict, or Enum....
if not name:
if isinstance(parent, Enum):
# if name was not given, use that of the parent
# this means, an extended Enum behaves like the parent
# THIS MAY BE CONFUSING SOMETIMES!
name=parent.name
# else:
# raise TypeError('Enum instances need a name or an Enum parent!')
if not isinstance(name, (str, text_type)):
raise TypeError('1st argument to Enum must be a name or an Enum!')
names = set()
values = set()
# pylint: disable=dangerous-default-value
def add(self, k, v, names = names, value = values):
"""helper for creating the enum members"""
if v is None:
# sugar: take the next free number if value was None
v = max(values or [0]) + 1
# sugar: if value is a name of another member,
# auto-assign the smallest free number which is bigger
# then that assigned to that name
if v in names:
v = self[v].value
while v in values:
v +=1
# check that the value is an int
_v = int(v)
if _v != v:
raise TypeError('Values must be integers!')
v = _v
# check for duplicates
if k in names:
raise TypeError('duplicate name %r' % k)
if v in values:
raise TypeError('duplicate value %d (key=%r)' % (v, k))
# remember it
self[v] = self[k] = EnumMember(self, k, v)
names.add(k)
values.add(v)
if isinstance(parent, Enum):
for m in parent.members:
add(self, m.name, m.value)
elif isinstance(parent, dict):
for k, v in parent.items():
add(self, k, v)
elif parent != None:
raise TypeError('parent (if given) MUST be a dict or an Enum!')
for k, v in kwds.items():
add(self, k, v)
self.members = tuple(sorted(self[n] for n in names))
self.name = name
def __getattr__(self, key):
return self[key]
def __setattr__(self, key, value):
if self.name:
raise TypeError('Enum %r can not be changed!' % self.name)
super(Enum, self).__setattr__(key, value)
def __setitem__(self, key, value):
if self.name:
raise TypeError('Enum %r can not be changed!' % self.name)
super(Enum, self).__setitem__(key, value)
def __delitem__(self, key):
raise TypeError('Enum %r can not be changed!' % self.name)
def __repr__(self):
return '<Enum %r (%d values)>' % (self.name, len(self)/2)
def __call__(self, key):
return self[key]

View File

@ -52,10 +52,10 @@ except ImportError:
return wrapper
from secop.lib import formatExtendedStack, mkthread
from secop.lib import formatExtendedStack, mkthread, unset_value
from secop.lib.enum import Enum
from secop.lib.parsing import format_time
from secop.errors import ConfigError, ProgrammingError
from secop.protocol import status
from secop.datatypes import DataType, EnumType, TupleOf, StringType, FloatRange, get_datatype
@ -81,12 +81,14 @@ class Param(object):
def __init__(self,
description,
datatype=None,
default=Ellipsis,
unit=None,
default=unset_value,
unit='',
readonly=True,
export=True,
group='',
poll=False):
poll=False,
value=unset_value,
timestamp=0):
if not isinstance(datatype, DataType):
if issubclass(datatype, DataType):
# goodie: make an instance from a class (forgotten ()???)
@ -115,15 +117,7 @@ class Param(object):
def copy(self):
# return a copy of ourselfs
return Param(description=self.description,
datatype=self.datatype,
default=self.default,
unit=self.unit,
readonly=self.readonly,
export=self.export,
group=self.group,
poll=self.poll,
)
return Param(**self.__dict__)
def as_dict(self, static_only=False):
# used for serialisation only
@ -147,7 +141,10 @@ class Param(object):
class Override(object):
"""Stores the overrides to ba applied to a Param
note: overrides are applied by the metaclass during class creating
"""
def __init__(self, **kwds):
self.kwds = kwds
@ -167,9 +164,9 @@ class Override(object):
paramobj)
# storage for Commands settings (description + call signature...)
class Command(object):
"""storage for Commands settings (description + call signature...)
"""
def __init__(self, description, arguments=None, result=None):
# descriptive text for humans
self.description = description
@ -195,7 +192,14 @@ class Command(object):
# warning: MAGIC!
class ModuleMeta(type):
"""Metaclass
joining the class's properties, parameters and commands dicts with
those of base classes.
also creates getters/setter for parameter access
and wraps read_*/write_* methods
(so the dispatcher will get notfied of changed values)
"""
def __new__(mcs, name, bases, attrs):
newtype = type.__new__(mcs, name, bases, attrs)
if '__constructed__' in attrs:
@ -219,6 +223,11 @@ class ModuleMeta(type):
for n, o in attrs.get('overrides', {}).items():
newparams[n] = o.apply(newparams[n].copy())
# Check naming of EnumType
for k, v in newparams.items():
if isinstance(v.datatype, EnumType) and not v.datatype._enum.name:
v.datatype._enum.name = k
# check validity of Param entries
for pname, pobj in newtype.parameters.items():
# XXX: allow dicts for overriding certain aspects only.
@ -284,7 +293,7 @@ class ModuleMeta(type):
pobj = self.parameters[pname]
value = pobj.datatype.validate(value)
pobj.timestamp = time.time()
if not EVENT_ONLY_ON_CHANGED_VALUES or (value != pobj.value):
if (not EVENT_ONLY_ON_CHANGED_VALUES) or (value != pobj.value):
pobj.value = value
# also send notification
if self.parameters[pname].export:
@ -311,52 +320,61 @@ class ModuleMeta(type):
return newtype
# Basic module class
#
# within Modules, parameters should only be addressed as self.<pname>
# i.e. self.value, self.target etc...
# these are accesses to the cached version.
# they can also be written to
# (which auto-calls self.write_<pname> and generate an async update)
# if you want to 'update from the hardware', call self.read_<pname>
# the return value of this method will be used as the new cached value and
# be returned.
@add_metaclass(ModuleMeta)
class Module(object):
"""Basic Module, doesn't do much"""
"""Basic Module
ALL secop Modules derive from this
note: within Modules, parameters should only be addressed as self.<pname>
i.e. self.value, self.target etc...
these are accessing the cached version.
they can also be written to (which auto-calls self.write_<pname> and
generate an async update)
if you want to 'update from the hardware', call self.read_<pname>() instead
the return value of this method will be used as the new cached value and
be an async update sent automatically.
"""
# static properties, definitions in derived classes should overwrite earlier ones.
# how to configure some stuff which makes sense to take from configfile???
# note: properties don't change after startup and are usually filled
# with data from a cfg file...
# note: so far all properties are STRINGS
# note: only the properties defined here are allowed to be set in the cfg file
properties = {
'group': None, # some Modules may be grouped together
'description': "Short description of this Module class and its functionality.",
'meaning': None, # XXX: ???
'priority': None, # XXX: ???
'visibility': None, # XXX: ????
'description': "The manufacturer forgot to set a meaningful description. please nag him!",
# what else?
}
# parameter and commands are auto-merged upon subclassing
# parameters = {
# 'description': Param('short description of this module and its function', datatype=StringType(), default='no specified'),
# }
# properties, parameter and commands are auto-merged upon subclassing
parameters = {}
commands = {}
# reference to the dispatcher (used for sending async updates)
DISPATCHER = None
def __init__(self, logger, cfgdict, devname, dispatcher):
def __init__(self, logger, cfgdict, modname, dispatcher):
# remember the dispatcher object (for the async callbacks)
self.DISPATCHER = dispatcher
self.log = logger
self.name = devname
# make local copies of parameter
self.name = modname
# make local copies of parameter objects
# they need to be individual per instance since we use them also
# to cache the current value + qualifiers...
params = {}
for k, v in list(self.parameters.items()):
params[k] = v.copy()
# do not re-use self.parameters as this is the same for all instances
self.parameters = params
# make local copies of properties
props = {}
for k, v in list(self.properties.items()):
props[k] = v
self.properties = props
# check and apply properties specified in cfgdict
@ -365,21 +383,21 @@ class Module(object):
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
if k[0] == '.':
if k[1:] in self.properties:
self.properties[k[1:]] = v
del cfgdict[k]
self.properties[k[1:]] = cfgdict.pop(k)
else:
raise ConfigError('Module %r has no property %r' %
(self.name, k[1:]))
# remove unset (default) module properties
for k, v in list(self.properties.items()): # keep list() as dict may change during iter
if v is None:
del self.properties[k]
# derive automatic properties
# MAGIC: derive automatic properties
mycls = self.__class__
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
self.properties['_implementation'] = myclassname
self.properties['interface_class'] = [
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
#self.properties['interface'] = self.properties['interfaces'][0]
# remove unset (default) module properties
for k, v in list(self.properties.items()): # keep list() as dict may change during iter
if v is None:
del self.properties[k]
# check and apply parameter_properties
# specified as '<paramname>.<propertyname> = <propertyvalue>'
@ -391,11 +409,10 @@ class Module(object):
if propname == 'datatype':
paramobj.datatype = get_datatype(cfgdict.pop(k))
elif hasattr(paramobj, propname):
setattr(paramobj, propname, v)
del cfgdict[k]
setattr(paramobj, propname, cfgdict.pop(k))
# check config for problems
# only accept config items specified in parameters
# only accept remaining config items specified in parameters
for k, v in cfgdict.items():
if k not in self.parameters:
raise ConfigError(
@ -407,8 +424,8 @@ class Module(object):
# is not specified in cfgdict
for k, v in self.parameters.items():
if k not in cfgdict:
if v.default is Ellipsis and k != 'value':
# Ellipsis is the one single value you can not specify....
if v.default is unset_value and k != 'value':
# unset_value is the one single value you can not specify....
raise ConfigError('Module %s: Parameter %r has no default '
'value and was not given in config!' %
(self.name, k))
@ -416,7 +433,7 @@ class Module(object):
cfgdict[k] = v.default
# replace CLASS level Param objects with INSTANCE level ones
self.parameters[k] = self.parameters[k].copy()
# self.parameters[k] = self.parameters[k].copy() # already done above...
# now 'apply' config:
# pass values through the datatypes and store as attributes
@ -425,15 +442,15 @@ class Module(object):
continue
# apply datatype, complain if type does not fit
datatype = self.parameters[k].datatype
if datatype is not None:
# only check if datatype given
try:
v = datatype.validate(v)
except (ValueError, TypeError):
self.log.exception(formatExtendedStack())
raise
try:
v = datatype.validate(v)
except (ValueError, TypeError):
self.log.exception(formatExtendedStack())
raise
# raise ConfigError('Module %s: config parameter %r:\n%r' %
# (self.name, k, e))
# note: this will call write_* methods which will
# write to the hardware, if possible!
setattr(self, k, v)
def init(self):
@ -450,23 +467,26 @@ class Readable(Module):
"""Basic readable Module
providing the readonly parameter 'value' and 'status'
Also allow configurable polling per 'pollinterval' parameter.
"""
# pylint: disable=invalid-name
Status = Enum('Status',
IDLE = 100,
WARN = 200,
UNSTABLE = 250,
ERROR = 400,
UNKNOWN = 900,
)
parameters = {
'value': Param('current value of the Module', readonly=True, default=0.,
datatype=FloatRange(), unit='', poll=True),
'pollinterval': Param('sleeptime between polls', default=5,
readonly=False, datatype=FloatRange(0.1, 120), ),
'status': Param('current status of the Module', default=(status.OK, ''),
datatype=TupleOf(
EnumType(**{
'IDLE': status.OK,
'BUSY': status.BUSY,
'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN
}), StringType()),
readonly=True, poll=True),
'status': Param('current status of the Module',
default=(Status.IDLE, ''),
datatype=TupleOf(EnumType(Status), StringType()),
readonly=True, poll=True),
}
def init(self):
@ -530,11 +550,16 @@ class Drivable(Writable):
Also status gets extended with a BUSY state indicating a running action.
"""
Status = Enum(Readable.Status, BUSY=300)
overrides = {
'status' : Override(datatype=TupleOf(EnumType(Status), StringType())),
}
# improved polling: may poll faster if module is BUSY
def poll(self, nr=0):
# poll status first
stat = self.read_status(0)
fastpoll = stat[0] == status.BUSY
fastpoll = stat[0] == self.Status.BUSY
for pname, pobj in self.parameters.items():
if not pobj.poll:
continue

View File

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Define Status constants"""
# could also be some objects
OK = 100
WARN = 200
UNSTABLE = 250
BUSY = 300
ERROR = 400
UNKNOWN = -1
#OK = 'idle'
#BUSY = 'busy'
#WARN = 'alarm'
#UNSTABLE = 'unstable'
#ERROR = 'ERROR'
#UNKNOWN = 'unknown'

View File

@ -25,7 +25,6 @@ import time
import random
from secop.modules import Drivable, Command, Param
from secop.protocol import status
from secop.datatypes import FloatRange, EnumType, TupleOf
from secop.lib import clamp, mkthread
@ -100,7 +99,7 @@ class Cryostat(CryoBase):
group='pid',
),
mode=Param("mode of regulation",
datatype=EnumType('ramp', 'pid', 'openloop'),
datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
default='ramp',
readonly=False,
),
@ -153,7 +152,7 @@ class Cryostat(CryoBase):
return value
self.target = value
# next read_status will see this status, until the loop updates it
self.status = status.BUSY, 'new target set'
self.status = self.Status.BUSY, 'new target set'
return value
def read_maxpower(self, maxage=0):
@ -209,13 +208,13 @@ class Cryostat(CryoBase):
def thread(self):
self.sampletemp = self.T_start
self.regulationtemp = self.T_start
self.status = status.OK, ''
self.status = self.Status.IDLE, ''
while not self._stopflag:
try:
self.__sim()
except Exception as e:
self.log.exception(e)
self.status = status.ERROR, str(e)
self.status = self.Status.ERROR, str(e)
def __sim(self):
# complex thread handling:
@ -264,7 +263,7 @@ class Cryostat(CryoBase):
# b) see
# http://brettbeauregard.com/blog/2011/04/
# improving-the-beginners-pid-introduction/
if self.mode != 'openloop':
if self.mode != self.mode.openloop:
# fix artefacts due to too big timesteps
# actually i would prefer reducing looptime, but i have no
# good idea on when to increase it back again
@ -328,7 +327,7 @@ class Cryostat(CryoBase):
lastmode = self.mode
# c)
if self.setpoint != self.target:
if self.ramp == 0:
if self.ramp == 0 or self.mode == self.mode.enum.pid:
maxdelta = 10000
else:
maxdelta = self.ramp / 60. * h
@ -354,12 +353,12 @@ class Cryostat(CryoBase):
if abs(_T - self.target) > deviation:
deviation = abs(_T - self.target)
if (len(window) < 3) or deviation > self.tolerance:
self.status = status.BUSY, 'unstable'
self.status = self.Status.BUSY, 'unstable'
elif self.setpoint == self.target:
self.status = status.OK, 'at target'
self.status = self.Status.IDLE, 'at target'
damper -= (damper - 1) * 0.1 # max value for damper is 11
else:
self.status = status.BUSY, 'ramping setpoint'
self.status = self.Status.BUSY, 'ramping setpoint'
damper -= (damper - 1) * 0.05
self.regulationtemp = round(regulation, 3)
self.sampletemp = round(sample, 3)

View File

@ -24,9 +24,9 @@ import time
import random
import threading
from secop.lib.enum import Enum
from secop.modules import Readable, Drivable, Param
from secop.datatypes import EnumType, FloatRange, IntRange, ArrayOf, StringType, TupleOf, StructOf, BoolType
from secop.protocol import status
class Switch(Drivable):
@ -50,9 +50,6 @@ class Switch(Drivable):
),
}
def init(self):
self._started = 0
def read_value(self, maxage=0):
# could ask HW
# we just return the value of the target here.
@ -65,7 +62,7 @@ class Switch(Drivable):
def write_target(self, value):
# could tell HW
pass
setattr(self, 'status', (self.Status.BUSY, 'switching %s' % value.name.upper()))
# note: setting self.target to the new value is done after this....
# note: we may also return the read-back value from the hw here
@ -73,8 +70,8 @@ class Switch(Drivable):
self.log.info("read status")
info = self._update()
if self.target == self.value:
return status.OK, ''
return status.BUSY, info
return self.Status.IDLE, ''
return self.Status.BUSY, info
def _update(self):
started = self.parameters['target'].timestamp
@ -90,7 +87,7 @@ class Switch(Drivable):
info = 'is switched OFF'
self.value = self.target
if info:
self.log.debug(info)
self.log.info(info)
return info
@ -119,7 +116,7 @@ class MagneticField(Drivable):
}
def init(self):
self._state = 'idle'
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
_thread = threading.Thread(target=self._thread)
_thread.daemon = True
@ -135,44 +132,45 @@ class MagneticField(Drivable):
# note: we may also return the read-back value from the hw here
def read_status(self, maxage=0):
return (status.OK, '') if self._state == 'idle' else (status.BUSY,
self._state)
if self._state == self._state.enum.idle:
return (self.Status.IDLE, '')
return (self.Status.BUSY, self._state.name)
def _thread(self):
loopdelay = 1
while True:
ts = time.time()
if self._state == 'idle':
if self._state == self._state.enum.idle:
if self.target != self.value:
self.log.debug('got new target -> switching heater on')
self._state = 'switch_on'
self._state = self._state.enum.switch_on
self._heatswitch.write_target('on')
if self._state == 'switch_on':
if self._state == self._state.enum.switch_on:
# wait until switch is on
if self._heatswitch.read_value() == 'on':
self.log.debug('heatswitch is on -> ramp to %.3f' %
self.target)
self._state = 'ramp'
if self._state == 'ramp':
self._state = self._state.enum.ramp
if self._state == self._state.enum.ramp:
if self.target == self.value:
self.log.debug('at field! mode is %r' % self.mode)
if self.mode:
self.log.debug('at field -> switching heater off')
self._state = 'switch_off'
self._state = self._state.enum.switch_off
self._heatswitch.write_target('off')
else:
self.log.debug('at field -> hold')
self._state = 'idle'
self.status = self.read_status() # push async
self._state = self._state.enum.idle
self.read_status() # push async
else:
step = self.ramp * loopdelay / 60.
step = max(min(self.target - self.value, step), -step)
self.value += step
if self._state == 'switch_off':
if self._state == self._state.enum.switch_off:
# wait until switch is off
if self._heatswitch.read_value() == 'off':
self.log.debug('heatswitch is off at %.3f' % self.value)
self._state = 'idle'
self._state = self._state.enum.idle
self.read_status() # update async
time.sleep(max(0.01, ts + loopdelay - time.time()))
self.log.error(self, 'main thread exited unexpectedly!')
@ -229,10 +227,10 @@ class SampleTemp(Drivable):
while True:
ts = time.time()
if self.value == self.target:
if self.status != status.OK:
self.status = status.OK, ''
if self.status[0] != self.Status.IDLE:
self.status = self.Status.IDLE, ''
else:
self.status = status.BUSY, 'ramping'
self.status = self.Status.BUSY, 'ramping'
step = self.ramp * loopdelay / 60.
step = max(min(self.target - self.value, step), -step)
self.value += step
@ -278,7 +276,7 @@ class Label(Readable):
mf_mode = dev_mf.mode
mf_val = dev_mf.value
mf_unit = dev_mf.parameters['value'].unit
if mf_stat[0] == status.OK:
if mf_stat[0] == self.Status.IDLE:
state = 'Persistent' if mf_mode else 'Non-persistent'
else:
state = mf_stat[1] or 'ramping'
@ -293,21 +291,24 @@ class DatatypesTest(Readable):
"""for demoing all datatypes
"""
parameters = {
'enum': Param(
'enum', datatype=EnumType(
'boo', 'faar', z=9), readonly=False, default=1), 'tupleof': Param(
'tuple of int, float and str', datatype=TupleOf(
IntRange(), FloatRange(), StringType()), readonly=False, default=(
1, 2.3, 'a')), 'arrayof': Param(
'array: 2..3 times bool', datatype=ArrayOf(
BoolType(), 2, 3), readonly=False, default=[
1, 0, 1]), 'intrange': Param(
'intrange', datatype=IntRange(
2, 9), readonly=False, default=4), 'floatrange': Param(
'floatrange', datatype=FloatRange(
-1, 1), readonly=False, default=0, ), 'struct': Param(
'struct(a=str, b=int, c=bool)', datatype=StructOf(
a=StringType(), b=IntRange(), c=BoolType()), ), }
'enum': Param('enum', datatype=EnumType(boo=None, faar=None, z=9),
readonly=False, default=1),
'tupleof': Param('tuple of int, float and str',
datatype=TupleOf(IntRange(), FloatRange(),
StringType()),
readonly=False, default=(1, 2.3, 'a')),
'arrayof': Param('array: 2..3 times bool',
datatype=ArrayOf(BoolType(), 2, 3),
readonly=False, default=[1, 0, 1]),
'intrange': Param('intrange', datatype=IntRange(2, 9),
readonly=False, default=4),
'floatrange': Param('floatrange', datatype=FloatRange(-1, 1),
readonly=False, default=0, ),
'struct': Param('struct(a=str, b=int, c=bool)',
datatype=StructOf(a=StringType(), b=IntRange(),
c=BoolType()),
),
}
class ArrayTest(Readable):

View File

@ -24,7 +24,6 @@ from __future__ import absolute_import
from secop.datatypes import EnumType, FloatRange, StringType
from secop.modules import Readable, Drivable, Param
from secop.protocol import status
try:
from pvaccess import Channel # import EPIVSv4 functionallity, PV access
@ -112,9 +111,9 @@ class EpicsReadable(Readable):
# XXX: Hardware may have it's own idea about the status: how to obtain?
if self.status_pv != 'unset':
# XXX: how to map an unknown type+value to an valid status ???
return status.UNKNOWN, self._read_pv(self.status_pv)
return Drivable.Status.UNKNOWN, self._read_pv(self.status_pv)
# status_pv is unset
return (status.OK, 'no pv set')
return (Drivable.Status.IDLE, 'no pv set')
class EpicsDrivable(Drivable):
@ -179,13 +178,11 @@ class EpicsDrivable(Drivable):
# XXX: Hardware may have it's own idea about the status: how to obtain?
if self.status_pv != 'unset':
# XXX: how to map an unknown type+value to an valid status ???
return status.UNKNOWN, self._read_pv(self.status_pv)
return Drivable.Status.UNKNOWN, self._read_pv(self.status_pv)
# status_pv is unset, derive status from equality of value + target
return (
status.OK,
'') if self.read_value() == self.read_target() else (
status.BUSY,
'Moving')
if self.read_value() == self.read_target():
return (Drivable.Status.OK, '')
return (Drivable.Status.BUSY, 'Moving')
# """Temperature control loop"""
@ -223,11 +220,9 @@ class EpicsTempCtrl(EpicsDrivable):
# XXX: comparison may need to collect a history to detect oscillations
at_target = abs(self.read_value(maxage) - self.read_target(maxage)) \
<= self.tolerance
return (
status.OK,
'at Target') if at_target else (
status.BUSY,
'Moving')
if at_target:
return (Drivable.Status.OK, 'at Target')
return (Drivable.Status.BUSY, 'Moving')
# TODO: add support for strings over epics pv
# def read_heaterrange(self, maxage=0):

View File

@ -36,7 +36,6 @@ import threading
import PyTango
from secop.lib import lazy_property
from secop.protocol import status
#from secop.parse import Parser
from secop.datatypes import IntRange, FloatRange, StringType, TupleOf, \
ArrayOf, EnumType
@ -175,11 +174,11 @@ class PyTangoDevice(Module):
}
tango_status_mapping = {
PyTango.DevState.ON: status.OK,
PyTango.DevState.ALARM: status.WARN,
PyTango.DevState.OFF: status.ERROR,
PyTango.DevState.FAULT: status.ERROR,
PyTango.DevState.MOVING: status.BUSY,
PyTango.DevState.ON: Drivable.Status.IDLE,
PyTango.DevState.ALARM: Drivable.Status.WARN,
PyTango.DevState.OFF: Drivable.Status.ERROR,
PyTango.DevState.FAULT: Drivable.Status.ERROR,
PyTango.DevState.MOVING: Drivable.Status.BUSY,
}
@lazy_property
@ -225,7 +224,7 @@ class PyTangoDevice(Module):
def _hw_wait(self):
"""Wait until hardware status is not BUSY."""
while self.read_status(0)[0] == 'BUSY':
while self.read_status(0)[0] == Drivable.Status.BUSY:
sleep(0.3)
def _getProperty(self, name, dev=None):
@ -363,7 +362,7 @@ class PyTangoDevice(Module):
tangoStatus = self._dev.Status()
# Map status
myState = self.tango_status_mapping.get(tangoState, status.UNKNOWN)
myState = self.tango_status_mapping.get(tangoState, Drivable.Status.UNKNOWN)
return (myState, tangoStatus)
@ -465,7 +464,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
if attrInfo.unit != 'No unit':
self.parameters['value'].unit = attrInfo.unit
def poll(self, nr):
def poll(self, nr=0):
super(AnalogOutput, self).poll(nr)
while len(self._history) > 2:
# if history would be too short, break
@ -509,8 +508,10 @@ class AnalogOutput(PyTangoDevice, Drivable):
return super(AnalogOutput, self).read_status()
if self._timeout:
if self._timeout < currenttime():
return status.UNSTABLE, 'timeout after waiting for stable value'
return (status.BUSY, 'Moving') if self._moving else (status.OK, 'stable')
return self.Status.UNSTABLE, 'timeout after waiting for stable value'
if self._moving:
return (self.Status.BUSY, 'moving')
return (self.Status.OK, 'stable')
@property
def absmin(self):
@ -555,7 +556,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
return self._checkLimits(value)
def write_target(self, value=FloatRange()):
if self.status[0] == status.BUSY:
if self.status[0] == self.Status.BUSY:
# changing target value during movement is not allowed by the
# Tango base class state machine. If we are moving, stop first.
self.do_stop()
@ -576,7 +577,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
self.read_status(0) # poll our status to keep it updated
def _hw_wait(self):
while super(AnalogOutput, self).read_status()[0] == status.BUSY:
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
sleep(0.3)
def do_stop(self):
@ -791,7 +792,7 @@ class NamedDigitalInput(DigitalInput):
super(NamedDigitalInput, self).init()
try:
# pylint: disable=eval-used
self.parameters['value'].datatype = EnumType(**eval(self.mapping))
self.parameters['value'].datatype = EnumType('value', **eval(self.mapping))
except Exception as e:
raise ValueError('Illegal Value for mapping: %r' % e)
@ -858,16 +859,15 @@ class NamedDigitalOutput(DigitalOutput):
super(NamedDigitalOutput, self).init()
try:
# pylint: disable=eval-used
self.parameters['value'].datatype = EnumType(**eval(self.mapping))
self.parameters['value'].datatype = EnumType('value', **eval(self.mapping))
# pylint: disable=eval-used
self.parameters['target'].datatype = EnumType(**eval(self.mapping))
self.parameters['target'].datatype = EnumType('target', **eval(self.mapping))
except Exception as e:
raise ValueError('Illegal Value for mapping: %r' % e)
def write_target(self, value):
# map from enum-str to integer value
self._dev.value = self.parameters[
'target'].datatype.reversed.get(value, value)
self._dev.value = int(value)
self.read_value()
@ -941,20 +941,20 @@ class StringIO(PyTangoDevice, Module):
'communicate': Command('Send a string and return the reply',
arguments=[StringType()],
result=StringType()),
'flush': Command('Flush output buffer',
arguments=[], result=None),
'read': Command('read some characters from input buffer',
arguments=[IntRange()], result=StringType()),
'write': Command('write some chars to output',
arguments=[StringType()], result=None),
'readLine': Command('Read sol - a whole line - eol',
arguments=[], result=StringType()),
'writeLine': Command('write sol + a whole line + eol',
arguments=[StringType()], result=None),
'availablechars': Command('return number of chars in input buffer',
arguments=[], result=IntRange(0)),
'availablelines': Command('return number of lines in input buffer',
arguments=[], result=IntRange(0)),
'flush': Command('Flush output buffer',
arguments=[], result=None),
'read': Command('read some characters from input buffer',
arguments=[IntRange()], result=StringType()),
'write': Command('write some chars to output',
arguments=[StringType()], result=None),
'readLine': Command('Read sol - a whole line - eol',
arguments=[], result=StringType()),
'writeLine': Command('write sol + a whole line + eol',
arguments=[StringType()], result=None),
'availablechars': Command('return number of chars in input buffer',
arguments=[], result=IntRange(0)),
'availablelines': Command('return number of lines in input buffer',
arguments=[], result=IntRange(0)),
'multicommunicate': Command('perform a sequence of communications',
arguments=[ArrayOf(
TupleOf(StringType(), IntRange()), 100)],

View File

@ -90,12 +90,12 @@ def test_IntRange():
def test_EnumType():
# test constructor catching illegal arguments
with pytest.raises(ValueError):
with pytest.raises(TypeError):
EnumType(1)
with pytest.raises(ValueError):
EnumType('a', b=0)
with pytest.raises(TypeError):
EnumType(['b', 0])
dt = EnumType(a=3, c=7, stuff=1)
dt = EnumType('dt', a=3, c=7, stuff=1)
assert dt.as_json == ['enum', dict(a=3, c=7, stuff=1)]
with pytest.raises(ValueError):
@ -116,9 +116,9 @@ def test_EnumType():
assert dt.export_value('c') == 7
assert dt.export_value('stuff') == 1
assert dt.export_value(1) == 1
assert dt.import_value(7) == 'c'
assert dt.import_value(3) == 'a'
assert dt.import_value(1) == 'stuff'
assert dt.import_value('c') == 7
assert dt.import_value('a') == 3
assert dt.import_value('stuff') == 1
with pytest.raises(ValueError):
dt.export_value(2)
with pytest.raises(ValueError):
@ -304,7 +304,7 @@ def test_get_datatype():
with pytest.raises(ValueError):
get_datatype(['enum'])
assert isinstance(get_datatype(['enum', dict(a=-2.718)]), EnumType)
assert isinstance(get_datatype(['enum', dict(a=-2)]), EnumType)
with pytest.raises(ValueError):
get_datatype(['enum', 10, -10])

80
test/test_lib_enum.py Normal file
View File

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""test Enum type."""
import sys
sys.path.insert(0, sys.path[0] + '/..')
# no fixtures needed
import pytest
from secop.lib.enum import EnumMember, Enum
def test_EnumMember():
with pytest.raises(TypeError):
a = EnumMember(None, 'name', 'value')
with pytest.raises(TypeError):
a = EnumMember(None, 'name', 1)
e1=Enum('X')
with pytest.raises(ValueError):
a = EnumMember(e1, 'a', 'value')
a = EnumMember(e1, 'a', 1)
with pytest.raises(TypeError):
a.value = 2
with pytest.raises(TypeError):
a.value += 2
with pytest.raises(TypeError):
a += 2
# this shall work
assert 2 == (a + 1) # pylint: disable=C0122
assert (a - 1) == 0
assert a
assert a + a
assert (2 - a) == 1
assert -a == -1 # numeric negation
assert ~a == -2 # bitmask like NOT
assert (a & 3) == 1
assert (a | 6) == 7
assert (a ^ 7) == 6
assert a < 2
assert a > 0
assert a != 3
assert a == 1
def test_Enum():
e1 = Enum('e1')
e2 = Enum('e2', e1, a=1, b=3)
e3 = Enum('e3', e2, c='a')
assert e3.c == 2
with pytest.raises(TypeError):
e2.c = 2
assert e3.a < e2.b
assert e2.b > e3.a
assert e3.c >= e2.a
assert e3.b <= e2.b