diff --git a/frappy/datatypes.py b/frappy/datatypes.py index 90c8b2c..381b229 100644 --- a/frappy/datatypes.py +++ b/frappy/datatypes.py @@ -1242,7 +1242,12 @@ class LimitsType(TupleOf): class StatusType(TupleOf): - # shorten initialisation and allow access to status enumMembers from status values + """convenience type for status + + :param first: an Enum or Module to inherit from, or the first member name + :param args: member names (codes will be taken from class attributes below) + :param kwds: additional members not matching the standard + """ DISABLED = 0 IDLE = 100 STANDBY = 130 @@ -1250,7 +1255,7 @@ class StatusType(TupleOf): WARN = 200 WARN_STANDBY = 230 WARN_PREPARED = 250 - UNSTABLE = 270 # no SECoP standard (yet) + UNSTABLE = 270 # not in SECoP standard (yet) BUSY = 300 DISABLING = 310 INITIALIZING = 320 @@ -1262,13 +1267,30 @@ class StatusType(TupleOf): ERROR = 400 ERROR_STANDBY = 430 ERROR_PREPARED = 450 + UNKNOWN = 401 # not in SECoP standard (yet) - def __init__(self, enum): - super().__init__(EnumType(enum), StringType()) - self._enum = enum + def __init__(self, first, *args, **kwds): + if first: + if isinstance(first, str): + args = (first,) + args + first = 'Status' # enum name + else: + if not isinstance(first, Enum): + # assume first is a Module with a status parameter + try: + first = first.status.datatype.members[0]._enum + except AttributeError: + raise ProgrammingError('first argument must be either str, Enum or a module') from None + else: + first = 'Status' # enum name + bad = {n for n in args if n not in StatusType.__dict__ or n.startswith('_')} # avoid built-in attributes + if bad: + raise ProgrammingError('positional arguments %r must be standard status code names' % bad) + self.enum = Enum(Enum(first, **{n: StatusType.__dict__[n] for n in args}), **kwds) + super().__init__(EnumType(self.enum), StringType()) def __getattr__(self, key): - return getattr(self._enum, key) + return self.enum[key] def floatargs(kwds): diff --git a/frappy/lib/enum.py b/frappy/lib/enum.py index 2bdcf1c..de6d7dc 100644 --- a/frappy/lib/enum.py +++ b/frappy/lib/enum.py @@ -264,8 +264,7 @@ class Enum(dict): names = set() values = set() - # pylint: disable=dangerous-default-value - def add(self, k, v, names=names, value=values): + def add(self, k, v): """helper for creating the enum members""" if v is None: # sugar: take the next free number if value was None @@ -285,10 +284,10 @@ class Enum(dict): 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)) + if self.get(k, v) != v: + raise TypeError('%s=%d conflicts with %s=%d' % (k, v, k, self[k])) + if self.get(v, k) != k: + raise TypeError('%s=%d conflicts with %s=%d' % (k, v, self[v].name, v)) # remember it self[v] = self[k] = EnumMember(self, k, v) diff --git a/frappy/modules.py b/frappy/modules.py index 171da21..915d3d0 100644 --- a/frappy/modules.py +++ b/frappy/modules.py @@ -220,6 +220,11 @@ class HasAccessibles(HasProperties): raise ProgrammingError('%s.%s defined, but %r is no parameter' % (cls.__name__, attrname, pname)) + try: + # update Status type + cls.Status = cls.status.datatype.members[0]._enum + except AttributeError: + pass res = {} # collect info about properties for pn, pv in cls.propertyDict.items(): @@ -826,7 +831,6 @@ class Module(HasAccessibles): class Readable(Module): """basic readable module""" - # pylint: disable=invalid-name Status = Enum('Status', IDLE=StatusType.IDLE, WARN=StatusType.WARN, @@ -834,11 +838,10 @@ class Readable(Module): ERROR=StatusType.ERROR, DISABLED=StatusType.DISABLED, UNKNOWN=401, # not SECoP standard. TODO: remove and adapt entangle and epics - ) #: status codes - + ) #: status code Enum: extended automatically in inherited modules value = Parameter('current value of the module', FloatRange()) status = Parameter('current status of the module', StatusType(Status), - default=(Status.IDLE, '')) + default=(StatusType.IDLE, '')) pollinterval = Parameter('default poll interval', FloatRange(0.1, 120), default=5, readonly=False, export=True) @@ -869,9 +872,7 @@ class Writable(Readable): class Drivable(Writable): """basic drivable module""" - Status = Enum(Readable.Status, BUSY=StatusType.BUSY) #: status codes - - status = Parameter(datatype=StatusType(Status)) # override Readable.status + status = Parameter(datatype=StatusType(Readable, 'BUSY')) # extend Readable.status def isBusy(self, status=None): """check for busy, treating substates correctly diff --git a/test/test_datatypes.py b/test/test_datatypes.py index 720b6ce..17bfbf6 100644 --- a/test/test_datatypes.py +++ b/test/test_datatypes.py @@ -26,7 +26,7 @@ import pytest from frappy.datatypes import ArrayOf, BLOBType, BoolType, \ - CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \ + CommandType, ConfigError, DataType, EnumType, FloatRange, \ IntRange, ProgrammingError, ScaledInteger, StatusType, \ StringType, StructOf, TextType, TupleOf, get_datatype, \ DiscouragedConversion @@ -495,11 +495,23 @@ def test_Command(): def test_StatusType(): - status_codes = Enum('Status', IDLE=100, WARN=200, BUSY=300, ERROR=400) - dt = StatusType(status_codes) - assert dt.IDLE == status_codes.IDLE - assert dt.ERROR == status_codes.ERROR - assert dt._enum == status_codes + dt = StatusType('IDLE', 'WARN', 'ERROR', 'DISABLED') + assert dt.IDLE == StatusType.IDLE == 100 + assert dt.ERROR == StatusType.ERROR == 400 + + dt2 = StatusType(None, IDLE=100, WARN=200, ERROR=400, DISABLED=0) + assert dt2.export_datatype() == dt.export_datatype() + + dt3 = StatusType(dt.enum) + assert dt3.export_datatype() == dt.export_datatype() + + with pytest.raises(ProgrammingError): + StatusType('__init__') # built in attribute of StatusType + + with pytest.raises(ProgrammingError): + StatusType(dt.enum, 'custom') # not a standard attribute + + StatusType(dt.enum, custom=499) # o.k., if value is given def test_get_datatype(): diff --git a/test/test_lib_enum.py b/test/test_lib_enum.py index b349358..88864dd 100644 --- a/test/test_lib_enum.py +++ b/test/test_lib_enum.py @@ -83,3 +83,12 @@ def test_Enum_bool(): e = Enum('OffOn', off=0, on=1) assert bool(e(0)) is False assert bool(e(1)) is True + + +def test_Enum_duplicate(): + e = Enum('x', a=1, b=2) + Enum(e, b=2, c=3) # matching duplicate + with pytest.raises(TypeError): + Enum(e, b=3, c=4) # duplicate name with value mismatch + with pytest.raises(TypeError): + Enum(e, c=1) # duplicate value with name mismatch