refactor(webui): deduplicate _extract_checkbox (3 copies → 1)

Extracted extract_checkbox from settings.py, form.py (x2) into
webui/routes/utils/common.py. Moved coerce_bool from settings.py
to common.py to break circular import.

Fixed bug in second form.py copy (was missing __contains__ check).
1006 tests pass.
This commit is contained in:
Artem Akymenko
2026-07-16 07:46:51 +00:00
parent c2c584e741
commit 17229b2390
4 changed files with 42 additions and 56 deletions
+2 -9
View File
@@ -19,6 +19,7 @@ from abogen.webui.routes.utils.settings import (
_NORMALIZATION_STRING_KEYS,
_DEFAULT_ANALYSIS_THRESHOLD,
)
from abogen.webui.routes.utils.common import extract_checkbox
from abogen.webui.routes.utils.voice import template_options
from abogen.webui.debug_tts_runner import run_debug_tts_wavs
from abogen.debug_tts_samples import DEBUG_TTS_SAMPLES
@@ -93,17 +94,9 @@ def update_settings() -> ResponseReturnValue:
maximum=25,
)
def _extract_checkbox(name: str, default: bool) -> bool:
values = form.getlist(name) if hasattr(form, "getlist") else []
if values:
return coerce_bool(values[-1], default)
if hasattr(form, "__contains__") and name in form:
return False
return default
# Normalization settings
for key in _NORMALIZATION_BOOLEAN_KEYS:
current[key] = _extract_checkbox(key, bool(current.get(key, True)))
current[key] = extract_checkbox(form, key, bool(current.get(key, True)))
for key in _NORMALIZATION_STRING_KEYS:
if hasattr(form, "__contains__") and key in form:
current[key] = (form.get(key) or "").strip()
+36 -1
View File
@@ -1,6 +1,17 @@
from typing import Any, Optional, Tuple, Iterable, List
from typing import Any, Optional, Tuple, Iterable, List, Mapping
from pathlib import Path
def coerce_bool(value: Any, default: bool) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in {"true", "1", "yes", "on"}
if value is None:
return default
return bool(value)
def split_profile_spec(value: Any) -> Tuple[str, Optional[str]]:
text = str(value or "").strip()
if not text:
@@ -18,7 +29,31 @@ def split_speaker_spec(value: Any) -> Tuple[str, Optional[str]]:
return split_profile_spec(value)
def existing_paths(paths: Optional[Iterable[Path]]) -> List[Path]:
if not paths:
return []
return [p for p in paths if p.exists()]
def extract_checkbox(form: Mapping[str, Any], name: str, default: bool) -> bool:
"""Extract a boolean checkbox value from a form-like mapping.
Handles both multi-value forms (Flask's `getlist`) and simple mappings.
If the checkbox name is present but has no value, it means unchecked (False).
"""
values: List[str] = []
getter = getattr(form, "getlist", None)
if callable(getter):
raw_values = getter(name)
if raw_values:
values = list(raw_values)
else:
raw_flag = form.get(name)
if raw_flag is not None:
values = [raw_flag]
if values:
return coerce_bool(values[-1], default)
if name in form:
return False
return default
+3 -35
View File
@@ -29,7 +29,7 @@ from abogen.webui.routes.utils.voice import (
)
from abogen.webui.routes.utils.entity import sync_pronunciation_overrides
from abogen.webui.routes.utils.epub import job_download_flags
from abogen.webui.routes.utils.common import split_profile_spec
from abogen.webui.routes.utils.common import split_profile_spec, extract_checkbox
from abogen.utils import calculate_text_length
from abogen.voice_profiles import serialize_profiles, normalize_profile_entry
from abogen.chunking import ChunkLevel, build_chunks_for_chapters
@@ -537,28 +537,11 @@ def apply_book_step_form(
else:
pending.normalize_chapter_opening_caps = caps_default
def _extract_checkbox(name: str, default: bool) -> bool:
values: List[str] = []
getter = getattr(form, "getlist", None)
if callable(getter):
raw_values = getter(name)
if raw_values:
values = list(cast(Iterable[str], raw_values))
else:
raw_flag = form.get(name)
if raw_flag is not None:
values = [raw_flag]
if values:
return coerce_bool(values[-1], default)
if hasattr(form, "__contains__") and name in form:
return False
return default
overrides_existing = getattr(pending, "normalization_overrides", None)
overrides: Dict[str, Any] = dict(overrides_existing or {})
for key in _NORMALIZATION_BOOLEAN_KEYS:
default_toggle = overrides.get(key, bool(settings.get(key, True)))
overrides[key] = _extract_checkbox(key, default_toggle)
overrides[key] = extract_checkbox(form, key, default_toggle)
for key in _NORMALIZATION_STRING_KEYS:
default_val = overrides.get(key, str(settings.get(key, "")))
val = form.get(key)
@@ -886,25 +869,10 @@ def build_pending_job_from_extraction(
apply_config=bool(speaker_config_payload),
)
def _extract_checkbox(name: str, default: bool) -> bool:
values: List[str] = []
getter = getattr(form, "getlist", None)
if callable(getter):
raw_values = getter(name)
if raw_values:
values = list(cast(Iterable[str], raw_values))
else:
raw_flag = form.get(name)
if raw_flag is not None:
values = [raw_flag]
if values:
return coerce_bool(values[-1], default)
return default
normalization_overrides = {}
for key in _NORMALIZATION_BOOLEAN_KEYS:
default_val = bool(settings.get(key, True))
normalization_overrides[key] = _extract_checkbox(key, default_val)
normalization_overrides[key] = extract_checkbox(form, key, default_val)
for key in _NORMALIZATION_STRING_KEYS:
default_val = str(settings.get(key, ""))
+1 -11
View File
@@ -15,7 +15,7 @@ from abogen.normalization_settings import (
from abogen.utils import load_config, save_config
from abogen.integrations.calibre_opds import CalibreOPDSClient
from abogen.integrations.audiobookshelf import AudiobookshelfConfig
from abogen.webui.routes.utils.common import split_profile_spec
from abogen.webui.routes.utils.common import split_profile_spec, coerce_bool
SAVE_MODE_LABELS = {
"save_next_to_input": "Save next to input file",
@@ -245,16 +245,6 @@ def render_prompt_template(template: str, context: Mapping[str, str]) -> str:
return _PROMPT_TOKEN_RE.sub(_replace, template)
def coerce_bool(value: Any, default: bool) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in {"true", "1", "yes", "on"}
if value is None:
return default
return bool(value)
def coerce_float(value: Any, default: float) -> float:
try:
return max(0.0, float(value))