Files
epub-tts/src/epub_tts/utils.py
T

176 lines
4.8 KiB
Python

#!/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)
# )