[REF] format : passer l'outillage et les tests sous ruff

Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.

--- EN ---

This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-09-24 13:30:31 -04:00
parent e496b107a6
commit cfbdd9406e
155 changed files with 1522 additions and 1190 deletions

View file

@ -39,16 +39,12 @@ RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, RACINE)
sys.path.insert(0, os.path.join(RACINE, "long_test"))
from script.proxmox import proxmox_deploy as pve # noqa: E402
import descente # noqa: E402
from descente import ( # noqa: E402,F401
DELAIS,
mener,
_lance_une_descente,
Famille,
_lance_une_descente,
a_defaire,
alias_etage as _alias_etage,
autre_descente,
capacite_hote,
cle_publique,
@ -58,10 +54,18 @@ from descente import ( # noqa: E402,F401
detruire_etage1,
dire,
identite_de,
mener,
module_qemu,
nom_etage as _nom_etage,
retirer_alias,
)
from descente import (
alias_etage as _alias_etage,
)
from descente import (
nom_etage as _nom_etage,
)
from script.proxmox import proxmox_deploy as pve # noqa: E402
# L'image des étages imbriqués. Debian parce que install_proxmox.sh s'installe
# SUR une Debian — Proxmox ne publie pas d'image cloud.

View file

@ -42,15 +42,11 @@ RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, RACINE)
sys.path.insert(0, os.path.join(RACINE, "long_test"))
from script.proxmox import nesting # noqa: E402
from script.proxmox import proxmox_deploy as pve # noqa: E402
import descente # noqa: E402
from descente import ( # noqa: E402,F401
DELAIS,
Famille,
a_defaire,
alias_etage as _alias_etage,
autre_descente,
capacite_hote,
cle_publique,
@ -62,9 +58,17 @@ from descente import ( # noqa: E402,F401
identite_de,
mener,
module_qemu,
nom_etage as _nom_etage,
retirer_alias,
)
from descente import (
alias_etage as _alias_etage,
)
from descente import (
nom_etage as _nom_etage,
)
from script.proxmox import nesting # noqa: E402
from script.proxmox import proxmox_deploy as pve # noqa: E402
# Une Debian nue : c'est elle qui recevra libvirt et qemu-kvm.
DISTRO = "debian"

View file

@ -756,8 +756,7 @@ class Descente:
)
else:
self.dire(
f" profondeur atteinte : {atteint}"
f" / {self.plan['demandee']}"
f" profondeur atteinte : {atteint} / {self.plan['demandee']}"
)
# Deux causes très différentes rendaient le même « 5 / 10 » : la
# machine trop petite pour dix, ou un étage tombé en route. La
@ -1099,7 +1098,8 @@ def a_defaire(rapport, nom_base=""):
etages = [
e
for e in (rapport.get("etages") or [])
if identite_de(e) and e.get("parent_alias")
if identite_de(e)
and e.get("parent_alias")
# « cree » est le seul champ qui dise que la machine est à NOUS. Les
# deux autres conditions ne protégeaient que par accident : elles
# tenaient parce que rien ne décrivait un hôte emprunté. Depuis qu'une

View file

@ -396,8 +396,7 @@ def _table_block(lst_row, exact, source="database"):
)
return lines
lines = [
f" {'table':<34}{'total':>10}{'heap':>10}{'index':>10}"
f"{t('rows'):>14}"
f" {'table':<34}{'total':>10}{'heap':>10}{'index':>10}{t('rows'):>14}"
]
for row in lst_row:
count = row["exact_rows"] if exact else row["est_rows"]

View file

@ -382,7 +382,7 @@ class ProjectManagement:
os.system(cmd)
if not self.keep_bd_alive:
cmd = "./odoo_bin.sh db --drop --database" f" {bd_name_demo}"
cmd = f"./odoo_bin.sh db --drop --database {bd_name_demo}"
_logger.info(cmd)
os.system(cmd)
@ -463,7 +463,7 @@ class ProjectManagement:
os.system(cmd)
if not self.keep_bd_alive:
cmd = "./odoo_bin.sh db --drop --database" f" {bd_name_template}"
cmd = f"./odoo_bin.sh db --drop --database {bd_name_template}"
_logger.info(cmd)
os.system(cmd)
@ -531,7 +531,7 @@ class ProjectManagement:
os.system(cmd)
if not self.keep_bd_alive:
cmd = "./odoo_bin.sh db --drop --database" f" {bd_name_generator}"
cmd = f"./odoo_bin.sh db --drop --database {bd_name_generator}"
_logger.info(cmd)
os.system(cmd)

View file

@ -152,7 +152,7 @@ def search_and_replace(
# new_file_content = (
# f'{f_lines[:t_index_second_quote]}"{models_name}"{f_lines[t_index_third_quote + len(second_char):]}'
# )
new_file_content = f'{f_lines[:t_index_second_quote]}"{models_name}"{f_lines[t_index_third_quote + len(second_char):]}'
new_file_content = f'{f_lines[:t_index_second_quote]}"{models_name}"{f_lines[t_index_third_quote + len(second_char) :]}'
return new_file_content

View file

@ -116,7 +116,7 @@ def main():
)
module_name = get_module_name(config.source)
method_name = (
f'get_{"_".join(url.replace("-", "_").strip("/").split("/"))}'
f"get_{'_'.join(url.replace('-', '_').strip('/').split('/'))}"
)
# template = f"""

View file

@ -645,7 +645,9 @@ def add_line(
"with"
f" cw.indent({4 + nb_space if nb_space else ''}):"
)
with cw.indent():
with (
cw.indent()
):
cw.emit(
f'cw.emit("{line}")'
)
@ -723,7 +725,9 @@ def add_line(
f"with"
f" cw.indent():"
)
with cw.indent():
with (
cw.indent()
):
if (
no_indent
!= init_no_intend
@ -808,7 +812,9 @@ def add_line(
f"with"
f" cw.indent():"
)
with cw.indent():
with (
cw.indent()
):
if (
nb_indent
- 1
@ -902,7 +908,9 @@ def add_line(
f"with"
f" cw.indent():"
)
with cw.indent():
with (
cw.indent()
):
if (
nb_indent
- 1
@ -1007,7 +1015,9 @@ def add_line(
f"with"
f" cw.indent():"
)
with cw.indent():
with (
cw.indent()
):
if (
nb_indent
- 1
@ -1123,7 +1133,9 @@ def add_line(
f"with"
f" cw.indent():"
)
with cw.indent():
with (
cw.indent()
):
if (
nb_indent
- 1

View file

@ -19,7 +19,7 @@ Les formats en pur stdlib — CSV, JSON, XML, et la détection de macros qui
n'est qu'un `zipfile.namelist()` — tournent sous l'interpréteur du CLI.
Excel et Access exigent un venv dédié. Le module doit donc s'importer sous
les deux : AUCUN import de bibliothèque tierce au niveau du module, chacun
vit dans la fonction qui en a besoin.
vit dans la fonction qui en a besoin.
Le canal de sortie
------------------
@ -57,8 +57,7 @@ ERREURS = {
"format_inconnu": "Format not recognised: ",
"illisible_ici": "Recognised format, unreadable here"
" — re-save it as .xlsx.",
"protege": "Protected by a password, or not a workbook"
" — unreadable here.",
"protege": "Protected by a password, or not a workbook — unreadable here.",
"vide": "Empty file.",
"droits": "Not readable: check the permissions.",
"pas_un_fichier": "Not an ordinary file.",
@ -66,12 +65,11 @@ ERREURS = {
" nothing was written.",
"repertoire_non_vide": "The destination directory exists"
" and is not empty.",
"aucune_feuille": "The selection matches no sheet;"
" nothing was written.",
"aucune_feuille": "The selection matches no sheet; nothing was written.",
"tout_exclu": "Nothing was anonymised: every region was excluded.",
"rien_a_faire": "Nothing to anonymise in this file.",
"place": "Not enough room to write.",
"conversion_impossible": "This target cannot hold the source's" " shape: ",
"conversion_impossible": "This target cannot hold the source's shape: ",
"table_source": "The mapping table would overwrite the source"
" or the copy; nothing was written.",
"table_illisible": "The mapping table is unreadable,"

View file

@ -2864,8 +2864,7 @@ def _avertissements(rapport, options):
hors = rapport.get("hors_cellules") or {}
if rapport.get("format") == "xlsx":
dits.append(
"Cached formula results are dropped;"
" the sheet recomputes on open."
"Cached formula results are dropped; the sheet recomputes on open."
)
dits.append("Document properties were cleared on the copy.")
if hors.get("croises") or hors.get("graphiques"):
@ -2884,8 +2883,7 @@ def _avertissements(rapport, options):
)
if hors.get("liens_externes"):
dits.append(
"External links were dropped; formulas that used them"
" show #REF!."
"External links were dropped; formulas that used them show #REF!."
)
survivent = _noms_de_feuille_survivent(rapport, options)
if hors.get("plages_nommees") and survivent:

View file

@ -273,8 +273,7 @@ def restore_or_clone(config, arg_base, cache_database, lst_db_cache):
)
else:
_logger.info(
f"## Clone cache {cache_database} to database"
f" {config.database} ##"
f"## Clone cache {cache_database} to database {config.database} ##"
)
arg = (
f"{arg_base} --clone --from_database"

View file

@ -307,7 +307,7 @@ def main():
if not base_image_name or base_image_name == image_name_to_generate:
with_demo = dct_config_image.get("with_demo")
# Create a new one
cmd = f"./odoo_bin.sh db --create --database" f" {bd_temp_name}"
cmd = f"./odoo_bin.sh db --create --database {bd_temp_name}"
if with_demo:
cmd += " --demo"
else:

View file

@ -1,7 +1,8 @@
#!/usr/bin/env bash
import xmlrpc.client
import sys
import xmlrpc.client
import click

View file

@ -12,15 +12,14 @@ def process_zip(
):
# Ouvrir le zip d'entrée en lecture
try:
with zipfile.ZipFile(path_backup_zip, "r") as zin, zipfile.ZipFile(
path_output_zip, "w"
) as zout:
with (
zipfile.ZipFile(path_backup_zip, "r") as zin,
zipfile.ZipFile(path_output_zip, "w") as zout,
):
# Parcourir tous les fichiers du zip
for item in zin.infolist():
data = zin.read(item.filename)
if item.filename == file_to_modify:
try:
# On suppose un fichier texte en UTF-8
text = data.decode("utf-8")

View file

@ -88,7 +88,7 @@ def edit_text(config):
key = "image:"
value = lst_docker_info[i]
lst_docker_info[i] = (
f"{value[:value.find(key) + len(key)]} {config.prod_version}\n"
f"{value[: value.find(key) + len(key)]} {config.prod_version}\n"
)
break
if "ERPLibre" in docker_info:

View file

@ -101,7 +101,7 @@ class Execute:
f"gnome-terminal -- bash -c 'source"
f" ./{VENV_ERPLIBRE}/bin/activate;%s'"
)
self.cmd_source_default = "gnome-terminal -- bash -c '" f"%s'"
self.cmd_source_default = f"gnome-terminal -- bash -c '%s'"
else:
exec_path_tell = shutil.which("osascript")
if exec_path_tell:
@ -280,7 +280,7 @@ class Execute:
process.wait()
exit_code = process.returncode
if process.returncode != 0 and not quiet:
print("Command returned error code:" f" {process.returncode}")
print(f"Command returned error code: {process.returncode}")
# An exception MUST report a failure. exit_code stays None otherwise,
# and None is falsy: callers testing « if not status: » would mark the

View file

@ -17,6 +17,7 @@ le rendra.
Compté en CARACTÈRES et non en octets : « préchauffer » pèse 11 caractères et
12 octets, et une limite en octets refuserait des sujets français conformes.
"""
import re
import sys
from pathlib import Path

View file

@ -81,9 +81,7 @@ Use --production-ready for /srv/git (requires root).
"-p",
"--path",
default=None,
help=(
"Path for git server bare repos" f" (default: {DEFAULT_GIT_PATH})"
),
help=(f"Path for git server bare repos (default: {DEFAULT_GIT_PATH})"),
)
parser.add_argument(
"--production-ready",
@ -97,12 +95,12 @@ Use --production-ready for /srv/git (requires root).
"-m",
"--manifest",
default=DEFAULT_MANIFEST,
help="Manifest XML file" f" (default: {DEFAULT_MANIFEST})",
help=f"Manifest XML file (default: {DEFAULT_MANIFEST})",
)
parser.add_argument(
"--remote-name",
default=DEFAULT_REMOTE_NAME,
help="Remote name to add" f" (default: {DEFAULT_REMOTE_NAME})",
help=f"Remote name to add (default: {DEFAULT_REMOTE_NAME})",
)
parser.add_argument(
"--port",
@ -150,9 +148,7 @@ Use --production-ready for /srv/git (requires root).
"--jobs",
type=int,
default=DEFAULT_JOBS,
help=(
"Parallel jobs for init/remote/push" f" (default: {DEFAULT_JOBS})"
),
help=(f"Parallel jobs for init/remote/push (default: {DEFAULT_JOBS})"),
)
parser.add_argument(
"-v",
@ -367,8 +363,7 @@ async def _add_single_remote(
)
if rc != 0:
_logger.warning(
f" set-url failed for"
f" {project['path']}: {err.strip()}"
f" set-url failed for {project['path']}: {err.strip()}"
)
return "error"
_logger.info(f" Updated: {project['path']}")
@ -385,7 +380,7 @@ async def _add_single_remote(
)
if rc != 0:
_logger.warning(
f" add failed for" f" {project['path']}: {err.strip()}"
f" add failed for {project['path']}: {err.strip()}"
)
return "error"
_logger.info(f" Added: {project['path']}")
@ -428,7 +423,7 @@ async def add_remotes(
added = results.count("added")
updated = results.count("updated")
errors = results.count("error")
print(f"Remotes: {added} added, {updated} updated," f" {errors} errors")
print(f"Remotes: {added} added, {updated} updated, {errors} errors")
# --- Async workers for push ---
@ -568,7 +563,7 @@ async def _push_single_repo(
"receive.shallowUpdate",
"true",
)
_logger.info(f" Shallow push for" f" {project['path']}")
_logger.info(f" Shallow push for {project['path']}")
# Handle detached HEAD: checkout manifest branch
did_checkout = False
@ -611,9 +606,7 @@ async def _push_single_repo(
_, err, rc = await _run_git(*cmd, timeout=120)
if rc != 0:
_logger.warning(
f" Push failed for"
f" {project['path']}:"
f" {err.strip()}"
f" Push failed for {project['path']}: {err.strip()}"
)
return "error", did_checkout
else:
@ -656,8 +649,7 @@ async def push_to_local(
errors = sum(1 for s, _ in results if s == "error")
checkouts = sum(1 for _, c in results if c)
print(
f"Push: {pushed} pushed, {checkouts} branch"
f" checkouts, {errors} errors"
f"Push: {pushed} pushed, {checkouts} branch checkouts, {errors} errors"
)
@ -682,9 +674,7 @@ def print_clone_commands(git_path, projects, port):
clone_path = project["path"]
if clone_path == ".":
clone_path = "erplibre"
lines.append(
f" git clone {base_url}/{repo_name}" f" {clone_path}"
)
lines.append(f" git clone {base_url}/{repo_name} {clone_path}")
lines.sort()
for line in lines:
print(line)

View file

@ -419,7 +419,7 @@ class GitTool:
# '"${EL_CONFIG_FILE}"\n'
# )
str_repo = (
f' printf "{update_repo}," >> ' '"${EL_CONFIG_FILE}"\n'
f' printf "{update_repo}," >> "${{EL_CONFIG_FILE}}"\n'
)
# Ignore repo if not starting by addons
# if update_repo.startswith("addons"):

View file

@ -35,7 +35,7 @@ def get_pull_request_repo(
user_name = user["login"] if not organization_name else organization_name
status, lst_pull = gh.repos[user_name][parsed_url.repo].pulls.get()
if type(lst_pull) is dict:
print(f"For url {upstream_url}," f" got {lst_pull.get('message')}")
print(f"For url {upstream_url}, got {lst_pull.get('message')}")
return False
else:
for pull in lst_pull:
@ -106,9 +106,7 @@ def add_and_fetch_remote(
except git.NoSuchPathError:
print(f"New repo {repo_info.relative_path}")
if not root_repo:
print(
"Missing git repository to root for repo" f" {repo_info.path}"
)
print(f"Missing git repository to root for repo {repo_info.path}")
return
if branch_name:
submodule_repo = retry(

View file

@ -18,6 +18,7 @@ ce module.
Découpage voulu : TOUT ce qui construit une commande ou lit une sortie est une
fonction PURE, vérifiable sans hôte Proxmox. Seul `run()` parle au réseau.
"""
from __future__ import annotations
import ipaddress

View file

@ -1116,8 +1116,10 @@ class SeleniumLib(object):
self.driver.refresh()
if wait_ready:
WebDriverWait(self.driver, 15).until(
lambda d: d.execute_script("return document.readyState")
== "complete"
lambda d: (
d.execute_script("return document.readyState")
== "complete"
)
)
def check_bot_chat_and_close(self):
@ -1824,10 +1826,13 @@ class SeleniumLib(object):
)
)
WebDriverWait(driver, 10).until(
lambda d: picker.find_element(
By.CSS_SELECTOR, ".o_datetime_picker_header .o_header_part"
).text
!= header_txt
lambda d: (
picker.find_element(
By.CSS_SELECTOR,
".o_datetime_picker_header .o_header_part",
).text
!= header_txt
)
)
else:
raise RuntimeError(

View file

@ -556,7 +556,7 @@ async def test_exec(
new_hook_line = (
hook_line[: first_index + len(f_key)]
+ f'"{s_first_path}"\n'
+ f'{nb_space_indentation * " "}template_dir ='
+ f"{nb_space_indentation * ' '}template_dir ="
f' "{s_first_path}/" + MODULE_NAME\n\n'
+ hook_line[index_end_string:]
)

View file

@ -29,6 +29,7 @@ l'identique : un seul client `openai` les couvre toutes, pointé sur ce que
`servers.base_url()` rend. C'est aussi ce client qu'un test injecte pour
parler à un vrai serveur de boucle locale plutôt qu'à un double.
"""
from __future__ import annotations
import json

View file

@ -33,6 +33,7 @@ d'où une échelle qui teste `is_loopback` d'abord, `is_private` ensuite. Et
ne résout pas se lit comme `global`, la lecture pessimiste, jamais comme
satisfait.
"""
from __future__ import annotations
import ipaddress

View file

@ -25,6 +25,7 @@ Les valeurs de `COMMANDS` SONT les clés i18n : `t()` rend une clé absente
inchangée, donc une commande non traduite s'affiche en anglais correct. La
traduction se fait à l'affichage, dans le menu.
"""
from __future__ import annotations
import re

View file

@ -35,6 +35,7 @@ deux dépôts voisins s'y confondent.
machine se compte en dizaines de mégaoctets ; seules les premières lignes sont
lues, et le listage se garde de les relire à chaque affichage.
"""
from __future__ import annotations
import json

View file

@ -35,6 +35,7 @@ d'ordinaire. La moitié « noms » du filtre est donc inerte par défaut, et
`gate` le dit au lieu de laisser croire à un contrôle complet : une
destination tierce est REFUSÉE tant que cette liste est vide.
"""
from __future__ import annotations
import os

View file

@ -41,6 +41,7 @@ D'où l'ordre des passes : une passe de CONNEXION SEULE sur tout le réseau
d'abord, puis le budget coûteux des GET de reconnaissance dépensé sur la
poignée d'hôtes qui ont accepté. `sweep` fait la première, et rien d'autre.
"""
from __future__ import annotations
import ipaddress
@ -50,9 +51,8 @@ import shlex
import socket
import subprocess
import time
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import TimeoutError as PoolTimeout
from concurrent.futures import as_completed
from dataclasses import dataclass
from script.todo.assistant import fingerprint

View file

@ -31,6 +31,7 @@ un 503 « starting » ou « Loading model » est vivant et identifié, un 401 es
un accord de reconnaissance et jamais une invitation à saisir une clé, et un
corps tronqué vaut ce qui en est arrivé.
"""
from __future__ import annotations
import functools

View file

@ -38,6 +38,7 @@ d'où deux restrictions sur elle : `hosting` y est forcé à `loopback`, et un
gpt qui y déclarerait une COMMANDE est refusé. Un fichier qu'on n'a pas relu
n'est pas une donnée, c'est de la configuration exécutable.
"""
from __future__ import annotations
import os

View file

@ -26,6 +26,7 @@ dernier contact, ni rapport de balayage, ni résultat négatif. La liste de qui
a répondu parmi les 254 adresses d'un /24 décrit des machines que personne
n'a désignées, là où un serveur retenu en désigne une seule, volontairement.
"""
from __future__ import annotations
from dataclasses import dataclass, replace

View file

@ -235,8 +235,7 @@ class DatabaseManager:
)
commande = (
f"python3 ./script/database/db_duplicate.py"
f" -s {source} -d {cible}"
f"python3 ./script/database/db_duplicate.py -s {source} -d {cible}"
)
if neutraliser:
commande += " --neutralize"
@ -392,8 +391,7 @@ class DatabaseManager:
with zipfile.ZipFile(default_output_path, "r") as zip_ref:
manifest_file_1 = zip_ref.open("manifest.json")
_logger.info(
f"Log file '{default_output_path}' is complete"
" and validated."
f"Log file '{default_output_path}' is complete and validated."
)
except Exception as e:
_logger.error(e)

View file

@ -10,6 +10,7 @@ de demander l'information, mais doivent appeler exactement le même code une
fois qu'ils l'ont : sinon les deux copies dérivent. Ce module ne connaît ni
`input`, ni Textual, ni aucune bibliothèque d'interface.
"""
from __future__ import annotations
import os

View file

@ -12,6 +12,7 @@ Les préréglages `gmail`, `outlook` et `icloud` supposent un MOT DE PASSE
D'APPLICATION : l'authentification simple ne passe plus autrement chez ces
fournisseurs. C'est la limite assumée de la phase 1 ; la phase 2 apporte OAuth.
"""
from __future__ import annotations
import json

View file

@ -16,6 +16,7 @@ Cette fonction a été réinventée quatre fois dans ce paquet
un charset non fiable ne doit jamais faire tomber l'affichage ou la
synchronisation d'un message entier.
"""
from __future__ import annotations

View file

@ -11,6 +11,7 @@ que de rendre du charabia.
clair b"P0" + donnees
chiffre b"E1" + nonce(12) + AES-256-GCM(chiffre || tag)
"""
from __future__ import annotations
import os

View file

@ -15,6 +15,7 @@ plus rien dire, et garder l'ancien cache produirait des messages faux.
Les corps ne descendent JAMAIS pendant une passe : une boîte de 20 000
messages doit se synchroniser en secondes, pas en gigaoctets.
"""
from __future__ import annotations
import logging

View file

@ -12,6 +12,7 @@ sur un cas limite ou l'autre.
`BODY.PEEK` et non `BODY` : lire un message dans le TUI ne doit pas le marquer
lu sur le serveur à l'insu de l'utilisateur.
"""
from __future__ import annotations
import email

View file

@ -7,6 +7,7 @@ Ce module est le SEUL point de contact entre le paquet `mail` et le CLI :
`todo.py` importe `prompt_execute_mail` et rien d'autre. Le sens de la
dépendance est volontaire — `mail` ne doit jamais importer `todo`.
"""
from __future__ import annotations
import getpass

View file

@ -16,6 +16,7 @@ Référence de secret : "<coffre>:<chemin>"
kdbx:ERPLibre/Mail/perso/cache-key -> ... entrée cache-key
keyring:perso -> service "erplibre-mail", user perso
"""
from __future__ import annotations
import logging

View file

@ -11,6 +11,7 @@ d'être caché tout en restant destinataire d'enveloppe.
`date` et `msgid` sont injectables pour que les tests soient déterministes ;
en production on laisse la stdlib les produire.
"""
from __future__ import annotations
import mimetypes

View file

@ -13,6 +13,7 @@ des personnes — expéditeur, destinataires, sujet, extrait, Message-ID — est
scellé. Le Message-ID a en plus un haché salé par la clé, pour qu'on puisse
recoller les fils de discussion sans le lire.
"""
from __future__ import annotations
import base64

View file

@ -11,6 +11,7 @@ Une session = un compte ouvert. Elle survit à une panne réseau : le cache
s'ouvre d'abord, la connexion est tentée ensuite, et son échec ne fait que
poser un drapeau `online = False`. Une boîte hors ligne reste lisible.
"""
from __future__ import annotations
import logging
@ -549,10 +550,9 @@ def deliver(session, msg, send_fn=None, connect_fn=None) -> str:
n'annule rien. Le message est déjà parti ; le signaler comme un échec
pousserait l'utilisateur à l'envoyer deux fois.
"""
from script.todo.mail.smtp_send import SmtpError
from script.todo.mail.smtp_send import SmtpError, without_bcc
from script.todo.mail.smtp_send import connect as smtp_connect
from script.todo.mail.smtp_send import send as smtp_send_fn
from script.todo.mail.smtp_send import without_bcc
if not session.online:
raise SmtpError(t("mail_offline_cannot_send"))
@ -706,9 +706,8 @@ def run_tui(
return
from script.todo import todo_prefs
from script.todo.mail import account_setup
from script.todo.mail import account_setup, tui_text
from script.todo.mail import accounts as mail_accounts
from script.todo.mail import tui_text
from script.todo.mail.accounts import PRESETS
from script.todo.mail.secrets import SecretStore

View file

@ -12,6 +12,7 @@ charset menteur, pièce jointe sans nom. Aucune de ces fonctions ne lève ; au
pire elles rendent une chaîne vide. Un message illisible doit s'afficher mal,
pas faire tomber la boîte de réception.
"""
from __future__ import annotations
import datetime

View file

@ -15,6 +15,7 @@ en progression. La décision est donc écrite une seule fois.
`ctx` vient de `TodoUpgrade.resume_context()` : pure donnée, aucun accès à la
base ni au disque depuis l'affichage.
"""
from __future__ import annotations
try:

View file

@ -13,6 +13,7 @@ autre session sans risquer de la perturber.
réimplémenter une seconde fois la lecture des clés « state_* », qui
divergerait de l'écran de reprise.
"""
from __future__ import annotations
import datetime

View file

@ -142,7 +142,7 @@ def pane_text(dct, row, colour=False, show_log=True):
if scan["errors"]:
lignes.append("")
lignes.append(
f"── {t('errors in the log')} :" f" {status.severe_count(scan)} ──"
f"── {t('errors in the log')} : {status.severe_count(scan)} ──"
)
for item in scan["errors"][:15]:
lignes.append(

View file

@ -23,6 +23,7 @@ Le formulaire ne touche à rien : il rend une spec. C'est l'appelant
import os
import re
from script.todo.deploy_form_extras import ExtrasMixin
from script.todo.deploy_form_lib import (
CSS_BASE,
FREE,
@ -40,7 +41,6 @@ from script.todo.deploy_form_lib import (
res_row_widgets,
t,
)
from script.todo.deploy_form_extras import ExtrasMixin
from script.todo.deploy_form_plan import PlanMixin, preview_screen
# Aucun disque orphelin à craindre : les disques d'un Proxmox distant vivent
@ -833,9 +833,9 @@ def run_proxmox_form(ctx, run_app: bool = True):
self.profile = choix
sur_mesure = choix == "custom"
for champ in RES_FIELDS:
self.query_one(RES_FIELDS[champ][0], Select).disabled = (
not sur_mesure
)
self.query_one(
RES_FIELDS[champ][0], Select
).disabled = not sur_mesure
if not sur_mesure:
self._show_free(champ, False)
# Un réglage commun reprend la main sur les VM non figées :

View file

@ -1824,8 +1824,7 @@ class ProxmoxMenuMixin:
return False
if fini.returncode:
print(
f" ⚠ {t('download cache bypass not set')}"
f" ({fini.returncode})"
f" ⚠ {t('download cache bypass not set')} ({fini.returncode})"
)
return False
self._pve_note(
@ -1889,8 +1888,7 @@ class ProxmoxMenuMixin:
# certificate in certificate chain » — plus tard, dans la VM,
# loin d'ici. C'est pourquoi il est nommé.
cle = (
"no trust store for this distribution, its downloads "
"will fail"
"no trust store for this distribution, its downloads will fail"
)
self._pve_note(vm, f" ⚠ {t(cle)} : {vm.get('distro') or '?'}")
return False
@ -2009,8 +2007,7 @@ class ProxmoxMenuMixin:
groupes = list(getattr(mod, "GPU_GROUPS", ()) or ("render", "video"))
gestes = [f"sudo groupadd -f {g}" for g in groupes]
gestes.append(
f"sudo usermod -aG {','.join(groupes)}"
f" {shlex.quote(utilisateur)}"
f"sudo usermod -aG {','.join(groupes)} {shlex.quote(utilisateur)}"
)
code, _o = self._pve_ssh(cible, " && ".join(gestes), timeout=60)
if code:

View file

@ -18,6 +18,7 @@ Le formulaire ne lance AUCUNE commande privilégiée ni réseau : tout appel
Textual. Les données coûteuses (domaines existants, branches distantes) sont
préchargées par l'appelant et arrivent dans `ctx`.
"""
from __future__ import annotations
import os

View file

@ -25,6 +25,7 @@ Deux pièges, appris sur l'hôte :
- « --add-device --graphics type=egl-headless » n'est PAS idempotent : appelé
deux fois, il pose deux affichages. D'où la lecture de l'état AVANT le plan.
"""
from __future__ import annotations
import xml.etree.ElementTree as ET

View file

@ -11,6 +11,7 @@ quitter le dashboard n'arrête rien, on peut le rouvrir pour ré-attacher.
- launch_installs(...) : lance les process détachés + écrit un manifeste JSON.
- run_monitor(manifest_path) : ouvre le dashboard Textual sur un manifeste.
"""
from __future__ import annotations
import asyncio
@ -3282,9 +3283,7 @@ def run_monitor(manifest_path: str, run_app: bool = True):
return
with self.suspend():
print(f"\n=== {title} — {vm['name']} ===")
os.system(
f"{vm_ssh_prefix(vm)} " f"{shlex.quote(cmd)} || true"
)
os.system(f"{vm_ssh_prefix(vm)} {shlex.quote(cmd)} || true")
input("\nEntrée pour revenir au suivi… ")
def _run_update(self, vm, parts) -> None:

View file

@ -16,13 +16,14 @@ from script.todo import ssh_config, todo_install
from script.todo.qemu_cache_menu import bypass_menage
from script.todo.qemu_privilege import (
LIBVIRT_URI as URI,
)
from script.todo.qemu_privilege import (
sudo_prefix,
system_path,
virsh_argv,
)
from script.todo.todo_i18n import t
# Les fichiers d'état de dnsmasq, un par réseau libvirt. Sur une installation
# standard ils sont en 0644 dans un répertoire en 0755, donc lisibles sans
# privilège — le « .conf » posé à côté est en 0600, et c'est lui qui donne
@ -1682,10 +1683,7 @@ class QemuManageMixin:
# Arrêt gracieux trop long : proposer un arrêt forcé.
if self._is_yes(
input(
t(
"Graceful shutdown timed out. Force off (destroy)? "
"(y/N): "
)
t("Graceful shutdown timed out. Force off (destroy)? (y/N): ")
)
):
cmd = (
@ -2029,10 +2027,7 @@ class QemuManageMixin:
print(f"⚠ {t('Not enough free space for a full backup.')}")
return self._is_yes(
input(
t(
"Back up anyway, at the risk of filling the disk?"
" (y/N): "
)
t("Back up anyway, at the risk of filling the disk? (y/N): ")
)
)
@ -2684,9 +2679,7 @@ class QemuManageMixin:
if proteges:
print(f"\n{t('Kept (still attached to a VM):')}")
for size, path, porteur in sorted(proteges, key=lambda o: -o[0]):
print(
f" {self._human_size(size):>9} {path}" f" ← {porteur}"
)
print(f" {self._human_size(size):>9} {path} ← {porteur}")
if orphans:
total = sum(o[0] for o in orphans)
print(f"\n{t('Orphan files:')}")
@ -3171,9 +3164,8 @@ class QemuManageMixin:
`timeout` : délai max PAR VM (borne l'attente d'une VM sans IP). Un
BATTEMENT toutes les 30 s liste les VM encore en attente -> jamais de
silence prolongé qui donne l'impression d'un blocage."""
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import TimeoutError as _FTimeout
from concurrent.futures import as_completed
labels = labels or {}
print(

View file

@ -16,6 +16,7 @@ ligne, rien de plus. Un alias déclaré dans un fichier inclus reste donc
invisible aux trois lecteurs, alors même que `ssh -G` le résoudrait — la
source est incomplète sans être fausse.
"""
from __future__ import annotations
import re

View file

@ -14,6 +14,7 @@ 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

View file

@ -13,6 +13,7 @@ télémétrie de navigation) et non dans un fichier versionné.
Tout est best-effort : une préférence illisible ou un disque plein ne doivent
JAMAIS empêcher le CLI de démarrer.
"""
from __future__ import annotations
import json

View file

@ -10,6 +10,7 @@ DIAGRAMME arborescent des fonctionnalités dans un TUI Textual, trié par usage.
dédupe les ré-affichages consécutifs pour ne compter que les TRANSITIONS.
- run_tui() : ouvre l'arbre de navigation (compteurs par menu).
"""
from __future__ import annotations
import ast

View file

@ -13,7 +13,6 @@ import sys
import zipfile
from uuid import uuid4
from script.todo import (
auto_ask,
migration_status,
@ -1196,8 +1195,8 @@ class TodoUpgrade:
target_addons_path = dct_module.get("target_addons_path")
source_addons_path = dct_module.get("source_addons_path")
module_name = dct_module.get("module_name")
source_version_odoo = f'{dct_module.get("source_version_odoo")}.0'
target_version_odoo = f'{dct_module.get("target_version_odoo")}.0'
source_version_odoo = f"{dct_module.get('source_version_odoo')}.0"
target_version_odoo = f"{dct_module.get('target_version_odoo')}.0"
source_module_path_to_copy = dct_module.get("source_module_path")
# Prepare git environment for target
if target_addons_path not in lst_path_git_clone_migrate:
@ -1512,7 +1511,7 @@ class TodoUpgrade:
for next_version in range_version:
print(
f"https://oca.github.io/OpenUpgrade/coverage_analysis/modules{next_version*10}-{(next_version+1)*10}.html"
f"https://oca.github.io/OpenUpgrade/coverage_analysis/modules{next_version * 10}-{(next_version + 1) * 10}.html"
)
# ⚠️ ℹ 💬 ❗ 🔷 ✨ 🟦 🔹 🔵 ⟳ ⧖ ⚙ ✔ ✅ ❌ ⏵ ⏸ ⏹ ◆ ◇ … ➤ ⚑ ★ ☆ ☰ ⬍ ⍟ ⊗ ⌘ ⏻ ⍰
@ -2041,8 +2040,7 @@ class TodoUpgrade:
print(f"✅ -> {t('Clone done for Odoo')}{next_version}")
else:
print(
f"✅ -> {t('Clone already done for Odoo')}"
f"{next_version}"
f"✅ -> {t('Clone already done for Odoo')}{next_version}"
)
option_comment += 1
@ -2144,8 +2142,7 @@ class TodoUpgrade:
)
else:
print(
f"✅ -> {t('Switch already done for Odoo')}"
f"{next_version}"
f"✅ -> {t('Switch already done for Odoo')}{next_version}"
)
lst_state_4_module_migrate_code = self.dct_progression.get(
@ -2526,8 +2523,7 @@ class TodoUpgrade:
self.write_config()
print(
f"✅ -> {t('Module upgrade done for Odoo')}"
f"{next_version}"
f"✅ -> {t('Module upgrade done for Odoo')}{next_version}"
)
else:
print(
@ -4047,8 +4043,7 @@ class TodoUpgrade:
)
if lst_detail:
print(
f"✨ {t('Modules to uninstall before Odoo')}"
f"{next_version} :"
f"✨ {t('Modules to uninstall before Odoo')}{next_version} :"
)
self.print_uninstall_reason(lst_detail)
return list(dict.fromkeys(list(extra) + lst_file))
@ -4340,9 +4335,7 @@ class TodoUpgrade:
# Clone a project for next version
# Get actual branch
cmd_git_clone_migrate_source = (
f"cd {source_addons_path} && "
f"git branch --show-current && "
f"cd ~-"
f"cd {source_addons_path} && git branch --show-current && cd ~-"
)
status, cmd_executed, lst_output = self.todo_upgrade_execute(
cmd_git_clone_migrate_source,
@ -4408,9 +4401,7 @@ class TodoUpgrade:
# TODO check config if path is added
# Get remote branch address
cmd_remote_address = (
f"cd {source_addons_path} "
f"&& git remote get-url {remote} "
f"&& cd ~-"
f"cd {source_addons_path} && git remote get-url {remote} && cd ~-"
)
status, cmd_executed, lst_output = self.todo_upgrade_execute(
cmd_remote_address,

View file

@ -25,6 +25,7 @@ attend `{}` supprimerait le repli textuel en silence.
partir du JSON de `--report` déjà en main : aucune entrée-sortie et aucun
sous-processus depuis l'affichage.
"""
from __future__ import annotations
try:
@ -256,9 +257,9 @@ def run_transform_form(ctx, run_app: bool = True):
# TOUS les imports textual ICI : le CLI importe ce module pour ses
# fonctions pures, et les libellés des BINDINGS ne doivent être
# évalués qu'à l'appel, après le choix de la langue.
from rich.text import Text
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from rich.text import Text
from textual.widgets import DataTable, Footer, Header, OptionList, Static
from textual.widgets.option_list import Option

View file

@ -473,8 +473,7 @@ class TransformMenuMixin:
)
if trouve is None:
manque = t(
"The selection matches no sheet;"
" nothing was written."
"The selection matches no sheet; nothing was written."
)
print(f"❌ {manque} {demandee}")
return None
@ -685,8 +684,7 @@ class TransformMenuMixin:
)
if apercu.get("hors_portee"):
print(
f" {apercu['hors_portee']}"
f" {t('cell(s) left out of scope')}"
f" {apercu['hors_portee']} {t('cell(s) left out of scope')}"
)
ecartees = apercu.get("colonnes_ecartees") or []
if ecartees:
@ -788,8 +786,7 @@ class TransformMenuMixin:
)
)
avis = t(
"The file name is not anonymised;"
" the default does not reuse it."
"The file name is not anonymised; the default does not reuse it."
)
print(f" ⚠ {avis}")
reponse = self._transform_ask(
@ -870,10 +867,7 @@ class TransformMenuMixin:
print(f" {cible}")
attendu = os.path.basename(existants[0])
reponse = input(
t(
"This file already exists. Type its name in full to"
" overwrite: "
)
t("This file already exists. Type its name in full to overwrite: ")
).strip()
if reponse != attendu:
print(f"❌ {t('Name does not match, nothing was written.')}")
@ -997,8 +991,7 @@ class TransformMenuMixin:
print(f" {t('Left intact')} : {detail}")
if bilan.get("hors_portee"):
print(
f" {bilan['hors_portee']}"
f" {t('cell(s) left out of scope')}"
f" {bilan['hors_portee']} {t('cell(s) left out of scope')}"
)
hors = bilan.get("hors_cellules") or {}
# `isinstance(True, int)` vaut True : sans exclure les booléens, le

View file

@ -74,7 +74,9 @@ def set_version_installed(
) -> None:
"""Record that an Odoo version has been installed (or reinstalled)."""
state = read_state()
entry = state["odoo_versions"].get(odoo_version, _deep_copy(_EMPTY_VERSION_ENTRY))
entry = state["odoo_versions"].get(
odoo_version, _deep_copy(_EMPTY_VERSION_ENTRY)
)
entry["installed"] = True
entry["extra"] = extra
if python:
@ -94,7 +96,9 @@ def set_version_installed(
def set_version_switched(odoo_version: str) -> None:
"""Record that the workspace was switched to an Odoo version."""
state = read_state()
entry = state["odoo_versions"].get(odoo_version, _deep_copy(_EMPTY_VERSION_ENTRY))
entry = state["odoo_versions"].get(
odoo_version, _deep_copy(_EMPTY_VERSION_ENTRY)
)
entry["switched_at"] = str(date.today())
state["odoo_versions"][odoo_version] = entry
state["current_odoo_version"] = odoo_version

View file

@ -32,6 +32,7 @@ fichiers vus dans le parc déclarent
`xmlns="http://schemas.xmlsoap.org/encoding/"`, et un site peut en
distribuer un sans. Les balises sont donc cherchées sur leur nom local.
"""
from __future__ import annotations
import re

View file

@ -24,6 +24,7 @@ Où vivent les fichiers, pour tous les pilotes :
sans sudo : `status` en a besoin, et il
tourne dans un autre processus que `up`.
"""
from __future__ import annotations
import ipaddress

View file

@ -37,6 +37,7 @@ Où vivent les fichiers, et pourquoi :
/etc/ipsec.conf, /etc/ipsec.secrets un bloc marqué, retiré au « down ».
/etc/strongswan.d/erplibre-vpn.conf le réglage install_routes.
"""
from __future__ import annotations
import os

View file

@ -109,6 +109,7 @@ toutes deux à un moment où l'humain peut agir : le menu l'annonce avant la
saisie du secret, et le montage compare les longueurs si ce qui est déposé
la dépasse. Zéro = aucune limite.
"""
from __future__ import annotations
import json

View file

@ -26,6 +26,7 @@ Le tunnel scindé se demande à OpenVPN par `--route-nopull` : ignorer les
routes poussées, puis poser les nôtres. C'est un gros marteau — il ignore
aussi le DNS poussé — et le pilote le dit quand il le prend.
"""
from __future__ import annotations
import os

View file

@ -21,6 +21,7 @@ Deux différences qui changent le code, et pas seulement les commandes :
ouvrir la session SSH par root, avec les clés de root — c'est-à-dire aucune.
D'où `sudo=False`, et un fichier de pid dans le home plutôt que dans /run.
"""
from __future__ import annotations
import os

View file

@ -17,6 +17,7 @@ compris, en « tout le trafic », l'astuce de marquage (fwmark) qui garde
l'endpoint joignable. On ne double donc PAS son travail : un `ip route` de
plus ici entrerait en conflit avec le sien.
"""
from __future__ import annotations
import shlex

View file

@ -35,6 +35,7 @@ une liste d'erreurs que l'appelant AFFICHE. Un préréglage fautif rendrait
autrement tous les autres inatteignables, et la panne se lirait « aucun
préréglage » alors qu'il y en a dix.
"""
from __future__ import annotations
import json

View file

@ -19,6 +19,7 @@ Toute valeur est VALIDÉE avant d'être écrite : elle finira dans un fichier de
configuration et dans une ligne de commande lancée par sudo. Un nom d'hôte
avec une espace ou un point-virgule n'y arrivera pas.
"""
from __future__ import annotations
import json

View file

@ -20,6 +20,7 @@ utilisateur de la machine, `/proc/<pid>/environ` par le seul propriétaire du
processus. Un mot de passe en argument est visible de tous pendant toute la
durée de la commande.
"""
from __future__ import annotations
import shlex

View file

@ -14,6 +14,7 @@ refusé ICI, pas découvert par `sh`.
Chaque fonction NORMALISE en place (`profile[key]` reçoit la valeur propre)
et lève `ProfileError` avec un message destiné à l'humain.
"""
from __future__ import annotations
import ipaddress

View file

@ -22,6 +22,7 @@ tout seul : `ensure_vault` demande, et une réponse vide fait renoncer. Un
outil qui crée silencieusement un fichier de mots de passe dans un répertoire
qu'on n'a pas choisi est un outil qu'on n'ose plus lancer.
"""
from __future__ import annotations
import getpass

View file

@ -19,6 +19,7 @@ de l'utilisateur et son mot de passe maître est saisi par lui. Chaque étape
privilégiée appelle `sudo` séparément, et `--dry-run` les montre toutes sans
en exécuter aucune.
"""
from __future__ import annotations
import argparse

View file

@ -26,6 +26,7 @@ Les valeurs y sont inventées — versions, noms de modèles, empreintes de
compilation. Un relevé pris sur une machine réelle figerait dans le dépôt le
nom d'un modèle et d'un hôte que personne n'a choisi d'y mettre.
"""
from __future__ import annotations
import http.server
@ -163,8 +164,7 @@ FIXTURES: dict[str, dict[str, tuple[int, bytes]]] = {
"open_webui": {
"/api/config": (
200,
b'{"name":"Open WebUI","version":"0.0.0",'
b'"deployment_id":"0000"}',
b'{"name":"Open WebUI","version":"0.0.0","deployment_id":"0000"}',
),
"/api/version": (200, b'{"version":"0.0.0"}'),
},

View file

@ -40,6 +40,7 @@ Rien ici ne sort de la machine : on se lie à 127.0.0.1 sur le port 0 (l'OS
choisit), jamais sur un port fixe qui entrerait en collision avec ce qui
écoute déjà. Aucun trousseau, aucun `~/.erplibre`, aucun identifiant réel.
"""
from __future__ import annotations
import atexit

View file

@ -25,6 +25,7 @@ lisent aussi les tests de transport : deux tables se seraient contredites sans
que rien ne le montre. `/api/show` n'y est pas et se définit ici, parce que la
découverte n'émet que des GET et qu'elle n'a donc jamais eu à le connaître.
"""
import ipaddress
import os
import sys

View file

@ -27,6 +27,7 @@ Aucun test ne lance `claude`, ne lit le registre de la machine, ni n'ouvre une
transcription réelle : le lanceur, le registre, l'état des processus et la
lecture d'en-tête sont tous injectés.
"""
from __future__ import annotations
import json
@ -160,7 +161,7 @@ class LeListageDesVivantes(unittest.TestCase):
def _live(self, agents=AGENTS, registre=None, demarrage=DEMARRAGE):
return CS.live(
run=lambda argv: agents,
read_registry=lambda: (REGISTRE if registre is None else registre),
read_registry=lambda: REGISTRE if registre is None else registre,
read_stat=lambda pid: stat_avec(demarrage),
)

View file

@ -22,6 +22,7 @@ valeurs identifiantes des cas sont INVENTÉES — une règle qui interdit de
nommer ne se cite pas elle-même en clair, et un test fige pour toujours ce
qu'il porte.
"""
from __future__ import annotations
import os

View file

@ -25,6 +25,7 @@ Rien ici n'ouvre de socket sortante, ne lance de vrai `claude`, ni ne lit la
configuration de la machine : le serveur est lié à la boucle locale sur un
port choisi par le système, et le lanceur de sous-processus est injecté.
"""
import json
import os
import sys

View file

@ -21,6 +21,7 @@ l'ordre d'écriture : un chargeur se juge sur ce qu'il refuse.
Aucun test ne lit le disque de la machine : les racines sont des répertoires
temporaires, et le répertoire personnel est détourné là où il compte.
"""
from __future__ import annotations
import os

View file

@ -18,6 +18,7 @@ vérifiée ici même — le paquet, lui, doit rester importable seul.
`_menu_header()` enregistre une télémétrie dans `~/.erplibre` : tout test qui
appelle une méthode de menu la neutralise, sinon il écrit pour de vrai.
"""
from __future__ import annotations
import ast
@ -97,10 +98,11 @@ class Cablage(unittest.TestCase):
from script.todo.todo import TODO
todo = TODO()
with patch.object(TODO, "prompt_assistant_llm") as mock_llm, patch(
"script.todo.mail.menu.prompt_execute_mail"
) as mock_mail, patch("click.prompt", side_effect=["1", "0"]), patch(
"script.todo.todo_telemetry.record"
with (
patch.object(TODO, "prompt_assistant_llm") as mock_llm,
patch("script.todo.mail.menu.prompt_execute_mail") as mock_mail,
patch("click.prompt", side_effect=["1", "0"]),
patch("script.todo.todo_telemetry.record"),
):
todo.prompt_assistant()
mock_llm.assert_called_once_with()
@ -110,10 +112,11 @@ class Cablage(unittest.TestCase):
from script.todo.todo import TODO
todo = TODO()
with patch.object(TODO, "prompt_assistant_llm") as mock_llm, patch(
"script.todo.mail.menu.prompt_execute_mail"
) as mock_mail, patch("click.prompt", side_effect=["2", "0"]), patch(
"script.todo.todo_telemetry.record"
with (
patch.object(TODO, "prompt_assistant_llm") as mock_llm,
patch("script.todo.mail.menu.prompt_execute_mail") as mock_mail,
patch("click.prompt", side_effect=["2", "0"]),
patch("script.todo.todo_telemetry.record"),
):
todo.prompt_assistant()
mock_mail.assert_called_once()
@ -128,12 +131,13 @@ class Cablage(unittest.TestCase):
from script.todo.todo import TODO
todo = TODO()
with patch(
"script.todo.assistant.fingerprint.collect", return_value={}
), patch("script.todo.assistant.servers.load", return_value=[]), patch(
"click.prompt", side_effect=["0"]
), patch(
"script.todo.todo_telemetry.record"
with (
patch(
"script.todo.assistant.fingerprint.collect", return_value={}
),
patch("script.todo.assistant.servers.load", return_value=[]),
patch("click.prompt", side_effect=["0"]),
patch("script.todo.todo_telemetry.record"),
):
todo.prompt_assistant_llm()
@ -244,16 +248,20 @@ class Balayage(unittest.TestCase):
todo = self._todo()
vus = {}
with patch.object(
llm_disc, "local_networks", return_value=[]
), patch.object(llm_disc, "run_ip", return_value=""), patch.object(
todo, "_qemu_host_addresses", staticmethod(lambda: set())
), patch.object(
todo,
"_llm_probe_and_keep",
lambda adresses, **kw: vus.update({"n": len(adresses), "kw": kw}),
), patch(
"click.prompt", side_effect=["198.51.100.0/24", "o"]
with (
patch.object(llm_disc, "local_networks", return_value=[]),
patch.object(llm_disc, "run_ip", return_value=""),
patch.object(
todo, "_qemu_host_addresses", staticmethod(lambda: set())
),
patch.object(
todo,
"_llm_probe_and_keep",
lambda adresses, **kw: vus.update(
{"n": len(adresses), "kw": kw}
),
),
patch("click.prompt", side_effect=["198.51.100.0/24", "o"]),
):
todo._llm_search_cidr()
self.assertEqual(vus.get("n"), 254)
@ -269,16 +277,19 @@ class Balayage(unittest.TestCase):
todo = self._todo()
vus = {}
voisinage = "198.51.100.1 dev lien0 lladdr aa:bb:cc:dd:ee:01 REACHABLE"
with patch.object(
llm_disc, "run_ip", return_value=voisinage
), patch.object(
todo, "_qemu_host_addresses", staticmethod(lambda: set())
), patch.object(
todo,
"_llm_probe_and_keep",
lambda adresses, **kw: vus.update({"n": len(adresses), "kw": kw}),
), patch(
"click.prompt", side_effect=["o"]
with (
patch.object(llm_disc, "run_ip", return_value=voisinage),
patch.object(
todo, "_qemu_host_addresses", staticmethod(lambda: set())
),
patch.object(
todo,
"_llm_probe_and_keep",
lambda adresses, **kw: vus.update(
{"n": len(adresses), "kw": kw}
),
),
patch("click.prompt", side_effect=["o"]),
):
todo._llm_sweep_cidr("198.51.100.0/24")
self.assertEqual(vus.get("n"), 254)
@ -290,16 +301,19 @@ class Balayage(unittest.TestCase):
todo = self._todo()
vus = {}
voisinage = "198.51.100.1 dev lien0 lladdr aa:bb:cc:dd:ee:01 REACHABLE"
with patch.object(
llm_disc, "run_ip", return_value=voisinage
), patch.object(
todo, "_qemu_host_addresses", staticmethod(lambda: set())
), patch.object(
todo,
"_llm_probe_and_keep",
lambda adresses, **kw: vus.update({"n": len(adresses), "kw": kw}),
), patch(
"click.prompt", side_effect=["v"]
with (
patch.object(llm_disc, "run_ip", return_value=voisinage),
patch.object(
todo, "_qemu_host_addresses", staticmethod(lambda: set())
),
patch.object(
todo,
"_llm_probe_and_keep",
lambda adresses, **kw: vus.update(
{"n": len(adresses), "kw": kw}
),
),
patch("click.prompt", side_effect=["v"]),
):
todo._llm_sweep_cidr("198.51.100.0/24")
self.assertEqual(vus.get("n"), 1)
@ -315,8 +329,9 @@ class Balayage(unittest.TestCase):
todo = self._todo()
sortie = io.StringIO()
with patch.object(llm_disc, "sweep", return_value=[]), redirect_stdout(
sortie
with (
patch.object(llm_disc, "sweep", return_value=[]),
redirect_stdout(sortie),
):
todo._llm_probe_and_keep(
["198.51.100.1"], cible="198.51.100.0/24", restreint=True
@ -332,8 +347,9 @@ class Balayage(unittest.TestCase):
todo = self._todo()
sortie = io.StringIO()
with patch.object(llm_disc, "sweep", return_value=[]), redirect_stdout(
sortie
with (
patch.object(llm_disc, "sweep", return_value=[]),
redirect_stdout(sortie),
):
todo._llm_probe_and_keep(["198.51.100.1"], cible="198.51.100.0/24")
self.assertNotIn(
@ -345,12 +361,15 @@ class Balayage(unittest.TestCase):
todo = self._todo()
appels = []
with patch.object(
todo, "_qemu_host_addresses", staticmethod(lambda: set())
), patch.object(llm_disc, "run_ip", return_value=""), patch.object(
todo, "_llm_probe_and_keep", lambda *a, **k: appels.append(a)
), patch(
"click.prompt", side_effect=[]
with (
patch.object(
todo, "_qemu_host_addresses", staticmethod(lambda: set())
),
patch.object(llm_disc, "run_ip", return_value=""),
patch.object(
todo, "_llm_probe_and_keep", lambda *a, **k: appels.append(a)
),
patch("click.prompt", side_effect=[]),
):
todo._llm_sweep_cidr("10.0.0.0/8")
self.assertEqual(appels, [])
@ -445,14 +464,13 @@ class SessionsClaudeCode(unittest.TestCase):
from script.todo.todo import TODO
todo = TODO()
with patch.object(
TODO, "prompt_claude_sessions"
) as mock_sessions, patch.object(
TODO, "prompt_execute_claude_plugins"
) as mock_plugins, patch(
"click.prompt", side_effect=["6", "0"]
), patch(
"script.todo.todo_telemetry.record"
with (
patch.object(TODO, "prompt_claude_sessions") as mock_sessions,
patch.object(
TODO, "prompt_execute_claude_plugins"
) as mock_plugins,
patch("click.prompt", side_effect=["6", "0"]),
patch("script.todo.todo_telemetry.record"),
):
todo.prompt_execute_gpt_code()
mock_sessions.assert_called_once_with()
@ -463,10 +481,12 @@ class SessionsClaudeCode(unittest.TestCase):
from script.todo.todo import TODO
todo = TODO()
with patch(
"script.todo.assistant.claude_sessions.fleet", return_value=[]
), patch("click.prompt", side_effect=["0"]), patch(
"script.todo.todo_telemetry.record"
with (
patch(
"script.todo.assistant.claude_sessions.fleet", return_value=[]
),
patch("click.prompt", side_effect=["0"]),
patch("script.todo.todo_telemetry.record"),
):
todo.prompt_claude_sessions()
@ -491,9 +511,11 @@ class SessionsClaudeCode(unittest.TestCase):
todo = TODO()
sortie = io.StringIO()
with patch("shutil.which", return_value="/usr/bin/claude"), patch(
"click.prompt", side_effect=["0"]
), redirect_stdout(sortie):
with (
patch("shutil.which", return_value="/usr/bin/claude"),
patch("click.prompt", side_effect=["0"]),
redirect_stdout(sortie),
):
todo._claude_reprendre([])
self.assertIn(t("No session on this machine."), sortie.getvalue())

View file

@ -45,8 +45,10 @@ sys.path.append(
os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
)
from script.todo.assistant import discover # noqa: E402
from script.todo.assistant import fingerprint # noqa: E402
from script.todo.assistant import (
discover, # noqa: E402
fingerprint, # noqa: E402
)
# Deux réseaux des blocs réservés à la documentation : ils ne peuvent
# désigner aucune machine réelle.

View file

@ -11,6 +11,7 @@ un chemin en gabarit doivent passer sans un mot.
La part qu'aucun motif ne juge — « cette phrase énonce-t-elle un fait durable
ou raconte-t-elle une journée » — n'est pas testée : elle n'est pas décidable.
"""
import os
import subprocess
import sys

View file

@ -94,24 +94,32 @@ class TestGetConfig(unittest.TestCase):
base_path = self._write_json(
"base.json", {"instance": [{"name": "test"}]}
)
with patch("script.config.config_file.CONFIG_FILE", base_path), patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
os.path.join(self.tmpdir, "nonexistent1.json"),
), patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
os.path.join(self.tmpdir, "nonexistent2.json"),
with (
patch("script.config.config_file.CONFIG_FILE", base_path),
patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
os.path.join(self.tmpdir, "nonexistent1.json"),
),
patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
os.path.join(self.tmpdir, "nonexistent2.json"),
),
):
result = self.cfg.get_config("instance")
self.assertEqual(result, [{"name": "test"}])
def test_get_config_returns_none_for_missing_key(self):
base_path = self._write_json("base.json", {"a": 1})
with patch("script.config.config_file.CONFIG_FILE", base_path), patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
os.path.join(self.tmpdir, "nonexistent1.json"),
), patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
os.path.join(self.tmpdir, "nonexistent2.json"),
with (
patch("script.config.config_file.CONFIG_FILE", base_path),
patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
os.path.join(self.tmpdir, "nonexistent1.json"),
),
patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
os.path.join(self.tmpdir, "nonexistent2.json"),
),
):
result = self.cfg.get_config("missing")
self.assertIsNone(result)
@ -125,12 +133,16 @@ class TestGetConfig(unittest.TestCase):
"override.json",
{"instance": [{"name": "override"}]},
)
with patch("script.config.config_file.CONFIG_FILE", base_path), patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
override_path,
), patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
os.path.join(self.tmpdir, "nonexistent.json"),
with (
patch("script.config.config_file.CONFIG_FILE", base_path),
patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
override_path,
),
patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
os.path.join(self.tmpdir, "nonexistent.json"),
),
):
result = self.cfg.get_config("instance")
# Lists with extend: base + override
@ -148,12 +160,16 @@ class TestGetConfig(unittest.TestCase):
"private.json",
{"data": {"key": "private_val"}},
)
with patch("script.config.config_file.CONFIG_FILE", base_path), patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
os.path.join(self.tmpdir, "nonexistent.json"),
), patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
private_path,
with (
patch("script.config.config_file.CONFIG_FILE", base_path),
patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
os.path.join(self.tmpdir, "nonexistent.json"),
),
patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
private_path,
),
):
result = self.cfg.get_config("data")
self.assertEqual(result, {"key": "private_val"})
@ -171,12 +187,16 @@ class TestGetConfig(unittest.TestCase):
"private.json",
{"items": [3], "meta": {"a": "private"}},
)
with patch("script.config.config_file.CONFIG_FILE", base_path), patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
override_path,
), patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
private_path,
with (
patch("script.config.config_file.CONFIG_FILE", base_path),
patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
override_path,
),
patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
private_path,
),
):
result_items = self.cfg.get_config("items")
result_meta = self.cfg.get_config("meta")
@ -188,15 +208,19 @@ class TestGetConfig(unittest.TestCase):
self.assertEqual(result_meta, {"a": "private", "b": "override"})
def test_no_config_files_exist(self):
with patch(
"script.config.config_file.CONFIG_FILE",
os.path.join(self.tmpdir, "nope1.json"),
), patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
os.path.join(self.tmpdir, "nope2.json"),
), patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
os.path.join(self.tmpdir, "nope3.json"),
with (
patch(
"script.config.config_file.CONFIG_FILE",
os.path.join(self.tmpdir, "nope1.json"),
),
patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
os.path.join(self.tmpdir, "nope2.json"),
),
patch(
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
os.path.join(self.tmpdir, "nope3.json"),
),
):
result = self.cfg.get_config("anything")
self.assertIsNone(result)
@ -284,12 +308,15 @@ class TestSetConfigValue(unittest.TestCase):
def test_round_trips_through_get_config_value(self):
self.cfg.set_config_value(["kdbx", "path"], "/round/trip.kdbx")
with patch(
"script.config.config_file.CONFIG_FILE",
os.path.join(self.tmp.name, "nonexistent_base.json"),
), patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
os.path.join(self.tmp.name, "nonexistent_override.json"),
with (
patch(
"script.config.config_file.CONFIG_FILE",
os.path.join(self.tmp.name, "nonexistent_base.json"),
),
patch(
"script.config.config_file.CONFIG_OVERRIDE_FILE",
os.path.join(self.tmp.name, "nonexistent_override.json"),
),
):
result = self.cfg.get_config_value(["kdbx", "path"])
self.assertEqual(result, "/round/trip.kdbx")

View file

@ -449,9 +449,8 @@ class TestTheSilenceThatLookedLikeAHang(unittest.TestCase):
def test_the_relay_shows_the_elapsed_time(self):
# Ce qui distingue « ça avance lentement » de « ça ne bouge plus ».
import io
from contextlib import redirect_stdout
import time as _time
from contextlib import redirect_stdout
echo = cleanup.make_echo(_time.monotonic() - 42)
out = io.StringIO()

View file

@ -158,8 +158,8 @@ class TestTheVersionGuard(unittest.TestCase):
self.vraie = database_cleanup.require_matching_version
self.vrai_shell = database_cleanup.run_shell
self.appels = []
database_cleanup.run_shell = (
lambda *a, **k: self.appels.append(a) or {}
database_cleanup.run_shell = lambda *a, **k: (
self.appels.append(a) or {}
)
def tearDown(self):

View file

@ -21,7 +21,7 @@ class TestEditText(unittest.TestCase):
def test_updates_image_after_erplibre(self):
path = self._write_compose(
"services:\n" " ERPLibre:\n" " image: old:1.0\n" " ports:\n"
"services:\n ERPLibre:\n image: old:1.0\n ports:\n"
)
config = SimpleNamespace(
docker_compose_file=path,

View file

@ -187,8 +187,7 @@ class TestWhatItRefusesToTouch(Base):
self.assertEqual(
"r",
self.lire(
"SELECT relkind FROM pg_class"
" WHERE relname = 'account_root'"
"SELECT relkind FROM pg_class WHERE relname = 'account_root'"
),
)

View file

@ -572,8 +572,8 @@ class TestTheToolIsCalledWithTheRightContract(unittest.TestCase):
def test_nothing_to_fix_asks_nothing(self):
demandes = []
self.obj.ask = (
lambda prompt, default="": demandes.append(prompt) or "y"
self.obj.ask = lambda prompt, default="": (
demandes.append(prompt) or "y"
)
self.obj.todo_upgrade_execute = lambda cmd, **kw: (0, cmd)
with redirect_stdout(io.StringIO()):

View file

@ -3,7 +3,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import unittest
from unittest.mock import patch, MagicMock
from unittest.mock import MagicMock, patch
from script.maintenance.format_file_to_commit import (
execute_shell,

View file

@ -15,6 +15,7 @@ Les messages sont traduits. Les assertions qui citent du texte fixent donc la
langue à « fr » pour la durée du module : sinon elles dépendraient de EL_LANG,
et un poste en anglais les ferait toutes échouer.
"""
import os
import subprocess
import sys

View file

@ -203,9 +203,7 @@ class TestStrInsert(unittest.TestCase):
class TestGetProjectConfig(unittest.TestCase):
def test_reads_github_token(self):
content = (
"#!/bin/bash\n"
'EL_GITHUB_TOKEN="my_token_123"\n'
'OTHER_VAR="value"\n'
'#!/bin/bash\nEL_GITHUB_TOKEN="my_token_123"\nOTHER_VAR="value"\n'
)
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", delete=False, dir="/tmp"

View file

@ -48,7 +48,9 @@ PyObject *sonde(long v) { return PyInt_FromLong(v); }
def _drapeau():
"""Le -D tel qu'il est écrit dans le script — source unique."""
m = re.search(r"export CPPFLAGS=\"[^\"]*?(-DPyInt_FromLong[^\" ]*)\"", SOURCE)
m = re.search(
r"export CPPFLAGS=\"[^\"]*?(-DPyInt_FromLong[^\" ]*)\"", SOURCE
)
return m.group(1) if m else ""
@ -65,7 +67,14 @@ def _compile(source, drapeaux):
fichier = Path(tmp, "sonde.cpp")
fichier.write_text(source, encoding="utf-8")
res = subprocess.run(
[_compilateur(), "-fsyntax-only", *drapeaux, "-I", inc, str(fichier)],
[
_compilateur(),
"-fsyntax-only",
*drapeaux,
"-I",
inc,
str(fichier),
],
capture_output=True,
text=True,
timeout=120,
@ -81,13 +90,13 @@ class TestLeDrapeauDansLeScript(unittest.TestCase):
"""Posé après, il n'atteindrait rien : c'est « poetry install » qui
lance les compilations."""
export = SOURCE.index("export CPPFLAGS=")
install = SOURCE.index('install --no-root ${POETRY_VERBOSE}')
install = SOURCE.index("install --no-root ${POETRY_VERBOSE}")
self.assertLess(export, install)
def test_it_appends_instead_of_replacing(self):
"""Écraser CPPFLAGS jetterait ce que l'appelant y avait mis — sur
s390x et openSUSE, des chemins d'en-têtes y passent."""
self.assertIn('${CPPFLAGS:+${CPPFLAGS} }', SOURCE)
self.assertIn("${CPPFLAGS:+${CPPFLAGS} }", SOURCE)
def test_it_is_cppflags_and_not_cflags(self):
"""Le piège qui a fait échouer le premier correctif : le fichier fautif
@ -145,9 +154,7 @@ class TestLeDrapeauCompile(unittest.TestCase):
"""Un hôte dont le SWIG écrit encore l'alias verrait, sinon, un
avertissement de redéfinition par fichier compilé. En -Werror, ce
serait un échec."""
ok, sortie = _compile(
SWIG_42 + APPEL_SWIG_43, ["-Werror", _drapeau()]
)
ok, sortie = _compile(SWIG_42 + APPEL_SWIG_43, ["-Werror", _drapeau()])
if ok is None:
self.skipTest(sortie)
self.assertTrue(ok, sortie[-400:])

View file

@ -11,6 +11,7 @@ la clé i18n brute, sans nommer ce qu'elle demandait.
Un mot de passe refusé est le cas NORMAL de cette fonction : elle doit le
dire, laisser recommencer, et laisser partir.
"""
import os
import tempfile
import unittest
@ -40,13 +41,16 @@ class KdbxCase(unittest.TestCase):
def _saisies(self, *reponses):
"""Renvoie (résultat, texte affiché) pour une suite de saisies."""
vues = []
with patch("getpass.getpass", side_effect=list(reponses)), patch(
"builtins.print",
# `**k` : un bouchon de `print` doit accepter la signature de
# `print`. Sans lui, ajouter un `flush=True` dans le code
# testé faisait échouer six tests sur une différence qui n'a
# rien à voir avec ce qu'ils vérifient.
side_effect=lambda *a, **k: vues.append(" ".join(map(str, a))),
with (
patch("getpass.getpass", side_effect=list(reponses)),
patch(
"builtins.print",
# `**k` : un bouchon de `print` doit accepter la signature de
# `print`. Sans lui, ajouter un `flush=True` dans le code
# testé faisait échouer six tests sur une différence qui n'a
# rien à voir avec ce qu'ils vérifient.
side_effect=lambda *a, **k: vues.append(" ".join(map(str, a))),
),
):
resultat = self._manager().get_kdbx()
return resultat, "\n".join(vues)
@ -86,9 +90,13 @@ class TestGivingUpIsPossible(KdbxCase):
def test_giving_up_asks_only_once(self):
appels = []
with patch(
"getpass.getpass", side_effect=lambda **k: appels.append(1) or ""
), patch("builtins.print"):
with (
patch(
"getpass.getpass",
side_effect=lambda **k: appels.append(1) or "",
),
patch("builtins.print"),
):
self._manager().get_kdbx()
self.assertEqual(len(appels), 1)

View file

@ -530,8 +530,8 @@ class TestComposeScreenMounted(unittest.IsolatedAsyncioTestCase):
app = await self._mounted_app(self._FakeIMAPTransport())
orig_connect, orig_send = smtp_send_mod.connect, smtp_send_mod.send
smtp_send_mod.connect = (
lambda account, password: self._FakeSMTPTransport()
smtp_send_mod.connect = lambda account, password: (
self._FakeSMTPTransport()
)
def boom(account, msg, transport):
@ -568,8 +568,8 @@ class TestComposeScreenMounted(unittest.IsolatedAsyncioTestCase):
imap_transport = self._FakeIMAPTransport()
app = await self._mounted_app(imap_transport)
orig_connect, orig_send = smtp_send_mod.connect, smtp_send_mod.send
smtp_send_mod.connect = (
lambda account, password: self._FakeSMTPTransport()
smtp_send_mod.connect = lambda account, password: (
self._FakeSMTPTransport()
)
smtp_send_mod.send = lambda account, msg, transport: [
"dest@example.com"
@ -637,8 +637,8 @@ class TestComposeScreenMounted(unittest.IsolatedAsyncioTestCase):
return ["dest@example.com"]
orig_connect, orig_send = smtp_send_mod.connect, smtp_send_mod.send
smtp_send_mod.connect = (
lambda account, password: self._FakeSMTPTransport()
smtp_send_mod.connect = lambda account, password: (
self._FakeSMTPTransport()
)
smtp_send_mod.send = capture_send
try:
@ -803,9 +803,10 @@ class TestBrowseFilesButton(TestComposeScreenMounted):
async with app.run_test() as pilot:
await pilot.press("c")
await pilot.pause()
with patch.object(
type(app), "suspend", fake_suspend
), patch.object(browser_mod, "FileBrowser", FakeFileBrowser):
with (
patch.object(type(app), "suspend", fake_suspend),
patch.object(browser_mod, "FileBrowser", FakeFileBrowser),
):
app.screen.query_one("#browse_files", Button).press()
await pilot.pause()

View file

@ -17,6 +17,7 @@ tout le fichier se saute proprement. Pour les lancer volontairement :
.venv.erplibre/bin/python -m unittest discover -s test \\
-p test_mail_live_server.py -v
"""
import email.utils
import threading
import unittest

View file

@ -80,24 +80,24 @@ class TestAddAccountRollsBack(unittest.TestCase):
vault = MagicMock()
vault.available_backends.return_value = ["kdbx"]
with patch.object(
menu, "secret_store_for", return_value=vault
), patch.object(
menu.mail_accounts, "save", side_effect=OSError("disque plein")
), patch.object(
menu, "_load_accounts", return_value=[]
), patch(
"builtins.input",
side_effect=[
"perso",
"moi@x.ca",
"",
"4",
"imap.x.ca",
"smtp.x.ca",
],
), patch(
"getpass.getpass", return_value="hunter2"
with (
patch.object(menu, "secret_store_for", return_value=vault),
patch.object(
menu.mail_accounts, "save", side_effect=OSError("disque plein")
),
patch.object(menu, "_load_accounts", return_value=[]),
patch(
"builtins.input",
side_effect=[
"perso",
"moi@x.ca",
"",
"4",
"imap.x.ca",
"smtp.x.ca",
],
),
patch("getpass.getpass", return_value="hunter2"),
):
menu._add_account(MagicMock())
@ -111,24 +111,24 @@ class TestAddAccountRollsBack(unittest.TestCase):
vault = MagicMock()
vault.available_backends.return_value = ["kdbx"]
with patch.object(
menu, "secret_store_for", return_value=vault
), patch.object(
menu.mail_accounts, "save", side_effect=OSError("disque plein")
), patch.object(
menu, "_load_accounts", return_value=[]
), patch(
"builtins.input",
side_effect=[
"perso",
"moi@x.ca",
"",
"4",
"imap.x.ca",
"smtp.x.ca",
],
), patch(
"getpass.getpass", return_value="hunter2"
with (
patch.object(menu, "secret_store_for", return_value=vault),
patch.object(
menu.mail_accounts, "save", side_effect=OSError("disque plein")
),
patch.object(menu, "_load_accounts", return_value=[]),
patch(
"builtins.input",
side_effect=[
"perso",
"moi@x.ca",
"",
"4",
"imap.x.ca",
"smtp.x.ca",
],
),
patch("getpass.getpass", return_value="hunter2"),
):
menu._add_account(MagicMock()) # ne doit pas lever
@ -144,9 +144,13 @@ class TestOpenTuiAllowsEmptyAccounts(unittest.TestCase):
import script.todo.mail.menu as menu
todo = MagicMock()
with patch.object(menu, "_load_accounts", return_value=[]), patch(
"script.todo.mail.tui.open_sessions", return_value=[]
) as mock_open, patch("script.todo.mail.tui.run_tui") as mock_run:
with (
patch.object(menu, "_load_accounts", return_value=[]),
patch(
"script.todo.mail.tui.open_sessions", return_value=[]
) as mock_open,
patch("script.todo.mail.tui.run_tui") as mock_run,
):
menu._open_tui(todo)
mock_open.assert_called_once()
@ -161,13 +165,12 @@ class TestOpenTuiAllowsEmptyAccounts(unittest.TestCase):
todo = MagicMock()
secrets = MagicMock()
with patch.object(
menu, "_load_accounts", return_value=[]
), patch.object(menu, "secret_store_for", return_value=secrets), patch(
"script.todo.mail.tui.open_sessions", return_value=[]
), patch(
"script.todo.mail.tui.run_tui"
) as mock_run:
with (
patch.object(menu, "_load_accounts", return_value=[]),
patch.object(menu, "secret_store_for", return_value=secrets),
patch("script.todo.mail.tui.open_sessions", return_value=[]),
patch("script.todo.mail.tui.run_tui") as mock_run,
):
menu._open_tui(todo)
_, kwargs = mock_run.call_args
@ -205,13 +208,13 @@ class TestSyncNowSurfacesResync(unittest.TestCase):
pass
buf = io.StringIO()
with patch.object(
menu, "_load_accounts", return_value=[account]
), patch(
"script.todo.mail.tui.open_sessions",
return_value=[FakeSession()],
), redirect_stdout(
buf
with (
patch.object(menu, "_load_accounts", return_value=[account]),
patch(
"script.todo.mail.tui.open_sessions",
return_value=[FakeSession()],
),
redirect_stdout(buf),
):
menu._sync_now(MagicMock())
@ -369,10 +372,10 @@ class TestCacheSizeAndPurge(unittest.TestCase):
ref, value
)
with patch.object(
menu, "_load_accounts", return_value=[account]
), patch.object(menu, "secret_store_for", return_value=secrets), patch(
"builtins.input", side_effect=["1", "o"]
with (
patch.object(menu, "_load_accounts", return_value=[account]),
patch.object(menu, "secret_store_for", return_value=secrets),
patch("builtins.input", side_effect=["1", "o"]),
):
menu._cache_size_and_purge(MagicMock()) # ne doit pas lever
@ -386,12 +389,10 @@ class TestCacheSizeAndPurge(unittest.TestCase):
store.root.mkdir(parents=True, exist_ok=True)
(store.root / "cache.db").write_bytes(b"pas une base sqlite" * 50)
with patch.object(
menu, "_load_accounts", return_value=[account]
), patch.object(
menu, "secret_store_for", return_value=MagicMock()
), patch(
"builtins.input", side_effect=["1", "o"]
with (
patch.object(menu, "_load_accounts", return_value=[account]),
patch.object(menu, "secret_store_for", return_value=MagicMock()),
patch("builtins.input", side_effect=["1", "o"]),
):
menu._cache_size_and_purge(MagicMock()) # ne doit pas lever
@ -469,9 +470,10 @@ class TestEnsureKdbx(unittest.TestCase):
import script.todo.mail.menu as menu
kdbx_path = os.path.join(self.tmp.name, "new.kdbx")
with mock_patch(
"builtins.input", side_effect=["1", kdbx_path]
), mock_patch("getpass.getpass", side_effect=["hunter2", "hunter2"]):
with (
mock_patch("builtins.input", side_effect=["1", kdbx_path]),
mock_patch("getpass.getpass", side_effect=["hunter2", "hunter2"]),
):
result = menu._ensure_kdbx(self.todo)
self.assertTrue(result)
self.assertTrue(os.path.isfile(kdbx_path))
@ -486,10 +488,11 @@ class TestEnsureKdbx(unittest.TestCase):
import script.todo.mail.menu as menu
kdbx_path = os.path.join(self.tmp.name, "new.kdbx")
with mock_patch(
"builtins.input", side_effect=["1", kdbx_path]
), mock_patch(
"getpass.getpass", side_effect=["hunter2", "somethingelse"]
with (
mock_patch("builtins.input", side_effect=["1", kdbx_path]),
mock_patch(
"getpass.getpass", side_effect=["hunter2", "somethingelse"]
),
):
result = menu._ensure_kdbx(self.todo)
self.assertFalse(result)
@ -537,9 +540,10 @@ class TestEnsureKdbx(unittest.TestCase):
import script.todo.mail.menu as menu
with mock_patch.object(
menu.mail_accounts, "save"
) as mock_save, mock_patch("builtins.input", side_effect=["0"]):
with (
mock_patch.object(menu.mail_accounts, "save") as mock_save,
mock_patch("builtins.input", side_effect=["0"]),
):
menu._add_account(self.todo)
mock_save.assert_not_called()
# Le squelette du vrai `todo.json` donne "" (pas None) tant que
@ -609,14 +613,11 @@ class TestTodoWiring(unittest.TestCase):
from script.todo.todo import TODO
todo = TODO()
with patch.object(
TODO, "prompt_assistant_llm"
) as mock_question, patch(
"script.todo.mail.menu.prompt_execute_mail"
) as mock_mail, patch(
"click.prompt", side_effect=["1", "0"]
), patch(
"script.todo.todo_telemetry.record"
with (
patch.object(TODO, "prompt_assistant_llm") as mock_question,
patch("script.todo.mail.menu.prompt_execute_mail") as mock_mail,
patch("click.prompt", side_effect=["1", "0"]),
patch("script.todo.todo_telemetry.record"),
):
todo.prompt_assistant()
@ -631,14 +632,11 @@ class TestTodoWiring(unittest.TestCase):
from script.todo.todo import TODO
todo = TODO()
with patch.object(
TODO, "prompt_assistant_llm"
) as mock_question, patch(
"script.todo.mail.menu.prompt_execute_mail"
) as mock_mail, patch(
"script.todo.todo_telemetry.record"
), patch(
"click.prompt", side_effect=["2", "0"]
with (
patch.object(TODO, "prompt_assistant_llm") as mock_question,
patch("script.todo.mail.menu.prompt_execute_mail") as mock_mail,
patch("script.todo.todo_telemetry.record"),
patch("click.prompt", side_effect=["2", "0"]),
):
todo.prompt_assistant()
@ -674,10 +672,13 @@ class TestRetryPassword(unittest.TestCase):
from script.todo.mail.menu import retry_password
vues = []
with patch(
"getpass.getpass", side_effect=lambda p="": vues.append(p) or ""
), patch("script.todo.mail.menu.secret_store_for"), patch(
"builtins.print"
with (
patch(
"getpass.getpass",
side_effect=lambda p="": vues.append(p) or "",
),
patch("script.todo.mail.menu.secret_store_for"),
patch("builtins.print"),
):
retry_password(
self._todo(),
@ -742,11 +743,13 @@ class TestRetryPassword(unittest.TestCase):
compte = account_from_preset("essai", "a@x.ca", preset_key)
vues = []
with patch("getpass.getpass", return_value=""), patch(
"script.todo.mail.menu.secret_store_for"
), patch(
"builtins.print",
side_effect=lambda *a: vues.append(" ".join(map(str, a))),
with (
patch("getpass.getpass", return_value=""),
patch("script.todo.mail.menu.secret_store_for"),
patch(
"builtins.print",
side_effect=lambda *a: vues.append(" ".join(map(str, a))),
),
):
retry_password(
self._todo(),
@ -782,9 +785,10 @@ class TestRetryPassword(unittest.TestCase):
def logout(self):
pass
with patch("getpass.getpass", return_value="bon"), patch(
"script.todo.mail.menu.secret_store_for"
) as store:
with (
patch("getpass.getpass", return_value="bon"),
patch("script.todo.mail.menu.secret_store_for") as store,
):
store.return_value.set.side_effect = self.vault.__setitem__
ok = retry_password(
self._todo(),
@ -802,9 +806,10 @@ class TestRetryPassword(unittest.TestCase):
def refuse(account, password):
raise OSError("530 refus")
with patch("getpass.getpass", return_value="faux"), patch(
"script.todo.mail.menu.secret_store_for"
) as store:
with (
patch("getpass.getpass", return_value="faux"),
patch("script.todo.mail.menu.secret_store_for") as store,
):
store.return_value.set.side_effect = self.vault.__setitem__
ok = retry_password(
self._todo(), self.account, attempts=2, connect_fn=refuse

View file

@ -182,13 +182,18 @@ class TestKeyringBranch(unittest.TestCase):
def test_set_and_get_through_keyring(self):
vault = {}
with patch(
"script.todo.mail.secrets.keyring_is_safe", return_value=True
), patch(
"keyring.set_password",
side_effect=lambda s, u, p: vault.__setitem__((s, u), p),
), patch(
"keyring.get_password", side_effect=lambda s, u: vault.get((s, u))
with (
patch(
"script.todo.mail.secrets.keyring_is_safe", return_value=True
),
patch(
"keyring.set_password",
side_effect=lambda s, u, p: vault.__setitem__((s, u), p),
),
patch(
"keyring.get_password",
side_effect=lambda s, u: vault.get((s, u)),
),
):
self.store.set("keyring:perso", "hunter2")
self.assertEqual(self.store.get("keyring:perso"), "hunter2")
@ -196,9 +201,12 @@ class TestKeyringBranch(unittest.TestCase):
def test_refuses_unsafe_backend(self):
# `keyring.get_keyring` est patché AUSSI : le message d'erreur passe par
# keyring_backend_name(), qui interrogerait sinon le vrai trousseau.
with patch(
"script.todo.mail.secrets.keyring_is_safe", return_value=False
), patch("keyring.get_keyring", return_value=MagicMock()):
with (
patch(
"script.todo.mail.secrets.keyring_is_safe", return_value=False
),
patch("keyring.get_keyring", return_value=MagicMock()),
):
with self.assertRaises(SecretError) as ctx:
self.store.set("keyring:perso", "hunter2")
# Traduit : on compare à la clé i18n elle-même, pas au mot français,

Some files were not shown because too many files have changed in this diff Show more