From ec4ebfbd8636a9e3b9f9f5d64eff2acc275b68b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Fri, 9 Sep 2022 17:56:05 -0700 Subject: [PATCH 1/2] Use PEP-621 to load project dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor --- docs/changelog/2499.feature.rst | 1 + pyproject.toml | 2 +- src/tox/config/source/legacy_toml.py | 9 ++- src/tox/execute/pep517_backend.py | 2 +- src/tox/plugin/manager.py | 4 +- src/tox/provision.py | 3 +- src/tox/pytest.py | 2 +- src/tox/session/cmd/run/single.py | 2 +- .../python/virtual_env/package/cmd_builder.py | 2 +- .../package/{pep517.py => pyproject.py} | 65 +++++++++++++++---- .../python/virtual_env/test_setuptools.py | 2 +- whitelist.txt | 8 +-- 12 files changed, 73 insertions(+), 29 deletions(-) create mode 100644 docs/changelog/2499.feature.rst rename src/tox/tox_env/python/virtual_env/package/{pep517.py => pyproject.py} (85%) diff --git a/docs/changelog/2499.feature.rst b/docs/changelog/2499.feature.rst new file mode 100644 index 000000000..4018b6f44 --- /dev/null +++ b/docs/changelog/2499.feature.rst @@ -0,0 +1 @@ +Support PEP-621 static metadata for getting package dependencies - by :user:`gaborbernat`. diff --git a/pyproject.toml b/pyproject.toml index d2ef1f1f9..c7b3ef082 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "platformdirs>=2.5.2", "pluggy>=1", "pyproject-api>=0.1.1", - "tomli>=2.0.1", + 'tomli>=2.0.1;python_version<"3.11"', "virtualenv>=20.16.5", 'importlib-metadata>=4.12; python_version < "3.8"', 'typing-extensions>=4.3; python_version < "3.8"', diff --git a/src/tox/config/source/legacy_toml.py b/src/tox/config/source/legacy_toml.py index 46be3da19..32ecab4a9 100644 --- a/src/tox/config/source/legacy_toml.py +++ b/src/tox/config/source/legacy_toml.py @@ -1,8 +1,13 @@ from __future__ import annotations +import sys from pathlib import Path -import tomli +if sys.version_info >= (3, 11): # pragma: no cover (py311+) + import tomllib +else: # pragma: no cover (py311+) + import tomli as tomllib + from .ini import IniSource @@ -14,7 +19,7 @@ def __init__(self, path: Path): if path.name != self.FILENAME or not path.exists(): raise ValueError with path.open("rb") as file_handler: - toml_content = tomli.load(file_handler) + toml_content = tomllib.load(file_handler) try: content = toml_content["tool"]["tox"]["legacy_tox_ini"] except KeyError: diff --git a/src/tox/execute/pep517_backend.py b/src/tox/execute/pep517_backend.py index 97d99fe43..d37db49e6 100644 --- a/src/tox/execute/pep517_backend.py +++ b/src/tox/execute/pep517_backend.py @@ -59,7 +59,7 @@ def local_execute(self, options: ExecuteOptions) -> tuple[LocalSubProcessExecute self.is_alive = True break if b"failed to start backend" in status.err: - from tox.tox_env.python.virtual_env.package.pep517 import ToxBackendFailed + from tox.tox_env.python.virtual_env.package.pyproject import ToxBackendFailed failure = BackendFailed( result={ diff --git a/src/tox/plugin/manager.py b/src/tox/plugin/manager.py index 2ac90d9d4..14f28b8fa 100644 --- a/src/tox/plugin/manager.py +++ b/src/tox/plugin/manager.py @@ -13,7 +13,7 @@ from tox.session.cmd.run import parallel, sequential from tox.tox_env import package as package_api from tox.tox_env.python.virtual_env import runner -from tox.tox_env.python.virtual_env.package import cmd_builder, pep517 +from tox.tox_env.python.virtual_env.package import cmd_builder, pyproject from tox.tox_env.register import REGISTER, ToxEnvRegister from ..execute import Outcome @@ -39,7 +39,7 @@ def _register_plugins(self, inline: ModuleType | None) -> None: loader_api, provision, runner, - pep517, + pyproject, cmd_builder, legacy, version_flag, diff --git a/src/tox/provision.py b/src/tox/provision.py index 9b43ce2b2..95273fe4a 100644 --- a/src/tox/provision.py +++ b/src/tox/provision.py @@ -58,11 +58,12 @@ def tox_add_option(parser: ArgumentParser) -> None: def provision(state: State) -> int | bool: + # remove the dev and marker to allow local development of the package state.conf.core.add_config( keys=["min_version", "minversion"], of_type=Version, # do not include local version specifier (because it's not allowed in version spec per PEP-440) - default=Version(current_version.split("+")[0]), + default=Version(current_version), desc="Define the minimal tox version required to run", ) state.conf.core.add_config( diff --git a/src/tox/pytest.py b/src/tox/pytest.py index 6ae122b05..b9bb65f26 100644 --- a/src/tox/pytest.py +++ b/src/tox/pytest.py @@ -314,7 +314,7 @@ def enable_pep517_backend_coverage() -> Iterator[None]: # noqa: PT004 yield # pragma: no cover return # pragma: no cover # the COV_ env variables needs to be passed on for the PEP-517 backend - from tox.tox_env.python.virtual_env.package.pep517 import Pep517VirtualEnvPackager + from tox.tox_env.python.virtual_env.package.pyproject import Pep517VirtualEnvPackager def default_pass_env(self: Pep517VirtualEnvPackager) -> list[str]: result = previous(self) diff --git a/src/tox/session/cmd/run/single.py b/src/tox/session/cmd/run/single.py index d4c8a024b..3406a39c3 100644 --- a/src/tox/session/cmd/run/single.py +++ b/src/tox/session/cmd/run/single.py @@ -12,7 +12,7 @@ from tox.execute.api import Outcome, StdinSource from tox.tox_env.api import ToxEnv from tox.tox_env.errors import Fail, Skip -from tox.tox_env.python.virtual_env.package.pep517 import ToxBackendFailed +from tox.tox_env.python.virtual_env.package.pyproject import ToxBackendFailed from tox.tox_env.runner import RunToxEnv LOGGER = logging.getLogger(__name__) diff --git a/src/tox/tox_env/python/virtual_env/package/cmd_builder.py b/src/tox/tox_env/python/virtual_env/package/cmd_builder.py index e83f6ffdc..d43e2d1d8 100644 --- a/src/tox/tox_env/python/virtual_env/package/cmd_builder.py +++ b/src/tox/tox_env/python/virtual_env/package/cmd_builder.py @@ -27,7 +27,7 @@ from tox.tox_env.register import ToxEnvRegister from tox.tox_env.runner import RunToxEnv -from .pep517 import Pep517VirtualEnvPackager +from .pyproject import Pep517VirtualEnvPackager from .util import dependencies_with_extras if sys.version_info >= (3, 8): # pragma: no cover (py38+) diff --git a/src/tox/tox_env/python/virtual_env/package/pep517.py b/src/tox/tox_env/python/virtual_env/package/pyproject.py similarity index 85% rename from src/tox/tox_env/python/virtual_env/package/pep517.py rename to src/tox/tox_env/python/virtual_env/package/pyproject.py index 5aba6d320..897772333 100644 --- a/src/tox/tox_env/python/virtual_env/package/pep517.py +++ b/src/tox/tox_env/python/virtual_env/package/pyproject.py @@ -30,6 +30,12 @@ from importlib.metadata import Distribution, PathDistribution else: # pragma: no cover (= (3, 11): # pragma: no cover (py311+) + import tomllib +else: # pragma: no cover (py311+) + import tomli as tomllib + ConfigSettings = Optional[Dict[str, Any]] @@ -143,23 +149,14 @@ def _teardown(self) -> None: def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]: """build the package to install""" + deps = self._load_deps(for_env) of_type: str = for_env["package"] - - reqs: list[Requirement] | None = None - if of_type == "wheel": - w_env = self._wheel_build_envs.get(for_env["wheel_build_env"]) - if w_env is not None and w_env is not self: - with w_env.display_context(self._has_display_suspended): - reqs = w_env.get_package_dependencies() if isinstance(w_env, Pep517VirtualEnvPackager) else [] - if reqs is None: - reqs = self.get_package_dependencies() - - extras: set[str] = for_env["extras"] - deps = dependencies_with_extras(reqs, extras) if of_type == "dev-legacy": + self.setup() deps = [*self.requires(), *self._frontend.get_requires_for_build_sdist().requires] + deps package: Package = DevLegacyPackage(self.core["tox_root"], deps) # the folder itself is the package elif of_type == "sdist": + self.setup() with self._pkg_lock: package = SdistPackage(self._frontend.build_sdist(sdist_directory=self.pkg_dir).sdist, deps) elif of_type == "wheel": @@ -168,6 +165,7 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]: with w_env.display_context(self._has_display_suspended): return w_env.perform_packaging(for_env) else: + self.setup() with self._pkg_lock: path = self._frontend.build_wheel( wheel_directory=self.pkg_dir, @@ -179,6 +177,49 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]: raise TypeError(f"cannot handle package type {of_type}") # pragma: no cover return [package] + def _load_deps(self, for_env: EnvConfigSet) -> list[Requirement]: + # first check if this is statically available via PEP-621 + deps = self._load_deps_from_static(for_env) + if deps is None: + deps = self._load_deps_from_built_metadata(for_env) + return deps + + def _load_deps_from_static(self, for_env: EnvConfigSet) -> list[Requirement] | None: + pyproject_file = self.core["package_root"] / "pyproject.toml" + if not pyproject_file.exists(): # check if it's static PEP-621 metadata + return None + with pyproject_file.open("rb") as file_handler: + pyproject = tomllib.load(file_handler) + project = pyproject.get("project") + if project is None: + return None # is not a PEP-621 pyproject + extras: set[str] = for_env["extras"] + for dynamic in project.get("dynamic"): + if dynamic == "dependencies" or (extras and dynamic == "optional-dependencies"): + return None # if any dependencies are dynamic we can just calculate all dynamically + + deps: list[Requirement] = [Requirement(i) for i in project.get("dependencies", [])] + optional_deps = project.get("optional-dependencies", {}) + for extra in extras: + deps.extend(Requirement(i) for i in optional_deps.get(extra, [])) + return deps + + def _load_deps_from_built_metadata(self, for_env: EnvConfigSet) -> list[Requirement]: + # dependencies might depend on the python environment we're running in => if we build a wheel use that env + # to calculate the package metadata, otherwise ourselves + of_type: str = for_env["package"] + reqs: list[Requirement] | None = None + if of_type == "wheel": # wheel packages + w_env = self._wheel_build_envs.get(for_env["wheel_build_env"]) + if w_env is not None and w_env is not self: + with w_env.display_context(self._has_display_suspended): + reqs = w_env.get_package_dependencies() if isinstance(w_env, Pep517VirtualEnvPackager) else [] + if reqs is None: + reqs = self.get_package_dependencies() + extras: set[str] = for_env["extras"] + deps = dependencies_with_extras(reqs, extras) + return deps + def get_package_dependencies(self) -> list[Requirement]: with self._pkg_lock: if self._package_dependencies is None: # pragma: no branch diff --git a/tests/tox_env/python/virtual_env/test_setuptools.py b/tests/tox_env/python/virtual_env/test_setuptools.py index 71b981bbb..2e8ebc2b0 100644 --- a/tests/tox_env/python/virtual_env/test_setuptools.py +++ b/tests/tox_env/python/virtual_env/test_setuptools.py @@ -9,7 +9,7 @@ from tox.pytest import ToxProjectCreator from tox.tox_env.python.package import WheelPackage -from tox.tox_env.python.virtual_env.package.pep517 import Pep517VirtualEnvPackager +from tox.tox_env.python.virtual_env.package.pyproject import Pep517VirtualEnvPackager from tox.tox_env.runner import RunToxEnv diff --git a/whitelist.txt b/whitelist.txt index 899e3274e..7bd825949 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -42,7 +42,6 @@ creq crypto ctrl cygwin -dedent deinit delenv dep @@ -143,7 +142,6 @@ objtype ov param parsers -pathlib pathname pathsep pep517 @@ -160,6 +158,7 @@ prog proj psutil purelib +py311 py38 py39 pygments @@ -183,9 +182,7 @@ rfind rpartition rreq rst -runtime sdist -setdefault setenv setitem shlex @@ -206,11 +203,11 @@ sysconfig teardown termux testenv -textwrap tmp tmpdir toml tomli +tomllib towncrier tox transcoding @@ -227,7 +224,6 @@ usedevelop usefixtures util utils -v3 vcs ver virtualenv From d9586c5eed773b1987d7ab0576ccca9d4eddbe88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sat, 10 Sep 2022 00:03:21 -0700 Subject: [PATCH 2/2] Add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor --- .../python/virtual_env/package/pyproject.py | 6 +- tests/demo_pkg_inline/build.py | 2 + .../package/test_package_pep517.py | 61 ------- .../package/test_package_pyproject.py | 171 ++++++++++++++++++ 4 files changed, 176 insertions(+), 64 deletions(-) delete mode 100644 tests/tox_env/python/virtual_env/package/test_package_pep517.py create mode 100644 tests/tox_env/python/virtual_env/package/test_package_pyproject.py diff --git a/src/tox/tox_env/python/virtual_env/package/pyproject.py b/src/tox/tox_env/python/virtual_env/package/pyproject.py index 897772333..e41eae44b 100644 --- a/src/tox/tox_env/python/virtual_env/package/pyproject.py +++ b/src/tox/tox_env/python/virtual_env/package/pyproject.py @@ -190,11 +190,11 @@ def _load_deps_from_static(self, for_env: EnvConfigSet) -> list[Requirement] | N return None with pyproject_file.open("rb") as file_handler: pyproject = tomllib.load(file_handler) - project = pyproject.get("project") - if project is None: + if "project" not in pyproject: return None # is not a PEP-621 pyproject + project = pyproject["project"] extras: set[str] = for_env["extras"] - for dynamic in project.get("dynamic"): + for dynamic in project.get("dynamic", []): if dynamic == "dependencies" or (extras and dynamic == "optional-dependencies"): return None # if any dependencies are dynamic we can just calculate all dynamically diff --git a/tests/demo_pkg_inline/build.py b/tests/demo_pkg_inline/build.py index f91f633fd..35e22f03a 100644 --- a/tests/demo_pkg_inline/build.py +++ b/tests/demo_pkg_inline/build.py @@ -28,12 +28,14 @@ Author: UNKNOWN Author-email: UNKNOWN License: UNKNOWN + {} Platform: UNKNOWN UNKNOWN """.format( pkg_name, version, + "\n ".join(os.environ.get("METADATA_EXTRA", "").split("\n")), ), wheel: """ Wheel-Version: 1.0 diff --git a/tests/tox_env/python/virtual_env/package/test_package_pep517.py b/tests/tox_env/python/virtual_env/package/test_package_pep517.py deleted file mode 100644 index 1db6837f7..000000000 --- a/tests/tox_env/python/virtual_env/package/test_package_pep517.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from tox.pytest import ToxProjectCreator - - -@pytest.mark.parametrize( - "pkg_type", - ["dev-legacy", "sdist", "wheel"], -) -def test_tox_ini_package_type_valid(tox_project: ToxProjectCreator, pkg_type: str) -> None: - proj = tox_project({"tox.ini": f"[testenv]\npackage={pkg_type}", "pyproject.toml": ""}) - result = proj.run("c", "-k", "package_tox_env_type") - result.assert_success() - res = result.env_conf("py")["package"] - assert res == pkg_type - got_type = result.env_conf("py")["package_tox_env_type"] - assert got_type == "virtualenv-pep-517" - - -def test_tox_ini_package_type_invalid(tox_project: ToxProjectCreator) -> None: - proj = tox_project({"tox.ini": "[testenv]\npackage=bad", "pyproject.toml": ""}) - result = proj.run("c", "-k", "package_tox_env_type") - result.assert_failed() - assert " invalid package config type bad requested, must be one of wheel, sdist, dev-legacy, skip" in result.out - - -def test_get_package_deps_different_extras(pkg_with_extras_project: Path, tox_project: ToxProjectCreator) -> None: - ini = "[testenv:a]\npackage=dev-legacy\nextras=docs\n[testenv:b]\npackage=sdist\nextras=format" - proj = tox_project({"tox.ini": ini}) - execute_calls = proj.patch_execute(lambda r: 0 if "install" in r.run_id else None) - result = proj.run("r", "--root", str(pkg_with_extras_project), "-e", "a,b") - result.assert_success() - installs = { - i[0][0].conf.name: i[0][3].cmd[5:] - for i in execute_calls.call_args_list - if i[0][3].run_id.startswith("install_package_deps") - } - assert installs == { - "a": ["colorama>=0.4.3", "platformdirs>=2.1", "setuptools", "sphinx-rtd-theme<1,>=0.4.3", "sphinx>=3", "wheel"], - "b": ["black>=3", "colorama>=0.4.3", "flake8", "platformdirs>=2.1"], - } - - -def test_package_root_via_root(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None: - ini = f"[tox]\npackage_root={demo_pkg_inline}\n[testenv]\npackage=wheel\nwheel_build_env=.pkg" - proj = tox_project({"tox.ini": ini, "pyproject.toml": ""}) - proj.patch_execute(lambda r: 0 if "install" in r.run_id else None) - result = proj.run("r", "--notest") - result.assert_success() - - -def test_package_root_via_testenv(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None: - ini = f"[testenv]\npackage=wheel\nwheel_build_env=.pkg\npackage_root={demo_pkg_inline}" - proj = tox_project({"tox.ini": ini, "pyproject.toml": ""}) - proj.patch_execute(lambda r: 0 if "install" in r.run_id else None) - result = proj.run("r", "--notest") - result.assert_success() diff --git a/tests/tox_env/python/virtual_env/package/test_package_pyproject.py b/tests/tox_env/python/virtual_env/package/test_package_pyproject.py new file mode 100644 index 000000000..22ecaac54 --- /dev/null +++ b/tests/tox_env/python/virtual_env/package/test_package_pyproject.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from tox.pytest import ToxProjectCreator + + +@pytest.mark.parametrize( + "pkg_type", + ["dev-legacy", "sdist", "wheel"], +) +def test_tox_ini_package_type_valid(tox_project: ToxProjectCreator, pkg_type: str) -> None: + proj = tox_project({"tox.ini": f"[testenv]\npackage={pkg_type}", "pyproject.toml": ""}) + result = proj.run("c", "-k", "package_tox_env_type") + result.assert_success() + res = result.env_conf("py")["package"] + assert res == pkg_type + got_type = result.env_conf("py")["package_tox_env_type"] + assert got_type == "virtualenv-pep-517" + + +def test_tox_ini_package_type_invalid(tox_project: ToxProjectCreator) -> None: + proj = tox_project({"tox.ini": "[testenv]\npackage=bad", "pyproject.toml": ""}) + result = proj.run("c", "-k", "package_tox_env_type") + result.assert_failed() + assert " invalid package config type bad requested, must be one of wheel, sdist, dev-legacy, skip" in result.out + + +def test_get_package_deps_different_extras(pkg_with_extras_project: Path, tox_project: ToxProjectCreator) -> None: + ini = "[testenv:a]\npackage=dev-legacy\nextras=docs\n[testenv:b]\npackage=sdist\nextras=format" + proj = tox_project({"tox.ini": ini}) + execute_calls = proj.patch_execute(lambda r: 0 if "install" in r.run_id else None) + result = proj.run("r", "--root", str(pkg_with_extras_project), "-e", "a,b") + result.assert_success() + installs = { + i[0][0].conf.name: i[0][3].cmd[5:] + for i in execute_calls.call_args_list + if i[0][3].run_id.startswith("install_package_deps") + } + assert installs == { + "a": ["colorama>=0.4.3", "platformdirs>=2.1", "setuptools", "sphinx-rtd-theme<1,>=0.4.3", "sphinx>=3", "wheel"], + "b": ["black>=3", "colorama>=0.4.3", "flake8", "platformdirs>=2.1"], + } + + +def test_package_root_via_root(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None: + ini = f"[tox]\npackage_root={demo_pkg_inline}\n[testenv]\npackage=wheel\nwheel_build_env=.pkg" + proj = tox_project({"tox.ini": ini, "pyproject.toml": ""}) + proj.patch_execute(lambda r: 0 if "install" in r.run_id else None) + result = proj.run("r", "--notest") + result.assert_success() + + +def test_package_root_via_testenv(tox_project: ToxProjectCreator, demo_pkg_inline: Path) -> None: + ini = f"[testenv]\npackage=wheel\nwheel_build_env=.pkg\npackage_root={demo_pkg_inline}" + proj = tox_project({"tox.ini": ini, "pyproject.toml": ""}) + proj.patch_execute(lambda r: 0 if "install" in r.run_id else None) + result = proj.run("r", "--notest") + result.assert_success() + + +@pytest.mark.parametrize( + ("conf", "extra", "deps"), + [ + pytest.param("[project]", "", [], id="no_deps"), + pytest.param("[project]", "alpha", [], id="no_deps_with_extra"), + pytest.param("[project]\ndependencies=['B', 'A']", "", ["A", "B"], id="deps"), + pytest.param( + "[project]\ndependencies=['A']\noptional-dependencies.alpha=['B']\noptional-dependencies.beta=['C']", + "alpha", + ["A", "B"], + id="deps_with_one_extra", + ), + pytest.param( + "[project]\ndependencies=['A']\noptional-dependencies.alpha=['B']\noptional-dependencies.beta=['C']", + "alpha,beta", + ["A", "B", "C"], + id="deps_with_two_extra", + ), + pytest.param( + "[project]\ndependencies=['A']\noptional-dependencies.alpha=[]", + "alpha,beta", + ["A"], + id="deps_with_one_empty_extra", + ), + pytest.param( + "[project]\ndependencies=['A']\ndynamic=['optional-dependencies']", + "", + ["A"], + id="deps_with_dynamic_optional_no_extra", + ), + ], +) +def test_pyproject_deps_from_static( + tox_project: ToxProjectCreator, + demo_pkg_inline: Path, + conf: str, + extra: str, + deps: list[str], +) -> None: + toml = f"{(demo_pkg_inline / 'pyproject.toml').read_text()}{conf}" + proj = tox_project({"tox.ini": f"[testenv]\nextras={extra}", "pyproject.toml": toml}, base=demo_pkg_inline) + execute_calls = proj.patch_execute(lambda r: 0 if "install" in r.run_id else None) + result = proj.run("r", "--notest") + result.assert_success() + + expected_calls = [(".pkg", "get_requires_for_build_sdist"), (".pkg", "build_sdist")] + if deps: + expected_calls.append(("py", "install_package_deps")) + expected_calls.extend((("py", "install_package"), (".pkg", "_exit"))) + found_calls = [(i[0][0].conf.name, i[0][3].run_id) for i in execute_calls.call_args_list] + assert found_calls == expected_calls + + if deps: + expected_args = ["python", "-I", "-m", "pip", "install"] + deps + args = execute_calls.call_args_list[-3][0][3].cmd + assert expected_args == args + + +@pytest.mark.parametrize( + ("metadata", "dynamic", "deps"), + [ + pytest.param("Requires-Dist: A", "['dependencies']", ["A"], id="deps"), + pytest.param( + "Requires-Dist: A\nRequires-Dist: B;extra=='alpha'", + "['dependencies']", + ["A", "B"], + id="deps_extra", + ), + pytest.param( + "Requires-Dist: A\nRequires-Dist: B;extra=='alpha'", + "['optional-dependencies']", + ["A", "B"], + id="deps_extra_dynamic_opt", + ), + ], +) +def test_pyproject_deps_static_with_dynamic( + tox_project: ToxProjectCreator, + demo_pkg_inline: Path, + monkeypatch: pytest.MonkeyPatch, + metadata: str, + dynamic: str, + deps: list[str], +) -> None: + + monkeypatch.setenv("METADATA_EXTRA", metadata) + toml = f"{(demo_pkg_inline / 'pyproject.toml').read_text()}[project]\ndynamic={dynamic}" + ini = "[testenv]\nextras=alpha\n[testenv:.pkg]\npass_env=METADATA_EXTRA" + proj = tox_project({"tox.ini": ini, "pyproject.toml": toml}, base=demo_pkg_inline) + + execute_calls = proj.patch_execute(lambda r: 0 if "install" in r.run_id else None) + result = proj.run("r", "--notest") + result.assert_success() + + expected_calls = [ + (".pkg", "get_requires_for_build_sdist"), + (".pkg", "prepare_metadata_for_build_wheel"), + (".pkg", "build_wheel"), + (".pkg", "build_sdist"), + ("py", "install_package_deps"), + ("py", "install_package"), + (".pkg", "_exit"), + ] + found_calls = [(i[0][0].conf.name, i[0][3].run_id) for i in execute_calls.call_args_list] + assert found_calls == expected_calls + + args = execute_calls.call_args_list[-3][0][3].cmd + assert args == ["python", "-I", "-m", "pip", "install", *deps]