Changeset 20920

Show
Ignore:
Timestamp:
08/02/2007 08:12:35 AM (3 years ago)
Author:
exarkun
Message:

Merge plugin-collision-2339

Author: exarkun
Reviewer: therve
Fixes #2339

Add a function, twisted.plugin.pluginPackagePaths, which returns a list of
additional directories to search for plugin modules for a particular plugin
package. Change twisted.plugins.init to use this function instead of
looping over sys.path directly.

Unlike the loop previously in twisted.plugins.init, pluginPackagePaths
will not include any directory which is a Python package. This prevents
plugins from being discovered from packages which Python may not actually
be able to import and limits plugin discovery to those installed on the
version of the package which is in use by an application. It also allows
a site installation to co-exist with another installation (for example,
a personal user installation) without interfering with each other.

Location:
trunk/twisted
Files:
3 modified

Legend:

Unmodified
Added
Removed
  • trunk/twisted/plugin.py

    r19305 r20920  
    11# -*- test-case-name: twisted.test.test_plugin -*- 
    22# Copyright (c) 2005 Divmod, Inc. 
     3# Copyright (c) 2007 Twisted Matrix Laboratories. 
    34# See LICENSE for details. 
    45 
     
    1011""" 
    1112 
    12 from __future__ import generators 
     13import os 
     14import sys 
    1315 
    1416from zope.interface import Interface, providedBy 
     
    8082    getComponent = __conform__ 
    8183 
     84 
     85 
    8286class CachedDropin(object): 
     87    """ 
     88    A collection of L{CachedPlugin} instances from a particular module in a 
     89    plugin package. 
     90 
     91    @type moduleName: C{str} 
     92    @ivar moduleName: The fully qualified name of the plugin module this 
     93        represents. 
     94 
     95    @type description: C{str} or C{NoneType} 
     96    @ivar description: A brief explanation of this collection of plugins 
     97        (probably the plugin module's docstring). 
     98 
     99    @type plugins: C{list} 
     100    @ivar plugins: The L{CachedPlugin} instances which were loaded from this 
     101        dropin. 
     102    """ 
    83103    def __init__(self, moduleName, description): 
    84104        self.moduleName = moduleName 
    85105        self.description = description 
    86106        self.plugins = [] 
     107 
     108 
    87109 
    88110def _generateCacheEntry(provider): 
     
    200222 
    201223 
    202 __all__ = ['getPlugins'] 
     224def pluginPackagePaths(name): 
     225    """ 
     226    Return a list of additional directories which should be searched for 
     227    modules to be included as part of the named plugin package. 
     228 
     229    @type name: C{str} 
     230    @param name: The fully-qualified Python name of a plugin package, eg 
     231        C{'twisted.plugins'}. 
     232 
     233    @rtype: C{list} of C{str} 
     234    @return: The absolute paths to other directories which may contain plugin 
     235        modules for the named plugin package. 
     236    """ 
     237    package = name.split('.') 
     238    # Note that this may include directories which do not exist.  It may be 
     239    # preferable to remove such directories at this point, rather than allow 
     240    # them to be searched later on. 
     241    # 
     242    # Note as well that only '__init__.py' will be considered to make a 
     243    # directory a package (and thus exclude it from this list).  This means 
     244    # that if you create a master plugin package which has some other kind of 
     245    # __init__ (eg, __init__.pyc) it will be incorrectly treated as a 
     246    # supplementary plugin directory. 
     247    return [ 
     248        os.path.abspath(os.path.join(x, *package)) 
     249        for x 
     250        in sys.path 
     251        if 
     252        not os.path.exists(os.path.join(x, *package + ['__init__.py']))] 
     253 
     254__all__ = ['getPlugins', 'pluginPackagePaths'] 
  • trunk/twisted/plugins/__init__.py

    r13752 r20920  
    11# -*- test-case-name: twisted.test.test_plugin -*- 
    22# Copyright (c) 2005 Divmod, Inc. 
     3# Copyright (c) 2007 Twisted Matrix Laboratories. 
    34# See LICENSE for details. 
    45 
     
    1213""" 
    1314 
    14 import os, sys 
    15 __path__ = [os.path.abspath(os.path.join(x, 'twisted', 'plugins')) for x in sys.path] 
    16  
     15from twisted.plugin import pluginPackagePaths 
     16__path__.extend(pluginPackagePaths(__name__)) 
    1717__all__ = []                    # nothing to see here, move along, move along 
  • trunk/twisted/test/test_plugin.py

    r19305 r20920  
    11# Copyright (c) 2005 Divmod, Inc. 
     2# Copyright (c) 2007 Twisted Matrix Laboratories. 
    23# See LICENSE for details. 
    34 
     
    282283# This is something like the Twisted plugins file. 
    283284pluginInitFile = """ 
    284 import os, sys 
    285 __path__ = [os.path.abspath(os.path.join(x, 'plugindummy', 'plugins')) for x in sys.path] 
     285from twisted.plugin import pluginPackagePaths 
     286__path__.extend(pluginPackagePaths(__name__)) 
    286287__all__ = [] 
    287288""" 
    288289 
    289 systemPluginFile = """ 
    290 from zope.interface import classProvides 
    291 from twisted.plugin import IPlugin, ITestPlugin 
    292  
    293 class system(object): 
    294     classProvides(IPlugin, ITestPlugin) 
    295 """ 
    296  
    297 devPluginFile = """ 
    298 from zope.interface import classProvides 
    299 from twisted.plugin import IPlugin, ITestPlugin 
    300  
    301 class system(object): 
    302     classProvides(IPlugin, ITestPlugin) 
    303  
    304 class dev(object): 
    305     classProvides(IPlugin, ITestPlugin) 
    306 """ 
    307  
    308 appPluginFile = """ 
    309 from zope.interface import classProvides 
    310 from twisted.plugin import IPlugin, ITestPlugin 
    311  
    312 class app(object): 
    313     classProvides(IPlugin, ITestPlugin) 
    314 """ 
    315  
    316 onePluginFile = """ 
    317 from zope.interface import classProvides 
    318 from twisted.plugin import IPlugin, ITestPlugin 
    319  
    320 class one(object): 
    321     classProvides(IPlugin, ITestPlugin) 
    322 """ 
    323  
    324 twoPluginFile = """ 
    325 from zope.interface import classProvides 
    326 from twisted.plugin import IPlugin, ITestPlugin 
    327  
    328 class two(object): 
    329     classProvides(IPlugin, ITestPlugin) 
    330 """ 
    331  
    332 def _createPluginDummy(entrypath, pluginContent, real=True): 
     290def pluginFileContents(name): 
     291    return ( 
     292        "from zope.interface import classProvides\n" 
     293        "from twisted.plugin import IPlugin, ITestPlugin\n" 
     294        "\n" 
     295        "class %s(object):\n" 
     296        "    classProvides(IPlugin, ITestPlugin)\n") % (name,) 
     297 
     298 
     299def _createPluginDummy(entrypath, pluginContent, real, pluginModule): 
    333300    """ 
    334301    Create a plugindummy package. 
     
    343310    if real: 
    344311        plugs.child('__init__.py').setContent(pluginInitFile) 
    345     if real: 
    346         n = 'plugindummy_builtin.py' 
    347     else: 
    348         n = 'plugindummy_app.py' 
    349     plugs.child(n).setContent(pluginContent) 
     312    plugs.child(pluginModule + '.py').setContent(pluginContent) 
    350313    return plugs 
     314 
    351315 
    352316 
     
    372336        self.devPath = self.fakeRoot.child('development_path') 
    373337        self.appPath = self.fakeRoot.child('application_path') 
    374         self.systemPackage = _createPluginDummy(self.systemPath, 
    375                                                 systemPluginFile) 
    376         self.devPackage = _createPluginDummy(self.devPath, devPluginFile) 
    377         self.appPackage = _createPluginDummy(self.appPath, appPluginFile, False) 
     338        self.systemPackage = _createPluginDummy( 
     339            self.systemPath, pluginFileContents('system'), 
     340            True, 'plugindummy_builtin') 
     341        self.devPackage = _createPluginDummy( 
     342            self.devPath, pluginFileContents('dev'), 
     343            True, 'plugindummy_builtin') 
     344        self.appPackage = _createPluginDummy( 
     345            self.appPath, pluginFileContents('app'), 
     346            False, 'plugindummy_app') 
    378347 
    379348        # Now we're going to do the system installation. 
     
    458427 
    459428 
    460     def test_newPluginsNotClobbered(self): 
    461         """ 
    462         Verify that plugins added in the 'development' path are loadable, even when 
    463         the (now non-importable) system path contains its own idea of the list 
    464         of plugins for a package. 
     429    def test_developmentPluginAvailability(self): 
     430        """ 
     431        Plugins added in the development path should be loadable, even when 
     432        the (now non-importable) system path contains its own idea of the 
     433        list of plugins for a package.  Inversely, plugins added in the 
     434        system path should not be available. 
    465435        """ 
    466436        # Run 3 times: uncached, cached, and then cached again to make sure we 
     
    468438        for x in range(3): 
    469439            names = self.getAllPlugins() 
    470             self.assertEqual(len(names), 3) 
    471             self.assertIn('app', names) 
    472             self.assertIn('dev', names) 
    473             self.assertIn('system', names) 
     440            names.sort() 
     441            self.assertEqual(names, ['app', 'dev']) 
    474442 
    475443 
     
    481449        """ 
    482450        mypath = self.appPackage.child("stale.py") 
    483         mypath.setContent(onePluginFile) 
     451        mypath.setContent(pluginFileContents('one')) 
    484452        # Make it super stale 
    485453        x = time.time() - 1000 
     
    497465        self.failIfIn('two', self.getAllPlugins()) 
    498466        self.resetEnvironment() 
    499         mypath.setContent(twoPluginFile) 
     467        mypath.setContent(pluginFileContents('two')) 
    500468        self.failIfIn('one', self.getAllPlugins()) 
    501469        self.assertIn('two', self.getAllPlugins()) 
     
    512480        """ 
    513481        self.unlockSystem() 
    514         self.sysplug.child('newstuff.py').setContent(onePluginFile) 
     482        self.sysplug.child('newstuff.py').setContent(pluginFileContents('one')) 
    515483        self.lockSystem() 
     484 
     485        # Take the developer path out, so that the system plugins are actually 
     486        # examined. 
     487        sys.path.remove(self.devPath.path) 
     488 
    516489        # Sanity check to make sure we're only flushing the error logged 
    517490        # below... 
     
    519492        self.assertIn('one', self.getAllPlugins()) 
    520493        self.assertEqual(len(self.flushLoggedErrors()), 1) 
     494 
     495 
     496 
     497class AdjacentPackageTests(unittest.TestCase): 
     498    """ 
     499    Tests for the behavior of the plugin system when there are multiple 
     500    installed copies of the package containing the plugins being loaded. 
     501    """ 
     502    def setUp(self): 
     503        """ 
     504        Save the elements of C{sys.path} and the items of C{sys.modules}. 
     505        """ 
     506        self.originalPath = sys.path[:] 
     507        self.savedModules = sys.modules.copy() 
     508 
     509 
     510    def tearDown(self): 
     511        """ 
     512        Restore C{sys.path} and C{sys.modules} to their original values. 
     513        """ 
     514        sys.path[:] = self.originalPath 
     515        sys.modules.clear() 
     516        sys.modules.update(self.savedModules) 
     517 
     518 
     519    def createDummyPackage(self, root, name, pluginName): 
     520        """ 
     521        Create a directory containing a Python package named I{dummy} with a 
     522        I{plugins} subpackage. 
     523 
     524        @type root: L{FilePath} 
     525        @param root: The directory in which to create the hierarchy. 
     526 
     527        @type name: C{str} 
     528        @param name: The name of the directory to create which will contain 
     529            the package. 
     530 
     531        @type pluginName: C{str} 
     532        @param pluginName: The name of a module to create in the 
     533            I{dummy.plugins} package. 
     534 
     535        @rtype: L{FilePath} 
     536        @return: The directory which was created to contain the I{dummy} 
     537            package. 
     538        """ 
     539        directory = root.child(name) 
     540        package = directory.child('dummy') 
     541        package.makedirs() 
     542        package.child('__init__.py').setContent('') 
     543        plugins = package.child('plugins') 
     544        plugins.makedirs() 
     545        plugins.child('__init__.py').setContent(pluginInitFile) 
     546        pluginModule = plugins.child(pluginName + '.py') 
     547        pluginModule.setContent(pluginFileContents(name)) 
     548        return directory 
     549 
     550 
     551    def test_hiddenPackageSamePluginModuleNameObscured(self): 
     552        """ 
     553        Only plugins from the first package in sys.path should be returned by 
     554        getPlugins in the case where there are two Python packages by the same 
     555        name installed, each with a plugin module by a single name. 
     556        """ 
     557        root = FilePath(self.mktemp()) 
     558        root.makedirs() 
     559 
     560        firstDirectory = self.createDummyPackage(root, 'first', 'someplugin') 
     561        secondDirectory = self.createDummyPackage(root, 'second', 'someplugin') 
     562 
     563        sys.path.append(firstDirectory.path) 
     564        sys.path.append(secondDirectory.path) 
     565 
     566        import dummy.plugins 
     567 
     568        plugins = list(plugin.getPlugins(plugin.ITestPlugin, dummy.plugins)) 
     569        self.assertEqual(['first'], [p.__name__ for p in plugins]) 
     570 
     571 
     572    def test_hiddenPackageDifferentPluginModuleNameObscured(self): 
     573        """ 
     574        Plugins from the first package in sys.path should be returned by 
     575        getPlugins in the case where there are two Python packages by the same 
     576        name installed, each with a plugin module by a different name. 
     577        """ 
     578        root = FilePath(self.mktemp()) 
     579        root.makedirs() 
     580 
     581        firstDirectory = self.createDummyPackage(root, 'first', 'thisplugin') 
     582        secondDirectory = self.createDummyPackage(root, 'second', 'thatplugin') 
     583 
     584        sys.path.append(firstDirectory.path) 
     585        sys.path.append(secondDirectory.path) 
     586 
     587        import dummy.plugins 
     588 
     589        plugins = list(plugin.getPlugins(plugin.ITestPlugin, dummy.plugins)) 
     590        self.assertEqual(['first'], [p.__name__ for p in plugins]) 
     591 
     592 
     593 
     594class PackagePathTests(unittest.TestCase): 
     595    """ 
     596    Tests for L{plugin.pluginPackagePaths} which constructs search paths for 
     597    plugin packages. 
     598    """ 
     599    def setUp(self): 
     600        """ 
     601        Save the elements of C{sys.path}. 
     602        """ 
     603        self.originalPath = sys.path[:] 
     604 
     605 
     606    def tearDown(self): 
     607        """ 
     608        Restore C{sys.path} to its original value. 
     609        """ 
     610        sys.path[:] = self.originalPath 
     611 
     612 
     613    def test_pluginDirectories(self): 
     614        """ 
     615        L{plugin.pluginPackagePaths} should return a list containing each 
     616        directory in C{sys.path} with a suffix based on the supplied package 
     617        name. 
     618        """ 
     619        foo = FilePath('foo') 
     620        bar = FilePath('bar') 
     621        sys.path = [foo.path, bar.path] 
     622        self.assertEqual( 
     623            plugin.pluginPackagePaths('dummy.plugins'), 
     624            [foo.child('dummy').child('plugins').path, 
     625             bar.child('dummy').child('plugins').path]) 
     626 
     627 
     628    def test_pluginPackagesExcluded(self): 
     629        """ 
     630        L{plugin.pluginPackagePaths} should exclude directories which are 
     631        Python packages.  The only allowed plugin package (the only one 
     632        associated with a I{dummy} package which Python will allow to be 
     633        imported) will already be known to the caller of 
     634        L{plugin.pluginPackagePaths} and will most commonly already be in 
     635        the C{__path__} they are about to mutate. 
     636        """ 
     637        root = FilePath(self.mktemp()) 
     638        foo = root.child('foo').child('dummy').child('plugins') 
     639        foo.makedirs() 
     640        foo.child('__init__.py').setContent('') 
     641        sys.path = [root.child('foo').path, root.child('bar').path] 
     642        self.assertEqual( 
     643            plugin.pluginPackagePaths('dummy.plugins'), 
     644            [root.child('bar').child('dummy').child('plugins').path])