add hook for manipulating the argument parser
Update Commands to load a separate set of extensions to be used as "hooks," triggered at different points in the processing of the command. Start with a hook that is given access to the argument parser for the command so it can modify it. Change-Id: I0785548fd36a61cda616921a4a21be3f67701300 Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
parent
decac4318e
commit
e72e54e757
|
@ -14,6 +14,7 @@ import abc
|
|||
import inspect
|
||||
|
||||
import six
|
||||
from stevedore import extension
|
||||
|
||||
from cliff import _argparse
|
||||
|
||||
|
@ -22,6 +23,12 @@ from cliff import _argparse
|
|||
class Command(object):
|
||||
"""Base class for command plugins.
|
||||
|
||||
When the command is instantiated, it loads extensions from a
|
||||
namespace based on the parent application namespace and the
|
||||
command name::
|
||||
|
||||
app.namespace + '.' + cmd_name.replace(' ', '_')
|
||||
|
||||
:param app: Application instance invoking the command.
|
||||
:paramtype app: cliff.app.App
|
||||
|
||||
|
@ -36,6 +43,26 @@ class Command(object):
|
|||
self.app = app
|
||||
self.app_args = app_args
|
||||
self.cmd_name = cmd_name
|
||||
self._load_hooks()
|
||||
|
||||
def _load_hooks(self):
|
||||
# Look for command extensions
|
||||
if self.app and self.cmd_name:
|
||||
namespace = '{}.{}'.format(
|
||||
self.app.command_manager.namespace,
|
||||
self.cmd_name.replace(' ', '_')
|
||||
)
|
||||
self._hooks = extension.ExtensionManager(
|
||||
namespace=namespace,
|
||||
invoke_on_load=True,
|
||||
invoke_kwds={
|
||||
'command': self,
|
||||
},
|
||||
)
|
||||
else:
|
||||
# Setting _hooks to an empty list allows iteration without
|
||||
# checking if there are hooks every time.
|
||||
self._hooks = []
|
||||
return
|
||||
|
||||
def get_description(self):
|
||||
|
@ -73,6 +100,8 @@ class Command(object):
|
|||
prog=prog_name,
|
||||
formatter_class=_SmartHelpFormatter,
|
||||
)
|
||||
for hook in self._hooks:
|
||||
hook.obj.get_parser(parser)
|
||||
return parser
|
||||
|
||||
@abc.abstractmethod
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CommandHook(object):
|
||||
"""Base class for command hooks.
|
||||
|
||||
:param app: Command instance being invoked
|
||||
:paramtype app: cliff.command.Command
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, command):
|
||||
self.cmd = command
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_parser(self, parser):
|
||||
"""Return an :class:`argparse.ArgumentParser`.
|
||||
|
||||
:param parser: An existing ArgumentParser instance to be modified.
|
||||
:paramtype parser: ArgumentParser
|
||||
:returns: ArgumentParser
|
||||
"""
|
||||
return parser
|
|
@ -0,0 +1,107 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from cliff import app as application
|
||||
from cliff import command
|
||||
from cliff import commandmanager
|
||||
from cliff import hooks
|
||||
from cliff.tests import base
|
||||
|
||||
import mock
|
||||
from stevedore import extension
|
||||
|
||||
|
||||
def make_app(**kwargs):
|
||||
cmd_mgr = commandmanager.CommandManager('cliff.tests')
|
||||
|
||||
# Register a command that succeeds
|
||||
cmd = mock.MagicMock(spec=command.Command)
|
||||
command_inst = mock.MagicMock(spec=command.Command)
|
||||
command_inst.run.return_value = 0
|
||||
command.return_value = command_inst
|
||||
cmd_mgr.add_command('mock', cmd)
|
||||
|
||||
# Register a command that fails
|
||||
err_command = mock.Mock(name='err_command', spec=command.Command)
|
||||
err_command_inst = mock.Mock(spec=command.Command)
|
||||
err_command_inst.run = mock.Mock(
|
||||
side_effect=RuntimeError('test exception')
|
||||
)
|
||||
err_command.return_value = err_command_inst
|
||||
cmd_mgr.add_command('error', err_command)
|
||||
|
||||
app = application.App('testing command hooks',
|
||||
'1',
|
||||
cmd_mgr,
|
||||
stderr=mock.Mock(), # suppress warning messages
|
||||
**kwargs
|
||||
)
|
||||
return app
|
||||
|
||||
|
||||
class TestCommand(command.Command):
|
||||
"""Description of command.
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(TestCommand, self).get_parser(prog_name)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
return 42
|
||||
|
||||
|
||||
class TestHook(hooks.CommandHook):
|
||||
|
||||
def get_parser(self, parser):
|
||||
parser.add_argument('--added-by-hook')
|
||||
return parser
|
||||
|
||||
|
||||
class TestCommandLoadHooks(base.TestBase):
|
||||
|
||||
def test_no_app_or_name(self):
|
||||
cmd = TestCommand(None, None)
|
||||
self.assertEqual([], cmd._hooks)
|
||||
|
||||
@mock.patch('stevedore.extension.ExtensionManager')
|
||||
def test_app_and_name(self, em):
|
||||
app = make_app()
|
||||
TestCommand(app, None, cmd_name='test')
|
||||
print(em.mock_calls[0])
|
||||
name, args, kwargs = em.mock_calls[0]
|
||||
print(kwargs)
|
||||
self.assertEqual('cliff.tests.test', kwargs['namespace'])
|
||||
|
||||
|
||||
class TestParserHook(base.TestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestParserHook, self).setUp()
|
||||
self.app = make_app()
|
||||
self.cmd = TestCommand(self.app, None, cmd_name='test')
|
||||
self.hook = TestHook(self.cmd)
|
||||
self.mgr = extension.ExtensionManager.make_test_instance(
|
||||
[extension.Extension(
|
||||
'parser-hook',
|
||||
None,
|
||||
None,
|
||||
self.hook)],
|
||||
)
|
||||
# Replace the auto-loaded hooks with our explicitly created
|
||||
# manager.
|
||||
self.cmd._hooks = self.mgr
|
||||
|
||||
def test_get_parser(self):
|
||||
parser = self.cmd.get_parser('test')
|
||||
results = parser.parse_args(['--added-by-hook', 'value'])
|
||||
self.assertEqual(results.added_by_hook, 'value')
|
|
@ -0,0 +1,41 @@
|
|||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from cliff.command import Command
|
||||
from cliff.hooks import CommandHook
|
||||
|
||||
|
||||
class Hooked(Command):
|
||||
"A command to demonstrate how the hooks work"
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.app.stdout.write('this command has an extension')
|
||||
|
||||
|
||||
class Hook(CommandHook):
|
||||
"""Hook sample for the 'hooked' command.
|
||||
|
||||
This would normally be provided by a separate package from the
|
||||
main application, but is included in the demo app for simplicity.
|
||||
|
||||
"""
|
||||
|
||||
def get_parser(self, parser):
|
||||
print('sample hook get_parser()')
|
||||
parser.add_argument('--added-by-hook')
|
||||
return parser
|
|
@ -60,6 +60,10 @@ setup(
|
|||
'file = cliffdemo.show:File',
|
||||
'show file = cliffdemo.show:File',
|
||||
'unicode = cliffdemo.encoding:Encoding',
|
||||
'hooked = cliffdemo.hook:Hooked',
|
||||
],
|
||||
'cliff.demo.hooked': [
|
||||
'sample-hook = cliffdemo.hook:Hook',
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
@ -29,6 +29,12 @@ Command
|
|||
.. autoclass:: cliff.command.Command
|
||||
:members:
|
||||
|
||||
CommandHook
|
||||
-----------
|
||||
|
||||
.. autoclass:: cliff.hooks.CommandHook
|
||||
:members:
|
||||
|
||||
ShowOne
|
||||
-------
|
||||
|
||||
|
|
|
@ -243,7 +243,6 @@ output formatter and printing the data to the console.
|
|||
| Modified Time | 1335569964.0 |
|
||||
+---------------+--------------+
|
||||
|
||||
|
||||
setup.py
|
||||
--------
|
||||
|
||||
|
@ -260,3 +259,40 @@ command namespace so that it only loads the command plugins that it
|
|||
should be managing.
|
||||
|
||||
.. _distribute: http://packages.python.org/distribute/
|
||||
|
||||
Command Extension Hooks
|
||||
=======================
|
||||
|
||||
Individual subcommands of an application can be extended via hooks
|
||||
registered as separate plugins. In the demo application, the
|
||||
``hooked`` command has a single extension registered.
|
||||
|
||||
The namespace for hooks is a combination of the application namespace
|
||||
and the command name. In this case, the application namespace is
|
||||
``cliff.demo`` and the command is ``hooked``, so the extension
|
||||
namespace is ``cliff.demo.hooked``. If the subcommand name includes
|
||||
spaces, they are replaced with underscores ("``_``") to build the
|
||||
namespace.
|
||||
|
||||
.. literalinclude:: ../../../demoapp/cliffdemo/hook.py
|
||||
:linenos:
|
||||
|
||||
Although the ``hooked`` command does not add any arguments to the
|
||||
parser it creates, the help output shows that the extension adds a
|
||||
single ``--added-by-hook`` option.
|
||||
|
||||
::
|
||||
|
||||
(.venv)$ cliffdemo hooked -h
|
||||
sample hook get_parser()
|
||||
usage: cliffdemo hooked [-h] [--added-by-hook ADDED_BY_HOOK]
|
||||
|
||||
A command to demonstrate how the hooks work
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--added-by-hook ADDED_BY_HOOK
|
||||
|
||||
.. seealso::
|
||||
|
||||
:class:`cliff.hooks.CommandHook` -- The API for command hooks.
|
||||
|
|
|
@ -19,7 +19,7 @@ fit.
|
|||
Cliff Objects
|
||||
=============
|
||||
|
||||
Cliff is organized around four objects that are combined to create a
|
||||
Cliff is organized around five objects that are combined to create a
|
||||
useful command line program.
|
||||
|
||||
The Application
|
||||
|
@ -50,6 +50,18 @@ responsible for taking action based on instructions from the user. It
|
|||
defines its own local argument parser (usually using argparse_) and a
|
||||
:func:`take_action` method that does the appropriate work.
|
||||
|
||||
The CommandHook
|
||||
---------------
|
||||
|
||||
The :class:`cliff.hooks.CommandHook` class can extend a Command by
|
||||
modifying the command line arguments available, for example to add
|
||||
options used by a driver. Each CommandHook subclass must implement the
|
||||
full hook API, defined by the base class. Extensions should be
|
||||
registered using an entry point namespace based on the application
|
||||
namespace and the command name::
|
||||
|
||||
application_namespace + '.' + command_name.replace(' ', '_')
|
||||
|
||||
The Interactive Application
|
||||
---------------------------
|
||||
|
||||
|
|
Loading…
Reference in New Issue