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
1=== modified file 'NEWS'
2--- NEWS 2010-09-27 19:31:45 +0000
3+++ NEWS 2010-09-28 08:31:43 +0000
4@@ -8,7 +8,7 @@
5 bzr 2.3b2
6 #########
7
8-:2.3.b2: NOT RELEASED YET
9+:2.3b2: NOT RELEASED YET
10
11 Compatibility Breaks
12 ********************
13@@ -45,6 +45,14 @@
14 API Changes
15 ***********
16
17+* Add ``bzrlib.pyutils`` module with helper functions for some Python
18+ tasks such as resolving a dotted name to a Python object
19+ (``get_named_object``). (Andrew Bennetts)
20+
21+* ``known_hooks_key_to_parent_and_attribute`` in ``bzrlib.hooks`` has been
22+ deprecated in favour of ``known_hooks.key_to_parent_and_attribute`` in
23+ the same module. (Andrew Bennetts)
24+
25 Internals
26 *********
27
28
29=== modified file 'bzrlib/bundle/serializer/__init__.py'
30--- bzrlib/bundle/serializer/__init__.py 2010-07-15 12:53:00 +0000
31+++ bzrlib/bundle/serializer/__init__.py 2010-09-28 08:31:43 +0000
32@@ -21,7 +21,10 @@
33 from StringIO import StringIO
34 import re
35
36-import bzrlib.errors as errors
37+from bzrlib import (
38+ errors,
39+ pyutils,
40+ )
41 from bzrlib.diff import internal_diff
42 from bzrlib.revision import NULL_REVISION
43 # For backwards-compatibility
44@@ -191,8 +194,7 @@
45 :param overwrite: Should this version override a default
46 """
47 def _loader(version):
48- mod = __import__(module, globals(), locals(), [classname])
49- klass = getattr(mod, classname)
50+ klass = pyutils.get_named_object(module, classname)
51 return klass(version)
52 register(version, _loader, overwrite=overwrite)
53
54
55=== modified file 'bzrlib/bzrdir.py'
56--- bzrlib/bzrdir.py 2010-09-10 09:46:15 +0000
57+++ bzrlib/bzrdir.py 2010-09-28 08:31:43 +0000
58@@ -45,6 +45,7 @@
59 lockable_files,
60 lockdir,
61 osutils,
62+ pyutils,
63 remote,
64 repository,
65 revision as _mod_revision,
66@@ -3092,15 +3093,12 @@
67 def _load(full_name):
68 mod_name, factory_name = full_name.rsplit('.', 1)
69 try:
70- mod = __import__(mod_name, globals(), locals(),
71- [factory_name])
72+ factory = pyutils.get_named_object(mod_name, factory_name)
73 except ImportError, e:
74 raise ImportError('failed to load %s: %s' % (full_name, e))
75- try:
76- factory = getattr(mod, factory_name)
77 except AttributeError:
78 raise AttributeError('no factory %s in module %r'
79- % (full_name, mod))
80+ % (full_name, sys.modules[mod_name]))
81 return factory()
82
83 def helper():
84
85=== modified file 'bzrlib/export/__init__.py'
86--- bzrlib/export/__init__.py 2010-03-25 09:39:03 +0000
87+++ bzrlib/export/__init__.py 2010-09-28 08:31:43 +0000
88@@ -20,7 +20,10 @@
89 """
90
91 import os
92-import bzrlib.errors as errors
93+from bzrlib import (
94+ errors,
95+ pyutils,
96+ )
97
98 # Maps format name => export function
99 _exporters = {}
100@@ -55,8 +58,7 @@
101 When requesting a specific type of export, load the respective path.
102 """
103 def _loader(tree, dest, root, subdir, filtered, per_file_timestamps):
104- mod = __import__(module, globals(), locals(), [funcname])
105- func = getattr(mod, funcname)
106+ func = pyutils.get_named_object(module, funcname)
107 return func(tree, dest, root, subdir, filtered=filtered,
108 per_file_timestamps=per_file_timestamps)
109 register_exporter(scheme, extensions, _loader)
110
111=== modified file 'bzrlib/hooks.py'
112--- bzrlib/hooks.py 2010-08-28 06:13:48 +0000
113+++ bzrlib/hooks.py 2010-09-28 08:31:43 +0000
114@@ -16,7 +16,11 @@
115
116
117 """Support for plugin hooking logic."""
118-from bzrlib import registry
119+from bzrlib import (
120+ pyutils,
121+ registry,
122+ symbol_versioning,
123+ )
124 from bzrlib.lazy_import import lazy_import
125 lazy_import(globals(), """
126 import textwrap
127@@ -29,39 +33,63 @@
128 """)
129
130
131-known_hooks = registry.Registry()
132-# known_hooks registry contains
133-# tuple of (module, member name) which is the hook point
134-# module where the specific hooks are defined
135-# callable to get the empty specific Hooks for that attribute
136-known_hooks.register_lazy(('bzrlib.branch', 'Branch.hooks'), 'bzrlib.branch',
137- 'BranchHooks')
138-known_hooks.register_lazy(('bzrlib.bzrdir', 'BzrDir.hooks'), 'bzrlib.bzrdir',
139- 'BzrDirHooks')
140-known_hooks.register_lazy(('bzrlib.commands', 'Command.hooks'),
141- 'bzrlib.commands', 'CommandHooks')
142-known_hooks.register_lazy(('bzrlib.info', 'hooks'),
143- 'bzrlib.info', 'InfoHooks')
144-known_hooks.register_lazy(('bzrlib.lock', 'Lock.hooks'), 'bzrlib.lock',
145- 'LockHooks')
146-known_hooks.register_lazy(('bzrlib.merge', 'Merger.hooks'), 'bzrlib.merge',
147- 'MergeHooks')
148-known_hooks.register_lazy(('bzrlib.msgeditor', 'hooks'), 'bzrlib.msgeditor',
149- 'MessageEditorHooks')
150-known_hooks.register_lazy(('bzrlib.mutabletree', 'MutableTree.hooks'),
151- 'bzrlib.mutabletree', 'MutableTreeHooks')
152-known_hooks.register_lazy(('bzrlib.smart.client', '_SmartClient.hooks'),
153- 'bzrlib.smart.client', 'SmartClientHooks')
154-known_hooks.register_lazy(('bzrlib.smart.server', 'SmartTCPServer.hooks'),
155- 'bzrlib.smart.server', 'SmartServerHooks')
156-known_hooks.register_lazy(('bzrlib.status', 'hooks'),
157- 'bzrlib.status', 'StatusHooks')
158-known_hooks.register_lazy(
159- ('bzrlib.version_info_formats.format_rio', 'RioVersionInfoBuilder.hooks'),
160- 'bzrlib.version_info_formats.format_rio', 'RioVersionInfoBuilderHooks')
161-known_hooks.register_lazy(
162- ('bzrlib.merge_directive', 'BaseMergeDirective.hooks'),
163- 'bzrlib.merge_directive', 'MergeDirectiveHooks')
164+class KnownHooksRegistry(registry.Registry):
165+ # known_hooks registry contains
166+ # tuple of (module, member name) which is the hook point
167+ # module where the specific hooks are defined
168+ # callable to get the empty specific Hooks for that attribute
169+
170+ def register_lazy_hook(self, hook_module_name, hook_member_name,
171+ hook_factory_member_name):
172+ self.register_lazy((hook_module_name, hook_member_name),
173+ hook_module_name, hook_factory_member_name)
174+
175+ def iter_parent_objects(self):
176+ """Yield (hook_key, (parent_object, attr)) tuples for every registered
177+ hook, where 'parent_object' is the object that holds the hook
178+ instance.
179+
180+ This is useful for resetting/restoring all the hooks to a known state,
181+ as is done in bzrlib.tests.TestCase._clear_hooks.
182+ """
183+ for key in self.keys():
184+ yield key, self.key_to_parent_and_attribute(key)
185+
186+ def key_to_parent_and_attribute(self, (module_name, member_name)):
187+ """Convert a known_hooks key to a (parent_obj, attr) pair.
188+
189+ :param key: A tuple (module_name, member_name) as found in the keys of
190+ the known_hooks registry.
191+ :return: The parent_object of the hook and the name of the attribute on
192+ that parent object where the hook is kept.
193+ """
194+ parent_mod, parent_member, attr = pyutils.calc_parent_name(module_name,
195+ member_name)
196+ return pyutils.get_named_object(parent_mod, parent_member), attr
197+
198+
199+_builtin_known_hooks = (
200+ ('bzrlib.branch', 'Branch.hooks', 'BranchHooks'),
201+ ('bzrlib.bzrdir', 'BzrDir.hooks', 'BzrDirHooks'),
202+ ('bzrlib.commands', 'Command.hooks', 'CommandHooks'),
203+ ('bzrlib.info', 'hooks', 'InfoHooks'),
204+ ('bzrlib.lock', 'Lock.hooks', 'LockHooks'),
205+ ('bzrlib.merge', 'Merger.hooks', 'MergeHooks'),
206+ ('bzrlib.msgeditor', 'hooks', 'MessageEditorHooks'),
207+ ('bzrlib.mutabletree', 'MutableTree.hooks', 'MutableTreeHooks'),
208+ ('bzrlib.smart.client', '_SmartClient.hooks', 'SmartClientHooks'),
209+ ('bzrlib.smart.server', 'SmartTCPServer.hooks', 'SmartServerHooks'),
210+ ('bzrlib.status', 'hooks', 'StatusHooks'),
211+ ('bzrlib.version_info_formats.format_rio', 'RioVersionInfoBuilder.hooks',
212+ 'RioVersionInfoBuilderHooks'),
213+ ('bzrlib.merge_directive', 'BaseMergeDirective.hooks',
214+ 'MergeDirectiveHooks'),
215+ )
216+
217+known_hooks = KnownHooksRegistry()
218+for (_hook_module, _hook_attribute, _hook_class) in _builtin_known_hooks:
219+ known_hooks.register_lazy_hook(_hook_module, _hook_attribute, _hook_class)
220+del _builtin_known_hooks, _hook_module, _hook_attribute, _hook_class
221
222
223 def known_hooks_key_to_object((module_name, member_name)):
224@@ -71,24 +99,13 @@
225 the known_hooks registry.
226 :return: The object this specifies.
227 """
228- return registry._LazyObjectGetter(module_name, member_name).get_obj()
229-
230-
231-def known_hooks_key_to_parent_and_attribute((module_name, member_name)):
232- """Convert a known_hooks key to a object.
233-
234- :param key: A tuple (module_name, member_name) as found in the keys of
235- the known_hooks registry.
236- :return: The object this specifies.
237- """
238- member_list = member_name.rsplit('.', 1)
239- if len(member_list) == 2:
240- parent_name, attribute = member_list
241- else:
242- parent_name = None
243- attribute = member_name
244- parent = known_hooks_key_to_object((module_name, parent_name))
245- return parent, attribute
246+ return pyutils.get_named_object(module_name, member_name)
247+
248+
249+@symbol_versioning.deprecated_function(symbol_versioning.deprecated_in((2, 3)))
250+def known_hooks_key_to_parent_and_attribute(key):
251+ """See KnownHooksRegistry.key_to_parent_and_attribute."""
252+ return known_hooks.key_to_parent_and_attribute(key)
253
254
255 class Hooks(dict):
256
257=== added file 'bzrlib/pyutils.py'
258--- bzrlib/pyutils.py 1970-01-01 00:00:00 +0000
259+++ bzrlib/pyutils.py 2010-09-28 08:31:43 +0000
260@@ -0,0 +1,91 @@
261+# Copyright (C) 2010 Canonical Ltd
262+#
263+# This program is free software; you can redistribute it and/or modify
264+# it under the terms of the GNU General Public License as published by
265+# the Free Software Foundation; either version 2 of the License, or
266+# (at your option) any later version.
267+#
268+# This program is distributed in the hope that it will be useful,
269+# but WITHOUT ANY WARRANTY; without even the implied warranty of
270+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
271+# GNU General Public License for more details.
272+#
273+# You should have received a copy of the GNU General Public License
274+# along with this program; if not, write to the Free Software
275+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
276+
277+"""Some convenience functions for general Python, such as a wrapper around
278+``_import__``."""
279+
280+
281+import sys
282+
283+
284+def get_named_object(module_name, member_name=None):
285+ """Get the Python object named by a given module and member name.
286+
287+ This is usually much more convenient than dealing with ``__import__``
288+ directly::
289+
290+ >>> doc = get_named_object('bzrlib.pyutils', 'get_named_object.__doc__')
291+ >>> doc.splitlines()[0]
292+ 'Get the Python object named by a given module and member name.'
293+
294+ :param module_name: a module name, as would be found in sys.modules if
295+ the module is already imported. It may contain dots. e.g. 'sys' or
296+ 'os.path'.
297+ :param member_name: (optional) a name of an attribute in that module to
298+ return. It may contain dots. e.g. 'MyClass.some_method'. If not
299+ given, the named module will be returned instead.
300+ :raises: ImportError or AttributeError.
301+ """
302+ # We may have just a module name, or a module name and a member name,
303+ # and either may contain dots. __import__'s return value is a bit
304+ # unintuitive, so we need to take care to always return the object
305+ # specified by the full combination of module name + member name.
306+ if member_name:
307+ # Give __import__ a from_list. It will return the last module in
308+ # the dotted module name.
309+ attr_chain = member_name.split('.')
310+ from_list = attr_chain[:1]
311+ obj = __import__(module_name, {}, {}, from_list)
312+ for attr in attr_chain:
313+ obj = getattr(obj, attr)
314+ else:
315+ # We're just importing a module, no attributes, so we have no
316+ # from_list. __import__ will return the first module in the dotted
317+ # module name, so we look up the module from sys.modules.
318+ __import__(module_name, globals(), locals(), [])
319+ obj = sys.modules[module_name]
320+ return obj
321+
322+
323+def calc_parent_name(module_name, member_name=None):
324+ """Determine the 'parent' of a given dotted module name and (optional)
325+ member name.
326+
327+ Typical use is::
328+
329+ >>> parent_mod, parent_member, final_attr = calc_parent_name(
330+ ... module_name, member_name) # doctest: +SKIP
331+ >>> parent_obj = get_named_object(parent_mod, parent_member)
332+ ... # doctest: +SKIP
333+
334+ The idea is that ``getattr(parent_obj, final_attr)`` will equal
335+ get_named_object(module_name, member_name).
336+
337+ :return: (module_name, member_name, final_attr) tuple.
338+ """
339+ if member_name is not None:
340+ split_name = member_name.rsplit('.', 1)
341+ if len(split_name) == 1:
342+ return (module_name, None, member_name)
343+ else:
344+ return (module_name, split_name[0], split_name[1])
345+ else:
346+ split_name = module_name.rsplit('.', 1)
347+ if len(split_name) == 1:
348+ raise AssertionError(
349+ 'No parent object for top-level module %r' % (module_name,))
350+ else:
351+ return (split_name[0], None, split_name[1])
352
353=== modified file 'bzrlib/registry.py'
354--- bzrlib/registry.py 2009-09-16 10:37:29 +0000
355+++ bzrlib/registry.py 2010-09-28 08:31:43 +0000
356@@ -17,6 +17,9 @@
357 """Classes to provide name-to-object registry-like support."""
358
359
360+from bzrlib.pyutils import get_named_object
361+
362+
363 class _ObjectGetter(object):
364 """Maintain a reference to an object, and return the object on request.
365
366@@ -58,26 +61,14 @@
367 return the imported object.
368 """
369 if not self._imported:
370- self._do_import()
371+ self._obj = get_named_object(self._module_name, self._member_name)
372+ self._imported = True
373 return super(_LazyObjectGetter, self).get_obj()
374
375- def _do_import(self):
376- if self._member_name:
377- segments = self._member_name.split('.')
378- names = segments[0:1]
379- else:
380- names = [self._member_name]
381- obj = __import__(self._module_name, globals(), locals(), names)
382- if self._member_name:
383- for segment in segments:
384- obj = getattr(obj, segment)
385- self._obj = obj
386- self._imported = True
387-
388 def __repr__(self):
389- return "<%s.%s object at %x, module=%r attribute=%r>" % (
390+ return "<%s.%s object at %x, module=%r attribute=%r imported=%r>" % (
391 self.__class__.__module__, self.__class__.__name__, id(self),
392- self._module_name, self._member_name)
393+ self._module_name, self._member_name, self._imported)
394
395
396 class Registry(object):
397
398=== modified file 'bzrlib/repository.py'
399--- bzrlib/repository.py 2010-09-24 01:59:46 +0000
400+++ bzrlib/repository.py 2010-09-28 08:31:43 +0000
401@@ -39,6 +39,7 @@
402 lockdir,
403 lru_cache,
404 osutils,
405+ pyutils,
406 revision as _mod_revision,
407 static_tuple,
408 symbol_versioning,
409@@ -52,6 +53,7 @@
410 from bzrlib.testament import Testament
411 """)
412
413+import sys
414 from bzrlib import (
415 errors,
416 registry,
417@@ -2825,12 +2827,11 @@
418 % (name, from_module),
419 DeprecationWarning,
420 stacklevel=2)
421- m = __import__(from_module, globals(), locals(), [name])
422 try:
423- return getattr(m, name)
424+ return pyutils.get_named_object(from_module, name)
425 except AttributeError:
426 raise AttributeError('module %s has no name %s'
427- % (m, name))
428+ % (sys.modules[from_module], name))
429 globals()[name] = _deprecated_repository_forwarder
430
431 for _name in [
432
433=== modified file 'bzrlib/tests/TestUtil.py'
434--- bzrlib/tests/TestUtil.py 2010-08-05 18:13:23 +0000
435+++ bzrlib/tests/TestUtil.py 2010-09-28 08:31:43 +0000
436@@ -20,6 +20,8 @@
437 import logging
438 import unittest
439
440+from bzrlib import pyutils
441+
442 # Mark this python module as being part of the implementation
443 # of unittest: this gives us better tracebacks where the last
444 # shown frame is the test code, not our assertXYZ.
445@@ -106,7 +108,7 @@
446
447 def loadTestsFromModuleName(self, name):
448 result = self.suiteClass()
449- module = _load_module_by_name(name)
450+ module = pyutils.get_named_object(name)
451
452 result.addTests(self.loadTestsFromModule(module))
453 return result
454@@ -179,17 +181,6 @@
455 return self.suiteClass()
456
457
458-def _load_module_by_name(mod_name):
459- parts = mod_name.split('.')
460- module = __import__(mod_name)
461- del parts[0]
462- # for historical reasons python returns the top-level module even though
463- # it loads the submodule; we need to walk down to get the one we want.
464- while parts:
465- module = getattr(module, parts.pop(0))
466- return module
467-
468-
469 class TestVisitor(object):
470 """A visitor for Tests"""
471 def visitSuite(self, aTestSuite):
472
473=== modified file 'bzrlib/tests/__init__.py'
474--- bzrlib/tests/__init__.py 2010-09-27 19:31:45 +0000
475+++ bzrlib/tests/__init__.py 2010-09-28 08:31:43 +0000
476@@ -71,6 +71,7 @@
477 lock as _mod_lock,
478 memorytree,
479 osutils,
480+ pyutils,
481 ui,
482 urlutils,
483 registry,
484@@ -877,14 +878,14 @@
485
486 def _clear_hooks(self):
487 # prevent hooks affecting tests
488+ known_hooks = hooks.known_hooks
489 self._preserved_hooks = {}
490- for key, factory in hooks.known_hooks.items():
491- parent, name = hooks.known_hooks_key_to_parent_and_attribute(key)
492- current_hooks = hooks.known_hooks_key_to_object(key)
493+ for key, (parent, name) in known_hooks.iter_parent_objects():
494+ current_hooks = getattr(parent, name)
495 self._preserved_hooks[parent] = (name, current_hooks)
496 self.addCleanup(self._restoreHooks)
497- for key, factory in hooks.known_hooks.items():
498- parent, name = hooks.known_hooks_key_to_parent_and_attribute(key)
499+ for key, (parent, name) in known_hooks.iter_parent_objects():
500+ factory = known_hooks.get(key)
501 setattr(parent, name, factory())
502 # this hook should always be installed
503 request._install_hook()
504@@ -3773,6 +3774,7 @@
505 'bzrlib.tests.test_permissions',
506 'bzrlib.tests.test_plugins',
507 'bzrlib.tests.test_progress',
508+ 'bzrlib.tests.test_pyutils',
509 'bzrlib.tests.test_read_bundle',
510 'bzrlib.tests.test_reconcile',
511 'bzrlib.tests.test_reconfigure',
512@@ -3856,6 +3858,7 @@
513 'bzrlib.lockdir',
514 'bzrlib.merge3',
515 'bzrlib.option',
516+ 'bzrlib.pyutils',
517 'bzrlib.symbol_versioning',
518 'bzrlib.tests',
519 'bzrlib.tests.fixtures',
520@@ -4098,7 +4101,7 @@
521 the module is available.
522 """
523
524- py_module = __import__(py_module_name, {}, {}, ['NO_SUCH_ATTRIB'])
525+ py_module = pyutils.get_named_object(py_module_name)
526 scenarios = [
527 ('python', {'module': py_module}),
528 ]
529@@ -4257,9 +4260,8 @@
530 symbol_versioning.warn(depr_msg + use_msg, DeprecationWarning)
531 # Import the new feature and use it as a replacement for the
532 # deprecated one.
533- mod = __import__(self._replacement_module, {}, {},
534- [self._replacement_name])
535- self._feature = getattr(mod, self._replacement_name)
536+ self._feature = pyutils.get_named_object(
537+ self._replacement_module, self._replacement_name)
538
539 def _probe(self):
540 self._ensure()
541
542=== modified file 'bzrlib/tests/per_transport.py'
543--- bzrlib/tests/per_transport.py 2010-08-24 13:03:18 +0000
544+++ bzrlib/tests/per_transport.py 2010-09-28 08:31:43 +0000
545@@ -26,28 +26,24 @@
546 from StringIO import StringIO as pyStringIO
547 import stat
548 import sys
549-import unittest
550
551 from bzrlib import (
552 errors,
553 osutils,
554+ pyutils,
555 tests,
556 urlutils,
557 )
558 from bzrlib.errors import (ConnectionError,
559- DirectoryNotEmpty,
560 FileExists,
561 InvalidURL,
562- LockError,
563 NoSuchFile,
564- NotLocalUrl,
565 PathError,
566 TransportNotPossible,
567 )
568 from bzrlib.osutils import getcwd
569 from bzrlib.smart import medium
570 from bzrlib.tests import (
571- TestCaseInTempDir,
572 TestSkipped,
573 TestNotApplicable,
574 multiply_tests,
575@@ -78,7 +74,7 @@
576 for module in _get_transport_modules():
577 try:
578 permutations = get_transport_test_permutations(
579- reduce(getattr, (module).split('.')[1:], __import__(module)))
580+ pyutils.get_named_object(module))
581 for (klass, server_factory) in permutations:
582 scenario = ('%s,%s' % (klass.__name__, server_factory.__name__),
583 {"transport_class":klass,
584
585=== modified file 'bzrlib/tests/test_hooks.py'
586--- bzrlib/tests/test_hooks.py 2010-02-17 17:11:16 +0000
587+++ bzrlib/tests/test_hooks.py 2010-09-28 08:31:43 +0000
588@@ -28,6 +28,9 @@
589 known_hooks_key_to_object,
590 known_hooks_key_to_parent_and_attribute,
591 )
592+from bzrlib.symbol_versioning import (
593+ deprecated_in,
594+ )
595
596
597 class TestHooks(tests.TestCase):
598@@ -175,10 +178,20 @@
599 self.assertIs(branch.Branch.hooks,
600 known_hooks_key_to_object(('bzrlib.branch', 'Branch.hooks')))
601
602+ def test_known_hooks_key_to_parent_and_attribute_deprecated(self):
603+ self.assertEqual((branch.Branch, 'hooks'),
604+ self.applyDeprecated(deprecated_in((2,3)),
605+ known_hooks_key_to_parent_and_attribute,
606+ ('bzrlib.branch', 'Branch.hooks')))
607+ self.assertEqual((branch, 'Branch'),
608+ self.applyDeprecated(deprecated_in((2,3)),
609+ known_hooks_key_to_parent_and_attribute,
610+ ('bzrlib.branch', 'Branch')))
611+
612 def test_known_hooks_key_to_parent_and_attribute(self):
613 self.assertEqual((branch.Branch, 'hooks'),
614- known_hooks_key_to_parent_and_attribute(
615+ known_hooks.key_to_parent_and_attribute(
616 ('bzrlib.branch', 'Branch.hooks')))
617 self.assertEqual((branch, 'Branch'),
618- known_hooks_key_to_parent_and_attribute(
619+ known_hooks.key_to_parent_and_attribute(
620 ('bzrlib.branch', 'Branch')))
621
622=== added file 'bzrlib/tests/test_pyutils.py'
623--- bzrlib/tests/test_pyutils.py 1970-01-01 00:00:00 +0000
624+++ bzrlib/tests/test_pyutils.py 2010-09-28 08:31:43 +0000
625@@ -0,0 +1,88 @@
626+# Copyright (C) 2010 Canonical Ltd
627+#
628+# This program is free software; you can redistribute it and/or modify
629+# it under the terms of the GNU General Public License as published by
630+# the Free Software Foundation; either version 2 of the License, or
631+# (at your option) any later version.
632+#
633+# This program is distributed in the hope that it will be useful,
634+# but WITHOUT ANY WARRANTY; without even the implied warranty of
635+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
636+# GNU General Public License for more details.
637+#
638+# You should have received a copy of the GNU General Public License
639+# along with this program; if not, write to the Free Software
640+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
641+
642+"""Tests for bzrlib.pyutils."""
643+
644+from bzrlib import (
645+ branch,
646+ tests,
647+ )
648+from bzrlib.pyutils import (
649+ calc_parent_name,
650+ get_named_object,
651+ )
652+
653+
654+class TestGetNamedObject(tests.TestCase):
655+ """Tests for get_named_object."""
656+
657+ def test_module_only(self):
658+ import sys
659+ self.assertIs(sys, get_named_object('sys'))
660+
661+ def test_dotted_module(self):
662+ self.assertIs(branch, get_named_object('bzrlib.branch'))
663+
664+ def test_module_attr(self):
665+ self.assertIs(
666+ branch.Branch, get_named_object('bzrlib.branch', 'Branch'))
667+
668+ def test_dotted_attr(self):
669+ self.assertIs(
670+ branch.Branch.hooks,
671+ get_named_object('bzrlib.branch', 'Branch.hooks'))
672+
673+ def test_package(self):
674+ # bzrlib.tests is a package, not simply a module
675+ self.assertIs(tests, get_named_object('bzrlib.tests'))
676+
677+ def test_package_attr(self):
678+ # bzrlib.tests is a package, not simply a module
679+ self.assertIs(
680+ tests.TestCase, get_named_object('bzrlib.tests', 'TestCase'))
681+
682+ def test_import_error(self):
683+ self.assertRaises(ImportError, get_named_object, 'NO_SUCH_MODULE')
684+
685+ def test_attribute_error(self):
686+ self.assertRaises(
687+ AttributeError, get_named_object, 'sys', 'NO_SUCH_ATTR')
688+
689+
690+
691+class TestCalcParent_name(tests.TestCase):
692+ """Tests for calc_parent_name."""
693+
694+ def test_dotted_member(self):
695+ self.assertEqual(
696+ ('mod_name', 'attr1', 'attr2'),
697+ calc_parent_name('mod_name', 'attr1.attr2'))
698+
699+ def test_undotted_member(self):
700+ self.assertEqual(
701+ ('mod_name', None, 'attr1'),
702+ calc_parent_name('mod_name', 'attr1'))
703+
704+ def test_dotted_module_no_member(self):
705+ self.assertEqual(
706+ ('mod', None, 'sub_mod'),
707+ calc_parent_name('mod.sub_mod'))
708+
709+ def test_undotted_module_no_member(self):
710+ err = self.assertRaises(AssertionError, calc_parent_name, 'mod_name')
711+ self.assertEqual(
712+ "No parent object for top-level module 'mod_name'", err.args[0])
713+
714
715=== modified file 'bzrlib/tests/test_registry.py'
716--- bzrlib/tests/test_registry.py 2009-09-16 10:37:29 +0000
717+++ bzrlib/tests/test_registry.py 2010-09-28 08:31:43 +0000
718@@ -20,6 +20,7 @@
719 import sys
720
721 from bzrlib import (
722+ branch,
723 errors,
724 osutils,
725 registry,
726@@ -286,6 +287,13 @@
727 '\n\n'
728 )
729
730+ def test_lazy_import_registry_foo(self):
731+ a_registry = registry.Registry()
732+ a_registry.register_lazy('foo', 'bzrlib.branch', 'Branch')
733+ a_registry.register_lazy('bar', 'bzrlib.branch', 'Branch.hooks')
734+ self.assertEqual(branch.Branch, a_registry.get('foo'))
735+ self.assertEqual(branch.Branch.hooks, a_registry.get('bar'))
736+
737 def test_lazy_import_registry(self):
738 plugin_name = self.create_simple_plugin()
739 a_registry = registry.Registry()
740
741=== modified file 'bzrlib/tests/test_selftest.py'
742--- bzrlib/tests/test_selftest.py 2010-09-27 19:31:45 +0000
743+++ bzrlib/tests/test_selftest.py 2010-09-28 08:31:43 +0000
744@@ -78,18 +78,6 @@
745 return [t.id() for t in tests.iter_suite_tests(test_suite)]
746
747
748-class SelftestTests(tests.TestCase):
749-
750- def test_import_tests(self):
751- mod = TestUtil._load_module_by_name('bzrlib.tests.test_selftest')
752- self.assertEqual(mod.SelftestTests, SelftestTests)
753-
754- def test_import_test_failure(self):
755- self.assertRaises(ImportError,
756- TestUtil._load_module_by_name,
757- 'bzrlib.no-name-yet')
758-
759-
760 class MetaTestLog(tests.TestCase):
761
762 def test_logging(self):
763
764=== modified file 'doc/developers/code-style.txt'
765--- doc/developers/code-style.txt 2010-09-24 08:42:02 +0000
766+++ doc/developers/code-style.txt 2010-09-28 08:31:43 +0000
767@@ -485,5 +485,19 @@
768
769 * Don't say "open source" when you mean "free software".
770
771+
772+Dynamic imports
773+===============
774+
775+If you need to import a module (or attribute of a module) named in a
776+variable:
777+
778+ * If importing a module, not an attribute, and the module is a top-level
779+ module (i.e. has no dots in the name), then it's ok to use the builtin
780+ ``__import__``, e.g. ``__import__(module_name)``.
781+ * In all other cases, prefer ``bzrlib.pyutils.get_named_object`` to the
782+ built-in ``__import__``. ``__import__`` has some subtleties and
783+ unintuitive behaviours that make it hard to use correctly.
784+
785 ..
786 vim: ft=rst tw=74 ai