Merge lp:~lifeless/bzr/Commands.hooks into lp:~bzr/bzr/trunk-old

Proposed by Robert Collins
Status: Merged
Merged at revision: not available
Proposed branch: lp:~lifeless/bzr/Commands.hooks
Merge into: lp:~bzr/bzr/trunk-old
Diff against target: 510 lines (has conflicts)
Text conflict in NEWS
To merge this branch: bzr merge lp:~lifeless/bzr/Commands.hooks
Reviewer Review Type Date Requested Status
Ian Clatworthy Needs Fixing
Review via email: mp+6936@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

This makes command lookup fully hookified.

Why? It makes the core logic much clearer IMO, and also makes it
possible to skin bzrlib more completely for things like loggerhead and
olive want to have their own command (not subcommand) but still want to
use bzr's Command and option handling infrastructure.

-Rob

Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

This was reviewed (via BB) last week and my review comments haven't been actioned yet. See https://lists.ubuntu.com/archives/bazaar/2009q2/058746.html.

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2009-06-12 08:39:44 +0000
+++ NEWS 2009-06-15 08:36:49 +0000
@@ -184,9 +184,19 @@
184Internals184Internals
185*********185*********
186186
187<<<<<<< TREE
187* Remove ``weave.py`` script for accessing internals of old weave-format188* Remove ``weave.py`` script for accessing internals of old weave-format
188 repositories. (Martin Pool)189 repositories. (Martin Pool)
189190
191=======
192* Command lookup has had hooks added. ``bzrlib.Command.hooks`` has
193 three new hook points: ``get_command``, ``get_missing_command`` and
194 ``list_commands``, which allow just-in-time command name provision
195 rather than requiring all command names be known a-priori.
196 (Robert Collins)
197
198
199>>>>>>> MERGE-SOURCE
190Testing200Testing
191*******201*******
192202
193203
=== modified file 'bzrlib/commands.py'
--- bzrlib/commands.py 2009-06-10 03:56:49 +0000
+++ bzrlib/commands.py 2009-06-15 08:36:49 +0000
@@ -49,10 +49,11 @@
49 )49 )
50""")50""")
5151
52from bzrlib import registry
53# Compatibility
54from bzrlib.hooks import HookPoint, Hooks52from bzrlib.hooks import HookPoint, Hooks
53# Compatibility - Option used to be in commands.
55from bzrlib.option import Option54from bzrlib.option import Option
55from bzrlib import registry
56from bzrlib.symbol_versioning import deprecated_function, deprecated_in
5657
5758
58class CommandInfo(object):59class CommandInfo(object):
@@ -133,98 +134,181 @@
133134
134def _builtin_commands():135def _builtin_commands():
135 import bzrlib.builtins136 import bzrlib.builtins
137 return _scan_module_for_commands(bzrlib.builtins)
138
139
140def _scan_module_for_commands(module):
136 r = {}141 r = {}
137 builtins = bzrlib.builtins.__dict__142 for name, obj in module.__dict__.iteritems():
138 for name in builtins:
139 if name.startswith("cmd_"):143 if name.startswith("cmd_"):
140 real_name = _unsquish_command_name(name)144 real_name = _unsquish_command_name(name)
141 r[real_name] = builtins[name]145 r[real_name] = obj
142 return r146 return r
143147
144148
149def _list_bzr_commands(names):
150 """Return a list of all the registered commands.
151
152 This searches plugins and the core.
153 """
154 # to eliminate duplicates
155 names.update(builtin_command_names())
156 names.update(plugin_command_names())
157 return names
158
159
160def all_command_names():
161 """Return a list of all command names."""
162 names = set()
163 for hook in Command.hooks['list_commands']:
164 new_names = hook(names)
165 if new_names is None:
166 raise AssertionError(
167 'hook %s returned None' % Command.hooks.get_hook_name(hook))
168 names = new_names
169 return names
170
171
145def builtin_command_names():172def builtin_command_names():
146 """Return list of builtin command names."""173 """Return list of builtin command names.
174
175 Use of all_command_names() is encouraged rather than builtin_command_names
176 and/or plugin_command_names.
177 """
147 return _builtin_commands().keys()178 return _builtin_commands().keys()
148179
149180
150def plugin_command_names():181def plugin_command_names():
182 """Returns command names from commands registered by plugins."""
151 return plugin_cmds.keys()183 return plugin_cmds.keys()
152184
153185
154def _get_cmd_dict(plugins_override=True):186@deprecated_function(deprecated_in((1, 16, 0)))
155 """Return name->class mapping for all commands."""187def get_all_cmds():
188 """Return canonical name and class for most commands.
189
190 NB: This does not return all commands since the introduction of
191 command hooks, and returning the class is not sufficient to
192 get correctly setup commands, which is why it is deprecated.
193
194 Use 'all_command_names' + 'get_cmd_object' instead.
195 """
156 d = _builtin_commands()196 d = _builtin_commands()
157 if plugins_override:197 if plugins_override:
158 d.update(plugin_cmds.iteritems())198 d.update(plugin_cmds.iteritems())
159 return d199 for k, v in d.iteritems():
160
161
162def get_all_cmds(plugins_override=True):
163 """Return canonical name and class for all registered commands."""
164 for k, v in _get_cmd_dict(plugins_override=plugins_override).iteritems():
165 yield k,v200 yield k,v
166201
167202
168def get_cmd_object(cmd_name, plugins_override=True):203def get_cmd_object(cmd_name, plugins_override=True):
169 """Return the canonical name and command class for a command.204 """Return the command object for a command.
170205
171 plugins_override206 plugins_override
172 If true, plugin commands can override builtins.207 If true, plugin commands can override builtins.
173 """208 """
174 try:209 try:
175 cmd = _get_cmd_object(cmd_name, plugins_override)210 return _get_cmd_object(cmd_name, plugins_override)
176 # Allow plugins to extend commands
177 for hook in Command.hooks['extend_command']:
178 hook(cmd)
179 return cmd
180 except KeyError:211 except KeyError:
181 raise errors.BzrCommandError('unknown command "%s"' % cmd_name)212 raise errors.BzrCommandError('unknown command "%s"' % cmd_name)
182213
183214
184def _get_cmd_object(cmd_name, plugins_override=True):215def _get_cmd_object(cmd_name, plugins_override=True):
185 """Worker for get_cmd_object which raises KeyError rather than BzrCommandError."""216 """Get a command object.
186 from bzrlib.externalcommand import ExternalCommand
187217
218 :param cmd_name: The name of the command.
219 :param plugins_override: Allow plugins to override builtins.
220 :return: A Command object instance
221 :raises: KeyError if no command is found.
222 """
188 # We want only 'ascii' command names, but the user may have typed223 # We want only 'ascii' command names, but the user may have typed
189 # in a Unicode name. In that case, they should just get a224 # in a Unicode name. In that case, they should just get a
190 # 'command not found' error later.225 # 'command not found' error later.
191 # In the future, we may actually support Unicode command names.226 # In the future, we may actually support Unicode command names.
192227 cmd = None
193 # first look up this command under the specified name228 # Get a command
194 if plugins_override:229 for hook in Command.hooks['get_command']:
230 cmd = hook(cmd, cmd_name)
231 if cmd is not None and not plugins_override:
232 # We've found a non-plugin command, don't permit it to be
233 # overridden.
234 if not cmd.plugin_name():
235 break
236 if cmd is None:
237 for hook in Command.hooks['get_missing_command']:
238 cmd = hook(cmd_name)
239 if cmd is not None:
240 break
241 if cmd is None:
242 # No command found.
243 raise KeyError
244 # Allow plugins to extend commands
245 for hook in Command.hooks['extend_command']:
246 hook(cmd)
247 return cmd
248
249
250def _try_plugin_provider(cmd_name):
251 """Probe for a plugin provider having cmd_name."""
252 try:
253 plugin_metadata, provider = probe_for_provider(cmd_name)
254 raise errors.CommandAvailableInPlugin(cmd_name,
255 plugin_metadata, provider)
256 except errors.NoPluginAvailable:
257 pass
258
259
260def probe_for_provider(cmd_name):
261 """Look for a provider for cmd_name.
262
263 :param cmd_name: The command name.
264 :return: plugin_metadata, provider for getting cmd_name.
265 :raises NoPluginAvailable: When no provider can supply the plugin.
266 """
267 # look for providers that provide this command but aren't installed
268 for provider in command_providers_registry:
195 try:269 try:
196 return plugin_cmds.get(cmd_name)()270 return provider.plugin_for_command(cmd_name), provider
197 except KeyError:271 except errors.NoPluginAvailable:
198 pass272 pass
199 cmds = _get_cmd_dict(plugins_override=False)273 raise errors.NoPluginAvailable(cmd_name)
274
275
276def _get_bzr_command(cmd_or_None, cmd_name):
277 """Get a command from bzr's core."""
278 cmds = _builtin_commands()
200 try:279 try:
201 return cmds[cmd_name]()280 return cmds[cmd_name]()
202 except KeyError:281 except KeyError:
203 pass282 pass
204 if plugins_override:
205 for key in plugin_cmds.keys():
206 info = plugin_cmds.get_info(key)
207 if cmd_name in info.aliases:
208 return plugin_cmds.get(key)()
209 # look for any command which claims this as an alias283 # look for any command which claims this as an alias
210 for real_cmd_name, cmd_class in cmds.iteritems():284 for real_cmd_name, cmd_class in cmds.iteritems():
211 if cmd_name in cmd_class.aliases:285 if cmd_name in cmd_class.aliases:
212 return cmd_class()286 return cmd_class()
213287 return cmd_or_None
288
289
290def _get_external_command(cmd_or_None, cmd_name):
291 """Lookup a command that is a shell script."""
292 # Only do external command lookups when no command is found so far.
293 if cmd_or_None is not None:
294 return cmd_or_None
295 from bzrlib.externalcommand import ExternalCommand
214 cmd_obj = ExternalCommand.find_command(cmd_name)296 cmd_obj = ExternalCommand.find_command(cmd_name)
215 if cmd_obj:297 if cmd_obj:
216 return cmd_obj298 return cmd_obj
217299
218 # look for plugins that provide this command but aren't installed300
219 for provider in command_providers_registry:301def _get_plugin_command(cmd_or_None, cmd_name):
220 try:302 """Get a command from bzr's plugins."""
221 plugin_metadata = provider.plugin_for_command(cmd_name)303 try:
222 except errors.NoPluginAvailable:304 return plugin_cmds.get(cmd_name)()
223 pass305 except KeyError:
224 else:306 pass
225 raise errors.CommandAvailableInPlugin(cmd_name,307 for key in plugin_cmds.keys():
226 plugin_metadata, provider)308 info = plugin_cmds.get_info(key)
227 raise KeyError309 if cmd_name in info.aliases:
310 return plugin_cmds.get(key)()
311 return cmd_or_None
228312
229313
230class Command(object):314class Command(object):
@@ -608,6 +692,23 @@
608 "Called after creating a command object to allow modifications "692 "Called after creating a command object to allow modifications "
609 "such as adding or removing options, docs etc. Called with the "693 "such as adding or removing options, docs etc. Called with the "
610 "new bzrlib.commands.Command object.", (1, 13), None))694 "new bzrlib.commands.Command object.", (1, 13), None))
695 self.create_hook(HookPoint('get_command',
696 "Called when creating a single command. Called with "
697 "(cmd_or_None, command_name). get_command should either return "
698 "the cmd_or_None parameter, or a replacement Command object that "
699 "should be used for the command.", (1, 16), None))
700 self.create_hook(HookPoint('get_missing_command',
701 "Called when creating a single command if no command could be "
702 "found. Called with (command_name). get_missing_command should "
703 "either return None, or a Command object to be used for the "
704 "command.", (1, 16), None))
705 self.create_hook(HookPoint('list_commands',
706 "Called when enumerating commands. Called with a dict of "
707 "cmd_name: cmd_class tuples for all the commands found "
708 "so far. This dict is safe to mutate - to remove a command or "
709 "to replace it with another (eg plugin supplied) version. "
710 "list_commands should return the updated dict of commands.",
711 (1, 16), None))
611712
612Command.hooks = CommandHooks()713Command.hooks = CommandHooks()
613714
@@ -952,6 +1053,20 @@
952 return ignore_pipe1053 return ignore_pipe
9531054
9541055
1056def install_bzr_command_hooks():
1057 """Install the hooks to supply bzr's own commands."""
1058 Command.hooks.install_named_hook("list_commands", _list_bzr_commands,
1059 "bzr commands")
1060 Command.hooks.install_named_hook("get_command", _get_bzr_command,
1061 "bzr commands")
1062 Command.hooks.install_named_hook("get_command", _get_plugin_command,
1063 "bzr plugin commands")
1064 Command.hooks.install_named_hook("get_command", _get_external_command,
1065 "bzr external command lookup")
1066 Command.hooks.install_named_hook("get_missing_command", _try_plugin_provider,
1067 "bzr plugin-provider-db check")
1068
1069
955def main(argv=None):1070def main(argv=None):
956 """Main entry point of command-line interface.1071 """Main entry point of command-line interface.
9571072
@@ -968,7 +1083,6 @@
9681083
969 # Is this a final release version? If so, we should suppress warnings1084 # Is this a final release version? If so, we should suppress warnings
970 if bzrlib.version_info[3] == 'final':1085 if bzrlib.version_info[3] == 'final':
971 from bzrlib import symbol_versioning
972 symbol_versioning.suppress_deprecation_warnings(override=False)1086 symbol_versioning.suppress_deprecation_warnings(override=False)
973 if argv is None:1087 if argv is None:
974 argv = osutils.get_unicode_argv()1088 argv = osutils.get_unicode_argv()
@@ -984,6 +1098,7 @@
984 except UnicodeDecodeError:1098 except UnicodeDecodeError:
985 raise errors.BzrError("argv should be list of unicode strings.")1099 raise errors.BzrError("argv should be list of unicode strings.")
986 argv = new_argv1100 argv = new_argv
1101 install_bzr_command_hooks()
987 ret = run_bzr_catch_errors(argv)1102 ret = run_bzr_catch_errors(argv)
988 trace.mutter("return code %d", ret)1103 trace.mutter("return code %d", ret)
989 return ret1104 return ret
9901105
=== modified file 'bzrlib/help.py'
--- bzrlib/help.py 2009-03-23 14:59:43 +0000
+++ bzrlib/help.py 2009-06-15 08:36:49 +0000
@@ -73,8 +73,7 @@
73 hidden = True73 hidden = True
74 else:74 else:
75 hidden = False75 hidden = False
76 names = set(_mod_commands.builtin_command_names()) # to eliminate duplicates76 names = list(_mod_commands.all_command_names())
77 names.update(_mod_commands.plugin_command_names())
78 commands = ((n, _mod_commands.get_cmd_object(n)) for n in names)77 commands = ((n, _mod_commands.get_cmd_object(n)) for n in names)
79 shown_commands = [(n, o) for n, o in commands if o.hidden == hidden]78 shown_commands = [(n, o) for n, o in commands if o.hidden == hidden]
80 max_name = max(len(n) for n, o in shown_commands)79 max_name = max(len(n) for n, o in shown_commands)
8180
=== modified file 'bzrlib/shellcomplete.py'
--- bzrlib/shellcomplete.py 2009-03-23 14:59:43 +0000
+++ bzrlib/shellcomplete.py 2009-06-15 08:36:49 +0000
@@ -64,15 +64,16 @@
64 outfile = sys.stdout64 outfile = sys.stdout
6565
66 cmds = []66 cmds = []
67 for cmdname, cmdclass in commands.get_all_cmds():67 for cmdname in commands.all_command_names():
68 cmds.append((cmdname, cmdclass))68 cmd = commands.get_cmd_object(cmdname)))
69 for alias in cmdclass.aliases:69 cmds.append((cmdname, cmd))
70 cmds.append((alias, cmdclass))70 for alias in cmd.aliases:
71 cmds.append((alias, cmd))
71 cmds.sort()72 cmds.sort()
72 for cmdname, cmdclass in cmds:73 for cmdname, cmd in cmds:
73 if cmdclass.hidden:74 if cmd.hidden:
74 continue75 continue
75 doc = getdoc(cmdclass)76 doc = getdoc(cmd)
76 if doc is None:77 if doc is None:
77 outfile.write(cmdname + '\n')78 outfile.write(cmdname + '\n')
78 else:79 else:
7980
=== modified file 'bzrlib/tests/test_commands.py'
--- bzrlib/tests/test_commands.py 2009-04-07 17:13:51 +0000
+++ bzrlib/tests/test_commands.py 2009-06-15 08:36:49 +0000
@@ -163,6 +163,7 @@
163 del sys.modules['bzrlib.tests.fake_command']163 del sys.modules['bzrlib.tests.fake_command']
164 global lazy_command_imported164 global lazy_command_imported
165 lazy_command_imported = False165 lazy_command_imported = False
166 commands.install_bzr_command_hooks()
166167
167 @staticmethod168 @staticmethod
168 def remove_fake():169 def remove_fake():
@@ -205,6 +206,7 @@
205 # commands are registered).206 # commands are registered).
206 # when they are simply created.207 # when they are simply created.
207 hook_calls = []208 hook_calls = []
209 commands.install_bzr_command_hooks()
208 commands.Command.hooks.install_named_hook(210 commands.Command.hooks.install_named_hook(
209 "extend_command", hook_calls.append, None)211 "extend_command", hook_calls.append, None)
210 # create a command, should not fire212 # create a command, should not fire
@@ -237,3 +239,87 @@
237 self.assertEqual([cmd], hook_calls)239 self.assertEqual([cmd], hook_calls)
238 finally:240 finally:
239 commands.plugin_cmds.remove('fake')241 commands.plugin_cmds.remove('fake')
242
243
244class TestGetCommandHook(tests.TestCase):
245
246 def test_fires_on_get_cmd_object(self):
247 # The get_command(cmd) hook fires when commands are delivered to the
248 # ui.
249 commands.install_bzr_command_hooks()
250 hook_calls = []
251 class ACommand(commands.Command):
252 """A sample command."""
253 def get_cmd(cmd_or_None, cmd_name):
254 hook_calls.append(('called', cmd_or_None, cmd_name))
255 if cmd_name in ('foo', 'info'):
256 return ACommand()
257 commands.Command.hooks.install_named_hook(
258 "get_command", get_cmd, None)
259 # create a command directly, should not fire
260 cmd = ACommand()
261 self.assertEqual([], hook_calls)
262 # ask by name, should fire and give us our command
263 cmd = commands.get_cmd_object('foo')
264 self.assertEqual([('called', None, 'foo')], hook_calls)
265 self.assertIsInstance(cmd, ACommand)
266 del hook_calls[:]
267 # ask by a name that is supplied by a builtin - the hook should still
268 # fire and we still get our object, but we should see the builtin
269 # passed to the hook.
270 cmd = commands.get_cmd_object('info')
271 self.assertIsInstance(cmd, ACommand)
272 self.assertEqual(1, len(hook_calls))
273 self.assertEqual('info', hook_calls[0][2])
274 self.assertIsInstance(hook_calls[0][1], builtins.cmd_info)
275
276
277class TestGetMissingCommandHook(tests.TestCase):
278
279 def test_fires_on_get_cmd_object(self):
280 # The get_missing_command(cmd) hook fires when commands are delivered to the
281 # ui.
282 hook_calls = []
283 class ACommand(commands.Command):
284 """A sample command."""
285 def get_missing_cmd(cmd_name):
286 hook_calls.append(('called', cmd_name))
287 if cmd_name in ('foo', 'info'):
288 return ACommand()
289 commands.Command.hooks.install_named_hook(
290 "get_missing_command", get_missing_cmd, None)
291 # create a command directly, should not fire
292 cmd = ACommand()
293 self.assertEqual([], hook_calls)
294 # ask by name, should fire and give us our command
295 cmd = commands.get_cmd_object('foo')
296 self.assertEqual([('called', 'foo')], hook_calls)
297 self.assertIsInstance(cmd, ACommand)
298 del hook_calls[:]
299 # ask by a name that is supplied by a builtin - the hook should not
300 # fire and we still get our object.
301 commands.install_bzr_command_hooks()
302 cmd = commands.get_cmd_object('info')
303 self.assertNotEqual(None, cmd)
304 self.assertEqual(0, len(hook_calls))
305
306
307class TestListCommandHook(tests.TestCase):
308
309 def test_fires_on_all_command_names(self):
310 # The list_commands() hook fires when all_command_names() is invoked.
311 hook_calls = []
312 commands.install_bzr_command_hooks()
313 def list_my_commands(cmd_names):
314 hook_calls.append('called')
315 cmd_names.update(['foo', 'bar'])
316 return cmd_names
317 commands.Command.hooks.install_named_hook(
318 "list_commands", list_my_commands, None)
319 # Get a command, which should not trigger the hook.
320 cmd = commands.get_cmd_object('info')
321 self.assertEqual([], hook_calls)
322 # Get all command classes (for docs and shell completion).
323 cmds = list(commands.all_command_names())
324 self.assertEqual(['called'], hook_calls)
325 self.assertSubset(['foo', 'bar'], cmds)
240326
=== modified file 'bzrlib/tests/test_options.py'
--- bzrlib/tests/test_options.py 2009-04-03 20:05:25 +0000
+++ bzrlib/tests/test_options.py 2009-06-15 08:36:49 +0000
@@ -324,8 +324,8 @@
324324
325 def get_builtin_command_options(self):325 def get_builtin_command_options(self):
326 g = []326 g = []
327 for cmd_name, cmd_class in sorted(commands.get_all_cmds()):327 for cmd_name in sorted(commands.all_command_names()):
328 cmd = cmd_class()328 cmd = commands.get_cmd_object(cmd_name)
329 for opt_name, opt in sorted(cmd.options().items()):329 for opt_name, opt in sorted(cmd.options().items()):
330 g.append((cmd_name, opt))330 g.append((cmd_name, opt))
331 return g331 return g
@@ -338,14 +338,15 @@
338 g = dict(option.Option.OPTIONS.items())338 g = dict(option.Option.OPTIONS.items())
339 used_globals = {}339 used_globals = {}
340 msgs = []340 msgs = []
341 for cmd_name, cmd_class in sorted(commands.get_all_cmds()):341 for cmd_name in sorted(commands.all_command_names()):
342 for option_or_name in sorted(cmd_class.takes_options):342 cmd = commands.get_cmd_object(cmd_name)
343 for option_or_name in sorted(cmd.takes_options):
343 if not isinstance(option_or_name, basestring):344 if not isinstance(option_or_name, basestring):
344 self.assertIsInstance(option_or_name, option.Option)345 self.assertIsInstance(option_or_name, option.Option)
345 elif not option_or_name in g:346 elif not option_or_name in g:
346 msgs.append("apparent reference to undefined "347 msgs.append("apparent reference to undefined "
347 "global option %r from %r"348 "global option %r from %r"
348 % (option_or_name, cmd_class))349 % (option_or_name, cmd))
349 else:350 else:
350 used_globals.setdefault(option_or_name, []).append(cmd_name)351 used_globals.setdefault(option_or_name, []).append(cmd_name)
351 unused_globals = set(g.keys()) - set(used_globals.keys())352 unused_globals = set(g.keys()) - set(used_globals.keys())