diff --git a/CHANGELOG.md b/CHANGELOG.md index 6837cbd3e..6b353cc00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,8 @@ prompt is displayed. treated as command output and sent to `self.stdout`, allowing them to be captured. - Verbose help table descriptions are no longer generated from help function output. The system now relies exclusively on command function docstrings. + - Removed `feedback_to_output` settable and changed `cmd2.Cmd.pfeedback` to always print to + `self.stdout` - Enhancements - New `cmd2.Cmd` parameters - **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These @@ -163,6 +165,11 @@ prompt is displayed. - `Cmd2Style.COMPLETION_MENU_META` - Style for "meta" information shown alongside a completion - `Cmd2Style.COMPLETION_MENU_META_CURRENT`- Style for meta info of current item + - Updated `set` command to consolidate its confirmation output into a single, colorized line. + The confirmation now uses `pfeedback()`, allowing it to be silenced when the `quiet` settable + is enabled. + - `alias` and `macro` subcommands for `create` and `delete` now output their non-essential + success case output using `pfeedback` ## 3.5.1 (April 24, 2026) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 8c60d04d5..672d877f4 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -474,7 +474,6 @@ def __init__( self.debug = False self.echo = False self.editor = self.DEFAULT_EDITOR - self.feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing) self.quiet = False # Do not suppress nonessential output self.scripts_add_to_history = True # Scripts and pyscripts add commands to history self.timing = False # Prints elapsed time for each command @@ -1370,7 +1369,6 @@ def allow_style_type(value: str) -> ru.AllowStyle: self.add_settable(Settable("debug", bool, "Show full traceback on exception", self)) self.add_settable(Settable("echo", bool, "Echo command issued into output", self)) self.add_settable(Settable("editor", str, "Program used by 'edit'", self)) - self.add_settable(Settable("feedback_to_output", bool, "Include nonessentials in '|' and '>' results", self)) self.add_settable( Settable( "max_completion_table_items", @@ -1754,40 +1752,23 @@ def pfeedback( rich_print_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: - """Print nonessential feedback. - - The output can be silenced with the `quiet` setting and its inclusion in redirected output - is controlled by the `feedback_to_output` setting. + """Print nonessential feedback where the output can be silenced with the `quiet` setting. For details on the parameters, refer to the `print_to` method documentation. """ if not self.quiet: - if self.feedback_to_output: - self.poutput( - *objects, - sep=sep, - end=end, - style=style, - soft_wrap=soft_wrap, - justify=justify, - emoji=emoji, - markup=markup, - highlight=highlight, - rich_print_kwargs=rich_print_kwargs, - ) - else: - self.perror( - *objects, - sep=sep, - end=end, - style=style, - soft_wrap=soft_wrap, - justify=justify, - emoji=emoji, - markup=markup, - highlight=highlight, - rich_print_kwargs=rich_print_kwargs, - ) + self.poutput( + *objects, + sep=sep, + end=end, + style=style, + soft_wrap=soft_wrap, + justify=justify, + emoji=emoji, + markup=markup, + highlight=highlight, + rich_print_kwargs=rich_print_kwargs, + ) def ppaged( self, @@ -2992,7 +2973,7 @@ def onecmd_plus_hooks( stop = self.postcmd(stop, statement) if self.timing: - self.pfeedback(f"Elapsed: {datetime.datetime.now(tz=datetime.timezone.utc) - timestart}") + self.perror(f"Elapsed: {datetime.datetime.now(tz=datetime.timezone.utc) - timestart}", style=None) finally: # Get sigint protection while we restore stuff with self.sigint_protection: @@ -3862,7 +3843,7 @@ def _alias_create(self, args: argparse.Namespace) -> None: # Set the alias result = "overwritten" if args.name in self.aliases else "created" - self.poutput(f"Alias '{args.name}' {result}") + self.pfeedback(f"Alias '{args.name}' {result}") self.aliases[args.name] = value self.last_result = True @@ -3891,7 +3872,7 @@ def _alias_delete(self, args: argparse.Namespace) -> None: if args.all: self.aliases.clear() - self.poutput("All aliases deleted") + self.pfeedback("All aliases deleted") elif not args.names: self.perror("Either --all or alias name(s) must be specified") self.last_result = False @@ -3899,7 +3880,7 @@ def _alias_delete(self, args: argparse.Namespace) -> None: for cur_name in utils.remove_duplicates(args.names): if cur_name in self.aliases: del self.aliases[cur_name] - self.poutput(f"Alias '{cur_name}' deleted") + self.pfeedback(f"Alias '{cur_name}' deleted") else: self.perror(f"Alias '{cur_name}' does not exist") @@ -4028,7 +4009,7 @@ def _build_macro_create_parser(cls) -> Cmd2ArgumentParser: "When the macro is called, the provided arguments are resolved and the assembled command is run. For example:", "\n\n", (" my_macro beef broccoli", Cmd2Style.COMMAND_LINE), - (" ───> ", Style(bold=True)), + (" ─> ", Style(bold=True)), ("make_dinner --meat beef --veggie broccoli", Cmd2Style.COMMAND_LINE), ) macro_create_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=macro_create_description) @@ -4150,7 +4131,7 @@ def _macro_create(self, args: argparse.Namespace) -> None: # Set the macro result = "overwritten" if args.name in self.macros else "created" - self.poutput(f"Macro '{args.name}' {result}") + self.pfeedback(f"Macro '{args.name}' {result}") self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, args=macro_args) self.last_result = True @@ -4179,7 +4160,7 @@ def _macro_delete(self, args: argparse.Namespace) -> None: if args.all: self.macros.clear() - self.poutput("All macros deleted") + self.pfeedback("All macros deleted") elif not args.names: self.perror("Either --all or macro name(s) must be specified") self.last_result = False @@ -4187,7 +4168,7 @@ def _macro_delete(self, args: argparse.Namespace) -> None: for cur_name in utils.remove_duplicates(args.names): if cur_name in self.macros: del self.macros[cur_name] - self.poutput(f"Macro '{cur_name}' deleted") + self.pfeedback(f"Macro '{cur_name}' deleted") else: self.perror(f"Macro '{cur_name}' does not exist") @@ -4737,12 +4718,17 @@ def do_set(self, args: argparse.Namespace) -> None: if args.value: # Try to update the settable's value try: - orig_value = settable.value settable.value = su.strip_quotes(args.value) except ValueError as ex: self.perror(f"Error setting {args.param}: {ex}") else: - self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {settable.value!r}") + # Create the feedback message using Rich Text for color + feedback_msg = Text.assemble( + f"{args.param} ─> ", + (f"{settable.value!r}", "green"), + ) + self.pfeedback(feedback_msg) + self.last_result = True return diff --git a/docs/features/builtin_commands.md b/docs/features/builtin_commands.md index ee92ec201..8957a1f0f 100644 --- a/docs/features/builtin_commands.md +++ b/docs/features/builtin_commands.md @@ -83,7 +83,6 @@ application: debug False Show full traceback on exception echo False Echo command issued into output editor vim Program used by 'edit' - feedback_to_output False Include nonessentials in '|' and '>' results max_column_completion_results 7 Maximum number of completion results to display in a single column max_completion_table_items 50 Maximum number of completion results allowed for a completion table to appear quiet False Don't print nonessential feedback diff --git a/docs/features/generating_output.md b/docs/features/generating_output.md index b93b7c2b9..e69c28bbd 100644 --- a/docs/features/generating_output.md +++ b/docs/features/generating_output.md @@ -84,15 +84,15 @@ output. You may have the need to display information to the user which is not intended to be part of the generated output. This could be debugging information or status information about the progress of -long running commands. It's not output, it's not error messages, it's feedback. If you use the +long running commands. It's not output, it's not error messages, it's status. If you use the [Timing](./settings.md#timing) setting, the output of how long it took the command to run will be -output as feedback. You can use the [pfeedback][cmd2.Cmd.pfeedback] method to produce this type of -output, and several [Settings](./settings.md) control how it is handled. +output as to `stderr` without any styling. You can use the [perror][cmd2.Cmd.perror] method to +produce this type of output by passing it `style=None`. If the [quiet](./settings.md#quiet) setting is `True`, then calling `cmd2.Cmd.pfeedback` produces no -output. If [quiet](./settings.md#quiet) is `False`, the -[feedback_to_output](./settings.md#feedback_to_output) setting is consulted to determine whether to -send the output to `stdout` or `stderr`. +output. If [quiet](./settings.md#quiet) is `False`,the `pfeedback` method sends output to `stdout`. +Hence, `pfeedback` is useful for non-essential output that you want the ability to silence when +`quiet` is `True`. ## Exceptions diff --git a/docs/features/initialization.md b/docs/features/initialization.md index c1ad6e2c0..f15cd613a 100644 --- a/docs/features/initialization.md +++ b/docs/features/initialization.md @@ -43,7 +43,6 @@ Here are instance attributes of `cmd2.Cmd` which developers might wish to overri - **editor**: text editor program to use with _edit_ command (e.g. `vim`) - **exclude_from_history**: commands to exclude from the _history_ command - **exit_code**: this determines the value returned by `cmdloop()` when exiting the application -- **feedback_to_output**: if `True`, send nonessential output to stdout, if `False` send them to stderr (Default: `False`) - **help_error**: the error that prints when no help information can be found - **hidden_commands**: commands to exclude from the help menu and tab completion - **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. diff --git a/docs/features/settings.md b/docs/features/settings.md index e0eefe25a..568356686 100644 --- a/docs/features/settings.md +++ b/docs/features/settings.md @@ -54,15 +54,6 @@ the prompt. Similar to the `EDITOR` shell variable, this setting contains the name of the program which should be run by the [edit](./builtin_commands.md#edit) command. -### feedback_to_output - -Controls whether feedback generated with the `cmd2.Cmd.pfeedback` method is sent to `self.stdout` or -`sys.stderr`. If `False` the output will be sent to `sys.stderr` - -If `True` the output is sent to `stdout` (which is often the screen but may be -[redirected](./redirection.md#output-redirection-and-pipes)). The feedback output will be mixed in -with and indistinguishable from output generated with `cmd2.Cmd.poutput`. - ### max_completion_table_items The maximum number of items to display in a completion table. A completion table is a special kind @@ -74,8 +65,8 @@ appear. ### quiet -If `True`, output generated by calling `cmd2.Cmd.pfeedback` is suppressed. If `False`, the -[feedback_to_output](#feedback_to_output) setting controls where the output is sent. +If `True`, output generated by calling `cmd2.Cmd.pfeedback` is suppressed. If `False`, the output is +sent to `stdout`. ### scripts_add_to_history diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 8a47a079f..8fc0230a2 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -160,16 +160,17 @@ def test_base_set(base_app) -> None: def test_set(base_app) -> None: - out, _err = run_cmd(base_app, "set quiet True") - expected = normalize( - """ -quiet - was: False -now: True -""" - ) - assert out == expected + out, err = run_cmd(base_app, "set quiet True") + assert not out + assert base_app.last_result is True + + # Test quiet respect + out, err = run_cmd(base_app, "set timing False") + assert not out + assert not err assert base_app.last_result is True + # Show one settable (this always goes to out) line_found = False out, _err = run_cmd(base_app, "set quiet") for line in out: @@ -180,6 +181,7 @@ def test_set(base_app) -> None: assert line_found assert len(base_app.last_result) == 1 assert base_app.last_result["quiet"] is True + base_app.quiet = False 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: # Verify the results assert expected == ru.ALLOW_STYLE if is_valid: - assert not err assert out + assert not err def test_set_traceback_show_locals(base_app: cmd2.Cmd) -> None: @@ -275,9 +277,11 @@ def test_set_with_choices(base_app) -> None: base_app.add_settable(fake_settable) # Try a valid choice - _out, err = run_cmd(base_app, f"set fake {fake_choices[1]}") + out, err = run_cmd(base_app, f"set fake {fake_choices[1]}") assert base_app.last_result is True assert not err + assert out[0].startswith("fake") + assert out[0].endswith(f"─> {fake_choices[1]!r}") # Try an invalid choice _out, err = run_cmd(base_app, "set fake bad_value") @@ -301,15 +305,10 @@ def onchange_app(): def test_set_onchange_hook(onchange_app) -> None: - out, _err = run_cmd(onchange_app, "set quiet True") - expected = normalize( - """ -You changed quiet -quiet - was: False -now: True -""" - ) - assert out == expected + out, err = run_cmd(onchange_app, "set quiet True") + assert out == ["You changed quiet"] + # quiet: False -> True is not shown because quiet is now True + assert not err assert onchange_app.last_result is True @@ -727,8 +726,7 @@ def test_output_redirection_to_too_long_filename(redirection_app) -> None: assert "Failed to redirect" in err[0] -def test_feedback_to_output_true(redirection_app) -> None: - redirection_app.feedback_to_output = True +def test_feedback(redirection_app) -> None: f, filename = tempfile.mkstemp(prefix="cmd2_test", suffix=".txt") os.close(f) @@ -741,22 +739,6 @@ def test_feedback_to_output_true(redirection_app) -> None: os.remove(filename) -def test_feedback_to_output_false(redirection_app) -> None: - redirection_app.feedback_to_output = False - f, filename = tempfile.mkstemp(prefix="feedback_to_output", suffix=".txt") - os.close(f) - - try: - _out, err = run_cmd(redirection_app, f"print_feedback > {filename}") - - with open(filename) as f: - content = f.read().splitlines() - assert not content - assert "feedback" in err - finally: - os.remove(filename) - - def test_disallow_redirection(redirection_app: RedirectionApp, capsys: pytest.CaptureFixture[str]) -> None: # Set allow_redirection to False redirection_app.allow_redirection = False @@ -873,14 +855,9 @@ def test_allow_clipboard(base_app) -> None: def test_base_timing(base_app) -> None: - base_app.feedback_to_output = False out, err = run_cmd(base_app, "set timing True") - expected = normalize( - """timing - was: False -now: True -""" - ) - assert out == expected + assert out[0].startswith("timing") + assert out[0].endswith("─> True") if sys.platform == "win32": assert err[0].startswith("Elapsed: 0:00:00") @@ -899,13 +876,9 @@ def test_base_debug(base_app) -> None: # Set debug true out, err = run_cmd(base_app, "set debug True") - expected = normalize( - """ -debug - was: False -now: True -""" - ) - assert out == expected + assert not err + assert out[0].startswith("debug") + assert out[0].endswith("─> True") # Verify that we now see the exception traceback out, err = run_cmd(base_app, "edit") diff --git a/tests/test_commandset.py b/tests/test_commandset.py index 72d31e52e..66b1d1885 100644 --- a/tests/test_commandset.py +++ b/tests/test_commandset.py @@ -1103,11 +1103,9 @@ def __init__(self) -> None: # change the value and verify the value changed out, err = run_cmd(app, "set arbitrary_value 10") - expected = """ -arbitrary_value - was: 5 -now: 10 -""" - assert out == normalize(expected) + assert not err + assert out[0].startswith("arbitrary_value") + assert out[0].endswith("─> 10") out, err = run_cmd(app, "set arbitrary_value") any("arbitrary_value" in line and "10" in line for line in out)