enhance documentation

- flatten hierarchy (some links do not work when using folders)
+ fix a bug with the redorder flag in Override
+ allow removal of parameters
+ clean description using inspect.cleandoc

Change-Id: I3dde4f4cb29c46e8a21014f1fad7aa3ad610a1bf
This commit is contained in:
2021-01-25 15:12:47 +01:00
parent e411ded55b
commit bc5edec06f
32 changed files with 608 additions and 381 deletions

View File

@ -126,6 +126,10 @@ class DataType(HasProperties):
"""
raise NotImplementedError
def short_doc(self):
"""short description for automatic extension of doc strings"""
return None
class Stub(DataType):
"""incomplete datatype, to be replaced with a proper one later during module load
@ -155,6 +159,10 @@ class Stub(DataType):
if isinstance(stub, cls):
prop.datatype = globals()[stub.name](*stub.args)
def short_doc(self):
return self.name.replace('Type', '').replace('Range', '').lower()
# SECoP types:
@ -163,6 +171,7 @@ class FloatRange(DataType):
:param minval: (property **min**)
:param maxval: (property **max**)
:param properties: any of the properties below
"""
properties = {
@ -240,6 +249,9 @@ class FloatRange(DataType):
other(max(sys.float_info.min, self.min))
other(min(sys.float_info.max, self.max))
def short_doc(self):
return 'float'
class IntRange(DataType):
"""restricted int type
@ -304,15 +316,25 @@ class IntRange(DataType):
for i in range(self.min, self.max + 1):
other(i)
def short_doc(self):
return 'int'
class ScaledInteger(DataType):
"""scaled integer (= fixed resolution float) type
| In general *ScaledInteger* is needed only in special cases,
e.g. when the a SEC node is running on very limited hardware
without floating point support.
| Please use *FloatRange* instead.
:param minval: (property **min**)
:param maxval: (property **max**)
:param properties: any of the properties below
note: limits are for the scaled float value
the scale is only used for calculating to/from transport serialisation
{properties}
:note: - limits are for the scaled float value
- the scale is only used for calculating to/from transport serialisation
"""
properties = {
'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True),
@ -413,14 +435,17 @@ class ScaledInteger(DataType):
other(self.min)
other(self.max)
def short_doc(self):
return 'float'
class EnumType(DataType):
"""enumeration
:param enum_or_name: the name of the Enum or an Enum to inherit from
:param members: members=<members dict>
:param members: each argument denotes <member name>=<member int value>
other keywords: (additional) members
exception: use members=<member dict> to add members from a dict
"""
def __init__(self, enum_or_name='', **members):
super().__init__()
@ -466,6 +491,9 @@ class EnumType(DataType):
for m in self._enum.members:
other(m)
def short_doc(self):
return 'one of %s' % str(tuple(self._enum.keys()))
class BLOBType(DataType):
"""binary large object
@ -547,11 +575,11 @@ class StringType(DataType):
Stub('BoolType'), extname='isUTF8', default=False),
}
def __init__(self, minchars=0, maxchars=None, **properties):
def __init__(self, minchars=0, maxchars=None, isUTF8=False):
super().__init__()
if maxchars is None:
maxchars = minchars or UNLIMITED
self.set_properties(minchars=minchars, maxchars=maxchars, **properties)
self.set_properties(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8)
def checkProperties(self):
self.default = ' ' * self.minchars
@ -607,12 +635,24 @@ class StringType(DataType):
except AttributeError:
raise BadValueError('incompatible datatypes')
def short_doc(self):
return 'str'
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
# whereas StringType is supposed to not contain '\n'
# unfortunately, SECoP makes no distinction here....
# note: content is supposed to follow the format of a git commit message, i.e. a line of text, 2 '\n' + a longer explanation
class TextType(StringType):
"""special string type, intended for longer texts
:param maxchars: maximum number of characters
whereas StringType is supposed to not contain '\n'
unfortunately, SECoP makes no distinction here....
note: content is supposed to follow the format of a git commit message,
i.e. a line of text, 2 '\n' + a longer explanation
"""
def __init__(self, maxchars=None):
if maxchars is None:
maxchars = UNLIMITED
@ -667,6 +707,9 @@ class BoolType(DataType):
other(False)
other(True)
def short_doc(self):
return 'bool'
Stub.fix_datatypes()
@ -678,6 +721,7 @@ Stub.fix_datatypes()
class ArrayOf(DataType):
"""data structure with fields of homogeneous type
:param members: the datatype for all elements
"""
properties = {
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen',
@ -774,10 +818,14 @@ class ArrayOf(DataType):
except AttributeError:
raise BadValueError('incompatible datatypes')
def short_doc(self):
return 'array of %s' % self.members.short_doc()
class TupleOf(DataType):
"""data structure with fields of inhomogeneous type
:param members: each argument is a datatype of an element
"""
def __init__(self, *members):
@ -841,6 +889,9 @@ class TupleOf(DataType):
for a, b in zip(self.members, other.members):
a.compatible(b)
def short_doc(self):
return 'tuple of (%s)' % ', '.join(m.short_doc() for m in self.members)
class ImmutableDict(dict):
def _no(self, *args, **kwds):
@ -851,6 +902,8 @@ class ImmutableDict(dict):
class StructOf(DataType):
"""data structure with named fields
:param optional: (*sequence*) optional members
:param members: each argument denotes <member name>=<member data type>
"""
def __init__(self, optional=None, **members):
super().__init__()
@ -926,11 +979,18 @@ class StructOf(DataType):
except (AttributeError, TypeError, KeyError):
raise BadValueError('incompatible datatypes')
def short_doc(self):
return 'dict'
class CommandType(DataType):
"""command
a pseudo datatype for commands with arguments and return values
:param argument: None or the data type of the argument. multiple arguments may be simulated
by TupleOf or StructOf
:param result: None or the data type of the result
"""
IS_COMMAND = True
@ -989,10 +1049,16 @@ class CommandType(DataType):
except AttributeError:
raise BadValueError('incompatible datatypes')
def short_doc(self):
argument = self.argument.short_doc() if self.argument else ''
result = ' -> %s' % self.argument.short_doc() if self.result else ''
return '(%s)%s' % (argument, result) # return argument list only
# internally used datatypes (i.e. only for programming the SEC-node)
class DataTypeType(DataType):
"""DataType type"""
def __call__(self, value):
"""check if given value (a python obj) is a valid datatype
@ -1036,7 +1102,9 @@ class ValueType(DataType):
class NoneOr(DataType):
"""validates a None or smth. else"""
"""validates a None or other
:param other: the other datatype"""
default = None
def __init__(self, other):
@ -1051,8 +1119,16 @@ class NoneOr(DataType):
return None
return self.other.export_value(value)
def short_doc(self):
other = self.other.short_doc()
return '%s or None' % other if other else None
class OrType(DataType):
"""validates one of the
:param types: each argument denotes one allowed type
"""
def __init__(self, *types):
super().__init__()
self.types = types
@ -1066,6 +1142,12 @@ class OrType(DataType):
pass
raise BadValueError("Invalid Value, must conform to one of %s" % (', '.join((str(t) for t in self.types))))
def short_doc(self):
types = [t.short_doc() for t in self.types]
if None in types:
return None
return ' or '.join(types)
Int8 = IntRange(-(1 << 7), (1 << 7) - 1)
Int16 = IntRange(-(1 << 15), (1 << 15) - 1)
@ -1079,6 +1161,12 @@ UInt64 = IntRange(0, (1 << 64) - 1)
# Goodie: Convenience Datatypes for Programming
class LimitsType(TupleOf):
"""limit (min, max) tuple
:param members: the type of both members
checks for min <= max
"""
def __init__(self, members):
TupleOf.__init__(self, members, members)
@ -1090,7 +1178,13 @@ class LimitsType(TupleOf):
class StatusType(TupleOf):
# shorten initialisation and allow acces to status enumMembers from status values
"""SECoP status type
:param enum: the status code enum type
allows to access enum members directly
"""
def __init__(self, enum):
TupleOf.__init__(self, EnumType(enum), StringType())
self.enum = enum

76
secop/lib/classdoc.py Normal file
View File

@ -0,0 +1,76 @@
# -*- 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:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
from inspect import cleandoc
from textwrap import indent
def indent_description(p):
"""indent lines except first one"""
return indent(p.description, ' ').replace(' ', '', 1)
def append_to_doc(cls, name, title, attrname, newitems, fmtfunc):
"""add information about some items to the doc
:param cls: the class with the doc string to be extended
:param name: the name of the attribute dict to be used
:param title: the title to be used
:param newitems: the set of new items defined for this class
:param fmtfunc: a function returning a formatted item to be displayed, including line feed at end
or an empty string to suppress output for this item
:type fmtfunc: function(key, value)
"""
doc = cleandoc(cls.__doc__ or '')
allitems = getattr(cls, attrname, {})
fmtdict = {n: fmtfunc(n, p) or ' - **%s** *removed*\n' % n for n, p in allitems.items()}
head, _, tail = doc.partition('{all %s}' % name)
if tail: # take all
inherited = set()
fmted = ''.join(fmtdict.values())
else:
inherited = {n: p for n, p in allitems.items() if fmtdict.get(n) and n not in newitems}
fmted = ''.join(' ' + v for k, v in fmtdict.items() if k in newitems)
head, _, tail = doc.partition('{%s}' % name)
if not tail:
head, _, tail = doc.partition('{no %s}' % name)
if tail: # add no information
return
# no tag found: append to the end
if fmted:
clsset = set()
for name in inherited:
p = allitems[name]
refcls = cls
for base in cls.__mro__:
dp = getattr(base, attrname, {}).get(name)
if dp:
if dp == p:
refcls = base
else:
break
clsset.add(refcls)
clsset.discard(cls)
if clsset:
fmted += ' - see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
for c in cls.__mro__ if c in clsset))
cls.__doc__ = '%s\n\n:%s: %s\n%s' % (head, title, fmted, tail)

View File

@ -28,7 +28,8 @@ from collections import OrderedDict
from secop.errors import ProgrammingError, BadValueError
from secop.params import Command, Override, Parameter
from secop.datatypes import EnumType
from secop.properties import PropertyMeta, add_extra_doc
from secop.properties import PropertyMeta
from secop.lib.classdoc import append_to_doc, indent_description
class Done:
@ -77,6 +78,9 @@ class ModuleMeta(PropertyMeta):
obj = obj.apply(accessibles[key])
accessibles[key] = obj
else:
if obj is None: # allow removal of accessibles
accessibles.pop(key, None)
continue
if key in accessibles:
# for now, accept redefinitions:
print("WARNING: module %s: %s should not be redefined"
@ -206,10 +210,33 @@ class ModuleMeta(PropertyMeta):
raise ProgrammingError('%r: command %r has to be specified '
'explicitly!' % (name, attrname[3:]))
add_extra_doc(newtype, '**parameters**',
{k: p for k, p in accessibles.items() if isinstance(p, Parameter)})
add_extra_doc(newtype, '**commands**',
{k: p for k, p in accessibles.items() if isinstance(p, Command)})
def fmt_param(name, param):
if not isinstance(param, Parameter):
return ''
desc = indent_description(param)
if '(' in desc[0:2]:
dtinfo = ''
else:
dtinfo = [param.datatype.short_doc(), 'rd' if param.readonly else 'wr',
None if param.export else 'hidden']
dtinfo = '*(%s)* ' % ', '.join(filter(None, dtinfo))
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
def fmt_command(name, command):
if not isinstance(command, Command):
return ''
desc = indent_description(command)
if '(' in desc[0:2]:
dtinfo = '' # note: we expect that desc contains argument list
else:
dtinfo = '*%s*' % command.datatype.short_doc() + ' -%s ' % ('' if command.export else ' *(hidden)*')
return '- **%s**\\ %s%s\n' % (name, dtinfo, desc)
append_to_doc(newtype, 'parameters', 'SECOP Parameters',
'accessibles', set(parameters) | set(overrides), fmt_param)
append_to_doc(newtype, 'commands', 'SECOP Commands',
'accessibles', set(commands) | set(overrides), fmt_command)
attrs['__constructed__'] = True
return newtype

View File

@ -50,17 +50,28 @@ class Module(HasProperties, metaclass=ModuleMeta):
all SECoP modules derive from this.
note: within modules, parameters should only be addressed as ``self.<pname>``
i.e. ``self.value``, ``self.target`` etc...
these are accessing the cached version.
they can also be written to, generating an async update
:param name: the modules name
:param logger: a logger instance
:param cfgdict: the dict from this modules section in the config file
:param srv: the server instance
if you want to 'update from the hardware', call ``self.read_<pname>()`` instead
the return value of this method will be used as the new cached value and
be an async update sent automatically.
Notes:
- the programmer normally should not need to reimplement :meth:`__init__`
- within modules, parameters should only be addressed as ``self.<pname>``, i.e. ``self.value``, ``self.target`` etc...
- these are accessing the cached version.
- they can also be written to, generating an async update
- if you want to 'update from the hardware', call ``self.read_<pname>()`` instead
- the return value of this method will be used as the new cached value and
be an async update sent automatically.
- if you want to 'update the hardware' call ``self.write_<pname>(<new value>)``.
- The return value of this method will also update the cache.
if you want to 'update the hardware' call ``self.write_<pname>(<new value>)``.
The return value of this method will also update the cache.
"""
# static properties, definitions in derived classes should overwrite earlier ones.
# note: properties don't change after startup and are usually filled
@ -69,22 +80,22 @@ class Module(HasProperties, metaclass=ModuleMeta):
# note: the names map to a [datatype, value] list, value comes from the cfg file,
# datatype is fixed!
properties = {
'export': Property('Flag if this Module is to be exported', BoolType(), default=True, export=False),
'group': Property('Optional group the Module belongs to', StringType(), default='', extname='group'),
'description': Property('Description of the module', TextType(), extname='description', mandatory=True),
'meaning': Property('Optional Meaning indicator', TupleOf(StringType(),IntRange(0,50)),
'export': Property('flag if this Module is to be exported', BoolType(), default=True, export=False),
'group': Property('optional group the Module belongs to', StringType(), default='', extname='group'),
'description': Property('description of the module', TextType(), extname='description', mandatory=True),
'meaning': Property('dptional Meaning indicator', TupleOf(StringType(),IntRange(0,50)),
default=('',0), extname='meaning'),
'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
default='user', extname='visibility'),
'implementation': Property('Internal name of the implementation class of the module', StringType(),
'implementation': Property('internal name of the implementation class of the module', StringType(),
extname='implementation'),
'interface_classes': Property('Offical highest Interface-class of the module', ArrayOf(StringType()),
'interface_classes': Property('offical highest Interface-class of the module', ArrayOf(StringType()),
extname='interface_classes'),
}
# properties, parameters and commands are auto-merged upon subclassing
parameters = {}
commands = {}
parameters = {} #: definition of parameters
commands = {} #: definition of commands
# reference to the dispatcher (used for sending async updates)
DISPATCHER = None
@ -354,12 +365,31 @@ class Module(HasProperties, metaclass=ModuleMeta):
return False
def earlyInit(self):
# may be overriden in derived classes to init stuff
"""may be overriden in derived classes to init stuff
after creating the module (no super call needed)
"""
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
def initModule(self):
"""may be overriden to do stuff after all modules are intiialized
no super call needed
"""
self.log.debug('empty %s.initModule()' % self.__class__.__name__)
def startModule(self, started_callback):
"""runs after init of all modules
:param started_callback: argument less function to be called when the thread
spawned by startModule has finished its initial work
:return: None or a timeout value, if different from default (30 sec)
override this method for doing stuff during startup, after all modules are
initialized. do not forget the super call
"""
mkthread(self.writeInitParams, started_callback)
def pollOneParam(self, pname):
"""poll parameter <pname> with proper error handling"""
try:
@ -391,15 +421,6 @@ class Module(HasProperties, metaclass=ModuleMeta):
if started_callback:
started_callback()
def startModule(self, started_callback):
"""runs after init of all modules
started_callback to be called when the thread spawned by startModule
has finished its initial work
might return a timeout value, if different from default
"""
mkthread(self.writeInitParams, started_callback)
class Readable(Module):
"""basic readable module"""
@ -421,7 +442,7 @@ class Readable(Module):
readonly=False,
datatype=FloatRange(0.1, 120),
),
'status': Parameter('current status of the Module',
'status': Parameter('*(rd, tuple of (Readable.Status, str))* current status of the Module',
default=(Status.IDLE, ''),
datatype=TupleOf(EnumType(Status), StringType()),
readonly=True, poll=True,
@ -496,7 +517,8 @@ class Drivable(Writable):
}
overrides = {
'status': Override(datatype=StatusType(Status)),
'status': Override('*(rd, tuple of (Drivable.Status, str))* current status of the Module',
datatype=StatusType(Status)),
}
def isBusy(self, status=None):
@ -561,7 +583,7 @@ class Communicator(Module):
class Attached(Property):
"""a special property, defining an attached modle
"""a special property, defining an attached module
assign a module name to this property in the cfg file,
and the server will create an attribute with this module

View File

@ -24,6 +24,7 @@
from collections import OrderedDict
from inspect import cleandoc
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
NoneOr, TextType, IntRange
@ -88,18 +89,30 @@ class Parameter(Accessible):
extname='visibility', default=1),
'constant': Property('optional constant value for constant parameters', ValueType(),
extname='constant', default=None, mandatory=False),
'default': Property('default (startup) value of this parameter if it can not be read from the hardware.',
ValueType(), export=False, default=None, mandatory=False),
'export': Property('[internal] is this parameter accessible via SECoP? (vs. internal parameter)',
OrType(BoolType(), StringType()), export=False, default=True),
'poll': Property('[internal] polling indicator, may be:\n' + '\n '.join(['',
'* None (omitted): will be converted to True/False if handler is/is not None',
'* False or 0 (never poll this parameter)',
'* True or 1 (AUTO), converted to SLOW (readonly=False), '
'DYNAMIC (*status* and *value*) or REGULAR (else)',
'* 2 (SLOW), polled with lower priority and a multiple of pollinterval',
'* 3 (REGULAR), polled with pollperiod',
'* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval, else polled with pollperiod']),
'default': Property('[internal] default (startup) value of this parameter '
'if it can not be read from the hardware.',
ValueType(), export=False, default=None, mandatory=False),
'export': Property('''
[internal] export settings
* False: not accessible via SECoP.
* True: exported, name automatic.
* a string: exported with custom name''',
OrType(BoolType(), StringType()), export=False, default=True),
'poll': Property('''
[internal] polling indicator
may be:
* None (omitted): will be converted to True/False if handler is/is not None
* False or 0 (never poll this parameter)
* True or 1 (AUTO), converted to SLOW (readonly=False)
DYNAMIC (*status* and *value*) or REGULAR (else)
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
* 3 (REGULAR), polled with pollperiod
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
else polled with pollperiod
''',
NoneOr(IntRange()), export=False, default=None),
'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None),
'optional': Property('[internal] is this parameter optional?', BoolType(), export=False,
@ -124,7 +137,7 @@ class Parameter(Accessible):
raise ProgrammingError(
'datatype MUST be derived from class DataType!')
kwds['description'] = description
kwds['description'] = cleandoc(description)
kwds['datatype'] = datatype
kwds['readonly'] = kwds.get('readonly', True) # for frappy optional, for SECoP mandatory
if unit is not None: # for legacy code only
@ -213,7 +226,7 @@ class Commands(Parameters):
class Override(CountedObj):
"""Stores the overrides to be applied to a Parameter
"""Stores the overrides to be applied to a Parameter or Command
note: overrides are applied by the metaclass during class creating
reorder=True: use position of Override instead of inherited for the order
@ -224,7 +237,7 @@ class Override(CountedObj):
self.reorder = reorder
# allow to override description and datatype without keyword
if description:
self.kwds['description'] = description
self.kwds['description'] = cleandoc(description)
if datatype is not None:
self.kwds['datatype'] = datatype
# for now, do not use the Override ctr
@ -252,12 +265,10 @@ class Override(CountedObj):
props.update(self.kwds)
if self.reorder:
#props['ctr'] = self.ctr
return type(obj)(ctr=self.ctr, **props)
return type(obj)(**props)
return type(obj)(**props)
return type(obj)(ctr=self.ctr, **props)
raise ProgrammingError(
"Overrides can only be applied to Accessibles, %r is none!" %
obj)
"Overrides can only be applied to Accessibles, %r is none!" % obj)
class Command(Accessible):
@ -270,8 +281,13 @@ class Command(Accessible):
extname='group', export=True, default=''),
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
extname='visibility', export=True, default=1),
'export': Property('[internal] flag: is the command accessible via SECoP? (vs. pure internal use)',
OrType(BoolType(), StringType()), export=False, default=True),
'export': Property('''
[internal] export settings
- False: not accessible via SECoP.
- True: exported, name automatic.
- a string: exported with custom name''',
OrType(BoolType(), StringType()), export=False, default=True),
'optional': Property('[internal] is the command optional to implement? (vs. mandatory)',
BoolType(), export=False, default=False, settable=False),
'datatype': Property('[internal] datatype of the command, auto generated from \'argument\' and \'result\'',
@ -283,7 +299,7 @@ class Command(Accessible):
}
def __init__(self, description, ctr=None, **kwds):
kwds['description'] = description
kwds['description'] = cleandoc(description)
kwds['datatype'] = CommandType(kwds.get('argument', None), kwds.get('result', None))
super(Command, self).__init__(**kwds)
if ctr is not None:

View File

@ -24,8 +24,10 @@
from collections import OrderedDict
from inspect import cleandoc
from secop.errors import ProgrammingError, ConfigError, BadValueError
from secop.lib.classdoc import append_to_doc, indent_description
# storage for 'properties of a property'
@ -33,7 +35,7 @@ class Property:
"""base class holding info about a property
:param description: mandatory
:param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed!
:param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed,
also for example ``ValueType()`` (any type!), ``NoneOr(...)``, etc.
:param default: a default value. SECoP properties are normally not sent to the ECS,
when they match the default
@ -48,7 +50,7 @@ class Property:
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True):
if not callable(datatype):
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
self.description = description
self.description = cleandoc(description)
self.default = datatype.default if default is None else datatype(default)
self.datatype = datatype
self.extname = extname
@ -85,17 +87,6 @@ class Properties(OrderedDict):
raise ProgrammingError('deleting Properties is not supported!')
def add_extra_doc(cls, title, items):
"""add bulleted list to doc string
using names and description of items
"""
bulletlist = ['\n - **%s** - %s' % (k, p.description) for k, p in items.items()]
if bulletlist:
doctext = '%s\n\n%s' % (title, ''.join(bulletlist))
cls.__doc__ = (cls.__doc__ or '') + '\n\n %s\n' % doctext
class PropertyMeta(type):
"""Metaclass for HasProperties
@ -142,7 +133,21 @@ class PropertyMeta(type):
% (newtype, k, attrs[k]))
setattr(newtype, k, property(getter))
add_extra_doc(newtype, '**properties**', attrs.get('properties', {})) # only new properties
# add property information to the doc string
def fmt_property(name, prop):
desc = indent_description(prop)
if '(' in desc[0:2]:
dtinfo = ''
else:
dtinfo = [prop.datatype.short_doc(), None if prop.export else 'hidden']
dtinfo = ', '.join(filter(None, dtinfo))
if dtinfo:
dtinfo = '*(%s)* ' % dtinfo
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
append_to_doc(newtype, 'properties', 'SECOP Properties',
'properties', attrs.get("properties", {}), fmt_property)
return newtype

View File

@ -27,7 +27,7 @@ import time
import threading
import re
from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.modules import Module, Communicator, Parameter, Command, Property, Attached
from secop.modules import Module, Communicator, Parameter, Command, Property, Attached, Override
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType
from secop.errors import CommunicationFailedError, CommunicationSilentError
from secop.poller import REGULAR
@ -65,8 +65,20 @@ class StringIO(Communicator):
Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10),
}
commands = {
'communicate':
Override('''
send a command and receive a reply
- using end_of_line, encoding and self._lock
- for commands without reply, the command must be joined with a query command,
- wait_before is respected for end_of_lines within a command
'''),
'multicomm':
Command('execute multiple commands in one go',
Command('''
execute multiple commands in one go
assuring that no other thread calls commands in between
''',
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
}
@ -169,12 +181,6 @@ class StringIO(Communicator):
self._reconnectCallbacks.pop(key)
def do_communicate(self, command):
"""send a command and receive a reply
using end_of_line, encoding and self._lock
for commands without reply, the command must be joined with a query command,
wait_before is respected for end_of_lines within a command.
"""
if not self.is_connected:
self.read_is_connected() # try to reconnect
try: