From 18f6fa239ba57dd2c17521633e24c5f23e777dd6 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Thu, 12 Feb 2026 09:00:22 +0100 Subject: [PATCH] frappy-edit: fix class completion when the suggestion ends with a dot, all possibilities have to be on the popup menu Change-Id: Ic6f759d1e9d4028695d8949be5d4e3e81bbbe044 --- frappy/tools/completion.py | 130 ++---------------------------------- frappy/tools/configdata.py | 37 ++++++---- frappy/tools/terminalgui.py | 6 +- 3 files changed, 31 insertions(+), 142 deletions(-) diff --git a/frappy/tools/completion.py b/frappy/tools/completion.py index 60959d92..8503cd3c 100644 --- a/frappy/tools/completion.py +++ b/frappy/tools/completion.py @@ -32,6 +32,7 @@ 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 @@ -58,7 +59,6 @@ def recommended_prs(cls): return result - class FrappyModule(str): """checker for finding subclasses of Module defined in a python module""" def check(self, cls): @@ -98,43 +98,18 @@ def get_suggested(guess, allowed_keys): 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): + """analyze class path and return an array of suggestions for + the last element not matching""" checker = ClassChecker(value) - if checker.position == len(value): + if not checker.error: 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: + file = checker.pyfile if file.name == '__init__.py': sdict = {p.stem: f'{p.stem}.' for p in sorted(file.parent.glob('*.py')) @@ -150,101 +125,6 @@ def class_completion(value): 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 '' of '' or '.' - # 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): diff --git a/frappy/tools/configdata.py b/frappy/tools/configdata.py index a11d3c0f..5604b330 100644 --- a/frappy/tools/configdata.py +++ b/frappy/tools/configdata.py @@ -231,31 +231,32 @@ def make_value(pname, cls, value): class ClassChecker: - root = None - modobj = None - clsobj = None - modname = None - pyfile = None - error = None + root = None # = clsobj if the imported object exists or modobj + modobj = None # the python module imported or None if no import succeeded + clsobj = None # the object imported or None on failure + modname = None # the name of the module imported + pyfile = None # the python file of the module imported + error = None # None on success or a reason of failure def __init__(self, clsstr): """analyze clsstr - returns the length of the valid part and a list of guesses + try to resolve clsstr from left to right, quit on error + overwrite the class attributes above """ clspath = clsstr.split('.') for pathpos, name in enumerate(clspath): base = '.'.join(clspath[:pathpos]) if name: if self.clsobj: - error = self.cls(base, name) + error = self.try_cls(base, name) elif name.isupper(): - error = self.cls(base, name) - if error and self.module(base, name) is None: + error = self.try_cls(base, name) + if error and self.try_module(base, name) is None: error = None else: - error = self.module(base, name) - if error and self.cls(base, name) is None: + error = self.try_module(base, name) + if error and self.try_cls(base, name) is None: error = None else: error = 'empty element' @@ -269,7 +270,11 @@ class ClassChecker: self.error = None self.position = len(clsstr) - def module(self, base, name): + def try_module(self, base, name): + """try if base + name is a python module + + return None on success or an error message otherwise + """ modname = f'{base}.{name}' if base else name try: self.modobj = self.root = import_module(modname) @@ -281,7 +286,11 @@ class ClassChecker: except Exception as e: return f'{modname}: {e!r}' - def cls(self, base, name): + def try_cls(self, base, name): + """try if base + name is a python object (typically a class) + + return None on success or an error message otherwise + """ try: self.clsobj = getattr(self.root, name) return None diff --git a/frappy/tools/terminalgui.py b/frappy/tools/terminalgui.py index fc2653d0..06e3bcc1 100644 --- a/frappy/tools/terminalgui.py +++ b/frappy/tools/terminalgui.py @@ -697,13 +697,13 @@ class CompletionMenu(PopUpMenu): row = wr.nextrow wr.left = max(1, wr.left) col = wr.left - 1 - wr.nextrow -= self.pos + wr.nextrow -= self.pos - 1 height = self.height + 1 if self.pos == 0: # rectangle is open on top - wr.rectangle(row - self.pos - 1, col, height, self.width + 1, top=row + 1) + wr.rectangle(row + 1, col, height - 1, self.width + 1, top=row + 2) else: - wr.rectangle(row - self.pos - 1, col, height, self.width + 1) + wr.rectangle(row - self.pos, col, height, self.width + 1) for i, value in enumerate(self.selection): wr.startrow() if i == self.pos: