feat: status translation overrides via YAML config

report.status_translation in YAML config overrides/extends the builtin
STATUS_TRANSLATION dictionary; without the section behavior is unchanged.

- AppConfig.report_status_translation + _resolve_str_dict (warns and
  skips non-scalar values, resolves ${VAR} references)
- Config.get_status_translation() returns merged copy (lazy import,
  builtin dict never mutated)
- build_grouped_report() accepts optional status_translation parameter
- --init-config template gains commented example, docs/CONFIG.md updated

Closes #65
This commit is contained in:
Кокос Артем Николаевич
2026-07-17 15:18:27 +07:00
parent 09f6062e8c
commit 8614062ecd
7 changed files with 332 additions and 2 deletions

View File

@@ -483,6 +483,55 @@ class TestInitConfig:
assert "report" in data
assert data["report"]["no_time"] is False
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
def test_init_config_includes_status_translation_example(self, tmp_path):
"""#65: --init-config дописывает закомментированный пример status_translation.
Файл остаётся валидным YAML, активной секции status_translation нет.
"""
import yaml
config_path = tmp_path / "config.yml"
code = main(["--init-config", "--config-path", str(config_path)])
assert code == 0
text = config_path.read_text(encoding="utf-8")
assert "# status_translation:" in text
with open(config_path) as fh:
data = yaml.safe_load(fh)
assert "status_translation" not in data["report"]
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
def test_cli_status_translation_from_yaml_reaches_builder(mock_fetch, tmp_path):
"""#65: report.status_translation из YAML доезжает до build_grouped_report."""
issue = _MockIssue()
mock_fetch.return_value = [(issue, 1.0, None)]
config_path = tmp_path / "config.yml"
config_path.write_text(
"report:\n"
" status_translation:\n"
' New: "Новая"\n'
' "Custom Status": "Кастом"\n'
)
with mock.patch("redmine_reporter.cli.build_grouped_report") as mock_build:
mock_build.return_value = []
code = main(
["--date", "2026-01-01--2026-01-31", "--config-path", str(config_path)]
)
assert code == 0
_, kwargs = mock_build.call_args
translation = kwargs["status_translation"]
assert translation["New"] == "Новая"
assert translation["Custom Status"] == "Кастом"
# встроенные переводы сохранились
assert translation["Closed"] == "Закрыто"
@mock.patch.dict(os.environ, {}, clear=True)
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")