62
62
ExplicitRequirement ,
63
63
RequiresPythonRequirement ,
64
64
SpecifierRequirement ,
65
+ SpecifierWithoutExtrasRequirement ,
65
66
UnsatisfiableRequirement ,
66
67
)
67
68
@@ -141,12 +142,14 @@ def _make_extras_candidate(
141
142
self ,
142
143
base : BaseCandidate ,
143
144
extras : FrozenSet [str ],
145
+ * ,
146
+ comes_from : Optional [InstallRequirement ] = None ,
144
147
) -> ExtrasCandidate :
145
148
cache_key = (id (base ), frozenset (canonicalize_name (e ) for e in extras ))
146
149
try :
147
150
candidate = self ._extras_candidate_cache [cache_key ]
148
151
except KeyError :
149
- candidate = ExtrasCandidate (base , extras )
152
+ candidate = ExtrasCandidate (base , extras , comes_from = comes_from )
150
153
self ._extras_candidate_cache [cache_key ] = candidate
151
154
return candidate
152
155
@@ -163,7 +166,7 @@ def _make_candidate_from_dist(
163
166
self ._installed_candidate_cache [dist .canonical_name ] = base
164
167
if not extras :
165
168
return base
166
- return self ._make_extras_candidate (base , extras )
169
+ return self ._make_extras_candidate (base , extras , comes_from = template )
167
170
168
171
def _make_candidate_from_link (
169
172
self ,
@@ -225,7 +228,7 @@ def _make_candidate_from_link(
225
228
226
229
if not extras :
227
230
return base
228
- return self ._make_extras_candidate (base , extras )
231
+ return self ._make_extras_candidate (base , extras , comes_from = template )
229
232
230
233
def _iter_found_candidates (
231
234
self ,
@@ -387,16 +390,21 @@ def find_candidates(
387
390
if ireq is not None :
388
391
ireqs .append (ireq )
389
392
390
- # If the current identifier contains extras, add explicit candidates
391
- # from entries from extra-less identifier.
393
+ # If the current identifier contains extras, add requires and explicit
394
+ # candidates from entries from extra-less identifier.
392
395
with contextlib .suppress (InvalidRequirement ):
393
396
parsed_requirement = get_requirement (identifier )
394
- explicit_candidates .update (
395
- self ._iter_explicit_candidates_from_base (
396
- requirements .get (parsed_requirement .name , ()),
397
- frozenset (parsed_requirement .extras ),
398
- ),
399
- )
397
+ if parsed_requirement .name != identifier :
398
+ explicit_candidates .update (
399
+ self ._iter_explicit_candidates_from_base (
400
+ requirements .get (parsed_requirement .name , ()),
401
+ frozenset (parsed_requirement .extras ),
402
+ ),
403
+ )
404
+ for req in requirements .get (parsed_requirement .name , []):
405
+ _ , ireq = req .get_candidate_lookup ()
406
+ if ireq is not None :
407
+ ireqs .append (ireq )
400
408
401
409
# Add explicit candidates from constraints. We only do this if there are
402
410
# known ireqs, which represent requirements not already explicit. If
@@ -439,37 +447,49 @@ def find_candidates(
439
447
and all (req .is_satisfied_by (c ) for req in requirements [identifier ])
440
448
)
441
449
442
- def _make_requirement_from_install_req (
450
+ def _make_requirements_from_install_req (
443
451
self , ireq : InstallRequirement , requested_extras : Iterable [str ]
444
- ) -> Optional [Requirement ]:
452
+ ) -> Iterator [Requirement ]:
453
+ """
454
+ Returns requirement objects associated with the given InstallRequirement. In
455
+ most cases this will be a single object but the following special cases exist:
456
+ - the InstallRequirement has markers that do not apply -> result is empty
457
+ - the InstallRequirement has both a constraint and extras -> result is split
458
+ in two requirement objects: one with the constraint and one with the
459
+ extra. This allows centralized constraint handling for the base,
460
+ resulting in fewer candidate rejections.
461
+ """
445
462
if not ireq .match_markers (requested_extras ):
446
463
logger .info (
447
464
"Ignoring %s: markers '%s' don't match your environment" ,
448
465
ireq .name ,
449
466
ireq .markers ,
450
467
)
451
- return None
452
- if not ireq .link :
453
- return SpecifierRequirement (ireq )
454
- self ._fail_if_link_is_unsupported_wheel (ireq .link )
455
- cand = self ._make_candidate_from_link (
456
- ireq .link ,
457
- extras = frozenset (ireq .extras ),
458
- template = ireq ,
459
- name = canonicalize_name (ireq .name ) if ireq .name else None ,
460
- version = None ,
461
- )
462
- if cand is None :
463
- # There's no way we can satisfy a URL requirement if the underlying
464
- # candidate fails to build. An unnamed URL must be user-supplied, so
465
- # we fail eagerly. If the URL is named, an unsatisfiable requirement
466
- # can make the resolver do the right thing, either backtrack (and
467
- # maybe find some other requirement that's buildable) or raise a
468
- # ResolutionImpossible eventually.
469
- if not ireq .name :
470
- raise self ._build_failures [ireq .link ]
471
- return UnsatisfiableRequirement (canonicalize_name (ireq .name ))
472
- return self .make_requirement_from_candidate (cand )
468
+ elif not ireq .link :
469
+ if ireq .extras and ireq .req is not None and ireq .req .specifier :
470
+ yield SpecifierWithoutExtrasRequirement (ireq )
471
+ yield SpecifierRequirement (ireq )
472
+ else :
473
+ self ._fail_if_link_is_unsupported_wheel (ireq .link )
474
+ cand = self ._make_candidate_from_link (
475
+ ireq .link ,
476
+ extras = frozenset (ireq .extras ),
477
+ template = ireq ,
478
+ name = canonicalize_name (ireq .name ) if ireq .name else None ,
479
+ version = None ,
480
+ )
481
+ if cand is None :
482
+ # There's no way we can satisfy a URL requirement if the underlying
483
+ # candidate fails to build. An unnamed URL must be user-supplied, so
484
+ # we fail eagerly. If the URL is named, an unsatisfiable requirement
485
+ # can make the resolver do the right thing, either backtrack (and
486
+ # maybe find some other requirement that's buildable) or raise a
487
+ # ResolutionImpossible eventually.
488
+ if not ireq .name :
489
+ raise self ._build_failures [ireq .link ]
490
+ yield UnsatisfiableRequirement (canonicalize_name (ireq .name ))
491
+ else :
492
+ yield self .make_requirement_from_candidate (cand )
473
493
474
494
def collect_root_requirements (
475
495
self , root_ireqs : List [InstallRequirement ]
@@ -490,30 +510,51 @@ def collect_root_requirements(
490
510
else :
491
511
collected .constraints [name ] = Constraint .from_ireq (ireq )
492
512
else :
493
- req = self ._make_requirement_from_install_req (
494
- ireq ,
495
- requested_extras = (),
513
+ reqs = list (
514
+ self ._make_requirements_from_install_req (
515
+ ireq ,
516
+ requested_extras = (),
517
+ )
496
518
)
497
- if req is None :
519
+ if not reqs :
498
520
continue
499
- if ireq .user_supplied and req .name not in collected .user_requested :
500
- collected .user_requested [req .name ] = i
501
- collected .requirements .append (req )
521
+ template = reqs [0 ]
522
+ if ireq .user_supplied and template .name not in collected .user_requested :
523
+ collected .user_requested [template .name ] = i
524
+ collected .requirements .extend (reqs )
525
+ # Put requirements with extras at the end of the root requires. This does not
526
+ # affect resolvelib's picking preference but it does affect its initial criteria
527
+ # population: by putting extras at the end we enable the candidate finder to
528
+ # present resolvelib with a smaller set of candidates to resolvelib, already
529
+ # taking into account any non-transient constraints on the associated base. This
530
+ # means resolvelib will have fewer candidates to visit and reject.
531
+ # Python's list sort is stable, meaning relative order is kept for objects with
532
+ # the same key.
533
+ collected .requirements .sort (key = lambda r : r .name != r .project_name )
502
534
return collected
503
535
504
536
def make_requirement_from_candidate (
505
537
self , candidate : Candidate
506
538
) -> ExplicitRequirement :
507
539
return ExplicitRequirement (candidate )
508
540
509
- def make_requirement_from_spec (
541
+ def make_requirements_from_spec (
510
542
self ,
511
543
specifier : str ,
512
544
comes_from : Optional [InstallRequirement ],
513
545
requested_extras : Iterable [str ] = (),
514
- ) -> Optional [Requirement ]:
546
+ ) -> Iterator [Requirement ]:
547
+ """
548
+ Returns requirement objects associated with the given specifier. In most cases
549
+ this will be a single object but the following special cases exist:
550
+ - the specifier has markers that do not apply -> result is empty
551
+ - the specifier has both a constraint and extras -> result is split
552
+ in two requirement objects: one with the constraint and one with the
553
+ extra. This allows centralized constraint handling for the base,
554
+ resulting in fewer candidate rejections.
555
+ """
515
556
ireq = self ._make_install_req_from_spec (specifier , comes_from )
516
- return self ._make_requirement_from_install_req (ireq , requested_extras )
557
+ return self ._make_requirements_from_install_req (ireq , requested_extras )
517
558
518
559
def make_requires_python_requirement (
519
560
self ,
0 commit comments