[ADD] todo: install Textual on demand
The TUI screens need Textual. Rather than naming it, the CLI offers to install it for the running interpreter, bounded to the major the screens are written for. --- FR --- Les écrans TUI ont besoin de Textual. Plutôt que de le nommer, le CLI propose de l'installer pour l'interpréteur courant, borné à la majeure pour laquelle les écrans sont écrits. Assisted-by: Claude Opus 4.8 Assisted-by: Claude Opus 5
This commit is contained in:
parent
dd98955192
commit
5e7391252b
2 changed files with 105 additions and 0 deletions
|
|
@ -24,6 +24,13 @@ openai
|
||||||
humanize
|
humanize
|
||||||
requests
|
requests
|
||||||
urwid
|
urwid
|
||||||
|
# Borne recopiée de script/todo/textual_setup.py (TEXTUAL_SPEC) : les écrans TUI
|
||||||
|
# sont écrits pour Textual 8, qui casse son API entre majeures. Modifier les deux.
|
||||||
|
textual>=8,<9
|
||||||
|
# Dépendance directe : les outils d'analyse comparent des arbres XML avec lxml.
|
||||||
|
# Il n'arrivait que par pykeepass / openupgradelib / odoo-module-migrator, donc
|
||||||
|
# un jour où l'un d'eux s'en passe, il disparaît sans que rien ne le réclame.
|
||||||
|
lxml
|
||||||
python-dotenv
|
python-dotenv
|
||||||
python-dateutil
|
python-dateutil
|
||||||
unidecode
|
unidecode
|
||||||
|
|
|
||||||
98
script/todo/textual_setup.py
Normal file
98
script/todo/textual_setup.py
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
"""Disponibilité de Textual, et installation à la demande.
|
||||||
|
|
||||||
|
Les écrans TUI du CLI TODO (télémétrie, dashboard d'installation, formulaire
|
||||||
|
de déploiement, reprise de migration) dépendent tous de Textual. Sans lui, ils
|
||||||
|
se contentaient d'un « Installez textual (pip) » qui laissait l'utilisateur
|
||||||
|
chercher la bonne commande pour son système.
|
||||||
|
|
||||||
|
`ensure(...)` répond à la question à sa place : Textual est-il là, et sinon
|
||||||
|
veut-on l'installer maintenant ?
|
||||||
|
|
||||||
|
Module à part, et non une méthode de `TODO` : `todo_upgrade` en a besoin
|
||||||
|
aussi, et il est importé PAR `todo` — le mettre là créerait un cycle.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import importlib.util # « import importlib » seul n'expose PAS .util
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from script.todo.todo_i18n import t
|
||||||
|
except Exception: # pragma: no cover - repli si i18n indisponible
|
||||||
|
|
||||||
|
def t(key: str) -> str:
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
# Borne de version, à UN seul endroit. Les écrans TUI du dépôt sont écrits pour
|
||||||
|
# Textual 8 ; la bibliothèque casse son API entre majeures. « pip install
|
||||||
|
# textual » sans borne installerait la majeure suivante et casserait les écrans
|
||||||
|
# sans prévenir, alors même que requirement/erplibre_require-ments.txt la borne.
|
||||||
|
# Les deux chemins d'installation doivent dire la même chose : ce littéral est
|
||||||
|
# recopié dans le fichier de requirements, avec un commentaire qui pointe ici.
|
||||||
|
TEXTUAL_SPEC = "textual>=8,<9"
|
||||||
|
|
||||||
|
|
||||||
|
def available() -> bool:
|
||||||
|
"""Textual est-il importable maintenant ?"""
|
||||||
|
return importlib.util.find_spec("textual") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def in_venv() -> bool:
|
||||||
|
"""Vrai si l'interpréteur courant est dans un environnement virtuel."""
|
||||||
|
return sys.prefix != getattr(sys, "base_prefix", sys.prefix)
|
||||||
|
|
||||||
|
|
||||||
|
def install_command():
|
||||||
|
"""Commande d'installation adaptée à l'interpréteur QUI TOURNE.
|
||||||
|
|
||||||
|
C'est lui qui devra importer Textual, pas le python du système : viser
|
||||||
|
`sys.executable` évite d'installer un paquet distribution que le venv ne
|
||||||
|
verrait jamais. Hors venv, « --user » contourne le refus des
|
||||||
|
distributions dont l'environnement est « externally managed » (PEP 668).
|
||||||
|
"""
|
||||||
|
cmd = [sys.executable, "-m", "pip", "install", TEXTUAL_SPEC]
|
||||||
|
if not in_venv():
|
||||||
|
cmd.insert(4, "--user")
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def ensure(prompt=True, ask=input):
|
||||||
|
"""Textual disponible ? Sinon proposer de l'installer. Renvoie un booléen.
|
||||||
|
|
||||||
|
`prompt=False` se contente de constater — pour les appels qui ne peuvent
|
||||||
|
pas poser de question. `ask` est injectable pour les tests.
|
||||||
|
"""
|
||||||
|
if available():
|
||||||
|
return True
|
||||||
|
print(f"\n⚠ {t('Textual is required for this screen.')}")
|
||||||
|
if not prompt:
|
||||||
|
return False
|
||||||
|
answer = ask(t("Install it now? (Y/n): ")).strip().lower()
|
||||||
|
if answer and answer not in ("y", "yes", "o", "oui"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
cmd = install_command()
|
||||||
|
print(f" {t('Will execute:')} {' '.join(cmd)}")
|
||||||
|
try:
|
||||||
|
status = subprocess.run(cmd).returncode
|
||||||
|
except (OSError, subprocess.SubprocessError) as exc:
|
||||||
|
print(f" ⚠ {exc}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Un import négatif est mémorisé : sans purge du cache, Textual resterait
|
||||||
|
# « absent » pour ce processus alors qu'il vient d'être installé.
|
||||||
|
importlib.invalidate_caches()
|
||||||
|
if available():
|
||||||
|
print(f"✅ {t('Textual is installed.')}")
|
||||||
|
return True
|
||||||
|
print(f" ⚠ {t('Installation finished but textual is still missing.')}")
|
||||||
|
if status:
|
||||||
|
print(f" {t('pip exited with')} {status}")
|
||||||
|
print(f" {t('Your distribution may package it as python3-textual.')}")
|
||||||
|
return False
|
||||||
Loading…
Reference in a new issue