Skip to content

Commit f896bce

Browse files
abndimbleby
andcommitted
fix(puzzle): handle self-referential extras
Co-authored-by: David Hotham <[email protected]>
1 parent 603646c commit f896bce

18 files changed

+897
-11
lines changed

src/poetry/puzzle/provider.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -460,18 +460,29 @@ def complete_package(
460460
dependency = dependency_package.dependency
461461
requires = package.requires
462462

463-
optional_dependencies = []
463+
found_extras = set()
464+
optional_dependencies = set()
464465
_dependencies = []
465466

466-
# If some extras/features were required, we need to
467-
# add a special dependency representing the base package
468-
# to the current package
469467
if dependency.extras:
470-
for extra in dependency.extras:
471-
if extra not in package.extras:
468+
# Find all the optional dependencies that are wanted - taking care to allow
469+
# for self-referential extras.
470+
stack = list(dependency.extras)
471+
while stack:
472+
extra = stack.pop()
473+
if extra in found_extras:
472474
continue
475+
found_extras.add(extra)
473476

474-
optional_dependencies += [d.name for d in package.extras[extra]]
477+
extra_dependencies = package.extras.get(extra, [])
478+
for extra_dependency in extra_dependencies:
479+
if extra_dependency.name == dependency.name:
480+
stack += list(extra_dependency.features)
481+
else:
482+
optional_dependencies.add(extra_dependency.name)
483+
484+
# If some extras/features were required, we need to add a special dependency
485+
# representing the base package to the current package.
475486

476487
dependency_package = dependency_package.with_features(
477488
list(dependency.extras)
@@ -507,10 +518,7 @@ def complete_package(
507518

508519
if not package.is_root() and (
509520
(dep.is_optional() and dep.name not in optional_dependencies)
510-
or (
511-
dep.in_extras
512-
and not set(dep.in_extras).intersection(dependency.extras)
513-
)
521+
or (dep.in_extras and not set(dep.in_extras).intersection(found_extras))
514522
):
515523
continue
516524

tests/conftest.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from keyring.credentials import SimpleCredential
2222
from keyring.errors import KeyringError
2323
from keyring.errors import KeyringLocked
24+
from packaging.utils import canonicalize_name
25+
from poetry.core.version.markers import parse_marker
2426
from pytest import FixtureRequest
2527

2628
from poetry.config.config import Config as BaseConfig
@@ -29,7 +31,9 @@
2931
from poetry.factory import Factory
3032
from poetry.layouts import layout
3133
from poetry.packages.direct_origin import _get_package_from_git
34+
from poetry.repositories import Repository
3235
from poetry.repositories import RepositoryPool
36+
from poetry.repositories.exceptions import PackageNotFoundError
3337
from poetry.repositories.installed_repository import InstalledRepository
3438
from poetry.utils.cache import ArtifactCache
3539
from poetry.utils.env import EnvManager
@@ -39,6 +43,7 @@
3943
from tests.helpers import MOCK_DEFAULT_GIT_REVISION
4044
from tests.helpers import TestLocker
4145
from tests.helpers import TestRepository
46+
from tests.helpers import get_dependency
4247
from tests.helpers import get_package
4348
from tests.helpers import http_setup_redirect
4449
from tests.helpers import isolated_environment
@@ -56,6 +61,9 @@
5661
from cleo.io.inputs.argument import Argument
5762
from cleo.io.inputs.option import Option
5863
from keyring.credentials import Credential
64+
from packaging.utils import NormalizedName
65+
from poetry.core.packages.dependency import Dependency
66+
from poetry.core.packages.package import Package
5967
from pytest import Config as PyTestConfig
6068
from pytest import Parser
6169
from pytest import TempPathFactory
@@ -65,6 +73,7 @@
6573
from tests.types import CommandFactory
6674
from tests.types import FixtureCopier
6775
from tests.types import FixtureDirGetter
76+
from tests.types import PackageFactory
6877
from tests.types import ProjectFactory
6978
from tests.types import SetProjectContext
7079

@@ -499,6 +508,69 @@ def _factory(
499508
return _factory
500509

501510

511+
@pytest.fixture
512+
def create_package(repo: Repository) -> PackageFactory:
513+
"""
514+
This function is a pytest fixture that creates a factory function to generate
515+
and customize package objects. These packages are added to the default repository
516+
fixture and configured with specific versions, optional extras, and self-referenced
517+
extras. This helps in setting up package dependencies for testing purposes.
518+
519+
:return: A factory function that can be used to create and configure packages.
520+
"""
521+
522+
def create_new_package(
523+
name: str,
524+
version: str | None = None,
525+
extras: dict[str, list[str]] | None = None,
526+
self_referenced_extras: dict[str, list[str]] | None = None,
527+
) -> Package:
528+
version = version or "1.0"
529+
package = get_package(name, version)
530+
531+
package_extras: dict[NormalizedName, list[Dependency]] = {}
532+
533+
for extra, dep_names in (extras or {}).items():
534+
extra = canonicalize_name(extra)
535+
536+
if extra not in package_extras:
537+
package_extras[extra] = []
538+
539+
for dep_name in dep_names:
540+
try:
541+
pkg = repo.package(dep_name, package.version)
542+
except PackageNotFoundError:
543+
pkg = get_package(dep_name, version)
544+
repo.add_package(pkg)
545+
546+
dep = get_dependency(pkg.name, f"^{pkg.version}", optional=True)
547+
dep.marker = parse_marker(f"extra == '{extra}'")
548+
package_extras[extra].append(dep)
549+
package.add_dependency(dep)
550+
551+
# add self-referencing extras
552+
for extra, target_names in (self_referenced_extras or {}).items():
553+
extra = canonicalize_name(extra)
554+
package_extras[extra] = [
555+
Factory.create_dependency(
556+
package.name,
557+
{
558+
"version": "*",
559+
"extras": [
560+
canonicalize_name(target) for target in target_names
561+
],
562+
},
563+
)
564+
]
565+
566+
package.extras = package_extras
567+
repo.add_package(package)
568+
569+
return package
570+
571+
return create_new_package
572+
573+
502574
@pytest.fixture(autouse=True)
503575
def set_simple_log_formatter() -> None:
504576
"""
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
2+
3+
[[package]]
4+
name = "A"
5+
version = "1.0"
6+
description = ""
7+
optional = false
8+
python-versions = "*"
9+
groups = ["main"]
10+
files = []
11+
12+
[package.dependencies]
13+
download-package = {version = "^1.0", optional = true, markers = "extra == \"download\""}
14+
install-package = {version = "^1.0", optional = true, markers = "extra == \"install\""}
15+
16+
[package.extras]
17+
all = ["a[download,install]"]
18+
download = ["download-package (>=1.0,<2.0)"]
19+
install = ["install-package (>=1.0,<2.0)"]
20+
nested = ["a[all]"]
21+
22+
[[package]]
23+
name = "B"
24+
version = "1.0"
25+
description = ""
26+
optional = false
27+
python-versions = "*"
28+
groups = ["main"]
29+
files = []
30+
31+
[package.dependencies]
32+
A = {version = "*", extras = ["all"]}
33+
34+
[[package]]
35+
name = "download-package"
36+
version = "1.0"
37+
description = ""
38+
optional = false
39+
python-versions = "*"
40+
groups = ["main"]
41+
files = []
42+
43+
[[package]]
44+
name = "install-package"
45+
version = "1.0"
46+
description = ""
47+
optional = false
48+
python-versions = "*"
49+
groups = ["main"]
50+
files = []
51+
52+
[metadata]
53+
lock-version = "2.1"
54+
python-versions = "*"
55+
content-hash = "123456789"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
2+
3+
[[package]]
4+
name = "A"
5+
version = "1.0"
6+
description = ""
7+
optional = false
8+
python-versions = "*"
9+
groups = ["main"]
10+
files = []
11+
12+
[package.dependencies]
13+
download-package = {version = "^1.0", optional = true, markers = "extra == \"download\""}
14+
install-package = {version = "^1.0", optional = true, markers = "extra == \"install\""}
15+
16+
[package.extras]
17+
all = ["a[download,install]"]
18+
download = ["download-package (>=1.0,<2.0)"]
19+
install = ["install-package (>=1.0,<2.0)"]
20+
nested = ["a[all]"]
21+
22+
[[package]]
23+
name = "download-package"
24+
version = "1.0"
25+
description = ""
26+
optional = false
27+
python-versions = "*"
28+
groups = ["main"]
29+
files = []
30+
31+
[[package]]
32+
name = "install-package"
33+
version = "1.0"
34+
description = ""
35+
optional = false
36+
python-versions = "*"
37+
groups = ["main"]
38+
files = []
39+
40+
[metadata]
41+
lock-version = "2.1"
42+
python-versions = "*"
43+
content-hash = "123456789"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
2+
3+
[[package]]
4+
name = "A"
5+
version = "1.0"
6+
description = ""
7+
optional = false
8+
python-versions = "*"
9+
groups = ["main"]
10+
files = []
11+
12+
[package.dependencies]
13+
download-package = {version = "^1.0", optional = true, markers = "extra == \"download\""}
14+
install-package = {version = "^1.0", optional = true, markers = "extra == \"install\""}
15+
16+
[package.extras]
17+
all = ["a[download,install]"]
18+
b = ["a[all]"]
19+
download = ["download-package (>=1.0,<2.0)"]
20+
install = ["install-package (>=1.0,<2.0)"]
21+
nested = ["a[all]"]
22+
23+
[[package]]
24+
name = "download-package"
25+
version = "1.0"
26+
description = ""
27+
optional = false
28+
python-versions = "*"
29+
groups = ["main"]
30+
files = []
31+
32+
[[package]]
33+
name = "install-package"
34+
version = "1.0"
35+
description = ""
36+
optional = false
37+
python-versions = "*"
38+
groups = ["main"]
39+
files = []
40+
41+
[metadata]
42+
lock-version = "2.1"
43+
python-versions = "*"
44+
content-hash = "123456789"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
2+
3+
[[package]]
4+
name = "A"
5+
version = "1.0"
6+
description = ""
7+
optional = false
8+
python-versions = "*"
9+
groups = ["main"]
10+
files = []
11+
12+
[package.extras]
13+
all = ["a[download,install]"]
14+
download = ["download-package (>=1.0,<2.0)"]
15+
install = ["install-package (>=1.0,<2.0)"]
16+
nested = ["a[all]"]
17+
18+
[[package]]
19+
name = "B"
20+
version = "1.0"
21+
description = ""
22+
optional = false
23+
python-versions = "*"
24+
groups = ["main"]
25+
files = []
26+
27+
[package.dependencies]
28+
A = "*"
29+
30+
[metadata]
31+
lock-version = "2.1"
32+
python-versions = "*"
33+
content-hash = "123456789"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
2+
3+
[[package]]
4+
name = "A"
5+
version = "1.0"
6+
description = ""
7+
optional = false
8+
python-versions = "*"
9+
groups = ["main"]
10+
files = []
11+
12+
[package.dependencies]
13+
download-package = {version = "^1.0", optional = true, markers = "extra == \"download\""}
14+
15+
[package.extras]
16+
all = ["a[download,install]"]
17+
download = ["download-package (>=1.0,<2.0)"]
18+
install = ["install-package (>=1.0,<2.0)"]
19+
nested = ["a[all]"]
20+
21+
[[package]]
22+
name = "B"
23+
version = "1.0"
24+
description = ""
25+
optional = false
26+
python-versions = "*"
27+
groups = ["main"]
28+
files = []
29+
30+
[package.dependencies]
31+
A = {version = "*", extras = ["download"]}
32+
33+
[[package]]
34+
name = "download-package"
35+
version = "1.0"
36+
description = ""
37+
optional = false
38+
python-versions = "*"
39+
groups = ["main"]
40+
files = []
41+
42+
[metadata]
43+
lock-version = "2.1"
44+
python-versions = "*"
45+
content-hash = "123456789"

0 commit comments

Comments
 (0)