Skip to content

Commit d734257

Browse files
committed
Handle invalid versions in the release_info section, allowing projects with invalid versions in their history to be rendered properly (e.g. pytz)
1 parent da4706c commit d734257

File tree

2 files changed

+80
-20
lines changed

2 files changed

+80
-20
lines changed

simple_repository_browser/short_release_info.py

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,59 @@
11
import dataclasses
22
from datetime import datetime
3+
import functools
34
import types
45
import typing
56

67
from 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
810
from simple_repository import model
911
from 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)
1353
class 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(

simple_repository_browser/templates/base/project.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,13 @@ <h1> {{ project.name }} {{ this_release.version }}</h1>
225225
</button>
226226
</span>
227227
{% endif %}
228+
{% if release_info.version.__class__.__name__ == 'InvalidVersion' %}
229+
<span style="float: right; text-decoration: none; font-size: smaller; color: gray;">
230+
<button class="btn btn-danger position-relative me-2 mb-1 btn-sm active" data-bs-toggle="tooltip" data-bs-placement="right" title="Version string does not conform to PEP 440">
231+
Invalid version
232+
</button>
233+
</span>
234+
{% endif %}
228235
</div>
229236
</div>
230237
{% if 'quarantined' not in release_info.labels %}

0 commit comments

Comments
 (0)