codeowners-coverage 0.3.0__tar.gz → 0.3.1__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 (32) hide show
  1. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/PKG-INFO +1 -1
  2. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/pyproject.toml +1 -1
  3. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/__init__.py +1 -1
  4. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/cli.py +27 -18
  5. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage.egg-info/PKG-INFO +1 -1
  6. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage.egg-info/SOURCES.txt +1 -0
  7. codeowners_coverage-0.3.1/tests/test_cli.py +228 -0
  8. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/README.md +0 -0
  9. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/setup.cfg +0 -0
  10. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/__main__.py +0 -0
  11. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/checker.py +0 -0
  12. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/config.py +0 -0
  13. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/directory_consolidator.py +0 -0
  14. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/git_analyzer.py +0 -0
  15. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/github_client.py +0 -0
  16. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/matcher.py +0 -0
  17. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/ollama_matcher.py +0 -0
  18. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/suggest_cache.py +0 -0
  19. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage/suggester.py +0 -0
  20. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage.egg-info/dependency_links.txt +0 -0
  21. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage.egg-info/entry_points.txt +0 -0
  22. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage.egg-info/requires.txt +0 -0
  23. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/src/codeowners_coverage.egg-info/top_level.txt +0 -0
  24. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/tests/test_checker.py +0 -0
  25. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/tests/test_config.py +0 -0
  26. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/tests/test_directory_consolidator.py +0 -0
  27. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/tests/test_git_analyzer.py +0 -0
  28. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/tests/test_github_client.py +0 -0
  29. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/tests/test_matcher.py +0 -0
  30. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/tests/test_ollama_matcher.py +0 -0
  31. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/tests/test_suggest_cache.py +0 -0
  32. {codeowners_coverage-0.3.0 → codeowners_coverage-0.3.1}/tests/test_suggester.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeowners-coverage
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Measure and enforce CODEOWNERS coverage
5
5
  Author-email: Sentry Team <hello@sentry.io>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codeowners-coverage"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Measure and enforce CODEOWNERS coverage"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,6 +1,6 @@
1
1
  """CODEOWNERS coverage checking tool."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.1"
4
4
 
5
5
  from .checker import CoverageChecker
6
6
  from .config import Config
@@ -30,7 +30,12 @@ def cli() -> None:
30
30
  @click.option("--json", "output_json", is_flag=True, help="Output JSON format")
31
31
  @click.option("--files", multiple=True, help="Specific files to check")
32
32
  @click.option("--config", default=".codeowners-config.yml", help="Config file path")
33
- def check(output_json: bool, files: Tuple[str, ...], config: str) -> None:
33
+ @click.option(
34
+ "--allow-dirty-baseline",
35
+ is_flag=True,
36
+ help="Don't fail when baseline contains entries that are now covered",
37
+ )
38
+ def check(output_json: bool, files: Tuple[str, ...], config: str, allow_dirty_baseline: bool) -> None:
34
39
  """
35
40
  Check CODEOWNERS coverage.
36
41
 
@@ -48,24 +53,21 @@ def check(output_json: bool, files: Tuple[str, ...], config: str) -> None:
48
53
  file_list: List[str] | None = list(files) if files else None
49
54
  result = checker.check_coverage(file_list)
50
55
 
51
- if output_json:
52
- click.echo(json.dumps(result, indent=2))
53
- else:
54
- _print_human_readable_result(result)
55
-
56
- # Exit with error if there are new uncovered files
57
- if result["uncovered_files"]:
58
- sys.exit(1)
59
-
60
- # Exit with code 2 if baseline can be reduced (positive signal)
61
- # Check if any baseline entries (literal paths or glob patterns)
62
- # no longer match any uncovered files
56
+ # Calculate unused baseline entries early
63
57
  loaded_baseline = checker._load_baseline()
64
58
  baseline_matched = cast(List[str], result["baseline_files"])
65
59
  unused_entries = loaded_baseline.get_unused_entries(baseline_matched)
66
60
 
67
- if unused_entries:
68
- if not output_json:
61
+ # Output results
62
+ if output_json:
63
+ json_output = result.copy()
64
+ json_output["unused_baseline_entries"] = unused_entries
65
+ click.echo(json.dumps(json_output, indent=2))
66
+ else:
67
+ _print_human_readable_result(result)
68
+
69
+ # Print unused entries message in human-readable mode
70
+ if unused_entries:
69
71
  click.echo(f"\n🎉 Great news! {len(unused_entries)} baseline entries can be removed:")
70
72
  for entry in unused_entries[:10]:
71
73
  click.echo(f" - {entry}")
@@ -73,6 +75,14 @@ def check(output_json: bool, files: Tuple[str, ...], config: str) -> None:
73
75
  click.echo(f" ... and {len(unused_entries) - 10} more")
74
76
  click.echo("\nThese entries now have CODEOWNERS coverage! Update the baseline:")
75
77
  click.echo(" codeowners-coverage baseline")
78
+
79
+ # Exit with error if there are new uncovered files
80
+ if result["uncovered_files"]:
81
+ sys.exit(1)
82
+
83
+ # Exit with code 2 if baseline can be reduced (positive signal)
84
+ # Only exit with code 2 if baseline is dirty AND flag is not set
85
+ if unused_entries and not allow_dirty_baseline:
76
86
  sys.exit(2)
77
87
 
78
88
  except FileNotFoundError as e:
@@ -207,9 +217,8 @@ def suggest(
207
217
  from .git_analyzer import GitHistoryAnalyzer
208
218
  from .github_client import GitHubClient
209
219
  from .matcher import CodeOwnersPatternMatcher
210
- from .ollama_matcher import OllamaLLMMatcher, TeamSuggestion
211
- from .suggest_cache import SuggestCache
212
- from .suggester import OwnershipSuggester, SuggestionResult
220
+ from .ollama_matcher import OllamaLLMMatcher
221
+ from .suggester import OwnershipSuggester
213
222
 
214
223
  # Load config
215
224
  cfg = Config.load(config)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeowners-coverage
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Measure and enforce CODEOWNERS coverage
5
5
  Author-email: Sentry Team <hello@sentry.io>
6
6
  License: Apache-2.0
@@ -19,6 +19,7 @@ src/codeowners_coverage.egg-info/entry_points.txt
19
19
  src/codeowners_coverage.egg-info/requires.txt
20
20
  src/codeowners_coverage.egg-info/top_level.txt
21
21
  tests/test_checker.py
22
+ tests/test_cli.py
22
23
  tests/test_config.py
23
24
  tests/test_directory_consolidator.py
24
25
  tests/test_git_analyzer.py
@@ -0,0 +1,228 @@
1
+ """Tests for CLI functionality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import tempfile
7
+ from pathlib import Path
8
+ from unittest.mock import patch
9
+
10
+ from click.testing import CliRunner
11
+
12
+ from codeowners_coverage.cli import cli
13
+
14
+
15
+ def test_check_dirty_baseline_without_flag_exits_2() -> None:
16
+ """Test that check exits with code 2 when baseline is dirty (default behavior)."""
17
+ runner = CliRunner()
18
+
19
+ with tempfile.TemporaryDirectory() as tmpdir:
20
+ # Create CODEOWNERS file
21
+ codeowners_path = Path(tmpdir) / "CODEOWNERS"
22
+ codeowners_path.write_text("*.py @python-team\n")
23
+
24
+ # Create baseline with an entry that now has coverage
25
+ baseline_path = Path(tmpdir) / "baseline.txt"
26
+ baseline_path.write_text("covered.py\n")
27
+
28
+ # Create config
29
+ config_path = Path(tmpdir) / ".codeowners-config.yml"
30
+ config_path.write_text(
31
+ f"codeowners_path: {codeowners_path}\n"
32
+ f"baseline_path: {baseline_path}\n"
33
+ f"exclusions: []\n"
34
+ )
35
+
36
+ # Mock get_repository_files to return empty (all files covered)
37
+ with patch(
38
+ "codeowners_coverage.checker.CoverageChecker.get_repository_files",
39
+ return_value=["covered.py"],
40
+ ):
41
+ result = runner.invoke(cli, ["check", "--config", str(config_path)])
42
+
43
+ # Should exit with code 2 (baseline can be reduced)
44
+ assert result.exit_code == 2
45
+ assert "baseline entries can be removed" in result.output
46
+
47
+
48
+ def test_check_dirty_baseline_with_flag_exits_0() -> None:
49
+ """Test that check exits with code 0 when baseline is dirty but flag is set."""
50
+ runner = CliRunner()
51
+
52
+ with tempfile.TemporaryDirectory() as tmpdir:
53
+ # Create CODEOWNERS file
54
+ codeowners_path = Path(tmpdir) / "CODEOWNERS"
55
+ codeowners_path.write_text("*.py @python-team\n")
56
+
57
+ # Create baseline with an entry that now has coverage
58
+ baseline_path = Path(tmpdir) / "baseline.txt"
59
+ baseline_path.write_text("covered.py\n")
60
+
61
+ # Create config
62
+ config_path = Path(tmpdir) / ".codeowners-config.yml"
63
+ config_path.write_text(
64
+ f"codeowners_path: {codeowners_path}\n"
65
+ f"baseline_path: {baseline_path}\n"
66
+ f"exclusions: []\n"
67
+ )
68
+
69
+ # Mock get_repository_files to return covered files
70
+ with patch(
71
+ "codeowners_coverage.checker.CoverageChecker.get_repository_files",
72
+ return_value=["covered.py"],
73
+ ):
74
+ result = runner.invoke(
75
+ cli, ["check", "--config", str(config_path), "--allow-dirty-baseline"]
76
+ )
77
+
78
+ # Should exit with code 0 (success despite dirty baseline)
79
+ assert result.exit_code == 0
80
+ assert "baseline entries can be removed" in result.output
81
+
82
+
83
+ def test_check_clean_baseline_with_flag_exits_0() -> None:
84
+ """Test that check exits with code 0 when baseline is clean (with or without flag)."""
85
+ runner = CliRunner()
86
+
87
+ with tempfile.TemporaryDirectory() as tmpdir:
88
+ # Create CODEOWNERS file
89
+ codeowners_path = Path(tmpdir) / "CODEOWNERS"
90
+ codeowners_path.write_text("*.py @python-team\n")
91
+
92
+ # Create empty baseline
93
+ baseline_path = Path(tmpdir) / "baseline.txt"
94
+ baseline_path.write_text("")
95
+
96
+ # Create config
97
+ config_path = Path(tmpdir) / ".codeowners-config.yml"
98
+ config_path.write_text(
99
+ f"codeowners_path: {codeowners_path}\n"
100
+ f"baseline_path: {baseline_path}\n"
101
+ f"exclusions: []\n"
102
+ )
103
+
104
+ # Mock get_repository_files to return covered files
105
+ with patch(
106
+ "codeowners_coverage.checker.CoverageChecker.get_repository_files",
107
+ return_value=["covered.py"],
108
+ ):
109
+ result = runner.invoke(
110
+ cli, ["check", "--config", str(config_path), "--allow-dirty-baseline"]
111
+ )
112
+
113
+ # Should exit with code 0
114
+ assert result.exit_code == 0
115
+ assert "baseline entries can be removed" not in result.output
116
+
117
+
118
+ def test_check_json_output_includes_unused_baseline_entries() -> None:
119
+ """Test that JSON output includes unused_baseline_entries field."""
120
+ runner = CliRunner()
121
+
122
+ with tempfile.TemporaryDirectory() as tmpdir:
123
+ # Create CODEOWNERS file
124
+ codeowners_path = Path(tmpdir) / "CODEOWNERS"
125
+ codeowners_path.write_text("*.py @python-team\n")
126
+
127
+ # Create baseline with entries that now have coverage
128
+ baseline_path = Path(tmpdir) / "baseline.txt"
129
+ baseline_path.write_text("covered1.py\ncovered2.py\n")
130
+
131
+ # Create config
132
+ config_path = Path(tmpdir) / ".codeowners-config.yml"
133
+ config_path.write_text(
134
+ f"codeowners_path: {codeowners_path}\n"
135
+ f"baseline_path: {baseline_path}\n"
136
+ f"exclusions: []\n"
137
+ )
138
+
139
+ # Mock get_repository_files to return covered files
140
+ with patch(
141
+ "codeowners_coverage.checker.CoverageChecker.get_repository_files",
142
+ return_value=["covered1.py", "covered2.py"],
143
+ ):
144
+ result = runner.invoke(
145
+ cli, ["check", "--config", str(config_path), "--json"]
146
+ )
147
+
148
+ # Parse JSON output
149
+ output = json.loads(result.output)
150
+
151
+ # Should include unused_baseline_entries
152
+ assert "unused_baseline_entries" in output
153
+ assert "covered1.py" in output["unused_baseline_entries"]
154
+ assert "covered2.py" in output["unused_baseline_entries"]
155
+
156
+
157
+ def test_check_new_uncovered_takes_precedence_over_dirty_baseline() -> None:
158
+ """Test that new uncovered files cause exit 1 even with --allow-dirty-baseline."""
159
+ runner = CliRunner()
160
+
161
+ with tempfile.TemporaryDirectory() as tmpdir:
162
+ # Create CODEOWNERS file
163
+ codeowners_path = Path(tmpdir) / "CODEOWNERS"
164
+ codeowners_path.write_text("*.py @python-team\n")
165
+
166
+ # Create baseline with an entry that now has coverage
167
+ baseline_path = Path(tmpdir) / "baseline.txt"
168
+ baseline_path.write_text("covered.py\n")
169
+
170
+ # Create config
171
+ config_path = Path(tmpdir) / ".codeowners-config.yml"
172
+ config_path.write_text(
173
+ f"codeowners_path: {codeowners_path}\n"
174
+ f"baseline_path: {baseline_path}\n"
175
+ f"exclusions: []\n"
176
+ )
177
+
178
+ # Mock get_repository_files to return covered + new uncovered file
179
+ with patch(
180
+ "codeowners_coverage.checker.CoverageChecker.get_repository_files",
181
+ return_value=["covered.py", "new_uncovered.txt"],
182
+ ):
183
+ result = runner.invoke(
184
+ cli, ["check", "--config", str(config_path), "--allow-dirty-baseline"]
185
+ )
186
+
187
+ # Should exit with code 1 (new uncovered files)
188
+ assert result.exit_code == 1
189
+ assert "new_uncovered.txt" in result.output
190
+
191
+
192
+ def test_check_json_output_with_clean_baseline() -> None:
193
+ """Test that JSON output includes empty unused_baseline_entries when baseline is clean."""
194
+ runner = CliRunner()
195
+
196
+ with tempfile.TemporaryDirectory() as tmpdir:
197
+ # Create CODEOWNERS file
198
+ codeowners_path = Path(tmpdir) / "CODEOWNERS"
199
+ codeowners_path.write_text("*.py @python-team\n")
200
+
201
+ # Create baseline with entries still uncovered
202
+ baseline_path = Path(tmpdir) / "baseline.txt"
203
+ baseline_path.write_text("still_uncovered.txt\n")
204
+
205
+ # Create config
206
+ config_path = Path(tmpdir) / ".codeowners-config.yml"
207
+ config_path.write_text(
208
+ f"codeowners_path: {codeowners_path}\n"
209
+ f"baseline_path: {baseline_path}\n"
210
+ f"exclusions: []\n"
211
+ )
212
+
213
+ # Mock get_repository_files to return baseline file (still uncovered)
214
+ with patch(
215
+ "codeowners_coverage.checker.CoverageChecker.get_repository_files",
216
+ return_value=["covered.py", "still_uncovered.txt"],
217
+ ):
218
+ result = runner.invoke(
219
+ cli, ["check", "--config", str(config_path), "--json"]
220
+ )
221
+
222
+ # Parse JSON output
223
+ output = json.loads(result.output)
224
+
225
+ # Should include empty unused_baseline_entries
226
+ assert "unused_baseline_entries" in output
227
+ assert output["unused_baseline_entries"] == []
228
+ assert result.exit_code == 0