[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:
parent
e496b107a6
commit
cfbdd9406e
155 changed files with 1522 additions and 1190 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
import xmlrpc.client
|
||||
import sys
|
||||
import xmlrpc.client
|
||||
|
||||
import click
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 :
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'"
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue