#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa # See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html) # Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> """Utilities for running vendored EPUB->TTS repositories in isolated venvs.""" import subprocess import shutil import sys import venv from pathlib import Path from typing import Optional, Union from collections.abc import MutableMapping from . import logger, PathLike def path(fpath: str|Path = None): return Path(fpath).resolve() class DataDict(dict): """Wrapper around dictionary objects for easier dict subclassing See module [collections](https://github.com/python/cpython/blob/3.14/Lib/collections/__init__.py#L1133) """ def __init__(self, *args, **kwargs): self._data = {} if args is not None: if isinstance(args, list): for a in args: if not isinstance(a,tuple) or len(a) != 2: # list is not a list of (k,v) tuples self._args = args break # all list items are (k,v) tuples self.update(args) self._args = args if kwargs: self.update(kwargs) # Emulate accessing class attributes def __getattr__(self, key): # This gets called only if __getattribute__ doesn't find key return self._data[key] def __setattr__(self, key, value): if key.startswith("_"): return super().__setattr__ (key, value) return self._data.__setitem__(key, value) def __delattr__(self, key): if key.startswith("_"): return super().__delattr__(key) return self._data.__delitem__(key) # Emulate dict methods def __len__(self): return len(self._data) def __getitem__(self, key): if key in self._data: return self._data[key] if hasattr(self.__class__, "__missing__"): return self.__class__.__missing__(self, key) raise KeyError(key) def __setitem__(self, key, value): self._data[key] = value def __delitem__(self, key): del self._data[key] def __iter__(self): return iter(self._data) def __contains__(self, key): return key in self._data def get(self, key, default=None): if key in self: return self[key] return default def __repr__(self): return repr(self._data) def __or__(self, other): return self._data | other def __ror__(self, other): return other | self._data def __ior__(self, other): self._data |= other return self def __copy__(self): # TODO: verify if this is correct return dict(self._data) def copy(self): import copy return copy.copy(self._data) @classmethod def fromkeys(cls, iterable, value=None): d = cls() for key in iterable: d[key] = value return d def get_vendor_root() -> Path: return Path(__file__).resolve().parents[1] / "vendor" def get_repo_path(repo_name: str) -> Path: repo_path = get_vendor_root() / repo_name if not repo_path.exists(): raise FileNotFoundError( f"Vendored repository '{repo_name}' not found under {get_vendor_root()}" ) return repo_path def get_venv_path(repo_name: str) -> Path: return get_repo_path(repo_name) / ".venv" def get_venv_python(repo_name: str) -> Path: venv_path = get_venv_path(repo_name) python_executable = ( venv_path / "Scripts" / "python.exe" if sys.platform == "win32" else venv_path / "bin" / "python" ) if not python_executable.exists(): raise FileNotFoundError( f"Python executable not found in virtualenv at {python_executable}" ) return python_executable def ensure_venv(repo_name: str) -> Path: repo_path = get_repo_path(repo_name) venv_path = get_venv_path(repo_name) if not venv_path.exists(): venv.EnvBuilder(with_pip=True).create(venv_path) python_executable = get_venv_python(repo_name) subprocess.run( [str(python_executable), "-m", "pip", "install", "-e", str(repo_path)], check=True, ) return get_venv_python(repo_name) def build_run_command( repo_name: str, # backend_cmd: str = None, ) -> str: return str(ensure_venv(repo_name)) # if not backend_cmd: # return str(ensure_venv(repo_name)) # ensure_venv(repo_name) # venv_path = get_venv_path(repo_name) # return ( # str( venv_path / "Scripts" / f"{backend_cmd}.exe") # if sys.platform == "win32" # else str(venv_path / "bin" / backend_cmd) # )