Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 4.3.1 (TBD)

- Bug Fixes
- Fixed `@with_annotated` command `--help` output bypassing the application's output stream,
leaving redirected help files empty and preventing help from being captured by embedded apps.

## 4.3.0 (October 13, 2026)

- Stabilized features
Expand Down
11 changes: 6 additions & 5 deletions cmd2/annotated.py
Original file line number Diff line number Diff line change
Expand Up @@ -3171,11 +3171,12 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> _CommandReturn:
namespace = ns_provider(provider_self if provider_self is not None else cmd2_app)

try:
if with_unknown_args:
ns, unknown = arg_parser.parse_known_args(parsed_arglist, namespace)
else:
ns = arg_parser.parse_args(parsed_arglist, namespace)
unknown = None
with arg_parser.output_to(cmd2_app.stdout):
if with_unknown_args:
ns, unknown = arg_parser.parse_known_args(parsed_arglist, namespace)
else:
ns = arg_parser.parse_args(parsed_arglist, namespace)
unknown = None
except SystemExit as exc:
raise Cmd2ArgparseError from exc

Expand Down
35 changes: 35 additions & 0 deletions tests/test_annotated.py
Original file line number Diff line number Diff line change
Expand Up @@ -2467,6 +2467,41 @@ def test_error_produces_stderr(self, app) -> None:
_out, err = run_cmd(app, "greet")
assert any("error" in line.lower() or "usage" in line.lower() for line in err)

@pytest.mark.parametrize(
("app_fixture", "command"),
[
("app", "greet"),
("app", "flex"),
("subcmd_app", "manage"),
("subcmd_app", "manage add"),
("subcmd_app", "manage member add"),
],
)
@pytest.mark.parametrize("destination", ["stdout", ">", ">>"])
def test_help_respects_output_destination(self, request, capsys, tmp_path, app_fixture, command, destination) -> None:
"""Help follows app.stdout, including file redirection, without leaking to sys.stdout."""
app = request.getfixturevalue(app_fixture)
original_stdout = app.stdout
help_file = tmp_path / "help.txt"
line = f"{command} --help"
if destination != "stdout":
help_file.write_text("existing\n", encoding="utf-8")
line += f' {destination} "{help_file}"'

out, err = run_cmd(app, line)

assert app.stdout is original_stdout
assert not err
if destination == "stdout":
help_text = "\n".join(out)
else:
assert not out
help_text = help_file.read_text(encoding="utf-8")
assert help_text.startswith("existing\n") == (destination == ">>")
assert f"Usage: {command}" in help_text
assert "--help" in help_text
assert capsys.readouterr() == ("", "")

def test_no_args_raises_type_error(self, app) -> None:
with pytest.raises(TypeError, match="Expected arguments"):
app.do_greet()
Expand Down
Loading