Merge lp:~spiv/bzr/hooks-refactoring into lp:bzr
- hooks-refactoring
- Merge into bzr.dev
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 |
Related bugs: |
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.
Description of the change
While reviewing <https:/
1. hooks.py was more confusing than necessary
2. _LazyObjectGett
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_
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[
Andrew Bennetts (spiv) wrote : | # |
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. _LazyObjectGett
>
> 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_
> 197 +known_
> 198 +known_
> 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_
> known_hooks.
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-
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_
>
> It's a pretty generic name. Perhaps the name should include 'import' to give
> you a bit more of a clue, like import_
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.
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...
Martin Pool (mbp) 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-
> 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_
>>
>> It's a pretty generic name. Perhaps the name should include 'import' to give
>> you a bit more of a clue, like import_
>
> 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.
> 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
> ...
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://
"The closing quotes are on the same line as the opening quotes. This
looks better for one-liners."
etc.
_Rob
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
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
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-
> > 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-
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.
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-
>
> 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
Vincent Ladeuil (vila) wrote : | # |
Are there still bits pending review here ?
Or is there a consensus already ?
Vincent Ladeuil (vila) wrote : | # |
In other words: do you need help from patch pilot or can this be landed ?
Vincent Ladeuil (vila) wrote : | # |
Ping.
https:/
Preview Diff
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 |
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. _LazyObjectGett er(...) .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') hooks.register_ lazy_hook( 'bzrlib. bzrdir' , 'BzrDir.hooks', 'BzrDirHooks') hooks.register_ lazy_hook(
197 +known_
198 +known_
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: hooks.register_ lazy_hook( hook_module, hook_attribute_ name, hook_class)
known_
+"""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