From 574a66c65ba3f32f9ab9d2b61d04c6838b240eab Mon Sep 17 00:00:00 2001 From: Enrico Faulhaber Date: Thu, 26 Apr 2018 16:29:09 +0200 Subject: [PATCH] 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 Reviewed-by: Enrico Faulhaber --- secop/client/baseclient.py | 10 +- secop/datatypes.py | 69 +++----- secop/gui/nodectrl.py | 18 +-- secop/gui/params/__init__.py | 33 ++-- secop/gui/valuewidgets.py | 14 +- secop/lib/__init__.py | 2 + secop/lib/enum.py | 306 +++++++++++++++++++++++++++++++++++ secop/modules.py | 171 +++++++++++--------- secop/protocol/status.py | 36 ----- secop_demo/cryo.py | 19 ++- secop_demo/modules.py | 81 +++++----- secop_ess/epics.py | 23 ++- secop_mlz/entangle.py | 64 ++++---- test/test_datatypes.py | 16 +- test/test_lib_enum.py | 80 +++++++++ 15 files changed, 644 insertions(+), 298 deletions(-) create mode 100755 secop/lib/enum.py delete mode 100644 secop/protocol/status.py create mode 100644 test/test_lib_enum.py 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