From 4d8f5fd826276b59a73a4851dcc2fbb388a0d574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 18 Sep 2024 20:39:24 -0700 Subject: [PATCH] Separate list dependencies to a separate installer class (#3347) --- docs/changelog/3347.feature.rst | 2 + src/tox/tox_env/python/pip/pip_install.py | 50 ++++++++++------ src/tox/tox_env/python/runner.py | 69 ++++++++++++++--------- 3 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 docs/changelog/3347.feature.rst diff --git a/docs/changelog/3347.feature.rst b/docs/changelog/3347.feature.rst new file mode 100644 index 000000000..06cf93270 --- /dev/null +++ b/docs/changelog/3347.feature.rst @@ -0,0 +1,2 @@ +Separate the list dependencies functionality to a separate abstract class allowing code reuse in plugins (such as +``tox-uv``) - by :gaborbernat`. diff --git a/src/tox/tox_env/python/pip/pip_install.py b/src/tox/tox_env/python/pip/pip_install.py index 38b39bd6f..22e09f3a1 100644 --- a/src/tox/tox_env/python/pip/pip_install.py +++ b/src/tox/tox_env/python/pip/pip_install.py @@ -2,6 +2,7 @@ import logging import operator +from abc import ABC, abstractmethod from collections import defaultdict from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Sequence @@ -21,14 +22,36 @@ from tox.tox_env.package import PathPackage -class Pip(Installer[Python]): - """Pip is a python installer that can install packages as defined by PEP-508 and PEP-517.""" - +class PythonInstallerListDependencies(Installer[Python], ABC): def __init__(self, tox_env: Python, with_list_deps: bool = True) -> None: # noqa: FBT001, FBT002 self._with_list_deps = with_list_deps super().__init__(tox_env) def _register_config(self) -> None: + if self._with_list_deps: # pragma: no branch + self._env.conf.add_config( + keys=["list_dependencies_command"], + of_type=Command, + default=Command(self.freeze_cmd()), + desc="command used to list installed packages", + ) + + @abstractmethod + def freeze_cmd(self) -> list[str]: + raise NotImplementedError + + def installed(self) -> list[str]: + cmd: Command = self._env.conf["list_dependencies_command"] + result = self._env.execute(cmd=cmd.args, stdin=StdinSource.OFF, run_id="freeze", show=False) + result.assert_success() + return result.out.splitlines() + + +class Pip(PythonInstallerListDependencies): + """Pip is a python installer that can install packages as defined by PEP-508 and PEP-517.""" + + def _register_config(self) -> None: + super()._register_config() self._env.conf.add_config( keys=["pip_pre"], of_type=bool, @@ -54,13 +77,9 @@ def _register_config(self) -> None: default=False, desc="Use the exact versions of installed deps as constraints, otherwise use the listed deps.", ) - if self._with_list_deps: # pragma: no branch - self._env.conf.add_config( - keys=["list_dependencies_command"], - of_type=Command, - default=Command(["python", "-m", "pip", "freeze", "--all"]), - desc="command used to list installed packages", - ) + + def freeze_cmd(self) -> list[str]: # noqa: PLR6301 + return ["python", "-m", "pip", "freeze", "--all"] def default_install_command(self, conf: Config, env_name: str | None) -> Command: # noqa: ARG002 isolated_flag = "-E" if self._env.base_python.version_info.major == 2 else "-I" # noqa: PLR2004 @@ -82,12 +101,6 @@ def post_process_install_command(self, cmd: Command) -> Command: install_command.pop(opts_at) return cmd - def installed(self) -> list[str]: - cmd: Command = self._env.conf["list_dependencies_command"] - result = self._env.execute(cmd=cmd.args, stdin=StdinSource.OFF, run_id="freeze", show=False) - result.assert_success() - return result.out.splitlines() - def install(self, arguments: Any, section: str, of_type: str) -> None: if isinstance(arguments, PythonDeps): self._install_requirement_file(arguments, section, of_type) @@ -239,4 +252,7 @@ def build_install_cmd(self, args: Sequence[str]) -> list[str]: return install_command[:opts_at] + list(args) + install_command[opts_at + 1 :] -__all__ = ("Pip",) +__all__ = [ + "Pip", + "PythonInstallerListDependencies", +] diff --git a/src/tox/tox_env/python/runner.py b/src/tox/tox_env/python/runner.py index e83266b87..336476efc 100644 --- a/src/tox/tox_env/python/runner.py +++ b/src/tox/tox_env/python/runner.py @@ -2,6 +2,7 @@ from __future__ import annotations +from abc import ABC from functools import partial from typing import TYPE_CHECKING, Set @@ -16,11 +17,13 @@ from .api import Python if TYPE_CHECKING: + from tox.config.cli.parser import Parsed + from tox.config.sets import CoreConfigSet, EnvConfigSet from tox.tox_env.api import ToxEnvCreateArgs from tox.tox_env.package import Package -class PythonRun(Python, RunToxEnv): +class PythonRun(Python, RunToxEnv, ABC): def __init__(self, create_args: ToxEnvCreateArgs) -> None: super().__init__(create_args) @@ -34,19 +37,7 @@ def register_config(self) -> None: default=PythonDeps("", root), desc="Name of the python dependencies as specified by PEP-440", ) - - def skip_missing_interpreters_post_process(value: bool) -> bool: # noqa: FBT001 - if getattr(self.options, "skip_missing_interpreters", "config") != "config": - return StrConvert().to_bool(self.options.skip_missing_interpreters) - return value - - self.core.add_config( - keys=["skip_missing_interpreters"], - default=True, - of_type=bool, - post_process=skip_missing_interpreters_post_process, - desc="skip running missing interpreters", - ) + add_skip_missing_interpreters_to_core(self.core, self.options) @property def _package_types(self) -> tuple[str, ...]: @@ -77,18 +68,7 @@ def _register_package_conf(self) -> bool: if pkg_type == "skip": return False - def _normalize_extras(values: set[str]) -> set[str]: - # although _ and . is allowed this will be normalized during packaging to - - # https://packaging.python.org/en/latest/specifications/dependency-specifiers/#grammar - return {canonicalize_name(v) for v in values} - - self.conf.add_config( - keys=["extras"], - of_type=Set[str], - default=set(), - desc="extras to install of the target package", - post_process=_normalize_extras, - ) + add_extras_to_env(self.conf) return True @property @@ -122,3 +102,40 @@ def _build_packages(self) -> list[Package]: msg = f"{exception.args[0]} for package environment {package_env.conf['env_name']}" raise Skip(msg) from exception return packages + + +def add_skip_missing_interpreters_to_core(core: CoreConfigSet, options: Parsed) -> None: + def skip_missing_interpreters_post_process(value: bool) -> bool: # noqa: FBT001 + if getattr(options, "skip_missing_interpreters", "config") != "config": + return StrConvert().to_bool(options.skip_missing_interpreters) + return value + + core.add_config( + keys=["skip_missing_interpreters"], + default=True, + of_type=bool, + post_process=skip_missing_interpreters_post_process, + desc="skip running missing interpreters", + ) + + +def add_extras_to_env(conf: EnvConfigSet) -> None: + def _normalize_extras(values: set[str]) -> set[str]: + # although _ and . is allowed this will be normalized during packaging to - + # https://packaging.python.org/en/latest/specifications/dependency-specifiers/#grammar + return {canonicalize_name(v) for v in values} + + conf.add_config( + keys=["extras"], + of_type=Set[str], + default=set(), + desc="extras to install of the target package", + post_process=_normalize_extras, + ) + + +__all__ = [ + "PythonRun", + "add_extras_to_env", + "add_skip_missing_interpreters_to_core", +]