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 <tab>.
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 <hpk@trillke.net>'
__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 <type>-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<name>...) 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='<exc>%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 = '<python keyword>'
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('~<tab> 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<width/2:
strings[0]=strings[0][:width-2].ljust(width-1)
###########################################
# classes for meta control flow
# they are usually raised and catched
# to avoid ugly recursive "returns"
###########################################
class Finish:
""" Finish Event
gets thrown from different completion-methods.
"""
def __init__(self, completions):
for list in completions:
rl_fixprefix(list)
self.completions = completions
class UniqueFinish(Finish):
""" UniqueFinish event
gets thrown from different completion-methods.
"""
def __init__(self, completions):
self.completions = completions
class Error(Finish):
""" Error Event, thrown if there was an not to be
ignored error in the command line.
"""
def __init__(self, obj):
Finish.__init__(self, [['*error',str(obj)]])
#####################################################################
# classes performing introspection into the parser to eventually
# obtain a code object
#####################################################################
class TryParser:
""" Repeatedly parse substrings to find
an AST-Expression where we can deliver
a Code object on the interesting part.
"""
__magicmarker__ = '__NO_MORE_WARS__'
wordmatch = re.compile('([\w_]*).*')
import compiler,parser
def _raise_codegenerator(self, node):
""" recurse into AST-node to raise a codegenerator
for the * in the substructure 'ast.Getattr(*,MAGICATTR)'
"""
if getattr(node,'attrname',None)==self.__magicmarker__:
node = self.compiler.ast.Expression(node.expr)
node.filename = "<subtree %s>" % repr(node)
raise self.compiler.pycodegen.InteractiveCodeGenerator(node)
filter(self._raise_codegenerator,node.getChildNodes())
def find_code(self, text):
""" return code object for last valid expression OR if
not determinable, then return a 'parsability' flag.
this flag indicates if we were able to parse
a subexpression at all.
"""
debug("trying to get code for %s" % repr(text))
parseable = not text
while text:
try:
if text[-1]=='.':
parsable = self.parser.expr(text[:-1])
expr = text+self.__magicmarker__
else:
parsable = self.parser.expr(text)
expr = text+' .'+self.__magicmarker__
tree = self.compiler.pycodegen.parse(expr, 'eval') # ) # , 'eval')
debug("parsing succeeded: "+ repr(expr))
self._raise_codegenerator(tree)
raise AssertionError,'for %s unexpected AST: %s' %(repr(expr), tree)
except (SyntaxError, self.parser.ParserError):
m = self.wordmatch.match(text)
text = text[m and m.span(1)[1] or 1:]
except self.compiler.pycodegen.InteractiveCodeGenerator, ic:
return ic.getCode()
except:
import traceback
traceback.print_exc()
print "internal failure on", expr
print "send mail to hpk at codespeak net"
# m = self.wordmatch.match(text)
# text = text[m and m.span(1)[1] or 1:]
return parseable
# simple singleton
TryParser = TryParser()
class EvalItem:
""" Evaluating a line
"""
def __init__(self, config, text = '', attr = None):
""" evaluate last possible expression part in text.
attr can be a string or a filter-function
which accepts/rejects (name,obj)-bindings.
"""
self.config = config
debug("got text %s, attr %s" % (repr(text),repr(attr)))
self.text = text
if attr and type(attr) is str:
self.attrname = attr
self.func = lambda x: x[0].startswith(attr)
else:
self.attrname = ''
self.func = attr or config.viewfilter
# try finding code and evaluting it...
self.code = TryParser.find_code(text)
if inspect.iscode(self.code):
try:
self.obj = eval(self.code, vars(config.mainmodule))
except:
raise Error(sys.exc_info()[1])
else:
self.text = ''
def has_undotted_object(self):
""" see if evaluated an object does not end in '.'"""
if hasattr(self, 'obj') and self.text[-1]!='.':
return 1
def completions(self):
""" return name-object bindings matching self.attr/name.
"""
debug("completions called")
if hasattr(self,'obj') and self.text[-1]=='.':
scope = allbindings(self.obj)
prefix = commonprefix(scope.keys(), self.attrname)
if prefix and prefix!=self.attrname:
return {prefix:''} # unique-complete prefix
#else:
# return scope
elif self.attrname:
scope = globalscope(self.config.mainmodule)
else:
scope = vars(self.config.mainmodule)
debug("scopes: %s" % scope.keys()[:10])
debug("func: %s" % repr(self.func))
d = {}
for k,v in filter(self.func, scope.items()):
d[k]=v
return d
def __str__(self):
return '%s hasobj %d' %(repr(self), hasattr(self, 'obj'))
class LineEval:
""" Encapusulates Recognition of line parts and
parsing/evaluation of the relevant last part
where completions are requested on.
"""
breakchars = ',;{[%+-*/=:`'
splitrex = re.compile(r'\A(?P<base>.*?\.?\s*)?(?P<attrname>[_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(<instance>.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('<INTERNAL ERROR')
elif match == text:
return None
else:
debug("dispatching unique completion: %s" % repr(match))
if state<len(self.rl_matches):
#debug("returning match: %s" % repr(self.rl_matches[state]))
return self.rl_matches[state]
return None
def construct(self, text):
""" constructs completions for the given text.
this method is called once per-completion request
from readline. Based on the input text and the
config-object it computes the list of completions
'self.rl_strings' which 'rl_complete' dipatches one
by one to readline.
"""
# count repitions, starting from -1
# (because first <tab> 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, <tab> for regexinfo'],
self.config.string_help[:]]
elif previous == 'u':
fin = [['<open unicode string>']]
else:
fin = [['<open string>']]
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 <tab> multiple times"
if __name__ == '__main__':
setup()
syntax highlighted by Code2HTML, v. 0.9.1