@@ -104,14 +104,7 @@ def resolve(
104104 req_type = req_type ,
105105 ignore_platform = ignore_platform ,
106106 )
107- # Apply cooldown settings to sdist resolution only. Wheel-only lookups
108- # (cache servers, pre_built packages) use a different trust model.
109- pbi = ctx .package_build_info (req )
110- provider .cooldown = (
111- resolve_package_cooldown (ctx .cooldown , pbi .resolver_min_release_age )
112- if include_sdists
113- else None
114- )
107+ provider .cooldown = resolve_package_cooldown (ctx , req )
115108 results = find_all_matching_from_provider (provider , req )
116109 return results [0 ]
117110
@@ -143,19 +136,20 @@ def default_resolver_provider(
143136
144137
145138def resolve_package_cooldown (
146- global_cooldown : Cooldown | None ,
147- per_package_days : int | None ,
139+ ctx : context . WorkContext ,
140+ req : Requirement ,
148141) -> Cooldown | None :
149142 """Compute the effective cooldown for a single package.
150143
151144 Args:
152- global_cooldown: The run-wide cooldown from ``--min-release-age``.
153- per_package_days: The ``resolver_dist.min_release_age`` setting for
154- this package (None = inherit, 0 = disabled, positive = override).
145+ ctx: The current work context (provides the global cooldown).
146+ req: The package requirement being resolved.
155147
156148 Returns:
157149 The cooldown to pass to the provider, or ``None`` if disabled.
158150 """
151+ per_package_days = ctx .package_build_info (req ).resolver_min_release_age
152+ global_cooldown = ctx .cooldown
159153 if per_package_days is None :
160154 return global_cooldown
161155 if per_package_days == 0 :
@@ -470,6 +464,7 @@ def get_project_from_pypi(
470464class BaseProvider (ExtrasProvider ):
471465 resolver_cache : typing .ClassVar [ResolverCache ] = {}
472466 provider_description : typing .ClassVar [str ]
467+ _cooldown_unsupported_warned : typing .ClassVar [set [str ]] = set ()
473468
474469 def __init__ (
475470 self ,
@@ -486,16 +481,11 @@ def __init__(
486481
487482 # cooldown specific settings
488483 self .cooldown = cooldown
489- self .cooldown_unsupported_previously_warned = False
490- """
491- Does this provider supply upload timestamps for candidates?
492-
493- Defaults to False (safe/unknown). Subclasses that reliably populate
494- upload_time on every candidate should set this to True in their __init__.
495-
496- When a cooldown is active and this is False, the cooldown check is
497- skipped with a warning rather than failing closed.
498- """
484+ # Does this provider supply upload timestamps for candidates?
485+ # Defaults to False (safe/unknown). Subclasses that reliably populate
486+ # upload_time on every candidate should set this to True in their __init__.
487+ # When a cooldown is active and this is False, the cooldown check is
488+ # skipped with a warning rather than failing closed.
499489 self .supports_upload_time : bool = False
500490
501491 @property
@@ -607,47 +597,62 @@ def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> boo
607597 )
608598 return False
609599
610- # Release-age cooldown: reject candidates published too recently.
611- if self .cooldown is not None :
612- if candidate .upload_time is None :
613- if not self .supports_upload_time :
614- # Provider does not yet support timestamp retrieval (e.g. GitHub).
615- # Warn once per provider instance (i.e. per package) rather than
616- # once per candidate.
617- if not self .cooldown_unsupported_previously_warned :
618- self .cooldown_unsupported_previously_warned = True
619- logger .warning (
620- "%s: release-age cooldown cannot be enforced — upload "
621- "timestamp support is not yet implemented for %s; "
622- "cooldown check skipped" ,
623- requirement .name ,
624- self .get_provider_description (),
625- )
626- return True
627- # Provider should supply timestamps but this candidate is missing one.
628- # Fail closed: we cannot verify the age, so reject it.
629- if DEBUG_RESOLVER :
630- logger .debug (
631- "%s: skipping %s — upload_time unknown, required for cooldown" ,
632- requirement .name ,
633- candidate .version ,
634- )
635- return False
636- cutoff = self .cooldown .bootstrap_time - self .cooldown .min_age
637- if candidate .upload_time > cutoff :
638- if DEBUG_RESOLVER :
639- age = self .cooldown .bootstrap_time - candidate .upload_time
640- logger .debug (
641- "%s: skipping %s uploaded %s ago (cooldown: %s)" ,
642- requirement .name ,
643- candidate .version ,
644- age ,
645- self .cooldown .min_age ,
646- )
647- return False
600+ if self .is_blocked_by_cooldown (candidate ):
601+ return False
648602
649603 return True
650604
605+ def is_blocked_by_cooldown (self , candidate : Candidate ) -> bool :
606+ """Return True if the candidate is rejected by the release-age cooldown."""
607+
608+ # a cooldown is not specified...
609+ if self .cooldown is None :
610+ return False
611+
612+ # the target candidate doesn't provide a valid upload timestamp
613+ if candidate .upload_time is None :
614+ if not self .supports_upload_time :
615+ # this provider does not yet support timestamp retrieval (e.g. GitHub).
616+ # Warn once per package name across all provider instances.
617+ if candidate .name not in BaseProvider ._cooldown_unsupported_warned :
618+ BaseProvider ._cooldown_unsupported_warned .add (candidate .name )
619+ logger .warning (
620+ "%s: release-age cooldown cannot be enforced — upload "
621+ "timestamp support is not yet implemented for %s; "
622+ "cooldown check skipped" ,
623+ candidate .name ,
624+ self .get_provider_description (),
625+ )
626+ return False
627+ # this provider is expected to supply timestamps,
628+ # but this candidate is missing one.
629+ # Fail closed: we cannot verify the age of this candidate, so reject it.
630+ if DEBUG_RESOLVER :
631+ logger .debug (
632+ "%s: skipping %s — upload_time unknown, required for cooldown" ,
633+ candidate .name ,
634+ candidate .version ,
635+ )
636+ return True
637+
638+ # cooldowns are enabled, and this candidate has a valid upload timestamp
639+ # so we can do the math to determine whether or not the candidate should
640+ # be blocked/skipped
641+ cutoff = self .cooldown .bootstrap_time - self .cooldown .min_age
642+ if candidate .upload_time > cutoff :
643+ # if this candidate is "too new", block/skip it
644+ if DEBUG_RESOLVER :
645+ age = self .cooldown .bootstrap_time - candidate .upload_time
646+ logger .debug (
647+ "%s: skipping %s uploaded %s ago (cooldown: %s)" ,
648+ candidate .name ,
649+ candidate .version ,
650+ age ,
651+ self .cooldown .min_age ,
652+ )
653+ return True
654+ return False
655+
651656 def get_dependencies (self , candidate : Candidate ) -> list [Requirement ]:
652657 # return candidate.dependencies
653658 return []
@@ -751,7 +756,7 @@ def __init__(
751756 use_resolver_cache : bool = True ,
752757 override_download_url : str | None = None ,
753758 cooldown : Cooldown | None = None ,
754- supports_upload_time : bool = True ,
759+ supports_upload_time : bool | None = None ,
755760 ):
756761 super ().__init__ (
757762 constraints = constraints ,
@@ -760,13 +765,10 @@ def __init__(
760765 cooldown = cooldown ,
761766 )
762767
763- # not all PyPI indexes reliably support
764- # https://peps.python.org/pep-0691/, which is necessary
765- # to reliably supply upload times for packages
766- #
767- # consumers of this provider which specify a custom sdist_server_url
768- # that only provides support for https://peps.python.org/pep-0503/
769- # Simple Repository API should specify support_upload_time=False
768+ # Only pypi.org reliably supports PEP 691 upload timestamps.
769+ # Default to True for pypi.org, False for all other indexes.
770+ if supports_upload_time is None :
771+ supports_upload_time = sdist_server_url .startswith (PYPI_SERVER_URL )
770772 self .supports_upload_time = supports_upload_time
771773 self .include_sdists = include_sdists
772774 self .include_wheels = include_wheels
0 commit comments