Skip to content
Open
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
51 changes: 35 additions & 16 deletions src/poetry/console/commands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class AddCommand(InstallerCommand, InitCommand):

def handle(self) -> int:
from poetry.core.constraints.version import parse_constraint
from poetry.core.constraints.version import VersionUnion
from tomlkit import array
from tomlkit import inline_table
from tomlkit import nl
Expand Down Expand Up @@ -318,29 +319,47 @@ def handle(self) -> int:
)
self.poetry.package.add_dependency(dependency)

# Check if the constraint is a union (e.g., ^4|^6) which cannot be
# represented in PEP 508 format
is_version_union = isinstance(dependency.constraint, VersionUnion)

if use_project_section or use_groups_section:
pep_section = (
project_section if use_project_section else groups_content[group]
)
try:
index = project_dependency_names.index(canonical_constraint_name)
except ValueError:
pep_section.append(dependency.to_pep_508())

if is_version_union:
# Union constraints (e.g., ^4|^6) cannot be represented in PEP 508
# Only write to tool.poetry.dependencies
self.line_error(
f"<warning>The constraint <c1>{constraint}</c1> for"
f" <c1>{constraint_name}</c1> uses union syntax (|) which cannot"
" be represented in PEP 508 format.</warning>"
)
self.line_error(
"<warning>Adding to [tool.poetry.dependencies] only.</warning>"
)
poetry_constraint = constraint
else:
pep_section[index] = dependency.to_pep_508()

# create a second constraint for tool.poetry.dependencies with keys
# that cannot be stored in the project section
poetry_constraint: dict[str, Any] = inline_table()
if not isinstance(constraint, str):
for key in ["allow-prereleases", "develop", "source"]:
if value := constraint.get(key):
poetry_constraint[key] = value
if poetry_constraint:
# add marker related keys to avoid ambiguity
for key in ["python", "platform"]:
try:
index = project_dependency_names.index(canonical_constraint_name)
except ValueError:
pep_section.append(dependency.to_pep_508())
else:
pep_section[index] = dependency.to_pep_508()

# create a second constraint for tool.poetry.dependencies with keys
# that cannot be stored in the project section
poetry_constraint = inline_table()
if not isinstance(constraint, str):
for key in ["allow-prereleases", "develop", "source"]:
if value := constraint.get(key):
poetry_constraint[key] = value
if poetry_constraint:
# add marker related keys to avoid ambiguity
for key in ["python", "platform"]:
if value := constraint.get(key):
poetry_constraint[key] = value
else:
poetry_constraint = constraint

Expand Down
162 changes: 162 additions & 0 deletions tests/console/commands/test_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -1998,3 +1998,165 @@ def test_add_poetry_dependencies_if_necessary(
"allow-prereleases": True,
}
}


def test_add_union_constraint_skips_project_dependencies(
project_factory: ProjectFactory,
repo: TestRepository,
command_tester_factory: CommandTesterFactory,
) -> None:
"""
Test that union constraints (e.g., ^0.1|^0.3) are NOT written to
project.dependencies since PEP 508 does not support the || operator.
Instead, they should only be written to tool.poetry.dependencies.

Note: We use non-adjacent versions (^0.1|^0.3) because adjacent versions
like ^0.1|^0.2 get merged into a single range (>=0.1,<0.3).

Regression test for issue #10569.
"""
pyproject_content = """\
[project]
name = "simple-project"
version = "1.2.3"
dependencies = [
"tomlkit >= 0.5",
]
"""

poetry = project_factory(name="simple-project", pyproject_content=pyproject_content)

# Add non-adjacent versions so they don't get merged into a single range
repo.add_package(get_package("cachy", "0.1.0"))
repo.add_package(get_package("cachy", "0.3.0"))

tester = command_tester_factory("add", poetry=poetry)
tester.execute("cachy@^0.1|^0.3")

# Verify the warning is shown
error_output = tester.io.fetch_error()
assert "union syntax" in error_output
assert "cannot be represented in PEP 508" in error_output
assert "[tool.poetry.dependencies]" in error_output

updated_pyproject: dict[str, Any] = poetry.file.read()

# The union constraint should NOT be in project.dependencies
project_deps = updated_pyproject["project"]["dependencies"]
assert len(project_deps) == 1 # Only the original tomlkit dependency
assert "tomlkit >= 0.5" in project_deps[0]

# The union constraint SHOULD be in tool.poetry.dependencies
assert "cachy" in updated_pyproject["tool"]["poetry"]["dependencies"]
assert updated_pyproject["tool"]["poetry"]["dependencies"]["cachy"] == "^0.1|^0.3"


def test_add_union_constraint_skips_dependency_groups(
project_factory: ProjectFactory,
repo: TestRepository,
command_tester_factory: CommandTesterFactory,
) -> None:
"""
Test that union constraints are NOT written to dependency-groups (PEP 735)
since PEP 508 does not support the || operator.
Instead, they should only be written to tool.poetry.group.<name>.dependencies.

Note: We use non-adjacent versions (^0.1|^0.3) because adjacent versions
like ^0.1|^0.2 get merged into a single range.

Regression test for issue #10569.
"""
pyproject_content = """\
[project]
name = "simple-project"
version = "1.2.3"

[dependency-groups]
dev = [
"tomlkit >= 0.5",
]
"""

poetry = project_factory(name="simple-project", pyproject_content=pyproject_content)

# Add non-adjacent versions so they don't get merged into a single range
repo.add_package(get_package("cachy", "0.1.0"))
repo.add_package(get_package("cachy", "0.3.0"))

tester = command_tester_factory("add", poetry=poetry)
tester.execute("cachy@^0.1|^0.3 --group dev")

# Verify the warning is shown
error_output = tester.io.fetch_error()
assert "union syntax" in error_output
assert "cannot be represented in PEP 508" in error_output

updated_pyproject: dict[str, Any] = poetry.file.read()

# The union constraint should NOT be in dependency-groups
dev_deps = updated_pyproject["dependency-groups"]["dev"]
assert len(dev_deps) == 1 # Only the original tomlkit dependency
assert "tomlkit >= 0.5" in dev_deps[0]

# The union constraint SHOULD be in tool.poetry.group.dev.dependencies
assert "cachy" in updated_pyproject["tool"]["poetry"]["group"]["dev"]["dependencies"]
assert (
updated_pyproject["tool"]["poetry"]["group"]["dev"]["dependencies"]["cachy"]
== "^0.1|^0.3"
)


@pytest.mark.parametrize(
"constraint",
[
"^0.1|^0.3", # No spaces around pipe
"^0.1 | ^0.3", # Spaces around pipe
"^0.1 | ^0.3", # Multiple spaces around pipe
"^0.1| ^0.3", # Space only on right
"^0.1 |^0.3", # Space only on left
],
)
def test_add_union_constraint_with_various_spacing(
constraint: str,
project_factory: ProjectFactory,
repo: TestRepository,
command_tester_factory: CommandTesterFactory,
) -> None:
"""
Test that union constraints with various spacing patterns around the pipe
operator are all correctly detected and skipped from project.dependencies.

Regression test for issue #10569.
"""
pyproject_content = """\
[project]
name = "simple-project"
version = "1.2.3"
dependencies = [
"tomlkit >= 0.5",
]
"""

poetry = project_factory(name="simple-project", pyproject_content=pyproject_content)

# Add non-adjacent versions so they don't get merged into a single range
repo.add_package(get_package("cachy", "0.1.0"))
repo.add_package(get_package("cachy", "0.3.0"))

tester = command_tester_factory("add", poetry=poetry)
# Quote the constraint to handle spaces in the version string
tester.execute(f'"cachy@{constraint}"')

# Verify the warning is shown
error_output = tester.io.fetch_error()
assert "union syntax" in error_output
assert "cannot be represented in PEP 508" in error_output

updated_pyproject: dict[str, Any] = poetry.file.read()

# The union constraint should NOT be in project.dependencies
project_deps = updated_pyproject["project"]["dependencies"]
assert len(project_deps) == 1 # Only the original tomlkit dependency

# The union constraint SHOULD be in tool.poetry.dependencies
assert "cachy" in updated_pyproject["tool"]["poetry"]["dependencies"]