Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 142 additions & 39 deletions src/poetry/core/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from collections import defaultdict
from collections.abc import Mapping
from itertools import chain
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
Expand All @@ -14,15 +15,15 @@
from packaging.licenses import canonicalize_license_expression
from packaging.utils import canonicalize_name

from poetry.core.packages.dependency import Dependency
from poetry.core.packages.dependency_group import DependencyGroup
from poetry.core.utils.helpers import combine_unicode
from poetry.core.utils.helpers import readme_content_type


if TYPE_CHECKING:
from packaging.utils import NormalizedName

from poetry.core.packages.dependency import Dependency
from poetry.core.packages.dependency_group import DependencyGroup
from poetry.core.packages.project_package import ProjectPackage
from poetry.core.poetry import Poetry
from poetry.core.pyproject.toml import PyProjectTOML
Expand Down Expand Up @@ -87,7 +88,27 @@ def get_package(cls, name: str, version: str) -> ProjectPackage:
return ProjectPackage(name, version)

@classmethod
def _add_package_group_dependencies(
def _add_package_pep735_group_dependencies(
cls,
package: ProjectPackage,
group: DependencyGroup,
dependencies: list[str | dict[str, str]],
) -> list[str]:
group_includes = []
for constraint in dependencies:
if isinstance(constraint, str):
dep = Dependency.create_from_pep_508(
constraint,
relative_to=package.root_dir,
groups=[group.pretty_name],
)
group.add_dependency(dep)
elif include := constraint.get("include-group"):
group_includes.append(include)
return group_includes

@classmethod
def _add_package_poetry_group_dependencies(
cls,
package: ProjectPackage,
group: str | DependencyGroup,
Expand Down Expand Up @@ -134,13 +155,18 @@ def configure_package(
) -> None:
project = pyproject.data.get("project", {})
tool_poetry = pyproject.poetry_config
dependency_groups = pyproject.data.get("dependency-groups", {})

package.root_dir = root

cls._configure_package_metadata(package, project, tool_poetry, root)
cls._configure_entry_points(package, project, tool_poetry)
cls._configure_package_dependencies(
package, project, tool_poetry, with_groups=with_groups
package=package,
project=project,
tool_poetry=tool_poetry,
dependency_groups=dependency_groups,
with_groups=with_groups,
)
cls._configure_package_poetry_specifics(package, tool_poetry)

Expand Down Expand Up @@ -344,6 +370,7 @@ def _configure_package_dependencies(
package: ProjectPackage,
project: dict[str, Any],
tool_poetry: dict[str, Any],
dependency_groups: dict[str, list[str | dict[str, str]]],
with_groups: bool = True,
) -> None:
from poetry.core.packages.dependency import Dependency
Expand Down Expand Up @@ -388,41 +415,19 @@ def _configure_package_dependencies(
package.extras = package_extras

if "dependencies" in tool_poetry:
cls._add_package_group_dependencies(
cls._add_package_poetry_group_dependencies(
package=package,
group=MAIN_GROUP,
dependencies=tool_poetry["dependencies"],
)

if with_groups and "group" in tool_poetry:
for group_name, group_config in tool_poetry["group"].items():
group = DependencyGroup(
group_name, optional=group_config.get("optional", False)
)
cls._add_package_group_dependencies(
package=package,
group=group,
dependencies=group_config.get("dependencies", {}),
)

for group_name, group_config in tool_poetry["group"].items():
if include_groups := group_config.get("include-groups", []):
current_group = package.dependency_group(group_name)
for name in include_groups:
try:
# `name` isn't normalized,
# but `.dependency_group()` handles that.
group_to_include = package.dependency_group(name)
except ValueError as e:
raise ValueError(
f"Group '{group_name}' includes group '{name}'"
" which is not defined."
) from e

current_group.include_dependency_group(group_to_include)
if with_groups:
cls._configure_package_dependency_groups(
package, tool_poetry, dependency_groups
)

if with_groups and "dev-dependencies" in tool_poetry:
cls._add_package_group_dependencies(
cls._add_package_poetry_group_dependencies(
package=package,
group="dev",
dependencies=tool_poetry["dev-dependencies"],
Expand All @@ -448,6 +453,74 @@ def _configure_package_dependencies(

package.extras = package_extras

@classmethod
def _configure_package_dependency_groups(
cls,
package: ProjectPackage,
tool_poetry: dict[str, Any],
dependency_groups: dict[str, list[str | dict[str, str]]],
) -> None:
tool_poetry_groups = tool_poetry.get("group", {})
tool_poetry_groups_normalized = {
canonicalize_name(name): config
for name, config in tool_poetry_groups.items()
}
# create groups from the dependency-groups section considering
# additional information from the corresponding tool.poetry.group section
pep739_include_groups = {}
for group_name, dependencies in dependency_groups.items():
poetry_group_config = tool_poetry_groups_normalized.get(
canonicalize_name(group_name), {}
)
group = DependencyGroup(
name=group_name,
optional=poetry_group_config.get("optional", False),
)
package.add_dependency_group(group)
included_groups = cls._add_package_pep735_group_dependencies(
package=package,
group=group,
dependencies=dependencies,
)
pep739_include_groups[group_name] = included_groups
# create groups from the tool.poetry.group section
# with no corresponding entry in dependency-groups
# and add dependency information for existing groups
poetry_include_groups = {}
for group_name, group_config in tool_poetry_groups.items():
poetry_include_groups[group_name] = group_config.get("include-groups", [])
if package.has_dependency_group(group_name):
group = package.dependency_group(group_name)
else:
group = DependencyGroup(
name=group_name,
optional=group_config.get("optional", False),
)
package.add_dependency_group(group)
cls._add_package_poetry_group_dependencies(
package=package,
group=group,
dependencies=group_config.get("dependencies", {}),
)

for group_name, include_groups in chain(
pep739_include_groups.items(), poetry_include_groups.items()
):
if include_groups:
current_group = package.dependency_group(group_name)
for name in include_groups:
try:
# `name` isn't normalized,
# but `.dependency_group()` handles that.
group_to_include = package.dependency_group(name)
except ValueError as e:
raise ValueError(
f"Group '{group_name}' includes group '{name}'"
" which is not defined."
) from e

current_group.include_dependency_group(group_to_include)

@classmethod
def _prepare_formats(
cls,
Expand Down Expand Up @@ -664,6 +737,14 @@ def validate(
]
result["errors"] += tool_poetry_validation_errors

dependency_groups = toml_data.get("dependency-groups")
if dependency_groups is not None:
dependency_groups_validation_errors = [
e.replace("data", "dependency-groups")
for e in validate_object(dependency_groups, "dependency-groups-schema")
]
result["errors"] += dependency_groups_validation_errors

# Check for required fields if package mode.
# In non-package mode, there are no required fields.
package_mode = tool_poetry.get("package-mode", True)
Expand All @@ -685,7 +766,7 @@ def validate(
' Use "poetry.group.dev.dependencies" instead.'
)

cls._validate_dependency_groups_includes(toml_data, result)
cls._validate_dependency_groups(toml_data, result)

if strict:
# Validate [project] section
Expand All @@ -700,19 +781,41 @@ def validate(
return result

@classmethod
def _validate_dependency_groups_includes(
def _validate_dependency_groups(
cls, toml_data: dict[str, Any], result: dict[str, list[str]]
) -> None:
"""Ensure that dependency groups do not include themselves."""
config = toml_data.setdefault("tool", {}).setdefault("poetry", {})

"""Ensure that there are no duplicated dependency groups
and that they do not include themselves."""
original_names = defaultdict(set)
group_includes: dict[NormalizedName, list[NormalizedName]] = {}
for group_name, group_config in config.get("group", {}).items():

for group_name, dependencies in toml_data.get("dependency-groups", {}).items():
normalized_group_name = canonicalize_name(group_name)
original_names[normalized_group_name].add(group_name)
for constraint in dependencies:
if isinstance(constraint, dict) and (
include := constraint.get("include-group")
):
group_includes.setdefault(normalized_group_name, []).append(
canonicalize_name(include)
)

poetry_config = toml_data.get("tool", {}).get("poetry", {})
for group_name, group_config in poetry_config.get("group", {}).items():
normalized_group_name = canonicalize_name(group_name)
original_names[normalized_group_name].add(group_name)
if include_groups := group_config.get("include-groups", []):
group_includes[canonicalize_name(group_name)] = [
group_includes[normalized_group_name] = [
canonicalize_name(name) for name in include_groups
]

for normed_name, names in original_names.items():
if len(names) > 1:
result["errors"].append(
"Duplicate dependency group name after normalization:"
f" {normed_name} ({', '.join(sorted(names))})"
)

for root in group_includes:
# group, path to group, ancestors
stack: list[
Expand Down
39 changes: 39 additions & 0 deletions src/poetry/core/json/schemas/dependency-groups-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"name": "dependency-groups",
"type": "object",
"additionalProperties": {
"title": "Dependency group requirements",
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"description": "A single dependency requirement"
},
{
"type": "object",
"additionalProperties": false,
"required": [
"include-group"
],
"properties": {
"include-group": {
"type": "string",
"description": "The name of the group to include"
}
}
}
]
},
"examples": [
[
"attrs",
"requests ~= 2.28",
{
"include-group": "dev"
}
]
]
}
}
Loading
Loading