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.

@@ -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
+ ]
@@ -0,0 +1,11 @@
1
+ """
2
+ Entry point for running uv_lock_report as a module.
3
+
4
+ This allows the package to be executed with:
5
+ python -m uv_lock_report
6
+ """
7
+
8
+ from uv_lock_report.cli import main
9
+
10
+ if __name__ == "__main__":
11
+ main()
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
+ )
@@ -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)
@@ -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
+ }