From ca8fda926bf87d1a3f9d37458b4380dce855b2c5 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Sat, 28 May 2022 20:17:15 +0100 Subject: [PATCH 1/3] Fix complicated export case By treating different python version ranges independently, we buy the flexibility needed to make better decisions. --- src/poetry/packages/locker.py | 51 ++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 75c20383fde..10d582d7dbd 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -15,8 +15,11 @@ from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.semver.helpers import parse_constraint +from poetry.core.semver.util import constraint_regions from poetry.core.semver.version import Version from poetry.core.toml.file import TOMLFile +from poetry.core.version.markers import AnyMarker +from poetry.core.version.markers import SingleMarker from poetry.core.version.markers import parse_marker from poetry.core.version.requirements import InvalidRequirement from tomlkit import array @@ -49,6 +52,33 @@ logger = logging.getLogger(__name__) +def get_python_version_region_markers(packages: list[Package]) -> list[BaseMarker]: + markers = [] + + regions = constraint_regions([package.python_constraint for package in packages]) + for region in regions: + marker: BaseMarker = AnyMarker() + if region.min is not None: + min_operator = ">=" if region.include_min else ">" + marker_name = ( + "python_full_version" if region.min.precision > 2 else "python_version" + ) + lo = SingleMarker(marker_name, f"{min_operator} {region.min}") + marker = marker.intersect(lo) + + if region.max is not None: + max_operator = "<=" if region.include_max else "<" + marker_name = ( + "python_full_version" if region.max.precision > 2 else "python_version" + ) + hi = SingleMarker(marker_name, f"{max_operator} {region.max}") + marker = marker.intersect(hi) + + markers.append(marker) + + return markers + + class Locker: _VERSION = "1.1" @@ -277,12 +307,25 @@ def __walk_dependencies( ): continue - require = deepcopy(require) - require.marker = require.marker.intersect( + base_marker = require.marker.intersect( requirement.marker.without_extras() ) - if not require.marker.is_empty(): - dependencies.append(require) + + if not base_marker.is_empty(): + # So as to give ourselves enough flexibility in choosing a solution, + # we need to split the world up into the python version ranges that + # this package might care about. + # + # We create a marker for all of the possible regions, and add a + # requirement for each separately. + candidates = packages_by_name.get(require.name, []) + region_markers = get_python_version_region_markers(candidates) + for region_marker in region_markers: + marker = region_marker.intersect(base_marker) + if not marker.is_empty(): + require2 = deepcopy(require) + require2.marker = marker + dependencies.append(require2) key = locked_package if key not in nested_dependencies: From bd6242b6db39d6b57cac7934794dec7a79b46e17 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Mon, 30 May 2022 13:13:17 +0100 Subject: [PATCH 2/3] tighten up correctness of export fail more visibly if we take a wrong turn --- src/poetry/packages/locker.py | 40 ++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 10d582d7dbd..cd0d22dabdd 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -247,25 +247,41 @@ def __get_locked_package( """ decided = decided or {} - # Get the packages that are consistent with this dependency. - packages = [ - package - for package in packages_by_name.get(dependency.name, []) - if package.python_constraint.allows_all(dependency.python_constraint) - and dependency.constraint.allows(package.version) - ] + candidates = packages_by_name.get(dependency.name, []) - # If we've previously made a choice that is compatible with the current - # requirement, stick with it. - for package in packages: + # If we've previously chosen a version of this package that is compatible with + # the current requirement, we are forced to stick with it. (Else we end up with + # different versions of the same package at the same time.) + overlapping_candidates = set() + for package in candidates: old_decision = decided.get(package) if ( old_decision is not None and not old_decision.marker.intersect(dependency.marker).is_empty() ): - return package + overlapping_candidates.add(package) + + # If we have more than one overlapping candidate, we've run into trouble. + if len(overlapping_candidates) > 1: + return None + + # Get the packages that are consistent with this dependency. + compatible_candidates = [ + package + for package in candidates + if package.python_constraint.allows_all(dependency.python_constraint) + and dependency.constraint.allows(package.version) + ] + + # If we have an overlapping candidate, we must use it. + if overlapping_candidates: + compatible_candidates = [ + package + for package in compatible_candidates + if package in overlapping_candidates + ] - return next(iter(packages), None) + return next(iter(compatible_candidates), None) @classmethod def __walk_dependencies( From 283f2e5bc67f88e18a0f18605e8320639f3a5066 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Sun, 5 Jun 2022 15:17:26 +0100 Subject: [PATCH 3/3] prefer requirement.clone() --- src/poetry/packages/locker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index cd0d22dabdd..725dae2571d 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -5,7 +5,6 @@ import os import re -from copy import deepcopy from hashlib import sha256 from pathlib import Path from typing import TYPE_CHECKING @@ -339,7 +338,7 @@ def __walk_dependencies( for region_marker in region_markers: marker = region_marker.intersect(base_marker) if not marker.is_empty(): - require2 = deepcopy(require) + require2 = require.clone() require2.marker = marker dependencies.append(require2) @@ -391,7 +390,7 @@ def get_project_dependency_packages( if project_python_marker is not None: marked_requires: list[Dependency] = [] for require in project_requires: - require = deepcopy(require) + require = require.clone() require.marker = require.marker.intersect(project_python_marker) marked_requires.append(require) project_requires = marked_requires