Skip to content

Commit 1d0d946

Browse files
authored
Merge pull request #31 from simple-repository/feature/file-enrichment-base-class
Improve invalid version handling
2 parents 8f1ba18 + 71a05fd commit 1d0d946

File tree

5 files changed

+95
-31
lines changed

5 files changed

+95
-31
lines changed

simple_repository_browser/controller.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
import fastapi
1010
from fastapi.responses import StreamingResponse
1111
from markupsafe import Markup
12-
from packaging.version import InvalidVersion, Version
12+
from packaging.version import InvalidVersion as InvalidVersionError
13+
from packaging.version import Version
1314

1415
from . import errors, model, view
16+
from .short_release_info import InvalidVersion
1517
from .static_files import HashedStaticFileHandler, StaticFilesManifest
1618

1719

@@ -137,14 +139,14 @@ async def project(
137139
recache: bool = False,
138140
) -> str | StreamingResponse:
139141
_ = page_section # Handled in javascript.
140-
_version = None
142+
_version: Version | InvalidVersion | None = None
141143
if version:
142144
try:
143145
_version = Version(version)
144-
except InvalidVersion:
145-
raise errors.RequestError(
146-
status_code=404, detail=f"Invalid version {version}."
147-
)
146+
except InvalidVersionError:
147+
# Version string doesn't conform to PEP 440.
148+
# Try to find it as an InvalidVersion in the releases.
149+
_version = InvalidVersion(version)
148150

149151
t = asyncio.create_task(
150152
self.model.project_page(project_name, _version, recache)

simple_repository_browser/crawler.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from . import fetch_projects
1717
from .fetch_description import PackageInfo, package_info
18-
from .short_release_info import ReleaseInfoModel, ShortReleaseInfo
18+
from .short_release_info import InvalidVersion, ReleaseInfoModel, ShortReleaseInfo
1919

2020

2121
class Crawler:
@@ -146,8 +146,8 @@ async def run_reindex_periodically(self) -> None:
146146
async def fetch_pkg_info(
147147
self,
148148
prj: model.ProjectDetail,
149-
version: Version,
150-
releases: dict[Version, ShortReleaseInfo],
149+
version: Version | InvalidVersion,
150+
releases: dict[Version | InvalidVersion, ShortReleaseInfo],
151151
force_recache: bool,
152152
) -> tuple[model.File, PackageInfo]:
153153
key = ("pkg-info", prj.name, str(version))

simple_repository_browser/model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from . import _search, compatibility_matrix, crawler, errors, fetch_projects
1616
from .fetch_description import PackageInfo
17-
from .short_release_info import ReleaseInfoModel, ShortReleaseInfo
17+
from .short_release_info import InvalidVersion, ReleaseInfoModel, ShortReleaseInfo
1818

1919

2020
@dataclasses.dataclass(frozen=True)
@@ -189,7 +189,7 @@ async def project_query(
189189
async def project_page(
190190
self,
191191
project_name: str,
192-
version: Version | None,
192+
version: Version | InvalidVersion | None,
193193
recache: bool,
194194
) -> ProjectPageModel:
195195
canonical_name = canonicalize_name(project_name)

simple_repository_browser/short_release_info.py

Lines changed: 75 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,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(

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)