11from __future__ import annotations
22
3+ import itertools
34import logging
45import re
56import time
1213from cleo .ui .progress_indicator import ProgressIndicator
1314from poetry .core .constraints .version import EmptyConstraint
1415from poetry .core .constraints .version import Version
16+ from poetry .core .constraints .version import VersionRange
1517from poetry .core .packages .utils .utils import get_python_constraint_from_marker
1618from poetry .core .version .markers import AnyMarker
17- from poetry .core .version .markers import EmptyMarker
18- from poetry .core .version .markers import MarkerUnion
19+ from poetry .core .version .markers import intersection as marker_intersection
20+ from poetry .core .version .markers import union as marker_union
1921
2022from poetry .mixology .incompatibility import Incompatibility
2123from poetry .mixology .incompatibility_cause import DependencyCause
@@ -59,10 +61,22 @@ class IncompatibleConstraintsError(Exception):
5961 Exception when there are duplicate dependencies with incompatible constraints.
6062 """
6163
62- def __init__ (self , package : Package , * dependencies : Dependency ) -> None :
63- constraints = "\n " .join (dep .to_pep_508 () for dep in dependencies )
64+ def __init__ (
65+ self , package : Package , * dependencies : Dependency , with_sources : bool = False
66+ ) -> None :
67+ constraints = []
68+ for dep in dependencies :
69+ constraint = dep .to_pep_508 ()
70+ if dep .is_direct_origin ():
71+ # add version info because issue might be a version conflict
72+ # with a version constraint
73+ constraint += f" ({ dep .constraint } )"
74+ if with_sources and dep .source_name :
75+ constraint += f" ; source={ dep .source_name } "
76+ constraints .append (constraint )
6477 super ().__init__ (
65- f"Incompatible constraints in requirements of { package } :\n { constraints } "
78+ f"Incompatible constraints in requirements of { package } :\n "
79+ + "\n " .join (constraints )
6680 )
6781
6882
@@ -590,55 +604,15 @@ def complete_package(
590604
591605 self .debug (f"<debug>Duplicate dependencies for { dep_name } </debug>" )
592606
593- # Group dependencies for merging.
594- # We must not merge dependencies from different sources!
595- dep_groups = self ._group_by_source (deps )
596- deps = []
597- for group in dep_groups :
598- # In order to reduce the number of overrides we merge duplicate
599- # dependencies by constraint. For instance, if we have:
600- # • foo (>=2.0) ; python_version >= "3.6" and python_version < "3.7"
601- # • foo (>=2.0) ; python_version >= "3.7"
602- # we can avoid two overrides by merging them to:
603- # • foo (>=2.0) ; python_version >= "3.6"
604- # However, if we want to merge dependencies by constraint we have to
605- # merge dependencies by markers first in order to avoid unnecessary
606- # solver failures. For instance, if we have:
607- # • foo (>=2.0) ; python_version >= "3.6" and python_version < "3.7"
608- # • foo (>=2.0) ; python_version >= "3.7"
609- # • foo (<2.1) ; python_version >= "3.7"
610- # we must not merge the first two constraints but the last two:
611- # • foo (>=2.0) ; python_version >= "3.6" and python_version < "3.7"
612- # • foo (>=2.0,<2.1) ; python_version >= "3.7"
613- deps += self ._merge_dependencies_by_constraint (
614- self ._merge_dependencies_by_marker (group )
615- )
607+ # For dependency resolution, markers of duplicate dependencies must be
608+ # mutually exclusive.
609+ deps = self ._resolve_overlapping_markers (package , deps )
610+
616611 if len (deps ) == 1 :
617612 self .debug (f"<debug>Merging requirements for { deps [0 ]!s} </debug>" )
618613 dependencies .append (deps [0 ])
619614 continue
620615
621- # We leave dependencies as-is if they have the same
622- # python/platform constraints.
623- # That way the resolver will pickup the conflict
624- # and display a proper error.
625- seen = set ()
626- for dep in deps :
627- pep_508_dep = dep .to_pep_508 (False )
628- if ";" not in pep_508_dep :
629- _requirements = ""
630- else :
631- _requirements = pep_508_dep .split (";" )[1 ].strip ()
632-
633- if _requirements not in seen :
634- seen .add (_requirements )
635-
636- if len (deps ) != len (seen ):
637- for dep in deps :
638- dependencies .append (dep )
639-
640- continue
641-
642616 # At this point, we raise an exception that will
643617 # tell the solver to make new resolutions with specific overrides.
644618 #
@@ -664,8 +638,6 @@ def fmt_warning(d: Dependency) -> str:
664638 f"<warning>Different requirements found for { warnings } .</warning>"
665639 )
666640
667- deps = self ._handle_any_marker_dependencies (package , deps )
668-
669641 overrides = []
670642 overrides_marker_intersection : BaseMarker = AnyMarker ()
671643 for dep_overrides in self ._overrides .values ():
@@ -690,18 +662,18 @@ def fmt_warning(d: Dependency) -> str:
690662 clean_dependencies = []
691663 for dep in dependencies :
692664 if not dependency .transitive_marker .without_extras ().is_any ():
693- marker_intersection = (
665+ transitive_marker_intersection = (
694666 dependency .transitive_marker .without_extras ().intersect (
695667 dep .marker .without_extras ()
696668 )
697669 )
698- if marker_intersection .is_empty ():
670+ if transitive_marker_intersection .is_empty ():
699671 # The dependency is not needed, since the markers specified
700672 # for the current package selection are not compatible with
701673 # the markers for the current dependency, so we skip it
702674 continue
703675
704- dep .transitive_marker = marker_intersection
676+ dep .transitive_marker = transitive_marker_intersection
705677
706678 if not dependency .python_constraint .is_any ():
707679 python_constraint_intersection = dep .python_constraint .intersect (
@@ -845,118 +817,121 @@ def _merge_dependencies_by_constraint(
845817 """
846818 Merge dependencies with the same constraint
847819 by building a union of their markers.
848- """
849- by_constraint : dict [VersionConstraint , list [Dependency ]] = defaultdict (list )
850- for dep in dependencies :
851- by_constraint [dep .constraint ].append (dep )
852- for constraint , _deps in by_constraint .items ():
853- new_markers = [dep .marker for dep in _deps ]
854820
855- dep = _deps [0 ]
856-
857- # Union with EmptyMarker is to make sure we get the benefit of marker
858- # simplifications.
859- dep .marker = MarkerUnion (* new_markers ).union (EmptyMarker ())
860- by_constraint [constraint ] = [dep ]
861-
862- return [value [0 ] for value in by_constraint .values ()]
863-
864- def _merge_dependencies_by_marker (
865- self , dependencies : Iterable [Dependency ]
866- ) -> list [Dependency ]:
821+ For instance, if we have:
822+ - foo (>=2.0) ; python_version >= "3.6" and python_version < "3.7"
823+ - foo (>=2.0) ; python_version >= "3.7"
824+ we can avoid two overrides by merging them to:
825+ - foo (>=2.0) ; python_version >= "3.6"
867826 """
868- Merge dependencies with the same marker
869- by building the intersection of their constraints.
827+ dep_groups = self ._group_by_source (dependencies )
828+ merged_dependencies = []
829+ for group in dep_groups :
830+ by_constraint : dict [VersionConstraint , list [Dependency ]] = defaultdict (list )
831+ for dep in group :
832+ by_constraint [dep .constraint ].append (dep )
833+ for constraint , deps in by_constraint .items ():
834+ new_markers = [dep .marker for dep in deps ]
835+ dep = deps [0 ]
836+ dep .marker = marker_union (* new_markers )
837+ by_constraint [constraint ] = [dep ]
838+
839+ merged_dependencies += [value [0 ] for value in by_constraint .values ()]
840+
841+ return merged_dependencies
842+
843+ def _is_relevant_marker (self , marker : BaseMarker ) -> bool :
870844 """
871- by_marker : dict [BaseMarker , list [Dependency ]] = defaultdict (list )
872- for dep in dependencies :
873- by_marker [dep .marker ].append (dep )
874- deps = []
875- for _deps in by_marker .values ():
876- if len (_deps ) == 1 :
877- deps .extend (_deps )
878- else :
879- new_constraint = _deps [0 ].constraint
880- for dep in _deps [1 :]:
881- new_constraint = new_constraint .intersect (dep .constraint )
882- if new_constraint .is_empty ():
883- # leave dependencies as-is so the resolver will pickup
884- # the conflict and display a proper error.
885- deps .extend (_deps )
886- else :
887- self .debug (
888- f"<debug>Merging constraints for { _deps [0 ].name } for"
889- f" marker { _deps [0 ].marker } </debug>"
890- )
891- deps .append (_deps [0 ].with_constraint (new_constraint ))
892- return deps
845+ A marker is relevant if
846+ - it is not empty
847+ - allowed by the project's python constraint
848+ - allowed by the environment (only during installation)
849+ """
850+ return (
851+ not marker .is_empty ()
852+ and self ._python_constraint .allows_any (
853+ get_python_constraint_from_marker (marker )
854+ )
855+ and (not self ._env or marker .validate (self ._env .marker_env ))
856+ )
893857
894- def _handle_any_marker_dependencies (
858+ def _resolve_overlapping_markers (
895859 self , package : Package , dependencies : list [Dependency ]
896860 ) -> list [Dependency ]:
897861 """
898- We need to check if one of the duplicate dependencies
899- has no markers. If there is one, we need to change its
900- environment markers to the inverse of the union of the
901- other dependencies markers.
902- For instance, if we have the following dependencies:
903- • ipython
904- • ipython (1.2.4) ; implementation_name == "pypy"
905-
906- the marker for `ipython` will become `implementation_name != "pypy"`.
907-
908- Further, we have to merge the constraints of the requirements
909- without markers into the constraints of the requirements with markers.
910- for instance, if we have the following dependencies:
911- • foo (>= 1.2)
912- • foo (!= 1.2.1) ; python == 3.10
913-
914- the constraint for the second entry will become (!= 1.2.1, >= 1.2).
862+ Convert duplicate dependencies with potentially overlapping markers
863+ into duplicate dependencies with mutually exclusive markers.
864+
865+ Therefore, the intersections of all combinations of markers and inverted markers
866+ have to be calculated. If such an intersection is relevant (not empty, etc.),
867+ the intersection of all constraints, whose markers were not inverted is built
868+ and a new dependency with the calculated version constraint and marker is added.
869+ (The marker of such a dependency does not overlap with the marker
870+ of any other new dependency.)
915871 """
916- any_markers_dependencies = [d for d in dependencies if d .marker .is_any ()]
917- other_markers_dependencies = [d for d in dependencies if not d .marker .is_any ()]
918-
919- if any_markers_dependencies :
920- for dep_other in other_markers_dependencies :
921- new_constraint = dep_other .constraint
922- for dep_any in any_markers_dependencies :
923- new_constraint = new_constraint .intersect (dep_any .constraint )
924- if new_constraint .is_empty ():
925- raise IncompatibleConstraintsError (
926- package , dep_other , * any_markers_dependencies
927- )
928- dep_other .constraint = new_constraint
929-
930- marker = other_markers_dependencies [0 ].marker
931- for other_dep in other_markers_dependencies [1 :]:
932- marker = marker .union (other_dep .marker )
933- inverted_marker = marker .invert ()
934-
935- if (
936- not inverted_marker .is_empty ()
937- and self ._python_constraint .allows_any (
938- get_python_constraint_from_marker (inverted_marker )
872+ # In order to reduce the number of intersections,
873+ # we merge duplicate dependencies by constraint.
874+ dependencies = self ._merge_dependencies_by_constraint (dependencies )
875+
876+ new_dependencies = []
877+ for uses in itertools .product ([True , False ], repeat = len (dependencies )):
878+ # intersection of markers
879+ used_marker_intersection = marker_intersection (
880+ * (
881+ dep .marker if use else dep .marker .invert ()
882+ for use , dep in zip (uses , dependencies )
883+ )
939884 )
940- and (not self ._env or inverted_marker .validate (self ._env .marker_env ))
941- ):
942- if any_markers_dependencies :
943- for dep_any in any_markers_dependencies :
944- dep_any .marker = inverted_marker
945- else :
946- # If there is no any marker dependency
947- # and the inverted marker is not empty,
948- # a dependency with the inverted union of all markers is required
949- # in order to not miss other dependencies later, for instance:
885+ if not self ._is_relevant_marker (used_marker_intersection ):
886+ continue
887+
888+ # intersection of constraints
889+ constraint : VersionConstraint = VersionRange ()
890+ specific_source_dependency = None
891+ used_dependencies = list (itertools .compress (dependencies , uses ))
892+ for dep in used_dependencies :
893+ if dep .is_direct_origin () or dep .source_name :
894+ # if direct origin or specific source:
895+ # conflict if specific source already set and not the same
896+ if specific_source_dependency and (
897+ not dep .is_same_source_as (specific_source_dependency )
898+ or dep .source_name != specific_source_dependency .source_name
899+ ):
900+ raise IncompatibleConstraintsError (
901+ package , dep , specific_source_dependency , with_sources = True
902+ )
903+ specific_source_dependency = dep
904+ constraint = constraint .intersect (dep .constraint )
905+ if constraint .is_empty ():
906+ # conflict in overlapping area
907+ raise IncompatibleConstraintsError (package , * used_dependencies )
908+
909+ if set (uses ) == {False }:
910+ # This is an edge case where the dependency is not required
911+ # for the resulting marker. However, we have to consider it anyway
912+ # in order to not miss other dependencies later, for instance:
950913 # • foo (1.0) ; python == 3.7
951914 # • foo (2.0) ; python == 3.8
952915 # • bar (2.0) ; python == 3.8
953916 # • bar (3.0) ; python == 3.9
954- #
955917 # the last dependency would be missed without this,
956918 # because the intersection with both foo dependencies is empty.
957- inverted_marker_dep = dependencies [0 ].with_constraint (EmptyConstraint ())
958- inverted_marker_dep .marker = inverted_marker
959- dependencies .append (inverted_marker_dep )
960- else :
961- dependencies = other_markers_dependencies
962- return dependencies
919+
920+ # Set constraint to empty to mark dependency as "not required".
921+ constraint = EmptyConstraint ()
922+ used_dependencies = dependencies
923+
924+ # build new dependency with intersected constraint and marker
925+ # (and correct source)
926+ new_dep = (
927+ specific_source_dependency
928+ if specific_source_dependency
929+ else used_dependencies [0 ]
930+ ).with_constraint (constraint )
931+ new_dep .marker = used_marker_intersection
932+ new_dependencies .append (new_dep )
933+
934+ # In order to reduce the number of overrides we merge duplicate
935+ # dependencies by constraint again. After overlapping markers were
936+ # resolved, there might be new dependencies with the same constraint.
937+ return self ._merge_dependencies_by_constraint (new_dependencies )
0 commit comments