Skip to content

Commit 87666e9

Browse files
authored
Merge branch 'main' into rich-argparse
2 parents c88c34a + 5e2b4ac commit 87666e9

8 files changed

Lines changed: 70 additions & 117 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ prompt is displayed.
108108
treated as command output and sent to `self.stdout`, allowing them to be captured.
109109
- Verbose help table descriptions are no longer generated from help function output. The system
110110
now relies exclusively on command function docstrings.
111+
- Removed `feedback_to_output` settable and changed `cmd2.Cmd.pfeedback` to always print to
112+
`self.stdout`
111113
- Enhancements
112114
- New `cmd2.Cmd` parameters
113115
- **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These
@@ -163,6 +165,11 @@ prompt is displayed.
163165
- `Cmd2Style.COMPLETION_MENU_META` - Style for "meta" information shown alongside a
164166
completion
165167
- `Cmd2Style.COMPLETION_MENU_META_CURRENT`- Style for meta info of current item
168+
- Updated `set` command to consolidate its confirmation output into a single, colorized line.
169+
The confirmation now uses `pfeedback()`, allowing it to be silenced when the `quiet` settable
170+
is enabled.
171+
- `alias` and `macro` subcommands for `create` and `delete` now output their non-essential
172+
success case output using `pfeedback`
166173

167174
## 3.5.1 (April 24, 2026)
168175

cmd2/cmd2.py

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,6 @@ def __init__(
474474
self.debug = False
475475
self.echo = False
476476
self.editor = self.DEFAULT_EDITOR
477-
self.feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing)
478477
self.quiet = False # Do not suppress nonessential output
479478
self.scripts_add_to_history = True # Scripts and pyscripts add commands to history
480479
self.timing = False # Prints elapsed time for each command
@@ -1370,7 +1369,6 @@ def allow_style_type(value: str) -> ru.AllowStyle:
13701369
self.add_settable(Settable("debug", bool, "Show full traceback on exception", self))
13711370
self.add_settable(Settable("echo", bool, "Echo command issued into output", self))
13721371
self.add_settable(Settable("editor", str, "Program used by 'edit'", self))
1373-
self.add_settable(Settable("feedback_to_output", bool, "Include nonessentials in '|' and '>' results", self))
13741372
self.add_settable(
13751373
Settable(
13761374
"max_completion_table_items",
@@ -1754,40 +1752,23 @@ def pfeedback(
17541752
rich_print_kwargs: Mapping[str, Any] | None = None,
17551753
**kwargs: Any, # noqa: ARG002
17561754
) -> None:
1757-
"""Print nonessential feedback.
1758-
1759-
The output can be silenced with the `quiet` setting and its inclusion in redirected output
1760-
is controlled by the `feedback_to_output` setting.
1755+
"""Print nonessential feedback where the output can be silenced with the `quiet` setting.
17611756
17621757
For details on the parameters, refer to the `print_to` method documentation.
17631758
"""
17641759
if not self.quiet:
1765-
if self.feedback_to_output:
1766-
self.poutput(
1767-
*objects,
1768-
sep=sep,
1769-
end=end,
1770-
style=style,
1771-
soft_wrap=soft_wrap,
1772-
justify=justify,
1773-
emoji=emoji,
1774-
markup=markup,
1775-
highlight=highlight,
1776-
rich_print_kwargs=rich_print_kwargs,
1777-
)
1778-
else:
1779-
self.perror(
1780-
*objects,
1781-
sep=sep,
1782-
end=end,
1783-
style=style,
1784-
soft_wrap=soft_wrap,
1785-
justify=justify,
1786-
emoji=emoji,
1787-
markup=markup,
1788-
highlight=highlight,
1789-
rich_print_kwargs=rich_print_kwargs,
1790-
)
1760+
self.poutput(
1761+
*objects,
1762+
sep=sep,
1763+
end=end,
1764+
style=style,
1765+
soft_wrap=soft_wrap,
1766+
justify=justify,
1767+
emoji=emoji,
1768+
markup=markup,
1769+
highlight=highlight,
1770+
rich_print_kwargs=rich_print_kwargs,
1771+
)
17911772

17921773
def ppaged(
17931774
self,
@@ -2992,7 +2973,7 @@ def onecmd_plus_hooks(
29922973
stop = self.postcmd(stop, statement)
29932974

29942975
if self.timing:
2995-
self.pfeedback(f"Elapsed: {datetime.datetime.now(tz=datetime.timezone.utc) - timestart}")
2976+
self.perror(f"Elapsed: {datetime.datetime.now(tz=datetime.timezone.utc) - timestart}", style=None)
29962977
finally:
29972978
# Get sigint protection while we restore stuff
29982979
with self.sigint_protection:
@@ -3862,7 +3843,7 @@ def _alias_create(self, args: argparse.Namespace) -> None:
38623843

38633844
# Set the alias
38643845
result = "overwritten" if args.name in self.aliases else "created"
3865-
self.poutput(f"Alias '{args.name}' {result}")
3846+
self.pfeedback(f"Alias '{args.name}' {result}")
38663847

38673848
self.aliases[args.name] = value
38683849
self.last_result = True
@@ -3891,15 +3872,15 @@ def _alias_delete(self, args: argparse.Namespace) -> None:
38913872

38923873
if args.all:
38933874
self.aliases.clear()
3894-
self.poutput("All aliases deleted")
3875+
self.pfeedback("All aliases deleted")
38953876
elif not args.names:
38963877
self.perror("Either --all or alias name(s) must be specified")
38973878
self.last_result = False
38983879
else:
38993880
for cur_name in utils.remove_duplicates(args.names):
39003881
if cur_name in self.aliases:
39013882
del self.aliases[cur_name]
3902-
self.poutput(f"Alias '{cur_name}' deleted")
3883+
self.pfeedback(f"Alias '{cur_name}' deleted")
39033884
else:
39043885
self.perror(f"Alias '{cur_name}' does not exist")
39053886

@@ -4028,7 +4009,7 @@ def _build_macro_create_parser(cls) -> Cmd2ArgumentParser:
40284009
"When the macro is called, the provided arguments are resolved and the assembled command is run. For example:",
40294010
"\n\n",
40304011
(" my_macro beef broccoli", Cmd2Style.COMMAND_LINE),
4031-
(" ───> ", Style(bold=True)),
4012+
(" ─> ", Style(bold=True)),
40324013
("make_dinner --meat beef --veggie broccoli", Cmd2Style.COMMAND_LINE),
40334014
)
40344015
macro_create_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=macro_create_description)
@@ -4150,7 +4131,7 @@ def _macro_create(self, args: argparse.Namespace) -> None:
41504131

41514132
# Set the macro
41524133
result = "overwritten" if args.name in self.macros else "created"
4153-
self.poutput(f"Macro '{args.name}' {result}")
4134+
self.pfeedback(f"Macro '{args.name}' {result}")
41544135

41554136
self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, args=macro_args)
41564137
self.last_result = True
@@ -4179,15 +4160,15 @@ def _macro_delete(self, args: argparse.Namespace) -> None:
41794160

41804161
if args.all:
41814162
self.macros.clear()
4182-
self.poutput("All macros deleted")
4163+
self.pfeedback("All macros deleted")
41834164
elif not args.names:
41844165
self.perror("Either --all or macro name(s) must be specified")
41854166
self.last_result = False
41864167
else:
41874168
for cur_name in utils.remove_duplicates(args.names):
41884169
if cur_name in self.macros:
41894170
del self.macros[cur_name]
4190-
self.poutput(f"Macro '{cur_name}' deleted")
4171+
self.pfeedback(f"Macro '{cur_name}' deleted")
41914172
else:
41924173
self.perror(f"Macro '{cur_name}' does not exist")
41934174

@@ -4737,12 +4718,17 @@ def do_set(self, args: argparse.Namespace) -> None:
47374718
if args.value:
47384719
# Try to update the settable's value
47394720
try:
4740-
orig_value = settable.value
47414721
settable.value = su.strip_quotes(args.value)
47424722
except ValueError as ex:
47434723
self.perror(f"Error setting {args.param}: {ex}")
47444724
else:
4745-
self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {settable.value!r}")
4725+
# Create the feedback message using Rich Text for color
4726+
feedback_msg = Text.assemble(
4727+
f"{args.param} ─> ",
4728+
(f"{settable.value!r}", Cmd2Style.SUCCESS),
4729+
)
4730+
self.pfeedback(feedback_msg)
4731+
47464732
self.last_result = True
47474733
return
47484734

docs/features/builtin_commands.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ application:
8383
debug False Show full traceback on exception
8484
echo False Echo command issued into output
8585
editor vim Program used by 'edit'
86-
feedback_to_output False Include nonessentials in '|' and '>' results
8786
max_column_completion_results 7 Maximum number of completion results to display in a single column
8887
max_completion_table_items 50 Maximum number of completion results allowed for a completion table to appear
8988
quiet False Don't print nonessential feedback

docs/features/generating_output.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ output.
8484

8585
You may have the need to display information to the user which is not intended to be part of the
8686
generated output. This could be debugging information or status information about the progress of
87-
long running commands. It's not output, it's not error messages, it's feedback. If you use the
87+
long running commands. It's not output, it's not error messages, it's status. If you use the
8888
[Timing](./settings.md#timing) setting, the output of how long it took the command to run will be
89-
output as feedback. You can use the [pfeedback][cmd2.Cmd.pfeedback] method to produce this type of
90-
output, and several [Settings](./settings.md) control how it is handled.
89+
output as to `stderr` without any styling. You can use the [perror][cmd2.Cmd.perror] method to
90+
produce this type of output by passing it `style=None`.
9191

9292
If the [quiet](./settings.md#quiet) setting is `True`, then calling `cmd2.Cmd.pfeedback` produces no
93-
output. If [quiet](./settings.md#quiet) is `False`, the
94-
[feedback_to_output](./settings.md#feedback_to_output) setting is consulted to determine whether to
95-
send the output to `stdout` or `stderr`.
93+
output. If [quiet](./settings.md#quiet) is `False`,the `pfeedback` method sends output to `stdout`.
94+
Hence, `pfeedback` is useful for non-essential output that you want the ability to silence when
95+
`quiet` is `True`.
9696

9797
## Exceptions
9898

docs/features/initialization.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ Here are instance attributes of `cmd2.Cmd` which developers might wish to overri
4343
- **editor**: text editor program to use with _edit_ command (e.g. `vim`)
4444
- **exclude_from_history**: commands to exclude from the _history_ command
4545
- **exit_code**: this determines the value returned by `cmdloop()` when exiting the application
46-
- **feedback_to_output**: if `True`, send nonessential output to stdout, if `False` send them to stderr (Default: `False`)
4746
- **help_error**: the error that prints when no help information can be found
4847
- **hidden_commands**: commands to exclude from the help menu and tab completion
4948
- **last_result**: stores results from the last command run to enable usage of results in a Python script or interactive console. Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.

docs/features/settings.md

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,6 @@ the prompt.
5454
Similar to the `EDITOR` shell variable, this setting contains the name of the program which should
5555
be run by the [edit](./builtin_commands.md#edit) command.
5656

57-
### feedback_to_output
58-
59-
Controls whether feedback generated with the `cmd2.Cmd.pfeedback` method is sent to `self.stdout` or
60-
`sys.stderr`. If `False` the output will be sent to `sys.stderr`
61-
62-
If `True` the output is sent to `stdout` (which is often the screen but may be
63-
[redirected](./redirection.md#output-redirection-and-pipes)). The feedback output will be mixed in
64-
with and indistinguishable from output generated with `cmd2.Cmd.poutput`.
65-
6657
### max_completion_table_items
6758

6859
The maximum number of items to display in a completion table. A completion table is a special kind
@@ -74,8 +65,8 @@ appear.
7465

7566
### quiet
7667

77-
If `True`, output generated by calling `cmd2.Cmd.pfeedback` is suppressed. If `False`, the
78-
[feedback_to_output](#feedback_to_output) setting controls where the output is sent.
68+
If `True`, output generated by calling `cmd2.Cmd.pfeedback` is suppressed. If `False`, the output is
69+
sent to `stdout`.
7970

8071
### scripts_add_to_history
8172

tests/test_cmd2.py

Lines changed: 24 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,17 @@ def test_base_set(base_app) -> None:
160160

161161

162162
def test_set(base_app) -> None:
163-
out, _err = run_cmd(base_app, "set quiet True")
164-
expected = normalize(
165-
"""
166-
quiet - was: False
167-
now: True
168-
"""
169-
)
170-
assert out == expected
163+
out, err = run_cmd(base_app, "set quiet True")
164+
assert not out
165+
assert base_app.last_result is True
166+
167+
# Test quiet respect
168+
out, err = run_cmd(base_app, "set timing False")
169+
assert not out
170+
assert not err
171171
assert base_app.last_result is True
172172

173+
# Show one settable (this always goes to out)
173174
line_found = False
174175
out, _err = run_cmd(base_app, "set quiet")
175176
for line in out:
@@ -180,6 +181,7 @@ def test_set(base_app) -> None:
180181
assert line_found
181182
assert len(base_app.last_result) == 1
182183
assert base_app.last_result["quiet"] is True
184+
base_app.quiet = False
183185

184186

185187
def test_set_val_empty(base_app) -> None:
@@ -236,8 +238,8 @@ def test_set_allow_style(base_app, new_val, is_valid, expected) -> None:
236238
# Verify the results
237239
assert expected == ru.ALLOW_STYLE
238240
if is_valid:
239-
assert not err
240241
assert out
242+
assert not err
241243

242244

243245
def test_set_traceback_show_locals(base_app: cmd2.Cmd) -> None:
@@ -275,9 +277,11 @@ def test_set_with_choices(base_app) -> None:
275277
base_app.add_settable(fake_settable)
276278

277279
# Try a valid choice
278-
_out, err = run_cmd(base_app, f"set fake {fake_choices[1]}")
280+
out, err = run_cmd(base_app, f"set fake {fake_choices[1]}")
279281
assert base_app.last_result is True
280282
assert not err
283+
assert out[0].startswith("fake")
284+
assert out[0].endswith(f"─> {fake_choices[1]!r}")
281285

282286
# Try an invalid choice
283287
_out, err = run_cmd(base_app, "set fake bad_value")
@@ -301,15 +305,10 @@ def onchange_app():
301305

302306

303307
def test_set_onchange_hook(onchange_app) -> None:
304-
out, _err = run_cmd(onchange_app, "set quiet True")
305-
expected = normalize(
306-
"""
307-
You changed quiet
308-
quiet - was: False
309-
now: True
310-
"""
311-
)
312-
assert out == expected
308+
out, err = run_cmd(onchange_app, "set quiet True")
309+
assert out == ["You changed quiet"]
310+
# quiet: False -> True is not shown because quiet is now True
311+
assert not err
313312
assert onchange_app.last_result is True
314313

315314

@@ -727,8 +726,7 @@ def test_output_redirection_to_too_long_filename(redirection_app) -> None:
727726
assert "Failed to redirect" in err[0]
728727

729728

730-
def test_feedback_to_output_true(redirection_app) -> None:
731-
redirection_app.feedback_to_output = True
729+
def test_feedback(redirection_app) -> None:
732730
f, filename = tempfile.mkstemp(prefix="cmd2_test", suffix=".txt")
733731
os.close(f)
734732

@@ -741,22 +739,6 @@ def test_feedback_to_output_true(redirection_app) -> None:
741739
os.remove(filename)
742740

743741

744-
def test_feedback_to_output_false(redirection_app) -> None:
745-
redirection_app.feedback_to_output = False
746-
f, filename = tempfile.mkstemp(prefix="feedback_to_output", suffix=".txt")
747-
os.close(f)
748-
749-
try:
750-
_out, err = run_cmd(redirection_app, f"print_feedback > {filename}")
751-
752-
with open(filename) as f:
753-
content = f.read().splitlines()
754-
assert not content
755-
assert "feedback" in err
756-
finally:
757-
os.remove(filename)
758-
759-
760742
def test_disallow_redirection(redirection_app: RedirectionApp, capsys: pytest.CaptureFixture[str]) -> None:
761743
# Set allow_redirection to False
762744
redirection_app.allow_redirection = False
@@ -873,14 +855,9 @@ def test_allow_clipboard(base_app) -> None:
873855

874856

875857
def test_base_timing(base_app) -> None:
876-
base_app.feedback_to_output = False
877858
out, err = run_cmd(base_app, "set timing True")
878-
expected = normalize(
879-
"""timing - was: False
880-
now: True
881-
"""
882-
)
883-
assert out == expected
859+
assert out[0].startswith("timing")
860+
assert out[0].endswith("─> True")
884861

885862
if sys.platform == "win32":
886863
assert err[0].startswith("Elapsed: 0:00:00")
@@ -899,13 +876,9 @@ def test_base_debug(base_app) -> None:
899876

900877
# Set debug true
901878
out, err = run_cmd(base_app, "set debug True")
902-
expected = normalize(
903-
"""
904-
debug - was: False
905-
now: True
906-
"""
907-
)
908-
assert out == expected
879+
assert not err
880+
assert out[0].startswith("debug")
881+
assert out[0].endswith("─> True")
909882

910883
# Verify that we now see the exception traceback
911884
out, err = run_cmd(base_app, "edit")

0 commit comments

Comments
 (0)