From 527ca8e673473ce792079ff9f91ec1aaa42f24b6 2007-07-29 21:38:44
From: fperez
Date: 2007-07-29 21:38:44
Subject: [PATCH] - Implement a traits-aware tab-completer.  See ipy_traits_completer in
Extensions/ for details on how to use it.

- Fix a bug reported by Stefan when using psearch on traits objects.

- Small but important foo? bugfix with class docstrings.


---

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<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.
+"""
+
+#############################################################################
+# 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)<COMPLETE_THRESHOLD:
+        attrs = list(set(attrs) - 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
+
+    # 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. <fperez@colorado.edu>
@@ -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  <Fernando.Perez@colorado.edu>
+
+	* 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  <Fernando.Perez@colorado.edu>
 
 	* IPython/OInspect.py (Inspector.pinfo): fix small glitches in