r"""Python interactive completion using GNU readline 2.1. $Id: rlcompleter2.py,v 1.1 2003/01/29 17:20:35 hpk Exp $ licensed under the Python Softare License (c) 2003-2005 Holger Krekel, hpk at codespeak net how the interactive completer works ------------------------------------- The Completer works as a callback object for the readline library. It doesn't just give you completions on much more expressions than the stdlib-rlcompleter. It also gives you incrementally more introspective information the more often you hit . The completer aims to be very intuitive and helpful. You can customize behaviour via the Config class which is instantiated per Completer (to enable it to be multiply embedded with e.g. IPython) Feel free to suggest better default behaviour or configuration options! """ __version__='0.96' __author__ ='holger krekel ' __url__='http://codespeak.net/rlcompleter2/' import os, sys, re, inspect import __builtin__, keyword # debug turned off for releases if 0: debugfile = open('/tmp/rlcompleter2.log','w') def debug(obj): debugfile.write(str(obj).strip()+'\n') debugfile.flush() else: debug = lambda obj: None class Config: """ run-time changeable configuration parameters and functions. An instance of this class can be modified and passed to the Completer constructor. """ # number of characters for -abbrevs. set 0 to omit type info. typesize = 3 # num of lines of completions/documentation chunks # set to 0 for using terminalheight (see below), # -1 for showing *all* lines in one go # >0 for the number of lines to show with each chunk spliteach = 0 # separator line when showing docstring + signatures separator_line = '-'*78 # show source code for functions showfuncsource = 1 # evaluation and completion of expressions happens in ... try: mainmodule = __import__('__main__') except ImportError: mainmodule = None def viewfilter(self, (name, obj)): """ return true if the given name, object combination should be included in the visible completion-list. """ if name.startswith('_'): return 0 return 1 def __init__(self, formatter=None): self.formatter = formatter or Formatter(self) try: import termios,fcntl,struct call = fcntl.ioctl(0,termios.TIOCGWINSZ,"\000"*8) height,width = struct.unpack( "hhhh", call ) [:2] self.terminalwidth = width self.terminalheight = height except: self.terminalwidth = 80 self.terminalheight = 30 string_help = r"""*regular expressions \A start of string \Z end of string \b-\B empty str beg/end of word-INVERSE \d-\D decimal digit-INVERSE \s-\S whitespace[ \t\n\r\f\v]-INVERSE \w-\W alphanumeric-INVERSE \\ backslash . any character ^ start of string $ end of string * zero or more reps + one or more reps *?,+?,?? non-greedy versions {m,n} m to n reps {m,n}? non-greedy m to n reps [] set of characters (^ inverses) | A|B matches A or B (...) RE inside parens (?iLmsux) set flag for RE (?:...) non-grouping parens (?P...) named group (?P = name) same as before named group (?#...) comment(ignored) (?=...) if ... next, nonconsuming (?!...) if ... doesnt match next """.strip().split('\n') class Formatter: """ Lots of hooks for showing non-unqiue completions. (unique completions are handled by the Completer itself). """ import types def __init__(self, config): self.config = config # construct type abbreviation list self.abbdict={} if config.typesize: t=type tlist = filter(lambda x: type(x) is type, vars(self.types).values()) for typ in tlist: self.abbdict[typ]=typ.__name__.split()[-1][:config.typesize].lower() debug("formatted types: %s" % self.abbdict) """ Formatting functions. """ def TypeView(self, name, obj): """ return a Type-specific view which is never used for unqiue (final) completion. """ if not keyword.iskeyword(name) and self.config.typesize: if inspect.isclass(obj) and issubclass(obj, Exception): name='%s' % name elif type(obj) in self.abbdict: name='<%s>%s' % (self.abbdict[type(obj)], name) if callable(obj): name=name+format_callable_signature(obj) return name def TypeCompletion(self, name, obj): """ determines natural completion characters for the given obj. For an appropriate definition of 'natural' :-) """ if callable(obj): if format_callable_signature(obj).endswith('()'): return "()" return "(" if inspect.ismodule(obj): return '.' if keyword.iskeyword(name): return ' ' return '' def rl_many(self, matches): """ return list of list of completion strings. [[string0a,string0b,...],[string1a,...]] """ debug("rl_many:"+str(matches.keys()[:10])+'...') keywidth = 8+max(map(len, matches.keys()) or [0]) subfin = [[],[]] for name,obj in matches.items(): subfin[0].append(self.TypeView(name, obj)) subfin[1].extend(self.doculines(name, obj, keywidth)) lines = subfin.pop(1) subfin.extend(linesplit(lines,self.config)) for i in range(1,len(subfin)): if subfin[i]: rl_fixlines(subfin[i], self.config.terminalwidth) else: del subfin[i] return subfin condense_rex = re.compile('\s*\n\s*',re.MULTILINE) def doculines(self, name, obj, keywidth): """ show documentation for one match trimmed to a few lines (currently one). """ if keyword.iskeyword(name): objdoc = '' else: objdoc = docstring(obj) if not objdoc: objdoc = ': %s, type: %s' % (obj,type(obj)) objdoc = self.condense_rex.sub('. ',objdoc.strip()) # cut out part of c-signature in doctring try: inspect.getargspec(obj) except TypeError: i = objdoc.find('->') if i!=-1: objdoc = objdoc[i:] namedoc = self.TypeView(name,obj) namedoc = namedoc.ljust(keywidth)+' ' line = namedoc + objdoc width = self.config.terminalwidth-4 return [line[:width-1].ljust(width-1)] def fulldoc(self, obj): """ return list of list of documentation strings. """ debug("fulldoc for %s" % repr(obj)) lines = [self.config.separator_line] if callable(obj): header = get_callable_name(obj) sig = format_callable_signature(obj, justnum=0) if sig: lines.append(header + sig) else: lines.append(repr(obj) + ' ' + repr(type(obj))) lines.append(self.config.separator_line) doc = docstring(obj) or '*' lines.extend(doc.strip().split('\n')) if isfunc(obj) and self.config.showfuncsource: try: source = inspect.getsource(obj) lines.append(self.config.separator_line) lines.extend(source.split('\n')) except IOError: pass rl_fixlines(lines, self.config.terminalwidth-4) rl_fixorder(lines) l = linesplit(lines, self.config) return l ########################################### # support functions # ########################################### def isfunc(obj): if callable(obj): if hasattr(obj, 'func_code') or hasattr(obj, 'im_func'): return 1 def get_callable_name(obj): assert callable(obj) if inspect.isclass(obj): return obj.__name__ try: return obj.im_class.__name__ + '.' + obj.im_func.func_name except AttributeError: try: return obj.func_name except AttributeError: return '' def format_callable_signature(obj, justnum=1): """return signature for any callable (including classes)""" assert callable(obj) if inspect.isclass(obj): obj = getattr(obj, '__init__', lambda: None) try: func = obj.im_func delta=1 except AttributeError: func = obj delta=0 try: args, vargs, kargs, defs = inspect.getargspec(func) if not justnum: return inspect.formatargspec(args, vargs, kargs, defs) arglen = (args and len(args)-delta) or 0 if defs: sig='(%d-%d' % (arglen-len(defs), arglen) elif arglen: sig='(%d' % arglen else: sig='(' if vargs: sig+= sig[-1]!='(' and ',*' or '*' if kargs: sig+= sig[-1]!='(' and ',**' or '**' sig+=')' return sig except TypeError: if justnum: return format_callable_c_signature(obj) return '' funcrex = re.compile('\w+\(([^\[\)]*)(\[.*?\])?\).*(-|=>)?\s*(\S*)') wordrex = re.compile('\w+') def format_callable_c_signature(obj, long = 0): """ heuristically get a C-function's signature """ assert callable(obj) doc = docstring(obj) if not doc: return '(?)' else: # look for typical doc c-signature m = funcrex.search(doc[:200]) if not m: return '*(?)' else: args = m.group(1) or '' dargs = m.group(2) or '' args = len(wordrex.findall(args)) dargs = len(wordrex.findall(dargs)) if dargs: return '*(%d-%d)' % (args,args+dargs) if not args: return '*()' return '*(%d)' % args def allbindings(obj): """ return dict with all attrname/obj bindings for a given obj. note that using the builtin vars() would not give the attributes of base classes whereas calling 'dir()' gives the names of inherited attributes. """ d = {} for name in dir(obj): try: d[name]=getattr(obj, name) except: pass return d def globalscope(module): """return (interactive) global scope for a module.""" scope = vars(__builtin__) scope.update(vars(module)) scope.update(keyword.kwdict) return scope def docstring(obj): """ return un-inherited doc-attribute of obj. (i.e. we detect inheritance from its type). XXX: this should eventually go into the inspect module! """ if getattr(type(obj),'__doc__','')!=getattr(obj,'__doc__',''): return inspect.getdoc(obj) def commonprefix(names, base = ''): """ return the common prefix of all 'names' starting with 'base' """ def commonfunc(s1,s2): while not s2.startswith(s1): s1=s1[:-1] return s1 if base: names = filter(lambda x, base=base: x.startswith(base), names) if not names: return '' return reduce(commonfunc,names) def linesplit(lines, config): """ return a list of sublists where each sublist is not longer than maxi lists. """ maxi = config.spliteach if maxi == -1: return [lines] elif maxi == 0: maxi = config.terminalheight-3 splitlist = [] for i in range(1+(len(lines)/maxi)): if i!=0: stilltogo = len(lines)-i*maxi splitlist[i-1].append('~ for remaining %d lines' % stilltogo) splitlist.append(lines[i*maxi:(i+1)*maxi]) debug("returning splitlist, size = %s" % len(splitlist)) #debug("first item in splitlist: %s" % splitlist[0]) return splitlist ########################################### # Fixing some bad readline behaviour # ########################################### def rl_fixorder(strings): """ fix implicit sorting of readline. if the strings are not accidentally sorted then fix the current 'strings' order. """ num = len(strings) for s1,s2 in zip(strings, strings[1:]): if s1 >= s2: digits = num<=10 and '%d ' or '%02d ' for i in range(num): strings[i]=digits % i + strings[i] break def rl_fixprefix(strings): """ avoid completion on a common prefix. """ if len(strings)==0: strings[:] = list('*?') else: if commonprefix(strings): strings.append('*') # destroys common prefix def rl_fixlines(strings, width): """ avoids multicolumns so that effectively each string in strings will be shown on its own line. """ l = reduce(max,map(len, strings or [])) if l.*?\.?\s*)?(?P[_a-zA-Z][\w_]*)?\Z') def __init__(self, text, config): self.text = text self.config = config for k,v in self.splitrex.match(text).groupdict('').items(): setattr(self,k,v) debug(" %s = %s" % (k,v)) lastchar = self.base[-1:] debug('lastchar: %s' % lastchar) if lastchar == '(': evalitems = [ EvalItem(config, self.base[:-1]), EvalItem(config, '', self.attrname), ] elif not lastchar or lastchar in self.breakchars: evalitems = [EvalItem(config, '', self.attrname)] elif lastchar == '.': evalitems = [ EvalItem(config, self.base, self.attrname), EvalItem(config, self.base[:-1]), ] #if inspect.ismodule(getattr(evalitems[0], 'obj')): # evalitems.reverse() else: evalitems = [EvalItem(config, self.base, self.attrname)] #if len(evalitems[0].completions())==1: # self.evalitems = [evalitems.pop(0)] #else: self.evalitems = evalitems def __str__(self): """ for debuggging purposes""" return 'matches: %s' % map(str, self.evalitems) class Completer: """ Class providing completions for readline's requests """ def __init__(self, config = None): """ Return a completer object whose 'rl_complete' method is suitable for use by the readline library via readline.set_completer(.rl_complete). """ self.config = config or Config() def rl_complete(self, text, state): """handle readline's complete requests readline calls consecutively with state == 0, 1, 2, ... until we return None. """ if state == 0: try: self.construct(text) except: self.rl_matches=['!!', 'Internal Error'] if len(self.rl_matches)==1: match = self.rl_matches[0] if match and not match.startswith(text): debug ("ERROR: catching wrong completion %s" % repr(match)) self.rl_matches.append(' is usually swallowed by readline) if text == getattr(self, 'text_last', None): debug("self.repeat = %d"%self.repeated) self.repeated+=1 else: self.repeated = 0 # -1 self.text_last = text self.completions = [[]] #print "constructing completions, repeat=%d" % self.repeated try: self.method_tokenize(text) self.method_eval(text) raise Error('nothing found, empty namespace?') except Finish, fin: self.completions = fin.completions if fin.__class__ == Error: self.text_last = None #debug("completions[:10]: %s" % str(self.completions[:10])) mod = self.repeated % len(self.completions) self.rl_matches = self.completions[mod] #debug("finish: %s" % str(self.rl_matches)) def method_tokenize(self, text): """ return true if tokenization information lets us shortcut completions such as returning error/string-information. this method basically checks if we are inside a raw/string definition. XXX: Could we incorporate trying filename-completions here? """ import token, tokenize class TokenEater: """ Token Receiver function (as class-instance) """ def __init__(self,text,config): self.text = text self.config = config def __call__(self, ttype, ttoken, (srow,scol), (erow,ecol),line): #debug("got token: %s" % repr(ttoken)) self.context = '%s' % (line[max(0,scol-1):scol+4]) if ttype == token.ERRORTOKEN: #debug("found error token: %s" % repr(ttoken)) if ttoken.strip(): if ttoken in '"\'': self.handle_open_string(line[:scol].rstrip()[-1:]) else: raise Error('error at %s' % self.context) def handle_open_string(self, previous): """ raise help completion for open strings """ #debug("previousstrip: "+repr(previous)) if previous == 'r': fin = [['in rawstring, for regexinfo'], self.config.string_help[:]] elif previous == 'u': fin = [['']] else: fin = [['']] raise Finish(fin) try: eater = TokenEater(text, self.config) tokenize.tokenize(['',text].pop, eater) except tokenize.TokenError, ex: debug("ex:%s" % ex) msg = ex.args[0] if msg[:3]=='EOF'and msg[-6:]=='string': eater.handle_open_string(text[ex.args[1][1]]) def method_eval(self, text): """ Partially parse and evaluate a single cmdline-text.""" debug("method eval %s"% repr(text)) line = LineEval(text, self.config) debug(line) fin = [] for evalitem in line.evalitems: if evalitem.has_undotted_object(): fin.extend(self.config.formatter.fulldoc(evalitem.obj)) continue matches = evalitem.completions() attrname = evalitem.attrname #debug("matches: %s " % matches.keys()) if len(matches)==0: if attrname: raise Error('no match found for name: %s' % repr(attrname)) else: continue raise Error('sorry. dunno, quite empty here, eh ...') elif len(matches)==1: name, obj = matches.popitem() if fin: fin.extend(self.config.formatter.fulldoc(obj)) else: debug("exact match, name %s, attrname %s" % (name,attrname)) if attrname == name: debug("getting typecompletion for %s"% repr(obj)) x = self.config.formatter.TypeCompletion(name,obj) if x: fin.append([text+x]) raise UniqueFinish(fin) else: fin.extend(self.config.formatter.fulldoc(obj)) else: fin.append([text+name[len(attrname):]]) raise UniqueFinish(fin) break else: fin.extend(self.config.formatter.rl_many(matches)) if fin: raise Finish(fin) def setup_readline_history(histfn): import readline try: readline.read_history_file(histfn) except IOError: # guess it doesn't exist pass def save(): try: readline.write_history_file(histfn) except IOError: print "bad luck, couldn't save readline history to", histfn import atexit atexit.register(save) def setup(histfn=os.getenv('HOME')+'/.pythonhist', verbose=1): import readline completer = Completer() readline.set_completer_delims('') readline.set_completer(completer.rl_complete) readline.parse_and_bind('tab: complete') if histfn: setup_readline_history(histfn) if verbose: print "Welcome to rlcompleter2 %(__version__)s" % globals() print "for nice experiences hit multiple times" if __name__ == '__main__': setup()