176 lines
4.8 KiB
Python
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)
|
|
# )
|
|
|