From 95a4f176f9cc125a264b1898e701eeb30157313d 2010-08-11 23:10:15 From: Brian Granger Date: 2010-08-11 23:10:15 Subject: [PATCH] Finishing work on configurables, plugins and extensions. --- diff --git a/IPython/core/alias.py b/IPython/core/alias.py index 752e8f4..c05db21 100644 --- a/IPython/core/alias.py +++ b/IPython/core/alias.py @@ -104,7 +104,7 @@ class AliasManager(Configurable): default_aliases = List(default_aliases(), config=True) user_aliases = List(default_value=[], config=True) - shell = Instance('IPython.core.iplib.InteractiveShell') + shell = Instance('IPython.core.iplib.InteractiveShellABC') def __init__(self, shell, config=None): super(AliasManager, self).__init__(config=config) diff --git a/IPython/core/builtin_trap.py b/IPython/core/builtin_trap.py index 2f26496..0924510 100755 --- a/IPython/core/builtin_trap.py +++ b/IPython/core/builtin_trap.py @@ -37,7 +37,7 @@ BuiltinUndefined = __BuiltinUndefined() class BuiltinTrap(Configurable): - shell = Instance('IPython.core.iplib.InteractiveShell') + shell = Instance('IPython.core.iplib.InteractiveShellABC') def __init__(self, shell): super(BuiltinTrap, self).__init__(None) diff --git a/IPython/core/extensions.py b/IPython/core/extensions.py index 03bb258..82f84cd 100644 --- a/IPython/core/extensions.py +++ b/IPython/core/extensions.py @@ -28,8 +28,32 @@ from IPython.utils.traitlets import Instance #----------------------------------------------------------------------------- class ExtensionManager(Configurable): + """A class to manage IPython extensions. - shell = Instance('IPython.core.iplib.InteractiveShell') + An IPython extension is an importable Python module that has + a function with the signature:: + + def load_ipython_extension(ipython): + # Do things with ipython + + This function is called after your extension is imported and the + currently active :class:`InteractiveShell` instance is passed as + the only argument. You can do anything you want with IPython at + that point, including defining new magic and aliases, adding new + components, etc. + + The :func:`load_ipython_extension` will be called again is you + load or reload the extension again. It is up to the extension + author to add code to manage that. + + You can put your extension modules anywhere you want, as long as + they can be imported by Python's standard import mechanism. However, + to make it easy to write extensions, you can also put your extensions + in ``os.path.join(self.ipython_dir, 'extensions')``. This directory + is added to ``sys.path`` automatically. + """ + + shell = Instance('IPython.core.iplib.InteractiveShellABC') def __init__(self, shell, config=None): super(ExtensionManager, self).__init__(config=config) @@ -54,28 +78,6 @@ class ExtensionManager(Configurable): def load_extension(self, module_str): """Load an IPython extension by its module name. - An IPython extension is an importable Python module that has - a function with the signature:: - - def load_ipython_extension(ipython): - # Do things with ipython - - This function is called after your extension is imported and the - currently active :class:`InteractiveShell` instance is passed as - the only argument. You can do anything you want with IPython at - that point, including defining new magic and aliases, adding new - components, etc. - - The :func:`load_ipython_extension` will be called again is you - load or reload the extension again. It is up to the extension - author to add code to manage that. - - You can put your extension modules anywhere you want, as long as - they can be imported by Python's standard import mechanism. However, - to make it easy to write extensions, you can also put your extensions - in ``os.path.join(self.ipython_dir, 'extensions')``. This directory - is added to ``sys.path`` automatically. - If :func:`load_ipython_extension` returns anything, this function will return that object. """ @@ -121,4 +123,4 @@ class ExtensionManager(Configurable): def _call_unload_ipython_extension(self, mod): if hasattr(mod, 'unload_ipython_extension'): - return mod.unload_ipython_extension(self.shell) \ No newline at end of file + return mod.unload_ipython_extension(self.shell) diff --git a/IPython/core/iplib.py b/IPython/core/iplib.py index b01d68f..af27eb9 100644 --- a/IPython/core/iplib.py +++ b/IPython/core/iplib.py @@ -18,6 +18,7 @@ from __future__ import with_statement from __future__ import absolute_import import __builtin__ +import abc import bdb import codeop import exceptions @@ -43,6 +44,7 @@ from IPython.core.extensions import ExtensionManager from IPython.core.fakemodule import FakeModule, init_fakemod_dict from IPython.core.logger import Logger from IPython.core.magic import Magic +from IPython.core.plugin import PluginManager from IPython.core.prefilter import PrefilterManager from IPython.core.prompts import CachedOutput from IPython.core.usage import interactive_usage, default_banner @@ -286,6 +288,7 @@ class InteractiveShell(Configurable, Magic): builtin_trap = Instance('IPython.core.builtin_trap.BuiltinTrap') display_trap = Instance('IPython.core.display_trap.DisplayTrap') extension_manager = Instance('IPython.core.extensions.ExtensionManager') + plugin_manager = Instance('IPython.core.plugin.PluginManager') def __init__(self, config=None, ipython_dir=None, usage=None, user_ns=None, user_global_ns=None, @@ -341,6 +344,7 @@ class InteractiveShell(Configurable, Magic): self.init_magics() self.init_pdb() self.init_extension_manager() + self.init_plugin_manager() self.hooks.late_startup_hook() @classmethod @@ -1759,12 +1763,15 @@ class InteractiveShell(Configurable, Magic): self.ns_table['alias'] = self.alias_manager.alias_table, #------------------------------------------------------------------------- - # Things related to extensions + # Things related to extensions and plugins #------------------------------------------------------------------------- def init_extension_manager(self): self.extension_manager = ExtensionManager(self, config=self.config) + def init_plugin_manager(self): + self.plugin_manager = PluginManager(config=self.config) + #------------------------------------------------------------------------- # Things related to the running of code #------------------------------------------------------------------------- @@ -2509,3 +2516,8 @@ class InteractiveShell(Configurable, Magic): self.restore_sys_module_state() +class InteractiveShellABC(object): + """An abstract base class for InteractiveShell.""" + __metaclass__ = abc.ABCMeta + +InteractiveShellABC.register(InteractiveShell) diff --git a/IPython/core/plugin.py b/IPython/core/plugin.py new file mode 100644 index 0000000..ee12abe --- /dev/null +++ b/IPython/core/plugin.py @@ -0,0 +1,51 @@ +# encoding: utf-8 +"""IPython plugins. + +Authors: + +* Brian Granger +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2010 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 +#----------------------------------------------------------------------------- + +from IPython.config.configurable import Configurable +from IPython.utils.traitlets import Dict + +#----------------------------------------------------------------------------- +# Main class +#----------------------------------------------------------------------------- + +class PluginManager(Configurable): + """A manager for IPython plugins.""" + + plugins = Dict({}) + + def __init__(self, config=None): + super(PluginManager, self).__init__(config=config) + + def register_plugin(self, name, plugin): + if not isinstance(plugin, Plugin): + raise TypeError('Expected Plugin, got: %r' % plugin) + if self.plugins.has_key(name): + raise KeyError('Plugin with name already exists: %r' % name) + self.plugins[name] = plugin + + def unregister_plugin(self, name): + del self.plugins[name] + + def get_plugin(self, name, default=None): + return self.plugins.get(name, default) + + +class Plugin(Configurable): + """Base class for IPython plugins.""" + pass diff --git a/IPython/core/prefilter.py b/IPython/core/prefilter.py index 3fe16e3..10e366b 100755 --- a/IPython/core/prefilter.py +++ b/IPython/core/prefilter.py @@ -210,7 +210,7 @@ class PrefilterManager(Configurable): """ multi_line_specials = CBool(True, config=True) - shell = Instance('IPython.core.iplib.InteractiveShell') + shell = Instance('IPython.core.iplib.InteractiveShellABC') def __init__(self, shell, config=None): super(PrefilterManager, self).__init__(config=config) @@ -447,7 +447,7 @@ class PrefilterTransformer(Configurable): priority = Int(100, config=True) # Transformers don't currently use shell or prefilter_manager, but as we # move away from checkers and handlers, they will need them. - shell = Instance('IPython.core.iplib.InteractiveShell') + shell = Instance('IPython.core.iplib.InteractiveShellABC') prefilter_manager = Instance('IPython.core.prefilter.PrefilterManager') enabled = Bool(True, config=True) @@ -555,7 +555,7 @@ class PrefilterChecker(Configurable): """Inspect an input line and return a handler for that line.""" priority = Int(100, config=True) - shell = Instance('IPython.core.iplib.InteractiveShell') + shell = Instance('IPython.core.iplib.InteractiveShellABC') prefilter_manager = Instance('IPython.core.prefilter.PrefilterManager') enabled = Bool(True, config=True) @@ -748,7 +748,7 @@ class PrefilterHandler(Configurable): handler_name = Str('normal') esc_strings = List([]) - shell = Instance('IPython.core.iplib.InteractiveShell') + shell = Instance('IPython.core.iplib.InteractiveShellABC') prefilter_manager = Instance('IPython.core.prefilter.PrefilterManager') def __init__(self, shell, prefilter_manager, config=None): diff --git a/IPython/core/tests/test_plugin.py b/IPython/core/tests/test_plugin.py new file mode 100644 index 0000000..92ff22b --- /dev/null +++ b/IPython/core/tests/test_plugin.py @@ -0,0 +1,46 @@ +"""Tests for plugin.py""" + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +from unittest import TestCase + +from IPython.core.plugin import Plugin, PluginManager + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- + +class FooPlugin(Plugin): + pass + + +class BarPlugin(Plugin): + pass + + +class BadPlugin(object): + pass + + +class PluginTest(TestCase): + + def setUp(self): + self.manager = PluginManager() + + def test_register_get(self): + self.assertEquals(None, self.manager.get_plugin('foo')) + foo = FooPlugin() + self.manager.register_plugin('foo', foo) + self.assertEquals(foo, self.manager.get_plugin('foo')) + bar = BarPlugin() + self.assertRaises(KeyError, self.manager.register_plugin, 'foo', bar) + bad = BadPlugin() + self.assertRaises(TypeError, self.manager.register_plugin, 'bad') + + def test_unregister(self): + foo = FooPlugin() + self.manager.register_plugin('foo', foo) + self.manager.unregister_plugin('foo') + self.assertEquals(None, self.manager.get_plugin('foo')) diff --git a/IPython/extensions/parallelmagic.py b/IPython/extensions/parallelmagic.py index 127e212..e61d9a7 100755 --- a/IPython/extensions/parallelmagic.py +++ b/IPython/extensions/parallelmagic.py @@ -16,7 +16,7 @@ import new -from IPython.config.configurable import Configurable +from IPython.core.plugin import Plugin from IPython.utils.traitlets import Bool, Any, Instance from IPython.utils.autoattr import auto_attr from IPython.testing import decorators as testdec @@ -31,15 +31,15 @@ Use activate() on a MultiEngineClient object to activate it for magics. """ -class ParalleMagicComponent(Configurable): +class ParalleMagic(Plugin): """A component to manage the %result, %px and %autopx magics.""" active_multiengine_client = Any() verbose = Bool(False, config=True) - shell = Instance('IPython.core.iplib.InteractiveShell') + shell = Instance('IPython.core.iplib.InteractiveShellABC') def __init__(self, shell, config=None): - super(ParalleMagicComponent, self).__init__(config=config) + super(ParalleMagic, self).__init__(config=config) self.shell = shell self._define_magics() # A flag showing if autopx is activated or not @@ -196,6 +196,7 @@ def load_ipython_extension(ip): """Load the extension in IPython.""" global _loaded if not _loaded: - prd = ParalleMagicComponent(ip, config=ip.config) + plugin = ParalleMagic(ip, config=ip.config) + ip.plugin_manager.register_plugin('parallel_magic', plugin) _loaded = True diff --git a/IPython/extensions/pretty.py b/IPython/extensions/pretty.py index 52501d1..5e94db0 100644 --- a/IPython/extensions/pretty.py +++ b/IPython/extensions/pretty.py @@ -37,7 +37,7 @@ by doing:: from IPython.core.error import TryNext from IPython.external import pretty -from IPython.config.configurable import Configurable +from IPython.core.plugin import Plugin from IPython.utils.traitlets import Bool, List, Instance from IPython.utils.io import Term from IPython.utils.autoattr import auto_attr @@ -51,11 +51,11 @@ from IPython.utils.importstring import import_item _loaded = False -class PrettyResultDisplay(Configurable): +class PrettyResultDisplay(Plugin): """A component for pretty printing on steroids.""" verbose = Bool(False, config=True) - shell = Instance('IPython.core.iplib.InteractiveShell') + shell = Instance('IPython.core.iplib.InteractiveShellABC') # A list of (type, func_name), like # [(dict, 'my_dict_printer')] @@ -124,10 +124,10 @@ def load_ipython_extension(ip): """Load the extension in IPython as a hook.""" global _loaded if not _loaded: - prd = PrettyResultDisplay(ip, config=ip.config) - ip.set_hook('result_display', prd, priority=99) + plugin = PrettyResultDisplay(ip, config=ip.config) + ip.set_hook('result_display', plugin, priority=99) _loaded = True - return prd + ip.plugin_manager.register_plugin('pretty_result_display', plugin) def unload_ipython_extension(ip): """Unload the extension.""" diff --git a/IPython/extensions/tests/test_pretty.py b/IPython/extensions/tests/test_pretty.py index bef6b12..29e9752 100644 --- a/IPython/extensions/tests/test_pretty.py +++ b/IPython/extensions/tests/test_pretty.py @@ -17,8 +17,8 @@ Simple tests for :mod:`IPython.extensions.pretty`. from unittest import TestCase -from IPython.core.component import Component, masquerade_as -from IPython.core.iplib import InteractiveShell +from IPython.config.configurable import Configurable +from IPython.core.iplib import InteractiveShellABC from IPython.extensions import pretty as pretty_ext from IPython.external import pretty from IPython.testing import decorators as dec @@ -29,9 +29,11 @@ from IPython.utils.traitlets import Bool # Tests #----------------------------------------------------------------------------- -class InteractiveShellStub(Component): +class InteractiveShellStub(Configurable): pprint = Bool(True) +InteractiveShellABC.register(InteractiveShellStub) + class A(object): pass @@ -41,12 +43,8 @@ def a_pprinter(o, p, c): class TestPrettyResultDisplay(TestCase): def setUp(self): - self.ip = InteractiveShellStub(None) - # This allows our stub to be retrieved instead of the real - # InteractiveShell - masquerade_as(self.ip, InteractiveShell) - self.prd = pretty_ext.PrettyResultDisplay(self.ip, - name='pretty_result_display') + self.ip = InteractiveShellStub() + self.prd = pretty_ext.PrettyResultDisplay(self.ip, config=None) def test_for_type(self): self.prd.for_type(A, a_pprinter) @@ -77,7 +75,8 @@ a b ip = get_ipython() -prd = ip.extension_manager.load_extension('pretty') +ip.extension_manager.load_extension('pretty') +prd = ip.plugin_manager.get_plugin('pretty_result_display') prd.for_type(A, a_pretty_printer) prd.for_type_by_name(B.__module__, B.__name__, b_pretty_printer) diff --git a/IPython/testing/globalipapp.py b/IPython/testing/globalipapp.py index 714554e..56dd52a 100644 --- a/IPython/testing/globalipapp.py +++ b/IPython/testing/globalipapp.py @@ -137,7 +137,7 @@ def start_ipython(): # Create and initialize our test-friendly IPython instance. shell = iplib.InteractiveShell( - parent=None, config=config, + config=config, user_ns=ipnsdict(), user_global_ns={} ) diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index 67af9c2..eb28b3b 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -1023,3 +1023,25 @@ class List(Instance): super(List,self).__init__(klass=list, args=args, allow_none=allow_none, **metadata) + + +class Dict(Instance): + """An instance of a Python dict.""" + + def __init__(self, default_value=None, allow_none=True, **metadata): + """Create a dict trait type from a dict. + + The default value is created by doing ``dict(default_value)``, + which creates a copy of the ``default_value``. + """ + if default_value is None: + args = ((),) + elif isinstance(default_value, dict): + args = (default_value,) + elif isinstance(default_value, SequenceTypes): + args = (default_value,) + else: + raise TypeError('default value of Dict was %s' % default_value) + + super(Dict,self).__init__(klass=dict, args=args, + allow_none=allow_none, **metadata)