aboutsummaryrefslogtreecommitdiff
path: root/app/backends/common.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-09-10 00:16:00 -0400
committerhistoria <historiavg@proton.me>2026-09-10 00:16:00 -0400
commite2da233cd1baa859a5721542fc8b80d9f3f880e7 (patch)
tree4e86797cbd7dce0434feef65bc0ad2e97bfbe4c0 /app/backends/common.py
parent31459b281b6a5368c692b3c42c91e522995ebd57 (diff)
downloadtts-audiobook-generator-e2da233cd1baa859a5721542fc8b80d9f3f880e7.tar.gz
fix: smart chunking, extraction, and process-safety issuesHEADmain
Diffstat (limited to 'app/backends/common.py')
-rw-r--r--app/backends/common.py46
1 files changed, 36 insertions, 10 deletions
diff --git a/app/backends/common.py b/app/backends/common.py
index 7974b58..20de432 100644
--- a/app/backends/common.py
+++ b/app/backends/common.py
@@ -9,6 +9,7 @@ TUI) so it can be reused without pulling curses into a non-interactive
run.
"""
+import ast
import os
import re
import shutil
@@ -306,25 +307,50 @@ def update_config_value(key: str, value,
"""Set ``KEY`` to VALUE in app/converter/config.py and in memory.
Only the value of the named assignment changes: indentation and any
- trailing comment are preserved. Strings render double-quoted; other
- literals (ints, booleans) render bare. After a successful write (or
- when the file already holds VALUE) the new value is mirrored onto the
- imported ``converter.config`` module, so a wizard's change takes
- effect immediately instead of only after the next process start.
- Returns True when the file now holds VALUE, False when it could not
- be read or written (or KEY has no line in it).
+ trailing comment are preserved. Strings render as proper Python
+ literals via ``repr`` (quoting with bare double quotes would instead
+ produce invalid syntax — or silently change the value — whenever the
+ string itself contains a quote or a backslash, corrupting
+ config.py); other literals (ints, booleans) render bare. After a
+ successful write (or when the file already holds VALUE) the new
+ value is mirrored onto the imported ``converter.config`` module, so
+ a wizard's change takes effect immediately instead of only after
+ the next process start. Returns True when the file now holds VALUE,
+ False when it could not be read or written (or KEY has no line in
+ it, or the edit would not parse).
"""
path = Path(config_path) if config_path is not None else CONFIG_PATH
- rendered = f'"{value}"' if isinstance(value, str) else str(value)
+ rendered = repr(value) if isinstance(value, str) else str(value)
try:
text = path.read_text(encoding="utf-8")
match = re.search(
- rf'(?m)^(\s*{re.escape(key)}\s*=\s*)("[^"]*"|\S+)(\s*(?:#.*)?)$',
+ rf'(?m)^(\s*{re.escape(key)}\s*=\s*)'
+ # Any valid Python string literal, single- or double-quoted
+ # (both spellings occur once repr() has written a value),
+ # else a bare literal token.
+ r'("[^"\\]*(?:\\.[^"\\]*)*"'
+ r"|'[^'\\]*(?:\\.[^'\\]*)*'"
+ r'|\S+)'
+ r'(\s*(?:#.*)?)$',
text)
if match is None:
return False
- if match.group(2) != rendered:
+ try:
+ # Semantic equality first: a file still holding the value in
+ # the old quoting style must not be rewritten (a no-op save
+ # stays a no-op), and the matched token may be any literal.
+ same = (match.group(2) == rendered
+ or ast.literal_eval(match.group(2)) == value)
+ except (ValueError, SyntaxError):
+ same = match.group(2) == rendered
+ if not same:
text = text[:match.start(2)] + rendered + text[match.end(2):]
+ try:
+ # Never write a file that fails to import: a broken
+ # config.py breaks every later process start.
+ compile(text, str(path), "exec")
+ except (SyntaxError, ValueError):
+ return False
path.write_text(text, encoding="utf-8")
except OSError:
return False