erplibre/script/todo/textual_setup.py

99 lines
3.7 KiB
Python
Raw Normal View History

[IMP] todo: offer to install textual instead of just naming it « Installez textual pour le TUI (pip) » left the user to work out which pip, which interpreter, which package — and it was printed from eight different places. Every TUI screen now asks: ⚠ Textual est nécessaire pour cet écran. L'installer maintenant ? (O/n, défaut : oui) The command targets sys.executable, the interpreter that will have to import it — installing a distribution package would land somewhere the venv never looks. Outside a venv it adds --user, which is also what gets past the refusal of distributions whose environment is externally managed (PEP 668). Two details that decide whether this works at all: · importlib.invalidate_caches() after installing. A failed import is remembered, so without it textual stays « missing » for the rest of the session despite having just been installed. · a pip that exits non-zero never reports success. The check is « is it importable NOW », not « did pip return 0 », and the failure suggests the distribution package by name. It lives in its own module rather than as a TODO method: todo_upgrade needs it too and is imported BY todo, so putting it there would close a cycle. One call site is deliberately NOT converted. The statistics screen only reads files; it never touches Textual, and its old message claimed otherwise. An import failure there is a real module problem and now says so. Verified: already-present asks nothing and runs nothing; refusal installs nothing; pip failing returns False and points at python3-textual; pip succeeding returns True; prompt=False reports without asking. Then each of the four TUI entries — telemetry, deploy form, deploy progress, migration resume — offers and falls back cleanly on refusal, while the statistics screen stays silent. Also caught by those tests: « import importlib » alone does not expose importlib.util, so availability could not be checked at all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 02:42:52 -04:00
#!/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 , 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 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
[FIX] requirement: borner Textual, et déclarer lxml plutôt que l'hériter Textual était déclaré sans version. Les quatre écrans TUI du dépôt sont écrits pour la 8, et la bibliothèque casse son API entre majeures : un « pip install -U » les casse tous sans prévenir. Borner le fichier de requirements ne suffisait pas. install_command() faisait « pip install textual », sans borne : « make install » aurait pris la 8, et l'installation proposée à l'écran la majeure suivante. Deux chemins pour la même dépendance, qui ne disent pas la même chose. La borne vit donc dans une seule constante, TEXTUAL_SPEC, que install_command() utilise, et que le fichier de requirements recopie avec un commentaire qui pointe dessus. La borne ne s'applique qu'à l'installation : ensure() vérifie « est-ce importable », pas « à quelle version ». Un Textual 9 déjà présent passe, et c'est volontaire — refuser de démarrer sur une version qui marche peut-être serait pire que le problème. lxml devient une dépendance déclarée. Il n'arrivait que par pykeepass, openupgradelib et odoo-module-migrator ; le jour où l'un d'eux s'en passe, il disparaît d'un venv sans que rien ne le réclame. Sans borne : cyclonedx-python-lib demande déjà « lxml >=4,<7 », en ajouter une seconde n'apporterait qu'un conflit possible. Vérifié : install_command() porte la borne, l'insertion de « --user » hors venv reste au bon rang, et le Textual installé (8.2.8) la satisfait. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 04:03:58 -04:00
# 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"
[IMP] todo: offer to install textual instead of just naming it « Installez textual pour le TUI (pip) » left the user to work out which pip, which interpreter, which package — and it was printed from eight different places. Every TUI screen now asks: ⚠ Textual est nécessaire pour cet écran. L'installer maintenant ? (O/n, défaut : oui) The command targets sys.executable, the interpreter that will have to import it — installing a distribution package would land somewhere the venv never looks. Outside a venv it adds --user, which is also what gets past the refusal of distributions whose environment is externally managed (PEP 668). Two details that decide whether this works at all: · importlib.invalidate_caches() after installing. A failed import is remembered, so without it textual stays « missing » for the rest of the session despite having just been installed. · a pip that exits non-zero never reports success. The check is « is it importable NOW », not « did pip return 0 », and the failure suggests the distribution package by name. It lives in its own module rather than as a TODO method: todo_upgrade needs it too and is imported BY todo, so putting it there would close a cycle. One call site is deliberately NOT converted. The statistics screen only reads files; it never touches Textual, and its old message claimed otherwise. An import failure there is a real module problem and now says so. Verified: already-present asks nothing and runs nothing; refusal installs nothing; pip failing returns False and points at python3-textual; pip succeeding returns True; prompt=False reports without asking. Then each of the four TUI entries — telemetry, deploy form, deploy progress, migration resume — offers and falls back cleanly on refusal, while the statistics screen stays silent. Also caught by those tests: « import importlib » alone does not expose importlib.util, so availability could not be checked at all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 02:42:52 -04:00
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).
"""
[FIX] requirement: borner Textual, et déclarer lxml plutôt que l'hériter Textual était déclaré sans version. Les quatre écrans TUI du dépôt sont écrits pour la 8, et la bibliothèque casse son API entre majeures : un « pip install -U » les casse tous sans prévenir. Borner le fichier de requirements ne suffisait pas. install_command() faisait « pip install textual », sans borne : « make install » aurait pris la 8, et l'installation proposée à l'écran la majeure suivante. Deux chemins pour la même dépendance, qui ne disent pas la même chose. La borne vit donc dans une seule constante, TEXTUAL_SPEC, que install_command() utilise, et que le fichier de requirements recopie avec un commentaire qui pointe dessus. La borne ne s'applique qu'à l'installation : ensure() vérifie « est-ce importable », pas « à quelle version ». Un Textual 9 déjà présent passe, et c'est volontaire — refuser de démarrer sur une version qui marche peut-être serait pire que le problème. lxml devient une dépendance déclarée. Il n'arrivait que par pykeepass, openupgradelib et odoo-module-migrator ; le jour où l'un d'eux s'en passe, il disparaît d'un venv sans que rien ne le réclame. Sans borne : cyclonedx-python-lib demande déjà « lxml >=4,<7 », en ajouter une seconde n'apporterait qu'un conflit possible. Vérifié : install_command() porte la borne, l'insertion de « --user » hors venv reste au bon rang, et le Textual installé (8.2.8) la satisfait. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 04:03:58 -04:00
cmd = [sys.executable, "-m", "pip", "install", TEXTUAL_SPEC]
[IMP] todo: offer to install textual instead of just naming it « Installez textual pour le TUI (pip) » left the user to work out which pip, which interpreter, which package — and it was printed from eight different places. Every TUI screen now asks: ⚠ Textual est nécessaire pour cet écran. L'installer maintenant ? (O/n, défaut : oui) The command targets sys.executable, the interpreter that will have to import it — installing a distribution package would land somewhere the venv never looks. Outside a venv it adds --user, which is also what gets past the refusal of distributions whose environment is externally managed (PEP 668). Two details that decide whether this works at all: · importlib.invalidate_caches() after installing. A failed import is remembered, so without it textual stays « missing » for the rest of the session despite having just been installed. · a pip that exits non-zero never reports success. The check is « is it importable NOW », not « did pip return 0 », and the failure suggests the distribution package by name. It lives in its own module rather than as a TODO method: todo_upgrade needs it too and is imported BY todo, so putting it there would close a cycle. One call site is deliberately NOT converted. The statistics screen only reads files; it never touches Textual, and its old message claimed otherwise. An import failure there is a real module problem and now says so. Verified: already-present asks nothing and runs nothing; refusal installs nothing; pip failing returns False and points at python3-textual; pip succeeding returns True; prompt=False reports without asking. Then each of the four TUI entries — telemetry, deploy form, deploy progress, migration resume — offers and falls back cleanly on refusal, while the statistics screen stays silent. Also caught by those tests: « import importlib » alone does not expose importlib.util, so availability could not be checked at all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 02:42:52 -04:00
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