[ADD] migration: an auto-run that takes the default after five seconds
A migration asks dozens of questions whose answer is almost always the
one offered, and someone has to stand there pressing Enter for hours.
Asked before the version choice — so it covers that choice too, now
that it has a default — and off by default: it decides in your place.
Every prompt of the migration goes through one reader, seventeen of
them. A prompt left as a bare input() would know nothing of auto mode
and would block the run with nothing to say why; a test reads the
function and rejects any that appears.
click.prompt had to go: it cannot hand back control after a delay, so
auto would have stopped at the first question. select() rather than a
thread or an alarm — a thread would leave a blocked input() behind it,
stealing the next keystroke.
--- FR ---
[ADD] migration : une auto-exécution qui prend le défaut après cinq secondes
Une migration pose des dizaines de questions dont la réponse est presque
toujours celle proposée, et il faut rester devant à taper Entrée pendant
des heures. Posée avant le choix de version — donc elle vaut aussi pour
lui, maintenant qu'il a un défaut — et éteinte par défaut : elle décide
à votre place.
Toutes les invites de la migration passent par une seule lecture,
dix-sept d'un coup. Une invite laissée en input() nu ne saurait rien du
mode auto et bloquerait la migration sans rien dire ; un test lit la
fonction et refuse toute apparition.
click.prompt devait partir : il ne sait pas rendre la main après un
délai, l'auto se serait arrêté à la première question. select() plutôt
qu'un fil ou une alarme — un fil laisserait un input() bloqué derrière
lui, qui volerait la frappe suivante.
Assisted-by: Claude Opus 5
2026-08-17 17:35:31 -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)
|
|
|
|
|
|
|
|
|
|
"""Enchaîner la migration sans rester devant à taper Entrée.
|
|
|
|
|
|
|
|
|
|
Une migration pose des dizaines de questions dont la réponse est presque
|
|
|
|
|
toujours celle proposée. Le mode auto attend cinq secondes puis prend le
|
|
|
|
|
défaut — assez pour reprendre la main, assez peu pour ne pas immobiliser
|
|
|
|
|
quelqu'un des heures.
|
|
|
|
|
|
|
|
|
|
La question est posée AVANT le choix de version, donc avant la première
|
|
|
|
|
décision : activée, elle vaut pour toutes les suivantes, celle-là comprise.
|
|
|
|
|
|
|
|
|
|
Le test qui compte le plus est le dernier : une invite écrite plus tard en
|
|
|
|
|
`input()` nu ne saurait rien du mode auto, et bloquerait la migration sans
|
|
|
|
|
que rien ne le signale.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import ast
|
|
|
|
|
import inspect
|
|
|
|
|
import io
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
|
|
|
|
from script.todo import todo_i18n # noqa: E402
|
|
|
|
|
from script.todo.todo_upgrade import TodoUpgrade # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def upgrade(auto, delay=0.2):
|
|
|
|
|
obj = TodoUpgrade.__new__(TodoUpgrade)
|
|
|
|
|
obj.auto_execute = auto
|
|
|
|
|
obj.AUTO_DELAY = delay
|
|
|
|
|
return obj
|
|
|
|
|
|
|
|
|
|
|
[ADD] migration: make Enter mean the answer you always give
Auto-run promised to "take the default after five seconds", but every
default was EMPTY: it took nothing. Worse, half the prompts of a
migration are asked by separate processes — the theme uninstaller, the
stale-SCSS detector, the smoke test. They knew nothing of auto-run and
waited forever for a keystroke that never came.
The countdown now lives in one file and travels through the ENVIRONMENT,
the only channel a fork shares. Enter takes the default everywhere, not
only under auto-run: a prompt that prints (Y/n) and does otherwise is
worse than no prompt. Every text was rewritten to say what Enter does,
and every default kept an explicit way out.
Two defaults now write. Both are tenable only because the backup runs
first, and a test locks that ORDER rather than trusting a promise.
--- FR ---
[ADD] migration : faire d'Entrée la réponse qu'on donne toujours
L'auto-exécution promettait « le défaut après cinq secondes », mais tous
les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des
invites d'une migration sont posées par d'autres processus — le
désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils
ignoraient l'auto-exécution et attendaient sans fin une frappe.
Le compte à rebours tient désormais dans un seul fichier et voyage par
l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut
partout, pas seulement en auto : une invite qui affiche (Y/n) et fait
l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée,
et chaque défaut garde une issue explicite.
Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe
d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse.
Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
|
|
|
class EnvCase(unittest.TestCase):
|
|
|
|
|
"""Rendre l'environnement comme on l'a trouvé.
|
|
|
|
|
|
|
|
|
|
`ask` pose le mode auto dans l'environnement — c'est ainsi qu'il atteint
|
|
|
|
|
les outils lancés à part. Le laisser posé le ferait fuir dans TOUTE la
|
|
|
|
|
suite : une invite sans rapport prendrait son défaut au lieu d'attendre,
|
|
|
|
|
et le test qui échouerait ne serait pas celui qui a fauté.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
from script.todo import auto_ask
|
|
|
|
|
|
|
|
|
|
avant = {
|
|
|
|
|
key: os.environ.get(key)
|
|
|
|
|
for key in (auto_ask.ENV_ENABLED, auto_ask.ENV_DELAY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def remettre():
|
|
|
|
|
for key, value in avant.items():
|
|
|
|
|
if value is None:
|
|
|
|
|
os.environ.pop(key, None)
|
|
|
|
|
else:
|
|
|
|
|
os.environ[key] = value
|
|
|
|
|
|
|
|
|
|
self.addCleanup(remettre)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTheTimedRead(EnvCase):
|
[ADD] migration: an auto-run that takes the default after five seconds
A migration asks dozens of questions whose answer is almost always the
one offered, and someone has to stand there pressing Enter for hours.
Asked before the version choice — so it covers that choice too, now
that it has a default — and off by default: it decides in your place.
Every prompt of the migration goes through one reader, seventeen of
them. A prompt left as a bare input() would know nothing of auto mode
and would block the run with nothing to say why; a test reads the
function and rejects any that appears.
click.prompt had to go: it cannot hand back control after a delay, so
auto would have stopped at the first question. select() rather than a
thread or an alarm — a thread would leave a blocked input() behind it,
stealing the next keystroke.
--- FR ---
[ADD] migration : une auto-exécution qui prend le défaut après cinq secondes
Une migration pose des dizaines de questions dont la réponse est presque
toujours celle proposée, et il faut rester devant à taper Entrée pendant
des heures. Posée avant le choix de version — donc elle vaut aussi pour
lui, maintenant qu'il a un défaut — et éteinte par défaut : elle décide
à votre place.
Toutes les invites de la migration passent par une seule lecture,
dix-sept d'un coup. Une invite laissée en input() nu ne saurait rien du
mode auto et bloquerait la migration sans rien dire ; un test lit la
fonction et refuse toute apparition.
click.prompt devait partir : il ne sait pas rendre la main après un
délai, l'auto se serait arrêté à la première question. select() plutôt
qu'un fil ou une alarme — un fil laisserait un input() bloqué derrière
lui, qui volerait la frappe suivante.
Assisted-by: Claude Opus 5
2026-08-17 17:35:31 -04:00
|
|
|
def setUp(self):
|
[ADD] migration: make Enter mean the answer you always give
Auto-run promised to "take the default after five seconds", but every
default was EMPTY: it took nothing. Worse, half the prompts of a
migration are asked by separate processes — the theme uninstaller, the
stale-SCSS detector, the smoke test. They knew nothing of auto-run and
waited forever for a keystroke that never came.
The countdown now lives in one file and travels through the ENVIRONMENT,
the only channel a fork shares. Enter takes the default everywhere, not
only under auto-run: a prompt that prints (Y/n) and does otherwise is
worse than no prompt. Every text was rewritten to say what Enter does,
and every default kept an explicit way out.
Two defaults now write. Both are tenable only because the backup runs
first, and a test locks that ORDER rather than trusting a promise.
--- FR ---
[ADD] migration : faire d'Entrée la réponse qu'on donne toujours
L'auto-exécution promettait « le défaut après cinq secondes », mais tous
les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des
invites d'une migration sont posées par d'autres processus — le
désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils
ignoraient l'auto-exécution et attendaient sans fin une frappe.
Le compte à rebours tient désormais dans un seul fichier et voyage par
l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut
partout, pas seulement en auto : une invite qui affiche (Y/n) et fait
l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée,
et chaque défaut garde une issue explicite.
Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe
d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse.
Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
|
|
|
super().setUp()
|
[ADD] migration: an auto-run that takes the default after five seconds
A migration asks dozens of questions whose answer is almost always the
one offered, and someone has to stand there pressing Enter for hours.
Asked before the version choice — so it covers that choice too, now
that it has a default — and off by default: it decides in your place.
Every prompt of the migration goes through one reader, seventeen of
them. A prompt left as a bare input() would know nothing of auto mode
and would block the run with nothing to say why; a test reads the
function and rejects any that appears.
click.prompt had to go: it cannot hand back control after a delay, so
auto would have stopped at the first question. select() rather than a
thread or an alarm — a thread would leave a blocked input() behind it,
stealing the next keystroke.
--- FR ---
[ADD] migration : une auto-exécution qui prend le défaut après cinq secondes
Une migration pose des dizaines de questions dont la réponse est presque
toujours celle proposée, et il faut rester devant à taper Entrée pendant
des heures. Posée avant le choix de version — donc elle vaut aussi pour
lui, maintenant qu'il a un défaut — et éteinte par défaut : elle décide
à votre place.
Toutes les invites de la migration passent par une seule lecture,
dix-sept d'un coup. Une invite laissée en input() nu ne saurait rien du
mode auto et bloquerait la migration sans rien dire ; un test lit la
fonction et refuse toute apparition.
click.prompt devait partir : il ne sait pas rendre la main après un
délai, l'auto se serait arrêté à la première question. select() plutôt
qu'un fil ou une alarme — un fil laisserait un input() bloqué derrière
lui, qui volerait la frappe suivante.
Assisted-by: Claude Opus 5
2026-08-17 17:35:31 -04:00
|
|
|
self.original = sys.stdin
|
|
|
|
|
self.addCleanup(setattr, sys, "stdin", self.original)
|
|
|
|
|
|
|
|
|
|
def pipe_stdin(self, payload=b""):
|
|
|
|
|
"""Un vrai descripteur : `select` ne sait pas lire un faux objet."""
|
|
|
|
|
read_fd, write_fd = os.pipe()
|
|
|
|
|
if payload:
|
|
|
|
|
os.write(write_fd, payload)
|
|
|
|
|
os.close(write_fd)
|
|
|
|
|
handle = os.fdopen(read_fd)
|
|
|
|
|
self.addCleanup(handle.close)
|
|
|
|
|
sys.stdin = handle
|
|
|
|
|
|
|
|
|
|
def test_without_auto_it_just_asks(self):
|
|
|
|
|
import builtins
|
|
|
|
|
|
|
|
|
|
original = builtins.input
|
|
|
|
|
builtins.input = lambda prompt="": "tapé"
|
|
|
|
|
self.addCleanup(setattr, builtins, "input", original)
|
|
|
|
|
self.assertEqual(upgrade(False).ask("q : ", default="6"), "tapé")
|
|
|
|
|
|
|
|
|
|
def test_with_auto_an_answer_wins(self):
|
|
|
|
|
self.pipe_stdin(b"2\n")
|
|
|
|
|
out = io.StringIO()
|
|
|
|
|
real = sys.stdout
|
|
|
|
|
sys.stdout = out
|
|
|
|
|
try:
|
|
|
|
|
got = upgrade(True).ask("q : ", default="6")
|
|
|
|
|
finally:
|
|
|
|
|
sys.stdout = real
|
|
|
|
|
self.assertEqual(got, "2")
|
|
|
|
|
|
|
|
|
|
def test_a_closed_stdin_gives_the_default(self):
|
|
|
|
|
# Exécution non interactive : le tuyau fermé est lisible TOUT DE
|
|
|
|
|
# SUITE et rend une ligne vide. Sans traiter le vide comme le
|
|
|
|
|
# défaut, le mode auto n'y servirait à rien.
|
|
|
|
|
self.pipe_stdin()
|
|
|
|
|
out = io.StringIO()
|
|
|
|
|
real = sys.stdout
|
|
|
|
|
sys.stdout = out
|
|
|
|
|
try:
|
|
|
|
|
got = upgrade(True).ask("q : ", default="6")
|
|
|
|
|
finally:
|
|
|
|
|
sys.stdout = real
|
|
|
|
|
self.assertEqual(got, "6")
|
|
|
|
|
|
|
|
|
|
def test_silence_gives_the_default_after_the_delay(self):
|
|
|
|
|
# Le tuyau reste OUVERT : rien à lire, et select doit rendre la main
|
|
|
|
|
# au bout du délai plutôt que d'attendre indéfiniment.
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
read_fd, write_fd = os.pipe()
|
|
|
|
|
self.addCleanup(os.close, write_fd)
|
|
|
|
|
handle = os.fdopen(read_fd)
|
|
|
|
|
self.addCleanup(handle.close)
|
|
|
|
|
sys.stdin = handle
|
|
|
|
|
out = io.StringIO()
|
|
|
|
|
real = sys.stdout
|
|
|
|
|
sys.stdout = out
|
|
|
|
|
debut = time.time()
|
|
|
|
|
try:
|
|
|
|
|
got = upgrade(True, delay=0.3).ask("q : ", default="6")
|
|
|
|
|
finally:
|
|
|
|
|
sys.stdout = real
|
|
|
|
|
self.assertEqual(got, "6")
|
|
|
|
|
self.assertGreaterEqual(time.time() - debut, 0.25)
|
|
|
|
|
|
|
|
|
|
def test_an_unselectable_stdin_falls_back_to_asking(self):
|
|
|
|
|
# Ne pas deviner quand on ne peut pas mesurer : mieux vaut demander
|
|
|
|
|
# que rendre un défaut que personne n'a voulu.
|
|
|
|
|
import builtins
|
|
|
|
|
|
|
|
|
|
class NotSelectable:
|
|
|
|
|
def fileno(self):
|
|
|
|
|
raise ValueError("pas de descripteur")
|
|
|
|
|
|
|
|
|
|
sys.stdin = NotSelectable()
|
|
|
|
|
original = builtins.input
|
|
|
|
|
builtins.input = lambda prompt="": "demandé"
|
|
|
|
|
self.addCleanup(setattr, builtins, "input", original)
|
|
|
|
|
out = io.StringIO()
|
|
|
|
|
real = sys.stdout
|
|
|
|
|
sys.stdout = out
|
|
|
|
|
try:
|
|
|
|
|
got = upgrade(True).ask("q : ", default="6")
|
|
|
|
|
finally:
|
|
|
|
|
sys.stdout = real
|
|
|
|
|
self.assertEqual(got, "demandé")
|
|
|
|
|
|
|
|
|
|
|
[ADD] migration: make Enter mean the answer you always give
Auto-run promised to "take the default after five seconds", but every
default was EMPTY: it took nothing. Worse, half the prompts of a
migration are asked by separate processes — the theme uninstaller, the
stale-SCSS detector, the smoke test. They knew nothing of auto-run and
waited forever for a keystroke that never came.
The countdown now lives in one file and travels through the ENVIRONMENT,
the only channel a fork shares. Enter takes the default everywhere, not
only under auto-run: a prompt that prints (Y/n) and does otherwise is
worse than no prompt. Every text was rewritten to say what Enter does,
and every default kept an explicit way out.
Two defaults now write. Both are tenable only because the backup runs
first, and a test locks that ORDER rather than trusting a promise.
--- FR ---
[ADD] migration : faire d'Entrée la réponse qu'on donne toujours
L'auto-exécution promettait « le défaut après cinq secondes », mais tous
les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des
invites d'une migration sont posées par d'autres processus — le
désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils
ignoraient l'auto-exécution et attendaient sans fin une frappe.
Le compte à rebours tient désormais dans un seul fichier et voyage par
l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut
partout, pas seulement en auto : une invite qui affiche (Y/n) et fait
l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée,
et chaque défaut garde une issue explicite.
Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe
d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse.
Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
|
|
|
class TestTheQuestionThatEnablesIt(EnvCase):
|
[ADD] migration: an auto-run that takes the default after five seconds
A migration asks dozens of questions whose answer is almost always the
one offered, and someone has to stand there pressing Enter for hours.
Asked before the version choice — so it covers that choice too, now
that it has a default — and off by default: it decides in your place.
Every prompt of the migration goes through one reader, seventeen of
them. A prompt left as a bare input() would know nothing of auto mode
and would block the run with nothing to say why; a test reads the
function and rejects any that appears.
click.prompt had to go: it cannot hand back control after a delay, so
auto would have stopped at the first question. select() rather than a
thread or an alarm — a thread would leave a blocked input() behind it,
stealing the next keystroke.
--- FR ---
[ADD] migration : une auto-exécution qui prend le défaut après cinq secondes
Une migration pose des dizaines de questions dont la réponse est presque
toujours celle proposée, et il faut rester devant à taper Entrée pendant
des heures. Posée avant le choix de version — donc elle vaut aussi pour
lui, maintenant qu'il a un défaut — et éteinte par défaut : elle décide
à votre place.
Toutes les invites de la migration passent par une seule lecture,
dix-sept d'un coup. Une invite laissée en input() nu ne saurait rien du
mode auto et bloquerait la migration sans rien dire ; un test lit la
fonction et refuse toute apparition.
click.prompt devait partir : il ne sait pas rendre la main après un
délai, l'auto se serait arrêté à la première question. select() plutôt
qu'un fil ou une alarme — un fil laisserait un input() bloqué derrière
lui, qui volerait la frappe suivante.
Assisted-by: Claude Opus 5
2026-08-17 17:35:31 -04:00
|
|
|
def setUp(self):
|
[ADD] migration: make Enter mean the answer you always give
Auto-run promised to "take the default after five seconds", but every
default was EMPTY: it took nothing. Worse, half the prompts of a
migration are asked by separate processes — the theme uninstaller, the
stale-SCSS detector, the smoke test. They knew nothing of auto-run and
waited forever for a keystroke that never came.
The countdown now lives in one file and travels through the ENVIRONMENT,
the only channel a fork shares. Enter takes the default everywhere, not
only under auto-run: a prompt that prints (Y/n) and does otherwise is
worse than no prompt. Every text was rewritten to say what Enter does,
and every default kept an explicit way out.
Two defaults now write. Both are tenable only because the backup runs
first, and a test locks that ORDER rather than trusting a promise.
--- FR ---
[ADD] migration : faire d'Entrée la réponse qu'on donne toujours
L'auto-exécution promettait « le défaut après cinq secondes », mais tous
les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des
invites d'une migration sont posées par d'autres processus — le
désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils
ignoraient l'auto-exécution et attendaient sans fin une frappe.
Le compte à rebours tient désormais dans un seul fichier et voyage par
l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut
partout, pas seulement en auto : une invite qui affiche (Y/n) et fait
l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée,
et chaque défaut garde une issue explicite.
Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe
d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse.
Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
|
|
|
super().setUp()
|
[ADD] migration: an auto-run that takes the default after five seconds
A migration asks dozens of questions whose answer is almost always the
one offered, and someone has to stand there pressing Enter for hours.
Asked before the version choice — so it covers that choice too, now
that it has a default — and off by default: it decides in your place.
Every prompt of the migration goes through one reader, seventeen of
them. A prompt left as a bare input() would know nothing of auto mode
and would block the run with nothing to say why; a test reads the
function and rejects any that appears.
click.prompt had to go: it cannot hand back control after a delay, so
auto would have stopped at the first question. select() rather than a
thread or an alarm — a thread would leave a blocked input() behind it,
stealing the next keystroke.
--- FR ---
[ADD] migration : une auto-exécution qui prend le défaut après cinq secondes
Une migration pose des dizaines de questions dont la réponse est presque
toujours celle proposée, et il faut rester devant à taper Entrée pendant
des heures. Posée avant le choix de version — donc elle vaut aussi pour
lui, maintenant qu'il a un défaut — et éteinte par défaut : elle décide
à votre place.
Toutes les invites de la migration passent par une seule lecture,
dix-sept d'un coup. Une invite laissée en input() nu ne saurait rien du
mode auto et bloquerait la migration sans rien dire ; un test lit la
fonction et refuse toute apparition.
click.prompt devait partir : il ne sait pas rendre la main après un
délai, l'auto se serait arrêté à la première question. select() plutôt
qu'un fil ou une alarme — un fil laisserait un input() bloqué derrière
lui, qui volerait la frappe suivante.
Assisted-by: Claude Opus 5
2026-08-17 17:35:31 -04:00
|
|
|
self.addCleanup(
|
|
|
|
|
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
|
|
|
|
|
)
|
|
|
|
|
todo_i18n._current_lang = "en"
|
|
|
|
|
|
|
|
|
|
def answer(self, typed):
|
|
|
|
|
import builtins
|
|
|
|
|
import contextlib
|
|
|
|
|
|
|
|
|
|
obj = TodoUpgrade.__new__(TodoUpgrade)
|
|
|
|
|
original = builtins.input
|
|
|
|
|
builtins.input = lambda prompt="": typed
|
|
|
|
|
self.addCleanup(setattr, builtins, "input", original)
|
|
|
|
|
out = io.StringIO()
|
|
|
|
|
with contextlib.redirect_stdout(out):
|
|
|
|
|
obj.prompt_auto_execute()
|
|
|
|
|
return obj.auto_execute, out.getvalue()
|
|
|
|
|
|
|
|
|
|
def test_yes_turns_it_on(self):
|
|
|
|
|
on, text = self.answer("y")
|
|
|
|
|
self.assertTrue(on)
|
|
|
|
|
self.assertIn("Auto-run on", text)
|
|
|
|
|
|
|
|
|
|
def test_the_default_is_off(self):
|
|
|
|
|
# Elle prend des décisions à votre place : ce n'est pas le défaut.
|
|
|
|
|
on, text = self.answer("")
|
|
|
|
|
self.assertFalse(on)
|
|
|
|
|
self.assertEqual(text, "")
|
|
|
|
|
|
|
|
|
|
def test_anything_else_is_off(self):
|
|
|
|
|
self.assertFalse(self.answer("peut-être")[0])
|
|
|
|
|
|
|
|
|
|
def test_the_delay_is_five_seconds(self):
|
|
|
|
|
self.assertEqual(TodoUpgrade.AUTO_DELAY, 5)
|
|
|
|
|
|
[ADD] migration: make Enter mean the answer you always give
Auto-run promised to "take the default after five seconds", but every
default was EMPTY: it took nothing. Worse, half the prompts of a
migration are asked by separate processes — the theme uninstaller, the
stale-SCSS detector, the smoke test. They knew nothing of auto-run and
waited forever for a keystroke that never came.
The countdown now lives in one file and travels through the ENVIRONMENT,
the only channel a fork shares. Enter takes the default everywhere, not
only under auto-run: a prompt that prints (Y/n) and does otherwise is
worse than no prompt. Every text was rewritten to say what Enter does,
and every default kept an explicit way out.
Two defaults now write. Both are tenable only because the backup runs
first, and a test locks that ORDER rather than trusting a promise.
--- FR ---
[ADD] migration : faire d'Entrée la réponse qu'on donne toujours
L'auto-exécution promettait « le défaut après cinq secondes », mais tous
les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des
invites d'une migration sont posées par d'autres processus — le
désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils
ignoraient l'auto-exécution et attendaient sans fin une frappe.
Le compte à rebours tient désormais dans un seul fichier et voyage par
l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut
partout, pas seulement en auto : une invite qui affiche (Y/n) et fait
l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée,
et chaque défaut garde une issue explicite.
Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe
d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse.
Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
|
|
|
def test_yes_also_arms_the_tools_launched_apart(self):
|
|
|
|
|
# La moitié des invites d'une migration sont posées par d'autres
|
|
|
|
|
# processus. Sans cette variable, ils attendraient une frappe qui
|
|
|
|
|
# ne vient jamais — et l'automatisation s'arrêterait là, en
|
|
|
|
|
# silence, puisque la question a bien été posée.
|
|
|
|
|
from script.todo import auto_ask
|
|
|
|
|
|
|
|
|
|
self.answer("y")
|
|
|
|
|
self.assertEqual(os.environ.get(auto_ask.ENV_ENABLED), "1")
|
|
|
|
|
|
|
|
|
|
def test_no_leaves_nothing_behind(self):
|
|
|
|
|
from script.todo import auto_ask
|
|
|
|
|
|
|
|
|
|
os.environ[auto_ask.ENV_ENABLED] = "1"
|
|
|
|
|
self.answer("")
|
|
|
|
|
self.assertNotIn(auto_ask.ENV_ENABLED, os.environ)
|
|
|
|
|
|
[ADD] migration: an auto-run that takes the default after five seconds
A migration asks dozens of questions whose answer is almost always the
one offered, and someone has to stand there pressing Enter for hours.
Asked before the version choice — so it covers that choice too, now
that it has a default — and off by default: it decides in your place.
Every prompt of the migration goes through one reader, seventeen of
them. A prompt left as a bare input() would know nothing of auto mode
and would block the run with nothing to say why; a test reads the
function and rejects any that appears.
click.prompt had to go: it cannot hand back control after a delay, so
auto would have stopped at the first question. select() rather than a
thread or an alarm — a thread would leave a blocked input() behind it,
stealing the next keystroke.
--- FR ---
[ADD] migration : une auto-exécution qui prend le défaut après cinq secondes
Une migration pose des dizaines de questions dont la réponse est presque
toujours celle proposée, et il faut rester devant à taper Entrée pendant
des heures. Posée avant le choix de version — donc elle vaut aussi pour
lui, maintenant qu'il a un défaut — et éteinte par défaut : elle décide
à votre place.
Toutes les invites de la migration passent par une seule lecture,
dix-sept d'un coup. Une invite laissée en input() nu ne saurait rien du
mode auto et bloquerait la migration sans rien dire ; un test lit la
fonction et refuse toute apparition.
click.prompt devait partir : il ne sait pas rendre la main après un
délai, l'auto se serait arrêté à la première question. select() plutôt
qu'un fil ou une alarme — un fil laisserait un input() bloqué derrière
lui, qui volerait la frappe suivante.
Assisted-by: Claude Opus 5
2026-08-17 17:35:31 -04:00
|
|
|
|
|
|
|
|
class TestWhereItIsAsked(unittest.TestCase):
|
|
|
|
|
def source(self):
|
|
|
|
|
return inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
|
|
|
|
|
|
|
|
|
|
def test_it_comes_before_the_version_choice(self):
|
|
|
|
|
# « avant de choisir la version d'Odoo » : activée, elle vaut aussi
|
|
|
|
|
# pour ce choix-là, qui a désormais un défaut.
|
|
|
|
|
source = self.source()
|
|
|
|
|
self.assertLess(
|
|
|
|
|
source.index("prompt_auto_execute"),
|
|
|
|
|
source.index("Which version do you want to upgrade to?"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_the_version_choice_goes_through_it(self):
|
|
|
|
|
# click.prompt ne sait pas rendre la main après un délai : le laisser
|
|
|
|
|
# là aurait fait un mode auto qui s'arrête à la première question.
|
|
|
|
|
source = self.source()
|
|
|
|
|
self.assertNotIn("click.prompt(", source)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestNoPromptEscapesIt(unittest.TestCase):
|
|
|
|
|
"""Une invite en `input()` nu ne saurait rien du mode auto.
|
|
|
|
|
|
|
|
|
|
Elle bloquerait la migration sans rien signaler — et c'est précisément
|
|
|
|
|
le genre de chose qu'on n'ajoute pas exprès, mais par habitude.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_every_prompt_of_the_migration_uses_ask(self):
|
|
|
|
|
path = os.path.join(REPO, "script", "todo", "todo_upgrade.py")
|
|
|
|
|
with open(path) as handle:
|
|
|
|
|
tree = ast.parse(handle.read())
|
|
|
|
|
target = [
|
|
|
|
|
node
|
|
|
|
|
for node in ast.walk(tree)
|
|
|
|
|
if isinstance(node, ast.FunctionDef)
|
|
|
|
|
and node.name == "execute_odoo_upgrade"
|
|
|
|
|
]
|
|
|
|
|
self.assertEqual(len(target), 1, "fonction introuvable")
|
|
|
|
|
bare = [
|
|
|
|
|
node.lineno
|
|
|
|
|
for node in ast.walk(target[0])
|
|
|
|
|
if isinstance(node, ast.Call)
|
|
|
|
|
and getattr(node.func, "id", "") == "input"
|
|
|
|
|
]
|
|
|
|
|
self.assertEqual(bare, [], "invites hors du mode auto")
|
|
|
|
|
|
|
|
|
|
def test_the_gate_uses_it_too(self):
|
|
|
|
|
# `ask_gate` porte le retour en arrière : la laisser en input() nu
|
|
|
|
|
# aurait fait un mode auto qui s'arrête à chaque « b = revenir ».
|
|
|
|
|
source = inspect.getsource(TodoUpgrade.ask_gate)
|
|
|
|
|
self.assertIn("self.ask(", source)
|
|
|
|
|
self.assertNotIn("= input(", source)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|