[FIX] télémétrie navigation : extraire les commandes des menus « choices »
« Code » (et Update, Git, Database…) apparaissait vide : l'extracteur AST ne comprenait que le motif « status == "N": self.methode() » (menu QEMU). Les menus bâtis par « choices = config_file.get_config(...) » + « choices.append( ...) » avec dispatch « str(len(choices)-N) » n'exposaient aucun enfant -> impossible d'ouvrir leurs commandes. Ajout de l'extraction de ce motif : - entrées de CONFIG lues depuis todo.json (rejouées via execute_from_configuration avec l'entrée en kwargs) ; - entrées APPENDÉES (dict littéral OU variable menu_entry=… suivie par n° de ligne) mappées à leur méthode via le dispatch « str(len(choices)-K) » ; - une entrée qui ouvre un sous-menu (ex. Update) est développée récursivement. Validé : Code -> 7 commandes (statut/remiser/formater/SHELL/màj module/débogage + sous-menu Update), méthodes correctes et existantes, TUI monte sans erreur. Les dispatches complexes (ex. « Migration BD » via TodoUpgrade) restent visibles mais non lançables (method None). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
839ef08ea4
commit
db1e9fa24f
1 changed files with 199 additions and 0 deletions
|
|
@ -210,12 +210,192 @@ def _menu_labels(cls) -> dict:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _config_list(config_key, todo_dir):
|
||||||
|
"""Entrées d'une liste de config de todo.json (ex. « code_from_makefile »),
|
||||||
|
cherchée à n'importe quel niveau. [] si absente."""
|
||||||
|
try:
|
||||||
|
data = json.loads(
|
||||||
|
(Path(todo_dir) / "todo.json").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return []
|
||||||
|
found = []
|
||||||
|
|
||||||
|
def walk(o):
|
||||||
|
if isinstance(o, dict):
|
||||||
|
for k, v in o.items():
|
||||||
|
if k == config_key and isinstance(v, list):
|
||||||
|
found.extend(v)
|
||||||
|
walk(v)
|
||||||
|
elif isinstance(o, list):
|
||||||
|
for x in o:
|
||||||
|
walk(x)
|
||||||
|
|
||||||
|
walk(data)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def _len_choices_offset(comp):
|
||||||
|
"""K pour « str(len(choices)) » -> 0 et « str(len(choices) - N) » -> N ;
|
||||||
|
None si le nœud n'est pas de cette forme."""
|
||||||
|
if not (
|
||||||
|
isinstance(comp, ast.Call)
|
||||||
|
and isinstance(comp.func, ast.Name)
|
||||||
|
and comp.func.id == "str"
|
||||||
|
and comp.args
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
arg = comp.args[0]
|
||||||
|
|
||||||
|
def is_len_choices(x):
|
||||||
|
return (
|
||||||
|
isinstance(x, ast.Call)
|
||||||
|
and isinstance(x.func, ast.Name)
|
||||||
|
and x.func.id == "len"
|
||||||
|
and x.args
|
||||||
|
and isinstance(x.args[0], ast.Name)
|
||||||
|
and x.args[0].id == "choices"
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_len_choices(arg):
|
||||||
|
return 0
|
||||||
|
if (
|
||||||
|
isinstance(arg, ast.BinOp)
|
||||||
|
and isinstance(arg.op, ast.Sub)
|
||||||
|
and is_len_choices(arg.left)
|
||||||
|
and isinstance(arg.right, ast.Constant)
|
||||||
|
):
|
||||||
|
return int(arg.right.value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _dispatch_len(func) -> dict:
|
||||||
|
"""{K: méthode} pour « status == str(len(choices) - K): self.M() »."""
|
||||||
|
out = {}
|
||||||
|
for node in ast.walk(func):
|
||||||
|
if not isinstance(node, ast.If):
|
||||||
|
continue
|
||||||
|
test = node.test
|
||||||
|
if not (
|
||||||
|
isinstance(test, ast.Compare)
|
||||||
|
and isinstance(test.left, ast.Name)
|
||||||
|
and test.left.id == "status"
|
||||||
|
and len(test.ops) == 1
|
||||||
|
and isinstance(test.ops[0], ast.Eq)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
k = _len_choices_offset(test.comparators[0])
|
||||||
|
if k is None:
|
||||||
|
continue
|
||||||
|
method = None
|
||||||
|
for stmt in node.body:
|
||||||
|
for n in ast.walk(stmt):
|
||||||
|
if (
|
||||||
|
isinstance(n, ast.Call)
|
||||||
|
and isinstance(n.func, ast.Attribute)
|
||||||
|
and isinstance(n.func.value, ast.Name)
|
||||||
|
and n.func.value.id == "self"
|
||||||
|
):
|
||||||
|
method = n.func.attr
|
||||||
|
break
|
||||||
|
if method:
|
||||||
|
break
|
||||||
|
if method:
|
||||||
|
out[k] = method
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _choices_children(func, todo_dir):
|
||||||
|
"""Commandes d'un menu bâti par « choices » (config_file.get_config +
|
||||||
|
choices.append/extend) avec dispatch « str(len(choices)-N) ». Renvoie une
|
||||||
|
liste de (label, méthode, kwargs) dans l'ordre affiché, ou None si le motif
|
||||||
|
ne s'applique pas. Les entrées de CONFIG rejouent via
|
||||||
|
execute_from_configuration ; les entrées APPENDÉES via leur méthode."""
|
||||||
|
def _dict_label(dnode):
|
||||||
|
d = {}
|
||||||
|
for k, v in zip(dnode.keys, dnode.values):
|
||||||
|
if isinstance(k, ast.Constant):
|
||||||
|
d[k.value] = _str_of(v)
|
||||||
|
return d.get("prompt_description") or d.get("prompt_description_key")
|
||||||
|
|
||||||
|
# On collecte affectations et append AVEC leur n° de ligne pour rejouer
|
||||||
|
# dans l'ORDRE (les entrées « menu_entry = {…}; choices.append(menu_entry) »
|
||||||
|
# réutilisent la même variable -> il faut suivre la dernière valeur).
|
||||||
|
config_key = None
|
||||||
|
events = [] # (lineno, kind, payload)
|
||||||
|
for node in ast.walk(func):
|
||||||
|
if (
|
||||||
|
isinstance(node, ast.Assign)
|
||||||
|
and len(node.targets) == 1
|
||||||
|
and isinstance(node.targets[0], ast.Name)
|
||||||
|
):
|
||||||
|
var = node.targets[0].id
|
||||||
|
val = node.value
|
||||||
|
if isinstance(val, ast.Dict):
|
||||||
|
events.append((node.lineno, "assign", (var, val)))
|
||||||
|
elif (
|
||||||
|
var == "choices"
|
||||||
|
and isinstance(val, ast.Call)
|
||||||
|
and isinstance(val.func, ast.Attribute)
|
||||||
|
and val.func.attr == "get_config"
|
||||||
|
and val.args
|
||||||
|
and isinstance(val.args[0], ast.Constant)
|
||||||
|
):
|
||||||
|
config_key = val.args[0].value
|
||||||
|
elif (
|
||||||
|
isinstance(node, ast.Call)
|
||||||
|
and isinstance(node.func, ast.Attribute)
|
||||||
|
and node.func.attr == "append"
|
||||||
|
and isinstance(node.func.value, ast.Name)
|
||||||
|
and node.func.value.id == "choices"
|
||||||
|
and node.args
|
||||||
|
):
|
||||||
|
events.append((node.lineno, "append", node.args[0]))
|
||||||
|
events.sort(key=lambda e: e[0])
|
||||||
|
var_dict, appended = {}, []
|
||||||
|
for _ln, kind, payload in events:
|
||||||
|
if kind == "assign":
|
||||||
|
var_dict[payload[0]] = payload[1]
|
||||||
|
else: # append : arg = Dict littéral ou Name (variable)
|
||||||
|
arg = payload
|
||||||
|
dnode = (
|
||||||
|
arg
|
||||||
|
if isinstance(arg, ast.Dict)
|
||||||
|
else var_dict.get(arg.id)
|
||||||
|
if isinstance(arg, ast.Name)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if dnode is not None:
|
||||||
|
lab = _dict_label(dnode)
|
||||||
|
if lab:
|
||||||
|
appended.append(lab)
|
||||||
|
if config_key is None and not appended:
|
||||||
|
return None
|
||||||
|
children = []
|
||||||
|
for entry in _config_list(config_key, todo_dir) if config_key else []:
|
||||||
|
lab = (
|
||||||
|
entry.get("prompt_description_key")
|
||||||
|
or entry.get("prompt_description")
|
||||||
|
or "?"
|
||||||
|
)
|
||||||
|
children.append((lab, "execute_from_configuration", {"instance": entry}))
|
||||||
|
len_disp = _dispatch_len(func)
|
||||||
|
n_config = len(children) # figé : `children` grossit dans la boucle
|
||||||
|
n_total = n_config + len(appended)
|
||||||
|
for j, lab in enumerate(appended):
|
||||||
|
pos = n_config + j # 0-based dans la liste globale (stable)
|
||||||
|
method = len_disp.get(n_total - 1 - pos) # None si non mappé
|
||||||
|
children.append((lab, method, {}))
|
||||||
|
return children
|
||||||
|
|
||||||
|
|
||||||
def build_code_tree(todo_path=None) -> dict | None:
|
def build_code_tree(todo_path=None) -> dict | None:
|
||||||
"""Construit l'arbre des menus/commandes EN LISANT le code de todo.py
|
"""Construit l'arbre des menus/commandes EN LISANT le code de todo.py
|
||||||
(AST). Chaque menu (méthode de _MENU_LABELS) devient un nœud ; ses branches
|
(AST). Chaque menu (méthode de _MENU_LABELS) devient un nœud ; ses branches
|
||||||
« status == N » deviennent des sous-menus (si la cible est un menu) ou des
|
« status == N » deviennent des sous-menus (si la cible est un menu) ou des
|
||||||
commandes (feuilles). None si l'analyse échoue."""
|
commandes (feuilles). None si l'analyse échoue."""
|
||||||
p = Path(todo_path) if todo_path else (Path(__file__).parent / "todo.py")
|
p = Path(todo_path) if todo_path else (Path(__file__).parent / "todo.py")
|
||||||
|
todo_dir = p.parent # todo.json est à côté de todo.py
|
||||||
try:
|
try:
|
||||||
mod = ast.parse(p.read_text(encoding="utf-8"))
|
mod = ast.parse(p.read_text(encoding="utf-8"))
|
||||||
except (OSError, SyntaxError):
|
except (OSError, SyntaxError):
|
||||||
|
|
@ -270,6 +450,25 @@ def build_code_tree(todo_path=None) -> dict | None:
|
||||||
"section": entry["section"] if entry else None,
|
"section": entry["section"] if entry else None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# Menus bâtis par « choices » (get_config + append) sans dispatch
|
||||||
|
# littéral « status == "N" » : Code, Update, Git, Database…
|
||||||
|
if not disp:
|
||||||
|
for lab, tmethod, tkwargs in (
|
||||||
|
_choices_children(func, todo_dir) or []
|
||||||
|
):
|
||||||
|
if tmethod in labels: # une entrée qui ouvre un sous-menu
|
||||||
|
node["children"].append(build(tmethod))
|
||||||
|
else:
|
||||||
|
node["children"].append(
|
||||||
|
{
|
||||||
|
"label": lab,
|
||||||
|
"is_menu": False,
|
||||||
|
"children": [],
|
||||||
|
"method": tmethod,
|
||||||
|
"kwargs": tkwargs,
|
||||||
|
"section": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
seen.discard(method)
|
seen.discard(method)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue