refactor(config): уточнены типы каскада настроек

- Зачем:
  - общий union допускал несоответствие между ключом настройки и его типом.
- Что:
  - разделены типы TOML, CLI и полностью разрешённой конфигурации.
  - каскад значений сохранён в типобезопасной generic-функции.
- Проверка:
  - mypy src/local_transcriber/config.py.
  - pytest tests/test_config.py tests/test_cli.py: 109 passed.
This commit is contained in:
Dmitriy Dementiev committed 2026-09-02 12:52:33 +03:00
1 parent ef61ced2e5
commit 2e30d668ce
3 files changed
+86 -26

No files matched your search

+10 -4
View File
@@ -11,7 +11,13 @@ import typer
from rich.console import Console
from rich.status import Status
from .config import ConfigValue, apply_device_defaults, load_config, resolve_defaults
from .config import (
CliValues,
ResolvedConfig,
apply_device_defaults,
load_config,
resolve_defaults,
)
from .context_menu import install_menu as install_context_menu
from .context_menu import uninstall_menu as uninstall_context_menu
from .diarization import build_speaker_transcript
@@ -337,7 +343,7 @@ def main(
try:
config = load_config()
cli_values = {
cli_values: CliValues = {
"model": model,
"language": language,
"device": device,
@@ -417,7 +423,7 @@ def main(
def _run_single(
file: Path,
defaults: dict[str, ConfigValue],
defaults: ResolvedConfig,
output: Path | None,
verbose: bool,
compute_type_explicit: bool = False,
@@ -561,7 +567,7 @@ def _run_single(
def _run_batch(
files: list[Path],
defaults: dict[str, ConfigValue],
defaults: ResolvedConfig,
verbose: bool,
force: bool,
compute_type_explicit: bool = False,
+76 -21
View File
@@ -3,10 +3,43 @@
import tomllib
import warnings
from pathlib import Path
from typing import TypedDict, TypeVar, cast
type ConfigValue = str | bool
_T = TypeVar("_T")
HARDCODED_DEFAULTS: dict[str, ConfigValue] = {
class ConfigValues(TypedDict, total=False):
"""Проверенные значения из TOML-конфига."""
model: str
language: str
device: str
compute_type: str
diarize: bool
class CliValues(TypedDict, total=False):
"""Явные значения CLI до разрешения каскада."""
model: str | None
language: str | None
device: str | None
compute_type: str | None
diarize: bool | None
class ResolvedConfig(TypedDict):
"""Полная конфигурация после разрешения каскада."""
model: str
language: str
device: str
compute_type: str
diarize: bool
HARDCODED_DEFAULTS: ResolvedConfig = {
"model": "medium",
"language": "ru",
"device": "auto",
@@ -49,7 +82,7 @@ def find_config_file() -> Path | None:
return None
def load_config(path: Path | None = None) -> dict[str, ConfigValue]:
def load_config(path: Path | None = None) -> ConfigValues:
"""Загружает и валидирует TOML-конфиг.
Неизвестные ключи вызывают предупреждение (а не ошибку) для forward
@@ -99,31 +132,53 @@ def load_config(path: Path | None = None) -> dict[str, ConfigValue]:
raise ValueError(f"Значение 'language' в {path} не может быть пустым")
result[key] = value
return result
return cast(ConfigValues, result)
def resolve_defaults(
cli_values: dict[str, ConfigValue | None], config: dict[str, ConfigValue]
) -> dict[str, ConfigValue]:
def resolve_defaults(cli_values: CliValues, config: ConfigValues) -> ResolvedConfig:
"""Каскад приоритетов: CLI > конфиг-файл > hardcoded-дефолты."""
result: dict[str, ConfigValue] = {}
for key in HARDCODED_DEFAULTS:
cli_val = cli_values.get(key)
if cli_val is not None:
result[key] = cli_val
elif key in config:
result[key] = config[key]
else:
result[key] = HARDCODED_DEFAULTS[key]
return result
return {
"model": _resolve_value(
cli_values.get("model"), config.get("model"), HARDCODED_DEFAULTS["model"]
),
"language": _resolve_value(
cli_values.get("language"),
config.get("language"),
HARDCODED_DEFAULTS["language"],
),
"device": _resolve_value(
cli_values.get("device"),
config.get("device"),
HARDCODED_DEFAULTS["device"],
),
"compute_type": _resolve_value(
cli_values.get("compute_type"),
config.get("compute_type"),
HARDCODED_DEFAULTS["compute_type"],
),
"diarize": _resolve_value(
cli_values.get("diarize"),
config.get("diarize"),
HARDCODED_DEFAULTS["diarize"],
),
}
def _resolve_value(cli_value: _T | None, config_value: _T | None, default: _T) -> _T:
"""Выбирает первое явно заданное значение каскада."""
if cli_value is not None:
return cli_value
if config_value is not None:
return config_value
return default
def apply_device_defaults(
defaults: dict[str, ConfigValue],
defaults: ResolvedConfig,
resolved_device: str,
cli_values: dict[str, ConfigValue | None],
config: dict[str, ConfigValue],
) -> dict[str, ConfigValue]:
cli_values: CliValues,
config: ConfigValues,
) -> ResolvedConfig:
"""Применяет device-aware дефолты для model и compute_type,
если они не были явно заданы через CLI или конфиг."""
device_defs = DEVICE_DEFAULTS.get(resolved_device, {})
@@ -135,4 +190,4 @@ def apply_device_defaults(
if cli_values.get(key) is None and key not in config:
if key in device_defs:
result[key] = device_defs[key]
return result
return cast(ResolvedConfig, result)
-1
View File
@@ -1,4 +1,3 @@
from pathlib import Path
from unittest.mock import patch
import pytest