diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2019-06-06 16:00:10 -0400 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2019-06-06 16:00:10 -0400 |
commit | a490975a030cf7a366e6436b141a7c3ff3b9b018 (patch) | |
tree | 522ba401fbec1960181222a5ec3a9aa503be3ba2 /cmd2/history.py | |
parent | c4fd5b6403651ccc89976edd4e04549471b4a23b (diff) | |
parent | 432427b46428a2b2234eab7b33bb89597f2f7957 (diff) | |
download | cmd2-git-a490975a030cf7a366e6436b141a7c3ff3b9b018.tar.gz |
Merge branch 'master' into script_refactor
Diffstat (limited to 'cmd2/history.py')
-rw-r--r-- | cmd2/history.py | 109 |
1 files changed, 76 insertions, 33 deletions
diff --git a/cmd2/history.py b/cmd2/history.py index 7cc36bfc..bbeb9199 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -7,25 +7,29 @@ import re from typing import List, Union +import attr + from . import utils from .parsing import Statement -class HistoryItem(str): +@attr.s(frozen=True) +class HistoryItem(): """Class used to represent one command in the History list""" - listformat = ' {:>4} {}\n' - ex_listformat = ' {:>4}x {}\n' + _listformat = ' {:>4} {}\n' + _ex_listformat = ' {:>4}x {}\n' - def __new__(cls, statement: Statement): - """Create a new instance of HistoryItem + statement = attr.ib(default=None, validator=attr.validators.instance_of(Statement)) + idx = attr.ib(default=None, validator=attr.validators.instance_of(int)) - 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 + def __str__(self): + """A convenient human readable representation of the history item""" + return self.statement.raw + + @property + def raw(self) -> str: + """Return the raw input from the user for this item""" + return self.statement.raw @property def expanded(self) -> str: @@ -40,22 +44,22 @@ class HistoryItem(str): :return: pretty print string version of a HistoryItem """ if verbose: - ret_str = self.listformat.format(self.idx, str(self).rstrip()) - if self != self.expanded: - ret_str += self.ex_listformat.format(self.idx, self.expanded.rstrip()) + ret_str = self._listformat.format(self.idx, self.raw) + if self.raw != self.expanded.rstrip(): + ret_str += self._ex_listformat.format(self.idx, self.expanded) else: if script: # display without entry numbers if expanded or self.statement.multiline_command: ret_str = self.expanded.rstrip() else: - ret_str = str(self) + ret_str = self.raw.rstrip() else: # display a numbered list if expanded or self.statement.multiline_command: - ret_str = self.listformat.format(self.idx, self.expanded.rstrip()) + ret_str = self._listformat.format(self.idx, self.expanded.rstrip()) else: - ret_str = self.listformat.format(self.idx, str(self).rstrip()) + ret_str = self._listformat.format(self.idx, self.raw.rstrip()) return ret_str @@ -69,8 +73,14 @@ class History(list): regex_search() - return a list of history items which match a given regex get() - return a single element of the list, using 1 based indexing span() - given a 1-based slice, return the appropriate list of history items - """ + def __init__(self, seq=()) -> None: + super().__init__(seq) + self.session_start_index = 0 + + def start_session(self) -> None: + """Start a new session, thereby setting the next index as the first index in the new session.""" + self.session_start_index = len(self) # noinspection PyMethodMayBeStatic def _zero_based_index(self, onebased: Union[int, str]) -> int: @@ -81,13 +91,17 @@ class History(list): return result def append(self, new: Statement) -> None: - """Append a HistoryItem to end of the History list + """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) + history_item = HistoryItem(new, len(self) + 1) + super().append(history_item) + + def clear(self) -> None: + """Remove all items from the History list.""" + super().clear() + self.start_session() def get(self, index: Union[int, str]) -> HistoryItem: """Get item from the History list using 1-based indexing. @@ -130,10 +144,11 @@ class History(list): # spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))?(?P<end>-?[1-9]\d*)?\s*$') - def span(self, span: str) -> List[HistoryItem]: + def span(self, span: str, include_persisted: bool = False) -> List[HistoryItem]: """Return an index or slice of the History list, :param span: string containing an index or a slice + :param include_persisted: (optional) if True, then retrieve full results including from persisted history :return: a list of HistoryItems This method can accommodate input in any of these forms: @@ -188,31 +203,43 @@ class History(list): # take a slice of the array result = self[start:] elif end is not None and sep is not None: - result = self[:end] + if include_persisted: + result = self[:end] + else: + result = self[self.session_start_index:end] elif start is not None: - # there was no separator so it's either a posative or negative integer + # there was no separator so it's either a positive or negative integer result = [self[start]] else: # we just have a separator, return the whole list - result = self[:] + if include_persisted: + result = self[:] + else: + result = self[self.session_start_index:] return result - def str_search(self, search: str) -> List[HistoryItem]: + def str_search(self, search: str, include_persisted: bool = False) -> List[HistoryItem]: """Find history items which contain a given string :param search: the string to search for + :param include_persisted: (optional) if True, then search full history including persisted history :return: a list of history items, or an empty list if the string was not found """ def isin(history_item): """filter function for string search of history""" sloppy = utils.norm_fold(search) - return sloppy in utils.norm_fold(history_item) or sloppy in utils.norm_fold(history_item.expanded) - return [item for item in self if isin(item)] + inraw = sloppy in utils.norm_fold(history_item.raw) + inexpanded = sloppy in utils.norm_fold(history_item.expanded) + return inraw or inexpanded - def regex_search(self, regex: str) -> List[HistoryItem]: + search_list = self if include_persisted else self[self.session_start_index:] + return [item for item in search_list if isin(item)] + + def regex_search(self, regex: str, include_persisted: bool = False) -> List[HistoryItem]: """Find history items which match a given regular expression :param regex: the regular expression to search for. + :param include_persisted: (optional) if True, then search full history including persisted history :return: a list of history items, or an empty list if the string was not found """ regex = regex.strip() @@ -222,5 +249,21 @@ class History(list): def isin(hi): """filter function for doing a regular expression search of history""" - return finder.search(hi) or finder.search(hi.expanded) - return [itm for itm in self if isin(itm)] + return finder.search(hi.raw) or finder.search(hi.expanded) + + search_list = self if include_persisted else self[self.session_start_index:] + return [itm for itm in search_list if isin(itm)] + + def truncate(self, max_length: int) -> None: + """Truncate the length of the history, dropping the oldest items if necessary + + :param max_length: the maximum length of the history, if negative, all history + items will be deleted + :return: nothing + """ + if max_length <= 0: + # remove all history + del self[:] + elif len(self) > max_length: + last_element = len(self) - max_length + del self[0:last_element] |