diff --git a/src/poetry/console/commands/add.py b/src/poetry/console/commands/add.py
index 81daf97407e..3707f9df265 100644
--- a/src/poetry/console/commands/add.py
+++ b/src/poetry/console/commands/add.py
@@ -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
@@ -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"The constraint {constraint} for"
+ f" {constraint_name} uses union syntax (|) which cannot"
+ " be represented in PEP 508 format."
+ )
+ self.line_error(
+ "Adding to [tool.poetry.dependencies] only."
+ )
+ 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
diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py
index 9c2fdeab9e2..9ce367948a1 100644
--- a/tests/console/commands/test_add.py
+++ b/tests/console/commands/test_add.py
@@ -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..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"]