From da2dc63a19c5e8448461ad8672390cf65869a5aa Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 24 Sep 2026 13:30:47 -0400 Subject: [PATCH] =?UTF-8?q?[FIX]=20outillage=20:=20d=C3=A9simbriquer=20les?= =?UTF-8?q?=20f-strings=20que=20seul=203.12=20sait=20lire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cinq fichiers employaient des f-strings PEP 701 — guillemets imbriqués du même type, backslash dans l'expression, expression sur plusieurs lignes — que Python refuse avant 3.12. Or les hooks de script/git/hooks portent « #!/usr/bin/env python3 » et tournent donc sur l'interpréteur du système, qu'une distribution livre encore en 3.10 : la SyntaxError tombe au chargement, sans qu'aucun garde puisse nommer la commande à taper. Les chaînes traduites gardent leur texte à l'octet près, sans quoi leur clé se perdrait. Les commentaires de ces fichiers passent du récit au présent, comme la règle le demande de ce qu'on touche. Vérifié : tout l'arbre Python parse sous 3.10, 3.11, 3.12 et 3.14. --- EN --- Five files used PEP 701 f-strings — quotes of the same kind nested, a backslash in the expression, an expression spanning lines — which Python refuses before 3.12. The hooks in script/git/hooks carry "#!/usr/bin/env python3" and so run on the system interpreter, which a distribution still ships as 3.10: the SyntaxError lands at load, before any guard can name the command to type. Translated strings keep their text byte for byte, else their key would be lost. The comments of those files move from tale to present tense, as the rule asks of what one touches. Checked: the whole Python tree parses under 3.10, 3.11, 3.12 and 3.14. Assisted-by: Claude Opus 5 --- script/analyse/analyse_view_custom.py | 6 +-- script/analyse/check_migration_quality.py | 57 ++++++++++---------- script/systemd/install_daemon.py | 15 +++--- script/todo/assistant_menu.py | 15 +++--- script/todo/qemu_install.py | 63 ++++++++++++----------- 5 files changed, 77 insertions(+), 79 deletions(-) diff --git a/script/analyse/analyse_view_custom.py b/script/analyse/analyse_view_custom.py index 7fdb22d..3f1d584 100755 --- a/script/analyse/analyse_view_custom.py +++ b/script/analyse/analyse_view_custom.py @@ -502,8 +502,8 @@ def collect( # et ce qu'il rapporte est fiable. « all » compare toute vue ayant # un arch_fs, ce qui trouve la dérive qu'aucun drapeau ne signale — # une vue réécrite en SQL direct — mais au prix d'un plancher de - # bruit MESURÉ : sur une base 18.0 fraîchement installée, 160 des - # 974 vues à arch_fs diffèrent déjà. read_arch_from_file rend le + # bruit : sur une base 18.0 fraîchement installée, 160 des 974 + # vues à arch_fs diffèrent déjà. read_arch_from_file rend le # XML brut du fichier, alors que la base porte l'arch APRÈS # traitement au chargement : un attribut « groups » est consommé, # un est appliqué. En « all », un @@ -707,7 +707,7 @@ def render(data, verbose=False, top=TOP_DEFAULT, category=None, hints=True): f"🔬 {t('Customised views')} — {data['database']} (Odoo {version}" f"{', ' + t('from a backup') if data.get('source') == 'backup' else ''})", "", - f" {t("Views"):<38}: {data['n_views']}", + f" {t('Views'):<38}: {data['n_views']}", ] for name in CATEGORIES: if counts.get(name): diff --git a/script/analyse/check_migration_quality.py b/script/analyse/check_migration_quality.py index 8c5ee59..835db07 100755 --- a/script/analyse/check_migration_quality.py +++ b/script/analyse/check_migration_quality.py @@ -19,9 +19,8 @@ lecture seule, plutôt que de rejouer quoi que ce soit. Pourquoi pas en démarrant Odoo ------------------------------ Six démarrages coûteraient une heure, écriraient dans les bases et -demanderaient de basculer le checkout à chaque palier. Mesuré : la même -inspection en SQL prend moins d'une demi-seconde par base, et ne touche à -rien. Ce qu'on y perd — les modèles abstraits, les champs calculés — ne +demanderaient de basculer le checkout à chaque palier. La même inspection +en SQL prend moins d'une demi-seconde par base, et ne touche à rien. Ce qu'on y perd — les modèles abstraits, les champs calculés — ne se compare pas d'une version à l'autre de toute façon. Ce qui compte le plus @@ -184,12 +183,12 @@ def version_of(database, dct): # Ce que la migration écrit dans le journal d'une étape autour d'un test : # -# [2026-08-26 03:19:44.166204] $ .venv…/python3 ./script/…/smoke.py -d … -# [2026-08-26 03:19:59.846406] -> 1 -# [2026-08-26 03:19:59.847489] [test] smoke_public_url -> 1 +# [] $ .venv…/python3 ./script/…/smoke.py -d … +# [] -> 1 +# [] [test] smoke_public_url -> 1 # -# Entre le « $ » et le « -> », RIEN : mesuré sur trois exécutions du même -# test, la sortie de l'outil n'est pas capturée. Ce qui explique l'échec +# Entre le « $ » et le « -> », RIEN : la sortie de l'outil n'est pas +# capturée. Ce qui explique l'échec # est donc AVANT, dans ce qu'Odoo écrivait juste avant qu'on le teste — # et c'est pour cela que l'extrait remonte, au lieu de descendre. MARQUEUR_COMMANDE = "] $ " @@ -451,7 +450,7 @@ def table_counts(database): Construite côté serveur puis exécutée d'un bloc : huit cents requêtes séparées coûteraient huit cents allers-retours, là où celle-ci prend - quatre dixièmes de seconde — mesuré sur une base de 890 tables. + quatre dixièmes de seconde sur une base de 890 tables. """ fabrique = run_psql( database, @@ -546,11 +545,11 @@ def inspect(database): etat["field_module"] = {cle: sorted(v) for cle, v in dct_origine.items()} # Lesquels PORTENT une donnée. Un champ `store=false` n'a jamais eu # de colonne : sa disparition ne perd rien, et il pesait jusqu'à 90 % - # du seau « non déclarés par OpenUpgrade ». Mesuré au palier 16 → 17 : - # `__last_update` à lui seul comptait pour 397 des 565. + # du seau « non déclarés par OpenUpgrade ». Au palier 16 → 17, + # `__last_update` compte à lui seul pour 397 des 565. # - # `store` existe de la 12 à la 18 et n'est jamais NULL — vérifié sur - # les sept bases — donc le renseignement est fiable partout. + # `store` existe de la 12 à la 18 et n'y est jamais NULL : le + # renseignement est donc fiable partout. stockes = run_psql( database, "SELECT model || '.' || name FROM ir_model_fields WHERE store", @@ -560,8 +559,8 @@ def inspect(database): # # Une pièce jointe dont `res_field` ne nomme aucun champ vivant est # DÉJÀ illisible : Odoo lève un KeyError en la contrôlant. Ce ne sont - # pas des données, ce sont des débris. Mesuré sur une chaîne 12 → 18 : - # la dette naît aux paliers 13 et 14, reste gelée pendant trois + # pas des données, ce sont des débris. Sur une chaîne 12 → 18, la + # dette naît aux paliers 13 et 14, reste gelée pendant trois # paliers, et la 18 ramasse 452 lignes d'un coup — qui se lisent # alors comme 452 pertes. # @@ -700,8 +699,7 @@ def render_detail(diff, categorie, colour=False): (gagnes, "+", "ok"), ): lignes.append( - f"── {paint(symbole, teinte, colour)} {len(lst)}" - f" {t(categorie)} ──" + f"── {paint(symbole, teinte, colour)} {len(lst)} {t(categorie)} ──" ) lignes.extend(f" {nom}" for nom in lst) lignes.append("") @@ -724,8 +722,7 @@ def render_missing(etat, colour=False, limit=60): return f"✅ {t('every attachment file is present')}" lignes = [ paint( - f"❌ {absents}" - f" {t('attachment files missing from the filestore')}", + f"❌ {absents} {t('attachment files missing from the filestore')}", "fail", colour, ), @@ -1019,8 +1016,8 @@ def overlay_declared( champs["model_gone"].append(cle) continue # AVANT « non analysé » : « sans donnée propre » est une raison - # plus forte que « hors du champ d'OpenUpgrade ». Mesuré au - # palier 16 → 17, le placement avant fait tomber `not_analysed` + # plus forte que « hors du champ d'OpenUpgrade ». Au palier + # 16 → 17, le placement avant fait tomber `not_analysed` # de 181 à 47 sans changer `undeclared` — le seau résiduel se # réduit alors au risque réel : des champs qui AVAIENT des # données, dans des modules dont OpenUpgrade ne peut rien dire. @@ -1256,11 +1253,10 @@ RENAME_RATIO = 0.75 def looks_renamed(un, deux): """Les deux noms se ressemblent-ils assez pour être le même sujet ? - Deux garde-fous ont été essayés et rejetés, mesurés sur une vraie - migration. Le seul nombre de lignes accouplait - `account_account_tag_account_tax_template_rel` à `dms_directory` : les - deux comptaient sept lignes. Un mot commun d'au moins cinq lettres - accouplait `cleanup_purge_wizard_menu` à + Deux garde-fous plus simples ne tiennent pas. Le seul nombre de lignes + accouple `account_account_tag_account_tax_template_rel` à + `dms_directory` dès qu'ils en comptent autant. Un mot commun d'au moins + cinq lettres accouple `cleanup_purge_wizard_menu` à `cleanup_create_indexes_line` — « cleanup » ne dit rien. La ressemblance d'ENSEMBLE tranche : `muk_dms_directory` et @@ -1315,9 +1311,9 @@ SANS_DONNEE_PROPRE = ("id",) # Pourquoi une pièce jointe a disparu — DÉDUIT, jamais déclaré. # # SEMANTIC_MAP ne peut pas porter ceci : elle nomme une TABLE, et la -# cause n'est pas la table, ce sont ces lignes-là. Mesuré sur une chaîne -# 12 → 18 : des 516 lignes parties au palier 18, 452 avaient perdu leur -# champ et 63 leur enregistrement. Une entrée « ir_attachment / pruned » +# cause n'est pas la table, ce sont ces lignes-là. Sur une chaîne 12 → 18, +# des 516 lignes parties au palier 18, 452 ont perdu leur champ et 63 leur +# enregistrement. Une entrée « ir_attachment / pruned » # aurait rangé les 516 sous « perte attendue » — et la 517e avec. ATTACHMENT_KIND = ( ("field_debt", "their field was already gone before this step", "dim"), @@ -1443,8 +1439,9 @@ def render_text(lst_snapshot, colour=None, limit=8): f" ⚠️ {etat['database']} : {t('database not found')}" ) continue + etape = paint(f"{etat['odoo']:<6}", "step", colour) lignes.append( - f" {paint(f'{etat['odoo']:<6}', 'step', colour)}" + f" {etape}" f" {etat['database']:<34}" f" {len(etat['installed']):>4} {t('modules')}" f" · {len(etat['model']):>4} {t('models')}" diff --git a/script/systemd/install_daemon.py b/script/systemd/install_daemon.py index 0369940..ca90639 100755 --- a/script/systemd/install_daemon.py +++ b/script/systemd/install_daemon.py @@ -13,10 +13,10 @@ from pathlib import Path # # Lancé seul, systemd doit EXÉCUTER le fichier, et l'échoue en « 203/EXEC » # dans quatre cas au moins : bit x absent, shebang qui ne résout pas, /home -# monté noexec, SELinux refusant l'execve. Vécu sur openSUSE s390x — le -# processus mourait en 3 ms, sans jamais entrer dans le script, ce qui rend le -# diagnostic très pénible : aucune sortie, et un code qui ressemble à une -# erreur d'application. +# monté noexec, SELinux refusant l'execve. L'échec est alors MUET : le +# processus meurt avant d'entrer dans le script, sans une ligne de sortie, et +# 203 ressemble à une erreur d'application — le diagnostic part donc dans la +# mauvaise direction. # # Passé à bash, run.sh n'est plus qu'une DONNÉE lue : les quatre causes # disparaissent ensemble, y compris noexec et SELinux, qui ne portent que sur @@ -116,10 +116,9 @@ def main(): args.config_name or f"erplibre_{el_user}_{os.path.basename(os.getcwd())}" ) - exec_param = ( - " " - + f" {w_cmd("-d", args.database or "")} {w_cmd("-p", args.port or "")}".strip() - ) + db_param = w_cmd("-d", args.database or "") + port_param = w_cmd("-p", args.port or "") + exec_param = " " + f" {db_param} {port_param}".strip() # Render the systemd service file content unit_content = UNIT_TEMPLATE.format( diff --git a/script/todo/assistant_menu.py b/script/todo/assistant_menu.py index 0852065..7439189 100644 --- a/script/todo/assistant_menu.py +++ b/script/todo/assistant_menu.py @@ -32,6 +32,7 @@ Le dépôt n'a ni pager, ni progression sur place : la sortie s'ajoute ligne à ligne. Une réponse longue se ferme sur une ligne de pied, jamais sur un défilement piloté. """ + from __future__ import annotations import os @@ -386,8 +387,7 @@ class AssistantMenuMixin: choices = [ { "prompt_description": ( - f"{t('Here (127.0.0.1)')}" - f" ({t('11 ports, instant')})" + f"{t('Here (127.0.0.1)')} ({t('11 ports, instant')})" ) }, { @@ -805,9 +805,8 @@ class AssistantMenuMixin: f" {t('Look somewhere else')} · {t('Type an address')}" f" · {t('Carry on with the OpenAI API (key from the vault)')}" ) - print( - f" 💡 {t('A local server: \"ollama serve\" listens on 11434.')}" - ) + astuce = t('A local server: "ollama serve" listens on 11434.') + print(f" 💡 {astuce}") def _llm_server_card(self): """Ce que le serveur en usage annonce savoir faire. @@ -1051,8 +1050,7 @@ class AssistantMenuMixin: if session is None: return commande = ( - f"{shlex.quote(chemin)} --resume" - f" {shlex.quote(session.session_id)}" + f"{shlex.quote(chemin)} --resume {shlex.quote(session.session_id)}" ) if not getattr(self.execute, "cmd_source_default", ""): print(t("No terminal can be opened here. Paste this command:")) @@ -1097,8 +1095,7 @@ class AssistantMenuMixin: 1 for _, verdict, _ in self._llm_apparier(gpts) if verdict == "ok" ) return ( - f"{t('gpt tools')} ({len(gpts)}," - f" {compatibles} {t('compatible')})" + f"{t('gpt tools')} ({len(gpts)}, {compatibles} {t('compatible')})" ) def _llm_apparier(self, gpts): diff --git a/script/todo/qemu_install.py b/script/todo/qemu_install.py index 5daf1ac..7bc009e 100644 --- a/script/todo/qemu_install.py +++ b/script/todo/qemu_install.py @@ -506,10 +506,12 @@ class QemuInstallMixin: # ERREUR, alors que son étape finale écrit encore l'autorité, les # variables et le sudoers : attendre l'unité, faute de quoi elles # arrivent après cette session, qui vivra sans elles. - + attente_cloud_final() + "; " + + attente_cloud_final() + + "; " # Les variables du cache sont écrites par cloud-init PENDANT # l'attente : cette session, ouverte avant, ne les a pas reçues. - + cache_env_reload() + "; " + + cache_env_reload() + + "; " # ICI, et nulle part avant. Le faisceau que ces exports désignent # est écrit par cloud-init lui aussi : mesuré sur une VM, la # session ssh est acceptée une seconde avant qu'il existe, donc @@ -599,31 +601,28 @@ class QemuInstallMixin: # Miroirs openSUSE préférés, du plus proche au dernier recours. Le # redirecteur officiel n'est PAS géographique pour cette distribution : - # mesuré depuis Montréal sur les métadonnées oss s390x (15 Mo), - # download.opensuse.org met 23,8 s — il sert depuis l'Europe — contre - # 2,7 s pour mirrors.rit.edu. Les trois familles dnf, elles, choisissent - # déjà un miroir canadien toutes seules ; rien à faire de ce côté. + # download.opensuse.org sert depuis l'Europe, là où un miroir + # nord-américain répond en une fraction du temps sur les mêmes + # métadonnées. Les trois familles dnf, elles, choisissent déjà un miroir + # proche toutes seules ; rien à faire de ce côté. # # Chaque miroir est SONDÉ sur le chemin de l'architecture ET du produit # courants, puis le premier qui répond gagne. C'est nécessaire : aucun ne - # réplique tout. Relevé le 2026-08-12 — - # csclub Leap oui, Tumbleweed non (404) - # rit.edu zsystems oui ; injoignable ce jour-là (curl 7) - # leaseweb Tumbleweed x86_64 et Leap oui, ports zsystems non - # D'où plusieurs entrées plutôt qu'une : avec la seule rit.edu, sa panne - # renvoyait tout le monde sur download.opensuse.org, servi d'Europe. - # Ordonnées par proximité de Montréal. Aucun sondage concluant : on garde - # les dépôts de l'image, donc le comportement d'avant. + # réplique tout — l'un sert Leap mais pas Tumbleweed, l'autre les ports + # zsystems mais pas x86_64. D'où plusieurs entrées plutôt qu'une : avec un + # seul miroir, sa panne renvoie tout le monde sur le redirecteur, servi + # d'Europe. Aucun sondage concluant : on garde les dépôts de l'image, + # donc le comportement d'avant. _QEMU_ZYPPER_MIRRORS = ( "https://mirror.csclub.uwaterloo.ca/opensuse", "https://mirrors.rit.edu/opensuse", "https://mirror.us.leaseweb.net/opensuse", ) - # Miroirs Arch canadiens, du plus rapide au suivant. Mesuré depuis - # Montréal sur extra.db : quantum5 2,0 s, xenyth 7,1 s, contre 8,0 s pour - # geo.mirror.pkgbuild.com — le miroir « géographique » officiel n'est donc - # pas le meilleur ici. Arch n'est proposé qu'en amd64 dans le catalogue, + # Miroirs Arch canadiens, du plus rapide au suivant. Le miroir + # « géographique » officiel, geo.mirror.pkgbuild.com, n'est pas le plus + # rapide depuis l'Amérique du Nord : d'où une liste explicite plutôt que + # lui. Arch n'est proposé qu'en amd64 dans le catalogue, # et ces deux-là ne servent que x86_64 (Arch Linux ARM a ses propres # miroirs) : la garde d'architecture le dit quand même. _QEMU_PACMAN_MIRRORS = ( @@ -1219,13 +1218,13 @@ class QemuInstallMixin: # couvre les quatre gestionnaires (Arch l'a dans extra, Debian et Ubuntu ne # l'ont qu'en snap — coupé ici —, Fedora et openSUSE pas du tout). # - # La ligne COMMUNITY, et non le produit unifié. Mesuré dans une VM : - # « code=PCC&latest » sert maintenant pycharm-2025.3, le build unifié, qui + # La ligne COMMUNITY, et non le produit unifié. + # « code=PCC&latest » sert le build unifié, qui # s'arrête sur sa licence — son journal dit « NoValidIdeLicense » puis # « Get licenses: request requires authentication », et le projet ne # s'ouvre jamais. Aucune ouverture, donc aucun .idea, donc rien à - # configurer ensuite. Community ne demande aucun compte, et elle est - # toujours publiée et corrigée : 2025.2.6.2 date du 2026-07-29. + # configurer ensuite. Community ne demande aucun compte, et elle reste + # publiée et corrigée. # # Aucun numéro figé ici : on prend la plus récente archive # « pycharm-community- » du flux officiel des versions, pour @@ -1248,7 +1247,7 @@ class QemuInstallMixin: # INDÉPENDANTS, l'un ne se déduit pas de l'autre, et le flux updates.xml de # Google ne publie ni l'un ni l'autre. On lit donc l'URL sur la page # officielle, qui la porte en clair, et on retombe sur celle-ci si la page - # change de forme. Relevée et vérifiée (HTTP 200) le 2026-08-17. + # change de forme. Ce repli est une URL figée : elle vieillit. _QEMU_ANDROID_URL = ( "https://dl.google.com/dl/android/studio/ide-zips/2026.1.3.8/" "android-studio-quail3-patch1-linux.tar.gz" @@ -1310,6 +1309,12 @@ class QemuInstallMixin: Tout le bloc est gardé : un IDE qui ne s'installe pas ne doit pas faire échouer l'installation d'ERPLibre, qui elle a duré une heure.""" el_dir = self._qemu_install_dir(prod) + # Hors de la f-string : une expression sur deux lignes dans ses + # accolades ne parse qu'à partir de 3.12 (PEP 701). + idea_note = t( + "open the project once and close PyCharm; the .idea " + "it writes is what the install configures" + ) return ( f'echo "== {t("Installing PyCharm (long)")} =="; ' "{ " @@ -1378,8 +1383,7 @@ class QemuInstallMixin: # PyCharm n'a évidemment jamais ouvert le dépôt. + f'echo " {t("PyCharm installed:")} /opt/pycharm ' f'({t("command")} pycharm, {t("project")} {el_dir})"; ' - f'echo " {t("open the project once and close PyCharm; the .idea " - "it writes is what the install configures")}"; ' + f'echo " {idea_note}"; ' f'}} || echo " ⚠ {t("PyCharm not installed (see above)")}"; ' ) @@ -1676,8 +1680,9 @@ class QemuInstallMixin: # Aucune n'est empaquetée par une distribution : on passe donc par le site. # # L'archive dépend de la version de GNOME Shell, et ce n'est pas une - # précaution de principe : mesuré le 2026-08-17, le même point d'entrée - # sert gTile v59 pour GNOME 46, v62 pour GNOME 48 et v52 pour GNOME 3.38. + # précaution de principe : le même point d'entrée sert une archive + # différente selon la version — gTile v59 pour GNOME 46, v62 pour + # GNOME 48, v52 pour GNOME 3.38. # Une URL figée poserait donc, tôt ou tard, une archive faite pour une # autre version. # @@ -2097,8 +2102,8 @@ class QemuInstallMixin: # peut rendre 0 sans avoir rien produit. # DEUX emplacements, et il faut les deux. Avec une ABI injectée, # AGP écrit dans « intermediates/apk/debug » et non dans - # « outputs/apk/debug » : mesuré, une compilation RÉUSSIE était - # rapportée « aucun APK produit » parce que je ne regardais que le + # « outputs/apk/debug » : une compilation RÉUSSIE se rapporte + # « aucun APK produit » dès que le contrôle ne regarde que le # second. Un contrôle qui cherche au mauvais endroit ne vaut pas # mieux que pas de contrôle. f"apk=$(ls {el_dir}/mobile/erplibre_home_mobile/android/app/build"