summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkotfu <kotfu@kotfu.net>2019-02-09 13:43:41 -0700
committerkotfu <kotfu@kotfu.net>2019-02-09 13:43:41 -0700
commit321a8c72a48d227011177bb91006ed20607a1e44 (patch)
treec69b95e823cb897d45ee04e3eb8b02f123181122
parent80327e0a7f21424554ade6626be0798ce6392a1d (diff)
downloadcmd2-git-321a8c72a48d227011177bb91006ed20607a1e44.tar.gz
Extract history classes and test into their own files
-rw-r--r--cmd2/cmd2.py141
-rw-r--r--cmd2/history.py151
-rw-r--r--tests/test_cmd2.py190
-rw-r--r--tests/test_history.py209
4 files changed, 361 insertions, 330 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index ba2b6e8a..69a6c2aa 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -50,6 +50,7 @@ from . import utils
from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .parsing import StatementParser, Statement, Macro, MacroArg
+from .history import History, HistoryItem
# Set up readline
from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt
@@ -287,39 +288,6 @@ class EmptyStatement(Exception):
pass
-class HistoryItem(str):
- """Class used to represent an item in the History list"""
- listformat = ' {:>4} {}\n'
- ex_listformat = ' Ex: {}\n'
-
- def __new__(cls, statement: Statement):
- """Create a new instance of HistoryItem
-
- We must override __new__ because we are subclassing `str` which is
- immutable and takes a different number of arguments as Statement.
- """
- hi = super().__new__(cls, statement.raw)
- hi.statement = statement
- hi.idx = None
- return hi
-
- @property
- def expanded(self) -> str:
- """Return the command as run which includes shortcuts and aliases resolved plus any changes made in hooks"""
- return self.statement.expanded_command_line
-
- def pr(self, verbose: bool) -> str:
- """Represent a HistoryItem in a pretty fashion suitable for printing.
-
- :return: pretty print string version of a HistoryItem
- """
- ret_str = self.listformat.format(self.idx, str(self).rstrip())
- if verbose and self != self.expanded:
- ret_str += self.ex_listformat.format(self.expanded.rstrip())
-
- return ret_str
-
-
class Cmd(cmd.Cmd):
"""An easy but powerful framework for writing line-oriented command interpreters.
@@ -3810,113 +3778,6 @@ class Cmd(cmd.Cmd):
self._cmdfinalization_hooks.append(func)
-class History(list):
- """ A list of HistoryItems that knows how to respond to user requests. """
-
- # noinspection PyMethodMayBeStatic
- def _zero_based_index(self, onebased: int) -> int:
- """Convert a one-based index to a zero-based index."""
- result = onebased
- if result > 0:
- result -= 1
- return result
-
- def _to_index(self, raw: str) -> Optional[int]:
- if raw:
- result = self._zero_based_index(int(raw))
- else:
- result = None
- return result
-
- spanpattern = re.compile(r'^\s*(?P<start>-?\d+)?\s*(?P<separator>:|(\.{2,}))?\s*(?P<end>-?\d+)?\s*$')
-
- def span(self, raw: str) -> List[HistoryItem]:
- """Parses the input string search for a span pattern and if if found, returns a slice from the History list.
-
- :param raw: string potentially containing a span of the forms a..b, a:b, a:, ..b
- :return: slice from the History list
- """
- if raw.lower() in ('*', '-', 'all'):
- raw = ':'
- results = self.spanpattern.search(raw)
- if not results:
- raise IndexError
- if not results.group('separator'):
- return [self[self._to_index(results.group('start'))]]
- start = self._to_index(results.group('start')) or 0 # Ensure start is not None
- end = self._to_index(results.group('end'))
- reverse = False
- if end is not None:
- if end < start:
- (start, end) = (end, start)
- reverse = True
- end += 1
- result = self[start:end]
- if reverse:
- result.reverse()
- return result
-
- rangePattern = re.compile(r'^\s*(?P<start>[\d]+)?\s*-\s*(?P<end>[\d]+)?\s*$')
-
- def append(self, new: Statement) -> None:
- """Append a HistoryItem to end of the History list
-
- :param new: command line to convert to HistoryItem and add to the end of the History list
- """
- new = HistoryItem(new)
- list.append(self, new)
- new.idx = len(self)
-
- def get(self, getme: Optional[Union[int, str]]=None) -> List[HistoryItem]:
- """Get an item or items from the History list using 1-based indexing.
-
- :param getme: optional item(s) to get (either an integer index or string to search for)
- :return: list of HistoryItems matching the retrieval criteria
- """
- if not getme:
- return self
- try:
- getme = int(getme)
- if getme < 0:
- return self[:(-1 * getme)]
- else:
- return [self[getme - 1]]
- except IndexError:
- return []
- except ValueError:
- range_result = self.rangePattern.search(getme)
- if range_result:
- start = range_result.group('start') or None
- end = range_result.group('start') or None
- if start:
- start = int(start) - 1
- if end:
- end = int(end)
- return self[start:end]
-
- getme = getme.strip()
-
- if getme.startswith(r'/') and getme.endswith(r'/'):
- finder = re.compile(getme[1:-1], re.DOTALL | re.MULTILINE | re.IGNORECASE)
-
- def isin(hi):
- """Listcomp filter function for doing a regular expression search of History.
-
- :param hi: HistoryItem
- :return: bool - True if search matches
- """
- return finder.search(hi)
- else:
- def isin(hi):
- """Listcomp filter function for doing a case-insensitive string search of History.
-
- :param hi: HistoryItem
- :return: bool - True if search matches
- """
- return utils.norm_fold(getme) in utils.norm_fold(hi)
- return [itm for itm in self if isin(itm)]
-
-
class Statekeeper(object):
"""Class used to save and restore state during load and py commands as well as when redirecting output or pipes."""
def __init__(self, obj: Any, attribs: Iterable) -> None:
diff --git a/cmd2/history.py b/cmd2/history.py
new file mode 100644
index 00000000..0989b7db
--- /dev/null
+++ b/cmd2/history.py
@@ -0,0 +1,151 @@
+# coding=utf-8
+"""
+History management classes
+"""
+
+import re
+
+from typing import List, Optional, Union
+
+from . import utils
+from .parsing import Statement
+
+
+class HistoryItem(str):
+ """Class used to represent one command in the History list"""
+ listformat = ' {:>4} {}\n'
+ ex_listformat = ' Ex: {}\n'
+
+ def __new__(cls, statement: Statement):
+ """Create a new instance of HistoryItem
+
+ We must override __new__ because we are subclassing `str` which is
+ immutable and takes a different number of arguments as Statement.
+ """
+ hi = super().__new__(cls, statement.raw)
+ hi.statement = statement
+ hi.idx = None
+ return hi
+
+ @property
+ def expanded(self) -> str:
+ """Return the command as run which includes shortcuts and aliases resolved plus any changes made in hooks"""
+ return self.statement.expanded_command_line
+
+ def pr(self, verbose: bool) -> str:
+ """Represent a HistoryItem in a pretty fashion suitable for printing.
+
+ :return: pretty print string version of a HistoryItem
+ """
+ ret_str = self.listformat.format(self.idx, str(self).rstrip())
+ if verbose and self != self.expanded:
+ ret_str += self.ex_listformat.format(self.expanded.rstrip())
+
+ return ret_str
+
+
+class History(list):
+ """ A list of HistoryItems that knows how to respond to user requests. """
+
+ # noinspection PyMethodMayBeStatic
+ def _zero_based_index(self, onebased: int) -> int:
+ """Convert a one-based index to a zero-based index."""
+ result = onebased
+ if result > 0:
+ result -= 1
+ return result
+
+ def _to_index(self, raw: str) -> Optional[int]:
+ if raw:
+ result = self._zero_based_index(int(raw))
+ else:
+ result = None
+ return result
+
+ spanpattern = re.compile(r'^\s*(?P<start>-?\d+)?\s*(?P<separator>:|(\.{2,}))?\s*(?P<end>-?\d+)?\s*$')
+
+ def span(self, raw: str) -> List[HistoryItem]:
+ """Parses the input string search for a span pattern and if if found, returns a slice from the History list.
+
+ :param raw: string potentially containing a span of the forms a..b, a:b, a:, ..b
+ :return: slice from the History list
+ """
+ if raw.lower() in ('*', '-', 'all'):
+ raw = ':'
+ results = self.spanpattern.search(raw)
+ if not results:
+ raise IndexError
+ if not results.group('separator'):
+ return [self[self._to_index(results.group('start'))]]
+ start = self._to_index(results.group('start')) or 0 # Ensure start is not None
+ end = self._to_index(results.group('end'))
+ reverse = False
+ if end is not None:
+ if end < start:
+ (start, end) = (end, start)
+ reverse = True
+ end += 1
+ result = self[start:end]
+ if reverse:
+ result.reverse()
+ return result
+
+ rangePattern = re.compile(r'^\s*(?P<start>[\d]+)?\s*-\s*(?P<end>[\d]+)?\s*$')
+
+ def append(self, new: Statement) -> None:
+ """Append a HistoryItem to end of the History list
+
+ :param new: command line to convert to HistoryItem and add to the end of the History list
+ """
+ new = HistoryItem(new)
+ list.append(self, new)
+ new.idx = len(self)
+
+ def get(self, getme: Optional[Union[int, str]]=None) -> List[HistoryItem]:
+ """Get an item or items from the History list using 1-based indexing.
+
+ :param getme: optional item(s) to get (either an integer index or string to search for)
+ :return: list of HistoryItems matching the retrieval criteria
+ """
+ if not getme:
+ return self
+ try:
+ getme = int(getme)
+ if getme < 0:
+ return self[:(-1 * getme)]
+ else:
+ return [self[getme - 1]]
+ except IndexError:
+ return []
+ except ValueError:
+ range_result = self.rangePattern.search(getme)
+ if range_result:
+ start = range_result.group('start') or None
+ end = range_result.group('start') or None
+ if start:
+ start = int(start) - 1
+ if end:
+ end = int(end)
+ return self[start:end]
+
+ getme = getme.strip()
+
+ if getme.startswith(r'/') and getme.endswith(r'/'):
+ finder = re.compile(getme[1:-1], re.DOTALL | re.MULTILINE | re.IGNORECASE)
+
+ def isin(hi):
+ """Listcomp filter function for doing a regular expression search of History.
+
+ :param hi: HistoryItem
+ :return: bool - True if search matches
+ """
+ return finder.search(hi)
+ else:
+ def isin(hi):
+ """Listcomp filter function for doing a case-insensitive string search of History.
+
+ :param hi: HistoryItem
+ :return: bool - True if search matches
+ """
+ return utils.norm_fold(getme) in utils.norm_fold(hi)
+ return [itm for itm in self if isin(itm)]
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 350991fa..bb415fe5 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -302,196 +302,6 @@ def test_base_error(base_app):
assert "is not a recognized command" in out[0]
-@pytest.fixture
-def hist():
- from cmd2.parsing import Statement
- from cmd2.cmd2 import History, HistoryItem
- h = History([HistoryItem(Statement('', raw='first')),
- HistoryItem(Statement('', raw='second')),
- HistoryItem(Statement('', raw='third')),
- HistoryItem(Statement('', raw='fourth'))])
- return h
-
-def test_history_span(hist):
- h = hist
- assert h == ['first', 'second', 'third', 'fourth']
- assert h.span('-2..') == ['third', 'fourth']
- assert h.span('2..3') == ['second', 'third'] # Inclusive of end
- assert h.span('3') == ['third']
- assert h.span(':') == h
- assert h.span('2..') == ['second', 'third', 'fourth']
- assert h.span('-1') == ['fourth']
- assert h.span('-2..-3') == ['third', 'second']
- assert h.span('*') == h
-
-def test_history_get(hist):
- h = hist
- assert h == ['first', 'second', 'third', 'fourth']
- assert h.get('') == h
- assert h.get('-2') == h[:-2]
- assert h.get('5') == []
- assert h.get('2-3') == ['second'] # Exclusive of end
- assert h.get('ir') == ['first', 'third'] # Normal string search for all elements containing "ir"
- assert h.get('/i.*d/') == ['third'] # Regex string search "i", then anything, then "d"
-
-def test_base_history(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- out = run_cmd(base_app, 'history')
- expected = normalize("""
- 1 help
- 2 shortcuts
-""")
- assert out == expected
-
- out = run_cmd(base_app, 'history he')
- expected = normalize("""
- 1 help
-""")
- assert out == expected
-
- out = run_cmd(base_app, 'history sh')
- expected = normalize("""
- 2 shortcuts
-""")
- assert out == expected
-
-def test_history_script_format(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- out = run_cmd(base_app, 'history -s')
- expected = normalize("""
-help
-shortcuts
-""")
- assert out == expected
-
-def test_history_with_string_argument(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
- out = run_cmd(base_app, 'history help')
- expected = normalize("""
- 1 help
- 3 help history
-""")
- assert out == expected
-
-
-def test_history_with_integer_argument(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- out = run_cmd(base_app, 'history 1')
- expected = normalize("""
- 1 help
-""")
- assert out == expected
-
-
-def test_history_with_integer_span(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
- out = run_cmd(base_app, 'history 1..2')
- expected = normalize("""
- 1 help
- 2 shortcuts
-""")
- assert out == expected
-
-def test_history_with_span_start(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
- out = run_cmd(base_app, 'history 2:')
- expected = normalize("""
- 2 shortcuts
- 3 help history
-""")
- assert out == expected
-
-def test_history_with_span_end(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
- out = run_cmd(base_app, 'history :2')
- expected = normalize("""
- 1 help
- 2 shortcuts
-""")
- assert out == expected
-
-def test_history_with_span_index_error(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'help history')
- run_cmd(base_app, '!ls -hal :')
- out = run_cmd(base_app, 'history "hal :"')
- expected = normalize("""
- 3 !ls -hal :
-""")
- assert out == expected
-
-def test_history_output_file(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
-
- fd, fname = tempfile.mkstemp(prefix='', suffix='.txt')
- os.close(fd)
- run_cmd(base_app, 'history -o "{}"'.format(fname))
- expected = normalize('\n'.join(['help', 'shortcuts', 'help history']))
- with open(fname) as f:
- content = normalize(f.read())
- assert content == expected
-
-def test_history_edit(base_app, monkeypatch):
- # Set a fake editor just to make sure we have one. We aren't really
- # going to call it due to the mock
- base_app.editor = 'fooedit'
-
- # Mock out the os.system call so we don't actually open an editor
- m = mock.MagicMock(name='system')
- monkeypatch.setattr("os.system", m)
-
- # Run help command just so we have a command in history
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'history -e 1')
-
- # We have an editor, so should expect a system call
- m.assert_called_once()
-
-def test_history_run_all_commands(base_app):
- # make sure we refuse to run all commands as a default
- run_cmd(base_app, 'shortcuts')
- out = run_cmd(base_app, 'history -r')
- # this should generate an error, but we don't currently have a way to
- # capture stderr in these tests. So we assume that if we got nothing on
- # standard out, that the error occurred because if the command executed
- # then we should have a list of shortcuts in our output
- assert out == []
-
-def test_history_run_one_command(base_app):
- expected = run_cmd(base_app, 'help')
- output = run_cmd(base_app, 'history -r 1')
- assert output == expected
-
-def test_history_clear(base_app):
- # Add commands to history
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'alias')
-
- # Make sure history has items
- out = run_cmd(base_app, 'history')
- assert out
-
- # Clear the history
- run_cmd(base_app, 'history --clear')
-
- # Make sure history is empty
- out = run_cmd(base_app, 'history')
- assert out == []
-
-
def test_base_load(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
filename = os.path.join(test_dir, 'script.txt')
diff --git a/tests/test_history.py b/tests/test_history.py
new file mode 100644
index 00000000..0eddcc1f
--- /dev/null
+++ b/tests/test_history.py
@@ -0,0 +1,209 @@
+# coding=utf-8
+# flake8: noqa E302
+"""
+Test history functions of cmd2
+"""
+import tempfile
+import os
+
+import pytest
+
+# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available
+try:
+ import mock
+except ImportError:
+ from unittest import mock
+
+import cmd2
+from cmd2 import clipboard
+from cmd2 import utils
+from .conftest import run_cmd, normalize
+
+@pytest.fixture
+def hist():
+ from cmd2.parsing import Statement
+ from cmd2.cmd2 import History, HistoryItem
+ h = History([HistoryItem(Statement('', raw='first')),
+ HistoryItem(Statement('', raw='second')),
+ HistoryItem(Statement('', raw='third')),
+ HistoryItem(Statement('', raw='fourth'))])
+ return h
+
+def test_history_span(hist):
+ h = hist
+ assert h == ['first', 'second', 'third', 'fourth']
+ assert h.span('-2..') == ['third', 'fourth']
+ assert h.span('2..3') == ['second', 'third'] # Inclusive of end
+ assert h.span('3') == ['third']
+ assert h.span(':') == h
+ assert h.span('2..') == ['second', 'third', 'fourth']
+ assert h.span('-1') == ['fourth']
+ assert h.span('-2..-3') == ['third', 'second']
+ assert h.span('*') == h
+
+def test_history_get(hist):
+ h = hist
+ assert h == ['first', 'second', 'third', 'fourth']
+ assert h.get('') == h
+ assert h.get('-2') == h[:-2]
+ assert h.get('5') == []
+ assert h.get('2-3') == ['second'] # Exclusive of end
+ assert h.get('ir') == ['first', 'third'] # Normal string search for all elements containing "ir"
+ assert h.get('/i.*d/') == ['third'] # Regex string search "i", then anything, then "d"
+
+def test_base_history(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ out = run_cmd(base_app, 'history')
+ expected = normalize("""
+ 1 help
+ 2 shortcuts
+""")
+ assert out == expected
+
+ out = run_cmd(base_app, 'history he')
+ expected = normalize("""
+ 1 help
+""")
+ assert out == expected
+
+ out = run_cmd(base_app, 'history sh')
+ expected = normalize("""
+ 2 shortcuts
+""")
+ assert out == expected
+
+def test_history_script_format(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ out = run_cmd(base_app, 'history -s')
+ expected = normalize("""
+help
+shortcuts
+""")
+ assert out == expected
+
+def test_history_with_string_argument(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ run_cmd(base_app, 'help history')
+ out = run_cmd(base_app, 'history help')
+ expected = normalize("""
+ 1 help
+ 3 help history
+""")
+ assert out == expected
+
+
+def test_history_with_integer_argument(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ out = run_cmd(base_app, 'history 1')
+ expected = normalize("""
+ 1 help
+""")
+ assert out == expected
+
+
+def test_history_with_integer_span(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ run_cmd(base_app, 'help history')
+ out = run_cmd(base_app, 'history 1..2')
+ expected = normalize("""
+ 1 help
+ 2 shortcuts
+""")
+ assert out == expected
+
+def test_history_with_span_start(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ run_cmd(base_app, 'help history')
+ out = run_cmd(base_app, 'history 2:')
+ expected = normalize("""
+ 2 shortcuts
+ 3 help history
+""")
+ assert out == expected
+
+def test_history_with_span_end(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ run_cmd(base_app, 'help history')
+ out = run_cmd(base_app, 'history :2')
+ expected = normalize("""
+ 1 help
+ 2 shortcuts
+""")
+ assert out == expected
+
+def test_history_with_span_index_error(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'help history')
+ run_cmd(base_app, '!ls -hal :')
+ out = run_cmd(base_app, 'history "hal :"')
+ expected = normalize("""
+ 3 !ls -hal :
+""")
+ assert out == expected
+
+def test_history_output_file(base_app):
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'shortcuts')
+ run_cmd(base_app, 'help history')
+
+ fd, fname = tempfile.mkstemp(prefix='', suffix='.txt')
+ os.close(fd)
+ run_cmd(base_app, 'history -o "{}"'.format(fname))
+ expected = normalize('\n'.join(['help', 'shortcuts', 'help history']))
+ with open(fname) as f:
+ content = normalize(f.read())
+ assert content == expected
+
+def test_history_edit(base_app, monkeypatch):
+ # Set a fake editor just to make sure we have one. We aren't really
+ # going to call it due to the mock
+ base_app.editor = 'fooedit'
+
+ # Mock out the os.system call so we don't actually open an editor
+ m = mock.MagicMock(name='system')
+ monkeypatch.setattr("os.system", m)
+
+ # Run help command just so we have a command in history
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'history -e 1')
+
+ # We have an editor, so should expect a system call
+ m.assert_called_once()
+
+def test_history_run_all_commands(base_app):
+ # make sure we refuse to run all commands as a default
+ run_cmd(base_app, 'shortcuts')
+ out = run_cmd(base_app, 'history -r')
+ # this should generate an error, but we don't currently have a way to
+ # capture stderr in these tests. So we assume that if we got nothing on
+ # standard out, that the error occurred because if the command executed
+ # then we should have a list of shortcuts in our output
+ assert out == []
+
+def test_history_run_one_command(base_app):
+ expected = run_cmd(base_app, 'help')
+ output = run_cmd(base_app, 'history -r 1')
+ assert output == expected
+
+def test_history_clear(base_app):
+ # Add commands to history
+ run_cmd(base_app, 'help')
+ run_cmd(base_app, 'alias')
+
+ # Make sure history has items
+ out = run_cmd(base_app, 'history')
+ assert out
+
+ # Clear the history
+ run_cmd(base_app, 'history --clear')
+
+ # Make sure history is empty
+ out = run_cmd(base_app, 'history')
+ assert out == []