summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2021-03-26 13:56:33 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2021-03-26 15:24:34 -0400
commit62ed8aebf6eefcf68a15fdd4b7a7cd5dfe4c6f6b (patch)
tree1e72725041a8e3f83f31dddbfd564fcd02f27401
parent070262e1f397e2297cdb1ad611db6b6d5bed8830 (diff)
downloadcmd2-git-py_refactor.tar.gz
Renamed use_ipython keyword parameter of cmd2.Cmd.__init__() to include_ipy.py_refactor
Added include_py keyword parameter to cmd2.Cmd.__init__(). If False, then the py command will not be available. Removed ability to run Python commands from the command line with py. Made banners and exit messages of Python and IPython consistent. Changed utils.is_text_file() to raise OSError if file cannot be read.
-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')