Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions simple_repository_browser/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import fastapi
from fastapi.responses import StreamingResponse
from markupsafe import Markup
from packaging.version import InvalidVersion, Version
from packaging.version import InvalidVersion as InvalidVersionError
from packaging.version import Version

from . import errors, model, view
from .short_release_info import InvalidVersion
from .static_files import HashedStaticFileHandler, StaticFilesManifest


Expand Down Expand Up @@ -137,14 +139,14 @@ async def project(
recache: bool = False,
) -> str | StreamingResponse:
_ = page_section # Handled in javascript.
_version = None
_version: Version | InvalidVersion | None = None
if version:
try:
_version = Version(version)
except InvalidVersion:
raise errors.RequestError(
status_code=404, detail=f"Invalid version {version}."
)
except InvalidVersionError:
# Version string doesn't conform to PEP 440.
# Try to find it as an InvalidVersion in the releases.
_version = InvalidVersion(version)

t = asyncio.create_task(
self.model.project_page(project_name, _version, recache)
Expand Down
6 changes: 3 additions & 3 deletions simple_repository_browser/crawler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from . import fetch_projects
from .fetch_description import PackageInfo, package_info
from .short_release_info import ReleaseInfoModel, ShortReleaseInfo
from .short_release_info import InvalidVersion, ReleaseInfoModel, ShortReleaseInfo


class Crawler:
Expand Down Expand Up @@ -146,8 +146,8 @@ async def run_reindex_periodically(self) -> None:
async def fetch_pkg_info(
self,
prj: model.ProjectDetail,
version: Version,
releases: dict[Version, ShortReleaseInfo],
version: Version | InvalidVersion,
releases: dict[Version | InvalidVersion, ShortReleaseInfo],
force_recache: bool,
) -> tuple[model.File, PackageInfo]:
key = ("pkg-info", prj.name, str(version))
Expand Down
4 changes: 2 additions & 2 deletions simple_repository_browser/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from . import _search, compatibility_matrix, crawler, errors, fetch_projects
from .fetch_description import PackageInfo
from .short_release_info import ReleaseInfoModel, ShortReleaseInfo
from .short_release_info import InvalidVersion, ReleaseInfoModel, ShortReleaseInfo


@dataclasses.dataclass(frozen=True)
Expand Down Expand Up @@ -189,7 +189,7 @@ async def project_query(
async def project_page(
self,
project_name: str,
version: Version | None,
version: Version | InvalidVersion | None,
recache: bool,
) -> ProjectPageModel:
canonical_name = canonicalize_name(project_name)
Expand Down
95 changes: 75 additions & 20 deletions simple_repository_browser/short_release_info.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,59 @@
import dataclasses
from datetime import datetime
import functools
import types
import typing

from packaging.utils import canonicalize_name
from packaging.version import InvalidVersion, Version
from packaging.version import InvalidVersion as InvalidVersionError
from packaging.version import Version
from simple_repository import model
from simple_repository.packaging import extract_package_version


@functools.total_ordering
class InvalidVersion:
"""Represents a version string that doesn't conform to PEP 440."""

def __init__(self, version_string: str = "unknown"):
self._version_string = version_string

def __str__(self):
return self._version_string

def __repr__(self):
return f"InvalidVersion({self._version_string!r})"

def __hash__(self):
return hash(("invalid-version", self._version_string))

def __eq__(self, other):
return (
isinstance(other, InvalidVersion)
and self._version_string == other._version_string
)

def __lt__(self, other):
# Sort invalid versions to the beginning (before all real versions)
# so they won't be selected as the latest version
if isinstance(other, InvalidVersion):
return self._version_string < other._version_string
return True

@property
def is_prerelease(self):
return False

@property
def is_devrelease(self):
return False


@dataclasses.dataclass(frozen=True)
class ShortReleaseInfo:
# A short representation of a release. Intended to be lightweight to compute,
# such that many ShortReleaseInfo instances can be provided to a view.
version: Version
version: Version | InvalidVersion
files: tuple[model.File, ...]
release_date: datetime | None
labels: typing.Mapping[
Expand All @@ -25,30 +65,39 @@ class ReleaseInfoModel:
@classmethod
def release_infos(
cls, project_detail: model.ProjectDetail
) -> tuple[dict[Version, ShortReleaseInfo], Version]:
files_grouped_by_version: dict[Version, list[model.File]] = {}
) -> tuple[
dict[Version | InvalidVersion, ShortReleaseInfo], Version | InvalidVersion
]:
files_grouped_by_version: dict[Version | InvalidVersion, list[model.File]] = {}

if not project_detail.files:
raise ValueError("No files for the release")

canonical_name = canonicalize_name(project_detail.name)
release: Version | InvalidVersion
for file in project_detail.files:
version_str = None
try:
release = Version(
version=extract_package_version(
filename=file.filename,
project_name=canonical_name,
),
version_str = extract_package_version(
filename=file.filename,
project_name=canonical_name,
)
except (ValueError, InvalidVersion):
release = Version("0.0rc0")
release = Version(version=version_str)
except (ValueError, InvalidVersionError):
# Use the extracted version_str if available, otherwise the filename
release = InvalidVersion(version_str or file.filename)
files_grouped_by_version.setdefault(release, []).append(file)

# Ensure there is a release for each version, even if there is no files for it.
version: Version | InvalidVersion
for version_str in project_detail.versions or []:
files_grouped_by_version.setdefault(Version(version_str), [])
try:
version = Version(version_str)
except (ValueError, InvalidVersionError):
version = InvalidVersion(version_str)
files_grouped_by_version.setdefault(version, [])

result: dict[Version, ShortReleaseInfo] = {}
result: dict[Version | InvalidVersion, ShortReleaseInfo] = {}

latest_version = cls.compute_latest_version(files_grouped_by_version)

Expand Down Expand Up @@ -77,7 +126,9 @@ def release_infos(
or []
)

quarantined_files_by_release: dict[Version, list[Quarantinefile]] = {}
quarantined_files_by_release: dict[
Version | InvalidVersion, list[Quarantinefile]
] = {}

date_format = "%Y-%m-%dT%H:%M:%SZ"
for file_info in quarantined_files:
Expand All @@ -88,12 +139,16 @@ def release_infos(
),
"upload_time": datetime.strptime(file_info["upload_time"], date_format),
}
release = Version(
extract_package_version(
version_str = None
try:
version_str = extract_package_version(
filename=quarantined_file["filename"],
project_name=canonical_name,
),
)
)
release = Version(version_str)
except (ValueError, InvalidVersionError):
# Use the extracted version_str if available, otherwise the filename
release = InvalidVersion(version_str or quarantined_file["filename"])
quarantined_files_by_release.setdefault(release, []).append(
quarantined_file
)
Expand Down Expand Up @@ -160,8 +215,8 @@ def release_infos(

@classmethod
def compute_latest_version(
cls, versions: dict[Version, list[typing.Any]]
) -> Version:
cls, versions: dict[Version | InvalidVersion, list[typing.Any]]
) -> Version | InvalidVersion:
# Use the pip logic to determine the latest release. First, pick the greatest non-dev version,
# and if nothing, fall back to the greatest dev version. If no release is available return None.
sorted_versions = sorted(
Expand Down
7 changes: 7 additions & 0 deletions simple_repository_browser/templates/base/project.html
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,13 @@ <h1> {{ project.name }} {{ this_release.version }}</h1>
</button>
</span>
{% endif %}
{% if release_info.version.__class__.__name__ == 'InvalidVersion' %}
<span style="float: right; text-decoration: none; font-size: smaller; color: gray;">
<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">
Invalid version
</button>
</span>
{% endif %}
</div>
</div>
{% if 'quarantined' not in release_info.labels %}
Expand Down