Skip to content

Commit cbb8dd1

Browse files
committed
refactor: remove redundant logic (use same logic for dependency-groups as for tool.poetry.group) (#823)
1 parent 00c547d commit cbb8dd1

File tree

5 files changed

+219
-106
lines changed

5 files changed

+219
-106
lines changed

src/poetry/core/factory.py

Lines changed: 92 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from collections import defaultdict
66
from collections.abc import Mapping
7+
from itertools import chain
78
from pathlib import Path
89
from typing import TYPE_CHECKING
910
from typing import Any
@@ -14,14 +15,14 @@
1415
from packaging.licenses import canonicalize_license_expression
1516
from packaging.utils import canonicalize_name
1617

18+
from poetry.core.packages.dependency import Dependency
1719
from poetry.core.utils.helpers import combine_unicode
1820
from poetry.core.utils.helpers import readme_content_type
1921

2022

2123
if TYPE_CHECKING:
2224
from packaging.utils import NormalizedName
2325

24-
from poetry.core.packages.dependency import Dependency
2526
from poetry.core.packages.dependency_group import DependencyGroup
2627
from poetry.core.packages.project_package import ProjectPackage
2728
from poetry.core.poetry import Poetry
@@ -87,7 +88,27 @@ def get_package(cls, name: str, version: str) -> ProjectPackage:
8788
return ProjectPackage(name, version)
8889

8990
@classmethod
90-
def _add_package_group_dependencies(
91+
def _add_package_pep735_group_dependencies(
92+
cls,
93+
package: ProjectPackage,
94+
group: DependencyGroup,
95+
dependencies: list[str | dict[str, str]],
96+
) -> list[str]:
97+
group_includes = []
98+
for constraint in dependencies:
99+
if isinstance(constraint, str):
100+
dep = Dependency.create_from_pep_508(
101+
constraint,
102+
relative_to=package.root_dir,
103+
groups=[group.pretty_name],
104+
)
105+
group.add_dependency(dep)
106+
else:
107+
group_includes.append(constraint["include-group"])
108+
return group_includes
109+
110+
@classmethod
111+
def _add_package_poetry_group_dependencies(
91112
cls,
92113
package: ProjectPackage,
93114
group: str | DependencyGroup,
@@ -394,48 +415,62 @@ def _configure_package_dependencies(
394415
package.extras = package_extras
395416

396417
if "dependencies" in tool_poetry:
397-
cls._add_package_group_dependencies(
418+
cls._add_package_poetry_group_dependencies(
398419
package=package,
399420
group=MAIN_GROUP,
400421
dependencies=tool_poetry["dependencies"],
401422
)
402423

403424
if with_groups:
404-
normalized_groups = cls._normalize_dependency_group_names(dependency_groups)
405-
included = cls._resolve_dependency_group_includes(normalized_groups)
406-
for group_name, dependencies in normalized_groups.items():
407-
poetry_group_config = tool_poetry.get("group", {}).get(group_name, {})
425+
tool_poetry_groups = tool_poetry.get("group", {})
426+
tool_poetry_groups_normalized = {
427+
canonicalize_name(name): config
428+
for name, config in tool_poetry_groups.items()
429+
}
430+
# create groups from the dependency-groups section considering
431+
# additional information from the corresponding tool.poetry.group section
432+
pep739_include_groups = {}
433+
for group_name, dependencies in dependency_groups.items():
434+
poetry_group_config = tool_poetry_groups_normalized.get(
435+
canonicalize_name(group_name), {}
436+
)
408437
group = DependencyGroup(
409438
name=group_name,
410439
optional=poetry_group_config.get("optional", False),
411440
)
412441
package.add_dependency_group(group)
413-
414-
for constraint in dependencies:
415-
dep = Dependency.create_from_pep_508(
416-
constraint,
417-
relative_to=package.root_dir,
418-
groups=[group_name],
419-
)
420-
group.add_dependency(dep)
421-
422-
for group_name, group_config in tool_poetry.get("group", {}).items():
423-
if not package.has_dependency_group(group_name):
442+
included_groups = cls._add_package_pep735_group_dependencies(
443+
package=package,
444+
group=group,
445+
dependencies=dependencies,
446+
)
447+
pep739_include_groups[group_name] = included_groups
448+
# create groups from the tool.poetry.group section
449+
# with no corresponding entry in dependency-groups
450+
# and add dependency information for existing groups
451+
poetry_include_groups = {}
452+
for group_name, group_config in tool_poetry_groups.items():
453+
poetry_include_groups[group_name] = group_config.get(
454+
"include-groups", []
455+
)
456+
if package.has_dependency_group(group_name):
457+
group = package.dependency_group(group_name)
458+
else:
424459
group = DependencyGroup(
425460
name=group_name,
426461
optional=group_config.get("optional", False),
427462
)
428463
package.add_dependency_group(group)
464+
cls._add_package_poetry_group_dependencies(
465+
package=package,
466+
group=group,
467+
dependencies=group_config.get("dependencies", {}),
468+
)
429469

430-
for group_name_ in (group_name, *included.get(group_name, [])):
431-
cls._add_package_group_dependencies(
432-
package=package,
433-
group=group_name_,
434-
dependencies=group_config.get("dependencies", {}),
435-
)
436-
437-
for group_name, group_config in tool_poetry.get("group", {}).items():
438-
if include_groups := group_config.get("include-groups", []):
470+
for group_name, include_groups in chain(
471+
pep739_include_groups.items(), poetry_include_groups.items()
472+
):
473+
if include_groups:
439474
current_group = package.dependency_group(group_name)
440475
for name in include_groups:
441476
try:
@@ -451,7 +486,7 @@ def _configure_package_dependencies(
451486
current_group.include_dependency_group(group_to_include)
452487

453488
if with_groups and "dev-dependencies" in tool_poetry:
454-
cls._add_package_group_dependencies(
489+
cls._add_package_poetry_group_dependencies(
455490
package=package,
456491
group="dev",
457492
dependencies=tool_poetry["dev-dependencies"],
@@ -477,65 +512,6 @@ def _configure_package_dependencies(
477512

478513
package.extras = package_extras
479514

480-
@classmethod
481-
def _normalize_dependency_group_names(
482-
cls,
483-
dependency_groups: dict[str, list[str | dict[str, str]]],
484-
) -> dict[NormalizedName, list[str | dict[str, str]]]:
485-
original_names = defaultdict(list)
486-
normalized_groups: dict[NormalizedName, list[str | dict[str, str]]] = {}
487-
488-
for group_name, value in dependency_groups.items():
489-
normed_group_name = canonicalize_name(group_name)
490-
original_names[normed_group_name].append(group_name)
491-
normalized_groups[normed_group_name] = value
492-
493-
errors = []
494-
for normed_name, names in original_names.items():
495-
if len(names) > 1:
496-
errors.append(f"{normed_name} ({', '.join(names)})")
497-
if errors:
498-
raise ValueError(f"Duplicate dependency group names: {', '.join(errors)}")
499-
500-
return normalized_groups
501-
502-
@classmethod
503-
def _resolve_dependency_group_includes(
504-
cls, dependency_groups: dict[NormalizedName, list[str | dict[str, str]]]
505-
) -> dict[NormalizedName, list[NormalizedName]]:
506-
resolved_groups: set[NormalizedName] = set()
507-
included: dict[NormalizedName, list[NormalizedName]] = defaultdict(list)
508-
while resolved_groups != set(dependency_groups):
509-
for group, dependencies in dependency_groups.items():
510-
if group in resolved_groups:
511-
continue
512-
if all(isinstance(dep, str) for dep in dependencies):
513-
resolved_groups.add(group)
514-
continue
515-
resolved_dependencies: list[str | dict[str, str]] = []
516-
for dep in dependencies:
517-
if isinstance(dep, str):
518-
resolved_dependencies.append(dep)
519-
else:
520-
included_group = canonicalize_name(dep["include-group"])
521-
if included_group in included[group]:
522-
raise ValueError(
523-
f"Cyclic dependency group include:"
524-
f" {group} -> {included_group}"
525-
)
526-
included[included_group].append(group)
527-
try:
528-
resolved_dependencies.extend(
529-
dependency_groups[included_group]
530-
)
531-
except KeyError:
532-
raise ValueError(
533-
f"Dependency group '{included_group}'"
534-
f" (included in '{group}') not found"
535-
)
536-
dependency_groups[group] = resolved_dependencies
537-
return included
538-
539515
@classmethod
540516
def _prepare_formats(
541517
cls,
@@ -781,7 +757,7 @@ def validate(
781757
' Use "poetry.group.dev.dependencies" instead.'
782758
)
783759

784-
cls._validate_dependency_groups_includes(toml_data, result)
760+
cls._validate_dependency_groups(toml_data, result)
785761

786762
if strict:
787763
# Validate [project] section
@@ -796,19 +772,41 @@ def validate(
796772
return result
797773

798774
@classmethod
799-
def _validate_dependency_groups_includes(
775+
def _validate_dependency_groups(
800776
cls, toml_data: dict[str, Any], result: dict[str, list[str]]
801777
) -> None:
802-
"""Ensure that dependency groups do not include themselves."""
803-
config = toml_data.setdefault("tool", {}).setdefault("poetry", {})
804-
778+
"""Ensure that there are no duplicated dependency groups
779+
and that they do not include themselves."""
780+
original_names = defaultdict(set)
805781
group_includes: dict[NormalizedName, list[NormalizedName]] = {}
806-
for group_name, group_config in config.get("group", {}).items():
782+
783+
for group_name, dependencies in toml_data.get("dependency-groups", {}).items():
784+
normalized_group_name = canonicalize_name(group_name)
785+
original_names[normalized_group_name].add(group_name)
786+
for constraint in dependencies:
787+
if isinstance(constraint, dict) and (
788+
include := constraint.get("include-group")
789+
):
790+
group_includes.setdefault(normalized_group_name, []).append(
791+
canonicalize_name(include)
792+
)
793+
794+
poetry_config = toml_data.get("tool", {}).get("poetry", {})
795+
for group_name, group_config in poetry_config.get("group", {}).items():
796+
normalized_group_name = canonicalize_name(group_name)
797+
original_names[normalized_group_name].add(group_name)
807798
if include_groups := group_config.get("include-groups", []):
808-
group_includes[canonicalize_name(group_name)] = [
799+
group_includes[normalized_group_name] = [
809800
canonicalize_name(name) for name in include_groups
810801
]
811802

803+
for normed_name, names in original_names.items():
804+
if len(names) > 1:
805+
result["errors"].append(
806+
"Duplicate dependency group name after normalization:"
807+
f" {normed_name} ({', '.join(sorted(names))})"
808+
)
809+
812810
for root in group_includes:
813811
# group, path to group, ancestors
814812
stack: list[
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
My Package
2+
==========
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
[tool.poetry]
2+
name = "my-package"
3+
version = "1.2.3"
4+
description = "Some description."
5+
authors = [
6+
"Sébastien Eustace <sebastien@eustace.io>"
7+
]
8+
maintainers = [
9+
"Sébastien Eustace <sebastien@eustace.io>"
10+
]
11+
license = "MIT"
12+
13+
readme = "README.rst"
14+
15+
homepage = "https://python-poetry.org"
16+
repository = "https://github.com/python-poetry/poetry"
17+
documentation = "https://python-poetry.org/docs"
18+
19+
keywords = ["packaging", "dependency", "poetry"]
20+
21+
classifiers = [
22+
"Topic :: Software Development :: Build Tools",
23+
"Topic :: Software Development :: Libraries :: Python Modules"
24+
]
25+
26+
# Requirements
27+
[tool.poetry.dependencies]
28+
python = ">=3.6"
29+
cleo = "^0.6"
30+
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
31+
tomlkit = { git = "https://github.com/sdispater/tomlkit.git", rev = "3bff550", develop = true }
32+
requests = { version = "^2.18", optional = true, extras = [ "security" ] }
33+
pathlib2 = { version = "^2.2", python = "~2.7" }
34+
35+
orator = { version = "^0.9", optional = true }
36+
37+
# File dependency
38+
demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" }
39+
40+
# Dir dependency with setup.py
41+
my-package = { path = "../project_with_setup/" }
42+
43+
# Dir dependency with pyproject.toml
44+
simple-project = { path = "../simple_project/" }
45+
46+
# Dependency with markers
47+
functools32 = { version = "^3.2.3", markers = "python_version ~= '2.7' and sys_platform == 'win32' or python_version in '3.4 3.5'" }
48+
49+
# Dependency with python constraint
50+
dataclasses = { version = "^0.7", python = ">=3.6.1,<3.7" }
51+
52+
53+
[tool.poetry.group.Test.dependencies]
54+
pytest = ">7"
55+
coverage = "*"
56+
57+
[tool.poetry.group.Dev]
58+
include-groups = [ "tEsT" ]
59+
60+
[tool.poetry.group.Docs]
61+
optional = true
62+
63+
[tool.poetry.group.Docs.dependencies]
64+
mkdocs = { git = "git+https://github.com/mkdocs/mkdocs.git", develop = true }
65+
66+
[tool.poetry.group.all]
67+
include-groups = [ "test", "dev", "docs" ]
68+
69+
70+
[tool.poetry.extras]
71+
db = [ "orator" ]
72+
network = [ "requests" ]
73+
74+
# Non-regression test for https://github.com/python-poetry/poetry-core/pull/492.
75+
# The underlying issue occurred because `tomlkit` can either return a TOML table as `Table` instance or an
76+
# `OutOfOrderProxy` one, if a table is discontinuous and multiple sections of a table are separated by a non-related
77+
# table, but we were too strict in our type check assertions.
78+
# So adding `tool.black` here ensure that we have discontinuous tables, so that we don't re-introduce the issue caused
79+
# by the type check assertion that ended up being reverted.
80+
[tool.black]
81+
preview = true
82+
83+
[tool.poetry.group.Dev.dependencies]
84+
pre-commit = "*"
85+
86+
87+
[tool.poetry.scripts]
88+
my-script = "my_package:main"
89+
90+
91+
[tool.poetry.plugins."blogtool.parsers"]
92+
".rst" = "some_module::SomeClass"

tests/fixtures/sample_project_with_groups_new/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ all = [
6969
{ "include-group" = "docs" },
7070
]
7171

72-
[tool.poetry.group.docs]
72+
[tool.poetry.group.Docs]
7373
optional = true
7474

75-
[tool.poetry.group.docs.dependencies]
75+
[tool.poetry.group.Docs.dependencies]
7676
mkdocs = { develop = true }

0 commit comments

Comments
 (0)