From ab1e83fc2a3a994f3b0ef1d08dbc8481ee69f0a6 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 19 Sep 2024 13:23:16 -0400 Subject: [PATCH] Added settable called save_scripted_commands which determines whether to save commands run in scripts and pyscripts to history. --- CHANGELOG.md | 2 ++ cmd2/cmd2.py | 40 ++++++++++++++++++++-------- cmd2/py_bridge.py | 17 +++++++++--- docs/conf.py | 13 +++------ docs/features/builtin_commands.rst | 29 ++++++++++---------- docs/features/initialization.rst | 18 ++++++++----- docs/features/multiline_commands.rst | 2 +- docs/features/settings.rst | 14 ++++++++++ tests/conftest.py | 29 ++++++++++---------- tests/test_cmd2.py | 21 +++++++++++++++ tests/test_run_pyscript.py | 21 +++++++++++++++ tests/transcripts/regex_set.txt | 31 ++++++++++----------- 12 files changed, 162 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d98b14970..56696a10d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ add output to the operating system clipboard * Updated unit tests to be Python 3.12 compliant. * Fall back to bz2 compression of history file when lzma is not installed. + * Added settable called `save_scripted_commands` which determines whether to save commands + run in scripts and pyscripts to history. * Deletions (potentially breaking changes) * Removed `apply_style` from `Cmd.pwarning()`. diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f162ef87e..a3625e1d6 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -322,6 +322,7 @@ def __init__( self.editor = Cmd.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.save_scripted_commands = True # Save commands run in scripts and pyscripts to history self.timing = False # Prints elapsed time for each command # The maximum number of CompletionItems to display during tab completion. If the number of completion @@ -1084,12 +1085,7 @@ def allow_style_type(value: str) -> ansi.AllowStyle: ) self.add_settable( - Settable( - 'always_show_hint', - bool, - 'Display tab completion hint even when completion suggestions print', - self, - ) + Settable('always_show_hint', bool, 'Display tab completion hint even when completion suggestions print', self) ) self.add_settable(Settable('debug', bool, "Show full traceback on exception", self)) self.add_settable(Settable('echo', bool, "Echo command issued into output", self)) @@ -1099,6 +1095,9 @@ def allow_style_type(value: str) -> ansi.AllowStyle: Settable('max_completion_items', int, "Maximum number of CompletionItems to display during tab completion", self) ) self.add_settable(Settable('quiet', bool, "Don't print nonessential feedback", self)) + self.add_settable( + Settable('save_scripted_commands', bool, 'Save commands run in scripts and pyscripts to history', self) + ) self.add_settable(Settable('timing', bool, "Report execution times", self)) # ----- Methods related to presenting output to the user ----- @@ -4459,7 +4458,8 @@ def py_quit() -> None: PyBridge, ) - py_bridge = PyBridge(self) + add_to_history = self.save_scripted_commands if pyscript else True + py_bridge = PyBridge(self, add_to_history=add_to_history) saved_sys_path = None if self.in_pyscript(): @@ -4955,7 +4955,13 @@ def _persist_history(self) -> None: except OSError as ex: self.perror(f"Cannot write persistent history file '{self.persistent_history_file}': {ex}") - def _generate_transcript(self, history: Union[List[HistoryItem], List[str]], transcript_file: str) -> None: + def _generate_transcript( + self, + history: Union[List[HistoryItem], List[str]], + transcript_file: str, + *, + add_to_history: bool = True, + ) -> None: """Generate a transcript file from a given history of commands""" self.last_result = False @@ -5005,7 +5011,11 @@ def _generate_transcript(self, history: Union[List[HistoryItem], List[str]], tra # then run the command and let the output go into our buffer try: - stop = self.onecmd_plus_hooks(history_item, raise_keyboard_interrupt=True) + stop = self.onecmd_plus_hooks( + history_item, + add_to_history=add_to_history, + raise_keyboard_interrupt=True, + ) except KeyboardInterrupt as ex: self.perror(ex) stop = True @@ -5149,9 +5159,17 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]: if args.transcript: # self.last_resort will be set by _generate_transcript() - self._generate_transcript(script_commands, os.path.expanduser(args.transcript)) + self._generate_transcript( + script_commands, + os.path.expanduser(args.transcript), + add_to_history=self.save_scripted_commands, + ) else: - stop = self.runcmds_plus_hooks(script_commands, stop_on_keyboard_interrupt=True) + stop = self.runcmds_plus_hooks( + script_commands, + add_to_history=self.save_scripted_commands, + stop_on_keyboard_interrupt=True, + ) self.last_result = True return stop diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py index ab7b40014..7873f9dcf 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -83,10 +83,17 @@ def __bool__(self) -> bool: class PyBridge: - """Provides a Python API wrapper for application commands.""" + """ + Provides a Python API wrapper for application commands. + + :param cmd2_app: app being controlled by this PyBridge. + :param add_to_history: If True, then add all commands run by this PyBridge to history. + Defaults to True. + """ - def __init__(self, cmd2_app: 'cmd2.Cmd') -> None: + def __init__(self, cmd2_app: 'cmd2.Cmd', *, add_to_history: bool = True) -> None: self._cmd2_app = cmd2_app + self._add_to_history = add_to_history self.cmd_echo = False # Tells if any of the commands run via __call__ returned True for stop @@ -126,7 +133,11 @@ def __call__(self, command: str, *, echo: Optional[bool] = None) -> CommandResul self._cmd2_app.stdout = cast(TextIO, copy_cmd_stdout) with redirect_stdout(cast(IO[str], copy_cmd_stdout)): with redirect_stderr(cast(IO[str], copy_stderr)): - stop = self._cmd2_app.onecmd_plus_hooks(command, py_bridge_call=True) + stop = self._cmd2_app.onecmd_plus_hooks( + command, + add_to_history=self._add_to_history, + py_bridge_call=True, + ) finally: with self._cmd2_app.sigint_protection: self._cmd2_app.stdout = cast(IO[str], copy_cmd_stdout.inner_stream) diff --git a/docs/conf.py b/docs/conf.py index ff64e04ba..ea0dc297b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -174,16 +174,9 @@ # Ignore nitpicky warnings from autodoc which are occurring for very new versions of Sphinx and autodoc # They seem to be happening because autodoc is now trying to add hyperlinks to docs for typehint classes nitpick_ignore = [ - ('py:class', 'Callable[[None], None]'), - ('py:class', 'cmd2.cmd2.Cmd'), - ('py:class', 'cmd2.parsing.Statement'), - ('py:class', 'IO'), - ('py:class', 'None'), - ('py:class', 'Optional[Callable[[...], argparse.Namespace]]'), - ('py:class', 'TextIO'), - ('py:class', 'Union[None, Iterable, Callable]'), + ('py:class', 'cmd2.decorators.CommandParent'), + ('py:obj', 'cmd2.decorators.CommandParent'), ('py:class', 'argparse._SubParsersAction'), ('py:class', 'cmd2.utils._T'), - ('py:class', 'StdSim'), - ('py:class', 'frame'), + ('py:class', 'types.FrameType'), ] diff --git a/docs/features/builtin_commands.rst b/docs/features/builtin_commands.rst index eb14e9abc..5f6ca321b 100644 --- a/docs/features/builtin_commands.rst +++ b/docs/features/builtin_commands.rst @@ -103,20 +103,21 @@ within a running application: .. code-block:: text (Cmd) set - Name Value Description - ================================================================================================================== - allow_style Terminal Allow ANSI text style sequences in output (valid values: - Always, Never, Terminal) - always_show_hint False Display tab completion hint even when completion suggestions - print - debug True Show full traceback on exception - echo False Echo command issued into output - editor vi Program used by 'edit' - feedback_to_output False Include nonessentials in '|', '>' results - max_completion_items 50 Maximum number of CompletionItems to display during tab - completion - quiet False Don't print nonessential feedback - timing False Report execution times + Name Value Description + ==================================================================================================================== + allow_style Terminal Allow ANSI text style sequences in output (valid values: + Always, Never, Terminal) + always_show_hint False Display tab completion hint even when completion suggestions + print + debug True Show full traceback on exception + echo False Echo command issued into output + editor vi Program used by 'edit' + feedback_to_output False Include nonessentials in '|', '>' results + max_completion_items 50 Maximum number of CompletionItems to display during tab + completion + quiet False Don't print nonessential feedback + save_scripted_commands True Save commands run in scripts and pyscripts to history + timing False Report execution times Any of these user-settable parameters can be set while running your app with diff --git a/docs/features/initialization.rst b/docs/features/initialization.rst index 06967a464..e4b852de8 100644 --- a/docs/features/initialization.rst +++ b/docs/features/initialization.rst @@ -104,17 +104,19 @@ Public instance attributes Here are instance attributes of ``cmd2.Cmd`` which developers might wish override: +- **always_show_hint**: if ``True``, display tab completion hint even when + completion suggestions print (Default: ``False``) - **broken_pipe_warning**: if non-empty, this string will be displayed if a broken pipe error occurs - **continuation_prompt**: used for multiline commands on 2nd+ line of input -- **debug**: if ``True`` show full stack trace on error (Default: ``False``) +- **debug**: if ``True``, show full stack trace on error (Default: ``False``) - **default_category**: if any command has been categorized, then all other commands that haven't been categorized will display under this section in the help output. - **default_error**: the error that prints when a non-existent command is run - **default_sort_key**: the default key for sorting string results. Its default value performs a case-insensitive alphabetical sort. -- **default_to_shell**: if ``True`` attempt to run unrecognized commands as +- **default_to_shell**: if ``True``, attempt to run unrecognized commands as shell commands (Default: ``False``) - **disabled_commands**: commands that have been disabled from use. This is to support commands that are only available during specific states of the @@ -130,7 +132,7 @@ override: - **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 +- **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 @@ -139,8 +141,6 @@ override: 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. -- **self_in_py**: if ``True`` allow access to your application in *py* - command via ``self`` (Default: ``False``) - **macros**: dictionary of macro names and their values - **max_completion_items**: max number of CompletionItems to display during tab completion (Default: 50) @@ -154,9 +154,13 @@ override: - **py_locals**: dictionary that defines specific variables/functions available in Python shells and scripts (provides more fine-grained control than making everything available with **self_in_py**) -- **quiet**: if ``True`` then completely suppress nonessential output (Default: +- **quiet**: if ``True``, then completely suppress nonessential output (Default: ``False``) +- **save_scripted_commands**: if ``True``, save commands run in scripts and + pyscripts to history (Default: ``True``) +- **self_in_py**: if ``True``, allow access to your application in *py* + command via ``self`` (Default: ``False``) - **settable**: dictionary that controls which of these instance attributes are settable at runtime using the *set* command -- **timing**: if ``True`` display execution time for each command (Default: +- **timing**: if ``True``, display execution time for each command (Default: ``False``) diff --git a/docs/features/multiline_commands.rst b/docs/features/multiline_commands.rst index 6e0a45399..a5ef26d8f 100644 --- a/docs/features/multiline_commands.rst +++ b/docs/features/multiline_commands.rst @@ -33,4 +33,4 @@ user to type in a SQL command, which can often span lines and which are terminated with a semicolon. We estimate that less than 5 percent of ``cmd2`` applications use this feature. -But it is here for those uses cases where it provides value. +But it is here for those use cases where it provides value. diff --git a/docs/features/settings.rst b/docs/features/settings.rst index 0be462925..cb470d29e 100644 --- a/docs/features/settings.rst +++ b/docs/features/settings.rst @@ -47,6 +47,13 @@ This setting can be one of three values: - ``Always`` - ANSI escape sequences are always passed through to the output +always_show_hint +~~~~~~~~~~~~~~~~ + +If ``True``, display tab completion hint even when completion suggestions print. +The default value of this setting is ``False``. + + debug ~~~~~ @@ -106,6 +113,13 @@ suppressed. If ``False``, the :ref:`features/settings:feedback_to_output` setting controls where the output is sent. +save_scripted_commands +~~~~~~~~~~~~~~~~~~~~~~ + +If ``True``, save commands run in scripts and pyscripts to history. The default +value of this setting is ``True``. + + timing ~~~~~~ diff --git a/tests/conftest.py b/tests/conftest.py index dca3f70b7..855068942 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -98,20 +98,21 @@ def verify_help_text( # Output from the set command SET_TXT = ( - "Name Value Description \n" - "==================================================================================================================\n" - "allow_style Terminal Allow ANSI text style sequences in output (valid values: \n" - " Always, Never, Terminal) \n" - "always_show_hint False Display tab completion hint even when completion suggestions\n" - " print \n" - "debug False Show full traceback on exception \n" - "echo False Echo command issued into output \n" - "editor vim Program used by 'edit' \n" - "feedback_to_output False Include nonessentials in '|', '>' results \n" - "max_completion_items 50 Maximum number of CompletionItems to display during tab \n" - " completion \n" - "quiet False Don't print nonessential feedback \n" - "timing False Report execution times \n" + "Name Value Description \n" + "====================================================================================================================\n" + "allow_style Terminal Allow ANSI text style sequences in output (valid values: \n" + " Always, Never, Terminal) \n" + "always_show_hint False Display tab completion hint even when completion suggestions\n" + " print \n" + "debug False Show full traceback on exception \n" + "echo False Echo command issued into output \n" + "editor vim Program used by 'edit' \n" + "feedback_to_output False Include nonessentials in '|', '>' results \n" + "max_completion_items 50 Maximum number of CompletionItems to display during tab \n" + " completion \n" + "quiet False Don't print nonessential feedback \n" + "save_scripted_commands True Save commands run in scripts and pyscripts to history \n" + "timing False Report execution times \n" ) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index e179239b8..8f6c027fc 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -442,6 +442,27 @@ def test_run_script_with_utf8_file(base_app, request): assert script_err == manual_err +def test_run_script_save_scripted_commands(base_app, request): + test_dir = os.path.dirname(request.module.__file__) + filename = os.path.join(test_dir, 'scripts', 'help.txt') + command = f'run_script {filename}' + + # Save scripted commands + base_app.save_scripted_commands = True + base_app.history.clear() + run_cmd(base_app, command) + assert len(base_app.history) == 2 + assert base_app.history.get(1).raw == command + assert base_app.history.get(2).raw == 'help -v' + + # Do not save scripted commands + base_app.save_scripted_commands = False + base_app.history.clear() + run_cmd(base_app, command) + assert len(base_app.history) == 1 + assert base_app.history.get(1).raw == command + + def test_run_script_nested_run_scripts(base_app, request): # Verify that running a script with nested run_script commands works correctly, # and runs the nested script commands in the correct order. diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py index 16ea4a39c..dd37a1e64 100644 --- a/tests/test_run_pyscript.py +++ b/tests/test_run_pyscript.py @@ -107,6 +107,27 @@ def test_run_pyscript_help(base_app, request): assert out1 and out1 == out2 +def test_run_pyscript_save_scripted_commands(base_app, request): + test_dir = os.path.dirname(request.module.__file__) + python_script = os.path.join(test_dir, 'pyscript', 'help.py') + command = f'run_pyscript {python_script}' + + # Save scripted commands + base_app.save_scripted_commands = True + base_app.history.clear() + run_cmd(base_app, command) + assert len(base_app.history) == 2 + assert base_app.history.get(1).raw == command + assert base_app.history.get(2).raw == 'help' + + # Do not save scripted commands + base_app.save_scripted_commands = False + base_app.history.clear() + run_cmd(base_app, command) + assert len(base_app.history) == 1 + assert base_app.history.get(1).raw == command + + def test_run_pyscript_dir(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'pyscript_dir.py') diff --git a/tests/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt index c2a0f0915..994c098f1 100644 --- a/tests/transcripts/regex_set.txt +++ b/tests/transcripts/regex_set.txt @@ -10,18 +10,19 @@ now: 'Terminal' editor - was: '/.*/' now: 'vim' (Cmd) set -Name Value Description/ +/ -================================================================================================================== -allow_style Terminal Allow ANSI text style sequences in output (valid values:/ +/ - Always, Never, Terminal)/ +/ -always_show_hint False Display tab completion hint even when completion suggestions - print/ +/ -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 '|', '>' results/ +/ -max_completion_items 50 Maximum number of CompletionItems to display during tab/ +/ - completion/ +/ -maxrepeats 3 Max number of `--repeat`s allowed/ +/ -quiet False Don't print nonessential feedback/ +/ -timing False Report execution times/ +/ +Name Value Description/ +/ +==================================================================================================================== +allow_style Terminal Allow ANSI text style sequences in output (valid values:/ +/ + Always, Never, Terminal)/ +/ +always_show_hint False Display tab completion hint even when completion suggestions + print/ +/ +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 '|', '>' results/ +/ +max_completion_items 50 Maximum number of CompletionItems to display during tab/ +/ + completion/ +/ +maxrepeats 3 Max number of `--repeat`s allowed/ +/ +quiet False Don't print nonessential feedback/ +/ +save_scripted_commands True Save commands run in scripts and pyscripts to history/ +/ +timing False Report execution times/ +/