Skip to content

Commit e39a802

Browse files
Merge --json/--xml/--lcov into --format=, keeping them as aliases
All four now share dest='format' in the same mutex group -- --json, --xml, and --lcov become store_const shortcuts for --format=json etc, so existing scripts keep working unchanged. Verified empirically that both argparse's mutex conflict detection and _detect_explicit_args' CLI-precedence tracking work correctly across differently-shaped actions sharing a dest, with no changes needed to either mechanism. The config surface becomes just 'format' (validated against the same choices) instead of three separate booleans -- the test oracle confirms this naturally, since all four CLI actions sharing one dest collapse to a single derived key. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 3a09c77 commit e39a802

3 files changed

Lines changed: 88 additions & 16 deletions

File tree

src/slipcover/__main__.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ def merge_files(args, base_path):
128128

129129
try:
130130
with args.out.open("w", encoding='utf-8') as jf:
131-
if args.xml:
131+
if args.format == 'xml':
132132
sc.print_xml(merged, source_paths=[str(base_path)], with_branches=args.branch,
133133
xml_package_depth=args.xml_package_depth, outfile=jf)
134-
elif args.lcov:
134+
elif args.format == 'lcov':
135135
sc.print_lcov(merged, with_branches=args.branch,
136136
test_name=args.lcov_test_name, comments=args.lcov_comments,
137137
outfile=jf)
@@ -199,14 +199,19 @@ def build_parser():
199199
ap = argparse.ArgumentParser(prog='SlipCover')
200200
ap.add_argument('--branch', action='store_true', help="measure both branch and line coverage")
201201
fmt = ap.add_mutually_exclusive_group()
202-
fmt.add_argument('--json', action='store_true', help="select JSON output")
202+
fmt.add_argument('--format', choices=['text', 'json', 'xml', 'lcov'], default='text',
203+
help="select output format")
204+
fmt.add_argument('--json', dest='format', action='store_const', const='json',
205+
help="select JSON output (shortcut for --format=json)")
203206
ap.add_argument('--pretty-print', action='store_true', help="pretty-print JSON output")
204-
fmt.add_argument('--xml', action='store_true', help="select XML output")
207+
fmt.add_argument('--xml', dest='format', action='store_const', const='xml',
208+
help="select XML output (shortcut for --format=xml)")
205209
ap.add_argument('--xml-package-depth', type=int, default=99, help=(
206210
"Controls which directories are identified as packages in the report. "
207211
"Directories deeper than this depth are not reported as packages. "
208212
"The default is that all directories are reported as packages."))
209-
fmt.add_argument('--lcov', action='store_true', help="select LCOV output")
213+
fmt.add_argument('--lcov', dest='format', action='store_const', const='lcov',
214+
help="select LCOV output (shortcut for --format=lcov)")
210215
ap.add_argument('--lcov-test-name', type=str, help="test name for LCOV TN: entries")
211216
ap.add_argument('--lcov-comment', action='append', dest='lcov_comments', help="add comment lines at the beginning of LCOV output (can be used multiple times)")
212217
ap.add_argument('--out', type=Path, help="specify output file name")
@@ -323,12 +328,12 @@ def sci_atexit():
323328
global output_tmpfile
324329

325330
def printit(coverage, outfile):
326-
if args.json:
331+
if args.format == 'json':
327332
print(json.dumps(coverage, indent=(4 if args.pretty_print else None)), file=outfile)
328-
elif args.xml:
333+
elif args.format == 'xml':
329334
sc.print_xml(coverage, source_paths=[str(base_path)], with_branches=args.branch,
330335
xml_package_depth=args.xml_package_depth, outfile=outfile)
331-
elif args.lcov:
336+
elif args.format == 'lcov':
332337
sc.print_lcov(coverage, with_branches=args.branch,
333338
test_name=args.lcov_test_name, comments=args.lcov_comments,
334339
outfile=outfile)

src/slipcover/config.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,7 @@ def derive_configurable_keys(ap):
112112
# part of the stable, user-facing config surface.
113113
_BOOL_KEYS = {
114114
"branch",
115-
"json",
116115
"pretty-print",
117-
"xml",
118-
"lcov",
119116
"immediate",
120117
"skip-covered",
121118
}
@@ -127,8 +124,16 @@ def _coerce_comments(value):
127124
return [str(v) for v in value]
128125

129126

127+
def _coerce_format(value):
128+
choices = ("text", "json", "xml", "lcov")
129+
if value not in choices:
130+
raise ValueError(f"must be one of {choices}, got {value!r}")
131+
return value
132+
133+
130134
# Keys that take a value
131135
_VALUE_KEYS = {
136+
"format": _coerce_format,
132137
"out": Path,
133138
"source": str,
134139
"omit": str,

tests/test_config.py

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import argparse
2+
import json
23
import subprocess
34
import sys
45
from pathlib import Path
@@ -163,8 +164,8 @@ def test_config_keys_match_cli_flags():
163164

164165
def _make_args(**kwargs):
165166
defaults = dict(
166-
branch=False, json=False, pretty_print=False, xml=False,
167-
xml_package_depth=99, lcov=False, lcov_test_name=None, lcov_comments=None,
167+
branch=False, format='text', pretty_print=False,
168+
xml_package_depth=99, lcov_test_name=None, lcov_comments=None,
168169
out=None, source=None, omit=None,
169170
immediate=False, skip_covered=False, fail_under=0,
170171
threshold=50, missing_width=80, silent=False, dis=False,
@@ -176,8 +177,8 @@ def _make_args(**kwargs):
176177

177178
def test_apply_config_lcov_keys():
178179
args = _make_args()
179-
apply_config({"lcov": True, "lcov-test-name": "Suite", "lcov-comments": ["a", "b"]}, args)
180-
assert args.lcov is True
180+
apply_config({"format": "lcov", "lcov-test-name": "Suite", "lcov-comments": ["a", "b"]}, args)
181+
assert args.format == "lcov"
181182
assert args.lcov_test_name == "Suite"
182183
assert args.lcov_comments == ["a", "b"]
183184

@@ -188,6 +189,22 @@ def test_apply_config_lcov_comments_scalar_becomes_list():
188189
assert args.lcov_comments == ["just one"]
189190

190191

192+
def test_apply_config_format_bad_value_raises():
193+
args = _make_args()
194+
with pytest.raises(ValueError, match="must be one of"):
195+
apply_config({"format": "bogus"}, args)
196+
197+
198+
@pytest.mark.parametrize("key", ["json", "xml", "lcov"])
199+
def test_apply_config_old_boolean_format_keys_now_unknown(key):
200+
"""json/xml/lcov as individual boolean config keys are no longer
201+
recognized -- format = "..." replaces them (see build_parser()).
202+
"""
203+
args = _make_args()
204+
with pytest.warns(UserWarning, match="Unknown"):
205+
apply_config({key: True}, args)
206+
207+
191208
def test_apply_config_sets_values():
192209
args = _make_args()
193210
apply_config({"branch": True, "fail-under": 85.5, "source": "src"}, args)
@@ -341,7 +358,7 @@ def test_cli_lcov_config_applied(tmp_path, monkeypatch):
341358
monkeypatch.chdir(tmp_path)
342359
(tmp_path / "pyproject.toml").write_text(
343360
'[tool.slipcover]\n'
344-
'lcov = true\n'
361+
'format = "lcov"\n'
345362
'lcov-test-name = "MySuite"\n'
346363
'lcov-comments = ["hello", "world"]\n'
347364
)
@@ -382,3 +399,48 @@ def test_cli_json_alone_still_works(tmp_path, monkeypatch):
382399
assert p.returncode == 0
383400
assert 'Traceback' not in p.stderr
384401

402+
403+
def test_cli_format_json_equivalent_to_json_flag(tmp_path, monkeypatch):
404+
"""--format=json is the primary spelling; --json is a shortcut alias
405+
for it (dest='format' is shared) -- both must produce identical output.
406+
"""
407+
monkeypatch.chdir(tmp_path)
408+
(tmp_path / "script.py").write_text("x = 1\n")
409+
410+
p_flag = subprocess.run([sys.executable, '-m', 'slipcover', '--json', 'script.py'],
411+
capture_output=True, text=True)
412+
p_format = subprocess.run([sys.executable, '-m', 'slipcover', '--format=json', 'script.py'],
413+
capture_output=True, text=True)
414+
415+
assert p_flag.returncode == 0 == p_format.returncode
416+
417+
flag_out = json.loads(p_flag.stdout)
418+
format_out = json.loads(p_format.stdout)
419+
del flag_out['meta']['timestamp'], format_out['meta']['timestamp']
420+
assert flag_out == format_out
421+
422+
423+
def test_cli_format_and_alias_together_is_an_error(tmp_path, monkeypatch):
424+
"""--format=json and --xml are different actions sharing the same
425+
dest -- mixing the new and old spellings must still be rejected.
426+
"""
427+
monkeypatch.chdir(tmp_path)
428+
(tmp_path / "script.py").write_text("x = 1\n")
429+
430+
p = subprocess.run([sys.executable, '-m', 'slipcover', '--format=json', '--xml', 'script.py'],
431+
capture_output=True, text=True)
432+
433+
assert p.returncode != 0
434+
assert 'Traceback' not in p.stderr
435+
436+
437+
def test_cli_format_bad_choice_is_a_clean_error(tmp_path, monkeypatch):
438+
monkeypatch.chdir(tmp_path)
439+
(tmp_path / "script.py").write_text("x = 1\n")
440+
441+
p = subprocess.run([sys.executable, '-m', 'slipcover', '--format=bogus', 'script.py'],
442+
capture_output=True, text=True)
443+
444+
assert p.returncode != 0
445+
assert 'Traceback' not in p.stderr
446+

0 commit comments

Comments
 (0)