aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-28 04:18:56 -0400
committerhistoria <historiavg@proton.me>2026-08-28 04:18:56 -0400
commit876a6eb3ba9974d0eadc1fd3f628dafdc56d8716 (patch)
tree8a788f19c7494c7133ecd94ec2bfb9813d3c0f83 /app
parent5247b2f2f83e224f3438e0afa552f2b4728d51c2 (diff)
downloadtts-audiobook-generator-876a6eb3ba9974d0eadc1fd3f628dafdc56d8716.tar.gz
fix: system python used instead of venv python, breaking whisper transcription
Diffstat (limited to 'app')
-rw-r--r--app/backends/envs.py11
-rw-r--r--app/tests/test_backends_envs.py33
2 files changed, 36 insertions, 8 deletions
diff --git a/app/backends/envs.py b/app/backends/envs.py
index ff6d1bf..494c3e2 100644
--- a/app/backends/envs.py
+++ b/app/backends/envs.py
@@ -89,9 +89,16 @@ def env_exists(env_dir: Optional[Path] = None) -> bool:
def is_managed_env() -> bool:
- """True when the current process is already running inside the app venv."""
+ """True when the current process is already running inside the app venv.
+
+ Compares sys.prefix (the environment the current interpreter belongs to)
+ with ENV_DIR instead of the interpreter path: a venv's bin/python is
+ typically a symlink to the base interpreter, so resolving sys.executable
+ would also make the bare system python look like the managed env and
+ silently skip the bootstrap (and with it the requirements install).
+ """
try:
- return Path(sys.executable).resolve() == env_python().resolve()
+ return Path(sys.prefix).resolve() == ENV_DIR.resolve()
except OSError:
return False
diff --git a/app/tests/test_backends_envs.py b/app/tests/test_backends_envs.py
index 2f0f910..d3b35c1 100644
--- a/app/tests/test_backends_envs.py
+++ b/app/tests/test_backends_envs.py
@@ -1,7 +1,9 @@
"""Tests for the managed Python environment (backends/envs.py)."""
import json
+import os
import sys
+import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
@@ -63,15 +65,34 @@ class EnvPathTests(unittest.TestCase):
return_value=Path("/no/such/path/python")):
self.assertFalse(envs.env_exists())
- def test_is_managed_env_compares_resolved_executable(self):
- fake_env_python = Path("/tmp/opencode/managed-env/bin/python")
- with patch.object(envs, "env_python", return_value=fake_env_python), \
- patch.object(sys, "executable", str(fake_env_python)):
+ def test_is_managed_env_compares_prefix_to_env_dir(self):
+ # sys.prefix is the environment the current interpreter belongs to:
+ # the env dir for its python, the base install (/usr, ...) otherwise.
+ with patch.object(envs, "ENV_DIR", Path(sys.prefix)):
self.assertTrue(envs.is_managed_env())
- with patch.object(envs, "env_python", return_value=fake_env_python), \
- patch.object(sys, "executable", "/usr/bin/python3"):
+ with patch.object(envs, "ENV_DIR",
+ Path(tempfile.gettempdir()) / "managed-env"):
self.assertFalse(envs.is_managed_env())
+ def test_is_managed_env_not_fooled_by_symlinked_venv_python(self):
+ # Regression: a venv's bin/python is a symlink to the base
+ # interpreter, so resolving sys.executable made the bare system
+ # python compare equal to env_python() and pass as the managed env —
+ # silently skipping the bootstrap (and the requirements install).
+ # The check must use sys.prefix, which stays the base install's.
+ if sys.platform == "win32":
+ self.skipTest("requires POSIX symlinks")
+ with tempfile.TemporaryDirectory() as tmp:
+ env_dir = Path(tmp) / "tts"
+ bin_dir = env_dir / "bin"
+ bin_dir.mkdir(parents=True)
+ os.symlink(sys.executable, bin_dir / "python")
+ with patch.object(envs, "ENV_DIR", env_dir):
+ # The bare system interpreter: sys.executable resolves to
+ # exactly what the venv python symlink points at, yet its
+ # prefix is not the env dir, so this must stay False.
+ self.assertFalse(envs.is_managed_env())
+
class CreateEnvTests(unittest.TestCase):
def test_create_env_invokes_venv_module(self):