Skip to content

Commit

Permalink
Support recursive extras defined in pyproject.toml
Browse files Browse the repository at this point in the history
Expand extras that reference an extra of the same package name to respect local
changes to package metadata.

Fix #2904
  • Loading branch information
masenf committed Jan 28, 2023
1 parent 1fc7b85 commit 39e0e94
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 6 deletions.
3 changes: 3 additions & 0 deletions docs/changelog/2904.bugfix.rst
@@ -0,0 +1,3 @@
Tox will now expand self-referential extras discovered in package deps to respect local modifications to package
metadata. This allows a package extra to explicitly depend on another package extra, which previously only worked with
non-static metadata - by :user:`masenf`.
16 changes: 11 additions & 5 deletions src/tox/tox_env/python/virtual_env/package/pyproject.py
Expand Up @@ -33,7 +33,7 @@
from tox.util.file_view import create_session_view

from ..api import VirtualEnv
from .util import dependencies_with_extras
from .util import dependencies_with_extras, dependencies_with_extras_from_markers

if sys.version_info >= (3, 8): # pragma: no cover (py38+)
from importlib.metadata import Distribution, PathDistribution
Expand Down Expand Up @@ -253,11 +253,17 @@ def _load_deps_from_static(self, for_env: EnvConfigSet) -> list[Requirement] | N
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", [])]
deps_with_markers: list[tuple[Requirement, set[str | None]]] = [
(Requirement(i), {None}) 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
for extra, reqs in optional_deps.items():
deps_with_markers.extend((Requirement(req), {extra}) for req in (reqs or []))
return dependencies_with_extras_from_markers(
deps_with_markers=deps_with_markers,
extras=extras,
package_name=project.get("name", "."),
)

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
Expand Down
9 changes: 8 additions & 1 deletion src/tox/tox_env/python/virtual_env/package/util.py
Expand Up @@ -8,7 +8,14 @@


def dependencies_with_extras(deps: list[Requirement], extras: set[str], package_name: str) -> list[Requirement]:
deps_with_markers = extract_extra_markers(deps)
return dependencies_with_extras_from_markers(extract_extra_markers(deps), extras, package_name)


def dependencies_with_extras_from_markers(
deps_with_markers: list[tuple[Requirement, set[str | None]]],
extras: set[str],
package_name: str,
) -> list[Requirement]:
result: list[Requirement] = []
found: set[str] = set()
todo: set[str | None] = extras | {None}
Expand Down
12 changes: 12 additions & 0 deletions tests/tox_env/python/virtual_env/package/test_package_pyproject.py
Expand Up @@ -132,6 +132,18 @@ def test_package_root_via_testenv(tox_project: ToxProjectCreator, demo_pkg_inlin
["A", "B", "D"],
id="deps_with_two_recursive_extra",
),
pytest.param(
dedent(
"""
[project]
name='foo'
optional-dependencies.alpha=['foo[beta]', 'A']
optional-dependencies.beta=['foo[alpha]', 'B']""",
),
"alpha",
["A", "B"],
id="deps_with_circular_recursive_extra",
),
],
)
def test_pyproject_deps_from_static(
Expand Down

0 comments on commit 39e0e94

Please sign in to comment.