From 09f6062e8c66863fbd6432273a7836615d1aed8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=BA=D0=BE=D1=81=20=D0=90=D1=80=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=9D=D0=B8=D0=BA=D0=BE=D0=BB=D0=B0=D0=B5=D0=B2=D0=B8?= =?UTF-8?q?=D1=87?= Date: Fri, 17 Jul 2026 14:45:31 +0700 Subject: [PATCH] fix: restore .env loading for --init-config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Регрессия от 1df1194 (#64): load_dotenv переехал с module-level в Config.load_yaml(), но --init-config выходит до этого вызова, поэтому _run_init_config больше не видел значения из .env — пользователь с настройками только в .env получал пустой YAML, хотя docs/CONFIG.md обещает чтение текущих значений из .env. В начале _run_init_config добавлен load_dotenv(find_dotenv(usecwd=True), override=False): поиск .env идёт от текущей директории (plain-вызов без usecwd стартует от директории cli.py и .env пользователя не находит), переменные окружения не перебиваются. Импорт модуля по-прежнему без side effects — вызов внутри функции. Регрессионный тест: test_init_config_reads_dotenv_file (реальный .env в tmp_path + chdir, без мока os.environ на целевые переменные). Refs #64 --- redmine_reporter/cli.py | 12 +++++++++++- tests/test_cli.py | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/redmine_reporter/cli.py b/redmine_reporter/cli.py index 6300fcc..1ed712b 100644 --- a/redmine_reporter/cli.py +++ b/redmine_reporter/cli.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import List, Optional import yaml +from dotenv import find_dotenv, load_dotenv from . import __version__ from .client import RedmineAPIError, fetch_issues_with_spent_time @@ -70,7 +71,16 @@ def parse_date_range(date_arg: str) -> tuple[str, str]: def _run_init_config(config_path: str, force: bool) -> int: - """Создаёт YAML-конфиг из текущих настроек окружения.""" + """Создаёт YAML-конфиг из текущих настроек окружения. + + Подгружает .env из текущей директории (override=False: реальные + переменные окружения не перебиваются), чтобы --init-config видел + значения, заданные только в .env (#64). Поиск идёт от cwd + (usecwd=True): find_dotenv() без него стартует от директории + cli.py и .env пользователя не находит. + """ + load_dotenv(find_dotenv(usecwd=True), override=False) + path = Path(config_path) if path.exists() and not force: diff --git a/tests/test_cli.py b/tests/test_cli.py index e2fdb2d..e362ec2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -425,6 +425,49 @@ class TestInitConfig: assert data["redmine"]["url"] == "" assert data["redmine"]["api_key"] == "" + def test_init_config_reads_dotenv_file(self, tmp_path, monkeypatch): + """#64: --init-config подхватывает значения из реального .env в cwd. + + Регрессионный тест: после 1df1194 load_dotenv перестал выполняться + до _run_init_config, и пользователь с настройками только в .env + получал пустой YAML. + """ + import yaml + + (tmp_path / ".env").write_text( + "REDMINE_URL=https://redmine.example.com\n" + "REDMINE_API_KEY=secret-from-dotenv\n" + "REDMINE_AUTHOR=ivan.ivanov\n" + "DEFAULT_FROM_DATE=2026-01-01\n" + "DEFAULT_TO_DATE=2026-01-31\n" + "SMTP_PASSWORD=smtp-secret\n" + ) + monkeypatch.chdir(tmp_path) + # Целевые переменные не мокаются: значения должны прийти только из .env. + for var in ( + "REDMINE_URL", + "REDMINE_API_KEY", + "REDMINE_AUTHOR", + "DEFAULT_FROM_DATE", + "DEFAULT_TO_DATE", + "SMTP_PASSWORD", + ): + monkeypatch.delenv(var, raising=False) + + config_path = tmp_path / "config.yml" + code = main(["--init-config", "--config-path", str(config_path)]) + assert code == 0 + + with open(config_path) as fh: + data = yaml.safe_load(fh) + + assert data["redmine"]["url"] == "https://redmine.example.com" + assert data["redmine"]["api_key"] == "${REDMINE_API_KEY}" + assert data["redmine"]["author"] == "ivan.ivanov" + assert data["period"]["default_from"] == "2026-01-01" + assert data["period"]["default_to"] == "2026-01-31" + assert data["email"]["smtp"]["password"] == "${SMTP_PASSWORD}" + @mock.patch.dict(os.environ, VALID_ENV, clear=True) def test_init_config_includes_report_section(self, tmp_path): """--init-config генерирует секцию report с no_time: false."""