Skip to content

Commit f9362a3

Browse files
committed
test: add coverage for interspersed options and command groups
1 parent f704cbf commit f9362a3

9 files changed

Lines changed: 373 additions & 1 deletion

tests/argparse/layout/test_help_rendering_regressions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,24 @@ def mul(self) -> None:
411411
assert "{add,mul}" in help_text
412412

413413

414+
def test_standard_layout_class_root_help_renders_one_commands_section() -> None:
415+
class Calculator:
416+
def add(self, a: float, b: float) -> float:
417+
return a + b
418+
419+
def mul(self, a: float, b: float) -> float:
420+
return a * b
421+
422+
parser = Argparser(help_layout=StandardLayout(), sys_exit_enabled=False, print_result=False)
423+
parser.add_command(Calculator)
424+
425+
help_text = parser.build_parser().format_help()
426+
427+
assert len(re.findall(r"^commands:$", help_text, re.MULTILINE)) == 1
428+
assert len(re.findall(r"^\s+add(?:\s|$)", help_text, re.MULTILINE)) == 1
429+
assert len(re.findall(r"^\s+mul(?:\s|$)", help_text, re.MULTILINE)) == 1
430+
431+
414432
def test_interfacy_layout_usage_subcommand_choices_exclude_aliases() -> None:
415433
ops = CommandGroup("ops")
416434
ops.add_command(lambda: None, name="cache_prune", aliases=("prune",))

tests/argparse/test_types/test_primitives.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@
1212

1313

1414
class TestPrimitives:
15+
@pytest.mark.parametrize("parser", ["argparse_req_pos"], indirect=True)
16+
def test_bad_int_error_uses_user_facing_type_name(
17+
self, parser: InterfacyParser, capsys: pytest.CaptureFixture[str]
18+
):
19+
"""Invalid typed values should show argparse-style type names."""
20+
21+
def greet(name: str, times: int = 1) -> str:
22+
return name * times
23+
24+
parser.add_command(greet)
25+
26+
with pytest.raises(SystemExit) as excinfo:
27+
parser.parse_args(["Ada", "--times", "bad"])
28+
29+
assert excinfo.value.code == 2
30+
captured = capsys.readouterr()
31+
assert "argument -t/--times: invalid int value: 'bad'" in captured.err
32+
assert "parser[int]" not in captured.err
33+
1534
@pytest.mark.parametrize("parser", ["argparse_req_pos", "argparse_kw_only"], indirect=True)
1635
def test_str_required(self, parser: InterfacyParser):
1736
"""Verify that a required string argument is correctly parsed from positional or flag input."""

tests/click/test_click_parity.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def test_interfacy_click_group_help_position_aligns_command_rows() -> None:
173173
help="Disable the per-job duration limit.",
174174
)
175175
)
176+
176177
group.interfacy_help_position = 42
177178
group.interfacy_help_position_explicit = True
178179

tests/common/test_command_group.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import sys
2+
13
import pytest
24

35
from interfacy import CommandGroup
@@ -197,6 +199,22 @@ def test_single_group_with_one_function(self, parser: InterfacyParser):
197199
parser.add_command(cli)
198200
assert parser.run(args=["cli", "greet", "Ada"]) == "Hello, Ada!"
199201

202+
@pytest.mark.parametrize("parser", ["argparse_req_pos", "click_req_pos"], indirect=True)
203+
def test_group_function_command_accepts_pipe_targets(
204+
self,
205+
parser: InterfacyParser,
206+
mocker,
207+
monkeypatch: pytest.MonkeyPatch,
208+
):
209+
"""Grouped function commands can receive stdin through per-command pipe config."""
210+
cli = CommandGroup("cli")
211+
cli.add_command(greet, pipe_targets="name")
212+
parser.add_command(cli)
213+
monkeypatch.setattr(sys.stdin, "isatty", lambda: False)
214+
mocker.patch("interfacy.core.read_piped", return_value="Ada")
215+
216+
assert parser.run(args=["cli", "greet"]) == "Hello, Ada!"
217+
200218
@pytest.mark.parametrize("parser", ["argparse_req_pos", "click_req_pos"], indirect=True)
201219
def test_single_group_with_multiple_functions(self, parser: InterfacyParser):
202220
"""Verify single group with multiple functions works."""
@@ -259,6 +277,30 @@ def test_class_init_args_become_group_flags(self, parser: InterfacyParser):
259277
result = parser.run(args=["workspace", "container", "--format", "json", "run", "nginx"])
260278
assert result == "Running nginx with format json"
261279

280+
@pytest.mark.parametrize("parser", ["argparse_req_pos", "click_req_pos"], indirect=True)
281+
def test_class_pipe_targets_apply_to_initializer(
282+
self,
283+
parser: InterfacyParser,
284+
mocker,
285+
monkeypatch: pytest.MonkeyPatch,
286+
):
287+
"""Grouped class pipe targets can satisfy initializer parameters."""
288+
289+
class PrefixTool:
290+
def __init__(self, prefix: str) -> None:
291+
self.prefix = prefix
292+
293+
def run(self, value: str = "x") -> str:
294+
return f"{self.prefix}{value}"
295+
296+
workspace = CommandGroup("workspace")
297+
workspace.add_command(PrefixTool, name="tool", pipe_targets="prefix")
298+
parser.add_command(workspace)
299+
monkeypatch.setattr(sys.stdin, "isatty", lambda: False)
300+
mocker.patch("interfacy.core.read_piped", return_value="pre-")
301+
302+
assert parser.run(args=["workspace", "tool", "run"]) == "pre-x"
303+
262304
@pytest.mark.parametrize("parser", ["argparse_req_pos", "click_req_pos"], indirect=True)
263305
def test_class_multiple_methods(self, parser: InterfacyParser):
264306
"""Verify multiple class methods are accessible."""

tests/common/test_help_command_groups.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
from interfacy import CommandGroup
6-
from interfacy.appearance.layouts import ArgparseLayout
6+
from interfacy.appearance.layouts import ArgparseLayout, StandardLayout
77
from interfacy.argparse_backend import Argparser
88
from interfacy.exceptions import ConfigurationError
99

@@ -24,6 +24,14 @@ def cmd_init() -> None:
2424
"""Initialize a repository."""
2525

2626

27+
def cmd_push() -> None:
28+
"""Push changes."""
29+
30+
31+
def cmd_pull() -> None:
32+
"""Pull changes."""
33+
34+
2735
@pytest.mark.parametrize("parser", ["argparse_req_pos", "click_req_pos"], indirect=True)
2836
def test_root_help_groups_commands_before_ungrouped(parser, capsys) -> None:
2937
parser.add_command(cmd_status, name="status")
@@ -53,6 +61,48 @@ def test_root_help_groups_commands_before_ungrouped(parser, capsys) -> None:
5361
assert init_idx < status_idx
5462

5563

64+
@pytest.mark.parametrize("parser", ["argparse_req_pos", "click_req_pos"], indirect=True)
65+
def test_help_group_command_indent_is_configurable(parser, capsys) -> None:
66+
parser.apply_setup(help_layout=StandardLayout(command_indent=5))
67+
parser.add_command(cmd_clone, name="clone", help_group="setup")
68+
parser.add_command(cmd_status, name="status")
69+
70+
result = parser.run(args=["--help"])
71+
assert isinstance(result, SystemExit)
72+
assert result.code == 0
73+
74+
captured = capsys.readouterr()
75+
combined = _strip_ansi(captured.out + captured.err)
76+
lines = combined.splitlines()
77+
78+
assert any(line.startswith(" clone") for line in lines)
79+
80+
81+
@pytest.mark.parametrize("parser", ["argparse_req_pos", "click_req_pos"], indirect=True)
82+
def test_help_group_spacing_is_configurable(parser, capsys) -> None:
83+
parser.apply_setup(help_layout=StandardLayout(command_group_spacing=0))
84+
parser.add_command(cmd_clone, name="clone", help_group="setup")
85+
parser.add_command(cmd_push, name="push", help_group="sync")
86+
parser.add_command(cmd_pull, name="pull")
87+
88+
result = parser.run(args=["--help"])
89+
assert isinstance(result, SystemExit)
90+
assert result.code == 0
91+
92+
captured = capsys.readouterr()
93+
combined = _strip_ansi(captured.out + captured.err)
94+
lines = combined.splitlines()
95+
clone_idx = next(idx for idx, line in enumerate(lines) if line.strip().startswith("clone"))
96+
sync_idx = lines.index("sync")
97+
push_idx = next(idx for idx, line in enumerate(lines) if line.strip().startswith("push"))
98+
pull_idx = next(idx for idx, line in enumerate(lines) if line.strip().startswith("pull"))
99+
100+
assert lines[clone_idx + 1] == "sync"
101+
assert lines[push_idx + 1].lstrip().startswith("pull")
102+
assert sync_idx == clone_idx + 1
103+
assert pull_idx == push_idx + 1
104+
105+
56106
@pytest.mark.parametrize("backend", ["argparse", "click"])
57107
def test_nested_help_groups_render_with_adaptive_layout(backend: str, capsys) -> None:
58108
if backend == "click":

tests/common/test_interrupt.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def slow_task(seconds: int = 10) -> str:
1212
"""A slow task that can be interrupted."""
1313
for _ in range(seconds):
1414
time.sleep(0.1)
15+
1516
return "Done"
1617

1718

0 commit comments

Comments
 (0)