Skip to content

Commit

Permalink
Renamed use_ipython keyword parameter of cmd2.Cmd.__init__() to inclu…
Browse files Browse the repository at this point in the history
…de_ipy.

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.
  • Loading branch information
kmvanbrunt authored and anselor committed Mar 30, 2021
1 parent 070262e commit 2397280
Show file tree
Hide file tree
Showing 23 changed files with 231 additions and 289 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `!`)
Expand Down Expand Up @@ -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'))

Expand Down
231 changes: 102 additions & 129 deletions cmd2/cmd2.py

Large diffs are not rendered by default.

32 changes: 11 additions & 21 deletions cmd2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,38 +186,28 @@ def set_value(self, value: Any) -> Any:


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

Expand Down
98 changes: 30 additions & 68 deletions docs/features/embedded_python_shells.rst
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,77 +27,29 @@ 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
arg 1: 'foo'
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

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/features/initialization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion examples/arg_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

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
Expand Down
2 changes: 1 addition & 1 deletion examples/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(self):
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) + ' 😀'
Expand Down
4 changes: 2 additions & 2 deletions examples/cmd_as_argument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions examples/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 1 addition & 4 deletions examples/decorator_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/dynamic_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(self):
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."""
Expand Down
5 changes: 2 additions & 3 deletions examples/hello_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
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())
3 changes: 1 addition & 2 deletions examples/help_categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
2 changes: 1 addition & 1 deletion examples/initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def __init__(self):
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
Expand Down
2 changes: 1 addition & 1 deletion examples/override_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
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())
4 changes: 2 additions & 2 deletions examples/plumbum_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions examples/python_scripting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: 😇 💩'

Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions tests/pyscript/py_locals.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 6 additions & 0 deletions tests/pyscript/self_in_py.py
Original file line number Diff line number Diff line change
@@ -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")
Loading

0 comments on commit 2397280

Please sign in to comment.