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

Fix complicated export case #5711

Closed
Closed
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
94 changes: 76 additions & 18 deletions src/poetry/packages/locker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,8 +14,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
Expand Down Expand Up @@ -49,6 +51,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"

Expand Down Expand Up @@ -217,25 +246,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)

return next(iter(packages), None)
# 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(compatible_candidates), None)

@classmethod
def __walk_dependencies(
Expand Down Expand Up @@ -277,12 +322,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 = require.clone()
require2.marker = marker
dependencies.append(require2)

key = locked_package
if key not in nested_dependencies:
Expand Down Expand Up @@ -332,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
Expand Down