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