Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

provider: do not merge dependencies from different sources #6679

Merged
merged 1 commit into from Oct 3, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 50 additions & 12 deletions src/poetry/puzzle/provider.py
Expand Up @@ -670,19 +670,29 @@ def complete_package(

self.debug(f"<debug>Duplicate dependencies for {dep_name}</debug>")

non_direct_origin_deps: list[Dependency] = []
direct_origin_deps: list[Dependency] = []
for dep in deps:
if dep.is_direct_origin():
direct_origin_deps.append(dep)
else:
non_direct_origin_deps.append(dep)
deps = (
self._merge_dependencies_by_constraint(
self._merge_dependencies_by_marker(non_direct_origin_deps)
# Group dependencies for merging.
# We must not merge dependencies from different sources!
dep_groups = self._group_by_source(deps)
neersighted marked this conversation as resolved.
Show resolved Hide resolved
deps = []
for group in dep_groups:
# In order to reduce the number of overrides we merge duplicate
# dependencies by constraint. For instance, if we have:
# - foo (>=2.0) ; python_version >= "3.6" and python_version < "3.7"
# - foo (>=2.0) ; python_version >= "3.7"
# we can avoid two overrides by merging them to:
# - foo (>=2.0) ; python_version >= "3.6"
# However, if we want to merge dependencies by constraint we have to
# merge dependencies by markers first in order to avoid unnecessary
# solver failures. For instance, if we have:
# - foo (>=2.0) ; python_version >= "3.6" and python_version < "3.7"
# - foo (>=2.0) ; python_version >= "3.7"
# - foo (<2.1) ; python_version >= "3.7"
# we must not merge the first two constraints but the last two:
# - foo (>=2.0) ; python_version >= "3.6" and python_version < "3.7"
# - foo (>=2.0,<2.1) ; python_version >= "3.7"
deps += self._merge_dependencies_by_constraint(
self._merge_dependencies_by_marker(group)
)
+ direct_origin_deps
)
if len(deps) == 1:
self.debug(f"<debug>Merging requirements for {deps[0]!s}</debug>")
dependencies.append(deps[0])
Expand Down Expand Up @@ -947,9 +957,33 @@ def debug(self, message: str, depth: int = 0) -> None:

self._io.write(debug_info)

def _group_by_source(
neersighted marked this conversation as resolved.
Show resolved Hide resolved
self, dependencies: Iterable[Dependency]
) -> list[list[Dependency]]:
"""
Takes a list of dependencies and returns a list of groups of dependencies,
each group containing all dependencies from the same source.
"""
groups: list[list[Dependency]] = []
for dep in dependencies:
for group in groups:
if (
dep.is_same_source_as(group[0])
and dep.source_name == group[0].source_name
neersighted marked this conversation as resolved.
Show resolved Hide resolved
):
group.append(dep)
break
else:
groups.append([dep])
return groups

def _merge_dependencies_by_constraint(
self, dependencies: Iterable[Dependency]
) -> list[Dependency]:
"""
Merge dependencies with the same constraint
by building a union of their markers.
"""
by_constraint: dict[VersionConstraint, list[Dependency]] = defaultdict(list)
for dep in dependencies:
by_constraint[dep.constraint].append(dep)
Expand All @@ -975,6 +1009,10 @@ def _merge_dependencies_by_constraint(
def _merge_dependencies_by_marker(
self, dependencies: Iterable[Dependency]
) -> list[Dependency]:
"""
Merge dependencies with the same marker
by building the intersection of their constraints.
"""
by_marker: dict[BaseMarker, list[Dependency]] = defaultdict(list)
for dep in dependencies:
by_marker[dep.marker].append(dep)
Expand Down
23 changes: 23 additions & 0 deletions tests/puzzle/test_provider.py
Expand Up @@ -622,6 +622,29 @@ def test_search_for_file_wheel_with_extras(provider: Provider):
}


def test_complete_package_does_not_merge_different_source_names(
provider: Provider, root: ProjectPackage
) -> None:
foo_source_1 = get_dependency("foo")
foo_source_1.source_name = "source_1"
foo_source_2 = get_dependency("foo")
foo_source_2.source_name = "source_2"

root.add_dependency(foo_source_1)
root.add_dependency(foo_source_2)

complete_package = provider.complete_package(
DependencyPackage(root.to_dependency(), root)
)

requires = complete_package.package.all_requires
assert len(requires) == 2
assert {requires[0].source_name, requires[1].source_name} == {
"source_1",
"source_2",
}


def test_complete_package_preserves_source_type(
provider: Provider, root: ProjectPackage
) -> None:
Expand Down
16 changes: 12 additions & 4 deletions tests/puzzle/test_solver.py
Expand Up @@ -1368,8 +1368,9 @@ def test_solver_duplicate_dependencies_different_constraints_merge_by_marker(
)


@pytest.mark.parametrize("git_first", [False, True])
def test_solver_duplicate_dependencies_different_sources_types_are_preserved(
solver: Solver, repo: Repository, package: ProjectPackage
solver: Solver, repo: Repository, package: ProjectPackage, git_first: bool
):
pendulum = get_package("pendulum", "2.0.3")
repo.add_package(pendulum)
Expand All @@ -1380,8 +1381,12 @@ def test_solver_duplicate_dependencies_different_sources_types_are_preserved(
dependency_git = Factory.create_dependency(
"demo", {"git": "https://github.com/demo/demo.git"}, groups=["dev"]
)
package.add_dependency(dependency_git)
package.add_dependency(dependency_pypi)
if git_first:
package.add_dependency(dependency_git)
package.add_dependency(dependency_pypi)
else:
package.add_dependency(dependency_pypi)
package.add_dependency(dependency_git)

demo = Package(
"demo",
Expand Down Expand Up @@ -1413,7 +1418,10 @@ def test_solver_duplicate_dependencies_different_sources_types_are_preserved(

assert len(complete_package.package.all_requires) == 2

pypi, git = complete_package.package.all_requires
if git_first:
git, pypi = complete_package.package.all_requires
else:
pypi, git = complete_package.package.all_requires

assert isinstance(pypi, Dependency)
assert pypi == dependency_pypi
Expand Down