1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
"""Map Git tags to PEP 440 versions (ADR-0016)."""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from packaging.version import InvalidVersion, Version
from lpip.errors import VersionTagNotFoundError
from lpip.gitutils import git_ls_remote_tags
from lpip.models import PackageSource
@dataclass(frozen=True)
class VersionRef:
version: Version
tag: str
@dataclass(frozen=True)
class VersionsReport:
package: str
refs: tuple[VersionRef, ...]
other_tags: tuple[str, ...]
def version_from_tag(tag: str) -> Version | None:
"""PEP 440 as-is, or after one leading ``v`` / ``V``."""
candidates = [tag]
if len(tag) > 1 and tag[0] in "vV":
candidates.append(tag[1:])
for candidate in candidates:
try:
return Version(candidate)
except InvalidVersion:
continue
return None
def select_tag_for_version(version: Version, tags: Sequence[str]) -> str:
pep = str(version)
for candidate in (pep, f"v{pep}", f"V{pep}"):
if candidate in tags:
return candidate
return sorted(tags)[0]
def collect_version_refs(
tags: Sequence[str],
*,
include_prereleases: bool,
) -> tuple[VersionRef, ...]:
grouped: dict[Version, list[str]] = {}
for tag in tags:
parsed = version_from_tag(tag)
if parsed is None:
continue
if parsed.is_prerelease and not include_prereleases:
continue
grouped.setdefault(parsed, []).append(tag)
return tuple(
VersionRef(version=ver, tag=select_tag_for_version(ver, grouped[ver]))
for ver in sorted(grouped)
)
def unversioned_tags(tags: Sequence[str]) -> tuple[str, ...]:
return tuple(sorted(tag for tag in tags if version_from_tag(tag) is None))
def format_versions_report(report: VersionsReport) -> str:
"""Pip-like text: name (latest) and newest-first versions."""
if report.refs:
latest = str(report.refs[-1].version)
listed = ", ".join(str(ref.version) for ref in reversed(report.refs))
header = f"{report.package} ({latest})"
body = f"Available versions: {listed}"
else:
header = report.package
body = "Available versions: (none)"
lines = [header, body]
if report.other_tags:
lines.append(f"Other tags: {', '.join(report.other_tags)}")
return "\n".join(lines)
def tag_for_version(
package: PackageSource,
version: Version,
*,
environ: Mapping[str, str] | None = None,
) -> str:
"""Return the Git tag for a PEP 440 version (ADR-0018)."""
tags = git_ls_remote_tags(package.url, environ=environ)
refs = collect_version_refs(tags, include_prereleases=True)
for ref in refs:
if ref.version == version:
return ref.tag
raise VersionTagNotFoundError(package.name, str(version))
def list_package_versions(
package: PackageSource,
*,
include_prereleases: bool = False,
include_other: bool = False,
environ: Mapping[str, str] | None = None,
) -> VersionsReport:
tags = git_ls_remote_tags(package.url, environ=environ)
refs = collect_version_refs(tags, include_prereleases=include_prereleases)
others = unversioned_tags(tags) if include_other else ()
return VersionsReport(package=package.name, refs=refs, other_tags=others)
|