File: ipy_traits_completer.py

package info (click to toggle)
ipython 0.13.1-2%2Bdeb7u1
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 15,752 kB
  • sloc: python: 69,537; makefile: 355; lisp: 272; sh: 80; objc: 37
file content (219 lines) | stat: -rw-r--r-- 6,862 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
"""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'