diff options
author | kotfu <kotfu@kotfu.net> | 2019-02-09 13:43:41 -0700 |
---|---|---|
committer | kotfu <kotfu@kotfu.net> | 2019-02-09 13:43:41 -0700 |
commit | 321a8c72a48d227011177bb91006ed20607a1e44 (patch) | |
tree | c69b95e823cb897d45ee04e3eb8b02f123181122 | |
parent | 80327e0a7f21424554ade6626be0798ce6392a1d (diff) | |
download | cmd2-git-321a8c72a48d227011177bb91006ed20607a1e44.tar.gz |
Extract history classes and test into their own files
-rw-r--r-- | cmd2/cmd2.py | 141 | ||||
-rw-r--r-- | cmd2/history.py | 151 | ||||
-rw-r--r-- | tests/test_cmd2.py | 190 | ||||
-rw-r--r-- | tests/test_history.py | 209 |
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 == [] |