From 5b86c0c8074ad9b952f4097226308e67b475993c 2009-09-03 23:58:31
From: Brian Granger <ellisonbg@gmail.com>
Date: 2009-09-03 23:58:31
Subject: [PATCH] More work on componentizing everything.

I have also done some work on component:

* componenets whose __init__ method fails are now unregistered.
* Added a masquerade_as function to enable components to masquerade
  as one another.  This is really a cheap and informal way of doing
  parts of an interface system.

---

diff --git a/IPython/core/alias.py b/IPython/core/alias.py
new file mode 100644
index 0000000..0b2ec60
--- /dev/null
+++ b/IPython/core/alias.py
@@ -0,0 +1,191 @@
+#!/usr/bin/env python
+# encoding: utf-8
+"""
+IPython's alias component
+
+Authors:
+
+* Brian Granger
+"""
+
+#-----------------------------------------------------------------------------
+#  Copyright (C) 2008-2009  The IPython Development Team
+#
+#  Distributed under the terms of the BSD License.  The full license is in
+#  the file COPYING, distributed as part of this software.
+#-----------------------------------------------------------------------------
+
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+
+import __builtin__
+import keyword
+import os
+import sys
+
+from IPython.core.component import Component
+
+from IPython.utils.traitlets import CBool, List, Instance
+from IPython.utils.genutils import error
+
+#-----------------------------------------------------------------------------
+# Functions and classes
+#-----------------------------------------------------------------------------
+
+def default_aliases():
+    # Make some aliases automatically
+    # Prepare list of shell aliases to auto-define
+    if os.name == 'posix':
+        auto_alias = ('mkdir mkdir', 'rmdir rmdir',
+                      'mv mv -i','rm rm -i','cp cp -i',
+                      'cat cat','less less','clear clear',
+                      # a better ls
+                      'ls ls -F',
+                      # long ls
+                      'll ls -lF')
+        # Extra ls aliases with color, which need special treatment on BSD
+        # variants
+        ls_extra = ( # color ls
+                     'lc ls -F -o --color',
+                     # ls normal files only
+                     'lf ls -F -o --color %l | grep ^-',
+                     # ls symbolic links
+                     'lk ls -F -o --color %l | grep ^l',
+                     # directories or links to directories,
+                     'ldir ls -F -o --color %l | grep /$',
+                     # things which are executable
+                     'lx ls -F -o --color %l | grep ^-..x',
+                     )
+        # The BSDs don't ship GNU ls, so they don't understand the
+        # --color switch out of the box
+        if 'bsd' in sys.platform:
+            ls_extra = ( # ls normal files only
+                         'lf ls -lF | grep ^-',
+                         # ls symbolic links
+                         'lk ls -lF | grep ^l',
+                         # directories or links to directories,
+                         'ldir ls -lF | grep /$',
+                         # things which are executable
+                         'lx ls -lF | grep ^-..x',
+                         )
+        auto_alias = auto_alias + ls_extra
+    elif os.name in ['nt','dos']:
+        auto_alias = ('ls dir /on',
+                      'ddir dir /ad /on', 'ldir dir /ad /on',
+                      'mkdir mkdir','rmdir rmdir','echo echo',
+                      'ren ren','cls cls','copy copy')
+    else:
+        auto_alias = ()
+    return [s.split(None,1) for s in auto_alias]
+
+
+class AliasError(Exception):
+    pass
+
+
+class InvalidAliasError(AliasError):
+    pass
+
+
+class AliasManager(Component):
+
+    auto_alias = List(default_aliases())
+    user_alias = List(default_value=[], config_key='USER_ALIAS')
+
+    def __init__(self, parent, config=None):
+        super(AliasManager, self).__init__(parent, config=config)
+        self.shell = Component.get_instances(
+            root=self.root,
+            klass='IPython.core.iplib.InteractiveShell'
+        )[0]
+        self.alias_table = {}
+        self.exclude_aliases()
+        self.init_aliases()
+
+    def __contains__(self, name):
+        if name in self.alias_table:
+            return True
+        else:
+            return False
+
+    @property
+    def aliases(self):
+        return [(item[0], item[1][1]) for item in self.alias_table.iteritems()]
+
+    def exclude_aliases(self):
+        # set of things NOT to alias (keywords, builtins and some magics)
+        no_alias = set(['cd','popd','pushd','dhist','alias','unalias'])
+        no_alias.update(set(keyword.kwlist))
+        no_alias.update(set(__builtin__.__dict__.keys()))
+        self.no_alias = no_alias
+
+    def init_aliases(self):
+        # Load default aliases
+        for name, cmd in self.auto_alias:
+            self.soft_define_alias(name, cmd)
+
+        # Load user aliases
+        for name, cmd in self.user_alias:
+            self.soft_define_alias(name, cmd)
+
+    def soft_define_alias(self, name, cmd):
+        """Define an alias, but don't raise on an AliasError."""
+        try:
+            self.define_alias(name, cmd)
+        except AliasError, e:
+            error("Invalid alias: %s" % e)
+
+    def define_alias(self, name, cmd):
+        """Define a new alias after validating it.
+
+        This will raise an :exc:`AliasError` if there are validation
+        problems.
+        """
+        nargs = self.validate_alias(name, cmd)
+        self.alias_table[name] = (nargs, cmd)
+
+    def validate_alias(self, name, cmd):
+        """Validate an alias and return the its number of arguments."""
+        if name in self.no_alias:
+            raise InvalidAliasError("The name %s can't be aliased "
+                                    "because it is a keyword or builtin." % name)
+        if not (isinstance(cmd, basestring)):
+            raise InvalidAliasError("An alias command must be a string, "
+                                    "got: %r" % name)
+        nargs = cmd.count('%s')
+        if nargs>0 and cmd.find('%l')>=0:
+            raise InvalidAliasError('The %s and %l specifiers are mutually '
+                                    'exclusive in alias definitions.')
+        return nargs
+
+    def call_alias(self, alias, rest=''):
+        """Call an alias given its name and the rest of the line."""
+        cmd = self.transform_alias(alias, rest)
+        try:
+            self.shell.system(cmd)
+        except:
+            self.shell.showtraceback()
+
+    def transform_alias(self, alias,rest=''):
+        """Transform alias to system command string."""
+        nargs, cmd = self.alias_table[alias]
+
+        if ' ' in cmd and os.path.isfile(cmd):
+            cmd = '"%s"' % cmd
+
+        # Expand the %l special to be the user's input line
+        if cmd.find('%l') >= 0:
+            cmd = cmd.replace('%l', rest)
+            rest = ''
+        if nargs==0:
+            # Simple, argument-less aliases
+            cmd = '%s %s' % (cmd, rest)
+        else:
+            # Handle aliases with positional arguments
+            args = rest.split(None, nargs)
+            if len(args) < nargs:
+                raise AliasError('Alias <%s> requires %s arguments, %s given.' %
+                      (alias, nargs, len(args)))
+            cmd = '%s %s' % (cmd % tuple(args[:nargs]),' '.join(args[nargs:]))
+        return cmd
diff --git a/IPython/core/application.py b/IPython/core/application.py
index 5c528ff..9378f9e 100644
--- a/IPython/core/application.py
+++ b/IPython/core/application.py
@@ -228,6 +228,7 @@ class Application(object):
                 self.print_traceback()
                 self.abort()
             elif action == 'exit':
+                self.print_traceback()
                 self.exit()
 
     def print_traceback(self):
diff --git a/IPython/core/builtin_trap.py b/IPython/core/builtin_trap.py
index 28996e5..a80e5c8 100644
--- a/IPython/core/builtin_trap.py
+++ b/IPython/core/builtin_trap.py
@@ -36,13 +36,14 @@ BuiltinUndefined = BuiltinUndefined()
 
 
 class BuiltinTrap(Component):
-    shell = Instance('IPython.core.iplib.InteractiveShell')
 
     def __init__(self, parent):
         super(BuiltinTrap, self).__init__(parent, None, None)
         # Don't just grab parent!!!
-        self.shell = Component.get_instances(root=self.root,
-            klass='IPython.core.iplib.InteractiveShell')[0]
+        self.shell = Component.get_instances(
+            root=self.root,
+            klass='IPython.core.iplib.InteractiveShell'
+        )[0]
         self._orig_builtins = {}
 
     def __enter__(self):
diff --git a/IPython/core/component.py b/IPython/core/component.py
index 5565098..26458a7 100644
--- a/IPython/core/component.py
+++ b/IPython/core/component.py
@@ -48,22 +48,55 @@ class MetaComponentTracker(type):
         cls.__numcreated = 0
 
     def __call__(cls, *args, **kw):
-        """Called when *class* is called (instantiated)!!!
+        """Called when a class is called (instantiated)!!!
         
         When a Component or subclass is instantiated, this is called and
         the instance is saved in a WeakValueDictionary for tracking.
         """
         instance = cls.__new__(cls, *args, **kw)
-        # Do this before __init__ is called so get_instances works inside
-        # __init__ methods!
+
+        # Register the instance before __init__ is called so get_instances 
+        # works inside __init__ methods!
+        indices = cls.register_instance(instance)
+
+        # This is in a try/except because of the __init__ method fails, the
+        # instance is discarded and shouldn't be tracked.
+        try:
+            if isinstance(instance, cls):
+                cls.__init__(instance, *args, **kw)
+        except:
+            # Unregister the instance because __init__ failed!
+            cls.unregister_instances(indices)
+            raise
+        else:
+            return instance
+
+    def register_instance(cls, instance):
+        """Register instance with cls and its subclasses."""
+        # indices is a list of the keys used to register the instance
+        # with.  This list is needed if the instance needs to be unregistered.
+        indices = []
         for c in cls.__mro__:
             if issubclass(cls, c) and issubclass(c, Component):
                 c.__numcreated += 1
+                indices.append(c.__numcreated)
                 c.__instance_refs[c.__numcreated] = instance
-        if isinstance(instance, cls):
-            cls.__init__(instance, *args, **kw)
+            else:
+                break
+        return indices
+
+    def unregister_instances(cls, indices):
+        """Unregister instance with cls and its subclasses."""
+        for c, index in zip(cls.__mro__, indices):
+            try:
+                del c.__instance_refs[index]
+            except KeyError:
+                pass
 
-        return instance
+    def clear_instances(cls):
+        """Clear all instances tracked by cls."""
+        cls.__instance_refs.clear()
+        cls.__numcreated = 0
 
     def get_instances(cls, name=None, root=None, klass=None):
         """Get all instances of cls and its subclasses.
@@ -82,6 +115,9 @@ class MetaComponentTracker(type):
         if klass is not None:
             if isinstance(klass, basestring):
                 klass = import_item(klass)
+            # Limit search to instances of klass for performance
+            if issubclass(klass, Component):
+                return klass.get_instances(name=name, root=root)
         instances = cls.__instance_refs.values()
         if name is not None:
             instances = [i for i in instances if i.name == name]
@@ -101,6 +137,26 @@ class MetaComponentTracker(type):
         return [i for i in cls.get_instances(name, root, klass) if call(i)]
 
 
+def masquerade_as(instance, cls):
+    """Let instance masquerade as an instance of cls.
+
+    Sometimes, such as in testing code, it is useful to let a class
+    masquerade as another.  Python, being duck typed, allows this by 
+    default.  But, instances of components are tracked by their class type.
+
+    After calling this, cls.get_instances() will return ``instance``.  This
+    does not, however, cause isinstance(instance, cls) to return ``True``.
+
+    Parameters
+    ----------
+    instance : an instance of a Component or Component subclass
+        The instance that will pretend to be a cls.
+    cls : subclass of Component
+        The Component subclass that instance will pretend to be.
+    """
+    cls.register_instance(instance)
+
+
 class ComponentNameGenerator(object):
     """A Singleton to generate unique component names."""
 
@@ -245,4 +301,5 @@ class Component(HasTraitlets):
             self._children.append(child)
 
     def __repr__(self):
-        return "<Component('%s')>" % self.name
+        return "<%s('%s')>" % (self.__class__.__name__, "DummyName")
+        # return "<Component('%s')>" % self.name
diff --git a/IPython/core/ipapp.py b/IPython/core/ipapp.py
index 155d36f..6988fb1 100644
--- a/IPython/core/ipapp.py
+++ b/IPython/core/ipapp.py
@@ -307,7 +307,8 @@ class IPythonApp(Application):
             parent=None,
             config=self.master_config
         )
-
+        print self.shell
+    
     def start_app(self):
         self.shell.mainloop()
 
diff --git a/IPython/core/iplib.py b/IPython/core/iplib.py
index e186a66..abab486 100644
--- a/IPython/core/iplib.py
+++ b/IPython/core/iplib.py
@@ -40,6 +40,7 @@ from IPython.core import debugger, oinspect
 from IPython.core import shadowns
 from IPython.core import history as ipcorehist
 from IPython.core import prefilter
+from IPython.core.alias import AliasManager
 from IPython.core.autocall import IPyAutocall
 from IPython.core.builtin_trap import BuiltinTrap
 from IPython.core.display_trap import DisplayTrap
@@ -62,6 +63,9 @@ from IPython.utils.genutils import *
 from IPython.utils.strdispatch import StrDispatch
 from IPython.utils.platutils import toggle_set_term_title, set_term_title
 
+from IPython.utils import growl
+growl.start("IPython")
+
 from IPython.utils.traitlets import (
     Int, Float, Str, CBool, CaselessStrEnum, Enum, List, Unicode
 )
@@ -303,7 +307,7 @@ class InteractiveShell(Component, Magic):
         self.init_traceback_handlers(custom_exceptions)
         self.init_user_ns()
         self.init_logger()
-        self.init_aliases()
+        self.init_alias()
         self.init_builtins()
         
         # pre_config_initialization
@@ -1660,129 +1664,8 @@ class InteractiveShell(Component, Magic):
     # Things related to aliases
     #-------------------------------------------------------------------------
 
-    def init_aliases(self):
-        # dict of things NOT to alias (keywords, builtins and some magics)
-        no_alias = {}
-        no_alias_magics = ['cd','popd','pushd','dhist','alias','unalias']
-        for key in keyword.kwlist + no_alias_magics:
-            no_alias[key] = 1
-        no_alias.update(__builtin__.__dict__)
-        self.no_alias = no_alias
-
-        # Make some aliases automatically
-        # Prepare list of shell aliases to auto-define
-        if os.name == 'posix':
-            auto_alias = ('mkdir mkdir', 'rmdir rmdir',
-                          'mv mv -i','rm rm -i','cp cp -i',
-                          'cat cat','less less','clear clear',
-                          # a better ls
-                          'ls ls -F',
-                          # long ls
-                          'll ls -lF')
-            # Extra ls aliases with color, which need special treatment on BSD
-            # variants
-            ls_extra = ( # color ls
-                         'lc ls -F -o --color',
-                         # ls normal files only
-                         'lf ls -F -o --color %l | grep ^-',
-                         # ls symbolic links
-                         'lk ls -F -o --color %l | grep ^l',
-                         # directories or links to directories,
-                         'ldir ls -F -o --color %l | grep /$',
-                         # things which are executable
-                         'lx ls -F -o --color %l | grep ^-..x',
-                         )
-            # The BSDs don't ship GNU ls, so they don't understand the
-            # --color switch out of the box
-            if 'bsd' in sys.platform:
-                ls_extra = ( # ls normal files only
-                             'lf ls -lF | grep ^-',
-                             # ls symbolic links
-                             'lk ls -lF | grep ^l',
-                             # directories or links to directories,
-                             'ldir ls -lF | grep /$',
-                             # things which are executable
-                             'lx ls -lF | grep ^-..x',
-                             )
-            auto_alias = auto_alias + ls_extra
-        elif os.name in ['nt','dos']:
-            auto_alias = ('ls dir /on',
-                          'ddir dir /ad /on', 'ldir dir /ad /on',
-                          'mkdir mkdir','rmdir rmdir','echo echo',
-                          'ren ren','cls cls','copy copy')
-        else:
-            auto_alias = ()
-        self.auto_alias = [s.split(None,1) for s in auto_alias]
-        
-        # Load default aliases
-        for alias, cmd in self.auto_alias:
-            self.define_alias(alias,cmd)
-
-        # Load user aliases
-        for alias in self.alias:
-            self.magic_alias(alias)
-
-    def call_alias(self,alias,rest=''):
-        """Call an alias given its name and the rest of the line.
-
-        This is only used to provide backwards compatibility for users of
-        ipalias(), use of which is not recommended for anymore."""
-
-        # Now call the macro, evaluating in the user's namespace
-        cmd = self.transform_alias(alias, rest)
-        try:
-            self.system(cmd)
-        except:
-            self.showtraceback()
-
-    def define_alias(self, name, cmd):
-        """ Define a new alias."""
-
-        if callable(cmd):
-            self.alias_table[name] = cmd
-            from IPython.core import shadowns
-            setattr(shadowns, name, cmd)
-            return
-
-        if isinstance(cmd, basestring):
-            nargs = cmd.count('%s')
-            if nargs>0 and cmd.find('%l')>=0:
-                raise Exception('The %s and %l specifiers are mutually '
-                                'exclusive in alias definitions.')
-                  
-            self.alias_table[name] = (nargs,cmd)
-            return
-        
-        self.alias_table[name] = cmd
-
-    def ipalias(self,arg_s):
-        """Call an alias by name.
-
-        Input: a string containing the name of the alias to call and any
-        additional arguments to be passed to the magic.
-
-        ipalias('name -opt foo bar') is equivalent to typing at the ipython
-        prompt:
-
-        In[1]: name -opt foo bar
-
-        To call an alias without arguments, simply use ipalias('name').
-
-        This provides a proper Python function to call IPython's aliases in any
-        valid Python code you can type at the interpreter, including loops and
-        compound statements.  It is added by IPython to the Python builtin
-        namespace upon initialization."""
-
-        args = arg_s.split(' ',1)
-        alias_name = args[0]
-        try:
-            alias_args = args[1]
-        except IndexError:
-            alias_args = ''
-        if alias_name in self.alias_table:
-            self.call_alias(alias_name,alias_args)
-        else:
-            error("Alias `%s` not found." % alias_name)
+    def init_alias(self):
+        self.alias_manager = AliasManager(self, config=self.config)
 
     def expand_alias(self, line):
         """ Expand an alias in the command line 
@@ -1817,81 +1700,25 @@ class InteractiveShell(Component, Magic):
         while 1:
             pre,fn,rest = prefilter.splitUserInput(line,
                                                    prefilter.shell_line_split)
-            if fn in self.alias_table:
+            if fn in self.alias_manager.alias_table:
                 if fn in done:
                     warn("Cyclic alias definition, repeated '%s'" % fn)
                     return ""
                 done.add(fn)
 
-                l2 = self.transform_alias(fn,rest)
-                # dir -> dir 
-                # print "alias",line, "->",l2  #dbg
+                l2 = self.alias_manager.transform_alias(fn, rest)
                 if l2 == line:
                     break
                 # ls -> ls -F should not recurse forever
                 if l2.split(None,1)[0] == line.split(None,1)[0]:
                     line = l2
                     break
-                
                 line=l2
-                
-                
-                # print "al expand to",line #dbg
             else:
                 break
                 
         return line
 
-    def transform_alias(self, alias,rest=''):
-        """ Transform alias to system command string.
-        """
-        trg = self.alias_table[alias]
-
-        nargs,cmd = trg
-        # print trg #dbg
-        if ' ' in cmd and os.path.isfile(cmd):
-            cmd = '"%s"' % cmd
-
-        # Expand the %l special to be the user's input line
-        if cmd.find('%l') >= 0:
-            cmd = cmd.replace('%l',rest)
-            rest = ''
-        if nargs==0:
-            # Simple, argument-less aliases
-            cmd = '%s %s' % (cmd,rest)
-        else:
-            # Handle aliases with positional arguments
-            args = rest.split(None,nargs)
-            if len(args)< nargs:
-                error('Alias <%s> requires %s arguments, %s given.' %
-                      (alias,nargs,len(args)))
-                return None
-            cmd = '%s %s' % (cmd % tuple(args[:nargs]),' '.join(args[nargs:]))
-        # Now call the macro, evaluating in the user's namespace
-        #print 'new command: <%r>' % cmd  # dbg
-        return cmd
-
-    def init_auto_alias(self):
-        """Define some aliases automatically.
-
-        These are ALL parameter-less aliases"""
-
-        for alias,cmd in self.auto_alias:
-            self.define_alias(alias,cmd)
-
-    def alias_table_validate(self,verbose=0):
-        """Update information about the alias table.
-
-        In particular, make sure no Python keywords/builtins are in it."""
-
-        no_alias = self.no_alias
-        for k in self.alias_table.keys():
-            if k in no_alias:
-                del self.alias_table[k]
-                if verbose:
-                    print ("Deleting alias <%s>, it's a Python "
-                           "keyword or builtin." % k)
-
     #-------------------------------------------------------------------------
     # Things related to the running of code
     #-------------------------------------------------------------------------
@@ -2513,9 +2340,10 @@ class InteractiveShell(Component, Magic):
           - continue_prompt(False): whether this line is the first one or a
           continuation in a sequence of inputs.
         """
-
+        growl.notify("raw_input: ", "prompt = %r\ncontinue_prompt = %s" % (prompt, continue_prompt))
         # Code run by the user may have modified the readline completer state.
         # We must ensure that our completer is back in place.
+
         if self.has_readline:
             self.set_completer()
         
@@ -2659,6 +2487,8 @@ class InteractiveShell(Component, Magic):
 
         # save the line away in case we crash, so the post-mortem handler can
         # record it
+        growl.notify("_prefilter: ", "line = %s\ncontinue_prompt = %s" % (line, continue_prompt))
+        
         self._last_input_line = line
 
         #print '***line: <%s>' % line # dbg
@@ -2715,15 +2545,17 @@ class InteractiveShell(Component, Magic):
         entry and presses enter.
         
         """
+        growl.notify("multiline_prefilter: ", "%s\n%s" % (line, continue_prompt))
         out = []
         for l in line.rstrip('\n').split('\n'):
             out.append(self._prefilter(l, continue_prompt))
+        growl.notify("multiline_prefilter return: ", '\n'.join(out))
         return '\n'.join(out)
     
     # Set the default prefilter() function (this can be user-overridden)
     prefilter = multiline_prefilter
 
-    def handle_normal(self,line_info):
+    def handle_normal(self, line_info):
         """Handle normal input lines. Use as a template for handlers."""
 
         # With autoindent on, we need some way to exit the input loop, and I
@@ -2742,10 +2574,9 @@ class InteractiveShell(Component, Magic):
         self.log(line,line,continue_prompt)
         return line
 
-    def handle_alias(self,line_info):
+    def handle_alias(self, line_info):
         """Handle alias input lines. """
-        tgt = self.alias_table[line_info.iFun]
-        # print "=>",tgt #dbg
+        tgt = self.alias_manager.alias_table[line_info.iFun]
         if callable(tgt):
             if '$' in line_info.line:
                 call_meth = '(_ip, _ip.var_expand(%s))'
diff --git a/IPython/core/prefilter.py b/IPython/core/prefilter.py
index 0fb4524..4ae5818 100644
--- a/IPython/core/prefilter.py
+++ b/IPython/core/prefilter.py
@@ -251,8 +251,8 @@ def checkAlias(l_info,ip):
     # Note: aliases can not contain '.'
     head = l_info.iFun.split('.',1)[0]
     
-    if l_info.iFun not in ip.alias_table \
-           or head not in ip.alias_table \
+    if l_info.iFun not in ip.alias_manager \
+           or head not in ip.alias_manager \
            or isShadowed(head,ip): 
         return None
 
diff --git a/IPython/utils/growl.py b/IPython/utils/growl.py
index cc4613c..fa11d96 100644
--- a/IPython/utils/growl.py
+++ b/IPython/utils/growl.py
@@ -18,7 +18,7 @@ class Notifier(object):
 
     def _notify(self, title, msg):
         if self.g_notifier is not None:
-            self.g_notifier.notify('kernel', title, msg)        
+            self.g_notifier.notify('core', title, msg)        
 
     def notify(self, title, msg):
         self._notify(title, msg)