From 9296c28bed61e12afa213eeb91fca1e9d92236e2 Mon Sep 17 00:00:00 2001 From: Domi31tls Date: Thu, 25 Jun 2026 17:56:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(gui):=20log=20fichier=20rotatif=20V6=20?= =?UTF-8?q?=C3=A0=20chemin=20connu=20(E1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gui_v6/logging_setup.py | 67 +++++++++++++++++++++++++ tests/unit/test_gui_v6_logging_setup.py | 16 ++++++ 2 files changed, 83 insertions(+) create mode 100644 gui_v6/logging_setup.py create mode 100644 tests/unit/test_gui_v6_logging_setup.py diff --git a/gui_v6/logging_setup.py b/gui_v6/logging_setup.py new file mode 100644 index 0000000..9d19ac5 --- /dev/null +++ b/gui_v6/logging_setup.py @@ -0,0 +1,67 @@ +"""Configuration du log fichier de la GUI V6 (E1). + +Sans ceci, la GUI frozen fenêtrée (sans console) perd ses logs de diagnostic. +Le log est posé dans le même répertoire applicatif que la licence +(``%LOCALAPPDATA%/Aivanov/Anonymisation``) pour faciliter sa récupération (E2/E3). +""" + +from __future__ import annotations + +import logging +import os +from logging.handlers import RotatingFileHandler +from pathlib import Path + +_CONFIGURED = False + + +def _app_data_dir() -> Path: + base = os.environ.get("LOCALAPPDATA") + if base: + root = Path(base) + else: # Linux/dev + root = Path.home() / ".local" / "share" + return root / "Aivanov" / "Anonymisation" + + +def log_file_path() -> Path: + return _app_data_dir() / "logs" / "anonymisation.log" + + +def setup_file_logging() -> Path: + """Configure un handler fichier rotatif sur le logger racine. Idempotent.""" + global _CONFIGURED + path = log_file_path() + if _CONFIGURED: + return path + path.parent.mkdir(parents=True, exist_ok=True) + handler = RotatingFileHandler( + str(path), maxBytes=2_000_000, backupCount=3, encoding="utf-8" + ) + handler.setFormatter( + logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s") + ) + root = logging.getLogger() + if root.level == logging.NOTSET: + root.setLevel(logging.INFO) + root.addHandler(handler) + # Best-effort : si le cœur utilise loguru, on ajoute aussi un sink fichier. + try: + from loguru import logger as _loguru + + _loguru.add(str(path), rotation="2 MB", retention=3, encoding="utf-8") + except Exception: + pass + _CONFIGURED = True + return path + + +def _reset_for_tests() -> None: + """Réinitialise l'état pour l'isolation des tests (NE PAS appeler en prod).""" + global _CONFIGURED + root = logging.getLogger() + for h in list(root.handlers): + if isinstance(h, RotatingFileHandler): + root.removeHandler(h) + h.close() + _CONFIGURED = False diff --git a/tests/unit/test_gui_v6_logging_setup.py b/tests/unit/test_gui_v6_logging_setup.py new file mode 100644 index 0000000..6c2cecd --- /dev/null +++ b/tests/unit/test_gui_v6_logging_setup.py @@ -0,0 +1,16 @@ +import logging + + +def test_setup_file_logging_writes_to_known_path(tmp_path, monkeypatch): + monkeypatch.setenv("LOCALAPPDATA", str(tmp_path)) + from gui_v6.logging_setup import setup_file_logging, _reset_for_tests + + try: + log_path = setup_file_logging() + assert log_path.parent.exists() + logging.getLogger("test.e1").warning("ligne-temoin-42") + for h in logging.getLogger().handlers: + h.flush() + assert "ligne-temoin-42" in log_path.read_text(encoding="utf-8") + finally: + _reset_for_tests()