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:
1 parent
ef61ced2e5
commit
2e30d668ce
3 files changed
+86
-26
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user