summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md9
-rwxr-xr-xREADME.md8
-rw-r--r--cmd2/cmd2.py231
-rw-r--r--cmd2/utils.py32
-rw-r--r--docs/features/embedded_python_shells.rst98
-rw-r--r--docs/features/initialization.rst2
-rwxr-xr-xexamples/arg_decorators.py2
-rwxr-xr-xexamples/basic.py2
-rwxr-xr-xexamples/cmd_as_argument.py4
-rwxr-xr-xexamples/colors.py4
-rwxr-xr-xexamples/decorator_example.py5
-rwxr-xr-xexamples/dynamic_commands.py2
-rwxr-xr-xexamples/hello_cmd2.py5
-rwxr-xr-xexamples/help_categories.py3
-rwxr-xr-xexamples/initialization.py2
-rwxr-xr-xexamples/override_parser.py2
-rwxr-xr-xexamples/plumbum_colors.py4
-rwxr-xr-xexamples/python_scripting.py4
-rw-r--r--tests/conftest.py2
-rw-r--r--tests/pyscript/py_locals.py5
-rw-r--r--tests/pyscript/self_in_py.py6
-rwxr-xr-xtests/test_cmd2.py52
-rw-r--r--tests/test_run_pyscript.py36
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
diff --git a/README.md b/README.md
index 13ce9410..8d366b74 100755
--- a/README.md
+++ b/README.md
@@ -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')