renamed tools to editcurses
+ fix current row for completion menu Change-Id: I46424c7b8d22f4b6646851576848c86c995a080e
This commit is contained in:
@@ -6,8 +6,8 @@ from pathlib import Path
|
||||
from psutil import pid_exists
|
||||
from frappy.errors import ConfigError
|
||||
from frappy.lib import generalConfig
|
||||
from frappy.lib import mkthread, formatExtendedTraceback
|
||||
from frappy.config import process_file, to_config_path
|
||||
from frappy.lib import formatExtendedTraceback
|
||||
from frappy.config import to_config_path
|
||||
from frappy.tools.configdata import Value, cfgdata_to_py, cfgdata_from_py, get_datatype, site, ModuleClass, stringtype
|
||||
from frappy.tools.completion import class_completion, recommended_prs
|
||||
import frappy.tools.terminalgui as tg
|
||||
@@ -290,8 +290,8 @@ class ModuleWidget(BaseWidget):
|
||||
MenuItem('add parameter/property', 'p', self.new_widget),
|
||||
MenuItem('add module', 'm', self.add_module),
|
||||
MenuItem('add io module', 'i', self.add_iomodule),
|
||||
MenuItem('purge empty prs', 'e', self.purge_prs),
|
||||
MenuItem('add recommended prs', '+', self.complete_prs),
|
||||
MenuItem('purge empty items', 'e', self.purge_prs),
|
||||
MenuItem('add configurable items', '+', self.complete_prs),
|
||||
MenuItem('cut module', KEY.CUT, parent.cut_module),
|
||||
]
|
||||
|
||||
@@ -333,7 +333,7 @@ class ModuleWidget(BaseWidget):
|
||||
|
||||
def height(self, to_focus=None):
|
||||
main = self.parent
|
||||
return super().height(to_focus) if main.detailed else 1
|
||||
return super().height() if main.detailed else 1
|
||||
|
||||
def check_data(self):
|
||||
"""check clsobj is valid and check all params and props"""
|
||||
@@ -367,7 +367,7 @@ class ModuleWidget(BaseWidget):
|
||||
self.update_widget_dict()
|
||||
|
||||
def purge_prs(self):
|
||||
self.widgets = [w for w in self.widgets if w.get_name() not in self.fixed_names and w.is_valid()]
|
||||
self.widgets = [w for w in self.widgets if w.get_name() in self.fixed_names or w.is_valid()]
|
||||
self.update_widget_dict()
|
||||
|
||||
def draw_summary_right(self, wr):
|
||||
@@ -512,13 +512,11 @@ class NodeWidget(BaseWidget):
|
||||
return True
|
||||
focus = self.focus + step
|
||||
|
||||
def height(self, to_focus=None):
|
||||
def focus_row(self, to_focus):
|
||||
main = self.parent
|
||||
if not main.detailed:
|
||||
return super().height(to_focus)
|
||||
if main.detailed:
|
||||
return super().focus_row(to_focus)
|
||||
height = 0
|
||||
if to_focus is None:
|
||||
to_focus = len(self.widgets)
|
||||
for nr, widget in enumerate(self.widgets[:to_focus]):
|
||||
name = widget.get_name()
|
||||
if name in self.summ_edit:
|
||||
@@ -942,7 +940,5 @@ class EditorMain(Main):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# os.environ['FRAPPY_CONFDIR'] = 'cfg:cfg/main:cfg/stick:cfg/addons'
|
||||
generalConfig.init()
|
||||
os.environ['FRAPPY_CONFDIR']
|
||||
EditorMain(sys.argv[1]).run()
|
||||
@@ -21,12 +21,14 @@
|
||||
|
||||
import re
|
||||
import frappy
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from ast import literal_eval
|
||||
from importlib import import_module
|
||||
from frappy.config import process_file, Node, fix_io_modules
|
||||
from frappy.core import Module
|
||||
from frappy.datatypes import DataType
|
||||
from frappy.lib.comparestring import compare
|
||||
from frappy.config import process_file, Node
|
||||
from frappy.core import Module, Parameter, Property
|
||||
from frappy.datatypes import DataType, EnumType
|
||||
|
||||
|
||||
HEADER = "# please edit this file with frappy edit"
|
||||
@@ -134,6 +136,8 @@ class Value:
|
||||
if value is None:
|
||||
raise ValueError(datatype)
|
||||
self.datatype = datatype
|
||||
if isinstance(datatype, EnumType):
|
||||
self.completion = NameCompletion([v.name for v in datatype._enum.members])
|
||||
self.error = error
|
||||
if callback:
|
||||
self.callback = callback
|
||||
@@ -477,6 +481,10 @@ def cfgdata_from_py(name, cfgpath, filecontent, logger):
|
||||
continue
|
||||
iomodcls = iomodcfg['cls']
|
||||
modcls = modcfg['cls']
|
||||
except Exception as e:
|
||||
logger.info('error %r when checking io for %r', e, modname)
|
||||
continue
|
||||
try:
|
||||
ioclass = f"{modcls}.ioClass"
|
||||
if ModuleClass.validate(iomodcls) != ModuleClass.validate(ioclass):
|
||||
continue
|
||||
@@ -484,16 +492,122 @@ def cfgdata_from_py(name, cfgpath, filecontent, logger):
|
||||
iomodcfg.pop('description', None)
|
||||
iodict[ioname] = iomodcfg
|
||||
except Exception as e:
|
||||
if ioclass:
|
||||
iomod, iocls = iomodcls.rsplit('.', 1)
|
||||
mod, cls = modcls.rsplit('.', 1)
|
||||
if mod == iomod:
|
||||
iomodcls = iocls
|
||||
errors[ioname] = f'{ioname}: missing ioClass={iomodcls} in source code of {modcls}'
|
||||
else:
|
||||
logger.info('error %r when checking io for %r', e, modname)
|
||||
iomod, iocls = iomodcls.rsplit('.', 1)
|
||||
mod, cls = modcls.rsplit('.', 1)
|
||||
if mod == iomod:
|
||||
iomodcls = iocls
|
||||
errors[ioname] = f'{ioname}: missing ioClass={iomodcls} in source code of {modcls}'
|
||||
|
||||
modules = {k: moddata_from_cfgfile(k, **v) for k, v in config.items()}
|
||||
for error in errors.values():
|
||||
logger.info(error)
|
||||
return nodedata_from_cfgfile(name, **nodecfg), modules
|
||||
|
||||
|
||||
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):
|
||||
checker = ClassChecker(cls)
|
||||
if checker.error or not checker.clsobj:
|
||||
return {}
|
||||
cls = checker.clsobj
|
||||
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
|
||||
|
||||
|
||||
def class_completion(value):
|
||||
"""analyze class path and return an array of suggestions for
|
||||
the last incomplete element"""
|
||||
checker = ClassChecker(value)
|
||||
if not checker.error:
|
||||
return checker.position, []
|
||||
if checker.root is None:
|
||||
sdict = {p: f'{p}.' for p in site.packages}
|
||||
else:
|
||||
sdict = {}
|
||||
if not checker.clsobj:
|
||||
file = checker.pyfile
|
||||
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 NameCompletion:
|
||||
def __init__(self, names):
|
||||
self.names = names
|
||||
self.nameset = set(names)
|
||||
|
||||
def __call__(self, value):
|
||||
if value in self.nameset:
|
||||
return len(value), []
|
||||
return 0, [value] + list(get_suggested(value, self.names))
|
||||
|
||||
|
||||
@@ -104,6 +104,17 @@ def clamp(*args):
|
||||
return sorted(args)[len(args) // 2]
|
||||
|
||||
|
||||
def mkthread(func, *args, **kwds):
|
||||
t = threading.Thread(
|
||||
name=f'{func.__module__}:{func.__name__}',
|
||||
target=func,
|
||||
args=args,
|
||||
kwargs=kwds)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
return t
|
||||
|
||||
|
||||
class Logger:
|
||||
def __init__(self):
|
||||
# self.parent = self
|
||||
@@ -150,7 +161,7 @@ class Widget:
|
||||
"""returns current row"""
|
||||
return 0
|
||||
|
||||
def height(self, to_focus=None):
|
||||
def height(self):
|
||||
"""returns current height"""
|
||||
return self.default_height
|
||||
|
||||
@@ -166,13 +177,16 @@ class HasWidgets:
|
||||
widgets = None # list of subwidgets
|
||||
|
||||
def current_row(self):
|
||||
return self.height(self.focus) + self.get_focus_widget().current_row()
|
||||
return self.focus_row(self.focus) + self.get_focus_widget().current_row()
|
||||
|
||||
def height(self, to_focus=None):
|
||||
if to_focus is None:
|
||||
to_focus = len(self.widgets)
|
||||
def focus_row(self, to_focus):
|
||||
"""return relative row position of focus widget"""
|
||||
return sum(w.height() for w in self.widgets[:to_focus])
|
||||
|
||||
def height(self):
|
||||
"""return the summed up height"""
|
||||
return self.focus_row(len(self.widgets))
|
||||
|
||||
def handle(self, main):
|
||||
try:
|
||||
while True:
|
||||
@@ -427,8 +441,8 @@ class TextEditCompl(TextEdit):
|
||||
if self.highlighted:
|
||||
return super().get_key(main)
|
||||
main.completion_widget = self
|
||||
# mkthread(self.get_selection_menu, main, self.value)
|
||||
self.get_selection_menu(main, self.value)
|
||||
mkthread(self.get_selection_menu, main, self.value)
|
||||
# self.get_selection_menu(main, self.value)
|
||||
key = super().get_key(main)
|
||||
self.menu = menu = main.popupmenu
|
||||
if not menu or self.highlighted or key != KEY.DOWN or self.pos < self.completion_pos:
|
||||
@@ -660,9 +674,6 @@ class PopUpMenu:
|
||||
pos = 0
|
||||
height = 0
|
||||
|
||||
def __repr__(self):
|
||||
return f'PopUp(pos={self.pos}/{len(self.selection)}, selected={self.selection[self.pos]!r})'
|
||||
|
||||
def advance(self, step):
|
||||
pos = self.pos + step
|
||||
if 0 <= pos < self.height:
|
||||
@@ -694,16 +705,16 @@ class CompletionMenu(PopUpMenu):
|
||||
return self.selection[self.pos]
|
||||
|
||||
def draw(self, wr):
|
||||
row = wr.nextrow
|
||||
row = wr.nextrow # the edit row
|
||||
wr.left = max(1, wr.left)
|
||||
col = wr.left - 1
|
||||
wr.nextrow -= self.pos - 1
|
||||
height = self.height + 1
|
||||
wr.nextrow -= self.pos
|
||||
if self.pos == 0:
|
||||
# rectangle is open on top
|
||||
wr.rectangle(row + 1, col, height - 1, self.width + 1, top=row + 2)
|
||||
wr.rectangle(row - 1, col, height, self.width + 1, top=row + 1)
|
||||
else:
|
||||
wr.rectangle(row - self.pos, col, height, self.width + 1)
|
||||
wr.rectangle(row - self.pos - 1, col, height, self.width + 1)
|
||||
for i, value in enumerate(self.selection):
|
||||
wr.startrow()
|
||||
if i == self.pos:
|
||||
@@ -924,7 +935,7 @@ class ContextMenu(PopUpMenu):
|
||||
|
||||
class BaseWriter:
|
||||
"""base for writer. does nothing else than keeping track of position"""
|
||||
highstyle = brightstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = None
|
||||
highstyle = brightstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = buttonstyle = None
|
||||
errorflag = '! '
|
||||
|
||||
def __init__(self):
|
||||
@@ -1110,7 +1121,7 @@ class Writer(BaseWriter):
|
||||
"""draw a vertical line
|
||||
|
||||
:param row, col: upper start point
|
||||
:param length: unclipped length
|
||||
:param length: length without clipping
|
||||
:param top, bottom, left, right: clipping
|
||||
:return: <start row or None>, <end row or None> (None is returned, when clipped on this corner)
|
||||
"""
|
||||
@@ -1134,7 +1145,7 @@ class Writer(BaseWriter):
|
||||
"""draw a horizontal line
|
||||
|
||||
:param row, col: left start point
|
||||
:param length: unclipped length
|
||||
:param length: length without clipping
|
||||
:param top, bottom, left, right: clipping
|
||||
:return: <start row or None>, <end row or None> (None is returned, when clipped on this corner)
|
||||
"""
|
||||
@@ -1153,7 +1164,6 @@ class Writer(BaseWriter):
|
||||
if length > 0:
|
||||
self.scr.hline(row, col, curses.ACS_HLINE, length, *attr)
|
||||
else:
|
||||
raise ValueError(length)
|
||||
return None, None
|
||||
return beg, end
|
||||
|
||||
@@ -1239,7 +1249,7 @@ class Main(HasWidgets):
|
||||
curses.noecho()
|
||||
curses.raw() # disable ctrl-C interrupt and ctrl-Q/S flow control
|
||||
curses.nonl() # accept ctrl-J
|
||||
self.scr.keypad(1)
|
||||
self.scr.keypad(True)
|
||||
KEY.init()
|
||||
self.context_menu = [
|
||||
MenuItem('help', KEY.HELP, self.handle_help, None),
|
||||
@@ -1255,7 +1265,7 @@ class Main(HasWidgets):
|
||||
raise
|
||||
finally:
|
||||
if self.scr:
|
||||
self.scr.keypad(0)
|
||||
self.scr.keypad(False)
|
||||
curses.echo()
|
||||
curses.nocbreak()
|
||||
curses.endwin()
|
||||
@@ -1346,7 +1356,7 @@ class Main(HasWidgets):
|
||||
botmargin = sum(v.height() for v in self.footers)
|
||||
|
||||
bot = wr.height - botmargin
|
||||
end_scroll = bot - topmargin - 1
|
||||
end_scroll = bot - topmargin - 1 - wr.height // 5
|
||||
|
||||
current_row = self.current_row()
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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
|
||||
Reference in New Issue
Block a user