##// END OF EJS Templates
Multiple improvements to tab completion....
Multiple improvements to tab completion. I refactored the API quite a bit, to retain readline compatibility but make it more independent of readline. There's still more to do in cleaning up our init_readline() method, but now the completer objects have separate rlcomplete() and complete() methods. The former uses the quirky readline API with a state flag, while the latter is stateless, takes only text information, and is more suitable for GUIs and other frontends to call programatically. Made other minor fixes to ensure the test suite passes in full. While all this code is a bit messy, we're getting in the direction of the APIs we need in the long run.

File last commit:

r2558:18cd5de2
r2839:8cff4913
Show More
ipy_traits_completer.py
219 lines | 6.7 KiB | text/x-python | PythonLexer
/ IPython / deathrow / ipy_traits_completer.py
"""Traits-aware tab completion.
This module provides a custom tab-completer that intelligently hides the names
that the enthought.traits library (http://code.enthought.com/traits)
automatically adds to all objects that inherit from its base HasTraits class.
Activation
==========
To use this, put in your ~/.ipython/ipy_user_conf.py file:
from ipy_traits_completer import activate
activate([complete_threshold])
The optional complete_threshold argument is the minimal length of text you need
to type for tab-completion to list names that are automatically generated by
traits. The default value is 3. Note that at runtime, you can change this
value simply by doing:
import ipy_traits_completer
ipy_traits_completer.COMPLETE_THRESHOLD = 4
Usage
=====
The system works as follows. If t is an empty object that HasTraits, then
(assuming the threshold is at the default value of 3):
In [7]: t.ed<TAB>
doesn't show anything at all, but:
In [7]: t.edi<TAB>
t.edit_traits t.editable_traits
shows these two names that come from traits. This allows you to complete on
the traits-specific names by typing at least 3 letters from them (or whatever
you set your threshold to), but to otherwise not see them in normal completion.
Notes
=====
- This requires Python 2.4 to work (I use sets). I don't think anyone is
using traits with 2.3 anyway, so that's OK.
- Imports from enthought.traits are deferred until an object with a class that
looks like it subclasses from HasTraits comes along. This test is done by
looking at the name of the class and its superclasses.
"""
#############################################################################
# IPython imports
from IPython.core.error import TryNext
from IPython.core.ipapi import get as ipget
from IPython.utils.dir2 import dir2
try:
set
except:
from sets import Set as set
#############################################################################
# Module constants
# The completion threshold
# This is currently implemented as a module global, since this sytem isn't
# likely to be modified at runtime by multiple instances. If needed in the
# future, we can always make it local to the completer as a function attribute.
COMPLETE_THRESHOLD = 3
# Set of names that Traits automatically adds to ANY traits-inheriting object.
# These are the names we'll filter out.
TRAIT_NAMES = None
def get_trait_names():
global TRAIT_NAMES
from enthought.traits.api import HasTraits
if TRAIT_NAMES is None:
TRAIT_NAMES = set( dir2(HasTraits()) ) - set( dir2(object()) )
else:
return TRAIT_NAMES
#############################################################################
# Code begins
def looks_like_isinstance(obj, classname):
""" Return True if the object has a class or superclass with the given class
name.
Ignores old-style classes.
"""
from types import InstanceType
t = type(obj)
if t is InstanceType:
# Old-style classes.
return False
elif t.__name__ == classname:
return True
for klass in t.__mro__:
if klass.__name__ == classname:
return True
return False
def trait_completer(self,event):
"""A custom IPython tab-completer that is traits-aware.
It tries to hide the internal traits attributes, and reveal them only when
it can reasonably guess that the user really is after one of them.
"""
#print '\nevent is:',event # dbg
symbol_parts = event.symbol.split('.')
base = '.'.join(symbol_parts[:-1])
#print 'base:',base # dbg
oinfo = self._ofind(base)
if not oinfo['found']:
raise TryNext
obj = oinfo['obj']
# OK, we got the object. See if it's traits, else punt
if not looks_like_isinstance(obj, 'HasTraits'):
raise TryNext
# Defer import until here so as not to require Traits until we get something
# that looks like it might be a HasTraits instance.
from enthought.traits.api import HasTraits
if not isinstance(obj, HasTraits):
raise TryNext
# it's a traits object, don't show the tr* attributes unless the completion
# begins with 'tr'
attrs = dir2(obj)
# Now, filter out the attributes that start with the user's request
attr_start = symbol_parts[-1]
if attr_start:
attrs = [a for a in attrs if a.startswith(attr_start)]
# Let's also respect the user's readline_omit__names setting:
omit__names = ipget().options.readline_omit__names
if omit__names == 1:
attrs = [a for a in attrs if not a.startswith('__')]
elif omit__names == 2:
attrs = [a for a in attrs if not a.startswith('_')]
#print '\nastart:<%r>' % attr_start # dbg
if len(attr_start)<COMPLETE_THRESHOLD:
attrs = list(set(attrs) - get_trait_names())
# The base of the completion, so we can form the final results list
bdot = base+'.'
tcomp = [bdot+a for a in attrs]
#print 'tcomp:',tcomp
return tcomp
def activate(complete_threshold = COMPLETE_THRESHOLD):
"""Activate the Traits completer.
:Keywords:
complete_threshold : int
The minimum number of letters that a user must type in order to
activate completion of traits-private names."""
if not (isinstance(complete_threshold,int) and
complete_threshold>0):
e='complete_threshold must be a positive integer, not %r' % \
complete_threshold
raise ValueError(e)
# Set the module global
global COMPLETE_THRESHOLD
COMPLETE_THRESHOLD = complete_threshold
# Activate the traits aware completer
ip = ipget()
ip.set_hook('complete_command', trait_completer, re_key = '.*')
#############################################################################
if __name__ == '__main__':
# Testing/debugging
from enthought.traits.api import HasTraits
# A sorted list of the names we'll filter out
TNL = list(get_trait_names())
TNL.sort()
# Make a few objects for testing
class TClean(HasTraits): pass
class Bunch(object): pass
# A clean traits object
t = TClean()
# A nested object containing t
f = Bunch()
f.t = t
# And a naked new-style object
o = object()
ip = ipget().IP
# A few simplistic tests
# Reset the threshold to the default, in case the test is running inside an
# instance of ipython that changed it
import ipy_traits_completer
ipy_traits_completer.COMPLETE_THRESHOLD = 3
assert ip.complete('t.ed') ==[]
# For some bizarre reason, these fail on the first time I run them, but not
# afterwards. Traits does some really weird stuff at object instantiation
# time...
ta = ip.complete('t.edi')
assert ta == ['t.edit_traits', 't.editable_traits']
print 'Tests OK'