diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py index 1495cf6..8e397d7 100644 --- a/secop/client/baseclient.py +++ b/secop/client/baseclient.py @@ -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(): diff --git a/secop/datatypes.py b/secop/datatypes.py index fc45f0c..8e01b40 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -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, diff --git a/secop/gui/nodectrl.py b/secop/gui/nodectrl.py index b36a87f..858111b 100644 --- a/secop/gui/nodectrl.py +++ b/secop/gui/nodectrl.py @@ -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) diff --git a/secop/gui/params/__init__.py b/secop/gui/params/__init__.py index 81a0592..7947471 100644 --- a/secop/gui/params/__init__.py +++ b/secop/gui/params/__init__.py @@ -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): diff --git a/secop/gui/valuewidgets.py b/secop/gui/valuewidgets.py index 9be2653..a497e69 100644 --- a/secop/gui/valuewidgets.py +++ b/secop/gui/valuewidgets.py @@ -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]) diff --git a/secop/lib/__init__.py b/secop/lib/__init__.py index bc2588a..736b304 100644 --- a/secop/lib/__init__.py +++ b/secop/lib/__init__.py @@ -49,6 +49,8 @@ CONFIG = { } +unset_value = object() + class lazy_property(object): """A property that calculates its value only once.""" diff --git a/secop/lib/enum.py b/secop/lib/enum.py new file mode 100755 index 0000000..bcd5cf7 --- /dev/null +++ b/secop/lib/enum.py @@ -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 +# +# ***************************************************************************** +"""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 '' % (self.name, len(self)/2) + + def __call__(self, key): + return self[key] diff --git a/secop/modules.py b/secop/modules.py index bc241c9..a1def52 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -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. -# 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_ and generate an async update) -# if you want to 'update from the hardware', call self.read_ -# 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. + i.e. self.value, self.target etc... + these are accessing the cached version. + they can also be written to (which auto-calls self.write_ and + generate an async update) + + if you want to 'update from the hardware', call self.read_() 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 '. = ' @@ -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 diff --git a/secop/protocol/status.py b/secop/protocol/status.py deleted file mode 100644 index 48da1c6..0000000 --- a/secop/protocol/status.py +++ /dev/null @@ -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 -# -# ***************************************************************************** -"""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' diff --git a/secop_demo/cryo.py b/secop_demo/cryo.py index 945ce08..b912d57 100644 --- a/secop_demo/cryo.py +++ b/secop_demo/cryo.py @@ -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) diff --git a/secop_demo/modules.py b/secop_demo/modules.py index fbc92d0..6b13b03 100644 --- a/secop_demo/modules.py +++ b/secop_demo/modules.py @@ -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): diff --git a/secop_ess/epics.py b/secop_ess/epics.py index a5d2e0a..c98fc37 100644 --- a/secop_ess/epics.py +++ b/secop_ess/epics.py @@ -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): diff --git a/secop_mlz/entangle.py b/secop_mlz/entangle.py index 2015008..f53a0da 100644 --- a/secop_mlz/entangle.py +++ b/secop_mlz/entangle.py @@ -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)], diff --git a/test/test_datatypes.py b/test/test_datatypes.py index 49d84f3..453d323 100644 --- a/test/test_datatypes.py +++ b/test/test_datatypes.py @@ -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]) diff --git a/test/test_lib_enum.py b/test/test_lib_enum.py new file mode 100644 index 0000000..fc844fe --- /dev/null +++ b/test/test_lib_enum.py @@ -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 +# +# ***************************************************************************** +"""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