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,37 @@ 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 )
3577 for file in project_detail .files :
78+ version_str = None
3679 try :
37- release = Version (
38- version = extract_package_version (
39- filename = file .filename ,
40- project_name = canonical_name ,
41- ),
80+ version_str = extract_package_version (
81+ filename = file .filename ,
82+ project_name = canonical_name ,
4283 )
43- except (ValueError , InvalidVersion ):
44- release = Version ("0.0rc0" )
84+ release = Version (version = version_str )
85+ except (ValueError , InvalidVersionError ):
86+ # Use the extracted version_str if available, otherwise the filename
87+ release = InvalidVersion (version_str or file .filename )
4588 files_grouped_by_version .setdefault (release , []).append (file )
4689
4790 # Ensure there is a release for each version, even if there is no files for it.
4891 for version_str in project_detail .versions or []:
49- files_grouped_by_version .setdefault (Version (version_str ), [])
92+ try :
93+ version = Version (version_str )
94+ except (ValueError , InvalidVersionError ):
95+ version = InvalidVersion (version_str )
96+ files_grouped_by_version .setdefault (version , [])
5097
51- result : dict [Version , ShortReleaseInfo ] = {}
98+ result : dict [Version | InvalidVersion , ShortReleaseInfo ] = {}
5299
53100 latest_version = cls .compute_latest_version (files_grouped_by_version )
54101
@@ -77,7 +124,9 @@ def release_infos(
77124 or []
78125 )
79126
80- quarantined_files_by_release : dict [Version , list [Quarantinefile ]] = {}
127+ quarantined_files_by_release : dict [
128+ Version | InvalidVersion , list [Quarantinefile ]
129+ ] = {}
81130
82131 date_format = "%Y-%m-%dT%H:%M:%SZ"
83132 for file_info in quarantined_files :
@@ -88,12 +137,16 @@ def release_infos(
88137 ),
89138 "upload_time" : datetime .strptime (file_info ["upload_time" ], date_format ),
90139 }
91- release = Version (
92- extract_package_version (
140+ version_str = None
141+ try :
142+ version_str = extract_package_version (
93143 filename = quarantined_file ["filename" ],
94144 project_name = canonical_name ,
95- ),
96- )
145+ )
146+ release = Version (version_str )
147+ except (ValueError , InvalidVersionError ):
148+ # Use the extracted version_str if available, otherwise the filename
149+ release = InvalidVersion (version_str or quarantined_file ["filename" ])
97150 quarantined_files_by_release .setdefault (release , []).append (
98151 quarantined_file
99152 )
@@ -160,8 +213,8 @@ def release_infos(
160213
161214 @classmethod
162215 def compute_latest_version (
163- cls , versions : dict [Version , list [typing .Any ]]
164- ) -> Version :
216+ cls , versions : dict [Version | InvalidVersion , list [typing .Any ]]
217+ ) -> Version | InvalidVersion :
165218 # Use the pip logic to determine the latest release. First, pick the greatest non-dev version,
166219 # and if nothing, fall back to the greatest dev version. If no release is available return None.
167220 sorted_versions = sorted (
0 commit comments