11import dataclasses
22from datetime import datetime
3+ import functools
34import types
45import typing
56
67from packaging .utils import canonicalize_name
7- from packaging .version import InvalidVersion , Version
8+ from packaging .version import InvalidVersion as InvalidVersionError
9+ from packaging .version import Version
810from simple_repository import model
911from simple_repository .packaging import extract_package_version
1012
1113
14+ @functools .total_ordering
15+ class InvalidVersion :
16+ """Represents a version string that doesn't conform to PEP 440."""
17+
18+ def __init__ (self , version_string : str = "unknown" ):
19+ self ._version_string = version_string
20+
21+ def __str__ (self ):
22+ return self ._version_string
23+
24+ def __repr__ (self ):
25+ return f"InvalidVersion({ self ._version_string !r} )"
26+
27+ def __hash__ (self ):
28+ return hash (("invalid-version" , self ._version_string ))
29+
30+ def __eq__ (self , other ):
31+ return (
32+ isinstance (other , InvalidVersion )
33+ and self ._version_string == other ._version_string
34+ )
35+
36+ def __lt__ (self , other ):
37+ # Sort invalid versions to the beginning (before all real versions)
38+ # so they won't be selected as the latest version
39+ if isinstance (other , InvalidVersion ):
40+ return self ._version_string < other ._version_string
41+ return True
42+
43+ @property
44+ def is_prerelease (self ):
45+ return False
46+
47+ @property
48+ def is_devrelease (self ):
49+ return False
50+
51+
1252@dataclasses .dataclass (frozen = True )
1353class ShortReleaseInfo :
1454 # A short representation of a release. Intended to be lightweight to compute,
1555 # such that many ShortReleaseInfo instances can be provided to a view.
16- version : Version
56+ version : Version | InvalidVersion
1757 files : tuple [model .File , ...]
1858 release_date : datetime | None
1959 labels : typing .Mapping [
@@ -25,30 +65,39 @@ class ReleaseInfoModel:
2565 @classmethod
2666 def release_infos (
2767 cls , project_detail : model .ProjectDetail
28- ) -> tuple [dict [Version , ShortReleaseInfo ], Version ]:
29- files_grouped_by_version : dict [Version , list [model .File ]] = {}
68+ ) -> tuple [
69+ dict [Version | InvalidVersion , ShortReleaseInfo ], Version | InvalidVersion
70+ ]:
71+ files_grouped_by_version : dict [Version | InvalidVersion , list [model .File ]] = {}
3072
3173 if not project_detail .files :
3274 raise ValueError ("No files for the release" )
3375
3476 canonical_name = canonicalize_name (project_detail .name )
77+ release : Version | InvalidVersion
3578 for file in project_detail .files :
79+ version_str = None
3680 try :
37- release = Version (
38- version = extract_package_version (
39- filename = file .filename ,
40- project_name = canonical_name ,
41- ),
81+ version_str = extract_package_version (
82+ filename = file .filename ,
83+ project_name = canonical_name ,
4284 )
43- except (ValueError , InvalidVersion ):
44- release = Version ("0.0rc0" )
85+ release = Version (version = version_str )
86+ except (ValueError , InvalidVersionError ):
87+ # Use the extracted version_str if available, otherwise the filename
88+ release = InvalidVersion (version_str or file .filename )
4589 files_grouped_by_version .setdefault (release , []).append (file )
4690
4791 # Ensure there is a release for each version, even if there is no files for it.
92+ version : Version | InvalidVersion
4893 for version_str in project_detail .versions or []:
49- files_grouped_by_version .setdefault (Version (version_str ), [])
94+ try :
95+ version = Version (version_str )
96+ except (ValueError , InvalidVersionError ):
97+ version = InvalidVersion (version_str )
98+ files_grouped_by_version .setdefault (version , [])
5099
51- result : dict [Version , ShortReleaseInfo ] = {}
100+ result : dict [Version | InvalidVersion , ShortReleaseInfo ] = {}
52101
53102 latest_version = cls .compute_latest_version (files_grouped_by_version )
54103
@@ -77,7 +126,9 @@ def release_infos(
77126 or []
78127 )
79128
80- quarantined_files_by_release : dict [Version , list [Quarantinefile ]] = {}
129+ quarantined_files_by_release : dict [
130+ Version | InvalidVersion , list [Quarantinefile ]
131+ ] = {}
81132
82133 date_format = "%Y-%m-%dT%H:%M:%SZ"
83134 for file_info in quarantined_files :
@@ -88,12 +139,16 @@ def release_infos(
88139 ),
89140 "upload_time" : datetime .strptime (file_info ["upload_time" ], date_format ),
90141 }
91- release = Version (
92- extract_package_version (
142+ version_str = None
143+ try :
144+ version_str = extract_package_version (
93145 filename = quarantined_file ["filename" ],
94146 project_name = canonical_name ,
95- ),
96- )
147+ )
148+ release = Version (version_str )
149+ except (ValueError , InvalidVersionError ):
150+ # Use the extracted version_str if available, otherwise the filename
151+ release = InvalidVersion (version_str or quarantined_file ["filename" ])
97152 quarantined_files_by_release .setdefault (release , []).append (
98153 quarantined_file
99154 )
@@ -160,8 +215,8 @@ def release_infos(
160215
161216 @classmethod
162217 def compute_latest_version (
163- cls , versions : dict [Version , list [typing .Any ]]
164- ) -> Version :
218+ cls , versions : dict [Version | InvalidVersion , list [typing .Any ]]
219+ ) -> Version | InvalidVersion :
165220 # Use the pip logic to determine the latest release. First, pick the greatest non-dev version,
166221 # and if nothing, fall back to the greatest dev version. If no release is available return None.
167222 sorted_versions = sorted (
0 commit comments