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,324 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from uv_lock_report.models import UvLockFile
|
|
6
|
+
from uv_lock_report.report import get_new_uv_lock_file, get_old_uv_lock_file
|
|
7
|
+
|
|
8
|
+
# Sample minimal valid uv.lock content for testing
|
|
9
|
+
SAMPLE_UV_LOCK = """version = 1
|
|
10
|
+
revision = 3
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
|
+
|
|
13
|
+
[[package]]
|
|
14
|
+
name = "test-package"
|
|
15
|
+
version = "1.0.0"
|
|
16
|
+
source = { registry = "https://pypi.org/simple" }
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
SAMPLE_UV_LOCK_UPDATED = """version = 1
|
|
20
|
+
revision = 3
|
|
21
|
+
requires-python = ">=3.13"
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "test-package"
|
|
25
|
+
version = "2.0.0"
|
|
26
|
+
source = { registry = "https://pypi.org/simple" }
|
|
27
|
+
|
|
28
|
+
[[package]]
|
|
29
|
+
name = "new-package"
|
|
30
|
+
version = "1.5.0"
|
|
31
|
+
source = { registry = "https://pypi.org/simple" }
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestGetNewUvLockFile:
|
|
36
|
+
"""Test the get_new_uv_lock_file function for reading current uv.lock files."""
|
|
37
|
+
|
|
38
|
+
def test_lockfile_exists(self, tmp_path):
|
|
39
|
+
"""Test when uv.lock exists in the base_path."""
|
|
40
|
+
# Create a uv.lock file in the temporary directory
|
|
41
|
+
uv_lock_path = tmp_path / "uv.lock"
|
|
42
|
+
uv_lock_path.write_text(SAMPLE_UV_LOCK)
|
|
43
|
+
|
|
44
|
+
# Call the function
|
|
45
|
+
result = get_new_uv_lock_file(str(tmp_path))
|
|
46
|
+
|
|
47
|
+
# Verify the result
|
|
48
|
+
assert result is not None
|
|
49
|
+
assert isinstance(result, UvLockFile)
|
|
50
|
+
assert result.version == 1
|
|
51
|
+
assert result.revision == 3
|
|
52
|
+
assert result.requires_python == ">=3.13"
|
|
53
|
+
assert len(result.packages) == 1
|
|
54
|
+
assert result.packages[0].name == "test-package"
|
|
55
|
+
assert str(result.packages[0].version) == "1.0.0"
|
|
56
|
+
|
|
57
|
+
def test_lockfile_not_exists(self, tmp_path, capsys):
|
|
58
|
+
"""Test when uv.lock does not exist in the base_path."""
|
|
59
|
+
# Call the function without creating a uv.lock file
|
|
60
|
+
result = get_new_uv_lock_file(str(tmp_path))
|
|
61
|
+
|
|
62
|
+
# Verify the result
|
|
63
|
+
assert result is None
|
|
64
|
+
|
|
65
|
+
# Verify the error message was printed
|
|
66
|
+
captured = capsys.readouterr()
|
|
67
|
+
assert "uv.lock not found in current working directory" in captured.out
|
|
68
|
+
|
|
69
|
+
def test_lockfile_with_multiple_packages(self, tmp_path):
|
|
70
|
+
"""Test parsing a lockfile with multiple packages."""
|
|
71
|
+
uv_lock_path = tmp_path / "uv.lock"
|
|
72
|
+
uv_lock_path.write_text(SAMPLE_UV_LOCK_UPDATED)
|
|
73
|
+
|
|
74
|
+
result = get_new_uv_lock_file(str(tmp_path))
|
|
75
|
+
|
|
76
|
+
assert result is not None
|
|
77
|
+
assert len(result.packages) == 2
|
|
78
|
+
package_names = {pkg.name for pkg in result.packages}
|
|
79
|
+
assert package_names == {"test-package", "new-package"}
|
|
80
|
+
|
|
81
|
+
def test_lockfile_with_relative_path(self, tmp_path):
|
|
82
|
+
"""Test when base_path is a relative path."""
|
|
83
|
+
# Create a subdirectory
|
|
84
|
+
subdir = tmp_path / "subdir"
|
|
85
|
+
subdir.mkdir()
|
|
86
|
+
uv_lock_path = subdir / "uv.lock"
|
|
87
|
+
uv_lock_path.write_text(SAMPLE_UV_LOCK)
|
|
88
|
+
|
|
89
|
+
# Call with relative path components
|
|
90
|
+
result = get_new_uv_lock_file(str(subdir))
|
|
91
|
+
|
|
92
|
+
assert result is not None
|
|
93
|
+
assert isinstance(result, UvLockFile)
|
|
94
|
+
|
|
95
|
+
def test_lockfile_malformed_toml(self, tmp_path):
|
|
96
|
+
"""Test when uv.lock contains malformed TOML."""
|
|
97
|
+
uv_lock_path = tmp_path / "uv.lock"
|
|
98
|
+
uv_lock_path.write_text("this is not valid toml {{{")
|
|
99
|
+
|
|
100
|
+
# Should raise an exception when parsing
|
|
101
|
+
with pytest.raises(Exception): # tomllib.TOMLDecodeError or similar
|
|
102
|
+
get_new_uv_lock_file(str(tmp_path))
|
|
103
|
+
|
|
104
|
+
def test_lockfile_empty(self, tmp_path):
|
|
105
|
+
"""Test when uv.lock is empty."""
|
|
106
|
+
uv_lock_path = tmp_path / "uv.lock"
|
|
107
|
+
uv_lock_path.write_text("")
|
|
108
|
+
|
|
109
|
+
# Should raise an exception when parsing
|
|
110
|
+
with pytest.raises(Exception):
|
|
111
|
+
get_new_uv_lock_file(str(tmp_path))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TestGetOldUvLockFile:
|
|
115
|
+
"""Test the get_old_uv_lock_file function for reading historical uv.lock files via git."""
|
|
116
|
+
|
|
117
|
+
def test_git_show_success(self, tmp_path, capsys):
|
|
118
|
+
"""Test when git show successfully retrieves the lockfile."""
|
|
119
|
+
base_sha = "abc123"
|
|
120
|
+
|
|
121
|
+
# Mock subprocess.run to return a successful result
|
|
122
|
+
mock_result = MagicMock()
|
|
123
|
+
mock_result.returncode = 0
|
|
124
|
+
mock_result.stdout = SAMPLE_UV_LOCK
|
|
125
|
+
mock_result.stderr = ""
|
|
126
|
+
|
|
127
|
+
with patch(
|
|
128
|
+
"uv_lock_report.report.subprocess.run", return_value=mock_result
|
|
129
|
+
) as mock_run:
|
|
130
|
+
result = get_old_uv_lock_file(base_sha, str(tmp_path))
|
|
131
|
+
|
|
132
|
+
# Verify subprocess.run was called correctly
|
|
133
|
+
mock_run.assert_called_once_with(
|
|
134
|
+
["git", "show", f"{base_sha}:uv.lock"],
|
|
135
|
+
capture_output=True,
|
|
136
|
+
text=True,
|
|
137
|
+
cwd=str(tmp_path),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Verify the result
|
|
141
|
+
assert result is not None
|
|
142
|
+
assert isinstance(result, UvLockFile)
|
|
143
|
+
assert result.version == 1
|
|
144
|
+
assert len(result.packages) == 1
|
|
145
|
+
assert result.packages[0].name == "test-package"
|
|
146
|
+
|
|
147
|
+
# Verify success message was printed
|
|
148
|
+
captured = capsys.readouterr()
|
|
149
|
+
assert "Found uv.lock in base commit." in captured.out
|
|
150
|
+
|
|
151
|
+
def test_git_show_file_not_found(self, tmp_path, capsys):
|
|
152
|
+
"""Test when git show fails because the file doesn't exist in the commit."""
|
|
153
|
+
base_sha = "abc123"
|
|
154
|
+
|
|
155
|
+
# Mock subprocess.run to return a failure
|
|
156
|
+
mock_result = MagicMock()
|
|
157
|
+
mock_result.returncode = 128
|
|
158
|
+
mock_result.stdout = ""
|
|
159
|
+
mock_result.stderr = "fatal: path 'uv.lock' does not exist in 'abc123'"
|
|
160
|
+
mock_result.args = ["git", "show", "abc123:uv.lock"]
|
|
161
|
+
|
|
162
|
+
with patch("uv_lock_report.report.subprocess.run", return_value=mock_result):
|
|
163
|
+
result = get_old_uv_lock_file(base_sha, str(tmp_path))
|
|
164
|
+
|
|
165
|
+
# Verify the result
|
|
166
|
+
assert result is None
|
|
167
|
+
|
|
168
|
+
# Verify error messages were printed
|
|
169
|
+
captured = capsys.readouterr()
|
|
170
|
+
assert "uv.lock not found in base commit" in captured.out
|
|
171
|
+
assert "fatal: path 'uv.lock' does not exist" in captured.out
|
|
172
|
+
|
|
173
|
+
def test_git_show_invalid_commit(self, tmp_path, capsys):
|
|
174
|
+
"""Test when git show fails because the commit SHA is invalid."""
|
|
175
|
+
base_sha = "invalidsha"
|
|
176
|
+
|
|
177
|
+
# Mock subprocess.run to return a failure
|
|
178
|
+
mock_result = MagicMock()
|
|
179
|
+
mock_result.returncode = 128
|
|
180
|
+
mock_result.stdout = ""
|
|
181
|
+
mock_result.stderr = "fatal: invalid object name 'invalidsha'"
|
|
182
|
+
mock_result.args = ["git", "show", "invalidsha:uv.lock"]
|
|
183
|
+
|
|
184
|
+
with patch("uv_lock_report.report.subprocess.run", return_value=mock_result):
|
|
185
|
+
result = get_old_uv_lock_file(base_sha, str(tmp_path))
|
|
186
|
+
|
|
187
|
+
# Verify the result
|
|
188
|
+
assert result is None
|
|
189
|
+
|
|
190
|
+
# Verify error messages were printed
|
|
191
|
+
captured = capsys.readouterr()
|
|
192
|
+
assert "uv.lock not found in base commit" in captured.out
|
|
193
|
+
|
|
194
|
+
def test_git_show_with_different_sha_formats(self, tmp_path):
|
|
195
|
+
"""Test with various SHA formats (full SHA, short SHA, branch name, tag)."""
|
|
196
|
+
test_cases = [
|
|
197
|
+
"a1b2c3d4e5f6", # Short SHA
|
|
198
|
+
"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", # Full SHA
|
|
199
|
+
"main", # Branch name
|
|
200
|
+
"v1.0.0", # Tag
|
|
201
|
+
"HEAD~1", # Relative reference
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
mock_result = MagicMock()
|
|
205
|
+
mock_result.returncode = 0
|
|
206
|
+
mock_result.stdout = SAMPLE_UV_LOCK
|
|
207
|
+
|
|
208
|
+
for sha in test_cases:
|
|
209
|
+
with patch(
|
|
210
|
+
"uv_lock_report.report.subprocess.run", return_value=mock_result
|
|
211
|
+
) as mock_run:
|
|
212
|
+
result = get_old_uv_lock_file(sha, str(tmp_path))
|
|
213
|
+
|
|
214
|
+
# Verify subprocess.run was called with the correct SHA
|
|
215
|
+
mock_run.assert_called_once_with(
|
|
216
|
+
["git", "show", f"{sha}:uv.lock"],
|
|
217
|
+
capture_output=True,
|
|
218
|
+
text=True,
|
|
219
|
+
cwd=str(tmp_path),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Verify the result
|
|
223
|
+
assert result is not None
|
|
224
|
+
|
|
225
|
+
def test_git_show_with_updated_lockfile(self, tmp_path):
|
|
226
|
+
"""Test parsing an updated lockfile from git."""
|
|
227
|
+
base_sha = "def456"
|
|
228
|
+
|
|
229
|
+
mock_result = MagicMock()
|
|
230
|
+
mock_result.returncode = 0
|
|
231
|
+
mock_result.stdout = SAMPLE_UV_LOCK_UPDATED
|
|
232
|
+
|
|
233
|
+
with patch("uv_lock_report.report.subprocess.run", return_value=mock_result):
|
|
234
|
+
result = get_old_uv_lock_file(base_sha, str(tmp_path))
|
|
235
|
+
|
|
236
|
+
assert result is not None
|
|
237
|
+
assert len(result.packages) == 2
|
|
238
|
+
package_names = {pkg.name for pkg in result.packages}
|
|
239
|
+
assert package_names == {"test-package", "new-package"}
|
|
240
|
+
|
|
241
|
+
def test_git_show_malformed_output(self, tmp_path):
|
|
242
|
+
"""Test when git show returns malformed TOML."""
|
|
243
|
+
base_sha = "abc123"
|
|
244
|
+
|
|
245
|
+
mock_result = MagicMock()
|
|
246
|
+
mock_result.returncode = 0
|
|
247
|
+
mock_result.stdout = "this is not valid toml {[["
|
|
248
|
+
|
|
249
|
+
with patch("uv_lock_report.report.subprocess.run", return_value=mock_result):
|
|
250
|
+
# Should raise an exception when parsing
|
|
251
|
+
with pytest.raises(Exception):
|
|
252
|
+
get_old_uv_lock_file(base_sha, str(tmp_path))
|
|
253
|
+
|
|
254
|
+
def test_git_command_construction(self, tmp_path):
|
|
255
|
+
"""Test that the git command is constructed correctly."""
|
|
256
|
+
base_sha = "test-sha"
|
|
257
|
+
base_path = str(tmp_path)
|
|
258
|
+
|
|
259
|
+
mock_result = MagicMock()
|
|
260
|
+
mock_result.returncode = 0
|
|
261
|
+
mock_result.stdout = SAMPLE_UV_LOCK
|
|
262
|
+
|
|
263
|
+
with patch(
|
|
264
|
+
"uv_lock_report.report.subprocess.run", return_value=mock_result
|
|
265
|
+
) as mock_run:
|
|
266
|
+
get_old_uv_lock_file(base_sha, base_path)
|
|
267
|
+
|
|
268
|
+
# Verify the exact command structure
|
|
269
|
+
call_args = mock_run.call_args
|
|
270
|
+
assert call_args[0][0] == ["git", "show", f"{base_sha}:uv.lock"]
|
|
271
|
+
assert call_args[1]["capture_output"] is True
|
|
272
|
+
assert call_args[1]["text"] is True
|
|
273
|
+
assert call_args[1]["cwd"] == base_path
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class TestGetLockfilesIntegration:
|
|
277
|
+
"""Integration tests comparing get_new_uv_lock_file and get_old_uv_lock_file."""
|
|
278
|
+
|
|
279
|
+
def test_same_lockfile_content(self, tmp_path):
|
|
280
|
+
"""Test that both functions can parse the same lockfile content."""
|
|
281
|
+
# Write current lockfile
|
|
282
|
+
uv_lock_path = tmp_path / "uv.lock"
|
|
283
|
+
uv_lock_path.write_text(SAMPLE_UV_LOCK)
|
|
284
|
+
|
|
285
|
+
# Mock git show to return the same content
|
|
286
|
+
mock_result = MagicMock()
|
|
287
|
+
mock_result.returncode = 0
|
|
288
|
+
mock_result.stdout = SAMPLE_UV_LOCK
|
|
289
|
+
|
|
290
|
+
new_lockfile = get_new_uv_lock_file(str(tmp_path))
|
|
291
|
+
|
|
292
|
+
with patch("uv_lock_report.report.subprocess.run", return_value=mock_result):
|
|
293
|
+
old_lockfile = get_old_uv_lock_file("abc123", str(tmp_path))
|
|
294
|
+
|
|
295
|
+
# Both should return valid lockfiles with identical content
|
|
296
|
+
assert new_lockfile is not None
|
|
297
|
+
assert old_lockfile is not None
|
|
298
|
+
assert new_lockfile.version == old_lockfile.version
|
|
299
|
+
assert new_lockfile.revision == old_lockfile.revision
|
|
300
|
+
assert len(new_lockfile.packages) == len(old_lockfile.packages)
|
|
301
|
+
assert new_lockfile.packages[0].name == old_lockfile.packages[0].name
|
|
302
|
+
assert new_lockfile.packages[0].version == old_lockfile.packages[0].version
|
|
303
|
+
|
|
304
|
+
def test_different_lockfile_content(self, tmp_path):
|
|
305
|
+
"""Test that functions correctly handle different lockfile versions."""
|
|
306
|
+
# Write current lockfile
|
|
307
|
+
uv_lock_path = tmp_path / "uv.lock"
|
|
308
|
+
uv_lock_path.write_text(SAMPLE_UV_LOCK_UPDATED)
|
|
309
|
+
|
|
310
|
+
# Mock git show to return the old content
|
|
311
|
+
mock_result = MagicMock()
|
|
312
|
+
mock_result.returncode = 0
|
|
313
|
+
mock_result.stdout = SAMPLE_UV_LOCK
|
|
314
|
+
|
|
315
|
+
new_lockfile = get_new_uv_lock_file(str(tmp_path))
|
|
316
|
+
|
|
317
|
+
with patch("uv_lock_report.report.subprocess.run", return_value=mock_result):
|
|
318
|
+
old_lockfile = get_old_uv_lock_file("abc123", str(tmp_path))
|
|
319
|
+
|
|
320
|
+
# Both should be valid but with different content
|
|
321
|
+
assert new_lockfile is not None
|
|
322
|
+
assert old_lockfile is not None
|
|
323
|
+
assert len(new_lockfile.packages) == 2
|
|
324
|
+
assert len(old_lockfile.packages) == 1
|