Source code for rics.jupyter._venv_helper

import logging
import subprocess
import sys
from pathlib import Path
from typing import Any

from rics.logs import get_logger


[docs] class VenvHelper: """Helper class for working with virtual environments.""" def __init__( self, logger: logging.Logger | str = __package__ + ".VenvHelper", ) -> None: self.logger = get_logger(logger) manager, exec_prefix, executable = self.resolve() self._manager = manager self.exec_prefix = exec_prefix self.executable = executable self.config_path = Path(self.exec_prefix) / "pyvenv.cfg" self._config = self._read_pyvenv_cfg() @property def manager(self) -> str: """Manager name.""" return self._manager @property def config(self) -> dict[str, Any]: """Parsed `pyvenv.cfg` dict.""" return {**self._config} @property def slug(self) -> str: """Virtual environment slug. Typically, the shell prompt.""" return self._config.get("prompt") or self.config_path.parent.parent.name
[docs] def resolve(self) -> tuple[str, str, str]: """Resolve venv paths. Returns: A tuple ``(name, exec_prefix, executable)``. """ logger = self.logger.getChild("resolve") resolvers = [Resolve.poetry, Resolve.uv, Resolve.sys] logger.debug(f"Resolver order ({len(resolvers)}): {' | '.join(r.__name__ for r in resolvers)}.") for i, resolver in enumerate(resolvers, start=1): name = resolver.__name__ msg = f"Resolver {i}/{len(resolvers)}: {name}(): %s." logger.debug(msg, "Starting") result = resolver() if result: logger.debug(msg, f"returned {result=}") return name, *result logger.debug(msg, "Failed") raise RuntimeError("exec resolution failed")
def _read_pyvenv_cfg(self) -> dict[str, str]: self.logger.debug("Reading pyvenv.cfg for manager='%s': '%s'.", self.manager, self.config_path) if not self.config_path.is_file(): msg = f"Not a virtualenv: '{self.executable}'." raise RuntimeError(msg) lines = self.config_path.read_text().splitlines() rv = {key: value for key, value in map(self._convert_pyenvcfg_line, lines)} self.logger.debug("Found %i keys in pyvenv.cfg: %s.", len(rv), [*rv]) return rv @classmethod def _convert_pyenvcfg_line(cls, line: str) -> tuple[str, str]: key, _, value = line.partition(" = ") return key, value
class Resolve: """Get ``(exec_prefix, executable)``-tuples for various package managers.""" @classmethod def poetry(cls) -> tuple[str, str] | None: """Implementation for https://github.com/python-poetry/poetry.""" exec_prefix = _get_output("poetry", "env", "info", "-p") executable = _get_output("poetry", "env", "info", "-e") return (exec_prefix, executable) if exec_prefix and executable else None @classmethod def uv(cls) -> None: """Implementation for https://github.com/astral-sh/uv. Always returns ``None``.""" return None # TODO(rs): uv @classmethod def sys(cls) -> tuple[str, str]: return sys.exec_prefix, sys.executable def _get_output(*args: str) -> str | None: try: process = subprocess.run(args, capture_output=True, check=False) # noqa: S603 except OSError: return None if process.returncode: return None return process.stdout.decode().strip()