diff --git a/IPython/Extensions/ipy_traits_completer.py b/IPython/Extensions/ipy_traits_completer.py new file mode 100644 index 0000000..7180e29 --- /dev/null +++ b/IPython/Extensions/ipy_traits_completer.py @@ -0,0 +1,173 @@ +"""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 + +doesn't show anything at all, but: + +In [7]: t.edi +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. +""" + +############################################################################# +# External imports +from enthought.traits import api as T + +# IPython imports +from IPython.ipapi import TryNext, get as ipget +from IPython.genutils import dir2 + +############################################################################# +# 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 = set( dir2(T.HasTraits()) ) - set( dir2(object()) ) + +############################################################################# +# Code begins + +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 isinstance(obj,T.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)] + + #print '\nastart:<%r>' % attr_start # dbg + + if len(attr_start)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 + + # A sorted list of the names we'll filter out + TNL = list(TRAIT_NAMES) + TNL.sort() + + # Make a few objects for testing + class TClean(T.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' diff --git a/IPython/OInspect.py b/IPython/OInspect.py index a594cdc..0be68eb 100644 --- a/IPython/OInspect.py +++ b/IPython/OInspect.py @@ -6,7 +6,7 @@ Uses syntax highlighting for presenting the various information elements. Similar in spirit to the inspect module, but all calls take a name argument to reference the name under which an object is being read. -$Id: OInspect.py 2558 2007-07-25 19:54:28Z fperez $ +$Id: OInspect.py 2568 2007-07-29 21:38:44Z fperez $ """ #***************************************************************************** @@ -468,8 +468,9 @@ class Inspector: if ds: class_ds = getdoc(obj.__class__) # Skip Python's auto-generated docstrings - if class_ds.startswith('function(code, globals[, name[,') or \ - class_ds.startswith('instancemethod(function, instance,'): + if class_ds and \ + (class_ds.startswith('function(code, globals[,') or \ + class_ds.startswith('instancemethod(function, instance,')): class_ds = None if class_ds and ds != class_ds: out.writeln(header('Class Docstring:\n') + @@ -530,6 +531,8 @@ class Inspector: - show_all(False): show all names, including those starting with underscores. """ + #print 'ps pattern:<%r>' % pattern # dbg + # defaults type_pattern = 'all' filter = '' diff --git a/IPython/completer.py b/IPython/completer.py index 2544b04..30885ac 100644 --- a/IPython/completer.py +++ b/IPython/completer.py @@ -84,17 +84,10 @@ try: except NameError: from sets import Set as set -from IPython.genutils import debugx +from IPython.genutils import debugx, dir2 __all__ = ['Completer','IPCompleter'] -def get_class_members(cls): - ret = dir(cls) - if hasattr(cls,'__bases__'): - for base in cls.__bases__: - ret.extend(get_class_members(base)) - return ret - class Completer: def __init__(self,namespace=None,global_namespace=None): """Create a new completer for the command line. @@ -199,55 +192,15 @@ class Completer: expr, attr = m.group(1, 3) try: - object = eval(expr, self.namespace) + obj = eval(expr, self.namespace) except: try: - object = eval(expr, self.global_namespace) + obj = eval(expr, self.global_namespace) except: return [] - - - # Start building the attribute list via dir(), and then complete it - # with a few extra special-purpose calls. - words = dir(object) - - if hasattr(object,'__class__'): - words.append('__class__') - words.extend(get_class_members(object.__class__)) - - # Some libraries (such as traits) may introduce duplicates, we want to - # track and clean this up if it happens - may_have_dupes = False - - # this is the 'dir' function for objects with Enthought's traits - if hasattr(object, 'trait_names'): - try: - words.extend(object.trait_names()) - may_have_dupes = True - except TypeError: - # This will happen if `object` is a class and not an instance. - pass - - # Support for PyCrust-style _getAttributeNames magic method. - if hasattr(object, '_getAttributeNames'): - try: - words.extend(object._getAttributeNames()) - may_have_dupes = True - except TypeError: - # `object` is a class and not an instance. Ignore - # this error. - pass - if may_have_dupes: - # eliminate possible duplicates, as some traits may also - # appear as normal attributes in the dir() call. - words = list(set(words)) - words.sort() + words = dir2(obj) - # filter out non-string attributes which may be stuffed by dir() calls - # and poor coding in third-party modules - words = [w for w in words - if isinstance(w, basestring) and w != "__builtins__"] # Build match list to return n = len(attr) return ["%s.%s" % (expr, w) for w in words if w[:n] == attr ] @@ -566,7 +519,7 @@ class IPCompleter(Completer): return argMatches def dispatch_custom_completer(self,text): - # print "Custom! '%s' %s" % (text, self.custom_completers) # dbg + #print "Custom! '%s' %s" % (text, self.custom_completers) # dbg line = self.full_lbuf if not line.strip(): return None @@ -590,7 +543,7 @@ class IPCompleter(Completer): self.custom_completers.s_matches(cmd), try_magic, self.custom_completers.flat_matches(self.lbuf)): - # print "try",c # dbg + #print "try",c # dbg try: res = c(event) return [r for r in res if r.lower().startswith(text.lower())] diff --git a/IPython/genutils.py b/IPython/genutils.py index fffc976..e7d8b02 100644 --- a/IPython/genutils.py +++ b/IPython/genutils.py @@ -5,7 +5,7 @@ General purpose utilities. This is a grab-bag of stuff I find useful in most programs I write. Some of these things are also convenient when working at the command line. -$Id: genutils.py 2439 2007-06-14 18:41:48Z vivainio $""" +$Id: genutils.py 2568 2007-07-29 21:38:44Z fperez $""" #***************************************************************************** # Copyright (C) 2001-2006 Fernando Perez. @@ -1742,6 +1742,70 @@ def map_method(method,object_list,*argseq,**kw): return out_list #---------------------------------------------------------------------------- +def get_class_members(cls): + ret = dir(cls) + if hasattr(cls,'__bases__'): + for base in cls.__bases__: + ret.extend(get_class_members(base)) + return ret + +#---------------------------------------------------------------------------- +def dir2(obj): + """dir2(obj) -> list of strings + + Extended version of the Python builtin dir(), which does a few extra + checks, and supports common objects with unusual internals that confuse + dir(), such as Traits and PyCrust. + + This version is guaranteed to return only a list of true strings, whereas + dir() returns anything that objects inject into themselves, even if they + are later not really valid for attribute access (many extension libraries + have such bugs). + """ + + # Start building the attribute list via dir(), and then complete it + # with a few extra special-purpose calls. + words = dir(obj) + + if hasattr(obj,'__class__'): + words.append('__class__') + words.extend(get_class_members(obj.__class__)) + #if '__base__' in words: 1/0 + + # Some libraries (such as traits) may introduce duplicates, we want to + # track and clean this up if it happens + may_have_dupes = False + + # this is the 'dir' function for objects with Enthought's traits + if hasattr(obj, 'trait_names'): + try: + words.extend(obj.trait_names()) + may_have_dupes = True + except TypeError: + # This will happen if `obj` is a class and not an instance. + pass + + # Support for PyCrust-style _getAttributeNames magic method. + if hasattr(obj, '_getAttributeNames'): + try: + words.extend(obj._getAttributeNames()) + may_have_dupes = True + except TypeError: + # `obj` is a class and not an instance. Ignore + # this error. + pass + + if may_have_dupes: + # eliminate possible duplicates, as some traits may also + # appear as normal attributes in the dir() call. + words = list(set(words)) + words.sort() + + # filter out non-string attributes which may be stuffed by dir() calls + # and poor coding in third-party modules + return [w for w in words if isinstance(w, basestring)] + +#---------------------------------------------------------------------------- def import_fail_info(mod_name,fns=None): """Inform load failure for a module.""" diff --git a/IPython/wildcard.py b/IPython/wildcard.py index 7f42168..b92ab5f 100644 --- a/IPython/wildcard.py +++ b/IPython/wildcard.py @@ -22,6 +22,8 @@ import pprint import re import types +from IPython.genutils import dir2 + def create_typestr2type_dicts(dont_include_in_type2type2str=["lambda"]): """Return dictionaries mapping lower case typename to type objects, from the types package, and vice versa.""" @@ -62,7 +64,6 @@ def show_hidden(str,show_all=False): """Return true for strings starting with single _ if show_all is true.""" return show_all or str.startswith("__") or not str.startswith("_") - class NameSpace(object): """NameSpace holds the dictionary for a namespace and implements filtering on name and types""" @@ -78,8 +79,20 @@ class NameSpace(object): if type(obj) == types.DictType: self._ns = obj else: - self._ns = dict([(key,getattr(obj,key)) for key in dir(obj) - if isinstance(key, basestring)]) + kv = [] + for key in dir2(obj): + if isinstance(key, basestring): + # This seemingly unnecessary try/except is actually needed + # because there is code out there with metaclasses that + # create 'write only' attributes, where a getattr() call + # will fail even if the attribute appears listed in the + # object's dictionary. Properties can actually do the same + # thing. In particular, Traits use this pattern + try: + kv.append((key,getattr(obj,key))) + except AttributeError: + pass + self._ns = dict(kv) def get_ns(self): """Return name space dictionary with objects matching type and name patterns.""" diff --git a/doc/ChangeLog b/doc/ChangeLog index 3bcb7b0..897f21d 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,3 +1,20 @@ +2007-07-29 Fernando Perez + + * IPython/Extensions/ipy_traits_completer.py: Add a new custom + completer that it traits-aware, so that traits objects don't show + all of their internal attributes all the time. + + * IPython/genutils.py (dir2): moved this code from inside + completer.py to expose it publicly, so I could use it in the + wildcards bugfix. + + * IPython/wildcard.py (NameSpace.__init__): fix a bug reported by + Stefan with Traits. + + * IPython/completer.py (Completer.attr_matches): change internal + var name from 'object' to 'obj', since 'object' is now a builtin + and this can lead to weird bugs if reusing this code elsewhere. + 2007-07-25 Fernando Perez * IPython/OInspect.py (Inspector.pinfo): fix small glitches in