88from collections import defaultdict
99from contextlib import contextmanager
1010from typing import TYPE_CHECKING
11+ from typing import Any
1112from typing import ClassVar
1213from typing import cast
1314
1718from poetry .core .constraints .version import VersionRange
1819from poetry .core .packages .utils .utils import get_python_constraint_from_marker
1920from poetry .core .version .markers import AnyMarker
21+ from poetry .core .version .markers import parse_marker
2022from poetry .core .version .markers import union as marker_union
2123
2224from poetry .mixology .incompatibility import Incompatibility
@@ -115,6 +117,7 @@ def __init__(
115117 io : IO ,
116118 * ,
117119 locked : list [Package ] | None = None ,
120+ active_root_extras : Collection [NormalizedName ] | None = None ,
118121 ) -> None :
119122 self ._package = package
120123 self ._pool = pool
@@ -130,6 +133,9 @@ def __init__(
130133 self ._direct_origin_packages : dict [str , Package ] = {}
131134 self ._locked : dict [NormalizedName , list [DependencyPackage ]] = defaultdict (list )
132135 self ._use_latest : Collection [NormalizedName ] = []
136+ self ._active_root_extras = (
137+ frozenset (active_root_extras ) if active_root_extras is not None else None
138+ )
133139
134140 self ._explicit_sources : dict [str , str ] = {}
135141 for package in locked or []:
@@ -416,21 +422,12 @@ def incompatibilities_for(
416422 )
417423 ]
418424
419- _dependencies = [
420- dep
421- for dep in dependencies
422- if dep .name not in self .UNSAFE_PACKAGES
423- and self ._python_constraint .allows_any (dep .python_constraint )
424- and (not self ._env or dep .marker .validate (self ._env .marker_env ))
425- ]
426- dependencies = self ._get_dependencies_with_overrides (_dependencies , package )
427-
428425 return [
429426 Incompatibility (
430427 [Term (package .to_dependency (), True ), Term (dep , False )],
431428 DependencyCauseError (),
432429 )
433- for dep in dependencies
430+ for dep in self . _get_dependencies_with_overrides ( dependencies , package )
434431 ]
435432
436433 def complete_package (
@@ -480,7 +477,7 @@ def complete_package(
480477 package = dependency_package .package
481478 dependency = dependency_package .dependency
482479 new_dependency = package .without_features ().to_dependency ()
483- new_dependency .marker = AnyMarker ()
480+ new_dependency .marker = dependency . marker
484481
485482 # When adding dependency foo[extra] -> foo, preserve foo's source, if it's
486483 # specified. This prevents us from trying to get foo from PyPI
@@ -497,8 +494,14 @@ def complete_package(
497494 if dep .name in self .UNSAFE_PACKAGES :
498495 continue
499496
500- if self ._env and not dep .marker .validate (self ._env .marker_env ):
501- continue
497+ if self ._env :
498+ marker_values = (
499+ self ._marker_values (self ._active_root_extras )
500+ if package .is_root ()
501+ else self ._env .marker_env
502+ )
503+ if not dep .marker .validate (marker_values ):
504+ continue
502505
503506 if not package .is_root () and (
504507 (dep .is_optional () and dep .name not in optional_dependencies )
@@ -509,6 +512,24 @@ def complete_package(
509512 ):
510513 continue
511514
515+ # For normal dependency resolution, we have to make sure that root extras
516+ # are represented in the markers. This is required to identify mutually
517+ # exclusive markers in cases like 'extra == "foo"' and 'extra != "foo"'.
518+ # However, for installation with re-resolving (installer.re-resolve=true,
519+ # which results in self._env being not None), this spoils the result
520+ # because we have to keep extras so that they are uninstalled
521+ # when calculating the operations of the transaction.
522+ if self ._env is None and package .is_root () and dep .in_extras :
523+ # The clone is required for installation with re-resolving
524+ # without an existing lock file because the root package is used
525+ # once for solving and a second time for re-resolving for installation.
526+ dep = dep .clone ()
527+ dep .marker = dep .marker .intersect (
528+ parse_marker (
529+ " or " .join (f'extra == "{ extra } "' for extra in dep .in_extras )
530+ )
531+ )
532+
512533 _dependencies .append (dep )
513534
514535 if self ._load_deferred :
@@ -545,7 +566,7 @@ def complete_package(
545566 # • pypiwin32 (219); sys_platform == "win32" and python_version < "3.6"
546567 duplicates : dict [str , list [Dependency ]] = defaultdict (list )
547568 for dep in dependencies :
548- duplicates [dep .complete_name ].append (dep )
569+ duplicates [dep .name ].append (dep )
549570
550571 dependencies = []
551572 for dep_name , deps in duplicates .items ():
@@ -556,9 +577,39 @@ def complete_package(
556577 self .debug (f"<debug>Duplicate dependencies for { dep_name } </debug>" )
557578
558579 # For dependency resolution, markers of duplicate dependencies must be
559- # mutually exclusive.
560- active_extras = None if package .is_root () else dependency .extras
561- deps = self ._resolve_overlapping_markers (package , deps , active_extras )
580+ # mutually exclusive. However, we have to take care about duplicates
581+ # with differing extras.
582+ duplicates_by_extras : dict [str , list [Dependency ]] = defaultdict (list )
583+ for dep in deps :
584+ duplicates_by_extras [dep .complete_name ].append (dep )
585+
586+ if len (duplicates_by_extras ) == 1 :
587+ active_extras = (
588+ self ._active_root_extras if package .is_root () else dependency .extras
589+ )
590+ deps = self ._resolve_overlapping_markers (package , deps , active_extras )
591+ else :
592+ # There are duplicates with different extras.
593+ for complete_dep_name , deps_by_extra in duplicates_by_extras .items ():
594+ if len (deps_by_extra ) > 1 :
595+ duplicates_by_extras [complete_dep_name ] = (
596+ self ._resolve_overlapping_markers (package , deps , None )
597+ )
598+ if all (len (d ) == 1 for d in duplicates_by_extras .values ()) and all (
599+ d1 [0 ].marker .intersect (d2 [0 ].marker ).is_empty ()
600+ for d1 , d2 in itertools .combinations (
601+ duplicates_by_extras .values (), 2
602+ )
603+ ):
604+ # Since all markers are mutually exclusive,
605+ # we can trigger overrides.
606+ deps = list (itertools .chain (* duplicates_by_extras .values ()))
607+ else :
608+ # Too complicated to handle with overrides,
609+ # fallback to basic handling without overrides.
610+ for d in duplicates_by_extras .values ():
611+ dependencies .extend (d )
612+ continue
562613
563614 if len (deps ) == 1 :
564615 self .debug (f"<debug>Merging requirements for { dep_name } </debug>" )
@@ -909,3 +960,19 @@ def _resolve_overlapping_markers(
909960 # dependencies by constraint again. After overlapping markers were
910961 # resolved, there might be new dependencies with the same constraint.
911962 return self ._merge_dependencies_by_constraint (new_dependencies )
963+
964+ def _marker_values (
965+ self , extras : Collection [NormalizedName ] | None = None
966+ ) -> dict [str , Any ]:
967+ """
968+ Marker values, from `self._env` if present plus the supplied extras
969+
970+ :param extras: the values to add to the 'extra' marker value
971+ """
972+ result = self ._env .marker_env .copy () if self ._env is not None else {}
973+ if extras is not None :
974+ assert (
975+ "extra" not in result
976+ ), "'extra' marker key is already present in environment"
977+ result ["extra" ] = set (extras )
978+ return result
0 commit comments