"""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'