6262    ExplicitRequirement,
6363    RequiresPythonRequirement,
6464    SpecifierRequirement,
65+     SpecifierWithoutExtrasRequirement,
6566    UnsatisfiableRequirement,
6667)
6768
@@ -141,12 +142,14 @@ def _make_extras_candidate(
141142        self,
142143        base: BaseCandidate,
143144        extras: FrozenSet[str],
145+         *,
146+         comes_from: Optional[InstallRequirement] = None,
144147    ) -> ExtrasCandidate:
145148        cache_key = (id(base), frozenset(canonicalize_name(e) for e in extras))
146149        try:
147150            candidate = self._extras_candidate_cache[cache_key]
148151        except KeyError:
149-             candidate = ExtrasCandidate(base, extras)
152+             candidate = ExtrasCandidate(base, extras, comes_from=comes_from )
150153            self._extras_candidate_cache[cache_key] = candidate
151154        return candidate
152155
@@ -163,7 +166,7 @@ def _make_candidate_from_dist(
163166            self._installed_candidate_cache[dist.canonical_name] = base
164167        if not extras:
165168            return base
166-         return self._make_extras_candidate(base, extras)
169+         return self._make_extras_candidate(base, extras, comes_from=template )
167170
168171    def _make_candidate_from_link(
169172        self,
@@ -225,7 +228,7 @@ def _make_candidate_from_link(
225228
226229        if not extras:
227230            return base
228-         return self._make_extras_candidate(base, extras)
231+         return self._make_extras_candidate(base, extras, comes_from=template )
229232
230233    def _iter_found_candidates(
231234        self,
@@ -387,16 +390,21 @@ def find_candidates(
387390            if ireq is not None:
388391                ireqs.append(ireq)
389392
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.
392395        with contextlib.suppress(InvalidRequirement):
393396            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)
400408
401409        # Add explicit candidates from constraints. We only do this if there are
402410        # known ireqs, which represent requirements not already explicit. If
@@ -439,37 +447,49 @@ def find_candidates(
439447            and all(req.is_satisfied_by(c) for req in requirements[identifier])
440448        )
441449
442-     def _make_requirement_from_install_req (
450+     def _make_requirements_from_install_req (
443451        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+         """
445462        if not ireq.match_markers(requested_extras):
446463            logger.info(
447464                "Ignoring %s: markers '%s' don't match your environment",
448465                ireq.name,
449466                ireq.markers,
450467            )
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)
473493
474494    def collect_root_requirements(
475495        self, root_ireqs: List[InstallRequirement]
@@ -490,30 +510,51 @@ def collect_root_requirements(
490510                else:
491511                    collected.constraints[name] = Constraint.from_ireq(ireq)
492512            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+                     )
496518                )
497-                 if req is None :
519+                 if not reqs :
498520                    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)
502534        return collected
503535
504536    def make_requirement_from_candidate(
505537        self, candidate: Candidate
506538    ) -> ExplicitRequirement:
507539        return ExplicitRequirement(candidate)
508540
509-     def make_requirement_from_spec (
541+     def make_requirements_from_spec (
510542        self,
511543        specifier: str,
512544        comes_from: Optional[InstallRequirement],
513545        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+         """
515556        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)
517558
518559    def make_requires_python_requirement(
519560        self,
0 commit comments