[WIP] work on curses cfg editor
state as of 2026-01-28 Change-Id: I73d2fa4e6fda8820a95fe4e7256c7a23bf565f67
This commit is contained in:
@@ -99,7 +99,7 @@ class SimpleDataType(HasProperties):
|
||||
- StringType: the bare string is returned
|
||||
- EnumType: the name of the enum is returned
|
||||
"""
|
||||
return self.format_value(value, False)
|
||||
return value if isinstance(value, str) else repr(value)
|
||||
|
||||
def export_value(self, value):
|
||||
"""if needed, reformat value for transport"""
|
||||
@@ -1132,7 +1132,7 @@ class CommandType(DataType):
|
||||
|
||||
# internally used datatypes (i.e. only for programming the SEC-node)
|
||||
|
||||
class DefaultType(DataType):
|
||||
class DefaultType(SimpleDataType):
|
||||
"""datatype used as default for parameters
|
||||
|
||||
needs some minimal interface to avoid errors when
|
||||
|
||||
@@ -492,3 +492,15 @@ def delayed_import(modname):
|
||||
except Exception:
|
||||
return _Raiser(modname)
|
||||
return module
|
||||
|
||||
|
||||
class LazyImport:
|
||||
module = None
|
||||
|
||||
def __init__(self, modulename):
|
||||
self.modulename = modulename
|
||||
|
||||
def __getattr__(self, name):
|
||||
if self.module is None:
|
||||
self.module = __import__(self.modulename)
|
||||
return getattr(self.module, name)
|
||||
0
frappy/tools/__init__.py
Normal file
0
frappy/tools/__init__.py
Normal file
295
frappy/tools/completion.py
Normal file
295
frappy/tools/completion.py
Normal file
@@ -0,0 +1,295 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from importlib import import_module
|
||||
from frappy.core import Module, Parameter, Property
|
||||
from frappy.datatypes import DataType
|
||||
from frappy.lib.comparestring import compare
|
||||
from frappy.tools.configdata import site, ClassChecker, ModuleClass
|
||||
from frappy.tools.terminalgui import Completion
|
||||
|
||||
|
||||
SKIP_PROPS = {'implementation', 'features',
|
||||
'interface_classes', 'slowinterval', 'omit_unchanged_within', 'original_id'}
|
||||
|
||||
def recommended_prs(cls):
|
||||
"""get properties and parameters which are useful in configuration
|
||||
|
||||
returns a dict <pname> of <is mandatory (bool)>
|
||||
"""
|
||||
if isinstance(cls, str):
|
||||
try:
|
||||
cls = ClassCompletion.validate(cls)
|
||||
except Exception:
|
||||
return {}
|
||||
result = {}
|
||||
for pname, pdict in cls.configurables.items():
|
||||
pr = getattr(cls, pname)
|
||||
if isinstance(pr, Property):
|
||||
if pname not in SKIP_PROPS:
|
||||
result[pname] = pr.mandatory
|
||||
elif isinstance(pr, Parameter):
|
||||
if pr.needscfg:
|
||||
result[pname] = True
|
||||
elif not pr.readonly or hasattr(cls, f'write_{pname}'):
|
||||
result[pname] = False
|
||||
if result.get('uri') is False and result.get('io') is False:
|
||||
result['io'] = True
|
||||
return result
|
||||
|
||||
|
||||
|
||||
class FrappyModule(str):
|
||||
"""checker for finding subclasses of Module defined in a python module"""
|
||||
def check(self, cls):
|
||||
return isinstance(cls, type) and issubclass(cls, Module) and self.startswith(cls.__module__)
|
||||
|
||||
|
||||
def get_suggested(guess, allowed_keys):
|
||||
"""select and sort values from allowed_keys based on similarity to a given value
|
||||
|
||||
:param guess: given value. when empty, the allowed keys are return in the given order
|
||||
:param allowed_keys: list of values sorted in a given order
|
||||
:return: a list of values
|
||||
"""
|
||||
if len(guess) == 0:
|
||||
return allowed_keys
|
||||
low = guess.lower()
|
||||
if len(guess) == 1:
|
||||
# return only items starting with a single letter
|
||||
return [k for k in allowed_keys if k.lower().startswith(low)]
|
||||
comp = {}
|
||||
# if there are items containing guess, return them only
|
||||
result = [k for k in allowed_keys if low in k.lower()]
|
||||
if result:
|
||||
return result
|
||||
# calculate similarity
|
||||
for key in allowed_keys:
|
||||
comp[key] = compare(guess, key)
|
||||
comp = sorted(comp.items(), key=lambda t: t[1], reverse=True)
|
||||
scorelimit = 2
|
||||
result = []
|
||||
for i, (key, score) in enumerate(comp):
|
||||
if score < scorelimit:
|
||||
break
|
||||
if i > 2:
|
||||
scorelimit = max(2, score - 0.05)
|
||||
result.append(key)
|
||||
return result or allowed_keys
|
||||
|
||||
|
||||
class CheckerObsolete:
|
||||
root = None
|
||||
modobj = None
|
||||
clsobj = None
|
||||
modname = None
|
||||
pyfile = None
|
||||
|
||||
def module(self, base, name):
|
||||
modname = f'{base}.{name}' if base else name
|
||||
try:
|
||||
self.modobj = self.root = import_module(modname)
|
||||
self.modname = modname
|
||||
self.pyfile = Path(self.modobj.__file__)
|
||||
return None
|
||||
except ImportError as e:
|
||||
return str(e)
|
||||
except Exception as e:
|
||||
return f'{modname}: {e!r}'
|
||||
|
||||
def cls(self, base, name):
|
||||
try:
|
||||
self.clsobj = getattr(self.root, name)
|
||||
return None
|
||||
except Exception:
|
||||
return f'{base}.{name} does not exist'
|
||||
|
||||
|
||||
def class_completion(value):
|
||||
checker = ClassChecker(value)
|
||||
if checker.position == len(value):
|
||||
return checker.position, []
|
||||
if checker.root is None:
|
||||
sdict = {p: f'{p}.' for p in site.packages}
|
||||
else:
|
||||
sdict = {}
|
||||
file = checker.pyfile
|
||||
if not checker.clsobj:
|
||||
if file.name == '__init__.py':
|
||||
sdict = {p.stem: f'{p.stem}.'
|
||||
for p in sorted(file.parent.glob('*.py'))
|
||||
if p.stem != '__init__'}
|
||||
sdict.update((k, k) for k, v in sorted(inspect.getmembers(
|
||||
checker.root, FrappyModule(checker.modname).check)))
|
||||
found = sdict.get(checker.name, None)
|
||||
if found:
|
||||
selection = [found]
|
||||
# selection = [found] + list(sdict.values())
|
||||
else:
|
||||
selection = list(get_suggested(checker.name, sdict.values()))
|
||||
return checker.position, [checker.name] + selection
|
||||
|
||||
|
||||
|
||||
class Base(Completion, DataType):
|
||||
def __init__(self, callback):
|
||||
self.callback = callback
|
||||
super().__init__()
|
||||
|
||||
def from_string(self, strvalue):
|
||||
value = self.validate(strvalue)
|
||||
if self.callback:
|
||||
self.callback(value)
|
||||
return value
|
||||
|
||||
def to_string(self, value):
|
||||
value = self.validate(value)
|
||||
return f'{value.__module__}.{value.__qualname__}'
|
||||
|
||||
def format_value(self, value, unit=None):
|
||||
result = repr(self.to_string(value))
|
||||
if '<' in result:
|
||||
raise ValueError(result, value)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
class ClassCompletionObsolete(Base):
|
||||
@staticmethod
|
||||
def propose(value, get_clsobj=False):
|
||||
"""analyze value to propositions of class path
|
||||
|
||||
returns the length of the valid part and a list of guesses
|
||||
"""
|
||||
clspath = value.split('.')
|
||||
check = CheckerObsolete()
|
||||
for pathpos, name in enumerate(clspath):
|
||||
base = '.'.join(clspath[:pathpos])
|
||||
clsroot = check.root
|
||||
if name:
|
||||
if check.clsobj:
|
||||
error = check.cls(base, name)
|
||||
elif name.isupper():
|
||||
error = check.cls(base, name)
|
||||
if error and check.module(base, name) is None:
|
||||
error = None
|
||||
else:
|
||||
error = check.module(base, name)
|
||||
if error and check.cls(base, name) is None:
|
||||
error = None
|
||||
else:
|
||||
error = 'empty element'
|
||||
|
||||
if get_clsobj:
|
||||
if error:
|
||||
return None, error
|
||||
elif pathpos == len(clspath) - 1 or error:
|
||||
# get suggestions
|
||||
# sdict is a dict '<proposed name>' of '<name>' or '<name>.'
|
||||
# the latter when it is a pymodule
|
||||
# sdict = {name: None}
|
||||
if pathpos == 0:
|
||||
sdict = {p: f'{p}.' for p in site.packages}
|
||||
else:
|
||||
sdict = {}
|
||||
file = check.pyfile
|
||||
if not check.clsobj:
|
||||
if file.name == '__init__.py':
|
||||
sdict = {p.stem: f'{p.stem}.'
|
||||
for p in sorted(file.parent.glob('*.py'))
|
||||
if p.stem != '__init__'}
|
||||
sdict.update((k, k) for k, v in sorted(inspect.getmembers(
|
||||
clsroot, FrappyModule(check.modname).check)))
|
||||
found = sdict.get(name, None)
|
||||
if found:
|
||||
selection = [found]
|
||||
# selection = [found] + list(sdict.values())
|
||||
else:
|
||||
selection = list(get_suggested(name, sdict.values()))
|
||||
position = sum(len(v) for v in clspath[:pathpos]) + pathpos
|
||||
return position, [name] + selection
|
||||
check.root = check.clsobj or check.modobj
|
||||
if get_clsobj:
|
||||
return check.clsobj, error
|
||||
return 0, None
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value, previous=None):
|
||||
if isinstance(value, type):
|
||||
if issubclass(value, Module):
|
||||
return value
|
||||
raise ValueError('value is a class, but not a frappy module')
|
||||
clsobj, error = cls.propose(value, True)
|
||||
if error:
|
||||
raise ValueError(error)
|
||||
return clsobj
|
||||
|
||||
|
||||
class NameCompletion(Completion, DataType):
|
||||
# TODO: make obsolete
|
||||
def __init__(self, callback, get_name_info):
|
||||
self.callback = callback
|
||||
self.get_name_info = get_name_info
|
||||
super().__init__()
|
||||
|
||||
def get_selection(self, param):
|
||||
cls, used_names = self.get_name_info()
|
||||
if param:
|
||||
names = dict(self.cls.configurables)
|
||||
selection = []
|
||||
for name in recommended_prs(self.cls):
|
||||
names.pop(name, None)
|
||||
if name not in used_names:
|
||||
selection.append(name)
|
||||
selection.extend(k for k in names if k not in used_names)
|
||||
else:
|
||||
names = dict(cls.configurables.get(param, {}))
|
||||
selection = [k for k in names if f'{param}.{k}' not in used_names]
|
||||
return selection
|
||||
|
||||
def propose(self, value):
|
||||
"""analyze value to propositions of class path
|
||||
|
||||
returns the length of the valid part and a list of guesses
|
||||
"""
|
||||
param, dot, prop = value.partition('.')
|
||||
if (param and not param.isidentifier()) or (prop and not prop.isidentifier()):
|
||||
return 0, None
|
||||
cls, used_names = self.get_name_info()
|
||||
if dot:
|
||||
print(param, repr(dot), prop, list(cls.configurables.get(param, {})))
|
||||
position = len(param) + 1
|
||||
selection = [k for k in cls.configurables.get(param, {}) if f'{param}.{k}' not in used_names]
|
||||
guess = prop
|
||||
else:
|
||||
position = 0
|
||||
selection = {}
|
||||
for name in recommended_prs(cls):
|
||||
if name not in used_names:
|
||||
selection[name] = 1
|
||||
selection.update({k: 1 for k in cls.configurables if k not in used_names})
|
||||
for k, v in cls.configurables.items():
|
||||
print(k, getattr(v, 'needscfg', ''))
|
||||
guess = param
|
||||
selection = get_suggested(guess, selection)
|
||||
return position, [guess] + selection
|
||||
442
frappy/tools/configdata.py
Normal file
442
frappy/tools/configdata.py
Normal file
@@ -0,0 +1,442 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import re
|
||||
import frappy
|
||||
from pathlib import Path
|
||||
from ast import literal_eval
|
||||
from importlib import import_module
|
||||
from frappy.config import process_file, Node
|
||||
from frappy.core import Module
|
||||
from frappy.datatypes import DataType
|
||||
|
||||
|
||||
class Site:
|
||||
domain = 'psi.ch'
|
||||
frappy_subdir = 'frappy_psi'
|
||||
base = Path(frappy.__file__).parent.parent
|
||||
|
||||
def __init__(self, domain='psi.ch', frappy_subdir='frappy_psi', default_interface='tcp://10767'):
|
||||
self.init(domain, frappy_subdir, default_interface)
|
||||
|
||||
def init(self, domain=None, frappy_subdir=None, default_interface=None):
|
||||
if domain:
|
||||
self.domain = domain
|
||||
if default_interface:
|
||||
self.default_interface = default_interface
|
||||
if frappy_subdir:
|
||||
self.packages = [v.name for v in self.base.glob('frappy_*')]
|
||||
try: # psi should be first
|
||||
self.packages.remove(frappy_subdir)
|
||||
self.packages.insert(0, frappy_subdir)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
site = Site()
|
||||
|
||||
|
||||
class NonStringType:
|
||||
"""any type except string"""
|
||||
def validate(self, value):
|
||||
self(value)
|
||||
|
||||
def __call__(self, value):
|
||||
return value
|
||||
|
||||
|
||||
def from_string(self, strvalue):
|
||||
"""convert from string """
|
||||
try:
|
||||
return literal_eval(strvalue)
|
||||
except Exception:
|
||||
raise ValueError('this is no python value')
|
||||
|
||||
def to_string(self, value):
|
||||
return repr(value)
|
||||
|
||||
def format_value(self, value, unit=None):
|
||||
"""convert to python code (inverse of ast.literal_eval)
|
||||
|
||||
:param value: the value
|
||||
:param unit: must be False (needed for compatibility with frappy.datatypes.DataType)
|
||||
:return: version to used as python code
|
||||
"""
|
||||
return repr(value)
|
||||
|
||||
|
||||
class SimpleStringType(NonStringType):
|
||||
def validate(self, value):
|
||||
pass
|
||||
|
||||
def from_string(self, strvalue):
|
||||
"""convert from string """
|
||||
return strvalue
|
||||
|
||||
def to_string(self, value):
|
||||
return value
|
||||
|
||||
def format_value(self, value, unit=None):
|
||||
"""convert to string
|
||||
|
||||
:param value:
|
||||
:param unit: must be False (needed for compatibility with frappy datatypes
|
||||
:return: stringified version (triple quoted when containing line breaks)
|
||||
"""
|
||||
if '\n' in value:
|
||||
value = value.replace('"""', '\\"\\"\\"')
|
||||
return f'"""{value}"""'
|
||||
return repr(value)
|
||||
|
||||
|
||||
nonstringtype = NonStringType()
|
||||
stringtype = SimpleStringType()
|
||||
|
||||
|
||||
class Value:
|
||||
"""a value with additional info
|
||||
|
||||
- hold a value (self.value) and a stringified value self.strvalue
|
||||
- set from and get a string representation for use in a input element
|
||||
- get a python code representation (to be reverted with ast.literal_eval)
|
||||
- verify if the datatype is valid (this typically needs extension)
|
||||
- get information for completion
|
||||
"""
|
||||
error = None
|
||||
strvalue = None
|
||||
modulecls = None
|
||||
datatype = None
|
||||
value = None
|
||||
completion = None
|
||||
|
||||
def __init__(self, value, datatype=None, error=None, from_string=False, callback=None):
|
||||
if value is None:
|
||||
raise ValueError(datatype)
|
||||
self.datatype = datatype
|
||||
self.error = error
|
||||
if callback:
|
||||
self.callback = callback
|
||||
if from_string:
|
||||
if datatype is None:
|
||||
try:
|
||||
literal_eval(value)
|
||||
self.datatype = nonstringtype
|
||||
except Exception:
|
||||
self.datatype = stringtype
|
||||
self.set_from_string(value)
|
||||
else:
|
||||
if datatype is None:
|
||||
self.datatype = stringtype if isinstance(value, str) else nonstringtype
|
||||
self.set_value(value)
|
||||
|
||||
def callback(self, value):
|
||||
return value
|
||||
|
||||
def set_value(self, value):
|
||||
self.strvalue = None
|
||||
try:
|
||||
dt = self.datatype
|
||||
value = dt(value)
|
||||
self.strvalue = dt.to_string(value)
|
||||
dt.validate(value)
|
||||
self.value = self.callback(value)
|
||||
except Exception as e:
|
||||
self.value = value
|
||||
self.error = repr(e)
|
||||
if self.strvalue is None:
|
||||
self.strvalue = str(value)
|
||||
|
||||
def validate_from_string(self, strvalue):
|
||||
self.strvalue = strvalue
|
||||
self.value = self.callback(self.datatype.from_string(strvalue))
|
||||
if type(self.datatype).__name__ == 'ClassCompletion':
|
||||
raise ValueError(self)
|
||||
|
||||
def set_from_string(self, strvalue):
|
||||
self.strvalue = strvalue
|
||||
try:
|
||||
self.validate_from_string(strvalue)
|
||||
except Exception as e:
|
||||
self.error = repr(e)
|
||||
|
||||
def get_repr(self):
|
||||
"""convert string value to repr
|
||||
|
||||
:return: repr, to be used when building config code
|
||||
"""
|
||||
if self.datatype:
|
||||
try:
|
||||
return self.datatype.format_value(self.value, False)
|
||||
except Exception as e:
|
||||
pass
|
||||
return repr(self.strvalue)
|
||||
|
||||
def __repr__(self):
|
||||
return f'Value({self.value!r}, {self.datatype!r})'
|
||||
|
||||
|
||||
def get_datatype(pname, cls, value):
|
||||
"""
|
||||
|
||||
:param pname: <property name> or <parameter name> or <parametger name>.<property name>
|
||||
:param cls: a frappy Module class or None
|
||||
:param value: the given value (needed only in case the datatype can not be determined)
|
||||
:return:
|
||||
"""
|
||||
param, _, prop = pname.partition('.')
|
||||
error = None
|
||||
if cls:
|
||||
try:
|
||||
prop_param = cls.configurables[param]
|
||||
if isinstance(prop_param, dict):
|
||||
propobj = prop_param.get(prop) if prop else cls.accessibles.get(param)
|
||||
if propobj is None:
|
||||
error = f'{cls.__module__}.{cls.__qualname__}.{param}.{prop} is not configurable'
|
||||
else:
|
||||
return propobj.datatype, None
|
||||
elif prop:
|
||||
error = f'{cls.__module__}.{cls.__qualname__}.{param} is not a parameter'
|
||||
else:
|
||||
return prop_param.datatype, None
|
||||
except AttributeError:
|
||||
error = f'{cls.__module__}.{cls.__qualname__} is not a Frappy Module'
|
||||
except KeyError:
|
||||
error = f'{cls.__module__}.{cls.__qualname__}.{param} is not configurable'
|
||||
if isinstance(value, str):
|
||||
return stringtype, error
|
||||
return nonstringtype, error
|
||||
|
||||
|
||||
def make_value(pname, cls, value):
|
||||
"""make value object"""
|
||||
return Value(value, *get_datatype(pname, cls, value))
|
||||
|
||||
|
||||
class ClassChecker:
|
||||
root = None
|
||||
modobj = None
|
||||
clsobj = None
|
||||
modname = None
|
||||
pyfile = None
|
||||
error = None
|
||||
|
||||
def __init__(self, clsstr):
|
||||
"""analyze clsstr
|
||||
|
||||
returns the length of the valid part and a list of guesses
|
||||
"""
|
||||
clspath = clsstr.split('.')
|
||||
for pathpos, name in enumerate(clspath):
|
||||
base = '.'.join(clspath[:pathpos])
|
||||
if name:
|
||||
if self.clsobj:
|
||||
error = self.cls(base, name)
|
||||
elif name.isupper():
|
||||
error = self.cls(base, name)
|
||||
if error and self.module(base, name) is None:
|
||||
error = None
|
||||
else:
|
||||
error = self.module(base, name)
|
||||
if error and self.cls(base, name) is None:
|
||||
error = None
|
||||
else:
|
||||
error = 'empty element'
|
||||
|
||||
if error:
|
||||
self.name = name
|
||||
self.error = error
|
||||
self.position = sum(len(v) for v in clspath[:pathpos]) + pathpos
|
||||
return
|
||||
self.root = self.clsobj or self.modobj
|
||||
self.name = None
|
||||
self.error = None
|
||||
self.position = len(clsstr)
|
||||
|
||||
def module(self, base, name):
|
||||
modname = f'{base}.{name}' if base else name
|
||||
try:
|
||||
self.modobj = self.root = import_module(modname)
|
||||
self.modname = modname
|
||||
self.pyfile = Path(self.modobj.__file__)
|
||||
return None
|
||||
except ImportError as e:
|
||||
return str(e)
|
||||
except Exception as e:
|
||||
return f'{modname}: {e!r}'
|
||||
|
||||
def cls(self, base, name):
|
||||
try:
|
||||
self.clsobj = getattr(self.root, name)
|
||||
return None
|
||||
except Exception:
|
||||
return f'{base}.{name} does not exist'
|
||||
|
||||
|
||||
class ModuleClass(DataType):
|
||||
@classmethod
|
||||
def validate(cls, value, previous=None):
|
||||
if isinstance(value, type):
|
||||
if issubclass(value, Module):
|
||||
return value
|
||||
raise ValueError('value is a class, but not a frappy module')
|
||||
checker = ClassChecker(value)
|
||||
if checker.error:
|
||||
raise ValueError(checker.error)
|
||||
return checker.clsobj
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, strvalue):
|
||||
return cls.validate(strvalue)
|
||||
|
||||
@classmethod
|
||||
def to_string(cls, value):
|
||||
value = cls.validate(value)
|
||||
return f'{value.__module__}.{value.__qualname__}'
|
||||
|
||||
@classmethod
|
||||
def format_value(cls, value, unit=None):
|
||||
result = repr(cls.to_string(value))
|
||||
if '<' in result:
|
||||
raise ValueError(result, value)
|
||||
return result
|
||||
|
||||
|
||||
module_class = ModuleClass()
|
||||
|
||||
|
||||
def moddata_from_cfgfile(name, cls, **kwds):
|
||||
if isinstance(cls, str):
|
||||
clsvalue = Value(cls, module_class, None, from_string=True)
|
||||
else:
|
||||
clsvalue = Value(cls, module_class, None)
|
||||
cls = None if clsvalue.error else clsvalue.value
|
||||
result = {
|
||||
'name': make_value('name', None, name),
|
||||
'cls': clsvalue,
|
||||
}
|
||||
for param, cfgvalue in kwds.items():
|
||||
if isinstance(cfgvalue, dict):
|
||||
for prop, value in cfgvalue.items():
|
||||
pname = param if prop == 'value' else f'{param}.{prop}'
|
||||
result[pname] = make_value(pname, cls, value)
|
||||
else:
|
||||
result[param] = make_value(param, cls, cfgvalue)
|
||||
return result
|
||||
|
||||
|
||||
def moddata_to_py(name, cls, description, **kwds):
|
||||
if '<' in cls.get_repr():
|
||||
raise ValueError(cls)
|
||||
items = [f'Mod({name!r}', cls.get_repr(), description.get_repr()]
|
||||
paramdict = {}
|
||||
for name, valobj in kwds.items():
|
||||
param, _, prop = name.partition('.')
|
||||
paramdict.setdefault(param, {})[prop or 'value'] = valobj
|
||||
for name, props in paramdict.items():
|
||||
valueitem = props.pop('value', None)
|
||||
if valueitem is None:
|
||||
args = []
|
||||
else:
|
||||
args = [valueitem.get_repr()]
|
||||
if not props:
|
||||
# single value
|
||||
items.append(f'{name} = {args[0]}')
|
||||
continue
|
||||
# args contains value
|
||||
# extend with keyworded values for parameter properties
|
||||
args.extend(f'{k}={v.get_repr()}' for k, v in props.items())
|
||||
items.append(f"{name} = Param({', '.join(args)})")
|
||||
items.append(')')
|
||||
return ',\n '.join(items)
|
||||
|
||||
|
||||
def fix_equipment_id(name, equipment_id):
|
||||
"""normalize equipment id"""
|
||||
if re.match(r'[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*$', equipment_id):
|
||||
return equipment_id
|
||||
return f'{name}.{site.domain}'
|
||||
|
||||
|
||||
def fix_node_class(cls):
|
||||
if cls == Node('', '')['cls']:
|
||||
return ''
|
||||
return cls
|
||||
|
||||
|
||||
def nodedata_from_cfgfile(name, equipment_id='', description='', interface='', cls='', **kwds):
|
||||
title, _, doc = description.partition('\n')
|
||||
if doc.startswith('\n'):
|
||||
doc = doc[1:]
|
||||
props = {
|
||||
'name': name,
|
||||
'title': title,
|
||||
'doc': doc,
|
||||
}
|
||||
eq = fix_equipment_id(name, equipment_id)
|
||||
if eq != fix_equipment_id(name, ''):
|
||||
props['equipment_id'] = eq
|
||||
if interface and interface != site.default_interface:
|
||||
props['interface'] = interface
|
||||
cls = fix_node_class(cls)
|
||||
if cls:
|
||||
props['cls'] = cls
|
||||
props.update(kwds)
|
||||
# TODO: do we have to check the proper datatype for node properties?
|
||||
return {k: Value(v) for k, v in props.items()}
|
||||
|
||||
|
||||
def nodedata_to_py(name, equipment_id, title, doc, interface=None, cls=None, **kwds):
|
||||
eq_id = fix_equipment_id(name.value, equipment_id.value)
|
||||
intfc = site.default_interface if interface is None else interface.value
|
||||
desc = title.value.strip()
|
||||
doc = doc.value.strip()
|
||||
if doc:
|
||||
desc = f'{desc}\n\n{doc}\n'
|
||||
items = [f"doc={Value(desc).get_repr()}\nNode({eq_id!r}, doc", f'interface={intfc!r}']
|
||||
if cls:
|
||||
clsstr = fix_node_class(cls.value)
|
||||
if clsstr:
|
||||
items.append(f'cls={clsstr!r}')
|
||||
for key, value in kwds.items():
|
||||
items.append(f'{key} = {value.get_repr()}')
|
||||
items.append(')')
|
||||
return ',\n '.join(items)
|
||||
|
||||
|
||||
def cfgdata_to_py(node, **moddata):
|
||||
"""convert cfgdata to python code
|
||||
|
||||
:param node: dict <key> of <value object>
|
||||
:param cfgdata: dict <module name> of dict <key> of <value object>
|
||||
:return: python code
|
||||
"""
|
||||
items = [nodedata_to_py(**node)] + [moddata_to_py(k, **v) for k, v in moddata.items()]
|
||||
return '\n\n'.join(items)
|
||||
|
||||
|
||||
def cfgdata_from_py(name, cfgpath, filecontent, logger):
|
||||
if filecontent:
|
||||
config = process_file(cfgpath, logger, filecontent)
|
||||
else:
|
||||
config = {}
|
||||
nodecfg = config.pop('node', {})
|
||||
modcfg = {k: moddata_from_cfgfile(k, **v) for k, v in config.items()}
|
||||
return nodedata_from_cfgfile(name, **nodecfg), modcfg
|
||||
106
frappy/tools/editorutils.py
Normal file
106
frappy/tools/editorutils.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""helper functions for configuration editor"""
|
||||
|
||||
import re
|
||||
from frappy.config import Node
|
||||
from frappy.lib import get_class
|
||||
|
||||
SITE_TAIL = 'psi.ch'
|
||||
|
||||
|
||||
def repr_param(value=object, **kwds):
|
||||
if value is object:
|
||||
if not kwds:
|
||||
return 'Param()'
|
||||
items = []
|
||||
else:
|
||||
if not kwds:
|
||||
return value
|
||||
items = [value]
|
||||
items.extend(f'{k}={v}' for k, v in kwds.items())
|
||||
return f"Param({', '.join(items)})"
|
||||
|
||||
|
||||
def repr_module(modcfg, name, cls):
|
||||
# items = [f'Mod({name}', f'cls={cls}', f'description={repr_param(**description)}'] + [
|
||||
# f'{k}={repr_param(**v)}' for k, v in kwds.items()] + [')']
|
||||
description = modcfg.pop('description', '')
|
||||
items = [f'Mod({name}', cls, repr_param(**description)] + [
|
||||
f'{k}={repr_param(**v)}' for k, v in modcfg.items()] + [')']
|
||||
return ',\n '.join(items)
|
||||
|
||||
|
||||
def repr_node(name, description, cls=None, equipment_id='', **kwds):
|
||||
equipment_id = fix_equipment_id(name, equipment_id)
|
||||
items = [f'Node({equipment_id!r}', f'description={description}']
|
||||
add_node_class(cls, kwds)
|
||||
items.extend(f'{k}={v}' for k, v in kwds.items())
|
||||
items.append(')')
|
||||
return ',\n '.join(items)
|
||||
|
||||
|
||||
def fix_equipment_id(name, equipment_id):
|
||||
if not re.match(r'[a-zA_Z0-9_]+(\.[a-zA_Z0-9_]+)*$', equipment_id):
|
||||
equipment_id = f'{name}.{SITE_TAIL}'
|
||||
return equipment_id
|
||||
|
||||
|
||||
def add_node_class(cls, result):
|
||||
if cls != Node('', '')['cls']:
|
||||
result['cls'] = cls
|
||||
|
||||
|
||||
def normalize_node(name, equipment_id='', description='', cls=None, interface=None, **kwds):
|
||||
result = {}
|
||||
eq = fix_equipment_id(name, equipment_id)
|
||||
if equipment_id and eq != equipment_id:
|
||||
result['equipment_id'] = eq
|
||||
result['description'] = description
|
||||
result['interface'] = interface or 'tcp://5555'
|
||||
add_node_class(cls, result)
|
||||
result.update(kwds)
|
||||
return result
|
||||
|
||||
|
||||
def convert_modcfg(cfgdict):
|
||||
"""convert cfgdict
|
||||
|
||||
convert parameter properties to individual items <paramname>.<propname>
|
||||
"""
|
||||
result = {}
|
||||
for key, cfgvalue in cfgdict.items():
|
||||
if isinstance(cfgvalue, dict):
|
||||
result.update((key, v) if k == 'value' else (f'{key}.{k}', v)
|
||||
for k, v in cfgvalue.items())
|
||||
else:
|
||||
result[key] = cfgvalue
|
||||
return result
|
||||
|
||||
|
||||
def needed_properties(cls):
|
||||
if isinstance(cls, str):
|
||||
cls = get_class(cls)
|
||||
result = []
|
||||
for pname, prop in cls.propertyDict.items():
|
||||
if prop.mandatory and pname not in {'implementation', 'features'}:
|
||||
result.append(pname)
|
||||
return result
|
||||
1249
frappy/tools/terminalgui.py
Normal file
1249
frappy/tools/terminalgui.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user