uv-lock-report 0.11.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of uv-lock-report might be problematic. Click here for more details.
- uv_lock_report/__init__.py +29 -0
- uv_lock_report/__main__.py +11 -0
- uv_lock_report/cli.py +41 -0
- uv_lock_report/models.py +408 -0
- uv_lock_report/report.py +68 -0
- uv_lock_report/tests/__init__.py +0 -0
- uv_lock_report/tests/conftest.py +134 -0
- uv_lock_report/tests/test_get_lockfiles.py +324 -0
- uv_lock_report/tests/test_lock_file_reporter.py +615 -0
- uv_lock_report/tests/test_lockfile.py +47 -0
- uv_lock_report/tests/test_lockfile_changes.py +71 -0
- uv_lock_report/tests/test_updated_package.py +11 -0
- uv_lock_report/tests/test_version_change_level.py +52 -0
- uv_lock_report-0.11.4.dist-info/METADATA +132 -0
- uv_lock_report-0.11.4.dist-info/RECORD +17 -0
- uv_lock_report-0.11.4.dist-info/WHEEL +4 -0
- uv_lock_report-0.11.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
uv-lock-report: Parses uv.lock changes and generates Markdown reports.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from uv_lock_report.cli import main as cli_main
|
|
6
|
+
from uv_lock_report.report import report
|
|
7
|
+
from uv_lock_report.models import (
|
|
8
|
+
LockFile,
|
|
9
|
+
UvLockFile,
|
|
10
|
+
LockFileReporter,
|
|
11
|
+
LockfileChanges,
|
|
12
|
+
LockfilePackage,
|
|
13
|
+
UpdatedPackage,
|
|
14
|
+
OutputFormat,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"cli_main",
|
|
21
|
+
"report",
|
|
22
|
+
"LockFile",
|
|
23
|
+
"UvLockFile",
|
|
24
|
+
"LockFileReporter",
|
|
25
|
+
"LockfileChanges",
|
|
26
|
+
"LockfilePackage",
|
|
27
|
+
"UpdatedPackage",
|
|
28
|
+
"OutputFormat",
|
|
29
|
+
]
|
uv_lock_report/cli.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from argparse import ArgumentParser, Namespace
|
|
2
|
+
|
|
3
|
+
from uv_lock_report.models import OutputFormat
|
|
4
|
+
from uv_lock_report.report import report
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_args() -> Namespace:
|
|
8
|
+
parser = ArgumentParser()
|
|
9
|
+
parser.add_argument("--base-sha", required=True)
|
|
10
|
+
parser.add_argument("--base-path", required=True)
|
|
11
|
+
parser.add_argument("--output-path", required=True)
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"--output-format",
|
|
14
|
+
choices=list(OutputFormat),
|
|
15
|
+
default=OutputFormat.TABLE.value,
|
|
16
|
+
required=False,
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--show-learn-more-link",
|
|
20
|
+
choices=["true", "false"],
|
|
21
|
+
default="true",
|
|
22
|
+
required=False,
|
|
23
|
+
help='Whether to show a "Learn More" link in the report comment.',
|
|
24
|
+
)
|
|
25
|
+
return parser.parse_args()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
args = parse_args()
|
|
30
|
+
print(args)
|
|
31
|
+
base_sha = args.base_sha
|
|
32
|
+
base_path = args.base_path
|
|
33
|
+
output_path = args.output_path
|
|
34
|
+
output_format = OutputFormat(args.output_format)
|
|
35
|
+
report(
|
|
36
|
+
base_sha=base_sha,
|
|
37
|
+
base_path=base_path,
|
|
38
|
+
output_path=output_path,
|
|
39
|
+
output_format=output_format,
|
|
40
|
+
show_learn_more_link=args.show_learn_more_link == "true",
|
|
41
|
+
)
|
uv_lock_report/models.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import tomllib
|
|
3
|
+
from enum import IntEnum, StrEnum, auto
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
|
|
6
|
+
from packaging.version import parse
|
|
7
|
+
from pydantic import (
|
|
8
|
+
BaseModel,
|
|
9
|
+
ConfigDict,
|
|
10
|
+
Field,
|
|
11
|
+
computed_field,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OutputFormat(StrEnum):
|
|
16
|
+
TABLE = auto()
|
|
17
|
+
SIMPLE = auto()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VersionChangeType(StrEnum):
|
|
21
|
+
UPGRADE = auto()
|
|
22
|
+
DOWNGRADE = auto()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VersionChangeLevel(IntEnum):
|
|
26
|
+
MAJOR = 0
|
|
27
|
+
MINOR = 1
|
|
28
|
+
PATCH = 2
|
|
29
|
+
UNKNOWN = 10
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def gitmoji(self) -> str:
|
|
33
|
+
match self:
|
|
34
|
+
case VersionChangeLevel.MAJOR:
|
|
35
|
+
return ":collision:"
|
|
36
|
+
case VersionChangeLevel.MINOR:
|
|
37
|
+
return ":sparkles:"
|
|
38
|
+
case VersionChangeLevel.PATCH:
|
|
39
|
+
return ":hammer_and_wrench:"
|
|
40
|
+
case VersionChangeLevel.UNKNOWN:
|
|
41
|
+
return ":question:"
|
|
42
|
+
case _:
|
|
43
|
+
raise NotImplementedError()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
BASEVERSION = re.compile(
|
|
47
|
+
r"""[vV]?
|
|
48
|
+
(?P<major>0|[1-9]\d*)
|
|
49
|
+
(\.
|
|
50
|
+
(?P<minor>0|[1-9]\d*)
|
|
51
|
+
(\.
|
|
52
|
+
(?P<patch>0|[1-9]\d*)
|
|
53
|
+
)?
|
|
54
|
+
)?
|
|
55
|
+
""",
|
|
56
|
+
re.VERBOSE,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LockfilePackage(BaseModel):
|
|
61
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
version: str | None = None
|
|
65
|
+
|
|
66
|
+
def __str__(self) -> str:
|
|
67
|
+
return f"{self.name}: {self.version}"
|
|
68
|
+
|
|
69
|
+
def __eq__(self, other):
|
|
70
|
+
if not isinstance(other, LockfilePackage):
|
|
71
|
+
return NotImplemented
|
|
72
|
+
if self.version is None or other.version is None:
|
|
73
|
+
return self.name == other.name and self.version == other.version
|
|
74
|
+
return self.name == other.name and parse(self.version) == parse(other.version)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class UpdatedPackage(BaseModel):
|
|
78
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
79
|
+
|
|
80
|
+
name: str
|
|
81
|
+
old_version: str
|
|
82
|
+
new_version: str
|
|
83
|
+
|
|
84
|
+
def __str__(self) -> str:
|
|
85
|
+
return f"{self.name}: {self.old_version} -> {self.new_version}"
|
|
86
|
+
|
|
87
|
+
def change_type(self) -> VersionChangeType:
|
|
88
|
+
old_ver = parse(self.old_version)
|
|
89
|
+
new_ver = parse(self.new_version)
|
|
90
|
+
if new_ver > old_ver:
|
|
91
|
+
return VersionChangeType.UPGRADE
|
|
92
|
+
else:
|
|
93
|
+
return VersionChangeType.DOWNGRADE
|
|
94
|
+
|
|
95
|
+
def change_level(self) -> VersionChangeLevel:
|
|
96
|
+
old_version = parse(self.old_version)
|
|
97
|
+
new_version = parse(self.new_version)
|
|
98
|
+
|
|
99
|
+
if new_version.major != old_version.major:
|
|
100
|
+
return VersionChangeLevel.MAJOR
|
|
101
|
+
elif new_version.minor != old_version.minor:
|
|
102
|
+
return VersionChangeLevel.MINOR
|
|
103
|
+
elif new_version.micro != old_version.micro:
|
|
104
|
+
return VersionChangeLevel.PATCH
|
|
105
|
+
|
|
106
|
+
return VersionChangeLevel.UNKNOWN
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class RequiresPythonChanges(BaseModel):
|
|
110
|
+
old: str | None
|
|
111
|
+
new: str | None
|
|
112
|
+
|
|
113
|
+
def has_changes(self) -> bool:
|
|
114
|
+
return self.old != self.new
|
|
115
|
+
|
|
116
|
+
def __str__(self) -> str:
|
|
117
|
+
return f"Requires-Python: {self.old} -> {self.new}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class LockfileChanges(BaseModel):
|
|
121
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
122
|
+
|
|
123
|
+
requires_python: RequiresPythonChanges
|
|
124
|
+
|
|
125
|
+
added: list[LockfilePackage] = []
|
|
126
|
+
removed: list[LockfilePackage] = []
|
|
127
|
+
updated: list[UpdatedPackage] = []
|
|
128
|
+
output_format: OutputFormat
|
|
129
|
+
show_learn_more_link: bool
|
|
130
|
+
|
|
131
|
+
def __str__(self) -> str:
|
|
132
|
+
all = []
|
|
133
|
+
if self.requires_python.has_changes():
|
|
134
|
+
all.append("Python Constraint Changed:")
|
|
135
|
+
all.append(
|
|
136
|
+
f"\\`{self.requires_python.old}\\` -> \\`{self.requires_python.new}\\`"
|
|
137
|
+
)
|
|
138
|
+
if self.added:
|
|
139
|
+
all.append("Added:")
|
|
140
|
+
all.extend([str(e) for e in self.added])
|
|
141
|
+
if self.updated:
|
|
142
|
+
all.append("Updated:")
|
|
143
|
+
all.extend([str(e) for e in self.updated])
|
|
144
|
+
if self.removed:
|
|
145
|
+
all.append("Removed:")
|
|
146
|
+
all.extend([str(e) for e in self.removed])
|
|
147
|
+
return "\n".join(all)
|
|
148
|
+
|
|
149
|
+
@computed_field
|
|
150
|
+
@property
|
|
151
|
+
def items(self) -> int:
|
|
152
|
+
return len(self.added) + len(self.removed) + len(self.updated)
|
|
153
|
+
|
|
154
|
+
@computed_field
|
|
155
|
+
@property
|
|
156
|
+
def markdown(self) -> str:
|
|
157
|
+
match self.output_format:
|
|
158
|
+
case OutputFormat.TABLE:
|
|
159
|
+
return self.markdown_table
|
|
160
|
+
case OutputFormat.SIMPLE:
|
|
161
|
+
return self.markdown_simple
|
|
162
|
+
case _:
|
|
163
|
+
raise ValueError(f"Unknown format: {format}")
|
|
164
|
+
|
|
165
|
+
@computed_field
|
|
166
|
+
@property
|
|
167
|
+
def markdown_table(self) -> str:
|
|
168
|
+
title = "##"
|
|
169
|
+
sections = "###"
|
|
170
|
+
|
|
171
|
+
all = [f"{title} uv Lockfile Report"]
|
|
172
|
+
if self.requires_python.has_changes():
|
|
173
|
+
all.append(f"{sections} Python Constraint Changed")
|
|
174
|
+
all.append(
|
|
175
|
+
f"\\`{self.requires_python.old}\\` -> \\`{self.requires_python.new}\\`"
|
|
176
|
+
)
|
|
177
|
+
if self.added:
|
|
178
|
+
all.append(f"{sections} Added Packages")
|
|
179
|
+
all.extend(
|
|
180
|
+
[
|
|
181
|
+
"| Package | Version |",
|
|
182
|
+
"|--|--|",
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
all.extend([f"| {added.name} | {added.version} |" for added in self.added])
|
|
186
|
+
|
|
187
|
+
if self.updated:
|
|
188
|
+
all.append(f"{sections} Changed Packages")
|
|
189
|
+
all.extend(
|
|
190
|
+
[
|
|
191
|
+
"| Package | Old Version | New Version |",
|
|
192
|
+
"|--|--|--|",
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
all.extend(
|
|
196
|
+
[
|
|
197
|
+
f"| {updated.name} | {updated.old_version} | {updated.new_version} |"
|
|
198
|
+
for updated in self.updated
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if self.removed:
|
|
203
|
+
all.append(f"{sections} Removed Packages")
|
|
204
|
+
all.extend(
|
|
205
|
+
[
|
|
206
|
+
"| Package | Version |",
|
|
207
|
+
"|--|--|",
|
|
208
|
+
]
|
|
209
|
+
)
|
|
210
|
+
all.extend(
|
|
211
|
+
[f"| {removed.name} | {removed.version} |" for removed in self.removed]
|
|
212
|
+
)
|
|
213
|
+
return "\n".join(all)
|
|
214
|
+
|
|
215
|
+
@computed_field
|
|
216
|
+
@property
|
|
217
|
+
def markdown_simple(self) -> str:
|
|
218
|
+
title = "##"
|
|
219
|
+
sections = "###"
|
|
220
|
+
all = [f"{title} uv Lockfile Report"]
|
|
221
|
+
if self.requires_python.has_changes():
|
|
222
|
+
all.append(f"{sections} Python Constraint Changed")
|
|
223
|
+
all.append(
|
|
224
|
+
f"\\`{self.requires_python.old}\\` -> \\`{self.requires_python.new}\\`"
|
|
225
|
+
)
|
|
226
|
+
if self.added:
|
|
227
|
+
all.append(f"{sections} Added Packages")
|
|
228
|
+
all.extend(
|
|
229
|
+
[f"\\`{added.name}\\`: \\`{added.version}\\`" for added in self.added]
|
|
230
|
+
)
|
|
231
|
+
if self.updated:
|
|
232
|
+
all.append(f"{sections} Changed Packages")
|
|
233
|
+
all.extend(
|
|
234
|
+
[
|
|
235
|
+
f"{updated.change_level().gitmoji} \\`{updated.name}\\`: \\`{updated.old_version}\\` -> \\`{updated.new_version}\\`"
|
|
236
|
+
for updated in self.updated
|
|
237
|
+
]
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if self.removed:
|
|
241
|
+
all.append(f"{sections} Removed Packages")
|
|
242
|
+
all.extend(
|
|
243
|
+
[
|
|
244
|
+
f"\\`{removed.name}\\`: \\`{removed.version}\\`"
|
|
245
|
+
for removed in self.removed
|
|
246
|
+
]
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if self.show_learn_more_link:
|
|
250
|
+
all.append(self.learn_more_link_text)
|
|
251
|
+
return "\n".join(all)
|
|
252
|
+
|
|
253
|
+
@computed_field
|
|
254
|
+
@property
|
|
255
|
+
def learn_more_link_text(self) -> str:
|
|
256
|
+
return "\n".join(
|
|
257
|
+
[
|
|
258
|
+
"",
|
|
259
|
+
"---",
|
|
260
|
+
"Learn more about this report at https://github.com/mw-root/uv-lock-report",
|
|
261
|
+
]
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class LockFileType(StrEnum):
|
|
266
|
+
UV = auto()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class LockFile(BaseModel):
|
|
270
|
+
type: LockFileType
|
|
271
|
+
packages: list[LockfilePackage]
|
|
272
|
+
|
|
273
|
+
@cached_property
|
|
274
|
+
def packages_by_name(self) -> dict[str, LockfilePackage]:
|
|
275
|
+
return {p.name: p for p in self.packages}
|
|
276
|
+
|
|
277
|
+
@cached_property
|
|
278
|
+
def package_names(self) -> set[str]:
|
|
279
|
+
return set(self.packages_by_name.keys())
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class UvLockFile(LockFile):
|
|
283
|
+
type: LockFileType = LockFileType.UV
|
|
284
|
+
version: int
|
|
285
|
+
revision: int
|
|
286
|
+
packages: list[LockfilePackage] = Field(alias="package")
|
|
287
|
+
requires_python: str = Field(alias="requires-python")
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def from_toml_str(cls, toml_str: str) -> "UvLockFile":
|
|
291
|
+
return cls.model_validate(tomllib.loads(toml_str))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class LockFileReporter:
|
|
295
|
+
def __init__(
|
|
296
|
+
self,
|
|
297
|
+
old_lockfile: UvLockFile | None,
|
|
298
|
+
new_lockfile: UvLockFile | None,
|
|
299
|
+
output_format: OutputFormat,
|
|
300
|
+
show_learn_more_link: bool,
|
|
301
|
+
) -> None:
|
|
302
|
+
self.old_lockfile = old_lockfile
|
|
303
|
+
self.new_lockfile = new_lockfile
|
|
304
|
+
self.output_format = output_format
|
|
305
|
+
self.show_learn_more_link = show_learn_more_link
|
|
306
|
+
|
|
307
|
+
@cached_property
|
|
308
|
+
def both_lockfile_package_names(self) -> set[str]:
|
|
309
|
+
old_package_names = (
|
|
310
|
+
self.old_lockfile.package_names if self.old_lockfile else set()
|
|
311
|
+
)
|
|
312
|
+
new_package_names = (
|
|
313
|
+
self.new_lockfile.package_names if self.new_lockfile else set()
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return old_package_names & new_package_names
|
|
317
|
+
|
|
318
|
+
def get_changes(self) -> LockfileChanges:
|
|
319
|
+
return LockfileChanges(
|
|
320
|
+
requires_python=self.get_requires_python_changes(),
|
|
321
|
+
added=self.get_added_packages(),
|
|
322
|
+
removed=self.get_removed_packages(),
|
|
323
|
+
updated=self.get_updated_packages(),
|
|
324
|
+
show_learn_more_link=self.show_learn_more_link,
|
|
325
|
+
output_format=self.output_format,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def get_requires_python_changes(self) -> RequiresPythonChanges:
|
|
329
|
+
old_requires_python = (
|
|
330
|
+
self.old_lockfile.requires_python if self.old_lockfile else None
|
|
331
|
+
)
|
|
332
|
+
new_requires_python = (
|
|
333
|
+
self.new_lockfile.requires_python if self.new_lockfile else None
|
|
334
|
+
)
|
|
335
|
+
return RequiresPythonChanges(
|
|
336
|
+
old=old_requires_python,
|
|
337
|
+
new=new_requires_python,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
@cached_property
|
|
341
|
+
def added_package_names(self) -> set[str]:
|
|
342
|
+
if self.old_lockfile is None:
|
|
343
|
+
if self.new_lockfile is None:
|
|
344
|
+
return set()
|
|
345
|
+
|
|
346
|
+
return self.new_lockfile.package_names
|
|
347
|
+
|
|
348
|
+
if self.new_lockfile is None:
|
|
349
|
+
return set()
|
|
350
|
+
|
|
351
|
+
return self.new_lockfile.package_names.difference(
|
|
352
|
+
self.old_lockfile.package_names
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
@cached_property
|
|
356
|
+
def removed_package_names(self) -> set[str]:
|
|
357
|
+
if self.new_lockfile is None:
|
|
358
|
+
if self.old_lockfile is None:
|
|
359
|
+
return set()
|
|
360
|
+
|
|
361
|
+
return self.old_lockfile.package_names
|
|
362
|
+
if self.old_lockfile is None:
|
|
363
|
+
return set()
|
|
364
|
+
|
|
365
|
+
return self.old_lockfile.package_names.difference(
|
|
366
|
+
self.new_lockfile.package_names
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def get_removed_packages(self) -> list[LockfilePackage]:
|
|
370
|
+
if self.old_lockfile is None:
|
|
371
|
+
return []
|
|
372
|
+
return [
|
|
373
|
+
pkg
|
|
374
|
+
for pkg in self.old_lockfile.packages
|
|
375
|
+
if pkg.name in self.removed_package_names
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
def get_added_packages(self) -> list[LockfilePackage]:
|
|
379
|
+
if self.new_lockfile is None:
|
|
380
|
+
return []
|
|
381
|
+
return [
|
|
382
|
+
pkg
|
|
383
|
+
for pkg in self.new_lockfile.packages
|
|
384
|
+
if pkg.name in self.added_package_names
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
def sort_packages_by_change_level(
|
|
388
|
+
self, packages: list[UpdatedPackage]
|
|
389
|
+
) -> list[UpdatedPackage]:
|
|
390
|
+
return sorted(packages, key=lambda x: (x.change_level(), x.name))
|
|
391
|
+
|
|
392
|
+
def get_updated_packages(self) -> list[UpdatedPackage]:
|
|
393
|
+
if self.old_lockfile is None or self.new_lockfile is None:
|
|
394
|
+
return []
|
|
395
|
+
updated_packages: list[UpdatedPackage] = []
|
|
396
|
+
|
|
397
|
+
for pkg_name in self.both_lockfile_package_names:
|
|
398
|
+
old_pkg = self.old_lockfile.packages_by_name[pkg_name]
|
|
399
|
+
new_pkg = self.new_lockfile.packages_by_name[pkg_name]
|
|
400
|
+
if old_pkg != new_pkg:
|
|
401
|
+
updated_packages.append(
|
|
402
|
+
UpdatedPackage(
|
|
403
|
+
name=pkg_name,
|
|
404
|
+
old_version=old_pkg.version,
|
|
405
|
+
new_version=new_pkg.version,
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
return self.sort_packages_by_change_level(updated_packages)
|
uv_lock_report/report.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from uv_lock_report.models import (
|
|
5
|
+
LockfileChanges,
|
|
6
|
+
LockFileReporter,
|
|
7
|
+
OutputFormat,
|
|
8
|
+
UvLockFile,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
CURRENT_UV_LOCK = Path("uv.lock")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_new_uv_lock_file(base_path: str) -> UvLockFile | None:
|
|
15
|
+
path = Path(base_path)
|
|
16
|
+
uv_lock_path = path / CURRENT_UV_LOCK
|
|
17
|
+
if not uv_lock_path.exists():
|
|
18
|
+
print("uv.lock not found in current working directory")
|
|
19
|
+
return None
|
|
20
|
+
return UvLockFile.from_toml_str(uv_lock_path.read_text())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_old_uv_lock_file(base_sha: str, base_path: str) -> UvLockFile | None:
|
|
24
|
+
cmd = ["git", "show", f"{base_sha}:uv.lock"]
|
|
25
|
+
|
|
26
|
+
run = subprocess.run(
|
|
27
|
+
cmd,
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True,
|
|
30
|
+
cwd=base_path,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if run.returncode != 0:
|
|
34
|
+
print("uv.lock not found in base commit")
|
|
35
|
+
print(run.stderr)
|
|
36
|
+
print(run.stdout)
|
|
37
|
+
print(run.args)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
print("Found uv.lock in base commit.")
|
|
41
|
+
return UvLockFile.from_toml_str(run.stdout)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write_changes_file(lockfile_changes: LockfileChanges, output_path: str) -> None:
|
|
45
|
+
Path(output_path).write_text(lockfile_changes.model_dump_json())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def report(
|
|
49
|
+
base_sha: str,
|
|
50
|
+
base_path: str,
|
|
51
|
+
output_path: str,
|
|
52
|
+
output_format: OutputFormat = OutputFormat.TABLE,
|
|
53
|
+
show_learn_more_link: bool = True,
|
|
54
|
+
) -> None:
|
|
55
|
+
old_lockfile = get_old_uv_lock_file(base_sha, base_path)
|
|
56
|
+
new_lockfile = get_new_uv_lock_file(base_path)
|
|
57
|
+
|
|
58
|
+
reporter = LockFileReporter(
|
|
59
|
+
old_lockfile=old_lockfile,
|
|
60
|
+
new_lockfile=new_lockfile,
|
|
61
|
+
output_format=output_format,
|
|
62
|
+
show_learn_more_link=show_learn_more_link,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
write_changes_file(
|
|
66
|
+
lockfile_changes=reporter.get_changes(),
|
|
67
|
+
output_path=output_path,
|
|
68
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from uv_lock_report.models import LockfilePackage, OutputFormat, UpdatedPackage
|
|
2
|
+
|
|
3
|
+
ADDED_PACKAGES: list[LockfilePackage] = [
|
|
4
|
+
LockfilePackage(name="added_1", version="1.0.0"),
|
|
5
|
+
LockfilePackage(name="added_2", version="4.2.0"),
|
|
6
|
+
]
|
|
7
|
+
REMOVED_PACKAGES: list[LockfilePackage] = [
|
|
8
|
+
LockfilePackage(name="removed_1", version="1.0.0"),
|
|
9
|
+
LockfilePackage(name="removed_2", version="4.2.0"),
|
|
10
|
+
]
|
|
11
|
+
UPDATED_PACKAGES: list[UpdatedPackage] = [
|
|
12
|
+
UpdatedPackage(name="updated_1", old_version="1.0.0", new_version="2.0.0"),
|
|
13
|
+
UpdatedPackage(name="updated_2", old_version="1.0.0", new_version="2.0.0"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
EXPECTED_LOCKFILE_CHANGES_FULL_TABLE = """
|
|
17
|
+
## uv Lockfile Report
|
|
18
|
+
### Added Packages
|
|
19
|
+
| Package | Version |
|
|
20
|
+
|--|--|
|
|
21
|
+
| added_1 | 1.0.0 |
|
|
22
|
+
| added_2 | 4.2.0 |
|
|
23
|
+
### Changed Packages
|
|
24
|
+
| Package | Old Version | New Version |
|
|
25
|
+
|--|--|--|
|
|
26
|
+
| updated_1 | 1.0.0 | 2.0.0 |
|
|
27
|
+
| updated_2 | 1.0.0 | 2.0.0 |
|
|
28
|
+
### Removed Packages
|
|
29
|
+
| Package | Version |
|
|
30
|
+
|--|--|
|
|
31
|
+
| removed_1 | 1.0.0 |
|
|
32
|
+
| removed_2 | 4.2.0 |
|
|
33
|
+
""".strip()
|
|
34
|
+
|
|
35
|
+
EXPECTED_LOCKFILE_CHANGES_FULL_SIMPLE = """
|
|
36
|
+
## uv Lockfile Report
|
|
37
|
+
### Added Packages
|
|
38
|
+
\\`added_1\\`: \\`1.0.0\\`
|
|
39
|
+
\\`added_2\\`: \\`4.2.0\\`
|
|
40
|
+
### Changed Packages
|
|
41
|
+
:collision: \\`updated_1\\`: \\`1.0.0\\` -> \\`2.0.0\\`
|
|
42
|
+
:collision: \\`updated_2\\`: \\`1.0.0\\` -> \\`2.0.0\\`
|
|
43
|
+
### Removed Packages
|
|
44
|
+
\\`removed_1\\`: \\`1.0.0\\`
|
|
45
|
+
\\`removed_2\\`: \\`4.2.0\\`
|
|
46
|
+
""".strip()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
EXPECTED_LOCKFILE_CHANGES_FULL_SIMPLE_WITH_LINK = """
|
|
50
|
+
## uv Lockfile Report
|
|
51
|
+
### Added Packages
|
|
52
|
+
\\`added_1\\`: \\`1.0.0\\`
|
|
53
|
+
\\`added_2\\`: \\`4.2.0\\`
|
|
54
|
+
### Changed Packages
|
|
55
|
+
:collision: \\`updated_1\\`: \\`1.0.0\\` -> \\`2.0.0\\`
|
|
56
|
+
:collision: \\`updated_2\\`: \\`1.0.0\\` -> \\`2.0.0\\`
|
|
57
|
+
### Removed Packages
|
|
58
|
+
\\`removed_1\\`: \\`1.0.0\\`
|
|
59
|
+
\\`removed_2\\`: \\`4.2.0\\`
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
Learn more about this report at https://github.com/mw-root/uv-lock-report
|
|
63
|
+
""".strip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
EXPECTED_LOCKFILE_CHANGES_FULL_MODEL_DUMP_TABLE = {
|
|
67
|
+
"added": [
|
|
68
|
+
{"name": "added_1", "version": "1.0.0"},
|
|
69
|
+
{"name": "added_2", "version": "4.2.0"},
|
|
70
|
+
],
|
|
71
|
+
"items": 6,
|
|
72
|
+
"learn_more_link_text": "\n---\nLearn more about this report at https://github.com/mw-root/uv-lock-report",
|
|
73
|
+
"markdown": EXPECTED_LOCKFILE_CHANGES_FULL_TABLE,
|
|
74
|
+
"markdown_simple": EXPECTED_LOCKFILE_CHANGES_FULL_SIMPLE,
|
|
75
|
+
"markdown_table": EXPECTED_LOCKFILE_CHANGES_FULL_TABLE,
|
|
76
|
+
"output_format": OutputFormat.TABLE,
|
|
77
|
+
"removed": [
|
|
78
|
+
{"name": "removed_1", "version": "1.0.0"},
|
|
79
|
+
{"name": "removed_2", "version": "4.2.0"},
|
|
80
|
+
],
|
|
81
|
+
"requires_python": {"new": None, "old": None},
|
|
82
|
+
"show_learn_more_link": False,
|
|
83
|
+
"updated": [
|
|
84
|
+
{"name": "updated_1", "new_version": "2.0.0", "old_version": "1.0.0"},
|
|
85
|
+
{"name": "updated_2", "new_version": "2.0.0", "old_version": "1.0.0"},
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
EXPECTED_LOCKFILE_CHANGES_FULL_MODEL_DUMP_SIMPLE = {
|
|
91
|
+
"added": [
|
|
92
|
+
{"name": "added_1", "version": "1.0.0"},
|
|
93
|
+
{"name": "added_2", "version": "4.2.0"},
|
|
94
|
+
],
|
|
95
|
+
"items": 6,
|
|
96
|
+
"learn_more_link_text": "\n---\nLearn more about this report at https://github.com/mw-root/uv-lock-report",
|
|
97
|
+
"markdown": EXPECTED_LOCKFILE_CHANGES_FULL_SIMPLE,
|
|
98
|
+
"markdown_simple": EXPECTED_LOCKFILE_CHANGES_FULL_SIMPLE,
|
|
99
|
+
"markdown_table": EXPECTED_LOCKFILE_CHANGES_FULL_TABLE,
|
|
100
|
+
"output_format": OutputFormat.SIMPLE,
|
|
101
|
+
"removed": [
|
|
102
|
+
{"name": "removed_1", "version": "1.0.0"},
|
|
103
|
+
{"name": "removed_2", "version": "4.2.0"},
|
|
104
|
+
],
|
|
105
|
+
"requires_python": {"new": None, "old": None},
|
|
106
|
+
"show_learn_more_link": False,
|
|
107
|
+
"updated": [
|
|
108
|
+
{"name": "updated_1", "new_version": "2.0.0", "old_version": "1.0.0"},
|
|
109
|
+
{"name": "updated_2", "new_version": "2.0.0", "old_version": "1.0.0"},
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
EXPECTED_LOCKFILE_CHANGES_FULL_MODEL_DUMP_SIMPLE_WITH_LINK = {
|
|
114
|
+
"added": [
|
|
115
|
+
{"name": "added_1", "version": "1.0.0"},
|
|
116
|
+
{"name": "added_2", "version": "4.2.0"},
|
|
117
|
+
],
|
|
118
|
+
"items": 6,
|
|
119
|
+
"learn_more_link_text": "\n---\nLearn more about this report at https://github.com/mw-root/uv-lock-report",
|
|
120
|
+
"markdown": EXPECTED_LOCKFILE_CHANGES_FULL_SIMPLE_WITH_LINK,
|
|
121
|
+
"markdown_simple": EXPECTED_LOCKFILE_CHANGES_FULL_SIMPLE_WITH_LINK,
|
|
122
|
+
"markdown_table": EXPECTED_LOCKFILE_CHANGES_FULL_TABLE,
|
|
123
|
+
"output_format": OutputFormat.SIMPLE,
|
|
124
|
+
"removed": [
|
|
125
|
+
{"name": "removed_1", "version": "1.0.0"},
|
|
126
|
+
{"name": "removed_2", "version": "4.2.0"},
|
|
127
|
+
],
|
|
128
|
+
"requires_python": {"new": None, "old": None},
|
|
129
|
+
"show_learn_more_link": True,
|
|
130
|
+
"updated": [
|
|
131
|
+
{"name": "updated_1", "new_version": "2.0.0", "old_version": "1.0.0"},
|
|
132
|
+
{"name": "updated_2", "new_version": "2.0.0", "old_version": "1.0.0"},
|
|
133
|
+
],
|
|
134
|
+
}
|