ipy_traits_completer.py
219 lines
| 6.7 KiB
| text/x-python
|
PythonLexer
fperez
|
r742 | """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 | ||||
Robert Kern
|
r2558 | 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. | ||||
fperez
|
r742 | """ | ||
############################################################################# | ||||
# IPython imports | ||||
Brian Granger
|
r2205 | from IPython.core.error import TryNext | ||
from IPython.core.ipapi import get as ipget | ||||
Brian Granger
|
r2498 | from IPython.utils.dir2 import dir2 | ||
vivainio
|
r942 | try: | ||
set | ||||
except: | ||||
from sets import Set as set | ||||
fperez
|
r742 | |||
############################################################################# | ||||
# 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. | ||||
Robert Kern
|
r2558 | 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 | ||||
fperez
|
r742 | |||
############################################################################# | ||||
# Code begins | ||||
Robert Kern
|
r2558 | 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 | ||||
fperez
|
r742 | 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 | ||||
Robert Kern
|
r2558 | 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): | ||||
fperez
|
r742 | 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)] | ||||
darren.dale
|
r891 | |||
# 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('_')] | ||||
fperez
|
r742 | |||
#print '\nastart:<%r>' % attr_start # dbg | ||||
if len(attr_start)<COMPLETE_THRESHOLD: | ||||
Robert Kern
|
r2558 | attrs = list(set(attrs) - get_trait_names()) | ||
fperez
|
r742 | |||
# 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 | ||||
Robert Kern
|
r2558 | from enthought.traits.api import HasTraits | ||
fperez
|
r742 | |||
# A sorted list of the names we'll filter out | ||||
Robert Kern
|
r2558 | TNL = list(get_trait_names()) | ||
fperez
|
r742 | TNL.sort() | ||
# Make a few objects for testing | ||||
Robert Kern
|
r2558 | class TClean(HasTraits): pass | ||
fperez
|
r742 | 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' | ||||