[ADD] migration: Enter targets the highest supported Odoo version
Migrating means going all the way, and that choice had to be typed every time. Enter did nothing visible: click.prompt without a default re-asks on an empty line without printing anything, so a default added without `default=""` would never have been reached and nothing would have said so. The highest is computed, not read off the end of the list: display order is not a guarantee, and « 9.0 » sorts after « 18.0 » as a string. The prompt announces it — a default nobody sees is a default nobody uses. No choice at all means no default rather than a crash. --- FR --- [ADD] migration : Entrée vise la version Odoo la plus élevée Migrer, c'est aller au bout, et ce choix devait être tapé chaque fois. Entrée ne faisait rien de visible : click.prompt sans défaut redemande sur une ligne vide sans rien afficher, si bien qu'un défaut ajouté sans `default=""` n'aurait jamais été atteint, sans que rien ne le signale. La plus haute est calculée, pas lue au bout de la liste : l'ordre d'affichage n'est pas une garantie, et « 9.0 » se trie après « 18.0 » en chaînes. L'invite l'annonce — un défaut qu'on ne montre pas est un défaut que personne n'utilise. Aucun choix possible ne donne aucun défaut plutôt qu'une erreur. Assisted-by: Claude Opus 5 (cherry picked from commit f0861363f496cdc3c0bb02216d30dd0aa8d3697b)
This commit is contained in:
parent
bc52a93e67
commit
c67324a223
3 changed files with 141 additions and 2 deletions
|
|
@ -5062,6 +5062,10 @@ TRANSLATIONS = {
|
|||
"fr": "il serait testé à la place de cette base.",
|
||||
"en": "it would be tested instead of this database.",
|
||||
},
|
||||
"Enter =": {
|
||||
"fr": "Entrée =",
|
||||
"en": "Enter =",
|
||||
},
|
||||
"Nothing to decide yet": {
|
||||
"fr": "Rien à décider pour l'instant",
|
||||
"en": "Nothing to decide yet",
|
||||
|
|
|
|||
|
|
@ -1248,11 +1248,36 @@ class TodoUpgrade:
|
|||
if "target_odoo_version" in self.dct_progression:
|
||||
odoo_target_version = self.dct_progression["target_odoo_version"]
|
||||
else:
|
||||
print(f"💬 {t('Which version do you want to upgrade to?')}")
|
||||
# La plus haute, pas la dernière de la liste : l'ordre
|
||||
# d'affichage n'est pas une garantie, la comparaison en est une.
|
||||
default_version = (
|
||||
max(
|
||||
lst_odoo_version,
|
||||
key=lambda a: float(a.get("prompt_description")),
|
||||
).get("prompt_description")
|
||||
if lst_odoo_version
|
||||
else None
|
||||
)
|
||||
print(
|
||||
f"💬 {t('Which version do you want to upgrade to?')}"
|
||||
+ (
|
||||
f" ({t('Enter =')} {default_version})"
|
||||
if default_version
|
||||
else ""
|
||||
)
|
||||
)
|
||||
odoo_target_version = None
|
||||
cmd_no_found = True
|
||||
while cmd_no_found:
|
||||
status = click.prompt(help_info)
|
||||
# `default=""` est nécessaire : sans lui, click redemande sur
|
||||
# une ligne vide sans rien dire, et Entrée n'atteint jamais
|
||||
# ce code — la valeur par défaut n'existait qu'en intention.
|
||||
status = click.prompt(
|
||||
help_info, default="", show_default=False
|
||||
)
|
||||
if not str(status).strip() and default_version:
|
||||
odoo_target_version = default_version
|
||||
break
|
||||
try:
|
||||
int_cmd = int(status)
|
||||
if 0 < int_cmd <= len(lst_odoo_version):
|
||||
|
|
|
|||
110
test/test_target_version_default.py
Executable file
110
test/test_target_version_default.py
Executable file
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
"""« Vers quelle version migrer ? » : Entrée doit viser la plus haute.
|
||||
|
||||
C'est le choix qu'on fait presque toujours — migrer, c'est aller au bout —
|
||||
et il fallait le taper. Pire : `click.prompt` sans `default` REDEMANDE sur
|
||||
une ligne vide, sans rien afficher. Entrée n'atteignait donc aucun code ;
|
||||
une valeur par défaut ajoutée sans ce `default=""` n'aurait jamais servi et
|
||||
rien ne l'aurait signalé.
|
||||
|
||||
La plus haute se calcule, elle ne se lit pas au bout de la liste : l'ordre
|
||||
d'affichage n'est pas une garantie, la comparaison en est une.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import inspect
|
||||
import os
|
||||
import unittest
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from script.todo.todo_upgrade import TodoUpgrade # noqa: E402
|
||||
|
||||
|
||||
class TestTheSource(unittest.TestCase):
|
||||
"""Le choix se joue dans une boucle interactive de mille lignes.
|
||||
|
||||
On vérifie donc les décisions à la source : que le défaut soit calculé
|
||||
par comparaison, et que click puisse rendre une ligne vide.
|
||||
"""
|
||||
|
||||
def source(self):
|
||||
return inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
|
||||
|
||||
def test_click_can_return_an_empty_line(self):
|
||||
# SANS cela, click redemande en silence et Entrée n'arrive jamais
|
||||
# jusqu'au code qui applique le défaut.
|
||||
source = self.source()
|
||||
appel = source.index("click.prompt(")
|
||||
fenetre = source[appel : appel + 160]
|
||||
self.assertIn('default=""', fenetre)
|
||||
|
||||
def test_the_default_is_the_highest_not_the_last(self):
|
||||
source = self.source()
|
||||
self.assertIn("max(", source)
|
||||
self.assertIn('float(a.get("prompt_description"))', source)
|
||||
|
||||
def test_the_default_is_announced(self):
|
||||
# Un défaut qu'on ne montre pas est un défaut que personne n'utilise.
|
||||
self.assertIn("Enter =", self.source())
|
||||
|
||||
|
||||
class TestTheChoiceItself(unittest.TestCase):
|
||||
"""Le calcul du défaut, extrait et rejoué sur des listes réelles."""
|
||||
|
||||
def highest(self, lst_version):
|
||||
lst_odoo_version = [{"prompt_description": v} for v in lst_version]
|
||||
return (
|
||||
max(
|
||||
lst_odoo_version,
|
||||
key=lambda a: float(a.get("prompt_description")),
|
||||
).get("prompt_description")
|
||||
if lst_odoo_version
|
||||
else None
|
||||
)
|
||||
|
||||
def test_the_case_reported(self):
|
||||
# Base en 12.0 : la liste proposée va de 13.0 à 18.0.
|
||||
self.assertEqual(
|
||||
self.highest(["13.0", "14.0", "15.0", "16.0", "17.0", "18.0"]),
|
||||
"18.0",
|
||||
)
|
||||
|
||||
def test_an_unsorted_list_still_gives_the_highest(self):
|
||||
# Prendre le dernier élément marcherait par chance ; comparer marche.
|
||||
self.assertEqual(self.highest(["18.0", "13.0", "16.0"]), "18.0")
|
||||
|
||||
def test_a_single_choice(self):
|
||||
self.assertEqual(self.highest(["18.0"]), "18.0")
|
||||
|
||||
def test_no_choice_at_all_has_no_default(self):
|
||||
# Une base déjà à la version maximale : proposer un défaut
|
||||
# inexistant ferait planter là où il n'y a simplement rien à faire.
|
||||
self.assertIsNone(self.highest([]))
|
||||
|
||||
def test_two_digit_versions_compare_as_numbers(self):
|
||||
# « 9.0 » > « 18.0 » en tri de chaînes : la comparaison numérique
|
||||
# est ce qui évite de proposer une version antérieure.
|
||||
self.assertEqual(self.highest(["9.0", "18.0"]), "18.0")
|
||||
|
||||
|
||||
class TestTheGuardIsReachable(unittest.TestCase):
|
||||
def test_the_empty_answer_is_handled_before_the_int(self):
|
||||
# int("") lève ValueError et retomberait sur « Commande non
|
||||
# trouvée » : le cas vide doit être traité AVANT.
|
||||
source = inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
|
||||
vide = source.index("if not str(status).strip()")
|
||||
conversion = source.index("int_cmd = int(status)")
|
||||
self.assertLess(vide, conversion)
|
||||
|
||||
def test_the_module_still_parses(self):
|
||||
path = os.path.join(REPO, "script", "todo", "todo_upgrade.py")
|
||||
with open(path) as handle:
|
||||
ast.parse(handle.read())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue