diff options
-rw-r--r-- | CHANGELOG.md | 9 | ||||
-rwxr-xr-x | README.md | 8 | ||||
-rw-r--r-- | cmd2/cmd2.py | 231 | ||||
-rw-r--r-- | cmd2/utils.py | 32 | ||||
-rw-r--r-- | docs/features/embedded_python_shells.rst | 98 | ||||
-rw-r--r-- | docs/features/initialization.rst | 2 | ||||
-rwxr-xr-x | examples/arg_decorators.py | 2 | ||||
-rwxr-xr-x | examples/basic.py | 2 | ||||
-rwxr-xr-x | examples/cmd_as_argument.py | 4 | ||||
-rwxr-xr-x | examples/colors.py | 4 | ||||
-rwxr-xr-x | examples/decorator_example.py | 5 | ||||
-rwxr-xr-x | examples/dynamic_commands.py | 2 | ||||
-rwxr-xr-x | examples/hello_cmd2.py | 5 | ||||
-rwxr-xr-x | examples/help_categories.py | 3 | ||||
-rwxr-xr-x | examples/initialization.py | 2 | ||||
-rwxr-xr-x | examples/override_parser.py | 2 | ||||
-rwxr-xr-x | examples/plumbum_colors.py | 4 | ||||
-rwxr-xr-x | examples/python_scripting.py | 4 | ||||
-rw-r--r-- | tests/conftest.py | 2 | ||||
-rw-r--r-- | tests/pyscript/py_locals.py | 5 | ||||
-rw-r--r-- | tests/pyscript/self_in_py.py | 6 | ||||
-rwxr-xr-x | tests/test_cmd2.py | 52 | ||||
-rw-r--r-- | tests/test_run_pyscript.py | 36 |
23 files changed, 231 insertions, 289 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index cde4f34d..582f0940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,11 @@ object that holds the settable attribute. `cmd2.Cmd.settables` is no longer a public dict attribute - it is now a property that aggregates all Settables across all registered CommandSets. * Failed transcript testing now sets self.exit_code to 1 instead of -1. + * Renamed `use_ipython` keyword parameter of `cmd2.Cmd.__init__()` to `include_ipy`. + * `py` command is only enabled if `include_py` parameter is `True`. See Enhancements for a description + of this parameter. + * Removed ability to run Python commands from the command line with `py`. Now `py` takes no arguments + and just opens an interactive Python shell. * Enhancements * Added support for custom tab completion and up-arrow input history to `cmd2.Cmd2.read_input`. See [read_input.py](https://github.com/python-cmd2/cmd2/blob/master/examples/read_input.py) @@ -37,7 +42,9 @@ attribute added to the cmd2 instance itself. * Raising ``SystemExit`` or calling ``sys.exit()`` in a command or hook function will set ``self.exit_code`` to the exit code used in those calls. It will also result in the command loop stopping. - * ipy command now includes all of `self.py_locals` in the IPython environment + * ipy command now includes all of `self.py_locals` in the IPython environment + * Added `include_py` keyword parameter to `cmd2.Cmd.__init__()`. If `False`, then the `py` command will + not be available. Defaults to `False`. `run_pyscript` is not affected by this parameter. ## 1.5.0 (January 31, 2021) * Bug Fixes @@ -26,7 +26,8 @@ Main Features - Pipe command output to shell commands with `|` - Redirect command output to file with `>`, `>>` - Bare `>`, `>>` with no filename send output to paste buffer (clipboard) -- `py` enters interactive Python console (opt-in `ipy` for IPython console) +- Optional `py` command runs interactive Python console which can be used to debug your application +- Optional `ipy` command runs interactive IPython console which can be used to debug your application - Option to display long output using a pager with ``cmd2.Cmd.ppaged()`` - Multi-line commands - Special-character command shortcuts (beyond cmd's `?` and `!`) @@ -249,9 +250,8 @@ class CmdLineApp(cmd2.Cmd): shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) shortcuts.update({'&': 'speak'}) - # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell - super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts) - + super().__init__(multiline_commands=['orate'], shortcuts=shortcuts) + # Make maxrepeats settable at runtime self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command')) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c91fb4db..073e9a2d 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -160,16 +160,6 @@ else: rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters") orig_rl_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value -# Detect whether IPython is installed to determine if the built-in "ipy" command should be included -ipython_available = True -try: - # noinspection PyUnresolvedReferences,PyPackageRequirements - from IPython import ( # type: ignore[import] - start_ipython, - ) -except ImportError: # pragma: no cover - ipython_available = False - class _SavedReadlineSettings: """readline settings that are backed up when switching between readline environments""" @@ -224,7 +214,8 @@ class Cmd(cmd.Cmd): persistent_history_length: int = 1000, startup_script: str = '', silent_startup_script: bool = False, - use_ipython: bool = False, + include_py: bool = False, + include_ipy: bool = False, allow_cli_args: bool = True, transcript_files: Optional[List[str]] = None, allow_redirection: bool = True, @@ -246,7 +237,8 @@ class Cmd(cmd.Cmd): :param startup_script: file path to a script to execute at startup :param silent_startup_script: if ``True``, then the startup script's output will be suppressed. Anything written to stderr will still display. - :param use_ipython: should the "ipy" command be included for an embedded IPython shell + :param include_py: should the "py" command be included for an embedded Python shell + :param include_ipy: should the "ipy" command be included for an embedded IPython shell :param allow_cli_args: if ``True``, then :meth:`cmd2.Cmd.__init__` will process command line arguments as either commands to be run or, if ``-t`` or ``--test`` are given, transcript files to run. This should be @@ -280,12 +272,11 @@ class Cmd(cmd.Cmd): instantiate and register all commands. If False, CommandSets must be manually installed with `register_command_set`. """ - # If use_ipython is False, make sure the ipy command isn't available in this instance - if not use_ipython: - try: - self.do_ipy = None - except AttributeError: - pass + # Check if py or ipy need to be disabled in this instance + if not include_py: + self.do_py: Optional[Callable] = None + if not include_ipy: + self.do_ipy: Optional[Callable] = None # initialize plugin system # needs to be done before we call __init__(0) @@ -3669,11 +3660,11 @@ class Cmd(cmd.Cmd): result = "\n".join('{}: {}'.format(sc[0], sc[1]) for sc in sorted_shortcuts) self.poutput("Shortcuts for other commands:\n{}".format(result)) - eof_parser = DEFAULT_ARGUMENT_PARSER(description="Called when <Ctrl>-D is pressed", epilog=INTERNAL_COMMAND_EPILOG) + eof_parser = DEFAULT_ARGUMENT_PARSER(description="Called when Ctrl-D is pressed", epilog=INTERNAL_COMMAND_EPILOG) @with_argparser(eof_parser) def do_eof(self, _: argparse.Namespace) -> bool: - """Called when <Ctrl>-D is pressed""" + """Called when Ctrl-D is pressed""" # Return True to stop the command loop return True @@ -4005,31 +3996,15 @@ class Cmd(cmd.Cmd): else: sys.modules['readline'] = cmd2_env.readline_module - py_description = ( - "Invoke Python command or shell\n" - "\n" - "Note that, when invoking a command directly from the command line, this shell\n" - "has limited ability to parse Python statements into tokens. In particular,\n" - "there may be problems with whitespace and quotes depending on their placement.\n" - "\n" - "If you see strange parsing behavior, it's best to just open the Python shell\n" - "by providing no arguments to py and run more complex statements there." - ) - - py_parser = DEFAULT_ARGUMENT_PARSER(description=py_description) - py_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to run") - py_parser.add_argument('remainder', nargs=argparse.REMAINDER, help="remainder of command") - - # Preserve quotes since we are passing these strings to Python - @with_argparser(py_parser, preserve_quotes=True) - def do_py(self, args: argparse.Namespace, *, pyscript: Optional[str] = None) -> Optional[bool]: + def _run_python(self, *, pyscript: Optional[str] = None) -> Optional[bool]: """ - Enter an interactive Python shell + Called by do_py() and do_run_pyscript(). + If pyscript is None, then this function runs an interactive Python shell. + Otherwise, it runs the pyscript file. :param args: Namespace of args on the command line - :param pyscript: optional path to a pyscript file to run. This is intended only to be used by run_pyscript - after it sets up sys.argv for the script. If populated, this takes precedence over all - other arguments. (Defaults to None) + :param pyscript: optional path to a pyscript file to run. This is intended only to be used by do_run_pyscript() + after it sets up sys.argv for the script. (Defaults to None) :return: True if running of commands should stop """ @@ -4064,7 +4039,7 @@ class Cmd(cmd.Cmd): if self.self_in_py: local_vars['self'] = self - # Handle case where we were called by run_pyscript + # Handle case where we were called by do_run_pyscript() if pyscript is not None: # Read the script file expanded_filename = os.path.expanduser(pyscript) @@ -4073,7 +4048,7 @@ class Cmd(cmd.Cmd): with open(expanded_filename) as f: py_code_to_run = f.read() except OSError as ex: - self.pexcept("Error reading script file '{}': {}".format(expanded_filename, ex)) + self.perror(f"Error reading script file '{expanded_filename}': {ex}") return local_vars['__name__'] = '__main__' @@ -4087,15 +4062,6 @@ class Cmd(cmd.Cmd): # This is the default name chosen by InteractiveConsole when no locals are passed in local_vars['__name__'] = '__console__' - if args.command: - py_code_to_run = args.command - if args.remainder: - py_code_to_run += ' ' + ' '.join(args.remainder) - - # Set cmd_echo to True so PyBridge statements like: py app('help') - # run at the command line will print their output. - py_bridge.cmd_echo = True - # Create the Python interpreter interp = InteractiveConsole(locals=local_vars) @@ -4112,9 +4078,10 @@ class Cmd(cmd.Cmd): else: cprt = 'Type "help", "copyright", "credits" or "license" for more information.' instructions = ( - 'End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n' - 'Non-Python commands can be issued with: {}("your command")'.format(self.py_bridge_name) + 'Use `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()` to exit.\n' + f'Run CLI commands with: {self.py_bridge_name}("command ...")' ) + banner = f"Python {sys.version} on {sys.platform}\n{cprt}\n\n{instructions}\n" saved_cmd2_env = None @@ -4124,16 +4091,18 @@ class Cmd(cmd.Cmd): with self.sigint_protection: saved_cmd2_env = self._set_up_py_shell_env(interp) - interp.interact(banner="Python {} on {}\n{}\n\n{}\n".format(sys.version, sys.platform, cprt, instructions)) + # Since quit() or exit() raise an EmbeddedConsoleExit, interact() exits before printing + # the exitmsg. Therefore we will not provide it one and print it manually later. + interp.interact(banner=banner, exitmsg='') except BaseException: # We don't care about any exception that happened in the interactive console pass - finally: # Get sigint protection while we restore cmd2 environment settings with self.sigint_protection: if saved_cmd2_env is not None: self._restore_cmd2_env(saved_cmd2_env) + self.poutput("Now exiting Python shell...") finally: with self.sigint_protection: @@ -4143,6 +4112,16 @@ class Cmd(cmd.Cmd): return py_bridge.stop + py_parser = DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell") + + @with_argparser(py_parser) + def do_py(self, _: argparse.Namespace) -> Optional[bool]: + """ + Run an interactive Python shell + :return: True if running of commands should stop + """ + return self._run_python() + run_pyscript_parser = DEFAULT_ARGUMENT_PARSER(description="Run a Python script file inside the console") run_pyscript_parser.add_argument('script_path', help='path to the script file', completer=path_complete) run_pyscript_parser.add_argument( @@ -4162,7 +4141,7 @@ class Cmd(cmd.Cmd): # Add some protection against accidentally running a non-Python file. The happens when users # mix up run_script and run_pyscript. if not args.script_path.endswith('.py'): - self.pwarning("'{}' does not have a .py extension".format(args.script_path)) + self.pwarning(f"'{args.script_path}' does not have a .py extension") selection = self.select('Yes No', 'Continue to try to run it as a Python script? ') if selection != 'Yes': return @@ -4173,28 +4152,28 @@ class Cmd(cmd.Cmd): try: # Overwrite sys.argv to allow the script to take command line arguments sys.argv = [args.script_path] + args.script_arguments - - # noinspection PyTypeChecker - py_return = self.do_py('', pyscript=args.script_path) - + py_return = self._run_python(pyscript=args.script_path) finally: # Restore command line arguments to original state sys.argv = orig_args return py_return - # Only include the do_ipy() method if IPython is available on the system - if ipython_available: # pragma: no cover - ipython_parser = DEFAULT_ARGUMENT_PARSER(description="Enter an interactive IPython shell") + ipython_parser = DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell") - @with_argparser(ipython_parser) - def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: - """ - Enter an interactive IPython shell + # noinspection PyPackageRequirements + @with_argparser(ipython_parser) + def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover + """ + Enter an interactive IPython shell - :return: True if running of commands should stop - """ - # noinspection PyPackageRequirements + :return: True if running of commands should stop + """ + # Detect whether IPython is installed + try: + from IPython import ( + start_ipython, + ) from IPython.terminal.interactiveshell import ( TerminalInteractiveShell, ) @@ -4204,47 +4183,51 @@ class Cmd(cmd.Cmd): from traitlets.config.loader import ( Config as TraitletsConfig, ) + except ImportError: + self.perror("IPython package is not installed") + return - from .py_bridge import ( - PyBridge, - ) + from .py_bridge import ( + PyBridge, + ) - if self.in_pyscript(): - self.perror("Recursively entering interactive Python shells is not allowed") - return + if self.in_pyscript(): + self.perror("Recursively entering interactive Python shells is not allowed") + return - try: - self._in_py = True - py_bridge = PyBridge(self) - - # Make a copy of self.py_locals for the locals dictionary in the IPython environment we are creating. - # This is to prevent ipy from editing it. (e.g. locals().clear()). Only make a shallow copy since - # it's OK for py_locals to contain objects which are editable in ipy. - local_vars = self.py_locals.copy() - local_vars[self.py_bridge_name] = py_bridge - if self.self_in_py: - local_vars['self'] = self - - # Configure IPython - config = TraitletsConfig() - config.InteractiveShell.banner2 = ( - 'Entering an embedded IPython shell. Type quit or <Ctrl>-d to exit.\n' - 'Run Python code from external files with: run filename.py\n' - ) + try: + self._in_py = True + py_bridge = PyBridge(self) - # Start IPython - start_ipython(config=config, argv=[], user_ns=local_vars) + # Make a copy of self.py_locals for the locals dictionary in the IPython environment we are creating. + # This is to prevent ipy from editing it. (e.g. locals().clear()). Only make a shallow copy since + # it's OK for py_locals to contain objects which are editable in ipy. + local_vars = self.py_locals.copy() + local_vars[self.py_bridge_name] = py_bridge + if self.self_in_py: + local_vars['self'] = self - # The IPython application is a singleton and won't be recreated next time - # this function runs. That's a problem since the contents of local_vars - # may need to be changed. Therefore we must destroy all instances of the - # relevant classes. - TerminalIPythonApp.clear_instance() - TerminalInteractiveShell.clear_instance() + # Configure IPython + config = TraitletsConfig() + config.InteractiveShell.banner2 = ( + 'Entering an IPython shell. Type exit, quit, or Ctrl-D to exit.\n' + f'Run CLI commands with: {self.py_bridge_name}("command ...")\n' + ) - return py_bridge.stop - finally: - self._in_py = False + # Start IPython + start_ipython(config=config, argv=[], user_ns=local_vars) + self.poutput("Now exiting IPython shell...") + + # The IPython application is a singleton and won't be recreated next time + # this function runs. That's a problem since the contents of local_vars + # may need to be changed. Therefore we must destroy all instances of the + # relevant classes. + TerminalIPythonApp.clear_instance() + TerminalInteractiveShell.clear_instance() + + return py_bridge.stop + finally: + self._in_py = False history_description = "View, run, edit, save, or clear previously entered commands" @@ -4652,39 +4635,29 @@ class Cmd(cmd.Cmd): """ expanded_path = os.path.abspath(os.path.expanduser(args.script_path)) - # Make sure the path exists and we can access it - if not os.path.exists(expanded_path): - self.perror("'{}' does not exist or cannot be accessed".format(expanded_path)) - return - - # Make sure expanded_path points to a file - if not os.path.isfile(expanded_path): - self.perror("'{}' is not a file".format(expanded_path)) - return - - # An empty file is not an error, so just return - if os.path.getsize(expanded_path) == 0: - return - - # Make sure the file is ASCII or UTF-8 encoded text - if not utils.is_text_file(expanded_path): - self.perror("'{}' is not an ASCII or UTF-8 encoded text file".format(expanded_path)) - return - # Add some protection against accidentally running a Python file. The happens when users # mix up run_script and run_pyscript. if expanded_path.endswith('.py'): - self.pwarning("'{}' appears to be a Python file".format(expanded_path)) + self.pwarning(f"'{expanded_path}' appears to be a Python file") selection = self.select('Yes No', 'Continue to try to run it as a text script? ') if selection != 'Yes': return try: + # An empty file is not an error, so just return + if os.path.getsize(expanded_path) == 0: + return + + # Make sure the file is ASCII or UTF-8 encoded text + if not utils.is_text_file(expanded_path): + self.perror(f"'{expanded_path}' is not an ASCII or UTF-8 encoded text file") + return + # Read all lines of the script with open(expanded_path, encoding='utf-8') as target: script_commands = target.read().splitlines() - except OSError as ex: # pragma: no cover - self.pexcept("Problem accessing script from '{}': {}".format(expanded_path, ex)) + except OSError as ex: + self.perror(f"Problem accessing script from '{expanded_path}': {ex}") return orig_script_dir_count = len(self._script_dir) diff --git a/cmd2/utils.py b/cmd2/utils.py index 2787c079..7e350f96 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -186,38 +186,28 @@ class Settable: def is_text_file(file_path: str) -> bool: - """Returns if a file contains only ASCII or UTF-8 encoded text. + """Returns if a file contains only ASCII or UTF-8 encoded text and isn't empty. :param file_path: path to the file being checked - :return: True if the file is a text file, False if it is binary. + :return: True if the file is a non-empty text file, otherwise False + :raises OSError if file can't be read """ import codecs expanded_path = os.path.abspath(os.path.expanduser(file_path.strip())) valid_text_file = False - # Check if the file is ASCII + # Only need to check for utf-8 compliance since that covers ASCII, too try: - with codecs.open(expanded_path, encoding='ascii', errors='strict') as f: - # Make sure the file has at least one line of text - # noinspection PyUnusedLocal - if sum(1 for line in f) > 0: + with codecs.open(expanded_path, encoding='utf-8', errors='strict') as f: + # Make sure the file has only utf-8 text and is not empty + if sum(1 for _ in f) > 0: valid_text_file = True - except OSError: # pragma: no cover - pass + except OSError: + raise except UnicodeDecodeError: - # The file is not ASCII. Check if it is UTF-8. - try: - with codecs.open(expanded_path, encoding='utf-8', errors='strict') as f: - # Make sure the file has at least one line of text - # noinspection PyUnusedLocal - if sum(1 for line in f) > 0: - valid_text_file = True - except OSError: # pragma: no cover - pass - except UnicodeDecodeError: - # Not UTF-8 - pass + # Not UTF-8 + pass return valid_text_file diff --git a/docs/features/embedded_python_shells.rst b/docs/features/embedded_python_shells.rst index cbedf992..fc04e020 100644 --- a/docs/features/embedded_python_shells.rst +++ b/docs/features/embedded_python_shells.rst @@ -1,11 +1,21 @@ Embedded Python Shells ====================== -The ``py`` command will run its arguments as a Python command. Entered without -arguments, it enters an interactive Python session. The session can call -"back" to your application through the name defined in ``self.pyscript_name`` -(defaults to ``app``). This wrapper provides access to execute commands in -your ``cmd2`` application while maintaining isolation. +Python (optional) +------------------ +If the ``cmd2.Cmd`` class is instantiated with ``include_py=True``, then the +optional ``py`` command will be present and run an interactive Python shell:: + + from cmd2 import Cmd + class App(Cmd): + def __init__(self): + Cmd.__init__(self, include_py=True) + +The Python shell can run CLI commands from you application using the object +named in ``self.pyscript_name`` (defaults to ``app``). This wrapper provides +access to execute commands in your ``cmd2`` application while maintaining +isolation from the full `Cmd` instance. For example, any application command +can be run with ``app("command ...")``. You may optionally enable full access to to your application by setting ``self.self_in_py`` to ``True``. Enabling this flag adds ``self`` to the @@ -17,57 +27,22 @@ in the CLI's environment. Anything in ``self.py_locals`` is always available in the Python environment. -The ``app`` object (or your custom name) provides access to application -commands through raw commands. For example, any application command call be -called with ``app("<command>")``. - -More Python examples: - -:: - - (Cmd) py print("-".join("spelling")) - s-p-e-l-l-i-n-g - (Cmd) py - Python 3.9.0 (default, Nov 11 2020, 21:21:51) - [Clang 12.0.0 (clang-1200.0.32.21)] on darwin - Type "help", "copyright", "credits" or "license" for more information. - - End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`. - Non-Python commands can be issued with: app("your command") - (CmdLineApp) - - End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`. - Non-python commands can be issued with: app("your command") - Run python code from external script files with: run("script.py") - - >>> import os - >>> os.uname() - ('Linux', 'eee', '2.6.31-19-generic', '#56-Ubuntu SMP Thu Jan 28 01:26:53 UTC 2010', 'i686') - >>> app("say --piglatin {os}".format(os=os.uname()[0])) - inuxLay - >>> self.prompt - '(Cmd) ' - >>> self.prompt = 'Python was here > ' - >>> quit() - Python was here > - -The ``py`` command also allows you to run Python scripts via ``py -run('myscript.py')``. This provides a more complicated and more powerful -scripting capability than that provided by the simple text file scripts -discussed in :ref:`features/scripting:Scripting`. Python scripts can include +All of these parameters are also available to Python scripts which run in your +application via the ``run_pyscript`` command: + +- supports tab completion of file system paths +- has the ability to pass command-line arguments to the scripts invoked + +This command provides a more complicated and more powerful scripting capability +than that provided by the simple text file scripts. Python scripts can include conditional control flow logic. See the **python_scripting.py** ``cmd2`` application and the **script_conditional.py** script in the ``examples`` source code directory for an example of how to achieve this in your own applications. +See :ref:`features/scripting:Scripting` for an explanation of both scripting +methods in **cmd2** applications. -Using ``py`` to run scripts directly is considered deprecated. The newer -``run_pyscript`` command is superior for doing this in two primary ways: - -- it supports tab completion of file system paths -- it has the ability to pass command-line arguments to the scripts invoked - -There are no disadvantages to using ``run_pyscript`` as opposed to ``py -run()``. A simple example of using ``run_pyscript`` is shown below along with -the arg_printer_ script:: +A simple example of using ``run_pyscript`` is shown below along with the +arg_printer_ script:: (Cmd) run_pyscript examples/scripts/arg_printer.py foo bar baz Running Python script 'arg_printer.py' which was called with 3 arguments @@ -75,19 +50,6 @@ the arg_printer_ script:: arg 2: 'bar' arg 3: 'baz' -.. note:: - - If you want to be able to pass arguments with spaces to commands, then we - strongly recommend using one of the decorators, such as - ``with_argument_list``. ``cmd2`` will pass your **do_*** methods a list of - arguments in this case. - - When using this decorator, you can then put arguments in quotes like so:: - - $ examples/arg_print.py - (Cmd) lprint foo "bar baz" - lprint was called with the following list of arguments: ['foo', 'bar baz'] - .. _arg_printer: https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/arg_printer.py @@ -96,13 +58,13 @@ IPython (optional) ------------------ **If** IPython_ is installed on the system **and** the ``cmd2.Cmd`` class is -instantiated with ``use_ipython=True``, then the optional ``ipy`` command will -be present:: +instantiated with ``include_ipy=True``, then the optional ``ipy`` command will +run an interactive IPython shell:: from cmd2 import Cmd class App(Cmd): def __init__(self): - Cmd.__init__(self, use_ipython=True) + Cmd.__init__(self, include_ipy=True) The ``ipy`` command enters an interactive IPython_ session. Similar to an interactive Python session, this shell can access your application instance via diff --git a/docs/features/initialization.rst b/docs/features/initialization.rst index d79b3818..ab792c2c 100644 --- a/docs/features/initialization.rst +++ b/docs/features/initialization.rst @@ -26,7 +26,7 @@ capabilities which you may wish to utilize while initializing the app:: def __init__(self): super().__init__(multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', - startup_script='scripts/startup.txt', use_ipython=True) + startup_script='scripts/startup.txt', include_ipy=True) # Prints an intro banner once upon application startup self.intro = style('Welcome to cmd2!', fg=fg.red, bg=bg.white, bold=True) diff --git a/examples/arg_decorators.py b/examples/arg_decorators.py index a0d08d43..8785fe4f 100755 --- a/examples/arg_decorators.py +++ b/examples/arg_decorators.py @@ -9,7 +9,7 @@ import cmd2 class ArgparsingApp(cmd2.Cmd): def __init__(self): - super().__init__(use_ipython=True) + super().__init__(include_ipy=True) self.intro = 'cmd2 has awesome decorators to make it easy to use Argparse to parse command arguments' # do_fsize parser diff --git a/examples/basic.py b/examples/basic.py index 24e6b7d8..8f507e03 100755 --- a/examples/basic.py +++ b/examples/basic.py @@ -24,7 +24,7 @@ class BasicApp(cmd2.Cmd): multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', startup_script='scripts/startup.txt', - use_ipython=True, + include_ipy=True, ) self.intro = style('Welcome to PyOhio 2019 and cmd2!', fg=fg.red, bg=bg.white, bold=True) + ' 😀' diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index 33ad699b..39332203 100755 --- a/examples/cmd_as_argument.py +++ b/examples/cmd_as_argument.py @@ -30,8 +30,8 @@ class CmdLineApp(cmd2.Cmd): def __init__(self): shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) shortcuts.update({'&': 'speak'}) - # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell - super().__init__(allow_cli_args=False, use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts) + # Set include_ipy to True to enable the "ipy" command which runs an interactive IPython shell + super().__init__(allow_cli_args=False, include_ipy=True, multiline_commands=['orate'], shortcuts=shortcuts) self.self_in_py = True self.maxrepeats = 3 diff --git a/examples/colors.py b/examples/colors.py index 8246c18a..50c9b906 100755 --- a/examples/colors.py +++ b/examples/colors.py @@ -44,8 +44,8 @@ class CmdLineApp(cmd2.Cmd): """Example cmd2 application demonstrating colorized output.""" def __init__(self): - # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell - super().__init__(use_ipython=True) + # Set include_ipy to True to enable the "ipy" command which runs an interactive IPython shell + super().__init__(include_ipy=True) self.maxrepeats = 3 # Make maxrepeats settable at runtime diff --git a/examples/decorator_example.py b/examples/decorator_example.py index 9497bcc0..6a5e6b10 100755 --- a/examples/decorator_example.py +++ b/examples/decorator_example.py @@ -24,10 +24,7 @@ class CmdLineApp(cmd2.Cmd): def __init__(self, ip_addr=None, port=None, transcript_files=None): shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) shortcuts.update({'&': 'speak'}) - # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell - super().__init__( - use_ipython=False, transcript_files=transcript_files, multiline_commands=['orate'], shortcuts=shortcuts - ) + super().__init__(transcript_files=transcript_files, multiline_commands=['orate'], shortcuts=shortcuts) self.maxrepeats = 3 # Make maxrepeats settable at runtime diff --git a/examples/dynamic_commands.py b/examples/dynamic_commands.py index aca370ec..eee5b8cc 100755 --- a/examples/dynamic_commands.py +++ b/examples/dynamic_commands.py @@ -33,7 +33,7 @@ class CommandsInLoop(cmd2.Cmd): help_func_name = HELP_FUNC_PREFIX + command setattr(self, help_func_name, help_func) - super().__init__(use_ipython=True) + super().__init__(include_ipy=True) def send_text(self, args: cmd2.Statement, *, text: str): """Simulate sending text to a server and printing the response.""" diff --git a/examples/hello_cmd2.py b/examples/hello_cmd2.py index e94ad2a5..0e639e29 100755 --- a/examples/hello_cmd2.py +++ b/examples/hello_cmd2.py @@ -11,9 +11,8 @@ if __name__ == '__main__': import sys # If run as the main application, simply start a bare-bones cmd2 application with only built-in functionality. - # Set "use_ipython" to True to include the ipy command if IPython is installed, which supports advanced interactive - # debugging of your application via introspection on self. - app = cmd2.Cmd(use_ipython=True, persistent_history_file='cmd2_history.dat') + # Enable commands to support interactive Python and IPython shells. + app = cmd2.Cmd(include_py=True, include_ipy=True, persistent_history_file='cmd2_history.dat') app.self_in_py = True # Enable access to "self" within the py command app.debug = True # Show traceback if/when an exception occurs sys.exit(app.cmdloop()) diff --git a/examples/help_categories.py b/examples/help_categories.py index d9a7cce2..80c976c1 100755 --- a/examples/help_categories.py +++ b/examples/help_categories.py @@ -33,8 +33,7 @@ class HelpCategories(cmd2.Cmd): CMD_CAT_SERVER_INFO = 'Server Information' def __init__(self): - # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell - super().__init__(use_ipython=False) + super().__init__() def do_connect(self, _): """Connect command""" diff --git a/examples/initialization.py b/examples/initialization.py index 54bef6d8..dfea2183 100755 --- a/examples/initialization.py +++ b/examples/initialization.py @@ -28,7 +28,7 @@ class BasicApp(cmd2.Cmd): multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', startup_script='scripts/startup.txt', - use_ipython=True, + include_ipy=True, ) # Prints an intro banner once upon application startup diff --git a/examples/override_parser.py b/examples/override_parser.py index f615e6e0..00161ef6 100755 --- a/examples/override_parser.py +++ b/examples/override_parser.py @@ -23,7 +23,7 @@ argparse.cmd2_parser_module = 'examples.custom_parser' if __name__ == '__main__': import sys - app = cmd2.Cmd(use_ipython=True, persistent_history_file='cmd2_history.dat') + app = cmd2.Cmd(include_ipy=True, persistent_history_file='cmd2_history.dat') app.self_in_py = True # Enable access to "self" within the py command app.debug = True # Show traceback if/when an exception occurs sys.exit(app.cmdloop()) diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py index 16326569..d138c433 100755 --- a/examples/plumbum_colors.py +++ b/examples/plumbum_colors.py @@ -78,8 +78,8 @@ class CmdLineApp(cmd2.Cmd): """Example cmd2 application demonstrating colorized output.""" def __init__(self): - # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell - super().__init__(use_ipython=True) + # Set include_ipy to True to enable the "ipy" command which runs an interactive IPython shell + super().__init__(include_ipy=True) self.maxrepeats = 3 # Make maxrepeats settable at runtime diff --git a/examples/python_scripting.py b/examples/python_scripting.py index bb43095e..33d8010d 100755 --- a/examples/python_scripting.py +++ b/examples/python_scripting.py @@ -33,8 +33,8 @@ class CmdLineApp(cmd2.Cmd): """ Example cmd2 application to showcase conditional control flow in Python scripting within cmd2 apps.""" def __init__(self): - # Enable the optional ipy command if IPython is installed by setting use_ipython=True - super().__init__(use_ipython=True) + # Set include_ipy to True to enable the "ipy" command which runs an interactive IPython shell + super().__init__(include_ipy=True) self._set_prompt() self.intro = 'Happy 𝛑 Day. Note the full Unicode support: 😇 💩' diff --git a/tests/conftest.py b/tests/conftest.py index 5ed185a3..c605a73e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -167,7 +167,7 @@ def run_cmd(app, cmd): @fixture def base_app(): - return cmd2.Cmd() + return cmd2.Cmd(include_py=True, include_ipy=True) # These are odd file names for testing quoting of them diff --git a/tests/pyscript/py_locals.py b/tests/pyscript/py_locals.py new file mode 100644 index 00000000..16cb6926 --- /dev/null +++ b/tests/pyscript/py_locals.py @@ -0,0 +1,5 @@ +# flake8: noqa F821 +# Tests how much a pyscript can affect cmd2.Cmd.py_locals + +del [locals()["test_var"]] +my_list.append(2) diff --git a/tests/pyscript/self_in_py.py b/tests/pyscript/self_in_py.py new file mode 100644 index 00000000..f0f6271a --- /dev/null +++ b/tests/pyscript/self_in_py.py @@ -0,0 +1,6 @@ +# flake8: noqa F821 +# Tests self_in_py in pyscripts +if 'self' in globals(): + print("I see self") +else: + print("I do not see self") diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 44d2b304..43c937ae 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -265,39 +265,6 @@ def test_shell_manual_call(base_app): base_app.do_shell(cmd) -def test_base_py(base_app): - # Make sure py can't edit Cmd.py_locals. It used to be that cmd2 was passing its py_locals - # dictionary to the py environment instead of a shallow copy. - base_app.py_locals['test_var'] = 5 - out, err = run_cmd(base_app, 'py del[locals()["test_var"]]') - assert not out and not err - assert base_app.py_locals['test_var'] == 5 - - out, err = run_cmd(base_app, 'py print(test_var)') - assert out[0].rstrip() == '5' - - # Place an editable object in py_locals. Since we make a shallow copy of py_locals, - # this object should be editable from the py environment. - base_app.py_locals['my_list'] = [] - out, err = run_cmd(base_app, 'py my_list.append(2)') - assert not out and not err - assert base_app.py_locals['my_list'][0] == 2 - - # Try a print statement - out, err = run_cmd(base_app, 'py print("spaces" + " in this " + "command")') - assert out[0].rstrip() == 'spaces in this command' - - # Set self_in_py to True and make sure we see self - base_app.self_in_py = True - out, err = run_cmd(base_app, 'py print(self)') - assert 'cmd2.cmd2.Cmd object' in out[0] - - # Set self_in_py to False and make sure we can't see self - base_app.self_in_py = False - out, err = run_cmd(base_app, 'py print(self)') - assert "NameError: name 'self' is not defined" in err - - def test_base_error(base_app): out, err = run_cmd(base_app, 'meow') assert "is not a recognized command" in err[0] @@ -336,15 +303,15 @@ def test_run_script_with_empty_args(base_app): assert "the following arguments are required" in err[1] -def test_run_script_with_nonexistent_file(base_app, capsys): +def test_run_script_with_invalid_file(base_app, request): + # Path does not exist out, err = run_cmd(base_app, 'run_script does_not_exist.txt') - assert "does not exist" in err[0] + assert "Problem accessing script from " in err[0] - -def test_run_script_with_directory(base_app, request): + # Path is a directory test_dir = os.path.dirname(request.module.__file__) out, err = run_cmd(base_app, 'run_script {}'.format(test_dir)) - assert "is not a file" in err[0] + assert "Problem accessing script from " in err[0] def test_run_script_with_empty_file(base_app, request): @@ -1562,12 +1529,12 @@ def test_commandresult_falsy(commandresult_app): def test_is_text_file_bad_input(base_app): # Test with a non-existent file - file_is_valid = utils.is_text_file('does_not_exist.txt') - assert not file_is_valid + with pytest.raises(OSError): + utils.is_text_file('does_not_exist.txt') # Test with a directory - dir_is_valid = utils.is_text_file('.') - assert not dir_is_valid + with pytest.raises(OSError): + utils.is_text_file('.') def test_eof(base_app): @@ -2270,6 +2237,7 @@ def test_get_all_commands(base_app): 'eof', 'help', 'history', + 'ipy', 'macro', 'py', 'quit', diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py index b8daa24b..02e75ed5 100644 --- a/tests/test_run_pyscript.py +++ b/tests/test_run_pyscript.py @@ -141,6 +141,42 @@ def test_run_pyscript_environment(base_app, request): assert out[0] == "PASSED" +def test_run_pyscript_self_in_py(base_app, request): + test_dir = os.path.dirname(request.module.__file__) + python_script = os.path.join(test_dir, 'pyscript', 'self_in_py.py') + + # Set self_in_py to True and make sure we see self + base_app.self_in_py = True + out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) + assert 'I see self' in out[0] + + # Set self_in_py to False and make sure we can't see self + base_app.self_in_py = False + out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) + assert 'I do not see self' in out[0] + + +def test_run_pyscript_py_locals(base_app, request): + test_dir = os.path.dirname(request.module.__file__) + python_script = os.path.join(test_dir, 'pyscript', 'py_locals.py') + + # Make sure pyscripts can't edit Cmd.py_locals. It used to be that cmd2 was passing its py_locals + # dictionary to the py environment instead of a shallow copy. + base_app.py_locals['test_var'] = 5 + + # Place an editable object in py_locals. Since we make a shallow copy of py_locals, + # this object should be editable from the py environment. + base_app.py_locals['my_list'] = [] + + run_cmd(base_app, 'run_pyscript {}'.format(python_script)) + + # test_var should still exist + assert base_app.py_locals['test_var'] == 5 + + # my_list should be edited + assert base_app.py_locals['my_list'][0] == 2 + + def test_run_pyscript_app_echo(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'echo.py') |