woolly 0.4.0__tar.gz → 0.5.0__tar.gz

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.
Files changed (62) hide show
  1. {woolly-0.4.0 → woolly-0.5.0}/PKG-INFO +1 -1
  2. {woolly-0.4.0 → woolly-0.5.0}/tests/conftest.py +49 -0
  3. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_commands/test_check.py +99 -64
  4. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_languages/test_base.py +312 -0
  5. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_languages/test_python.py +266 -9
  6. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_languages/test_rust.py +119 -10
  7. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_optional_dependencies.py +36 -45
  8. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_reporters/test_json.py +35 -0
  9. woolly-0.5.0/tests/unit/test_reporters/test_stdout.py +187 -0
  10. {woolly-0.4.0 → woolly-0.5.0}/woolly/cache.py +21 -6
  11. {woolly-0.4.0 → woolly-0.5.0}/woolly/commands/check.py +197 -83
  12. woolly-0.5.0/woolly/http.py +53 -0
  13. {woolly-0.4.0 → woolly-0.5.0}/woolly/languages/base.py +187 -10
  14. woolly-0.5.0/woolly/languages/python.py +413 -0
  15. {woolly-0.4.0 → woolly-0.5.0}/woolly/languages/rust.py +59 -1
  16. {woolly-0.4.0 → woolly-0.5.0}/woolly/reporters/base.py +22 -0
  17. {woolly-0.4.0 → woolly-0.5.0}/woolly/reporters/json.py +96 -2
  18. {woolly-0.4.0 → woolly-0.5.0}/woolly/reporters/markdown.py +71 -0
  19. woolly-0.5.0/woolly/reporters/stdout.py +208 -0
  20. {woolly-0.4.0 → woolly-0.5.0}/woolly/reporters/template.py +30 -0
  21. woolly-0.4.0/tests/unit/test_reporters/test_stdout.py +0 -133
  22. woolly-0.4.0/woolly/http.py +0 -34
  23. woolly-0.4.0/woolly/languages/python.py +0 -200
  24. woolly-0.4.0/woolly/reporters/stdout.py +0 -76
  25. {woolly-0.4.0 → woolly-0.5.0}/.coverage +0 -0
  26. {woolly-0.4.0 → woolly-0.5.0}/.github/workflows/lint.yml +0 -0
  27. {woolly-0.4.0 → woolly-0.5.0}/.github/workflows/publish.yml +0 -0
  28. {woolly-0.4.0 → woolly-0.5.0}/.github/workflows/tests.yml +0 -0
  29. {woolly-0.4.0 → woolly-0.5.0}/.gitignore +0 -0
  30. {woolly-0.4.0 → woolly-0.5.0}/.python-version +0 -0
  31. {woolly-0.4.0 → woolly-0.5.0}/LICENSE +0 -0
  32. {woolly-0.4.0 → woolly-0.5.0}/README.md +0 -0
  33. {woolly-0.4.0 → woolly-0.5.0}/examples/template/example_template.md +0 -0
  34. {woolly-0.4.0 → woolly-0.5.0}/pyproject.toml +0 -0
  35. {woolly-0.4.0 → woolly-0.5.0}/tests/__init__.py +0 -0
  36. {woolly-0.4.0 → woolly-0.5.0}/tests/functional/__init__.py +0 -0
  37. {woolly-0.4.0 → woolly-0.5.0}/tests/functional/test_cli.py +0 -0
  38. {woolly-0.4.0 → woolly-0.5.0}/tests/functional/test_optional_flag.py +0 -0
  39. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/__init__.py +0 -0
  40. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_cache.py +0 -0
  41. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_commands/__init__.py +0 -0
  42. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_commands/test_other_commands.py +0 -0
  43. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_debug.py +0 -0
  44. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_languages/__init__.py +0 -0
  45. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_languages/test_registry.py +0 -0
  46. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_progress.py +0 -0
  47. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_reporters/__init__.py +0 -0
  48. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_reporters/test_base.py +0 -0
  49. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_reporters/test_markdown.py +0 -0
  50. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_reporters/test_registry.py +0 -0
  51. {woolly-0.4.0 → woolly-0.5.0}/tests/unit/test_reporters/test_template.py +0 -0
  52. {woolly-0.4.0 → woolly-0.5.0}/uv.lock +0 -0
  53. {woolly-0.4.0 → woolly-0.5.0}/woolly/__init__.py +0 -0
  54. {woolly-0.4.0 → woolly-0.5.0}/woolly/__main__.py +0 -0
  55. {woolly-0.4.0 → woolly-0.5.0}/woolly/commands/__init__.py +0 -0
  56. {woolly-0.4.0 → woolly-0.5.0}/woolly/commands/clear_cache.py +0 -0
  57. {woolly-0.4.0 → woolly-0.5.0}/woolly/commands/list_formats.py +0 -0
  58. {woolly-0.4.0 → woolly-0.5.0}/woolly/commands/list_languages.py +0 -0
  59. {woolly-0.4.0 → woolly-0.5.0}/woolly/debug.py +0 -0
  60. {woolly-0.4.0 → woolly-0.5.0}/woolly/languages/__init__.py +0 -0
  61. {woolly-0.4.0 → woolly-0.5.0}/woolly/progress.py +0 -0
  62. {woolly-0.4.0 → woolly-0.5.0}/woolly/reporters/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: woolly
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Check if package dependencies are available in Fedora. Supports Rust, Python, and more.
5
5
  Author-email: Rodolfo Olivieri <rodolfo.olivieri3@gmail.com>
6
6
  License-File: LICENSE
@@ -24,6 +24,10 @@ def temp_cache_dir(tmp_path, monkeypatch):
24
24
  cache_dir = tmp_path / "cache"
25
25
  cache_dir.mkdir()
26
26
  monkeypatch.setattr("woolly.cache.CACHE_DIR", cache_dir)
27
+ # Reset the ensured-namespaces set so that the new CACHE_DIR is used
28
+ import woolly.cache as _cache_mod
29
+
30
+ _cache_mod._ensured_namespaces.clear()
27
31
  return cache_dir
28
32
 
29
33
 
@@ -119,6 +123,7 @@ def mock_crates_io_response():
119
123
  "description": "A serialization framework for Rust",
120
124
  "homepage": "https://serde.rs",
121
125
  "repository": "https://github.com/serde-rs/serde",
126
+ "license": "MIT OR Apache-2.0",
122
127
  }
123
128
  }
124
129
 
@@ -154,12 +159,55 @@ def mock_pypi_response():
154
159
  "summary": "Python HTTP for Humans",
155
160
  "home_page": "https://requests.readthedocs.io",
156
161
  "project_url": "https://github.com/psf/requests",
162
+ "license": "Apache-2.0",
163
+ "license_expression": None,
164
+ "provides_extra": ["socks"],
165
+ "requires_dist": [
166
+ "charset-normalizer<4,>=2",
167
+ "idna<4,>=2.5",
168
+ "urllib3<3,>=1.21.1",
169
+ "certifi>=2017.4.17",
170
+ "PySocks!=1.5.7,>=1.5.6; extra == 'socks'",
171
+ ],
172
+ }
173
+ }
174
+
175
+
176
+ @pytest.fixture
177
+ def mock_crates_io_version_response():
178
+ """Create a mock crates.io version API response (for features)."""
179
+ return {
180
+ "version": {
181
+ "num": "1.0.200",
182
+ "features": {
183
+ "default": ["std", "derive"],
184
+ "std": [],
185
+ "derive": ["serde_derive"],
186
+ "alloc": [],
187
+ },
188
+ }
189
+ }
190
+
191
+
192
+ @pytest.fixture
193
+ def mock_pypi_version_response():
194
+ """Create a mock PyPI version API response (for extras/features)."""
195
+ return {
196
+ "info": {
197
+ "name": "requests",
198
+ "version": "2.31.0",
199
+ "summary": "Python HTTP for Humans",
200
+ "license": "Apache-2.0",
201
+ "license_expression": None,
202
+ "provides_extra": ["socks", "security"],
157
203
  "requires_dist": [
158
204
  "charset-normalizer<4,>=2",
159
205
  "idna<4,>=2.5",
160
206
  "urllib3<3,>=1.21.1",
161
207
  "certifi>=2017.4.17",
162
208
  "PySocks!=1.5.7,>=1.5.6; extra == 'socks'",
209
+ "pyOpenSSL>=0.14; extra == 'security'",
210
+ "cryptography>=1.3.4; extra == 'security'",
163
211
  ],
164
212
  }
165
213
  }
@@ -174,6 +222,7 @@ def mock_pypi_response():
174
222
  def mock_console():
175
223
  """Create a mock Rich console."""
176
224
  console = MagicMock(spec=Console)
225
+ console.width = 120 # Wide enough for side-by-side layout
177
226
  return console
178
227
 
179
228
 
@@ -12,7 +12,7 @@ from unittest.mock import MagicMock
12
12
  import pytest
13
13
  from rich.tree import Tree
14
14
 
15
- from woolly.commands.check import TreeStats, build_tree, collect_stats
15
+ from woolly.commands.check import TreeStats, _compute_stats_from_visited, build_tree
16
16
  from woolly.languages.base import (
17
17
  Dependency,
18
18
  FedoraPackageStatus,
@@ -41,6 +41,9 @@ class MockProvider(LanguageProvider):
41
41
  def fetch_dependencies(self, package_name: str, version: str):
42
42
  return self.dependencies.get(f"{package_name}:{version}", [])
43
43
 
44
+ def fetch_features(self, package_name: str, version: str):
45
+ return []
46
+
44
47
  def check_fedora_packaging(self, package_name: str):
45
48
  return self.fedora_status.get(
46
49
  package_name, FedoraPackageStatus(is_packaged=False)
@@ -106,7 +109,7 @@ class TestBuildTree:
106
109
  @pytest.mark.unit
107
110
  def test_returns_visited_marker_for_duplicate(self, provider):
108
111
  """Critical path: returns visited marker for already-visited packages."""
109
- visited = {"root": (True, "1.0.0")}
112
+ visited = {"root": (True, "1.0.0", False)}
110
113
 
111
114
  result = build_tree(provider, "root", visited=visited)
112
115
 
@@ -157,6 +160,32 @@ class TestBuildTree:
157
160
  label = str(tree.label)
158
161
  assert "1.0.0" in label
159
162
 
163
+ @pytest.mark.unit
164
+ def test_includes_license_in_label(self, provider):
165
+ """Good path: license is shown in tree label."""
166
+ provider.packages["licensed-pkg"] = PackageInfo(
167
+ name="licensed-pkg", latest_version="1.0.0", license="MIT"
168
+ )
169
+ provider.fedora_status["licensed-pkg"] = FedoraPackageStatus(
170
+ is_packaged=True, versions=["1.0.0"]
171
+ )
172
+
173
+ tree = build_tree(provider, "licensed-pkg")
174
+
175
+ assert isinstance(tree, Tree)
176
+ label = str(tree.label)
177
+ assert "MIT" in label
178
+
179
+ @pytest.mark.unit
180
+ def test_no_license_marker_when_no_license(self, provider):
181
+ """Good path: no license marker when package has no license."""
182
+ tree = build_tree(provider, "root")
183
+
184
+ assert isinstance(tree, Tree)
185
+ label = str(tree.label)
186
+ # License info from mock_package_info is None, so no license marker
187
+ assert "(None)" not in label
188
+
160
189
  @pytest.mark.unit
161
190
  def test_updates_progress_tracker(self, provider):
162
191
  """Good path: updates progress tracker when provided."""
@@ -291,115 +320,121 @@ class TestBuildTree:
291
320
  assert len(tree.children) == 1
292
321
 
293
322
 
294
- class TestCollectStats:
295
- """Tests for collect_stats function."""
323
+ class TestComputeStatsFromVisited:
324
+ """Tests for _compute_stats_from_visited function."""
296
325
 
297
326
  @pytest.mark.unit
298
327
  def test_returns_tree_stats_model(self):
299
328
  """Good path: returns TreeStats model."""
300
- tree = Tree("[bold]root[/bold] v1.0.0 • [green]✓ packaged[/green]")
329
+ visited = {"root": (True, "1.0.0", False)}
301
330
 
302
- stats = collect_stats(tree)
331
+ stats = _compute_stats_from_visited(visited)
303
332
 
304
333
  assert isinstance(stats, TreeStats)
305
334
 
306
335
  @pytest.mark.unit
307
336
  def test_counts_packaged(self):
308
337
  """Good path: counts packaged packages."""
309
- tree = Tree("[bold]root[/bold] v1.0.0 • [green]✓ packaged[/green]")
338
+ visited = {"root": (True, "1.0.0", False)}
310
339
 
311
- stats = collect_stats(tree)
340
+ stats = _compute_stats_from_visited(visited)
312
341
 
313
- assert stats.packaged >= 1
342
+ assert stats.packaged == 1
314
343
 
315
344
  @pytest.mark.unit
316
345
  def test_counts_missing(self):
317
346
  """Good path: counts missing packages."""
318
- tree = Tree("[bold]root[/bold] v1.0.0 • [red]✗ not packaged[/red]")
347
+ visited = {"root": (False, "1.0.0", False)}
319
348
 
320
- stats = collect_stats(tree)
349
+ stats = _compute_stats_from_visited(visited)
321
350
 
322
- assert stats.missing >= 1
351
+ assert stats.missing == 1
323
352
 
324
353
  @pytest.mark.unit
325
354
  def test_collects_missing_list(self):
326
355
  """Good path: collects list of missing packages."""
327
- tree = Tree("[bold]missing-pkg[/bold] v1.0.0 • [red]✗ not packaged[/red]")
356
+ visited = {"missing-pkg": (False, "1.0.0", False)}
328
357
 
329
- stats = collect_stats(tree)
358
+ stats = _compute_stats_from_visited(visited)
330
359
 
331
- assert len(stats.missing_list) >= 1
360
+ assert len(stats.missing_list) == 1
361
+ assert "missing-pkg" in stats.missing_list
332
362
 
333
363
  @pytest.mark.unit
334
364
  def test_collects_packaged_list(self):
335
365
  """Good path: collects list of packaged packages."""
336
- tree = Tree("[bold]pkg[/bold] v1.0.0 • [green]✓ packaged[/green]")
366
+ visited = {"pkg": (True, "1.0.0", False)}
337
367
 
338
- stats = collect_stats(tree)
368
+ stats = _compute_stats_from_visited(visited)
339
369
 
340
- assert len(stats.packaged_list) >= 1
370
+ assert len(stats.packaged_list) == 1
371
+ assert "pkg" in stats.packaged_list
341
372
 
342
373
  @pytest.mark.unit
343
- def test_handles_string_children(self):
344
- """Good path: handles string children (visited markers)."""
345
- tree = Tree("[bold]root[/bold]")
346
- tree.add("[dim]child[/dim] • [green]✓[/green] (already visited)")
374
+ def test_counts_not_found_as_missing(self):
375
+ """Good path: counts not-found packages (version=None) as missing."""
376
+ visited = {"unknown": (False, None, False)}
347
377
 
348
- stats = collect_stats(tree)
378
+ stats = _compute_stats_from_visited(visited)
349
379
 
350
- assert stats.total >= 1
380
+ assert stats.missing == 1
381
+ assert "unknown" in stats.missing_list
351
382
 
352
383
  @pytest.mark.unit
353
- def test_counts_not_found_as_missing(self):
354
- """Good path: counts 'not found' as missing."""
355
- tree = Tree("[bold]unknown[/bold] • [red]not found on registry[/red]")
384
+ def test_multiple_packages(self):
385
+ """Critical path: counts all packages from visited dict."""
386
+ visited = {
387
+ "root": (True, "1.0.0", False),
388
+ "child1": (True, "1.0.0", False),
389
+ "child2": (False, "1.0.0", False),
390
+ }
356
391
 
357
- stats = collect_stats(tree)
392
+ stats = _compute_stats_from_visited(visited)
358
393
 
359
- assert stats.missing >= 1
394
+ assert stats.total == 3
395
+ assert stats.packaged == 2
396
+ assert stats.missing == 1
360
397
 
361
398
  @pytest.mark.unit
362
- def test_extracts_name_from_not_found_string(self):
363
- """Bug fix: correctly extracts package name from 'not found' string with [bold red] format."""
364
- # This is the format returned by build_tree when a package is not found on the registry
365
- tree = Tree("[bold]root[/bold] v1.0.0 • [green]✓ packaged[/green]")
366
- # Add a child that represents a package not found on the registry (string format)
367
- tree.add(
368
- "[bold red]nonexistent-pkg[/bold red] • [red]not found on crates.io[/red]"
369
- )
370
-
371
- stats = collect_stats(tree)
399
+ def test_empty_visited(self):
400
+ """Good path: handles empty visited dict."""
401
+ stats = _compute_stats_from_visited({})
372
402
 
373
- assert stats.missing == 1
374
- assert "nonexistent-pkg" in stats.missing_list
375
- # Ensure we don't have malformed names like "[bold"
376
- for name in stats.missing_list:
377
- assert not name.startswith("["), f"Malformed package name: {name}"
403
+ assert stats.total == 0
404
+ assert stats.packaged == 0
405
+ assert stats.missing == 0
406
+ assert stats.missing_list == []
407
+ assert stats.packaged_list == []
378
408
 
379
409
  @pytest.mark.unit
380
- def test_recursive_counting(self):
381
- """Critical path: counts all nodes recursively."""
382
- root = Tree("[bold]root[/bold] v1.0.0 • [green]✓ packaged[/green]")
383
- child1 = Tree("[bold]child1[/bold] v1.0.0 • [green]✓ packaged[/green]")
384
- child2 = Tree("[bold]child2[/bold] v1.0.0 • [red]✗ not packaged[/red]")
385
- root.children.append(child1)
386
- root.children.append(child2)
410
+ def test_has_dev_build_stats(self):
411
+ """Good path: stats model has dev/build dependency stats (zeroed by default)."""
412
+ visited = {"pkg": (True, "1.0.0", False)}
387
413
 
388
- stats = collect_stats(root)
414
+ stats = _compute_stats_from_visited(visited)
389
415
 
390
- assert stats.total == 3
391
- assert stats.packaged == 2
392
- assert stats.missing == 1
416
+ assert hasattr(stats, "dev_total")
417
+ assert hasattr(stats, "dev_packaged")
418
+ assert hasattr(stats, "dev_missing")
419
+ assert hasattr(stats, "build_total")
420
+ assert hasattr(stats, "build_packaged")
421
+ assert hasattr(stats, "build_missing")
422
+ assert stats.dev_total == 0
423
+ assert stats.build_total == 0
393
424
 
394
425
  @pytest.mark.unit
395
- def test_initializes_stats_if_not_provided(self):
396
- """Good path: initializes stats model if not provided."""
397
- tree = Tree("[bold]pkg[/bold]")
426
+ def test_optional_dependency_tracking(self):
427
+ """Good path: tracks optional dependency statistics."""
428
+ visited = {
429
+ "required": (True, "1.0.0", False),
430
+ "opt-packaged": (True, "2.0.0", True),
431
+ "opt-missing": (False, "3.0.0", True),
432
+ }
398
433
 
399
- stats = collect_stats(tree)
434
+ stats = _compute_stats_from_visited(visited)
400
435
 
401
- assert hasattr(stats, "total")
402
- assert hasattr(stats, "packaged")
403
- assert hasattr(stats, "missing")
404
- assert hasattr(stats, "missing_list")
405
- assert hasattr(stats, "packaged_list")
436
+ assert stats.total == 3
437
+ assert stats.optional_total == 2
438
+ assert stats.optional_packaged == 1
439
+ assert stats.optional_missing == 1
440
+ assert "opt-missing" in stats.optional_missing_list