summaryrefslogtreecommitdiff
path: root/src/tox/tox_env/python/package.py
blob: 289bd7744718720d84c776b9f0eacd9e76b5db04 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
"""
A tox build environment that handles Python packages.
"""
from __future__ import annotations

from abc import ABC, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING, Any, Generator, Iterator, List, Sequence, cast

from packaging.requirements import Requirement

from ...config.sets import EnvConfigSet
from ..api import ToxEnvCreateArgs
from ..errors import Skip
from ..package import Package, PackageToxEnv, PathPackage
from ..runner import RunToxEnv
from .api import Python
from .pip.req_file import PythonDeps

if TYPE_CHECKING:
    from tox.config.main import Config


class PythonPackage(Package):
    """python package"""


class PythonPathPackageWithDeps(PathPackage):
    def __init__(self, path: Path, deps: Sequence[Any]) -> None:
        super().__init__(path=path)
        self.deps: Sequence[Package] = deps


class WheelPackage(PythonPathPackageWithDeps):
    """wheel package"""


class SdistPackage(PythonPathPackageWithDeps):
    """sdist package"""


class EditableLegacyPackage(PythonPathPackageWithDeps):
    """legacy editable package"""


class EditablePackage(PythonPathPackageWithDeps):
    """PEP-660 editable package"""


class PythonPackageToxEnv(Python, PackageToxEnv, ABC):
    def __init__(self, create_args: ToxEnvCreateArgs) -> None:
        self._wheel_build_envs: dict[str, PythonPackageToxEnv] = {}
        super().__init__(create_args)

    def _setup_env(self) -> None:
        """setup the tox environment"""
        super()._setup_env()
        self._install(self.requires(), PythonPackageToxEnv.__name__, "requires")
        self._install(self.conf["deps"], PythonPackageToxEnv.__name__, "deps")

    @abstractmethod
    def requires(self) -> tuple[Requirement, ...] | PythonDeps:
        raise NotImplementedError

    def register_run_env(self, run_env: RunToxEnv) -> Generator[tuple[str, str], PackageToxEnv, None]:
        yield from super().register_run_env(run_env)
        if run_env.conf["package"] != "skip" and "deps" not in self.conf:
            self.conf.add_config(
                keys="deps",
                of_type=List[Requirement],
                default=[],
                desc="Name of the python dependencies as specified by PEP-440",
            )

        if (
            not isinstance(run_env, Python)
            or run_env.conf["package"] not in {"wheel", "editable"}
            or "wheel_build_env" in run_env.conf
        ):
            return

        def default_wheel_tag(conf: Config, env_name: str | None) -> str:  # noqa: U100
            # https://www.python.org/dev/peps/pep-0427/#file-name-convention
            # when building wheels we need to ensure that the built package is compatible with the target env
            # compatibility is documented within https://www.python.org/dev/peps/pep-0427/#file-name-convention
            # a wheel tag example: {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
            # python only code are often compatible at major level (unless universal wheel in which case both 2/3)
            # c-extension codes are trickier, but as of today both poetry/setuptools uses pypa/wheels logic
            # https://github.com/pypa/wheel/blob/master/src/wheel/bdist_wheel.py#L234-L280
            run_py = cast(Python, run_env).base_python
            if run_py is None:
                base = ",".join(run_env.conf["base_python"])
                raise Skip(f"could not resolve base python with {base}")

            default_pkg_py = self.base_python
            if (
                default_pkg_py.version_no_dot == run_py.version_no_dot
                and default_pkg_py.impl_lower == run_py.impl_lower
            ):
                return self.conf.name

            return f"{self.conf.name}-{run_py.impl_lower}{run_py.version_no_dot}"

        run_env.conf.add_config(
            keys=["wheel_build_env"],
            of_type=str,
            default=default_wheel_tag,
            desc="wheel tag to use for building applications",
        )
        pkg_env = run_env.conf["wheel_build_env"]
        result = yield pkg_env, run_env.conf["package_tox_env_type"]
        self._wheel_build_envs[pkg_env] = cast(PythonPackageToxEnv, result)

    def child_pkg_envs(self, run_conf: EnvConfigSet) -> Iterator[PackageToxEnv]:
        if run_conf["package"] == "wheel":
            env = self._wheel_build_envs.get(run_conf["wheel_build_env"])
            if env is not None and env.name != self.name:
                yield env

    def _teardown(self) -> None:
        for env in self._wheel_build_envs.values():
            if env is not self:
                with env.display_context(self._has_display_suspended):
                    env.teardown()
        super()._teardown()