Merge lp:~spiv/bzr/hooks-refactoring into lp:bzr

Proposed by Andrew Bennetts
Status: Merged
Merged at revision: 5500
Proposed branch: lp:~spiv/bzr/hooks-refactoring
Merge into: lp:bzr
Diff against target: 786 lines (+334/-124)
16 files modified
NEWS (+9/-1)
bzrlib/bundle/serializer/__init__.py (+5/-3)
bzrlib/bzrdir.py (+3/-5)
bzrlib/export/__init__.py (+5/-3)
bzrlib/hooks.py (+69/-52)
bzrlib/pyutils.py (+91/-0)
bzrlib/registry.py (+7/-16)
bzrlib/repository.py (+4/-3)
bzrlib/tests/TestUtil.py (+3/-12)
bzrlib/tests/__init__.py (+11/-9)
bzrlib/tests/per_transport.py (+2/-6)
bzrlib/tests/test_hooks.py (+15/-2)
bzrlib/tests/test_pyutils.py (+88/-0)
bzrlib/tests/test_registry.py (+8/-0)
bzrlib/tests/test_selftest.py (+0/-12)
doc/developers/code-style.txt (+14/-0)
To merge this branch: bzr merge lp:~spiv/bzr/hooks-refactoring
Reviewer Review Type Date Requested Status
Vincent Ladeuil Needs Information
Martin Pool Needs Fixing
Review via email: mp+36101@code.launchpad.net

Commit message

Add bzrlib/pyutils.py, and replace many awkward (and sometimes buggy) uses of __import__ with calls to bzrlib.pyutils.get_named_object.

Description of the change

While reviewing <https://code.edge.launchpad.net/~parthm/bzr/173274-export-hooks/+merge/35919> I noticed a workaround for a bug in hook collection registration that seemed a bit strange. Digging into it I came to two conclusions:

1. hooks.py was more confusing than necessary
2. _LazyObjectGetter(...).get_obj() was buggy, and that functionality really belonged somewhere more reusable anyway.

This patch definitely addresses 2, but hopefully helps 1 as well.

It adds a new module, bzrlib.pyutils, which has a get_named_object helper for resolving a pair like ('module.submodle', 'attr.another_attr') into the Python object you'd usually access via module.submodule.attr.another_attr (or similar), where the second part is optional and any amount of dots are correctly handled. Importantly, this implementation has *tests*, which means it fixes the bug that _LazyObjectGetter('x.y', None).get_obj() returned x, not x.y. In general if you're doing something more complicated than __import__('top_level_module') with __import__, it's probably easier to use get_named_object instead.

I'm not thrilled by the name of the new module, or the helpers in it. I'm open to suggestions.

The main use of __import__ that I didn't replace is lazy_import.py, partly because it would be a shame to make it less standalone, but mainly because it needs to go to the effort of breaking the import statements down into "import this module, get a reference to module, get this attribute from that module" steps anyway, because it's parsing actual "import" statements in all their varied forms... so get_named_object isn't a drop-in replacement for its current logic.

One thing get_named_object does slightly differently than many __import__ calls it replaces is resolving a module name of 'x.y.z' by importing x.y.z, then fetching it from sys.modules['x.y.z'] directly. Previously we tended to do the __import__, which would return 'x', and then repeatedly getattr to get to z (or abuse __import__ by passing a fake from_list). Both ways should be equivalent barring really evil code, and the Python docs recommend the sys.modules method, and it should be marginally more efficient I think, just in case it matters ;)

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :

I would mention get_named_object in the api section of the news, and in the coding standard, so that people do use it in future.

> 2. _LazyObjectGetter(...).get_obj() was buggy,

Is there an actual bug number or user impact for this, or is it just buggy in the sense of being hard to use correctly (which is of course still worth fixing.)

Knowing the modules where all the hook objects can be found seems a bit roundabout to me compared to just actually having the hooks directly in the registry where they can be saved/restored altogether. But this is a cleaner way to implement it.

+known_hooks.register_lazy_hook('bzrlib.branch', 'Branch.hooks', 'BranchHooks')
197 +known_hooks.register_lazy_hook('bzrlib.bzrdir', 'BzrDir.hooks', 'BzrDirHooks')
198 +known_hooks.register_lazy_hook(
199 + 'bzrlib.commands', 'Command.hooks', 'CommandHooks')

For the sake of readability and to avoid all the line wrapping, could you turn this into a

  for (hook_module, hook_attribute_name, hook_class) in _builtin_known_hooks:
    known_hooks.register_lazy_hook(hook_module, hook_attribute_name, hook_class)

+"""Some convenience functions for general Python, such as a wrapper around
+``_import__``.
+"""

Should be a single line.

+def get_named_object(module_name, member_name=None):

It's a pretty generic name. Perhaps the name should include 'import' to give you a bit more of a clue, like import_named_object?

I think this could do with a docstring example (add it to the list of docstrings to test) and it should be feasible to test that way without too much complication. You need to add it to the list anyhow to make the calc_parent_name doctest run.

+ :param module_name: a module name, as found in sys.module. It may contain
+ dots. e.g. 'sys' or 'os.path'.

This seems to imply that it needs to already be in sys.module, but in fact it's fine to call this with a module that might not already be loaded?

Nice removal of duplications there.

tweak

review: Needs Fixing
Revision history for this message
Andrew Bennetts (spiv) wrote :
Download full text (4.4 KiB)

Martin Pool wrote:
> Review: Needs Fixing
> I would mention get_named_object in the api section of the news, and in the coding standard, so that people do use it in future.

Done.

> > 2. _LazyObjectGetter(...).get_obj() was buggy,
>
> Is there an actual bug number or user impact for this, or is it just buggy in the sense of being hard to use correctly (which is of course still worth fixing.)

No bug number I found, but it came up during a review (of a currently back in
work-in-progress) patch to add pre- and post-export hooks. There was a strange
workaround in the hook registration to do with some sort of interaction with the
hook point being a module-global in a __init__.py, rather than an attribute of
an object.

> Knowing the modules where all the hook objects can be found seems a bit
> roundabout to me compared to just actually having the hooks directly in the
> registry where they can be saved/restored altogether. But this is a cleaner
> way to implement it.

Hmm, quite possibly. I don't have any incentive to worry about that atm, though
:)

> +known_hooks.register_lazy_hook('bzrlib.branch', 'Branch.hooks', 'BranchHooks')
> 197 +known_hooks.register_lazy_hook('bzrlib.bzrdir', 'BzrDir.hooks', 'BzrDirHooks')
> 198 +known_hooks.register_lazy_hook(
> 199 + 'bzrlib.commands', 'Command.hooks', 'CommandHooks')
>
> For the sake of readability and to avoid all the line wrapping, could you turn this into a
>
> for (hook_module, hook_attribute_name, hook_class) in _builtin_known_hooks:
> known_hooks.register_lazy_hook(hook_module, hook_attribute_name, hook_class)

Ok, done, although a couple of them still needed to be wrapped.

> +"""Some convenience functions for general Python, such as a wrapper around
> +``_import__``.
> +"""
>
> Should be a single line.

Really? I don't think you can mean those three lines should be on one, because
that's over 80 columns.

I guess you mean the closing triple-double-quotes should be on the same line as
the final line of the text. I interpret this situation as a multiline
docstring, where AIUI the convention is closing quotes get their own line. I
guess you consider the fact it's one sentence to mean it's more like a single
line docstring?

Anyway, changed, because while I disagree I also don't care very much :)

If it really bugs us too much just add another paragraph to that docstring to
make the situation unambiguous... ;)

> +def get_named_object(module_name, member_name=None):
>
> It's a pretty generic name. Perhaps the name should include 'import' to give
> you a bit more of a clue, like import_named_object?

It is pretty generic :(, but it seems other projects solving this problem have
chosen similarly generic names, so perhaps it's unavoidable. There's some
overlap with twisted.python.util.getNamedAny, and I saw today that unittest2 has
getObjectFromName which is also similar in purpose. So it appears the broader
Python community tends towards this sort of generic name (and this feature
should be part of core Python, clearly...)

I think “import” is perhaps a misleading hint, because that's only part of what
it does.

> I think this could do with a docstring example (add it to the...

Read more...

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (3.2 KiB)

On 28 September 2010 18:37, Andrew Bennetts
<email address hidden> wrote:
>> +"""Some convenience functions for general Python, such as a wrapper around
>> +``_import__``.
>> +"""
>>
>> Should be a single line.
>
> Really?  I don't think you can mean those three lines should be on one, because
> that's over 80 columns.
>
> I guess you mean the closing triple-double-quotes should be on the same line as
> the final line of the text.  I interpret this situation as a multiline
> docstring, where AIUI the convention is closing quotes get their own line.  I
> guess you consider the fact it's one sentence to mean it's more like a single
> line docstring?

No, I meant that I thought the Python convention was that docstrings
should be a single sentence that fits on a single line, such as

"""General Python convenience functions.
"""

Perhaps it doesn't matter if it's on one line and it just needs to be
a single-sentence paragraph?

>
> Anyway, changed, because while I disagree I also don't care very much :)
>
> If it really bugs us too much just add another paragraph to that docstring to
> make the situation unambiguous... ;)
>
>> +def get_named_object(module_name, member_name=None):
>>
>> It's a pretty generic name.  Perhaps the name should include 'import' to give
>> you a bit more of a clue, like import_named_object?
>
> It is pretty generic :(, but it seems other projects solving this problem have
> chosen similarly generic names, so perhaps it's unavoidable.  There's some
> overlap with twisted.python.util.getNamedAny, and I saw today that unittest2 has
> getObjectFromName which is also similar in purpose.  So it appears the broader
> Python community tends towards this sort of generic name (and this feature
> should be part of core Python, clearly...)
>
> I think “import” is perhaps a misleading hint, because that's only part of what
> it does.

ok

>
>> I think this could do with a docstring example (add it to the list of
>> docstrings to test) and it should be feasible to test that way without too
>> much complication.  You need to add it to the list anyhow to make the
>> calc_parent_name doctest run.
>
> I've added a doctest-friendly example to get_named_object, and added to the list
> of doctested modules.
>
> The calc_parent_name example is not a doctest, though.  It's simply
> documentation, because the effort to contrive a doctest-runnable example is a
> bit disproportionate (and so I think would detract from the clarity of the
> example as documentation).  So it fails when I add pyutils to the list of
> doctestable modules, and I have to sprinkle #doctest: +SKIP to avoid that.  The
> docstring example is pretty much verbatim how I expect the actual callers of the
> code to look.
>
>> +    :param module_name: a module name, as found in sys.module.  It may contain
>> +        dots.  e.g. 'sys' or 'os.path'.
>>
>> This seems to imply that it needs to already be in sys.module, but in fact
>> it's fine to call this with a module that might not already be loaded?
>
> Updated to:
>
>    :param module_name: a module name, as would be found in sys.modules if
>        the module is already imported.  It may contain dots.  e.g. 'sys' or
> ...

Read more...

Revision history for this message
Robert Collins (lifeless) wrote :

On Wed, Sep 29, 2010 at 3:43 PM, Martin Pool <email address hidden> wrote:
> No, I meant that I thought the Python convention was that docstrings
> should be a single sentence that fits on a single line, such as
>
> """General Python convenience functions.
> """

I may be misunderstanding, but that particular layout really squicks.

"""General Python convenience functions."""

or
"""General Python convenience functions.

More detail here.
"""

> Perhaps it doesn't matter if it's on one line and it just needs to be
> a single-sentence paragraph?

http://www.python.org/dev/peps/pep-0257/
"The closing quotes are on the same line as the opening quotes. This
looks better for one-liners."

etc.

_Rob

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

I don't personally care very much where the quotes are, but what I was
asking for, was, from pep 257

>> Multi-line docstrings consist of a summary line just like a one-line docstring, followed by a blank line, followed by a more elaborate description. The summary line may be used by automatic indexing tools; it is important that it fits on one line and is separated from the rest of the docstring by a blank line.

If you have eight characters of indent plus six characters of quotes
and a limit around 72-80 you do need to come up with a pretty terse
(down to 58 chars) summary sentence.

--
Martin

Revision history for this message
Robert Collins (lifeless) wrote :

On Wed, Sep 29, 2010 at 4:25 PM, Martin Pool <email address hidden> wrote:
>>> Multi-line docstrings consist of a summary line just like a one-line docstring, followed by a blank line, followed by a more elaborate description. The summary line may be used by automatic indexing tools; it is important that it fits on one line and is separated from the rest of the docstring by a blank line.
>
> If you have eight characters of indent plus six characters of quotes
> and a limit around 72-80 you do need to come up with a pretty terse
> (down to 58 chars) summary sentence.

Yea, it can be quite Zen sometimes. I wonder if trying for koans would help.

-Rob

Revision history for this message
Andrew Bennetts (spiv) wrote :

Martin Pool wrote:
> On 28 September 2010 18:37, Andrew Bennetts
> <email address hidden> wrote:
> >> +"""Some convenience functions for general Python, such as a wrapper around
> >> +``_import__``.
> >> +"""
> >>
> >> Should be a single line.
> >
> > Really?  I don't think you can mean those three lines should be on one, because
> > that's over 80 columns.
> >
> > I guess you mean the closing triple-double-quotes should be on the same line as
> > the final line of the text.  I interpret this situation as a multiline
> > docstring, where AIUI the convention is closing quotes get their own line.  I
> > guess you consider the fact it's one sentence to mean it's more like a single
> > line docstring?
>
> No, I meant that I thought the Python convention was that docstrings
> should be a single sentence that fits on a single line, such as
>
> """General Python convenience functions.
> """
>
> Perhaps it doesn't matter if it's on one line and it just needs to be
> a single-sentence paragraph?

In this instance, your proposed one-liner is good enough for me, so let's do
that.

In general, I don't bend over backwards to make the summary sentence fit on one
line: sometimes things really are a bit complex and a short, vague “does stuff”
that forces you to read the body seems to do the reader more of a disservice a
slightly-more-than-one-line initial sentence.

It's probably worth checking what pydoc and epydoc do with these overrunning
sentences... As a core developer I'm primarily reading them in the
source directly, but that might not be true for authors of code using bzrlib.

-Andrew.

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

On 5 October 2010 10:55, Andrew Bennetts <email address hidden> wrote:
> In this instance, your proposed one-liner is good enough for me, so let's do
> that.
>
> In general, I don't bend over backwards to make the summary sentence fit on one
> line: sometimes things really are a bit complex and a short, vague “does stuff”
> that forces you to read the body seems to do the reader more of a disservice a
> slightly-more-than-one-line initial sentence.
>
> It's probably worth checking what pydoc and epydoc do with these overrunning
> sentences...  As a core developer I'm primarily reading them in the
> source directly, but that might not be true for authors of code using bzrlib.

I agree with all that. I stick to it in the unverifiied belief it
might prevent epydoc printing truncated sentences as summaries. It's
not a big deal, just something I noticed.

--
Martin

Revision history for this message
Vincent Ladeuil (vila) wrote :

Are there still bits pending review here ?
Or is there a consensus already ?

Revision history for this message
Vincent Ladeuil (vila) wrote :

In other words: do you need help from patch pilot or can this be landed ?

review: Needs Information
Revision history for this message
Vincent Ladeuil (vila) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2010-09-27 19:31:45 +0000
+++ NEWS 2010-09-28 08:31:43 +0000
@@ -8,7 +8,7 @@
8bzr 2.3b28bzr 2.3b2
9#########9#########
1010
11:2.3.b2: NOT RELEASED YET11:2.3b2: NOT RELEASED YET
1212
13Compatibility Breaks13Compatibility Breaks
14********************14********************
@@ -45,6 +45,14 @@
45API Changes45API Changes
46***********46***********
4747
48* Add ``bzrlib.pyutils`` module with helper functions for some Python
49 tasks such as resolving a dotted name to a Python object
50 (``get_named_object``). (Andrew Bennetts)
51
52* ``known_hooks_key_to_parent_and_attribute`` in ``bzrlib.hooks`` has been
53 deprecated in favour of ``known_hooks.key_to_parent_and_attribute`` in
54 the same module. (Andrew Bennetts)
55
48Internals56Internals
49*********57*********
5058
5159
=== modified file 'bzrlib/bundle/serializer/__init__.py'
--- bzrlib/bundle/serializer/__init__.py 2010-07-15 12:53:00 +0000
+++ bzrlib/bundle/serializer/__init__.py 2010-09-28 08:31:43 +0000
@@ -21,7 +21,10 @@
21from StringIO import StringIO21from StringIO import StringIO
22import re22import re
2323
24import bzrlib.errors as errors24from bzrlib import (
25 errors,
26 pyutils,
27 )
25from bzrlib.diff import internal_diff28from bzrlib.diff import internal_diff
26from bzrlib.revision import NULL_REVISION29from bzrlib.revision import NULL_REVISION
27# For backwards-compatibility30# For backwards-compatibility
@@ -191,8 +194,7 @@
191 :param overwrite: Should this version override a default194 :param overwrite: Should this version override a default
192 """195 """
193 def _loader(version):196 def _loader(version):
194 mod = __import__(module, globals(), locals(), [classname])197 klass = pyutils.get_named_object(module, classname)
195 klass = getattr(mod, classname)
196 return klass(version)198 return klass(version)
197 register(version, _loader, overwrite=overwrite)199 register(version, _loader, overwrite=overwrite)
198200
199201
=== modified file 'bzrlib/bzrdir.py'
--- bzrlib/bzrdir.py 2010-09-10 09:46:15 +0000
+++ bzrlib/bzrdir.py 2010-09-28 08:31:43 +0000
@@ -45,6 +45,7 @@
45 lockable_files,45 lockable_files,
46 lockdir,46 lockdir,
47 osutils,47 osutils,
48 pyutils,
48 remote,49 remote,
49 repository,50 repository,
50 revision as _mod_revision,51 revision as _mod_revision,
@@ -3092,15 +3093,12 @@
3092 def _load(full_name):3093 def _load(full_name):
3093 mod_name, factory_name = full_name.rsplit('.', 1)3094 mod_name, factory_name = full_name.rsplit('.', 1)
3094 try:3095 try:
3095 mod = __import__(mod_name, globals(), locals(),3096 factory = pyutils.get_named_object(mod_name, factory_name)
3096 [factory_name])
3097 except ImportError, e:3097 except ImportError, e:
3098 raise ImportError('failed to load %s: %s' % (full_name, e))3098 raise ImportError('failed to load %s: %s' % (full_name, e))
3099 try:
3100 factory = getattr(mod, factory_name)
3101 except AttributeError:3099 except AttributeError:
3102 raise AttributeError('no factory %s in module %r'3100 raise AttributeError('no factory %s in module %r'
3103 % (full_name, mod))3101 % (full_name, sys.modules[mod_name]))
3104 return factory()3102 return factory()
31053103
3106 def helper():3104 def helper():
31073105
=== modified file 'bzrlib/export/__init__.py'
--- bzrlib/export/__init__.py 2010-03-25 09:39:03 +0000
+++ bzrlib/export/__init__.py 2010-09-28 08:31:43 +0000
@@ -20,7 +20,10 @@
20"""20"""
2121
22import os22import os
23import bzrlib.errors as errors23from bzrlib import (
24 errors,
25 pyutils,
26 )
2427
25# Maps format name => export function28# Maps format name => export function
26_exporters = {}29_exporters = {}
@@ -55,8 +58,7 @@
55 When requesting a specific type of export, load the respective path.58 When requesting a specific type of export, load the respective path.
56 """59 """
57 def _loader(tree, dest, root, subdir, filtered, per_file_timestamps):60 def _loader(tree, dest, root, subdir, filtered, per_file_timestamps):
58 mod = __import__(module, globals(), locals(), [funcname])61 func = pyutils.get_named_object(module, funcname)
59 func = getattr(mod, funcname)
60 return func(tree, dest, root, subdir, filtered=filtered,62 return func(tree, dest, root, subdir, filtered=filtered,
61 per_file_timestamps=per_file_timestamps)63 per_file_timestamps=per_file_timestamps)
62 register_exporter(scheme, extensions, _loader)64 register_exporter(scheme, extensions, _loader)
6365
=== modified file 'bzrlib/hooks.py'
--- bzrlib/hooks.py 2010-08-28 06:13:48 +0000
+++ bzrlib/hooks.py 2010-09-28 08:31:43 +0000
@@ -16,7 +16,11 @@
1616
1717
18"""Support for plugin hooking logic."""18"""Support for plugin hooking logic."""
19from bzrlib import registry19from bzrlib import (
20 pyutils,
21 registry,
22 symbol_versioning,
23 )
20from bzrlib.lazy_import import lazy_import24from bzrlib.lazy_import import lazy_import
21lazy_import(globals(), """25lazy_import(globals(), """
22import textwrap26import textwrap
@@ -29,39 +33,63 @@
29""")33""")
3034
3135
32known_hooks = registry.Registry()36class KnownHooksRegistry(registry.Registry):
33# known_hooks registry contains37 # known_hooks registry contains
34# tuple of (module, member name) which is the hook point38 # tuple of (module, member name) which is the hook point
35# module where the specific hooks are defined39 # module where the specific hooks are defined
36# callable to get the empty specific Hooks for that attribute40 # callable to get the empty specific Hooks for that attribute
37known_hooks.register_lazy(('bzrlib.branch', 'Branch.hooks'), 'bzrlib.branch',41
38 'BranchHooks')42 def register_lazy_hook(self, hook_module_name, hook_member_name,
39known_hooks.register_lazy(('bzrlib.bzrdir', 'BzrDir.hooks'), 'bzrlib.bzrdir',43 hook_factory_member_name):
40 'BzrDirHooks')44 self.register_lazy((hook_module_name, hook_member_name),
41known_hooks.register_lazy(('bzrlib.commands', 'Command.hooks'),45 hook_module_name, hook_factory_member_name)
42 'bzrlib.commands', 'CommandHooks')46
43known_hooks.register_lazy(('bzrlib.info', 'hooks'),47 def iter_parent_objects(self):
44 'bzrlib.info', 'InfoHooks')48 """Yield (hook_key, (parent_object, attr)) tuples for every registered
45known_hooks.register_lazy(('bzrlib.lock', 'Lock.hooks'), 'bzrlib.lock',49 hook, where 'parent_object' is the object that holds the hook
46 'LockHooks')50 instance.
47known_hooks.register_lazy(('bzrlib.merge', 'Merger.hooks'), 'bzrlib.merge',51
48 'MergeHooks')52 This is useful for resetting/restoring all the hooks to a known state,
49known_hooks.register_lazy(('bzrlib.msgeditor', 'hooks'), 'bzrlib.msgeditor',53 as is done in bzrlib.tests.TestCase._clear_hooks.
50 'MessageEditorHooks')54 """
51known_hooks.register_lazy(('bzrlib.mutabletree', 'MutableTree.hooks'),55 for key in self.keys():
52 'bzrlib.mutabletree', 'MutableTreeHooks')56 yield key, self.key_to_parent_and_attribute(key)
53known_hooks.register_lazy(('bzrlib.smart.client', '_SmartClient.hooks'),57
54 'bzrlib.smart.client', 'SmartClientHooks')58 def key_to_parent_and_attribute(self, (module_name, member_name)):
55known_hooks.register_lazy(('bzrlib.smart.server', 'SmartTCPServer.hooks'),59 """Convert a known_hooks key to a (parent_obj, attr) pair.
56 'bzrlib.smart.server', 'SmartServerHooks')60
57known_hooks.register_lazy(('bzrlib.status', 'hooks'),61 :param key: A tuple (module_name, member_name) as found in the keys of
58 'bzrlib.status', 'StatusHooks')62 the known_hooks registry.
59known_hooks.register_lazy(63 :return: The parent_object of the hook and the name of the attribute on
60 ('bzrlib.version_info_formats.format_rio', 'RioVersionInfoBuilder.hooks'),64 that parent object where the hook is kept.
61 'bzrlib.version_info_formats.format_rio', 'RioVersionInfoBuilderHooks')65 """
62known_hooks.register_lazy(66 parent_mod, parent_member, attr = pyutils.calc_parent_name(module_name,
63 ('bzrlib.merge_directive', 'BaseMergeDirective.hooks'),67 member_name)
64 'bzrlib.merge_directive', 'MergeDirectiveHooks')68 return pyutils.get_named_object(parent_mod, parent_member), attr
69
70
71_builtin_known_hooks = (
72 ('bzrlib.branch', 'Branch.hooks', 'BranchHooks'),
73 ('bzrlib.bzrdir', 'BzrDir.hooks', 'BzrDirHooks'),
74 ('bzrlib.commands', 'Command.hooks', 'CommandHooks'),
75 ('bzrlib.info', 'hooks', 'InfoHooks'),
76 ('bzrlib.lock', 'Lock.hooks', 'LockHooks'),
77 ('bzrlib.merge', 'Merger.hooks', 'MergeHooks'),
78 ('bzrlib.msgeditor', 'hooks', 'MessageEditorHooks'),
79 ('bzrlib.mutabletree', 'MutableTree.hooks', 'MutableTreeHooks'),
80 ('bzrlib.smart.client', '_SmartClient.hooks', 'SmartClientHooks'),
81 ('bzrlib.smart.server', 'SmartTCPServer.hooks', 'SmartServerHooks'),
82 ('bzrlib.status', 'hooks', 'StatusHooks'),
83 ('bzrlib.version_info_formats.format_rio', 'RioVersionInfoBuilder.hooks',
84 'RioVersionInfoBuilderHooks'),
85 ('bzrlib.merge_directive', 'BaseMergeDirective.hooks',
86 'MergeDirectiveHooks'),
87 )
88
89known_hooks = KnownHooksRegistry()
90for (_hook_module, _hook_attribute, _hook_class) in _builtin_known_hooks:
91 known_hooks.register_lazy_hook(_hook_module, _hook_attribute, _hook_class)
92del _builtin_known_hooks, _hook_module, _hook_attribute, _hook_class
6593
6694
67def known_hooks_key_to_object((module_name, member_name)):95def known_hooks_key_to_object((module_name, member_name)):
@@ -71,24 +99,13 @@
71 the known_hooks registry.99 the known_hooks registry.
72 :return: The object this specifies.100 :return: The object this specifies.
73 """101 """
74 return registry._LazyObjectGetter(module_name, member_name).get_obj()102 return pyutils.get_named_object(module_name, member_name)
75103
76104
77def known_hooks_key_to_parent_and_attribute((module_name, member_name)):105@symbol_versioning.deprecated_function(symbol_versioning.deprecated_in((2, 3)))
78 """Convert a known_hooks key to a object.106def known_hooks_key_to_parent_and_attribute(key):
79107 """See KnownHooksRegistry.key_to_parent_and_attribute."""
80 :param key: A tuple (module_name, member_name) as found in the keys of108 return known_hooks.key_to_parent_and_attribute(key)
81 the known_hooks registry.
82 :return: The object this specifies.
83 """
84 member_list = member_name.rsplit('.', 1)
85 if len(member_list) == 2:
86 parent_name, attribute = member_list
87 else:
88 parent_name = None
89 attribute = member_name
90 parent = known_hooks_key_to_object((module_name, parent_name))
91 return parent, attribute
92109
93110
94class Hooks(dict):111class Hooks(dict):
95112
=== added file 'bzrlib/pyutils.py'
--- bzrlib/pyutils.py 1970-01-01 00:00:00 +0000
+++ bzrlib/pyutils.py 2010-09-28 08:31:43 +0000
@@ -0,0 +1,91 @@
1# Copyright (C) 2010 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Some convenience functions for general Python, such as a wrapper around
18``_import__``."""
19
20
21import sys
22
23
24def get_named_object(module_name, member_name=None):
25 """Get the Python object named by a given module and member name.
26
27 This is usually much more convenient than dealing with ``__import__``
28 directly::
29
30 >>> doc = get_named_object('bzrlib.pyutils', 'get_named_object.__doc__')
31 >>> doc.splitlines()[0]
32 'Get the Python object named by a given module and member name.'
33
34 :param module_name: a module name, as would be found in sys.modules if
35 the module is already imported. It may contain dots. e.g. 'sys' or
36 'os.path'.
37 :param member_name: (optional) a name of an attribute in that module to
38 return. It may contain dots. e.g. 'MyClass.some_method'. If not
39 given, the named module will be returned instead.
40 :raises: ImportError or AttributeError.
41 """
42 # We may have just a module name, or a module name and a member name,
43 # and either may contain dots. __import__'s return value is a bit
44 # unintuitive, so we need to take care to always return the object
45 # specified by the full combination of module name + member name.
46 if member_name:
47 # Give __import__ a from_list. It will return the last module in
48 # the dotted module name.
49 attr_chain = member_name.split('.')
50 from_list = attr_chain[:1]
51 obj = __import__(module_name, {}, {}, from_list)
52 for attr in attr_chain:
53 obj = getattr(obj, attr)
54 else:
55 # We're just importing a module, no attributes, so we have no
56 # from_list. __import__ will return the first module in the dotted
57 # module name, so we look up the module from sys.modules.
58 __import__(module_name, globals(), locals(), [])
59 obj = sys.modules[module_name]
60 return obj
61
62
63def calc_parent_name(module_name, member_name=None):
64 """Determine the 'parent' of a given dotted module name and (optional)
65 member name.
66
67 Typical use is::
68
69 >>> parent_mod, parent_member, final_attr = calc_parent_name(
70 ... module_name, member_name) # doctest: +SKIP
71 >>> parent_obj = get_named_object(parent_mod, parent_member)
72 ... # doctest: +SKIP
73
74 The idea is that ``getattr(parent_obj, final_attr)`` will equal
75 get_named_object(module_name, member_name).
76
77 :return: (module_name, member_name, final_attr) tuple.
78 """
79 if member_name is not None:
80 split_name = member_name.rsplit('.', 1)
81 if len(split_name) == 1:
82 return (module_name, None, member_name)
83 else:
84 return (module_name, split_name[0], split_name[1])
85 else:
86 split_name = module_name.rsplit('.', 1)
87 if len(split_name) == 1:
88 raise AssertionError(
89 'No parent object for top-level module %r' % (module_name,))
90 else:
91 return (split_name[0], None, split_name[1])
092
=== modified file 'bzrlib/registry.py'
--- bzrlib/registry.py 2009-09-16 10:37:29 +0000
+++ bzrlib/registry.py 2010-09-28 08:31:43 +0000
@@ -17,6 +17,9 @@
17"""Classes to provide name-to-object registry-like support."""17"""Classes to provide name-to-object registry-like support."""
1818
1919
20from bzrlib.pyutils import get_named_object
21
22
20class _ObjectGetter(object):23class _ObjectGetter(object):
21 """Maintain a reference to an object, and return the object on request.24 """Maintain a reference to an object, and return the object on request.
2225
@@ -58,26 +61,14 @@
58 return the imported object.61 return the imported object.
59 """62 """
60 if not self._imported:63 if not self._imported:
61 self._do_import()64 self._obj = get_named_object(self._module_name, self._member_name)
65 self._imported = True
62 return super(_LazyObjectGetter, self).get_obj()66 return super(_LazyObjectGetter, self).get_obj()
6367
64 def _do_import(self):
65 if self._member_name:
66 segments = self._member_name.split('.')
67 names = segments[0:1]
68 else:
69 names = [self._member_name]
70 obj = __import__(self._module_name, globals(), locals(), names)
71 if self._member_name:
72 for segment in segments:
73 obj = getattr(obj, segment)
74 self._obj = obj
75 self._imported = True
76
77 def __repr__(self):68 def __repr__(self):
78 return "<%s.%s object at %x, module=%r attribute=%r>" % (69 return "<%s.%s object at %x, module=%r attribute=%r imported=%r>" % (
79 self.__class__.__module__, self.__class__.__name__, id(self),70 self.__class__.__module__, self.__class__.__name__, id(self),
80 self._module_name, self._member_name)71 self._module_name, self._member_name, self._imported)
8172
8273
83class Registry(object):74class Registry(object):
8475
=== modified file 'bzrlib/repository.py'
--- bzrlib/repository.py 2010-09-24 01:59:46 +0000
+++ bzrlib/repository.py 2010-09-28 08:31:43 +0000
@@ -39,6 +39,7 @@
39 lockdir,39 lockdir,
40 lru_cache,40 lru_cache,
41 osutils,41 osutils,
42 pyutils,
42 revision as _mod_revision,43 revision as _mod_revision,
43 static_tuple,44 static_tuple,
44 symbol_versioning,45 symbol_versioning,
@@ -52,6 +53,7 @@
52from bzrlib.testament import Testament53from bzrlib.testament import Testament
53""")54""")
5455
56import sys
55from bzrlib import (57from bzrlib import (
56 errors,58 errors,
57 registry,59 registry,
@@ -2825,12 +2827,11 @@
2825 % (name, from_module),2827 % (name, from_module),
2826 DeprecationWarning,2828 DeprecationWarning,
2827 stacklevel=2)2829 stacklevel=2)
2828 m = __import__(from_module, globals(), locals(), [name])
2829 try:2830 try:
2830 return getattr(m, name)2831 return pyutils.get_named_object(from_module, name)
2831 except AttributeError:2832 except AttributeError:
2832 raise AttributeError('module %s has no name %s'2833 raise AttributeError('module %s has no name %s'
2833 % (m, name))2834 % (sys.modules[from_module], name))
2834 globals()[name] = _deprecated_repository_forwarder2835 globals()[name] = _deprecated_repository_forwarder
28352836
2836for _name in [2837for _name in [
28372838
=== modified file 'bzrlib/tests/TestUtil.py'
--- bzrlib/tests/TestUtil.py 2010-08-05 18:13:23 +0000
+++ bzrlib/tests/TestUtil.py 2010-09-28 08:31:43 +0000
@@ -20,6 +20,8 @@
20import logging20import logging
21import unittest21import unittest
2222
23from bzrlib import pyutils
24
23# Mark this python module as being part of the implementation25# Mark this python module as being part of the implementation
24# of unittest: this gives us better tracebacks where the last26# of unittest: this gives us better tracebacks where the last
25# shown frame is the test code, not our assertXYZ.27# shown frame is the test code, not our assertXYZ.
@@ -106,7 +108,7 @@
106108
107 def loadTestsFromModuleName(self, name):109 def loadTestsFromModuleName(self, name):
108 result = self.suiteClass()110 result = self.suiteClass()
109 module = _load_module_by_name(name)111 module = pyutils.get_named_object(name)
110112
111 result.addTests(self.loadTestsFromModule(module))113 result.addTests(self.loadTestsFromModule(module))
112 return result114 return result
@@ -179,17 +181,6 @@
179 return self.suiteClass()181 return self.suiteClass()
180182
181183
182def _load_module_by_name(mod_name):
183 parts = mod_name.split('.')
184 module = __import__(mod_name)
185 del parts[0]
186 # for historical reasons python returns the top-level module even though
187 # it loads the submodule; we need to walk down to get the one we want.
188 while parts:
189 module = getattr(module, parts.pop(0))
190 return module
191
192
193class TestVisitor(object):184class TestVisitor(object):
194 """A visitor for Tests"""185 """A visitor for Tests"""
195 def visitSuite(self, aTestSuite):186 def visitSuite(self, aTestSuite):
196187
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2010-09-27 19:31:45 +0000
+++ bzrlib/tests/__init__.py 2010-09-28 08:31:43 +0000
@@ -71,6 +71,7 @@
71 lock as _mod_lock,71 lock as _mod_lock,
72 memorytree,72 memorytree,
73 osutils,73 osutils,
74 pyutils,
74 ui,75 ui,
75 urlutils,76 urlutils,
76 registry,77 registry,
@@ -877,14 +878,14 @@
877878
878 def _clear_hooks(self):879 def _clear_hooks(self):
879 # prevent hooks affecting tests880 # prevent hooks affecting tests
881 known_hooks = hooks.known_hooks
880 self._preserved_hooks = {}882 self._preserved_hooks = {}
881 for key, factory in hooks.known_hooks.items():883 for key, (parent, name) in known_hooks.iter_parent_objects():
882 parent, name = hooks.known_hooks_key_to_parent_and_attribute(key)884 current_hooks = getattr(parent, name)
883 current_hooks = hooks.known_hooks_key_to_object(key)
884 self._preserved_hooks[parent] = (name, current_hooks)885 self._preserved_hooks[parent] = (name, current_hooks)
885 self.addCleanup(self._restoreHooks)886 self.addCleanup(self._restoreHooks)
886 for key, factory in hooks.known_hooks.items():887 for key, (parent, name) in known_hooks.iter_parent_objects():
887 parent, name = hooks.known_hooks_key_to_parent_and_attribute(key)888 factory = known_hooks.get(key)
888 setattr(parent, name, factory())889 setattr(parent, name, factory())
889 # this hook should always be installed890 # this hook should always be installed
890 request._install_hook()891 request._install_hook()
@@ -3773,6 +3774,7 @@
3773 'bzrlib.tests.test_permissions',3774 'bzrlib.tests.test_permissions',
3774 'bzrlib.tests.test_plugins',3775 'bzrlib.tests.test_plugins',
3775 'bzrlib.tests.test_progress',3776 'bzrlib.tests.test_progress',
3777 'bzrlib.tests.test_pyutils',
3776 'bzrlib.tests.test_read_bundle',3778 'bzrlib.tests.test_read_bundle',
3777 'bzrlib.tests.test_reconcile',3779 'bzrlib.tests.test_reconcile',
3778 'bzrlib.tests.test_reconfigure',3780 'bzrlib.tests.test_reconfigure',
@@ -3856,6 +3858,7 @@
3856 'bzrlib.lockdir',3858 'bzrlib.lockdir',
3857 'bzrlib.merge3',3859 'bzrlib.merge3',
3858 'bzrlib.option',3860 'bzrlib.option',
3861 'bzrlib.pyutils',
3859 'bzrlib.symbol_versioning',3862 'bzrlib.symbol_versioning',
3860 'bzrlib.tests',3863 'bzrlib.tests',
3861 'bzrlib.tests.fixtures',3864 'bzrlib.tests.fixtures',
@@ -4098,7 +4101,7 @@
4098 the module is available.4101 the module is available.
4099 """4102 """
41004103
4101 py_module = __import__(py_module_name, {}, {}, ['NO_SUCH_ATTRIB'])4104 py_module = pyutils.get_named_object(py_module_name)
4102 scenarios = [4105 scenarios = [
4103 ('python', {'module': py_module}),4106 ('python', {'module': py_module}),
4104 ]4107 ]
@@ -4257,9 +4260,8 @@
4257 symbol_versioning.warn(depr_msg + use_msg, DeprecationWarning)4260 symbol_versioning.warn(depr_msg + use_msg, DeprecationWarning)
4258 # Import the new feature and use it as a replacement for the4261 # Import the new feature and use it as a replacement for the
4259 # deprecated one.4262 # deprecated one.
4260 mod = __import__(self._replacement_module, {}, {},4263 self._feature = pyutils.get_named_object(
4261 [self._replacement_name])4264 self._replacement_module, self._replacement_name)
4262 self._feature = getattr(mod, self._replacement_name)
42634265
4264 def _probe(self):4266 def _probe(self):
4265 self._ensure()4267 self._ensure()
42664268
=== modified file 'bzrlib/tests/per_transport.py'
--- bzrlib/tests/per_transport.py 2010-08-24 13:03:18 +0000
+++ bzrlib/tests/per_transport.py 2010-09-28 08:31:43 +0000
@@ -26,28 +26,24 @@
26from StringIO import StringIO as pyStringIO26from StringIO import StringIO as pyStringIO
27import stat27import stat
28import sys28import sys
29import unittest
3029
31from bzrlib import (30from bzrlib import (
32 errors,31 errors,
33 osutils,32 osutils,
33 pyutils,
34 tests,34 tests,
35 urlutils,35 urlutils,
36 )36 )
37from bzrlib.errors import (ConnectionError,37from bzrlib.errors import (ConnectionError,
38 DirectoryNotEmpty,
39 FileExists,38 FileExists,
40 InvalidURL,39 InvalidURL,
41 LockError,
42 NoSuchFile,40 NoSuchFile,
43 NotLocalUrl,
44 PathError,41 PathError,
45 TransportNotPossible,42 TransportNotPossible,
46 )43 )
47from bzrlib.osutils import getcwd44from bzrlib.osutils import getcwd
48from bzrlib.smart import medium45from bzrlib.smart import medium
49from bzrlib.tests import (46from bzrlib.tests import (
50 TestCaseInTempDir,
51 TestSkipped,47 TestSkipped,
52 TestNotApplicable,48 TestNotApplicable,
53 multiply_tests,49 multiply_tests,
@@ -78,7 +74,7 @@
78 for module in _get_transport_modules():74 for module in _get_transport_modules():
79 try:75 try:
80 permutations = get_transport_test_permutations(76 permutations = get_transport_test_permutations(
81 reduce(getattr, (module).split('.')[1:], __import__(module)))77 pyutils.get_named_object(module))
82 for (klass, server_factory) in permutations:78 for (klass, server_factory) in permutations:
83 scenario = ('%s,%s' % (klass.__name__, server_factory.__name__),79 scenario = ('%s,%s' % (klass.__name__, server_factory.__name__),
84 {"transport_class":klass,80 {"transport_class":klass,
8581
=== modified file 'bzrlib/tests/test_hooks.py'
--- bzrlib/tests/test_hooks.py 2010-02-17 17:11:16 +0000
+++ bzrlib/tests/test_hooks.py 2010-09-28 08:31:43 +0000
@@ -28,6 +28,9 @@
28 known_hooks_key_to_object,28 known_hooks_key_to_object,
29 known_hooks_key_to_parent_and_attribute,29 known_hooks_key_to_parent_and_attribute,
30 )30 )
31from bzrlib.symbol_versioning import (
32 deprecated_in,
33 )
3134
3235
33class TestHooks(tests.TestCase):36class TestHooks(tests.TestCase):
@@ -175,10 +178,20 @@
175 self.assertIs(branch.Branch.hooks,178 self.assertIs(branch.Branch.hooks,
176 known_hooks_key_to_object(('bzrlib.branch', 'Branch.hooks')))179 known_hooks_key_to_object(('bzrlib.branch', 'Branch.hooks')))
177180
181 def test_known_hooks_key_to_parent_and_attribute_deprecated(self):
182 self.assertEqual((branch.Branch, 'hooks'),
183 self.applyDeprecated(deprecated_in((2,3)),
184 known_hooks_key_to_parent_and_attribute,
185 ('bzrlib.branch', 'Branch.hooks')))
186 self.assertEqual((branch, 'Branch'),
187 self.applyDeprecated(deprecated_in((2,3)),
188 known_hooks_key_to_parent_and_attribute,
189 ('bzrlib.branch', 'Branch')))
190
178 def test_known_hooks_key_to_parent_and_attribute(self):191 def test_known_hooks_key_to_parent_and_attribute(self):
179 self.assertEqual((branch.Branch, 'hooks'),192 self.assertEqual((branch.Branch, 'hooks'),
180 known_hooks_key_to_parent_and_attribute(193 known_hooks.key_to_parent_and_attribute(
181 ('bzrlib.branch', 'Branch.hooks')))194 ('bzrlib.branch', 'Branch.hooks')))
182 self.assertEqual((branch, 'Branch'),195 self.assertEqual((branch, 'Branch'),
183 known_hooks_key_to_parent_and_attribute(196 known_hooks.key_to_parent_and_attribute(
184 ('bzrlib.branch', 'Branch')))197 ('bzrlib.branch', 'Branch')))
185198
=== added file 'bzrlib/tests/test_pyutils.py'
--- bzrlib/tests/test_pyutils.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/test_pyutils.py 2010-09-28 08:31:43 +0000
@@ -0,0 +1,88 @@
1# Copyright (C) 2010 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Tests for bzrlib.pyutils."""
18
19from bzrlib import (
20 branch,
21 tests,
22 )
23from bzrlib.pyutils import (
24 calc_parent_name,
25 get_named_object,
26 )
27
28
29class TestGetNamedObject(tests.TestCase):
30 """Tests for get_named_object."""
31
32 def test_module_only(self):
33 import sys
34 self.assertIs(sys, get_named_object('sys'))
35
36 def test_dotted_module(self):
37 self.assertIs(branch, get_named_object('bzrlib.branch'))
38
39 def test_module_attr(self):
40 self.assertIs(
41 branch.Branch, get_named_object('bzrlib.branch', 'Branch'))
42
43 def test_dotted_attr(self):
44 self.assertIs(
45 branch.Branch.hooks,
46 get_named_object('bzrlib.branch', 'Branch.hooks'))
47
48 def test_package(self):
49 # bzrlib.tests is a package, not simply a module
50 self.assertIs(tests, get_named_object('bzrlib.tests'))
51
52 def test_package_attr(self):
53 # bzrlib.tests is a package, not simply a module
54 self.assertIs(
55 tests.TestCase, get_named_object('bzrlib.tests', 'TestCase'))
56
57 def test_import_error(self):
58 self.assertRaises(ImportError, get_named_object, 'NO_SUCH_MODULE')
59
60 def test_attribute_error(self):
61 self.assertRaises(
62 AttributeError, get_named_object, 'sys', 'NO_SUCH_ATTR')
63
64
65
66class TestCalcParent_name(tests.TestCase):
67 """Tests for calc_parent_name."""
68
69 def test_dotted_member(self):
70 self.assertEqual(
71 ('mod_name', 'attr1', 'attr2'),
72 calc_parent_name('mod_name', 'attr1.attr2'))
73
74 def test_undotted_member(self):
75 self.assertEqual(
76 ('mod_name', None, 'attr1'),
77 calc_parent_name('mod_name', 'attr1'))
78
79 def test_dotted_module_no_member(self):
80 self.assertEqual(
81 ('mod', None, 'sub_mod'),
82 calc_parent_name('mod.sub_mod'))
83
84 def test_undotted_module_no_member(self):
85 err = self.assertRaises(AssertionError, calc_parent_name, 'mod_name')
86 self.assertEqual(
87 "No parent object for top-level module 'mod_name'", err.args[0])
88
089
=== modified file 'bzrlib/tests/test_registry.py'
--- bzrlib/tests/test_registry.py 2009-09-16 10:37:29 +0000
+++ bzrlib/tests/test_registry.py 2010-09-28 08:31:43 +0000
@@ -20,6 +20,7 @@
20import sys20import sys
2121
22from bzrlib import (22from bzrlib import (
23 branch,
23 errors,24 errors,
24 osutils,25 osutils,
25 registry,26 registry,
@@ -286,6 +287,13 @@
286 '\n\n'287 '\n\n'
287 )288 )
288289
290 def test_lazy_import_registry_foo(self):
291 a_registry = registry.Registry()
292 a_registry.register_lazy('foo', 'bzrlib.branch', 'Branch')
293 a_registry.register_lazy('bar', 'bzrlib.branch', 'Branch.hooks')
294 self.assertEqual(branch.Branch, a_registry.get('foo'))
295 self.assertEqual(branch.Branch.hooks, a_registry.get('bar'))
296
289 def test_lazy_import_registry(self):297 def test_lazy_import_registry(self):
290 plugin_name = self.create_simple_plugin()298 plugin_name = self.create_simple_plugin()
291 a_registry = registry.Registry()299 a_registry = registry.Registry()
292300
=== modified file 'bzrlib/tests/test_selftest.py'
--- bzrlib/tests/test_selftest.py 2010-09-27 19:31:45 +0000
+++ bzrlib/tests/test_selftest.py 2010-09-28 08:31:43 +0000
@@ -78,18 +78,6 @@
78 return [t.id() for t in tests.iter_suite_tests(test_suite)]78 return [t.id() for t in tests.iter_suite_tests(test_suite)]
7979
8080
81class SelftestTests(tests.TestCase):
82
83 def test_import_tests(self):
84 mod = TestUtil._load_module_by_name('bzrlib.tests.test_selftest')
85 self.assertEqual(mod.SelftestTests, SelftestTests)
86
87 def test_import_test_failure(self):
88 self.assertRaises(ImportError,
89 TestUtil._load_module_by_name,
90 'bzrlib.no-name-yet')
91
92
93class MetaTestLog(tests.TestCase):81class MetaTestLog(tests.TestCase):
9482
95 def test_logging(self):83 def test_logging(self):
9684
=== modified file 'doc/developers/code-style.txt'
--- doc/developers/code-style.txt 2010-09-24 08:42:02 +0000
+++ doc/developers/code-style.txt 2010-09-28 08:31:43 +0000
@@ -485,5 +485,19 @@
485485
486 * Don't say "open source" when you mean "free software".486 * Don't say "open source" when you mean "free software".
487487
488
489Dynamic imports
490===============
491
492If you need to import a module (or attribute of a module) named in a
493variable:
494
495 * If importing a module, not an attribute, and the module is a top-level
496 module (i.e. has no dots in the name), then it's ok to use the builtin
497 ``__import__``, e.g. ``__import__(module_name)``.
498 * In all other cases, prefer ``bzrlib.pyutils.get_named_object`` to the
499 built-in ``__import__``. ``__import__`` has some subtleties and
500 unintuitive behaviours that make it hard to use correctly.
501
488..502..
489 vim: ft=rst tw=74 ai503 vim: ft=rst tw=74 ai