# -*- coding: UTF-8 -*- ''' This file should contain all "helper" code for Cmd2. This includes things such as: * hook methods * ParsedString * Stubborndict * Borg * Statekeeper * History ...and so on. ''' import formatter, \ optparse, \ re, \ string.Formatter, \ os, \ sys import decorator, \ pyparsing from .parsers import * class StubbornDict(dict): ''' Dictionary that tolerates many input formats. Create it with stubbornDict(arg) factory function. >>> d = StubbornDict(large='gross', small='klein') >>> sorted(d.items()) [('large', 'gross'), ('small', 'klein')] >>> d.append(['plain', ' plaid']) >>> sorted(d.items()) [('large', 'gross'), ('plaid', ''), ('plain', ''), ('small', 'klein')] >>> d += ' girl Frauelein, Maedchen\\n\\n shoe schuh' >>> sorted(d.items()) [('girl', 'Frauelein, Maedchen'), ('large', 'gross'), ('plaid', ''), ('plain', ''), ('shoe', 'schuh'), ('small', 'klein')] ''' def update(self, arg): dict.update(self, StubbornDict.to_dict(arg)) append = update def __iadd__(self, arg): self.update(arg) return self def __add__(self, arg): selfcopy = copy.copy(self) selfcopy.update(stubbornDict(arg)) return selfcopy def __radd__(self, arg): selfcopy = copy.copy(self) selfcopy.update(stubbornDict(arg)) return selfcopy @classmethod def to_dict(cls, arg): 'Generates dictionary from string or list of strings' if hasattr(arg, 'splitlines'): arg = arg.splitlines() if hasattr(arg, '__reversed__'): result = {} for a in arg: a = a.strip() if a: key_val = a.split(None, 1) key = key_val[0] if len(key_val) > 1: val = key_val[1] else: val = '' result[key] = val else: result = arg return result # @TODO # Why is this defined outside of the StubbornDict class? # # I suspect it's something obvious I just haven't yet learned # in my Python n00b13-ism. # def stubbornDict(*arg, **kwarg): # @FIXME # Add docstring describing this method's # purpose and relation to the StubbornDict class ''' >>> sorted(stubbornDict('cow a bovine\\nhorse an equine').items()) [('cow', 'a bovine'), ('horse', 'an equine')] >>> sorted(stubbornDict(['badger', 'porcupine a poky creature']).items()) [('badger', ''), ('porcupine', 'a poky creature')] >>> sorted(stubbornDict(turtle='has shell', frog='jumpy').items()) [('frog', 'jumpy'), ('turtle', 'has shell')] ''' result = {} for a in arg: result.update(StubbornDict.to_dict(a)) result.update(kwarg) return StubbornDict(result) class HistoryItem(str): listformat = '-------------------------[%d]\n%s\n' def __init__(self, instr): str.__init__(self) self.lowercase = self.lower() self.idx = None def pr(self): return self.listformat.format((self.idx, str(self))) class History(list): ''' A list of HistoryItems that knows how to respond to user requests. >>> h = History([HistoryItem('first'), HistoryItem('second'), HistoryItem('third'), HistoryItem('fourth')]) >>> h.span('-2..') ['third', 'fourth'] >>> h.span('2..3') ['second', 'third'] >>> h.span('3') ['third'] >>> h.span(':') ['first', 'second', 'third', 'fourth'] >>> h.span('2..') ['second', 'third', 'fourth'] >>> h.span('-1') ['fourth'] >>> h.span('-2..-3') ['third', 'second'] >>> h.search('o') ['second', 'fourth'] >>> h.search('/IR/') ['first', 'third'] ''' def zero_based_index(self, onebased): result = onebased if result > 0: result -= 1 return result def to_index(self, raw): if raw: result = self.zero_based_index(int(raw)) else: result = None return result def search(self, target): target = target.strip() if target[0] == target[-1] == '/' and len(target) > 1: target = target[1:-1] else: target = re.escape(target) pattern = re.compile(target, re.IGNORECASE) return [s for s in self if pattern.search(s)] spanpattern = re.compile(r'^\s*(?P\-?\d+)?\s*(?P:|(\.{2,}))?\s*(?P\-?\d+)?\s*$') def span(self, raw): 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')) 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[\d]+)?\s*\-\s*(?P[\d]+)?\s*$') def append(self, new): new = HistoryItem(new) list.append(self, new) new.idx = len(self) def extend(self, new): for n in new: self.append(n) def get(self, getme=None, fromEnd=False): 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: rangeResult = self.rangePattern.search(getme) if rangeResult: start = rangeResult.group('start') or None end = rangeResult.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): return finder.search(hi) else: def isin(hi): return (getme.lower() in hi.lowercase) return [itm for itm in self if isin(itm)] class Statekeeper(object): def __init__(self, obj, attribs): self.obj = obj self.attribs = attribs if self.obj: self.save() def save(self): for attrib in self.attribs: setattr(self, attrib, getattr(self.obj, attrib)) def restore(self): if self.obj: for attrib in self.attribs: setattr(self.obj, attrib, getattr(self, attrib)) class Borg(object): ''' All instances of any Borg subclass will share state. (Python Cookbook, 2nd Ed., recipe 6.16) ''' _shared_state = {} def __new__(cls, *a, **k): obj = object.__new__(cls, *a, **k) obj.__dict__ = cls._shared_state return obj class OutputTrap(Borg): ''' Instantiate an OutputTrap to divert/capture ALL stdout output. For use in unit testing. Call `tearDown()` to return to normal output. ''' def __init__(self): self.contents = '' self.old_stdout = sys.stdout sys.stdout = self def write(self, txt): self.contents += txt def read(self): result = self.contents self.contents = '' return result def tearDown(self): sys.stdout = self.old_stdout self.contents = '' @decorator def options(option_list, arg_desc="arg"): ''' Used as a decorator and passed a list of optparse-style options, alters a cmd2 method to populate its ``opts`` argument from its raw text argument. EXAMPLE: Change this... def do_something(self, arg): ...into this: @options([make_option('-q', '--quick', action="store_true", help="Makes things fast")], "source dest") def do_something(self, arg, opts): if opts.quick: self.fast_button = True ''' if not isinstance(option_list, list): option_list = [option_list] for opt in option_list: options_defined.append(pyparsing.Literal(opt.get_opt_string())) def option_setup(func): optionParser = OptionParser() for opt in option_list: optionParser.add_option(opt) optionParser.set_usage("{} [options] {}".format(func.__name__[3:], arg_desc)) optionParser._func = func def new_func(instance, arg): try: opts, newArgList = optionParser.parse_args(arg.split()) # Must find the remaining args in the original argument list, but # mustn't include the command itself #if hasattr(arg, 'parsed') and newArgList[0] == arg.parsed.command: # newArgList = newArgList[1:] newArgs = remaining_args(arg, newArgList) if isinstance(arg, ParsedString): arg = arg.with_args_replaced(newArgs) else: arg = newArgs except (optparse.OptParseError) as e: print(e) optionParser.print_help() return if hasattr(opts, '_exit'): return None result = func(instance, arg, opts) return result new_func.__doc__ = "{}\n{}".format(func.__doc__, optionParser.format_help()) return new_func return option_setup def cast(current, new): '''Tries to force a new value into the same type as the current.''' typ = type(current) if typ == bool: try: return bool(int(new)) except (ValueError, TypeError): pass try: new = new.lower() except: pass if (new=='on') or (new[0] in ('y','t')): return True if (new=='off') or (new[0] in ('n','f')): return False else: try: return typ(new) except: pass print ("Problem setting parameter (now {}) to {}; incorrect type?".format(current, new)) return current def replace_with_file_contents(fname): if fname: try: result = open(os.path.expanduser(fname[0])).read() except IOError: result = '< {}'.format(fname[0]) # wasn't a file after all else: result = get_paste_buffer() return result def ljust(x, width, fillchar=' '): '''Like str.ljust(), but for lists.''' if hasattr(x, 'ljust'): return x.ljust(width, fillchar) else: if len(x) < width: x = (x + [fillchar] * width)[:width] return x