frappy-edit: fix class completion
when the suggestion ends with a dot, all possibilities have to be on the popup menu Change-Id: Ic6f759d1e9d4028695d8949be5d4e3e81bbbe044
This commit is contained in:
@@ -32,6 +32,7 @@ from frappy.tools.terminalgui import Completion
|
|||||||
SKIP_PROPS = {'implementation', 'features',
|
SKIP_PROPS = {'implementation', 'features',
|
||||||
'interface_classes', 'slowinterval', 'omit_unchanged_within', 'original_id'}
|
'interface_classes', 'slowinterval', 'omit_unchanged_within', 'original_id'}
|
||||||
|
|
||||||
|
|
||||||
def recommended_prs(cls):
|
def recommended_prs(cls):
|
||||||
"""get properties and parameters which are useful in configuration
|
"""get properties and parameters which are useful in configuration
|
||||||
|
|
||||||
@@ -58,7 +59,6 @@ def recommended_prs(cls):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FrappyModule(str):
|
class FrappyModule(str):
|
||||||
"""checker for finding subclasses of Module defined in a python module"""
|
"""checker for finding subclasses of Module defined in a python module"""
|
||||||
def check(self, cls):
|
def check(self, cls):
|
||||||
@@ -98,43 +98,18 @@ def get_suggested(guess, allowed_keys):
|
|||||||
return result or 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):
|
def class_completion(value):
|
||||||
|
"""analyze class path and return an array of suggestions for
|
||||||
|
the last element not matching"""
|
||||||
checker = ClassChecker(value)
|
checker = ClassChecker(value)
|
||||||
if checker.position == len(value):
|
if not checker.error:
|
||||||
return checker.position, []
|
return checker.position, []
|
||||||
if checker.root is None:
|
if checker.root is None:
|
||||||
sdict = {p: f'{p}.' for p in site.packages}
|
sdict = {p: f'{p}.' for p in site.packages}
|
||||||
else:
|
else:
|
||||||
sdict = {}
|
sdict = {}
|
||||||
file = checker.pyfile
|
|
||||||
if not checker.clsobj:
|
if not checker.clsobj:
|
||||||
|
file = checker.pyfile
|
||||||
if file.name == '__init__.py':
|
if file.name == '__init__.py':
|
||||||
sdict = {p.stem: f'{p.stem}.'
|
sdict = {p.stem: f'{p.stem}.'
|
||||||
for p in sorted(file.parent.glob('*.py'))
|
for p in sorted(file.parent.glob('*.py'))
|
||||||
@@ -150,101 +125,6 @@ def class_completion(value):
|
|||||||
return checker.position, [checker.name] + selection
|
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):
|
class NameCompletion(Completion, DataType):
|
||||||
# TODO: make obsolete
|
# TODO: make obsolete
|
||||||
def __init__(self, callback, get_name_info):
|
def __init__(self, callback, get_name_info):
|
||||||
|
|||||||
@@ -231,31 +231,32 @@ def make_value(pname, cls, value):
|
|||||||
|
|
||||||
|
|
||||||
class ClassChecker:
|
class ClassChecker:
|
||||||
root = None
|
root = None # = clsobj if the imported object exists or modobj
|
||||||
modobj = None
|
modobj = None # the python module imported or None if no import succeeded
|
||||||
clsobj = None
|
clsobj = None # the object imported or None on failure
|
||||||
modname = None
|
modname = None # the name of the module imported
|
||||||
pyfile = None
|
pyfile = None # the python file of the module imported
|
||||||
error = None
|
error = None # None on success or a reason of failure
|
||||||
|
|
||||||
def __init__(self, clsstr):
|
def __init__(self, clsstr):
|
||||||
"""analyze 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('.')
|
clspath = clsstr.split('.')
|
||||||
for pathpos, name in enumerate(clspath):
|
for pathpos, name in enumerate(clspath):
|
||||||
base = '.'.join(clspath[:pathpos])
|
base = '.'.join(clspath[:pathpos])
|
||||||
if name:
|
if name:
|
||||||
if self.clsobj:
|
if self.clsobj:
|
||||||
error = self.cls(base, name)
|
error = self.try_cls(base, name)
|
||||||
elif name.isupper():
|
elif name.isupper():
|
||||||
error = self.cls(base, name)
|
error = self.try_cls(base, name)
|
||||||
if error and self.module(base, name) is None:
|
if error and self.try_module(base, name) is None:
|
||||||
error = None
|
error = None
|
||||||
else:
|
else:
|
||||||
error = self.module(base, name)
|
error = self.try_module(base, name)
|
||||||
if error and self.cls(base, name) is None:
|
if error and self.try_cls(base, name) is None:
|
||||||
error = None
|
error = None
|
||||||
else:
|
else:
|
||||||
error = 'empty element'
|
error = 'empty element'
|
||||||
@@ -269,7 +270,11 @@ class ClassChecker:
|
|||||||
self.error = None
|
self.error = None
|
||||||
self.position = len(clsstr)
|
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
|
modname = f'{base}.{name}' if base else name
|
||||||
try:
|
try:
|
||||||
self.modobj = self.root = import_module(modname)
|
self.modobj = self.root = import_module(modname)
|
||||||
@@ -281,7 +286,11 @@ class ClassChecker:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f'{modname}: {e!r}'
|
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:
|
try:
|
||||||
self.clsobj = getattr(self.root, name)
|
self.clsobj = getattr(self.root, name)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -697,13 +697,13 @@ class CompletionMenu(PopUpMenu):
|
|||||||
row = wr.nextrow
|
row = wr.nextrow
|
||||||
wr.left = max(1, wr.left)
|
wr.left = max(1, wr.left)
|
||||||
col = wr.left - 1
|
col = wr.left - 1
|
||||||
wr.nextrow -= self.pos
|
wr.nextrow -= self.pos - 1
|
||||||
height = self.height + 1
|
height = self.height + 1
|
||||||
if self.pos == 0:
|
if self.pos == 0:
|
||||||
# rectangle is open on top
|
# 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:
|
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):
|
for i, value in enumerate(self.selection):
|
||||||
wr.startrow()
|
wr.startrow()
|
||||||
if i == self.pos:
|
if i == self.pos:
|
||||||
|
|||||||
Reference in New Issue
Block a user