enhance documentation
- flatten hierarchy (some links do not work when using folders) - add a tutorial for programming a simple driver - clean description using inspect.cleandoc + fix a bug with 'unit' pseudo property in a Parameter used as override Change-Id: I31ddba5d516d1ee5e785e28fbd79fca44ed23f5e Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/25000 Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
@ -32,6 +32,7 @@ from secop.lib.enum import Enum
|
||||
from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached
|
||||
from secop.properties import Property
|
||||
from secop.params import Parameter, Command, Override, usercommand
|
||||
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC
|
||||
from secop.metaclass import Done
|
||||
from secop.iohandler import IOHandler, IOHandlerBase
|
||||
from secop.stringio import StringIO, HasIodev
|
||||
|
@ -55,6 +55,7 @@ Parser = Parser()
|
||||
|
||||
# base class for all DataTypes
|
||||
class DataType(HasProperties):
|
||||
"""base class for all data types"""
|
||||
IS_COMMAND = False
|
||||
unit = ''
|
||||
default = None
|
||||
@ -158,7 +159,12 @@ class Stub(DataType):
|
||||
# SECoP types:
|
||||
|
||||
class FloatRange(DataType):
|
||||
"""Restricted float type"""
|
||||
"""(restricted) float type
|
||||
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
:param kwds: any of the properties below
|
||||
"""
|
||||
properties = {
|
||||
'min': Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max),
|
||||
'max': Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max),
|
||||
@ -236,7 +242,11 @@ class FloatRange(DataType):
|
||||
|
||||
|
||||
class IntRange(DataType):
|
||||
"""Restricted int type"""
|
||||
"""restricted int type
|
||||
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
"""
|
||||
properties = {
|
||||
'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True),
|
||||
'max': Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True),
|
||||
@ -296,10 +306,15 @@ class IntRange(DataType):
|
||||
|
||||
|
||||
class ScaledInteger(DataType):
|
||||
"""Scaled integer int type
|
||||
"""scaled integer (= fixed resolution float) type
|
||||
|
||||
note: limits are for the scaled value (i.e. the internal value)
|
||||
the scale is only used for calculating to/from transport serialisation"""
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
:param kwds: 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 = {
|
||||
'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True),
|
||||
'min': Property('low limit', FloatRange(), extname='min', mandatory=True),
|
||||
@ -401,13 +416,16 @@ class ScaledInteger(DataType):
|
||||
|
||||
|
||||
class EnumType(DataType):
|
||||
"""enumeration
|
||||
|
||||
def __init__(self, enum_or_name='', **kwds):
|
||||
:param enum_or_name: the name of the Enum or an Enum to inherit from
|
||||
:param members: members dict or None when using kwds only
|
||||
:param kwds: (additional) members
|
||||
"""
|
||||
def __init__(self, enum_or_name='', *, members=None, **kwds):
|
||||
super().__init__()
|
||||
if 'members' in kwds:
|
||||
kwds = dict(kwds)
|
||||
kwds.update(kwds['members'])
|
||||
kwds.pop('members')
|
||||
if members is not None:
|
||||
kwds.update(members)
|
||||
self._enum = Enum(enum_or_name, **kwds)
|
||||
self.default = self._enum[self._enum.members[0]]
|
||||
|
||||
@ -448,6 +466,11 @@ class EnumType(DataType):
|
||||
|
||||
|
||||
class BLOBType(DataType):
|
||||
"""binary large object
|
||||
|
||||
internally treated as bytes
|
||||
"""
|
||||
|
||||
properties = {
|
||||
'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
||||
default=0),
|
||||
@ -511,6 +534,10 @@ class BLOBType(DataType):
|
||||
|
||||
|
||||
class StringType(DataType):
|
||||
"""string
|
||||
|
||||
for parameters see properties below
|
||||
"""
|
||||
properties = {
|
||||
'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='minchars', default=0),
|
||||
@ -602,6 +629,7 @@ class TextType(StringType):
|
||||
|
||||
|
||||
class BoolType(DataType):
|
||||
"""boolean"""
|
||||
default = False
|
||||
|
||||
def export_datatype(self):
|
||||
@ -646,6 +674,10 @@ Stub.fix_datatypes()
|
||||
|
||||
|
||||
class ArrayOf(DataType):
|
||||
"""data structure with fields of homogeneous type
|
||||
|
||||
:param members: the datatype of the elements
|
||||
"""
|
||||
properties = {
|
||||
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen',
|
||||
default=0),
|
||||
@ -743,7 +775,10 @@ class ArrayOf(DataType):
|
||||
|
||||
|
||||
class TupleOf(DataType):
|
||||
"""data structure with fields of inhomogeneous type
|
||||
|
||||
types are given as positional arguments
|
||||
"""
|
||||
def __init__(self, *members):
|
||||
super().__init__()
|
||||
if not members:
|
||||
@ -813,7 +848,11 @@ class ImmutableDict(dict):
|
||||
|
||||
|
||||
class StructOf(DataType):
|
||||
"""data structure with named fields
|
||||
|
||||
:param optional: a list of optional members
|
||||
:param members: names as keys and types as values for all members
|
||||
"""
|
||||
def __init__(self, optional=None, **members):
|
||||
super().__init__()
|
||||
self.members = members
|
||||
@ -890,6 +929,10 @@ class StructOf(DataType):
|
||||
|
||||
|
||||
class CommandType(DataType):
|
||||
"""command
|
||||
|
||||
a pseudo datatype for commands with arguments and return values
|
||||
"""
|
||||
IS_COMMAND = True
|
||||
|
||||
def __init__(self, argument=None, result=None):
|
||||
@ -1111,7 +1154,10 @@ def get_datatype(json, pname=''):
|
||||
"""returns a DataType object from description
|
||||
|
||||
inverse of <DataType>.export_datatype()
|
||||
the pname argument, if given, is used to name EnumTypes from the parameter name
|
||||
|
||||
:param json: the datainfo object as returned from json.loads
|
||||
:param pname: if given, used to name EnumTypes from the parameter name
|
||||
:return: the datatype (instance of DataType)
|
||||
"""
|
||||
if json is None:
|
||||
return json
|
||||
|
@ -197,10 +197,14 @@ class IOHandler(IOHandlerBase):
|
||||
the same format as the arguments for the change command.
|
||||
Examples: devices from LakeShore, PPMS
|
||||
|
||||
implementing classes may override the following class variables
|
||||
:param group: the handler group (used for analyze_<group> and change_<group>)
|
||||
:param querycmd: the command for a query, may contain named formats for cmdargs
|
||||
:param replyfmt: the format for reading the reply with some scanf like behaviour
|
||||
:param changecmd: the first part of the change command (without values), may be
|
||||
omitted if no write happens
|
||||
"""
|
||||
CMDARGS = [] # list of properties or parameters to be used for building some of the the query and change commands
|
||||
CMDSEPARATOR = None # if not None, it is possible to join a command and a query with the given separator
|
||||
CMDARGS = [] #: list of properties or parameters to be used for building some of the the query and change commands
|
||||
CMDSEPARATOR = None #: if not None, it is possible to join a command and a query with the given separator
|
||||
|
||||
def __init__(self, group, querycmd, replyfmt, changecmd=None):
|
||||
"""initialize the IO handler
|
||||
@ -269,7 +273,7 @@ class IOHandler(IOHandlerBase):
|
||||
return self.read
|
||||
|
||||
def read(self, module):
|
||||
"""write values from module"""
|
||||
# read values from module
|
||||
assert module.__class__ == self._module_class
|
||||
try:
|
||||
# do a read of the current hw values
|
||||
@ -293,7 +297,8 @@ class IOHandler(IOHandlerBase):
|
||||
def get_write_func(self, pname):
|
||||
"""returns the write function passed to the metaclass
|
||||
|
||||
If pre_wfunc is given, it is to be called before change_<group>.
|
||||
:param pname: the parameter name
|
||||
|
||||
May be overriden to return None, if not used
|
||||
"""
|
||||
|
||||
@ -304,7 +309,7 @@ class IOHandler(IOHandlerBase):
|
||||
return wfunc
|
||||
|
||||
def write(self, module, pname, value):
|
||||
"""write value to the module"""
|
||||
# write value to parameter pname of the module
|
||||
assert module.__class__ == self._module_class
|
||||
force_read = False
|
||||
valuedict = {pname: value}
|
||||
|
@ -126,7 +126,7 @@ class AsynConn:
|
||||
self._rxbuffer += data
|
||||
|
||||
def readbytes(self, nbytes, timeout=None):
|
||||
"""read one line
|
||||
"""read a fixed number of bytes
|
||||
|
||||
return either <nbytes> bytes or None if not enough data available within 1 sec (self.timeout)
|
||||
if a non-zero timeout is given, a timeout error is raised instead of returning None
|
||||
|
185
secop/lib/classdoc.py
Normal file
185
secop/lib/classdoc.py
Normal file
@ -0,0 +1,185 @@
|
||||
# -*- 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 textwrap import indent
|
||||
from secop.modules import Module, HasProperties, Property, Parameter, Command
|
||||
|
||||
|
||||
def indent_description(p):
|
||||
"""indent lines except first one"""
|
||||
return indent(p.description, ' ').replace(' ', '', 1)
|
||||
|
||||
|
||||
def fmt_param(name, param):
|
||||
desc = indent_description(param)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = ''
|
||||
else:
|
||||
dtinfo = [short_doc(param.datatype), '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):
|
||||
desc = indent_description(command)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = '' # note: we expect that desc contains argument list
|
||||
else:
|
||||
dtinfo = '*%s*' % short_doc(command.datatype) + ' -%s ' % ('' if command.export else ' *(hidden)*')
|
||||
return '- **%s**\\ %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
|
||||
def fmt_property(name, prop):
|
||||
desc = indent_description(prop)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = ''
|
||||
else:
|
||||
dtinfo = [short_doc(prop.datatype), None if prop.export else 'hidden']
|
||||
dtinfo = ', '.join(filter(None, dtinfo))
|
||||
if dtinfo:
|
||||
dtinfo = '*(%s)* ' % dtinfo
|
||||
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
|
||||
SIMPLETYPES = {
|
||||
'FloatRange': 'float',
|
||||
'ScaledInteger': 'float',
|
||||
'IntRange': 'int',
|
||||
'BlobType': 'bytes',
|
||||
'StringType': 'str',
|
||||
'BoolType': 'bool',
|
||||
'StructOf': 'dict',
|
||||
}
|
||||
|
||||
|
||||
def short_doc(datatype):
|
||||
# pylint: disable=possibly-unused-variable
|
||||
|
||||
def doc_EnumType(dt):
|
||||
return 'one of %s' % str(tuple(dt._enum.keys()))
|
||||
|
||||
def doc_ArrayOf(dt):
|
||||
return 'array of %s' % short_doc(dt.members)
|
||||
|
||||
def doc_TupleOf(dt):
|
||||
return 'tuple of (%s)' % ', '.join(short_doc(m) for m in dt.members)
|
||||
|
||||
def doc_CommandType(dt):
|
||||
argument = short_doc(dt.argument) if dt.argument else ''
|
||||
result = ' -> %s' % short_doc(dt.result) if dt.result else ''
|
||||
return '(%s)%s' % (argument, result) # return argument list only
|
||||
|
||||
def doc_NoneOr(dt):
|
||||
other = short_doc(dt.other)
|
||||
return '%s or None' % other if other else None
|
||||
|
||||
def doc_OrType(dt):
|
||||
types = [short_doc(t) for t in dt.types]
|
||||
if None in types: # type is anyway broad: no doc
|
||||
return None
|
||||
return ' or '.join(types)
|
||||
|
||||
def doc_Stub(dt):
|
||||
return dt.name.replace('Type', '').replace('Range', '').lower()
|
||||
|
||||
clsname = datatype.__class__.__name__
|
||||
result = SIMPLETYPES.get(clsname)
|
||||
if result:
|
||||
return result
|
||||
fun = locals().get('doc_' + clsname)
|
||||
if fun:
|
||||
return fun(datatype)
|
||||
return None # broad type like ValueType: no doc
|
||||
|
||||
|
||||
def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
|
||||
"""add information about some items to the doc
|
||||
|
||||
:param cls: the class with the doc string to be extended
|
||||
:param lines: content of the docstring, as lines
|
||||
:param itemcls: the class of the attribute to be collected, a tuple of classes is also allowed.
|
||||
:param attrname: the name of the attribute dict to look for
|
||||
:param name: the name of the items to be collected (used for the title and for the tags)
|
||||
: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)
|
||||
|
||||
rules, assuming name='properties':
|
||||
|
||||
- if the docstring contains ``{properties}``, new properties are inserted here
|
||||
- if the docstring contains ``{all properties}``, all properties are inserted here
|
||||
- if the docstring contains ``{no properties}``, no properties are inserted
|
||||
|
||||
only the first appearance of a tag above is considered
|
||||
"""
|
||||
doc = '\n'.join(lines)
|
||||
title = 'SECoP %s' % name.title()
|
||||
allitems = getattr(cls, attrname, {})
|
||||
fmtdict = {n: fmtfunc(n, p) for n, p in allitems.items() if isinstance(p, itemcls)}
|
||||
head, _, tail = doc.partition('{all %s}' % name)
|
||||
clsset = set()
|
||||
if tail: # take all
|
||||
fmted = fmtdict.values()
|
||||
else:
|
||||
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
|
||||
|
||||
fmted = []
|
||||
for key, formatted_item in fmtdict.items():
|
||||
if not formatted_item:
|
||||
continue
|
||||
# find where item is defined or modified
|
||||
refcls = None
|
||||
for base in cls.__mro__:
|
||||
p = getattr(base, attrname, {}).get(key)
|
||||
if isinstance(p, itemcls):
|
||||
if fmtfunc(key, p) == formatted_item:
|
||||
refcls = base
|
||||
else:
|
||||
break
|
||||
if refcls == cls:
|
||||
# definition in cls is new or modified
|
||||
fmted.append(formatted_item)
|
||||
else:
|
||||
# definition of last modification in refcls
|
||||
clsset.add(refcls)
|
||||
if fmted:
|
||||
if clsset:
|
||||
fmted.append('- see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
|
||||
for c in cls.__mro__ if c in clsset)))
|
||||
|
||||
doc = '%s\n\n:%s: %s\n\n%s' % (head, title, ' '.join(fmted), tail)
|
||||
lines[:] = doc.split('\n')
|
||||
|
||||
|
||||
def class_doc_handler(app, what, name, cls, options, lines):
|
||||
if what == 'class':
|
||||
if issubclass(cls, HasProperties):
|
||||
append_to_doc(cls, lines, Property, 'properties', 'properties', fmt_property)
|
||||
if issubclass(cls, Module):
|
||||
append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param)
|
||||
append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command)
|
@ -270,7 +270,10 @@ class Enum(dict):
|
||||
self.name = name
|
||||
|
||||
def __getattr__(self, key):
|
||||
return self[key]
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError as e:
|
||||
raise AttributeError(str(e))
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if self.name and key != 'name':
|
||||
@ -286,7 +289,7 @@ class Enum(dict):
|
||||
raise TypeError('Enum %r can not be changed!' % self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Enum %r (%d values)>' % (self.name, len(self)//2)
|
||||
return 'Enum(%r, %s)' % (self.name, ', '.join('%s=%d' % (m.name, m.value) for m in self.members))
|
||||
|
||||
def __call__(self, key):
|
||||
return self[key]
|
||||
|
@ -46,19 +46,32 @@ from secop.poller import Poller, BasicPoller
|
||||
|
||||
|
||||
class Module(HasProperties, metaclass=ModuleMeta):
|
||||
"""Basic Module
|
||||
"""basic module
|
||||
|
||||
ALL secop Modules derive from this
|
||||
all SECoP modules derive from this.
|
||||
|
||||
note: within Modules, parameters should only be addressed as self.<pname>
|
||||
i.e. self.value, self.target etc...
|
||||
these are accessing the cached version.
|
||||
they can also be written to (which auto-calls self.write_<pname> and
|
||||
generate an async update)
|
||||
: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
|
||||
|
||||
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 from the hardware', call self.read_<pname>() instead
|
||||
the return value of this method will be used as the new cached value and
|
||||
be an async update sent automatically.
|
||||
"""
|
||||
# static properties, definitions in derived classes should overwrite earlier ones.
|
||||
# note: properties don't change after startup and are usually filled
|
||||
@ -88,7 +101,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
# reference to the dispatcher (used for sending async updates)
|
||||
DISPATCHER = None
|
||||
|
||||
pollerClass = Poller
|
||||
pollerClass = Poller #: default poller used
|
||||
|
||||
def __init__(self, name, logger, cfgdict, srv):
|
||||
# remember the dispatcher object (for the async callbacks)
|
||||
@ -390,12 +403,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
|
||||
|
||||
class Readable(Module):
|
||||
"""Basic readable Module
|
||||
|
||||
providing the readonly parameter 'value' and 'status'
|
||||
|
||||
Also allow configurable polling per 'pollinterval' parameter.
|
||||
"""
|
||||
"""basic readable Module"""
|
||||
# pylint: disable=invalid-name
|
||||
Status = Enum('Status',
|
||||
IDLE = 100,
|
||||
@ -404,7 +412,7 @@ class Readable(Module):
|
||||
ERROR = 400,
|
||||
DISABLED = 0,
|
||||
UNKNOWN = 401,
|
||||
)
|
||||
) #: status codes
|
||||
parameters = {
|
||||
'value': Parameter('current value of the Module', readonly=True,
|
||||
datatype=FloatRange(),
|
||||
@ -467,10 +475,7 @@ class Readable(Module):
|
||||
|
||||
|
||||
class Writable(Readable):
|
||||
"""Basic Writable Module
|
||||
|
||||
providing a settable 'target' parameter to those of a Readable
|
||||
"""
|
||||
"""basic writable module"""
|
||||
parameters = {
|
||||
'target': Parameter('target value of the Module',
|
||||
default=0, readonly=False, datatype=FloatRange(),
|
||||
@ -479,13 +484,9 @@ class Writable(Readable):
|
||||
|
||||
|
||||
class Drivable(Writable):
|
||||
"""Basic Drivable Module
|
||||
"""basic drivable module"""
|
||||
|
||||
provides a stop command to interrupt actions.
|
||||
Also status gets extended with a BUSY state indicating a running action.
|
||||
"""
|
||||
|
||||
Status = Enum(Readable.Status, BUSY=300)
|
||||
Status = Enum(Readable.Status, BUSY=300) #: status codes
|
||||
|
||||
commands = {
|
||||
'stop': Command(
|
||||
@ -500,11 +501,17 @@ class Drivable(Writable):
|
||||
}
|
||||
|
||||
def isBusy(self, status=None):
|
||||
"""helper function for treating substates of BUSY correctly"""
|
||||
"""check for busy, treating substates correctly
|
||||
|
||||
returns True when busy (also when finalizing)
|
||||
"""
|
||||
return 300 <= (status or self.status)[0] < 400
|
||||
|
||||
def isDriving(self, status=None):
|
||||
"""helper function (finalize is busy, not driving)"""
|
||||
"""check for driving, treating status substates correctly
|
||||
|
||||
returns True when busy, but not finalizing
|
||||
"""
|
||||
return 300 <= (status or self.status)[0] < 390
|
||||
|
||||
# improved polling: may poll faster if module is BUSY
|
||||
@ -532,10 +539,7 @@ class Drivable(Writable):
|
||||
|
||||
|
||||
class Communicator(Module):
|
||||
"""Basic communication Module
|
||||
|
||||
providing no parameters, but a 'communicate' command.
|
||||
"""
|
||||
"""basic abstract communication module"""
|
||||
|
||||
commands = {
|
||||
"communicate": Command("provides the simplest mean to communication",
|
||||
@ -554,6 +558,14 @@ class Communicator(Module):
|
||||
|
||||
|
||||
class Attached(Property):
|
||||
"""a special property, defining an attached modle
|
||||
|
||||
assign a module name to this property in the cfg file,
|
||||
and the server will create an attribute with this module
|
||||
|
||||
:param attrname: the name of the to be created attribute. if not given
|
||||
the attribute name is the property name prepended by an underscore.
|
||||
"""
|
||||
# we can not put this to properties.py, as it needs datatypes
|
||||
def __init__(self, attrname=None):
|
||||
self.attrname = attrname
|
||||
|
135
secop/params.py
135
secop/params.py
@ -23,8 +23,9 @@
|
||||
"""Define classes for Parameters/Commands and Overriding them"""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
import inspect
|
||||
import itertools
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
|
||||
NoneOr, TextType, IntRange, TupleOf
|
||||
@ -82,58 +83,59 @@ class Accessible(HasProperties):
|
||||
|
||||
|
||||
class Parameter(Accessible):
|
||||
"""storage for Parameter settings + value + qualifiers
|
||||
"""defines a parameter
|
||||
|
||||
:param description: description
|
||||
:param datatype: the datatype
|
||||
:param inherit: whether properties not given should be inherited.
|
||||
defaults to True when datatype or description is missing, else to False
|
||||
:param ctr: inherited ctr
|
||||
:param internally_called: True when called internally, else called from a definition
|
||||
:param reorder: when True, put this parameter after all inherited items in the accessible list
|
||||
:param kwds: optional properties
|
||||
|
||||
if readonly is False, the value can be changed (by code, or remote)
|
||||
if no default is given, the parameter MUST be specified in the configfile
|
||||
during startup, value is initialized with the default value or
|
||||
from the config file if specified there
|
||||
|
||||
poll can be:
|
||||
- None: will be converted to True/False if handler is/is not None
|
||||
- False or 0 (never poll this parameter)
|
||||
- True or > 0 (poll this parameter)
|
||||
- the exact meaning depends on the used poller
|
||||
meaning for secop.poller.Poller:
|
||||
- 1 or True (AUTO), converted to SLOW (readonly=False), DYNAMIC('status' and 'value') or REGULAR(else)
|
||||
- 2 (SLOW), polled with lower priority and a multiple of pollperiod
|
||||
- 3 (REGULAR), polled with pollperiod
|
||||
- 4 (DYNAMIC), polled with pollperiod, if not BUSY, else with a fraction of pollperiod
|
||||
meaning for the basicPoller:
|
||||
- True or 1 (poll this every pollinterval)
|
||||
- positive int (poll every N(th) pollinterval)
|
||||
- negative int (normally poll every N(th) pollinterval, if module is busy, poll every pollinterval)
|
||||
note: Drivable (and derived classes) poll with 10 fold frequency if module is busy....
|
||||
:param ctr: (for internal use only)
|
||||
:param internally_used: (for internal use only)
|
||||
"""
|
||||
# storage for Parameter settings + value + qualifiers
|
||||
|
||||
properties = {
|
||||
'description': Property('Description of the Parameter', TextType(),
|
||||
'description': Property('mandatory description of the parameter', TextType(),
|
||||
extname='description', mandatory=True),
|
||||
'datatype': Property('Datatype of the Parameter', DataTypeType(),
|
||||
'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(),
|
||||
extname='datainfo', mandatory=True),
|
||||
'readonly': Property('Is the Parameter readonly? (vs. changeable via SECoP)', BoolType(),
|
||||
'readonly': Property('not changeable via SECoP (default True)', BoolType(),
|
||||
extname='readonly', default=True),
|
||||
'group': Property('Optional parameter group this parameter belongs to', StringType(),
|
||||
'group': Property('optional parameter group this parameter belongs to', StringType(),
|
||||
extname='group', default=''),
|
||||
'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),
|
||||
extname='visibility', default=1),
|
||||
'constant': Property('Optional constant value for constant parameters', ValueType(),
|
||||
'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.',
|
||||
'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('Is this parameter accessible via SECoP? (vs. internal parameter)',
|
||||
'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('Polling indicator', NoneOr(IntRange()), export=False, default=None),
|
||||
'needscfg': Property('needs value in config', NoneOr(BoolType()), export=False, default=None),
|
||||
'optional': Property('[Internal] is this parameter optional?', BoolType(), export=False,
|
||||
'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,
|
||||
settable=False, default=False),
|
||||
'handler': Property('[internal] overload the standard read and write functions',
|
||||
ValueType(), export=False, default=None, mandatory=False, settable=False),
|
||||
@ -143,7 +145,7 @@ class Parameter(Accessible):
|
||||
}
|
||||
|
||||
def __init__(self, description=None, datatype=None, inherit=True, *,
|
||||
ctr=None, internally_called=False, reorder=False, **kwds):
|
||||
reorder=False, ctr=None, internally_called=False, **kwds):
|
||||
if datatype is not None:
|
||||
if not isinstance(datatype, DataType):
|
||||
if isinstance(datatype, type) and issubclass(datatype, DataType):
|
||||
@ -153,11 +155,14 @@ class Parameter(Accessible):
|
||||
raise ProgrammingError(
|
||||
'datatype MUST be derived from class DataType!')
|
||||
kwds['datatype'] = datatype
|
||||
|
||||
if description is not None:
|
||||
if not internally_called:
|
||||
description = inspect.cleandoc(description)
|
||||
kwds['description'] = description
|
||||
|
||||
unit = kwds.pop('unit', None)
|
||||
if unit is not None: # for legacy code only
|
||||
if unit is not None and datatype: # for legacy code only
|
||||
datatype.setProperty('unit', unit)
|
||||
|
||||
constant = kwds.get('constant')
|
||||
@ -179,6 +184,8 @@ class Parameter(Accessible):
|
||||
if inherit:
|
||||
if reorder:
|
||||
kwds['ctr'] = next(object_counter)
|
||||
if unit is not None:
|
||||
kwds['unit'] = unit
|
||||
self.kwds = kwds # contains only the items which must be overwritten
|
||||
|
||||
# internal caching: value and timestamp of last change...
|
||||
@ -249,7 +256,7 @@ class Override:
|
||||
"""Stores the overrides to be applied to a Parameter
|
||||
|
||||
note: overrides are applied by the metaclass during class creating
|
||||
reorder= True: use position of Override instead of inherited for the order
|
||||
reorder=True: use position of Override instead of inherited for the order
|
||||
"""
|
||||
def __init__(self, description="", datatype=None, *, reorder=False, **kwds):
|
||||
self.kwds = kwds
|
||||
@ -270,29 +277,33 @@ class Override:
|
||||
|
||||
|
||||
class Command(Accessible):
|
||||
"""storage for Commands settings (description + call signature...)
|
||||
"""
|
||||
# to be merged with usercommand
|
||||
properties = {
|
||||
'description': Property('Description of the Command', TextType(),
|
||||
'description': Property('description of the Command', TextType(),
|
||||
extname='description', export=True, mandatory=True),
|
||||
'group': Property('Optional command group of the command.', StringType(),
|
||||
'group': Property('optional command group of the command.', StringType(),
|
||||
extname='group', export=True, default=''),
|
||||
'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),
|
||||
extname='visibility', export=True, default=1),
|
||||
'export': Property('[internal] Flag: is the command accessible via SECoP? (vs. pure internal use)',
|
||||
'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\'',
|
||||
DataTypeType(), extname='datainfo', mandatory=True),
|
||||
'argument': Property('Datatype of the argument to the command, or None.',
|
||||
'argument': Property('datatype of the argument to the command, or None',
|
||||
NoneOr(DataTypeType()), export=False, mandatory=True),
|
||||
'result': Property('Datatype of the result from the command, or None.',
|
||||
'result': Property('datatype of the result from the command, or None',
|
||||
NoneOr(DataTypeType()), export=False, mandatory=True),
|
||||
}
|
||||
|
||||
def __init__(self, description=None, *, ctr=None, inherit=True,
|
||||
internally_called=False, reorder=False, **kwds):
|
||||
def __init__(self, description=None, *, reorder=False, inherit=True,
|
||||
internally_called=False, ctr=None, **kwds):
|
||||
if internally_called:
|
||||
inherit = False
|
||||
# make sure either all or no datatype info is in kwds
|
||||
@ -326,27 +337,39 @@ class Command(Accessible):
|
||||
|
||||
|
||||
class usercommand(Command):
|
||||
"""decorator to turn a method into a command"""
|
||||
"""decorator to turn a method into a command
|
||||
|
||||
:param argument: the datatype of the argument or None
|
||||
:param result: the datatype of the result or None
|
||||
:param inherit: whether properties not given should be inherited.
|
||||
defaults to True when datatype or description is missing, else to False
|
||||
:param reorder: when True, put this command after all inherited items in the accessible list
|
||||
:param kwds: optional properties
|
||||
|
||||
{all properties}
|
||||
"""
|
||||
|
||||
func = None
|
||||
|
||||
def __init__(self, arg0=False, result=None, inherit=True, *, internally_called=False, **kwds):
|
||||
if result or kwds or isinstance(arg0, DataType) or not callable(arg0):
|
||||
argument = kwds.pop('argument', arg0) # normal case
|
||||
def __init__(self, argument=False, result=None, inherit=True, **kwds):
|
||||
if result or kwds or isinstance(argument, DataType) or not callable(argument):
|
||||
# normal case
|
||||
self.func = None
|
||||
if argument is False and result:
|
||||
argument = None
|
||||
if argument is not False:
|
||||
if isinstance(argument, (tuple, list)):
|
||||
# goodie: allow declaring multiple arguments as a tuple
|
||||
# TODO: check that calling works properly
|
||||
argument = TupleOf(*argument)
|
||||
kwds['argument'] = argument
|
||||
kwds['result'] = result
|
||||
self.kwds = kwds
|
||||
else:
|
||||
# goodie: allow @usercommand instead of @usercommand()
|
||||
self.func = arg0 # this is the wrapped method!
|
||||
if arg0.__doc__ is not None:
|
||||
kwds['description'] = arg0.__doc__
|
||||
self.func = argument # this is the wrapped method!
|
||||
if argument.__doc__ is not None:
|
||||
kwds['description'] = argument.__doc__
|
||||
self.name = self.func.__name__
|
||||
super().__init__(kwds.pop('description', ''), inherit=inherit, **kwds)
|
||||
|
||||
|
@ -40,10 +40,10 @@ from secop.lib import mkthread
|
||||
from secop.errors import ProgrammingError
|
||||
|
||||
# poll types:
|
||||
AUTO = 1 # equivalent to True, converted to REGULAR, SLOW or DYNAMIC
|
||||
SLOW = 2
|
||||
REGULAR = 3
|
||||
DYNAMIC = 4
|
||||
AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC
|
||||
SLOW = 2 #: polling with low priority and increased poll interval (used by default when readonly=False)
|
||||
REGULAR = 3 #: polling with standard interval (used by default for read only parameters except status and value)
|
||||
DYNAMIC = 4 #: polling with shorter poll interval when BUSY (used by default for status and value)
|
||||
|
||||
|
||||
class PollerBase:
|
||||
|
@ -23,6 +23,7 @@
|
||||
"""Define validated data types."""
|
||||
|
||||
|
||||
import inspect
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.errors import ProgrammingError, ConfigError, BadValueError
|
||||
@ -47,19 +48,26 @@ def flatten_dict(dictname, itemcls, attrs, remove=True):
|
||||
|
||||
# storage for 'properties of a property'
|
||||
class Property:
|
||||
'''base class holding info about a 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!
|
||||
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
|
||||
:param extname: external name
|
||||
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given
|
||||
:param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value
|
||||
assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute)
|
||||
:param settable: settable from the cfg file
|
||||
"""
|
||||
|
||||
properties are only sent to the ECS if export is True, or an extname is set
|
||||
if mandatory is True, they MUST have a value in the cfg file assigned to them.
|
||||
otherwise, this is optional in which case the default value is applied.
|
||||
All values MUST pass the datatype.
|
||||
'''
|
||||
# note: this is intended to be used on base classes.
|
||||
# the VALUES of the properties are on the instances!
|
||||
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 = inspect.cleandoc(description)
|
||||
self.default = datatype.default if default is None else datatype(default)
|
||||
self.datatype = datatype
|
||||
self.extname = extname
|
||||
|
@ -49,8 +49,12 @@ class StringIO(Communicator):
|
||||
Property('used encoding', datatype=StringType(),
|
||||
default='ascii', settable=True),
|
||||
'identification':
|
||||
Property('a list of tuples with commands and expected responses as regexp',
|
||||
datatype=ArrayOf(TupleOf(StringType(),StringType())), default=[], export=False),
|
||||
Property('''
|
||||
identification
|
||||
|
||||
a list of tuples with commands and expected responses as regexp,
|
||||
to be sent on connect''',
|
||||
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False),
|
||||
}
|
||||
parameters = {
|
||||
'timeout':
|
||||
@ -65,7 +69,7 @@ class StringIO(Communicator):
|
||||
commands = {
|
||||
'multicomm':
|
||||
Command('execute multiple commands in one go',
|
||||
argument=ArrayOf(StringType()), result= ArrayOf(StringType()))
|
||||
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
|
||||
}
|
||||
|
||||
_reconnectCallbacks = None
|
||||
@ -221,7 +225,8 @@ class HasIodev(Module):
|
||||
"""
|
||||
properties = {
|
||||
'iodev': Attached(),
|
||||
'uri': Property('uri for auto creation of iodev', StringType(), default=''),
|
||||
'uri': Property('uri for automatic creation of the attached communication module',
|
||||
StringType(), default=''),
|
||||
}
|
||||
|
||||
iodevDict = {}
|
||||
|
Reference in New Issue
Block a user