diff options
author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2018-02-11 23:23:51 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-11 23:23:51 -0500 |
commit | 4a765e16e64d0b8479921f2e36b4cb8b97db92b1 (patch) | |
tree | d5177fbf8ea7a732ae21febbf7e1c006e059ce2e | |
parent | 4895d5d8db4e57e2ef9a062473a8536f4f07f213 (diff) | |
parent | f3c6b1b32d614076dc17d2736ae1860d37a36cd5 (diff) | |
download | cmd2-git-4a765e16e64d0b8479921f2e36b4cb8b97db92b1.tar.gz |
Merge pull request #270 from python-cmd2/persistent_history
Added optional persistent readline history feature
-rw-r--r-- | CHANGELOG.md | 9 | ||||
-rwxr-xr-x | README.md | 3 | ||||
-rwxr-xr-x | cmd2.py | 28 | ||||
-rw-r--r-- | docs/conf.py | 2 | ||||
-rw-r--r-- | docs/freefeatures.rst | 15 | ||||
-rw-r--r-- | docs/requirements.txt | 1 | ||||
-rwxr-xr-x | examples/persistent_history.py | 33 | ||||
-rwxr-xr-x[-rw-r--r--] | examples/submenus.py | 6 | ||||
-rwxr-xr-x | setup.py | 4 | ||||
-rw-r--r-- | tests/test_cmd2.py | 37 | ||||
-rw-r--r-- | tox.ini | 8 |
11 files changed, 125 insertions, 21 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad98d0d..110dec68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.8.1 (TBD, 2018) + +* Enhancements + * Added support for sub-menus. + * See [submenus.py](https://github.com/python-cmd2/cmd2/blob/master/examples/submenus.py) for an example of how to use it + * Added option for persistent readline history + * See [persistent_history.py](https://github.com/python-cmd2/cmd2/blob/master/examples/persistent_history.py) for an example + * See the [Searchable command history](http://cmd2.readthedocs.io/en/latest/freefeatures.html#searchable-command-history) section of the documentation for more info + ## 0.8.0 (February 1, 2018) * Bug Fixes * Fixed unit tests on Python 3.7 due to changes in how re.escape() behaves in Python 3.7 @@ -18,7 +18,7 @@ when using cmd. Main Features ------------- -- Searchable command history (`history` command and `<Ctrl>+r`) +- Searchable command history (`history` command and `<Ctrl>+r`) - optionally persistent - Text file scripting of your application with `load` (`@`) and `_relative_load` (`@@`) - Python scripting of your application with ``pyscript`` - Run shell commands with ``!`` @@ -30,6 +30,7 @@ Main Features - Special-character command shortcuts (beyond cmd's `@` and `!`) - Settable environment parameters - Parsing commands with arguments using `argparse`, including support for sub-commands +- Sub-menu support via the ``AddSubmenu`` decorator - Unicode character support (*Python 3 only*) - Good tab-completion of commands, sub-commands, file system paths, and shell commands - Python 2.7 and 3.4+ support @@ -24,6 +24,8 @@ is used in place of `print`. Git repository on GitHub at https://github.com/python-cmd2/cmd2 """ +import argparse +import atexit import cmd import codecs import collections @@ -31,7 +33,6 @@ import datetime import glob import io import optparse -import argparse import os import platform import re @@ -112,7 +113,7 @@ if six.PY2 and sys.platform.startswith('lin'): except ImportError: pass -__version__ = '0.8.0' +__version__ = '0.8.1' # Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past pyparsing.ParserElement.enablePackrat() @@ -549,6 +550,7 @@ def strip_ansi(text): def _pop_readline_history(clear_history=True): """Returns a copy of readline's history and optionally clears it (default)""" + # noinspection PyArgumentList history = [ readline.get_history_item(i) for i in range(1, 1 + readline.get_current_history_length()) @@ -689,6 +691,7 @@ class AddSubmenu(object): ) submenu.cmdloop() if self.reformat_prompt is not None: + # noinspection PyUnboundLocalVariable self.submenu.prompt = prompt _push_readline_history(history) finally: @@ -761,12 +764,12 @@ class AddSubmenu(object): _Cmd.complete_help = _complete_submenu_help # Create bindings in the parent command to the submenus commands. - setattr(_Cmd, 'do_' + self.command, enter_submenu) + setattr(_Cmd, 'do_' + self.command, enter_submenu) setattr(_Cmd, 'complete_' + self.command, complete_submenu) # Create additional bindings for aliases for _alias in self.aliases: - setattr(_Cmd, 'do_' + _alias, enter_submenu) + setattr(_Cmd, 'do_' + _alias, enter_submenu) setattr(_Cmd, 'complete_' + _alias, complete_submenu) return _Cmd @@ -833,12 +836,15 @@ class Cmd(cmd.Cmd): 'quiet': "Don't print nonessential feedback", 'timing': 'Report execution times'} - def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False, transcript_files=None): + def __init__(self, completekey='tab', stdin=None, stdout=None, persistent_history_file='', + persistent_history_length=1000, use_ipython=False, transcript_files=None): """An easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package. :param completekey: str - (optional) readline name of a completion key, default to Tab :param stdin: (optional) alternate input file object, if not specified, sys.stdin is used :param stdout: (optional) alternate output file object, if not specified, sys.stdout is used + :param persistent_history_file: str - (optional) file path to load a persistent readline history from + :param persistent_history_length: int - (optional) max number of lines which will be written to the history file :param use_ipython: (optional) should the "ipy" command be included for an embedded IPython shell :param transcript_files: str - (optional) allows running transcript tests when allow_cli_args is False """ @@ -849,6 +855,17 @@ class Cmd(cmd.Cmd): except AttributeError: pass + # If persistent readline history is enabled, then read history from file and register to write to file at exit + if persistent_history_file: + persistent_history_file = os.path.expanduser(persistent_history_file) + try: + readline.read_history_file(persistent_history_file) + # default history len is -1 (infinite), which may grow unruly + readline.set_history_length(persistent_history_length) + except FileNotFoundError: + pass + atexit.register(readline.write_history_file, persistent_history_file) + # Call super class constructor. Need to do it in this way for Python 2 and 3 compatibility cmd.Cmd.__init__(self, completekey=completekey, stdin=stdin, stdout=stdout) @@ -2901,6 +2918,7 @@ def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')) :return: namedtuple type """ T = collections.namedtuple(typename, field_names) + # noinspection PyUnresolvedReferences T.__new__.__defaults__ = default_values return T diff --git a/docs/conf.py b/docs/conf.py index d4ef14bf..09d68b9c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,7 +62,7 @@ author = 'Catherine Devlin and Todd Leonhardt' # The short X.Y version. version = '0.8' # The full version, including alpha/beta/rc tags. -release = '0.8.0' +release = '0.8.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst index ea40c87c..a439db56 100644 --- a/docs/freefeatures.rst +++ b/docs/freefeatures.rst @@ -13,7 +13,7 @@ Script files ============ Text files can serve as scripts for your ``cmd2``-based -application, with the ``load``, ``_relative_load``, ``edit`` and ``history`` commands. +application, with the ``load``, ``_relative_load``, and ``edit`` commands. Both ASCII and UTF-8 encoded unicode text files are supported. @@ -25,8 +25,6 @@ Simply include one command per line, typed exactly as you would inside a ``cmd2` .. automethod:: cmd2.Cmd.do_edit -.. automethod:: cmd2.Cmd.do_history - Comments ======== @@ -250,17 +248,22 @@ Searchable command history ========================== All cmd_-based applications have access to previous commands with -the up- and down- cursor keys. +the up- and down- arrow keys. All cmd_-based applications on systems with the ``readline`` module -also provide `bash-like history list editing`_. +also provide `Readline Emacs editing mode`_. With this you can, for example, use **Ctrl-r** to search backward through +the readline history. -.. _`bash-like history list editing`: http://www.talug.org/events/20030709/cmdline_history.html +``cmd2`` adds the option of making this readline history persistent via optional arguments to ``cmd2.Cmd.__init__()``: + +.. automethod:: cmd2.Cmd.__init__ ``cmd2`` makes a third type of history access available with the **history** command: .. automethod:: cmd2.Cmd.do_history +.. _`Readline Emacs editing mode`: http://readline.kablamo.org/emacs.html + Quitting the application ======================== diff --git a/docs/requirements.txt b/docs/requirements.txt index fa4e3570..b50df7d1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ pyparsing six pyperclip +contextlib2 diff --git a/examples/persistent_history.py b/examples/persistent_history.py new file mode 100755 index 00000000..e1874212 --- /dev/null +++ b/examples/persistent_history.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# coding=utf-8 +"""This example demonstrates how to enable persistent readline history in your cmd2 application. + +This will allow end users of your cmd2-based application to use the arrow keys and Ctrl+r in a manner which persists +across invocations of your cmd2 application. This can make it much easier for them to use your application. +""" +import cmd2 + + +class Cmd2PersistentHistory(cmd2.Cmd): + """Basic example of how to enable persistent readline history within your cmd2 app.""" + def __init__(self, hist_file): + """Configure the app to load persistent readline history from a file. + + :param hist_file: file to load readline history from at start and write it to at end + """ + cmd2.Cmd.__init__(self, persistent_history_file=hist_file, persistent_history_length=500) + self.allow_cli_args = False + self.prompt = 'ph> ' + + # ... your class code here ... + + +if __name__ == '__main__': + import sys + + history_file = '~/.persistent_history.cmd2' + if len(sys.argv) > 1: + history_file = sys.argv[1] + + app = Cmd2PersistentHistory(hist_file=history_file) + app.cmdloop() diff --git a/examples/submenus.py b/examples/submenus.py index 52f26e08..1e3da0da 100644..100755 --- a/examples/submenus.py +++ b/examples/submenus.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# coding=utf-8 """ Create a CLI with a nested command structure as follows. The commands 'second' and 'third' navigate the CLI to the scope of the submenu. Nesting of the submenus is done with the cmd2.AddSubmenu() decorator. @@ -6,9 +7,6 @@ of the submenu. Nesting of the submenus is done with the cmd2.AddSubmenu() decor (Top Level)----second----->(2nd Level)----third----->(3rd Level) | | | ---> say ---> say ---> say - - - """ from __future__ import print_function import sys @@ -71,7 +69,6 @@ class SecondLevel(cmd2.Cmd): return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)] - @cmd2.AddSubmenu(SecondLevel(), command='second', aliases=('second_alias',), @@ -105,7 +102,6 @@ class TopLevel(cmd2.Cmd): return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)] - if __name__ == '__main__': root = TopLevel() @@ -6,7 +6,7 @@ Setuptools setup file, used to install or test 'cmd2' import sys from setuptools import setup -VERSION = '0.8.0' +VERSION = '0.8.1' DESCRIPTION = "cmd2 - a tool for building interactive command line applications in Python" LONG_DESCRIPTION = """cmd2 is a tool for building interactive command line applications in Python. Its goal is to make it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It @@ -77,7 +77,7 @@ if sys.version_info < (3, 0): INSTALL_REQUIRES += ['subprocess32'] # unittest.mock was added in Python 3.3. mock is a backport of unittest.mock to all versions of Python -TESTS_REQUIRE = ['mock', 'pytest'] +TESTS_REQUIRE = ['mock', 'pytest', 'pexpect'] DOCS_REQUIRE = ['sphinx', 'sphinx_rtd_theme', 'pyparsing', 'pyperclip', 'six'] setup( diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 186def65..ee9c1fc3 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -11,6 +11,7 @@ import io import tempfile import mock +import pexpect import pytest import six @@ -25,7 +26,7 @@ from conftest import run_cmd, normalize, BASE_HELP, HELP_HISTORY, SHORTCUTS_TXT, def test_ver(): - assert cmd2.__version__ == '0.8.0' + assert cmd2.__version__ == '0.8.1' def test_empty_statement(base_app): @@ -1525,3 +1526,37 @@ def test_poutput_none(base_app): out = base_app.stdout.buffer expected = '' assert out == expected + + +@pytest.mark.skipif(sys.platform == 'win32' or sys.platform.startswith('lin'), + reason="pexpect doesn't have a spawn() function on Windows and readline doesn't work on TravisCI") +def test_persistent_history(request): + """Will run on macOS to verify expected persistent history behavior.""" + test_dir = os.path.dirname(request.module.__file__) + persistent_app = os.path.join(test_dir, '..', 'examples', 'persistent_history.py') + + python = 'python3' + if six.PY2: + python = 'python2' + + command = '{} {}'.format(python, persistent_app) + + # Start an instance of the persistent history example and send it a few commands + child = pexpect.spawn(command) + prompt = 'ph> ' + child.expect(prompt) + child.sendline('help') + child.expect(prompt) + child.sendline('help history') + child.expect(prompt) + child.sendline('quit') + child.close() + + # Start a 2nd instance of the persistent history example and send it an up arrow to verify persistent history + up_arrow = '\x1b[A' + child2 = pexpect.spawn(command) + child2.expect(prompt) + child2.send(up_arrow) + child2.expect('quit') + assert child2.after == b'quit' + child2.close() @@ -13,6 +13,7 @@ setenv = deps = codecov mock + pexpect pyparsing pyperclip pytest @@ -28,6 +29,7 @@ commands = deps = codecov mock + pexpect pyparsing pyperclip pyreadline @@ -43,6 +45,7 @@ commands = [testenv:py34] deps = mock + pexpect pyparsing pyperclip pytest @@ -53,6 +56,7 @@ commands = py.test -v -n2 [testenv:py35] deps = mock + pexpect pyparsing pyperclip pytest @@ -63,6 +67,7 @@ commands = py.test -v -n2 [testenv:py35-win] deps = mock + pexpect pyparsing pyperclip pyreadline @@ -75,6 +80,7 @@ commands = py.test -v -n2 deps = codecov mock + pexpect pyparsing pyperclip pytest @@ -88,6 +94,7 @@ commands = [testenv:py36-win] deps = mock + pexpect pyparsing pyperclip pyreadline @@ -99,6 +106,7 @@ commands = py.test -v -n2 [testenv:py37] deps = mock + pexpect pyparsing pyperclip pytest |