Merge lp:~vila/bzr/82693-plugin-at-path into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Martin Pool
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~vila/bzr/82693-plugin-at-path
Merge into: lp:bzr
Prerequisite: lp:~vila/bzr/411413-disable-plugin
Diff against target: 498 lines (+295/-77) (has conflicts)
5 files modified
NEWS (+10/-0)
bzrlib/help_topics/en/configuration.txt (+30/-4)
bzrlib/plugin.py (+161/-66)
bzrlib/tests/__init__.py (+1/-0)
bzrlib/tests/test_plugins.py (+93/-7)
Text conflict in NEWS
To merge this branch: bzr merge lp:~vila/bzr/82693-plugin-at-path
Reviewer Review Type Date Requested Status
Martin Pool Approve
Review via email: mp+21547@code.launchpad.net

Description of the change

This path fixes bug #82693 by introducing a BZR_PLUGINS_AT environment variable.

Doc excerpt:

+BZR_PLUGINS_AT
+~~~~~~~~~~~~~~
+
+When adding a new feature or working on a bug in a plugin,
+developers often need to use a specific version of a given
+plugin. Since python requires that the directory containing the
+code is named like the plugin itself this make it impossible to
+use arbitrary directory names (using a two-level directory scheme
+is inconvenient). ``BZR_PLUGINS_AT`` allows such directories even
+if they don't appear in ``BZR_PLUGIN_PATH`` .
+
+Plugins specified in this environment variable takes precedence
+over the ones in ``BZR_PLUGIN_PATH``.
+
+The variable specified a list of ``plugin_name@plugin path``,
+``plugin_name`` being the name of the plugin as it appears in
+python module paths, ``plugin_path`` being the path to the
+directory containing the plugin code itself
+(i.e. ``plugins/myplugin`` not ``plugins``). Use ':' as the list
+separator, use ';' on windows.

This requires https://code.edge.launchpad.net/~vila/bzr/411413-disable-plugin/+merge/21435 and reuse the same sys.meta_hook importer.

I've tested it against python2.4, 2.5 and 2.6 ans this seems robust enough there.
python3.1 introduces some simpler ways to achieve that but there is no urgency for that :)

This should assist plugin developers working on several branches in paralle.

To post a comment you must log in.
Revision history for this message
Gary van der Merwe (garyvdm) wrote :

This will be very usefull.
I tested it, and works as expected.

Revision history for this message
Martin Pool (mbp) wrote :

looks ok

Gary, feel free to approve things

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2010-03-24 07:27:44 +0000
+++ NEWS 2010-03-24 14:00:53 +0000
@@ -58,10 +58,20 @@
58 a list of plugin names separated by ':' (';' on windows).58 a list of plugin names separated by ':' (';' on windows).
59 (Vincent Ladeuil, #411413)59 (Vincent Ladeuil, #411413)
6060
61<<<<<<< TREE
61* Tag names can now be determined automatically by ``automatic_tag_name`` 62* Tag names can now be determined automatically by ``automatic_tag_name``
62 hooks on ``Branch`` if they are not specified on the command line.63 hooks on ``Branch`` if they are not specified on the command line.
63 (Jelmer Vernooij)64 (Jelmer Vernooij)
6465
66=======
67* Plugins can be loaded from arbitrary locations by defining
68 ``BZR_PLUGINS_AT`` as a list of name@path separated by ':' (';' on
69 windows). This takes precedence over ``BZR_PLUGIN_PATH`` for the
70 specified plugins. This is targeted at plugin developers for punctual
71 needs and *not* intended to replace ``BZR_PLUGIN_PATH``.
72 (Vincent Ladeuil, #82693)
73
74>>>>>>> MERGE-SOURCE
65* Tree-shape conflicts can be resolved by providing ``--take-this`` and75* Tree-shape conflicts can be resolved by providing ``--take-this`` and
66 ``--take-other`` to the ``bzr resolve`` command. Just marking the conflict76 ``--take-other`` to the ``bzr resolve`` command. Just marking the conflict
67 as resolved is still accessible via the ``--done`` default action.77 as resolved is still accessible via the ``--done`` default action.
6878
=== modified file 'bzrlib/help_topics/en/configuration.txt'
--- bzrlib/help_topics/en/configuration.txt 2010-03-19 12:09:05 +0000
+++ bzrlib/help_topics/en/configuration.txt 2010-03-24 14:00:53 +0000
@@ -120,10 +120,10 @@
120BZR_DISABLE_PLUGINS120BZR_DISABLE_PLUGINS
121~~~~~~~~~~~~~~~~~~~121~~~~~~~~~~~~~~~~~~~
122122
123Under special circumstances, it's better to disable a plugin (or123Under special circumstances (mostly when trying to diagnose a
124several) rather than uninstalling them completely. Such plugins124bug), it's better to disable a plugin (or several) rather than
125can be specified in the ``BZR_DISABLE_PLUGINS`` environment125uninstalling them completely. Such plugins can be specified in
126variable.126the ``BZR_DISABLE_PLUGINS`` environment variable.
127127
128In that case, ``bzr`` will stop loading the specified plugins and128In that case, ``bzr`` will stop loading the specified plugins and
129will raise an import error if they are explicitly imported (by129will raise an import error if they are explicitly imported (by
@@ -133,6 +133,32 @@
133133
134 BZR_DISABLE_PLUGINS='myplugin:yourplugin'134 BZR_DISABLE_PLUGINS='myplugin:yourplugin'
135135
136BZR_PLUGINS_AT
137~~~~~~~~~~~~~~
138
139When adding a new feature or working on a bug in a plugin,
140developers often need to use a specific version of a given
141plugin. Since python requires that the directory containing the
142code is named like the plugin itself this make it impossible to
143use arbitrary directory names (using a two-level directory scheme
144is inconvenient). ``BZR_PLUGINS_AT`` allows such directories even
145if they don't appear in ``BZR_PLUGIN_PATH`` .
146
147Plugins specified in this environment variable takes precedence
148over the ones in ``BZR_PLUGIN_PATH``.
149
150The variable specified a list of ``plugin_name@plugin path``,
151``plugin_name`` being the name of the plugin as it appears in
152python module paths, ``plugin_path`` being the path to the
153directory containing the plugin code itself
154(i.e. ``plugins/myplugin`` not ``plugins``). Use ':' as the list
155separator, use ';' on windows.
156
157Example:
158~~~~~~~~
159
160Using a specific version of ``myplugin``:
161``BZR_PLUGINS_AT='myplugin@/home/me/bugfixes/123456-myplugin``
136162
137BZRPATH163BZRPATH
138~~~~~~~164~~~~~~~
139165
=== modified file 'bzrlib/plugin.py'
--- bzrlib/plugin.py 2010-03-17 07:16:32 +0000
+++ bzrlib/plugin.py 2010-03-24 14:00:53 +0000
@@ -91,12 +91,19 @@
91 if path is None:91 if path is None:
92 path = get_standard_plugins_path()92 path = get_standard_plugins_path()
93 _mod_plugins.__path__ = path93 _mod_plugins.__path__ = path
94 # Set up a blacklist for disabled plugins if any94 PluginImporter.reset()
95 PluginBlackListImporter.blacklist = {}95 # Set up a blacklist for disabled plugins
96 disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None)96 disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None)
97 if disabled_plugins is not None:97 if disabled_plugins is not None:
98 for name in disabled_plugins.split(os.pathsep):98 for name in disabled_plugins.split(os.pathsep):
99 PluginBlackListImporter.blacklist['bzrlib.plugins.' + name] = True99 PluginImporter.blacklist.add('bzrlib.plugins.' + name)
100 # Set up a the specific paths for plugins
101 specific_plugins = os.environ.get('BZR_PLUGINS_AT', None)
102 if specific_plugins is not None:
103 for spec in specific_plugins.split(os.pathsep):
104 plugin_name, plugin_path = spec.split('@')
105 PluginImporter.specific_paths[
106 'bzrlib.plugins.%s' % plugin_name] = plugin_path
100 return path107 return path
101108
102109
@@ -237,6 +244,11 @@
237244
238 The python module path for bzrlib.plugins will be modified to be 'dirs'.245 The python module path for bzrlib.plugins will be modified to be 'dirs'.
239 """246 """
247 # Explicitly load the plugins with a specific path
248 for fullname, path in PluginImporter.specific_paths.iteritems():
249 name = fullname[len('bzrlib.plugins.'):]
250 _load_plugin_module(name, path)
251
240 # We need to strip the trailing separators here as well as in the252 # We need to strip the trailing separators here as well as in the
241 # set_plugins_path function because calling code can pass anything in to253 # set_plugins_path function because calling code can pass anything in to
242 # this function, and since it sets plugins.__path__, it should set it to254 # this function, and since it sets plugins.__path__, it should set it to
@@ -256,72 +268,99 @@
256load_from_dirs = load_from_path268load_from_dirs = load_from_path
257269
258270
271def _find_plugin_module(dir, name):
272 """Check if there is a valid python module that can be loaded as a plugin.
273
274 :param dir: The directory where the search is performed.
275 :param path: An existing file path, either a python file or a package
276 directory.
277
278 :return: (name, path, description) name is the module name, path is the
279 file to load and description is the tuple returned by
280 imp.get_suffixes().
281 """
282 path = osutils.pathjoin(dir, name)
283 if os.path.isdir(path):
284 # Check for a valid __init__.py file, valid suffixes depends on -O and
285 # can be .py, .pyc and .pyo
286 for suffix, mode, kind in imp.get_suffixes():
287 if kind not in (imp.PY_SOURCE, imp.PY_COMPILED):
288 # We don't recognize compiled modules (.so, .dll, etc)
289 continue
290 init_path = osutils.pathjoin(path, '__init__' + suffix)
291 if os.path.isfile(init_path):
292 return name, init_path, (suffix, mode, kind)
293 else:
294 for suffix, mode, kind in imp.get_suffixes():
295 if name.endswith(suffix):
296 # Clean up the module name
297 name = name[:-len(suffix)]
298 if kind == imp.C_EXTENSION and name.endswith('module'):
299 name = name[:-len('module')]
300 return name, path, (suffix, mode, kind)
301 # There is no python module here
302 return None, None, (None, None, None)
303
304
305def _load_plugin_module(name, dir):
306 """Load plugine name from dir.
307
308 :param name: The plugin name in the bzrlib.plugins namespace.
309 :param dir: The directory the plugin is loaded from for error messages.
310 """
311 if ('bzrlib.plugins.%s' % name) in PluginImporter.blacklist:
312 return
313 try:
314 exec "import bzrlib.plugins.%s" % name in {}
315 except KeyboardInterrupt:
316 raise
317 except errors.IncompatibleAPI, e:
318 trace.warning("Unable to load plugin %r. It requested API version "
319 "%s of module %s but the minimum exported version is %s, and "
320 "the maximum is %s" %
321 (name, e.wanted, e.api, e.minimum, e.current))
322 except Exception, e:
323 trace.warning("%s" % e)
324 if re.search('\.|-| ', name):
325 sanitised_name = re.sub('[-. ]', '_', name)
326 if sanitised_name.startswith('bzr_'):
327 sanitised_name = sanitised_name[len('bzr_'):]
328 trace.warning("Unable to load %r in %r as a plugin because the "
329 "file path isn't a valid module name; try renaming "
330 "it to %r." % (name, dir, sanitised_name))
331 else:
332 trace.warning('Unable to load plugin %r from %r' % (name, dir))
333 trace.log_exception_quietly()
334 if 'error' in debug.debug_flags:
335 trace.print_exception(sys.exc_info(), sys.stderr)
336
337
259def load_from_dir(d):338def load_from_dir(d):
260 """Load the plugins in directory d.339 """Load the plugins in directory d.
261340
262 d must be in the plugins module path already.341 d must be in the plugins module path already.
342 This function is called once for each directory in the module path.
263 """343 """
264 # Get the list of valid python suffixes for __init__.py?
265 # this includes .py, .pyc, and .pyo (depending on if we are running -O)
266 # but it doesn't include compiled modules (.so, .dll, etc)
267 valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes()
268 if flags in (imp.PY_SOURCE, imp.PY_COMPILED)]
269 package_entries = ['__init__'+suffix for suffix in valid_suffixes]
270 plugin_names = set()344 plugin_names = set()
271 for f in os.listdir(d):345 for p in os.listdir(d):
272 path = osutils.pathjoin(d, f)346 name, path, desc = _find_plugin_module(d, p)
273 if os.path.isdir(path):347 if name is not None:
274 for entry in package_entries:348 if name == '__init__':
275 # This directory should be a package, and thus added to349 # We do nothing with the __init__.py file in directories from
276 # the list350 # the bzrlib.plugins module path, we may want to, one day
277 if os.path.isfile(osutils.pathjoin(path, entry)):351 # -- vila 20100316.
278 break352 continue # We don't load __init__.py in the plugins dirs
279 else: # This directory is not a package353 elif getattr(_mod_plugins, name, None) is not None:
280 continue354 # The module has already been loaded from another directory
281 else:355 # during a previous call.
282 for suffix_info in imp.get_suffixes():356 # FIXME: There should be a better way to report masked plugins
283 if f.endswith(suffix_info[0]):357 # -- vila 20100316
284 f = f[:-len(suffix_info[0])]358 trace.mutter('Plugin name %s already loaded', name)
285 if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'):
286 f = f[:-len('module')]
287 break
288 else:359 else:
289 continue360 plugin_names.add(name)
290 if f == '__init__':
291 continue # We don't load __init__.py again in the plugin dir
292 elif getattr(_mod_plugins, f, None):
293 trace.mutter('Plugin name %s already loaded', f)
294 else:
295 # trace.mutter('add plugin name %s', f)
296 plugin_names.add(f)
297361
298 for name in plugin_names:362 for name in plugin_names:
299 if ('bzrlib.plugins.%s' % name) in PluginBlackListImporter.blacklist:363 _load_plugin_module(name, d)
300 continue
301 try:
302 exec "import bzrlib.plugins.%s" % name in {}
303 except KeyboardInterrupt:
304 raise
305 except errors.IncompatibleAPI, e:
306 trace.warning("Unable to load plugin %r. It requested API version "
307 "%s of module %s but the minimum exported version is %s, and "
308 "the maximum is %s" %
309 (name, e.wanted, e.api, e.minimum, e.current))
310 except Exception, e:
311 trace.warning("%s" % e)
312 ## import pdb; pdb.set_trace()
313 if re.search('\.|-| ', name):
314 sanitised_name = re.sub('[-. ]', '_', name)
315 if sanitised_name.startswith('bzr_'):
316 sanitised_name = sanitised_name[len('bzr_'):]
317 trace.warning("Unable to load %r in %r as a plugin because the "
318 "file path isn't a valid module name; try renaming "
319 "it to %r." % (name, d, sanitised_name))
320 else:
321 trace.warning('Unable to load plugin %r from %r' % (name, d))
322 trace.log_exception_quietly()
323 if 'error' in debug.debug_flags:
324 trace.print_exception(sys.exc_info(), sys.stderr)
325364
326365
327def plugins():366def plugins():
@@ -486,17 +525,73 @@
486 __version__ = property(_get__version__)525 __version__ = property(_get__version__)
487526
488527
489class _PluginBlackListImporter(object):528class _PluginImporter(object):
529 """An importer tailored to bzr specific needs.
530
531 This is a singleton that takes care of:
532 - disabled plugins specified in 'blacklist',
533 - plugins that needs to be loaded from specific directories.
534 """
490535
491 def __init__(self):536 def __init__(self):
492 self.blacklist = {}537 self.reset()
538
539 def reset(self):
540 self.blacklist = set()
541 self.specific_paths = {}
493542
494 def find_module(self, fullname, parent_path=None):543 def find_module(self, fullname, parent_path=None):
544 """Search a plugin module.
545
546 Disabled plugins raise an import error, plugins with specific paths
547 returns a specific loader.
548
549 :return: None if the plugin doesn't need special handling, self
550 otherwise.
551 """
552 if not fullname.startswith('bzrlib.plugins.'):
553 return None
495 if fullname in self.blacklist:554 if fullname in self.blacklist:
496 raise ImportError('%s is disabled' % fullname)555 raise ImportError('%s is disabled' % fullname)
556 if fullname in self.specific_paths:
557 return self
497 return None558 return None
498559
499PluginBlackListImporter = _PluginBlackListImporter()560 def load_module(self, fullname):
500sys.meta_path.append(PluginBlackListImporter)561 """Load a plugin from a specific directory."""
501562 # We are called only for specific paths
502563 plugin_dir = self.specific_paths[fullname]
564 candidate = None
565 maybe_package = False
566 for p in os.listdir(plugin_dir):
567 if os.path.isdir(osutils.pathjoin(plugin_dir, p)):
568 # We're searching for files only and don't want submodules to
569 # be recognized as plugins (they are submodules inside the
570 # plugin).
571 continue
572 name, path, (
573 suffix, mode, kind) = _find_plugin_module(plugin_dir, p)
574 if name is not None:
575 candidate = (name, path, suffix, mode, kind)
576 if kind == imp.PY_SOURCE:
577 # We favour imp.PY_SOURCE (which will use the compiled
578 # version if available) over imp.PY_COMPILED (which is used
579 # only if the source is not available)
580 break
581 if candidate is None:
582 raise ImportError('%s cannot be loaded from %s'
583 % (fullname, plugin_dir))
584 f = open(path, mode)
585 try:
586 mod = imp.load_module(fullname, f, path, (suffix, mode, kind))
587 # The plugin can contain modules, so be ready
588 mod.__path__ = [plugin_dir]
589 mod.__package__ = fullname
590 return mod
591 finally:
592 f.close()
593
594
595# Install a dedicated importer for plugins requiring special handling
596PluginImporter = _PluginImporter()
597sys.meta_path.append(PluginImporter)
503598
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2010-03-24 07:27:44 +0000
+++ bzrlib/tests/__init__.py 2010-03-24 14:00:53 +0000
@@ -1520,6 +1520,7 @@
1520 'BZR_LOG': None,1520 'BZR_LOG': None,
1521 'BZR_PLUGIN_PATH': None,1521 'BZR_PLUGIN_PATH': None,
1522 'BZR_DISABLE_PLUGINS': None,1522 'BZR_DISABLE_PLUGINS': None,
1523 'BZR_PLUGINS_AT': None,
1523 'BZR_CONCURRENCY': None,1524 'BZR_CONCURRENCY': None,
1524 # Make sure that any text ui tests are consistent regardless of1525 # Make sure that any text ui tests are consistent regardless of
1525 # the environment the test case is run in; you may want tests that1526 # the environment the test case is run in; you may want tests that
15261527
=== modified file 'bzrlib/tests/test_plugins.py'
--- bzrlib/tests/test_plugins.py 2010-03-17 07:16:32 +0000
+++ bzrlib/tests/test_plugins.py 2010-03-24 14:00:53 +0000
@@ -39,7 +39,11 @@
3939
40class TestPluginMixin(object):40class TestPluginMixin(object):
4141
42 def create_plugin(self, name, source='', dir='.', file_name=None):42 def create_plugin(self, name, source=None, dir='.', file_name=None):
43 if source is None:
44 source = '''\
45"""This is the doc for %s"""
46''' % (name)
43 if file_name is None:47 if file_name is None:
44 file_name = name + '.py'48 file_name = name + '.py'
45 # 'source' must not fail to load49 # 'source' must not fail to load
@@ -51,11 +55,20 @@
51 finally:55 finally:
52 f.close()56 f.close()
5357
54 def create_plugin_package(self, name, source='', dir='.'):58 def create_plugin_package(self, name, dir=None, source=None):
55 plugin_dir = osutils.pathjoin(dir, name)59 if dir is None:
56 os.mkdir(plugin_dir)60 dir = name
57 self.addCleanup(osutils.rmtree, plugin_dir)61 if source is None:
58 self.create_plugin(name, source, dir=plugin_dir,62 source = '''\
63"""This is the doc for %s"""
64dir_source = '%s'
65''' % (name, dir)
66 os.makedirs(dir)
67 def cleanup():
68 # Workaround lazy import random? madness
69 osutils.rmtree(dir)
70 self.addCleanup(cleanup)
71 self.create_plugin(name, source, dir,
59 file_name='__init__.py')72 file_name='__init__.py')
6073
61 def _unregister_plugin(self, name):74 def _unregister_plugin(self, name):
@@ -767,16 +780,89 @@
767 self.overrideAttr(plugin, '_loaded', False)780 self.overrideAttr(plugin, '_loaded', False)
768 plugin.load_plugins(['.'])781 plugin.load_plugins(['.'])
769 self.assertPluginKnown('test_foo')782 self.assertPluginKnown('test_foo')
783 self.assertEqual("This is the doc for test_foo",
784 bzrlib.plugins.test_foo.__doc__)
770785
771 def test_not_loaded(self):786 def test_not_loaded(self):
772 self.warnings = []787 self.warnings = []
773 def captured_warning(*args, **kwargs):788 def captured_warning(*args, **kwargs):
774 self.warnings.append((args, kwargs))789 self.warnings.append((args, kwargs))
775 self.overrideAttr(trace, 'warning', captured_warning)790 self.overrideAttr(trace, 'warning', captured_warning)
791 # Reset the flag that protect against double loading
776 self.overrideAttr(plugin, '_loaded', False)792 self.overrideAttr(plugin, '_loaded', False)
777 osutils.set_or_unset_env('BZR_DISABLE_PLUGINS', 'test_foo')793 osutils.set_or_unset_env('BZR_DISABLE_PLUGINS', 'test_foo')
778 plugin.load_plugins(plugin.set_plugins_path(['.']))794 plugin.load_plugins(['.'])
779 self.assertPluginUnknown('test_foo')795 self.assertPluginUnknown('test_foo')
780 # Make sure we don't warn about the plugin ImportError since this has796 # Make sure we don't warn about the plugin ImportError since this has
781 # been *requested* by the user.797 # been *requested* by the user.
782 self.assertLength(0, self.warnings)798 self.assertLength(0, self.warnings)
799
800
801class TestLoadPluginAt(tests.TestCaseInTempDir, TestPluginMixin):
802
803 def setUp(self):
804 super(TestLoadPluginAt, self).setUp()
805 # Make sure we don't pollute the plugins namespace
806 self.overrideAttr(plugins, '__path__')
807 # Be paranoid in case a test fail
808 self.addCleanup(self._unregister_plugin, 'test_foo')
809 # Reset the flag that protect against double loading
810 self.overrideAttr(plugin, '_loaded', False)
811 # Create the same plugin in two directories
812 self.create_plugin_package('test_foo', dir='non-standard-dir')
813 self.create_plugin_package('test_foo', dir='b/test_foo')
814
815 def assertTestFooLoadedFrom(self, dir):
816 self.assertPluginKnown('test_foo')
817 self.assertEqual('This is the doc for test_foo',
818 bzrlib.plugins.test_foo.__doc__)
819 self.assertEqual(dir, bzrlib.plugins.test_foo.dir_source)
820
821 def test_regular_load(self):
822 plugin.load_plugins(['b'])
823 self.assertTestFooLoadedFrom('b/test_foo')
824
825 def test_import(self):
826 osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
827 plugin.set_plugins_path(['b'])
828 try:
829 import bzrlib.plugins.test_foo
830 except ImportError:
831 pass
832 self.assertTestFooLoadedFrom('non-standard-dir')
833
834 def test_loading(self):
835 osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
836 plugin.load_plugins(['b'])
837 self.assertTestFooLoadedFrom('non-standard-dir')
838
839 def test_compiled_loaded(self):
840 osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
841 plugin.load_plugins(['b'])
842 self.assertTestFooLoadedFrom('non-standard-dir')
843 self.assertEqual('non-standard-dir/__init__.py',
844 bzrlib.plugins.test_foo.__file__)
845
846 # Try importing again now that the source has been compiled
847 self._unregister_plugin('test_foo')
848 plugin._loaded = False
849 plugin.load_plugins(['b'])
850 self.assertTestFooLoadedFrom('non-standard-dir')
851 if __debug__:
852 suffix = 'pyc'
853 else:
854 suffix = 'pyo'
855 self.assertEqual('non-standard-dir/__init__.%s' % suffix,
856 bzrlib.plugins.test_foo.__file__)
857
858 def test_submodule_loading(self):
859 # We create an additional directory under the one for test_foo
860 self.create_plugin_package('test_bar', dir='non-standard-dir/test_bar')
861 osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
862 plugin.set_plugins_path(['b'])
863 import bzrlib.plugins.test_foo
864 self.assertEqual('bzrlib.plugins.test_foo',
865 bzrlib.plugins.test_foo.__package__)
866 import bzrlib.plugins.test_foo.test_bar
867 self.assertEqual('non-standard-dir/test_bar/__init__.py',
868 bzrlib.plugins.test_foo.test_bar.__file__)