Merge lp:~vila/bzr/82693-plugin-at-path into lp:bzr
- 82693-plugin-at-path
- Merge into bzr.dev
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Pool | Approve | ||
Review via email: mp+21547@code.launchpad.net |
Commit message
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_
+
+The variable specified a list of ``plugin_
+``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/
+separator, use ';' on windows.
This requires https:/
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.
Gary van der Merwe (garyvdm) wrote : | # |
Martin Pool (mbp) wrote : | # |
looks ok
Gary, feel free to approve things
Preview Diff
1 | === modified file 'NEWS' | |||
2 | --- NEWS 2010-03-24 07:27:44 +0000 | |||
3 | +++ NEWS 2010-03-24 14:00:53 +0000 | |||
4 | @@ -58,10 +58,20 @@ | |||
5 | 58 | a list of plugin names separated by ':' (';' on windows). | 58 | a list of plugin names separated by ':' (';' on windows). |
6 | 59 | (Vincent Ladeuil, #411413) | 59 | (Vincent Ladeuil, #411413) |
7 | 60 | 60 | ||
8 | 61 | <<<<<<< TREE | ||
9 | 61 | * Tag names can now be determined automatically by ``automatic_tag_name`` | 62 | * Tag names can now be determined automatically by ``automatic_tag_name`` |
10 | 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. |
11 | 63 | (Jelmer Vernooij) | 64 | (Jelmer Vernooij) |
12 | 64 | 65 | ||
13 | 66 | ======= | ||
14 | 67 | * Plugins can be loaded from arbitrary locations by defining | ||
15 | 68 | ``BZR_PLUGINS_AT`` as a list of name@path separated by ':' (';' on | ||
16 | 69 | windows). This takes precedence over ``BZR_PLUGIN_PATH`` for the | ||
17 | 70 | specified plugins. This is targeted at plugin developers for punctual | ||
18 | 71 | needs and *not* intended to replace ``BZR_PLUGIN_PATH``. | ||
19 | 72 | (Vincent Ladeuil, #82693) | ||
20 | 73 | |||
21 | 74 | >>>>>>> MERGE-SOURCE | ||
22 | 65 | * Tree-shape conflicts can be resolved by providing ``--take-this`` and | 75 | * Tree-shape conflicts can be resolved by providing ``--take-this`` and |
23 | 66 | ``--take-other`` to the ``bzr resolve`` command. Just marking the conflict | 76 | ``--take-other`` to the ``bzr resolve`` command. Just marking the conflict |
24 | 67 | as resolved is still accessible via the ``--done`` default action. | 77 | as resolved is still accessible via the ``--done`` default action. |
25 | 68 | 78 | ||
26 | === modified file 'bzrlib/help_topics/en/configuration.txt' | |||
27 | --- bzrlib/help_topics/en/configuration.txt 2010-03-19 12:09:05 +0000 | |||
28 | +++ bzrlib/help_topics/en/configuration.txt 2010-03-24 14:00:53 +0000 | |||
29 | @@ -120,10 +120,10 @@ | |||
30 | 120 | BZR_DISABLE_PLUGINS | 120 | BZR_DISABLE_PLUGINS |
31 | 121 | ~~~~~~~~~~~~~~~~~~~ | 121 | ~~~~~~~~~~~~~~~~~~~ |
32 | 122 | 122 | ||
37 | 123 | Under special circumstances, it's better to disable a plugin (or | 123 | Under special circumstances (mostly when trying to diagnose a |
38 | 124 | several) rather than uninstalling them completely. Such plugins | 124 | bug), it's better to disable a plugin (or several) rather than |
39 | 125 | can be specified in the ``BZR_DISABLE_PLUGINS`` environment | 125 | uninstalling them completely. Such plugins can be specified in |
40 | 126 | variable. | 126 | the ``BZR_DISABLE_PLUGINS`` environment variable. |
41 | 127 | 127 | ||
42 | 128 | In that case, ``bzr`` will stop loading the specified plugins and | 128 | In that case, ``bzr`` will stop loading the specified plugins and |
43 | 129 | will raise an import error if they are explicitly imported (by | 129 | will raise an import error if they are explicitly imported (by |
44 | @@ -133,6 +133,32 @@ | |||
45 | 133 | 133 | ||
46 | 134 | BZR_DISABLE_PLUGINS='myplugin:yourplugin' | 134 | BZR_DISABLE_PLUGINS='myplugin:yourplugin' |
47 | 135 | 135 | ||
48 | 136 | BZR_PLUGINS_AT | ||
49 | 137 | ~~~~~~~~~~~~~~ | ||
50 | 138 | |||
51 | 139 | When adding a new feature or working on a bug in a plugin, | ||
52 | 140 | developers often need to use a specific version of a given | ||
53 | 141 | plugin. Since python requires that the directory containing the | ||
54 | 142 | code is named like the plugin itself this make it impossible to | ||
55 | 143 | use arbitrary directory names (using a two-level directory scheme | ||
56 | 144 | is inconvenient). ``BZR_PLUGINS_AT`` allows such directories even | ||
57 | 145 | if they don't appear in ``BZR_PLUGIN_PATH`` . | ||
58 | 146 | |||
59 | 147 | Plugins specified in this environment variable takes precedence | ||
60 | 148 | over the ones in ``BZR_PLUGIN_PATH``. | ||
61 | 149 | |||
62 | 150 | The variable specified a list of ``plugin_name@plugin path``, | ||
63 | 151 | ``plugin_name`` being the name of the plugin as it appears in | ||
64 | 152 | python module paths, ``plugin_path`` being the path to the | ||
65 | 153 | directory containing the plugin code itself | ||
66 | 154 | (i.e. ``plugins/myplugin`` not ``plugins``). Use ':' as the list | ||
67 | 155 | separator, use ';' on windows. | ||
68 | 156 | |||
69 | 157 | Example: | ||
70 | 158 | ~~~~~~~~ | ||
71 | 159 | |||
72 | 160 | Using a specific version of ``myplugin``: | ||
73 | 161 | ``BZR_PLUGINS_AT='myplugin@/home/me/bugfixes/123456-myplugin`` | ||
74 | 136 | 162 | ||
75 | 137 | BZRPATH | 163 | BZRPATH |
76 | 138 | ~~~~~~~ | 164 | ~~~~~~~ |
77 | 139 | 165 | ||
78 | === modified file 'bzrlib/plugin.py' | |||
79 | --- bzrlib/plugin.py 2010-03-17 07:16:32 +0000 | |||
80 | +++ bzrlib/plugin.py 2010-03-24 14:00:53 +0000 | |||
81 | @@ -91,12 +91,19 @@ | |||
82 | 91 | if path is None: | 91 | if path is None: |
83 | 92 | path = get_standard_plugins_path() | 92 | path = get_standard_plugins_path() |
84 | 93 | _mod_plugins.__path__ = path | 93 | _mod_plugins.__path__ = path |
87 | 94 | # Set up a blacklist for disabled plugins if any | 94 | PluginImporter.reset() |
88 | 95 | PluginBlackListImporter.blacklist = {} | 95 | # Set up a blacklist for disabled plugins |
89 | 96 | disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None) | 96 | disabled_plugins = os.environ.get('BZR_DISABLE_PLUGINS', None) |
90 | 97 | if disabled_plugins is not None: | 97 | if disabled_plugins is not None: |
91 | 98 | for name in disabled_plugins.split(os.pathsep): | 98 | for name in disabled_plugins.split(os.pathsep): |
93 | 99 | PluginBlackListImporter.blacklist['bzrlib.plugins.' + name] = True | 99 | PluginImporter.blacklist.add('bzrlib.plugins.' + name) |
94 | 100 | # Set up a the specific paths for plugins | ||
95 | 101 | specific_plugins = os.environ.get('BZR_PLUGINS_AT', None) | ||
96 | 102 | if specific_plugins is not None: | ||
97 | 103 | for spec in specific_plugins.split(os.pathsep): | ||
98 | 104 | plugin_name, plugin_path = spec.split('@') | ||
99 | 105 | PluginImporter.specific_paths[ | ||
100 | 106 | 'bzrlib.plugins.%s' % plugin_name] = plugin_path | ||
101 | 100 | return path | 107 | return path |
102 | 101 | 108 | ||
103 | 102 | 109 | ||
104 | @@ -237,6 +244,11 @@ | |||
105 | 237 | 244 | ||
106 | 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'. |
107 | 239 | """ | 246 | """ |
108 | 247 | # Explicitly load the plugins with a specific path | ||
109 | 248 | for fullname, path in PluginImporter.specific_paths.iteritems(): | ||
110 | 249 | name = fullname[len('bzrlib.plugins.'):] | ||
111 | 250 | _load_plugin_module(name, path) | ||
112 | 251 | |||
113 | 240 | # We need to strip the trailing separators here as well as in the | 252 | # We need to strip the trailing separators here as well as in the |
114 | 241 | # set_plugins_path function because calling code can pass anything in to | 253 | # set_plugins_path function because calling code can pass anything in to |
115 | 242 | # this function, and since it sets plugins.__path__, it should set it to | 254 | # this function, and since it sets plugins.__path__, it should set it to |
116 | @@ -256,72 +268,99 @@ | |||
117 | 256 | load_from_dirs = load_from_path | 268 | load_from_dirs = load_from_path |
118 | 257 | 269 | ||
119 | 258 | 270 | ||
120 | 271 | def _find_plugin_module(dir, name): | ||
121 | 272 | """Check if there is a valid python module that can be loaded as a plugin. | ||
122 | 273 | |||
123 | 274 | :param dir: The directory where the search is performed. | ||
124 | 275 | :param path: An existing file path, either a python file or a package | ||
125 | 276 | directory. | ||
126 | 277 | |||
127 | 278 | :return: (name, path, description) name is the module name, path is the | ||
128 | 279 | file to load and description is the tuple returned by | ||
129 | 280 | imp.get_suffixes(). | ||
130 | 281 | """ | ||
131 | 282 | path = osutils.pathjoin(dir, name) | ||
132 | 283 | if os.path.isdir(path): | ||
133 | 284 | # Check for a valid __init__.py file, valid suffixes depends on -O and | ||
134 | 285 | # can be .py, .pyc and .pyo | ||
135 | 286 | for suffix, mode, kind in imp.get_suffixes(): | ||
136 | 287 | if kind not in (imp.PY_SOURCE, imp.PY_COMPILED): | ||
137 | 288 | # We don't recognize compiled modules (.so, .dll, etc) | ||
138 | 289 | continue | ||
139 | 290 | init_path = osutils.pathjoin(path, '__init__' + suffix) | ||
140 | 291 | if os.path.isfile(init_path): | ||
141 | 292 | return name, init_path, (suffix, mode, kind) | ||
142 | 293 | else: | ||
143 | 294 | for suffix, mode, kind in imp.get_suffixes(): | ||
144 | 295 | if name.endswith(suffix): | ||
145 | 296 | # Clean up the module name | ||
146 | 297 | name = name[:-len(suffix)] | ||
147 | 298 | if kind == imp.C_EXTENSION and name.endswith('module'): | ||
148 | 299 | name = name[:-len('module')] | ||
149 | 300 | return name, path, (suffix, mode, kind) | ||
150 | 301 | # There is no python module here | ||
151 | 302 | return None, None, (None, None, None) | ||
152 | 303 | |||
153 | 304 | |||
154 | 305 | def _load_plugin_module(name, dir): | ||
155 | 306 | """Load plugine name from dir. | ||
156 | 307 | |||
157 | 308 | :param name: The plugin name in the bzrlib.plugins namespace. | ||
158 | 309 | :param dir: The directory the plugin is loaded from for error messages. | ||
159 | 310 | """ | ||
160 | 311 | if ('bzrlib.plugins.%s' % name) in PluginImporter.blacklist: | ||
161 | 312 | return | ||
162 | 313 | try: | ||
163 | 314 | exec "import bzrlib.plugins.%s" % name in {} | ||
164 | 315 | except KeyboardInterrupt: | ||
165 | 316 | raise | ||
166 | 317 | except errors.IncompatibleAPI, e: | ||
167 | 318 | trace.warning("Unable to load plugin %r. It requested API version " | ||
168 | 319 | "%s of module %s but the minimum exported version is %s, and " | ||
169 | 320 | "the maximum is %s" % | ||
170 | 321 | (name, e.wanted, e.api, e.minimum, e.current)) | ||
171 | 322 | except Exception, e: | ||
172 | 323 | trace.warning("%s" % e) | ||
173 | 324 | if re.search('\.|-| ', name): | ||
174 | 325 | sanitised_name = re.sub('[-. ]', '_', name) | ||
175 | 326 | if sanitised_name.startswith('bzr_'): | ||
176 | 327 | sanitised_name = sanitised_name[len('bzr_'):] | ||
177 | 328 | trace.warning("Unable to load %r in %r as a plugin because the " | ||
178 | 329 | "file path isn't a valid module name; try renaming " | ||
179 | 330 | "it to %r." % (name, dir, sanitised_name)) | ||
180 | 331 | else: | ||
181 | 332 | trace.warning('Unable to load plugin %r from %r' % (name, dir)) | ||
182 | 333 | trace.log_exception_quietly() | ||
183 | 334 | if 'error' in debug.debug_flags: | ||
184 | 335 | trace.print_exception(sys.exc_info(), sys.stderr) | ||
185 | 336 | |||
186 | 337 | |||
187 | 259 | def load_from_dir(d): | 338 | def load_from_dir(d): |
188 | 260 | """Load the plugins in directory d. | 339 | """Load the plugins in directory d. |
189 | 261 | 340 | ||
190 | 262 | d must be in the plugins module path already. | 341 | d must be in the plugins module path already. |
191 | 342 | This function is called once for each directory in the module path. | ||
192 | 263 | """ | 343 | """ |
193 | 264 | # Get the list of valid python suffixes for __init__.py? | ||
194 | 265 | # this includes .py, .pyc, and .pyo (depending on if we are running -O) | ||
195 | 266 | # but it doesn't include compiled modules (.so, .dll, etc) | ||
196 | 267 | valid_suffixes = [suffix for suffix, mod_type, flags in imp.get_suffixes() | ||
197 | 268 | if flags in (imp.PY_SOURCE, imp.PY_COMPILED)] | ||
198 | 269 | package_entries = ['__init__'+suffix for suffix in valid_suffixes] | ||
199 | 270 | plugin_names = set() | 344 | plugin_names = set() |
217 | 271 | for f in os.listdir(d): | 345 | for p in os.listdir(d): |
218 | 272 | path = osutils.pathjoin(d, f) | 346 | name, path, desc = _find_plugin_module(d, p) |
219 | 273 | if os.path.isdir(path): | 347 | if name is not None: |
220 | 274 | for entry in package_entries: | 348 | if name == '__init__': |
221 | 275 | # This directory should be a package, and thus added to | 349 | # We do nothing with the __init__.py file in directories from |
222 | 276 | # the list | 350 | # the bzrlib.plugins module path, we may want to, one day |
223 | 277 | if os.path.isfile(osutils.pathjoin(path, entry)): | 351 | # -- vila 20100316. |
224 | 278 | break | 352 | continue # We don't load __init__.py in the plugins dirs |
225 | 279 | else: # This directory is not a package | 353 | elif getattr(_mod_plugins, name, None) is not None: |
226 | 280 | continue | 354 | # The module has already been loaded from another directory |
227 | 281 | else: | 355 | # during a previous call. |
228 | 282 | for suffix_info in imp.get_suffixes(): | 356 | # FIXME: There should be a better way to report masked plugins |
229 | 283 | if f.endswith(suffix_info[0]): | 357 | # -- vila 20100316 |
230 | 284 | f = f[:-len(suffix_info[0])] | 358 | trace.mutter('Plugin name %s already loaded', name) |
214 | 285 | if suffix_info[2] == imp.C_EXTENSION and f.endswith('module'): | ||
215 | 286 | f = f[:-len('module')] | ||
216 | 287 | break | ||
231 | 288 | else: | 359 | else: |
240 | 289 | continue | 360 | plugin_names.add(name) |
233 | 290 | if f == '__init__': | ||
234 | 291 | continue # We don't load __init__.py again in the plugin dir | ||
235 | 292 | elif getattr(_mod_plugins, f, None): | ||
236 | 293 | trace.mutter('Plugin name %s already loaded', f) | ||
237 | 294 | else: | ||
238 | 295 | # trace.mutter('add plugin name %s', f) | ||
239 | 296 | plugin_names.add(f) | ||
241 | 297 | 361 | ||
242 | 298 | for name in plugin_names: | 362 | for name in plugin_names: |
269 | 299 | if ('bzrlib.plugins.%s' % name) in PluginBlackListImporter.blacklist: | 363 | _load_plugin_module(name, d) |
244 | 300 | continue | ||
245 | 301 | try: | ||
246 | 302 | exec "import bzrlib.plugins.%s" % name in {} | ||
247 | 303 | except KeyboardInterrupt: | ||
248 | 304 | raise | ||
249 | 305 | except errors.IncompatibleAPI, e: | ||
250 | 306 | trace.warning("Unable to load plugin %r. It requested API version " | ||
251 | 307 | "%s of module %s but the minimum exported version is %s, and " | ||
252 | 308 | "the maximum is %s" % | ||
253 | 309 | (name, e.wanted, e.api, e.minimum, e.current)) | ||
254 | 310 | except Exception, e: | ||
255 | 311 | trace.warning("%s" % e) | ||
256 | 312 | ## import pdb; pdb.set_trace() | ||
257 | 313 | if re.search('\.|-| ', name): | ||
258 | 314 | sanitised_name = re.sub('[-. ]', '_', name) | ||
259 | 315 | if sanitised_name.startswith('bzr_'): | ||
260 | 316 | sanitised_name = sanitised_name[len('bzr_'):] | ||
261 | 317 | trace.warning("Unable to load %r in %r as a plugin because the " | ||
262 | 318 | "file path isn't a valid module name; try renaming " | ||
263 | 319 | "it to %r." % (name, d, sanitised_name)) | ||
264 | 320 | else: | ||
265 | 321 | trace.warning('Unable to load plugin %r from %r' % (name, d)) | ||
266 | 322 | trace.log_exception_quietly() | ||
267 | 323 | if 'error' in debug.debug_flags: | ||
268 | 324 | trace.print_exception(sys.exc_info(), sys.stderr) | ||
270 | 325 | 364 | ||
271 | 326 | 365 | ||
272 | 327 | def plugins(): | 366 | def plugins(): |
273 | @@ -486,17 +525,73 @@ | |||
274 | 486 | __version__ = property(_get__version__) | 525 | __version__ = property(_get__version__) |
275 | 487 | 526 | ||
276 | 488 | 527 | ||
278 | 489 | class _PluginBlackListImporter(object): | 528 | class _PluginImporter(object): |
279 | 529 | """An importer tailored to bzr specific needs. | ||
280 | 530 | |||
281 | 531 | This is a singleton that takes care of: | ||
282 | 532 | - disabled plugins specified in 'blacklist', | ||
283 | 533 | - plugins that needs to be loaded from specific directories. | ||
284 | 534 | """ | ||
285 | 490 | 535 | ||
286 | 491 | def __init__(self): | 536 | def __init__(self): |
288 | 492 | self.blacklist = {} | 537 | self.reset() |
289 | 538 | |||
290 | 539 | def reset(self): | ||
291 | 540 | self.blacklist = set() | ||
292 | 541 | self.specific_paths = {} | ||
293 | 493 | 542 | ||
294 | 494 | def find_module(self, fullname, parent_path=None): | 543 | def find_module(self, fullname, parent_path=None): |
295 | 544 | """Search a plugin module. | ||
296 | 545 | |||
297 | 546 | Disabled plugins raise an import error, plugins with specific paths | ||
298 | 547 | returns a specific loader. | ||
299 | 548 | |||
300 | 549 | :return: None if the plugin doesn't need special handling, self | ||
301 | 550 | otherwise. | ||
302 | 551 | """ | ||
303 | 552 | if not fullname.startswith('bzrlib.plugins.'): | ||
304 | 553 | return None | ||
305 | 495 | if fullname in self.blacklist: | 554 | if fullname in self.blacklist: |
306 | 496 | raise ImportError('%s is disabled' % fullname) | 555 | raise ImportError('%s is disabled' % fullname) |
307 | 556 | if fullname in self.specific_paths: | ||
308 | 557 | return self | ||
309 | 497 | return None | 558 | return None |
310 | 498 | 559 | ||
315 | 499 | PluginBlackListImporter = _PluginBlackListImporter() | 560 | def load_module(self, fullname): |
316 | 500 | sys.meta_path.append(PluginBlackListImporter) | 561 | """Load a plugin from a specific directory.""" |
317 | 501 | 562 | # We are called only for specific paths | |
318 | 502 | 563 | plugin_dir = self.specific_paths[fullname] | |
319 | 564 | candidate = None | ||
320 | 565 | maybe_package = False | ||
321 | 566 | for p in os.listdir(plugin_dir): | ||
322 | 567 | if os.path.isdir(osutils.pathjoin(plugin_dir, p)): | ||
323 | 568 | # We're searching for files only and don't want submodules to | ||
324 | 569 | # be recognized as plugins (they are submodules inside the | ||
325 | 570 | # plugin). | ||
326 | 571 | continue | ||
327 | 572 | name, path, ( | ||
328 | 573 | suffix, mode, kind) = _find_plugin_module(plugin_dir, p) | ||
329 | 574 | if name is not None: | ||
330 | 575 | candidate = (name, path, suffix, mode, kind) | ||
331 | 576 | if kind == imp.PY_SOURCE: | ||
332 | 577 | # We favour imp.PY_SOURCE (which will use the compiled | ||
333 | 578 | # version if available) over imp.PY_COMPILED (which is used | ||
334 | 579 | # only if the source is not available) | ||
335 | 580 | break | ||
336 | 581 | if candidate is None: | ||
337 | 582 | raise ImportError('%s cannot be loaded from %s' | ||
338 | 583 | % (fullname, plugin_dir)) | ||
339 | 584 | f = open(path, mode) | ||
340 | 585 | try: | ||
341 | 586 | mod = imp.load_module(fullname, f, path, (suffix, mode, kind)) | ||
342 | 587 | # The plugin can contain modules, so be ready | ||
343 | 588 | mod.__path__ = [plugin_dir] | ||
344 | 589 | mod.__package__ = fullname | ||
345 | 590 | return mod | ||
346 | 591 | finally: | ||
347 | 592 | f.close() | ||
348 | 593 | |||
349 | 594 | |||
350 | 595 | # Install a dedicated importer for plugins requiring special handling | ||
351 | 596 | PluginImporter = _PluginImporter() | ||
352 | 597 | sys.meta_path.append(PluginImporter) | ||
353 | 503 | 598 | ||
354 | === modified file 'bzrlib/tests/__init__.py' | |||
355 | --- bzrlib/tests/__init__.py 2010-03-24 07:27:44 +0000 | |||
356 | +++ bzrlib/tests/__init__.py 2010-03-24 14:00:53 +0000 | |||
357 | @@ -1520,6 +1520,7 @@ | |||
358 | 1520 | 'BZR_LOG': None, | 1520 | 'BZR_LOG': None, |
359 | 1521 | 'BZR_PLUGIN_PATH': None, | 1521 | 'BZR_PLUGIN_PATH': None, |
360 | 1522 | 'BZR_DISABLE_PLUGINS': None, | 1522 | 'BZR_DISABLE_PLUGINS': None, |
361 | 1523 | 'BZR_PLUGINS_AT': None, | ||
362 | 1523 | 'BZR_CONCURRENCY': None, | 1524 | 'BZR_CONCURRENCY': None, |
363 | 1524 | # Make sure that any text ui tests are consistent regardless of | 1525 | # Make sure that any text ui tests are consistent regardless of |
364 | 1525 | # the environment the test case is run in; you may want tests that | 1526 | # the environment the test case is run in; you may want tests that |
365 | 1526 | 1527 | ||
366 | === modified file 'bzrlib/tests/test_plugins.py' | |||
367 | --- bzrlib/tests/test_plugins.py 2010-03-17 07:16:32 +0000 | |||
368 | +++ bzrlib/tests/test_plugins.py 2010-03-24 14:00:53 +0000 | |||
369 | @@ -39,7 +39,11 @@ | |||
370 | 39 | 39 | ||
371 | 40 | class TestPluginMixin(object): | 40 | class TestPluginMixin(object): |
372 | 41 | 41 | ||
374 | 42 | def create_plugin(self, name, source='', dir='.', file_name=None): | 42 | def create_plugin(self, name, source=None, dir='.', file_name=None): |
375 | 43 | if source is None: | ||
376 | 44 | source = '''\ | ||
377 | 45 | """This is the doc for %s""" | ||
378 | 46 | ''' % (name) | ||
379 | 43 | if file_name is None: | 47 | if file_name is None: |
380 | 44 | file_name = name + '.py' | 48 | file_name = name + '.py' |
381 | 45 | # 'source' must not fail to load | 49 | # 'source' must not fail to load |
382 | @@ -51,11 +55,20 @@ | |||
383 | 51 | finally: | 55 | finally: |
384 | 52 | f.close() | 56 | f.close() |
385 | 53 | 57 | ||
391 | 54 | def create_plugin_package(self, name, source='', dir='.'): | 58 | def create_plugin_package(self, name, dir=None, source=None): |
392 | 55 | plugin_dir = osutils.pathjoin(dir, name) | 59 | if dir is None: |
393 | 56 | os.mkdir(plugin_dir) | 60 | dir = name |
394 | 57 | self.addCleanup(osutils.rmtree, plugin_dir) | 61 | if source is None: |
395 | 58 | self.create_plugin(name, source, dir=plugin_dir, | 62 | source = '''\ |
396 | 63 | """This is the doc for %s""" | ||
397 | 64 | dir_source = '%s' | ||
398 | 65 | ''' % (name, dir) | ||
399 | 66 | os.makedirs(dir) | ||
400 | 67 | def cleanup(): | ||
401 | 68 | # Workaround lazy import random? madness | ||
402 | 69 | osutils.rmtree(dir) | ||
403 | 70 | self.addCleanup(cleanup) | ||
404 | 71 | self.create_plugin(name, source, dir, | ||
405 | 59 | file_name='__init__.py') | 72 | file_name='__init__.py') |
406 | 60 | 73 | ||
407 | 61 | def _unregister_plugin(self, name): | 74 | def _unregister_plugin(self, name): |
408 | @@ -767,16 +780,89 @@ | |||
409 | 767 | self.overrideAttr(plugin, '_loaded', False) | 780 | self.overrideAttr(plugin, '_loaded', False) |
410 | 768 | plugin.load_plugins(['.']) | 781 | plugin.load_plugins(['.']) |
411 | 769 | self.assertPluginKnown('test_foo') | 782 | self.assertPluginKnown('test_foo') |
412 | 783 | self.assertEqual("This is the doc for test_foo", | ||
413 | 784 | bzrlib.plugins.test_foo.__doc__) | ||
414 | 770 | 785 | ||
415 | 771 | def test_not_loaded(self): | 786 | def test_not_loaded(self): |
416 | 772 | self.warnings = [] | 787 | self.warnings = [] |
417 | 773 | def captured_warning(*args, **kwargs): | 788 | def captured_warning(*args, **kwargs): |
418 | 774 | self.warnings.append((args, kwargs)) | 789 | self.warnings.append((args, kwargs)) |
419 | 775 | self.overrideAttr(trace, 'warning', captured_warning) | 790 | self.overrideAttr(trace, 'warning', captured_warning) |
420 | 791 | # Reset the flag that protect against double loading | ||
421 | 776 | self.overrideAttr(plugin, '_loaded', False) | 792 | self.overrideAttr(plugin, '_loaded', False) |
422 | 777 | osutils.set_or_unset_env('BZR_DISABLE_PLUGINS', 'test_foo') | 793 | osutils.set_or_unset_env('BZR_DISABLE_PLUGINS', 'test_foo') |
424 | 778 | plugin.load_plugins(plugin.set_plugins_path(['.'])) | 794 | plugin.load_plugins(['.']) |
425 | 779 | self.assertPluginUnknown('test_foo') | 795 | self.assertPluginUnknown('test_foo') |
426 | 780 | # Make sure we don't warn about the plugin ImportError since this has | 796 | # Make sure we don't warn about the plugin ImportError since this has |
427 | 781 | # been *requested* by the user. | 797 | # been *requested* by the user. |
428 | 782 | self.assertLength(0, self.warnings) | 798 | self.assertLength(0, self.warnings) |
429 | 799 | |||
430 | 800 | |||
431 | 801 | class TestLoadPluginAt(tests.TestCaseInTempDir, TestPluginMixin): | ||
432 | 802 | |||
433 | 803 | def setUp(self): | ||
434 | 804 | super(TestLoadPluginAt, self).setUp() | ||
435 | 805 | # Make sure we don't pollute the plugins namespace | ||
436 | 806 | self.overrideAttr(plugins, '__path__') | ||
437 | 807 | # Be paranoid in case a test fail | ||
438 | 808 | self.addCleanup(self._unregister_plugin, 'test_foo') | ||
439 | 809 | # Reset the flag that protect against double loading | ||
440 | 810 | self.overrideAttr(plugin, '_loaded', False) | ||
441 | 811 | # Create the same plugin in two directories | ||
442 | 812 | self.create_plugin_package('test_foo', dir='non-standard-dir') | ||
443 | 813 | self.create_plugin_package('test_foo', dir='b/test_foo') | ||
444 | 814 | |||
445 | 815 | def assertTestFooLoadedFrom(self, dir): | ||
446 | 816 | self.assertPluginKnown('test_foo') | ||
447 | 817 | self.assertEqual('This is the doc for test_foo', | ||
448 | 818 | bzrlib.plugins.test_foo.__doc__) | ||
449 | 819 | self.assertEqual(dir, bzrlib.plugins.test_foo.dir_source) | ||
450 | 820 | |||
451 | 821 | def test_regular_load(self): | ||
452 | 822 | plugin.load_plugins(['b']) | ||
453 | 823 | self.assertTestFooLoadedFrom('b/test_foo') | ||
454 | 824 | |||
455 | 825 | def test_import(self): | ||
456 | 826 | osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo@non-standard-dir') | ||
457 | 827 | plugin.set_plugins_path(['b']) | ||
458 | 828 | try: | ||
459 | 829 | import bzrlib.plugins.test_foo | ||
460 | 830 | except ImportError: | ||
461 | 831 | pass | ||
462 | 832 | self.assertTestFooLoadedFrom('non-standard-dir') | ||
463 | 833 | |||
464 | 834 | def test_loading(self): | ||
465 | 835 | osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo@non-standard-dir') | ||
466 | 836 | plugin.load_plugins(['b']) | ||
467 | 837 | self.assertTestFooLoadedFrom('non-standard-dir') | ||
468 | 838 | |||
469 | 839 | def test_compiled_loaded(self): | ||
470 | 840 | osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo@non-standard-dir') | ||
471 | 841 | plugin.load_plugins(['b']) | ||
472 | 842 | self.assertTestFooLoadedFrom('non-standard-dir') | ||
473 | 843 | self.assertEqual('non-standard-dir/__init__.py', | ||
474 | 844 | bzrlib.plugins.test_foo.__file__) | ||
475 | 845 | |||
476 | 846 | # Try importing again now that the source has been compiled | ||
477 | 847 | self._unregister_plugin('test_foo') | ||
478 | 848 | plugin._loaded = False | ||
479 | 849 | plugin.load_plugins(['b']) | ||
480 | 850 | self.assertTestFooLoadedFrom('non-standard-dir') | ||
481 | 851 | if __debug__: | ||
482 | 852 | suffix = 'pyc' | ||
483 | 853 | else: | ||
484 | 854 | suffix = 'pyo' | ||
485 | 855 | self.assertEqual('non-standard-dir/__init__.%s' % suffix, | ||
486 | 856 | bzrlib.plugins.test_foo.__file__) | ||
487 | 857 | |||
488 | 858 | def test_submodule_loading(self): | ||
489 | 859 | # We create an additional directory under the one for test_foo | ||
490 | 860 | self.create_plugin_package('test_bar', dir='non-standard-dir/test_bar') | ||
491 | 861 | osutils.set_or_unset_env('BZR_PLUGINS_AT', 'test_foo@non-standard-dir') | ||
492 | 862 | plugin.set_plugins_path(['b']) | ||
493 | 863 | import bzrlib.plugins.test_foo | ||
494 | 864 | self.assertEqual('bzrlib.plugins.test_foo', | ||
495 | 865 | bzrlib.plugins.test_foo.__package__) | ||
496 | 866 | import bzrlib.plugins.test_foo.test_bar | ||
497 | 867 | self.assertEqual('non-standard-dir/test_bar/__init__.py', | ||
498 | 868 | bzrlib.plugins.test_foo.test_bar.__file__) |
This will be very usefull.
I tested it, and works as expected.