Rev 4123: Add Command lookup hooks: list_commands and get_command. in http://people.ubuntu.com/~robertc/baz2.0/pending/Commands.hooks
Robert Collins
robertc at robertcollins.net
Thu Mar 12 23:04:50 GMT 2009
At http://people.ubuntu.com/~robertc/baz2.0/pending/Commands.hooks
------------------------------------------------------------
revno: 4123
revision-id: robertc at robertcollins.net-20090312230445-wu5x0s0z9jrenu2y
parent: robertc at robertcollins.net-20090312083453-kvg3z61lgepkmh96
committer: Robert Collins <robertc at robertcollins.net>
branch nick: Commands.hooks
timestamp: Fri 2009-03-13 10:04:45 +1100
message:
Add Command lookup hooks: list_commands and get_command.
=== modified file 'NEWS'
--- a/NEWS 2009-03-12 06:24:39 +0000
+++ b/NEWS 2009-03-12 23:04:45 +0000
@@ -258,6 +258,11 @@
string, and never emptied the cache. This should slightly reduce
memory consumption. (John Arbash Meinel)
+ * Command lookup has had hooks added. ``bzrlib.Command.hooks`` has
+ two new hook points: ``get_command`` and ``list_commands``, which
+ allow just-in-time command name provision rather than requiring that
+ all command names be known a-priori. (Robert Collins)
+
* New branch method ``create_clone_on_transport`` that returns a
branch object. (Robert Collins)
=== modified file 'bzrlib/commands.py'
--- a/bzrlib/commands.py 2009-03-12 06:24:39 +0000
+++ b/bzrlib/commands.py 2009-03-12 23:04:45 +0000
@@ -141,12 +141,31 @@
return r
+def all_command_names():
+ """Return a list of all command names."""
+ # to eliminate duplicates
+ names = set(builtin_command_names())
+ names.update(plugin_command_names())
+ for hook in Command.hooks['list_commands']:
+ new_names = hook(names)
+ if new_names is None:
+ raise AssertionError(
+ 'hook %s returned None' % Command.hooks.get_hook_name(hook))
+ names = new_names
+ return names
+
+
def builtin_command_names():
- """Return list of builtin command names."""
+ """Return list of builtin command names.
+
+ Use of all_command_names() is encouraged rather than builtin_command_names
+ and/or plugin_command_names.
+ """
return _builtin_commands().keys()
def plugin_command_names():
+ """Returns command names from commands registered by plugins."""
return plugin_cmds.keys()
@@ -165,22 +184,62 @@
def get_cmd_object(cmd_name, plugins_override=True):
- """Return the canonical name and command class for a command.
+ """Return the command object for a command.
plugins_override
If true, plugin commands can override builtins.
"""
try:
- cmd = _get_cmd_object(cmd_name, plugins_override)
- # Allow plugins to extend commands
- for hook in Command.hooks['extend_command']:
- hook(cmd)
- return cmd
+ return _get_cmd_object(cmd_name, plugins_override)
except KeyError:
raise errors.BzrCommandError('unknown command "%s"' % cmd_name)
def _get_cmd_object(cmd_name, plugins_override=True):
+ """Get a command object.
+
+ :param cmd_name: The name of the command.
+ :param plugins_override: Allow plugins to override builtins.
+ :return: A Command object instance
+ :raises: KeyError if no command is found.
+ """
+ # Pre-hook command lookup logic.
+ cmd = __get_cmd_object(cmd_name, plugins_override)
+ # Allow hooks to supply/replace commands:
+ for hook in Command.hooks['get_command']:
+ cmd = hook(cmd, cmd_name)
+ if cmd is None:
+ try:
+ plugin_metadata, provider = probe_for_provider(cmd_name)
+ raise errors.CommandAvailableInPlugin(cmd_name,
+ plugin_metadata, provider)
+ except errors.NoPluginAvailable:
+ pass
+ # No command found.
+ raise KeyError
+ # Allow plugins to extend commands
+ for hook in Command.hooks['extend_command']:
+ hook(cmd)
+ return cmd
+
+
+def probe_for_provider(cmd_name):
+ """Look for a provider for cmd_name.
+
+ :param cmd_name: The command name.
+ :return: plugin_metadata, provider for getting cmd_name.
+ :raises NoPluginAvailable: When no provider can supply the plugin.
+ """
+ # look for providers that provide this command but aren't installed
+ for provider in command_providers_registry:
+ try:
+ return provider.plugin_for_command(cmd_name), provider
+ except errors.NoPluginAvailable:
+ pass
+ raise errors.NoPluginAvailable(cmd_name)
+
+
+def __get_cmd_object(cmd_name, plugins_override):
"""Worker for get_cmd_object which raises KeyError rather than BzrCommandError."""
from bzrlib.externalcommand import ExternalCommand
@@ -213,17 +272,7 @@
cmd_obj = ExternalCommand.find_command(cmd_name)
if cmd_obj:
return cmd_obj
-
- # look for plugins that provide this command but aren't installed
- for provider in command_providers_registry:
- try:
- plugin_metadata = provider.plugin_for_command(cmd_name)
- except errors.NoPluginAvailable:
- pass
- else:
- raise errors.CommandAvailableInPlugin(cmd_name,
- plugin_metadata, provider)
- raise KeyError
+ return None
class Command(object):
@@ -595,6 +644,18 @@
"Called after creating a command object to allow modifications "
"such as adding or removing options, docs etc. Called with the "
"new bzrlib.commands.Command object.", (1, 13), None))
+ self.create_hook(HookPoint('get_command',
+ "Called when creating a single command. Called with "
+ "(cmd_or_None, command_name). get_command should either return "
+ "the cmd_or_None parameter, or a replacement Command object that "
+ "should be used for the command.", (1, 14), None))
+ self.create_hook(HookPoint('list_commands',
+ "Called when enumerating commands. Called with a dict of "
+ "cmd_name: cmd_class tuples for all the commands found "
+ "so far. This dict is safe to mutate - to remove a command or "
+ "to replace it with another (eg plugin supplied) version. "
+ "list_commands should return the updated dict of commands.",
+ (1, 14), None))
Command.hooks = CommandHooks()
=== modified file 'bzrlib/help.py'
--- a/bzrlib/help.py 2009-01-17 01:30:58 +0000
+++ b/bzrlib/help.py 2009-03-12 23:04:45 +0000
@@ -72,8 +72,7 @@
hidden = True
else:
hidden = False
- names = set(_mod_commands.builtin_command_names()) # to eliminate duplicates
- names.update(_mod_commands.plugin_command_names())
+ names = list(_mod_commands.all_command_names())
commands = ((n, _mod_commands.get_cmd_object(n)) for n in names)
shown_commands = [(n, o) for n, o in commands if o.hidden == hidden]
max_name = max(len(n) for n, o in shown_commands)
=== modified file 'bzrlib/tests/test_commands.py'
--- a/bzrlib/tests/test_commands.py 2009-02-11 12:49:50 +0000
+++ b/bzrlib/tests/test_commands.py 2009-03-12 23:04:45 +0000
@@ -218,3 +218,55 @@
self.assertEqual([cmd], hook_calls)
finally:
commands.plugin_cmds.remove('fake')
+
+
+class TestGetCommandHook(tests.TestCase):
+
+ def test_fires_on_get_cmd_object(self):
+ # The get_command(cmd) hook fires when commands are delivered to the
+ # ui.
+ hook_calls = []
+ class ACommand(commands.Command):
+ """A sample command."""
+ def get_cmd(cmd_or_None, cmd_name):
+ hook_calls.append(('called', cmd_or_None, cmd_name))
+ if cmd_name in ('foo', 'info'):
+ return ACommand()
+ commands.Command.hooks.install_named_hook(
+ "get_command", get_cmd, None)
+ # create a command directly, should not fire
+ cmd = ACommand()
+ self.assertEqual([], hook_calls)
+ # ask by name, should fire and give us our command
+ cmd = commands.get_cmd_object('foo')
+ self.assertEqual([('called', None, 'foo')], hook_calls)
+ self.assertIsInstance(cmd, ACommand)
+ del hook_calls[:]
+ # ask by a name that is supplied by a builtin - the hook should still
+ # fire and we still get our object, but we should see the builtin
+ # passed to the hook.
+ cmd = commands.get_cmd_object('info')
+ self.assertIsInstance(cmd, ACommand)
+ self.assertEqual(1, len(hook_calls))
+ self.assertEqual('info', hook_calls[0][2])
+ self.assertIsInstance(hook_calls[0][1], builtins.cmd_info)
+
+
+class TestListCommandHook(tests.TestCase):
+
+ def test_fires_on_all_command_names(self):
+ # The list_commands() hook fires when all_command_names() is invoked.
+ hook_calls = []
+ def list_my_commands(cmd_names):
+ hook_calls.append('called')
+ cmd_names.update(['foo', 'bar'])
+ return cmd_names
+ commands.Command.hooks.install_named_hook(
+ "list_commands", list_my_commands, None)
+ # Get a command, which should not trigger the hook.
+ cmd = commands.get_cmd_object('info')
+ self.assertEqual([], hook_calls)
+ # Get all command classes (for docs and shell completion).
+ cmds = list(commands.all_command_names())
+ self.assertEqual(['called'], hook_calls)
+ self.assertSubset(['foo', 'bar'], cmds)
More information about the bazaar-commits
mailing list