55#
66from __future__ import annotations
77
8+ import datetime
89import functools
910import logging
1011import os
4041if typing .TYPE_CHECKING :
4142 from . import context
4243
44+
4345logger = logging .getLogger (__name__ )
4446
4547PYTHON_VERSION = Version (python_version ())
@@ -339,6 +341,10 @@ def get_project_from_pypi(
339341 ignored_candidates .add (dp .filename )
340342 continue
341343
344+ upload_time = dp .upload_time
345+ if upload_time is not None :
346+ upload_time = upload_time .astimezone (datetime .UTC )
347+
342348 c = Candidate (
343349 name = name ,
344350 version = version ,
@@ -347,6 +353,7 @@ def get_project_from_pypi(
347353 is_sdist = is_sdist ,
348354 build_tag = build_tag ,
349355 has_metadata = bool (dp .has_metadata ),
356+ upload_time = upload_time ,
350357 )
351358 if DEBUG_RESOLVER :
352359 logger .debug ("candidate %s (%s) %s" , dp .filename , c , dp .url )
@@ -367,7 +374,7 @@ def get_project_from_pypi(
367374]
368375VersionSource : typing .TypeAlias = typing .Callable [
369376 [str ],
370- typing .Iterable [tuple [str , str | Version ]],
377+ typing .Iterable [Candidate | tuple [str , str | Version ]],
371378]
372379
373380
@@ -714,17 +721,28 @@ def cache_key(self) -> str:
714721 def find_candidates (self , identifier ) -> Candidates :
715722 candidates : list [Candidate ] = []
716723 version : Version | None
717- for url , item in self ._version_source (identifier ):
718- if isinstance (item , Version ):
719- version = item
724+ for item in self ._version_source (identifier ):
725+ if isinstance (item , Candidate ):
726+ candidate = item
720727 else :
721- version = self ._match_function (identifier , item )
722- if version is None :
723- logger .debug (f"{ identifier } : match function ignores { item } " )
724- continue
725- assert isinstance (version , Version )
726- version = version
727- candidates .append (Candidate (name = identifier , version = version , url = url ))
728+ # TODO: deprecate (url, version_or_string)
729+ url , version_or_string = item
730+ if isinstance (version_or_string , Version ):
731+ version = version_or_string
732+ else :
733+ match_result = self ._match_function (identifier , version_or_string )
734+ if match_result is None :
735+ logger .debug (
736+ f"{ identifier } : match function ignores { version_or_string } "
737+ )
738+ continue
739+ assert isinstance (match_result , Version )
740+ version = match_result
741+
742+ candidate = Candidate (name = identifier , version = version , url = url )
743+
744+ candidates .append (candidate )
745+
728746 return candidates
729747
730748
@@ -770,7 +788,7 @@ def cache_key(self) -> str:
770788 def _find_tags (
771789 self ,
772790 identifier : str ,
773- ) -> Iterable [tuple [ str , Version ] ]:
791+ ) -> Iterable [Candidate ]:
774792 headers = {"accept" : "application/vnd.github+json" }
775793
776794 # Add GitHub authentication if available
@@ -784,13 +802,24 @@ def _find_tags(
784802 resp .raise_for_status ()
785803
786804 for entry in resp .json ():
787- name = entry ["name" ]
788- result = self ._match_function (identifier , name )
789- if result is None :
790- logger .debug (f"{ identifier } : match function ignores { name } " )
805+ tagname = entry ["name" ]
806+ version = self ._match_function (identifier , tagname )
807+ if version is None :
808+ logger .debug (f"{ identifier } : match function ignores { tagname } " )
791809 continue
792- assert isinstance (result , Version )
793- yield entry ["tarball_url" ], result
810+ assert isinstance (version , Version )
811+ url = entry ["tarball_url" ]
812+
813+ # Github tag API endpoint does not include commit date information.
814+ # It would be too expensive to query every commit API endpoint.
815+ yield Candidate (
816+ name = identifier ,
817+ version = version ,
818+ url = url ,
819+ remote_tag = tagname ,
820+ remote_commit = entry ["commit" ]["sha" ],
821+ upload_time = None ,
822+ )
794823
795824 # pagination links
796825 nexturl = resp .links .get ("next" , {}).get ("url" )
@@ -841,25 +870,43 @@ def cache_key(self) -> str:
841870 def _find_tags (
842871 self ,
843872 identifier : str ,
844- ) -> Iterable [tuple [ str , Version ] ]:
873+ ) -> Iterable [Candidate ]:
845874 nexturl : str = self .api_url
875+ created_at : datetime .datetime | None
846876 while nexturl :
847877 resp : Response = session .get (nexturl )
848878 resp .raise_for_status ()
849879 for entry in resp .json ():
850- name = entry ["name" ]
851- version = self ._match_function (identifier , name )
880+ tagname = entry ["name" ]
881+ version = self ._match_function (identifier , tagname )
852882 if version is None :
853- logger .debug (f"{ identifier } : match function ignores { name } " )
883+ logger .debug (f"{ identifier } : match function ignores { tagname } " )
854884 continue
855885 assert isinstance (version , Version )
856886
857- # GitLab provides a download URL for the archive, so return it
858- # in case prepare_source wants to download it instead of cloning
859- # the repository.
860- archive_path : str = f"{ self .project_path } /-/archive/{ name } /{ self .project_path .split ('/' )[- 1 ]} -{ name } .tar.gz"
861- archive_url : str = urljoin (self .server_url , archive_path )
862- yield archive_url , version
887+ archive_path : str = f"{ self .project_path } /-/archive/{ tagname } /{ self .project_path .split ('/' )[- 1 ]} -{ tagname } .tar.gz"
888+ url = urljoin (self .server_url , archive_path )
889+
890+ # get tag creation time, fall back to commit creation time
891+ created_at_str : str | None = entry .get ("created_at" )
892+ if created_at_str is None :
893+ created_at_str = entry ["commit" ].get ("created_at" )
894+
895+ if created_at_str is not None :
896+ created_at = datetime .datetime .fromisoformat (
897+ created_at_str
898+ ).astimezone (datetime .UTC )
899+ else :
900+ created_at = None
901+
902+ yield Candidate (
903+ name = identifier ,
904+ version = version ,
905+ url = url ,
906+ remote_tag = tagname ,
907+ remote_commit = entry ["commit" ]["id" ],
908+ upload_time = created_at ,
909+ )
863910
864911 # GitLab API uses Link headers for pagination
865912 nexturl = resp .links .get ("next" , {}).get ("url" )
0 commit comments