Add Blueprint level cli command registration

Implements #1357.
Adds ability to register click cli commands onto blueprint.
This commit is contained in:
Anthony Plunkett 2018-05-14 22:05:54 -04:00 committed by David Lord
parent 855d59b68b
commit ec1ccd7530
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
6 changed files with 136 additions and 8 deletions

View File

@ -56,6 +56,9 @@ Unreleased
returning a string will produce a ``text/html`` response, returning
a dict will call ``jsonify`` to produce a ``application/json``
response. :pr:`3111`
- Blueprints have a ``cli`` Click group like ``app.cli``. CLI commands
registered with a blueprint will be available as a group under the
``flask`` command. :issue:`1357`.
.. _#2935: https://github.com/pallets/flask/issues/2935
.. _#2957: https://github.com/pallets/flask/issues/2957

View File

@ -310,10 +310,66 @@ group. This is useful if you want to organize multiple related commands. ::
$ flask user create demo
See :ref:`testing-cli` for an overview of how to test your custom
commands.
Registering Commands with Blueprints
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If your application uses blueprints, you can optionally register CLI
commands directly onto them. When your blueprint is registered onto your
application, the associated commands will be available to the ``flask``
command. By default, those commands will be nested in a group matching
the name of the blueprint.
.. code-block:: python
from flask import Blueprint
bp = Blueprint('students', __name__)
@bp.cli.command('create')
@click.argument('name')
def create(name):
...
app.register_blueprint(bp)
.. code-block:: text
$ flask students create alice
You can alter the group name by specifying the ``cli_group`` parameter
when creating the :class:`Blueprint` object, or later with
:meth:`app.register_blueprint(bp, cli_group='...') <Flask.register_blueprint>`.
The following are equivalent:
.. code-block:: python
bp = Blueprint('students', __name__, cli_group='other')
# or
app.register_blueprint(bp, cli_group='other')
.. code-block:: text
$ flask other create alice
Specifying ``cli_group=None`` will remove the nesting and merge the
commands directly to the application's level:
.. code-block:: python
bp = Blueprint('students', __name__, cli_group=None)
# or
app.register_blueprint(bp, cli_group=None)
.. code-block:: text
$ flask create alice
Application Context
~~~~~~~~~~~~~~~~~~~

View File

@ -600,13 +600,9 @@ class Flask(_PackageBoundObject):
view_func=self.send_static_file,
)
#: The click command line context for this application. Commands
#: registered here show up in the :command:`flask` command once the
#: application has been discovered. The default commands are
#: provided by Flask itself and can be overridden.
#:
#: This is an instance of a :class:`click.Group` object.
self.cli = cli.AppGroup(self.name)
# Set the name of the Click group in case someone wants to add
# the app's commands to another CLI tool.
self.cli.name = self.name
@locked_cached_property
def name(self):

View File

@ -13,6 +13,9 @@ from functools import update_wrapper
from .helpers import _PackageBoundObject, _endpoint_from_view_func
# a singleton sentinel value for parameter defaults
_sentinel = object()
class BlueprintSetupState(object):
"""Temporary holder object for registering a blueprint with the
@ -90,6 +93,11 @@ class Blueprint(_PackageBoundObject):
or other things on the main application. See :ref:`blueprints` for more
information.
.. versionchanged:: 1.1.0
Blueprints have a ``cli`` group to register nested CLI commands.
The ``cli_group`` parameter controls the name of the group under
the ``flask`` command.
.. versionadded:: 0.7
"""
@ -129,6 +137,7 @@ class Blueprint(_PackageBoundObject):
subdomain=None,
url_defaults=None,
root_path=None,
cli_group=_sentinel,
):
_PackageBoundObject.__init__(
self, import_name, template_folder, root_path=root_path
@ -142,6 +151,7 @@ class Blueprint(_PackageBoundObject):
if url_defaults is None:
url_defaults = {}
self.url_values_defaults = url_defaults
self.cli_group = cli_group
def record(self, func):
"""Registers a function that is called when the blueprint is
@ -206,6 +216,17 @@ class Blueprint(_PackageBoundObject):
for deferred in self.deferred_functions:
deferred(state)
cli_resolved_group = options.get("cli_group", self.cli_group)
if cli_resolved_group is None:
app.cli.commands.update(self.cli.commands)
elif cli_resolved_group is _sentinel:
self.cli.name = self.name
app.cli.add_command(self.cli)
else:
self.cli.name = cli_resolved_group
app.cli.add_command(self.cli)
def route(self, rule, **options):
"""Like :meth:`Flask.route` but for a blueprint. The endpoint for the
:func:`url_for` function is prefixed with the name of the blueprint.

View File

@ -942,6 +942,15 @@ class _PackageBoundObject(object):
self._static_folder = None
self._static_url_path = None
# circular import
from .cli import AppGroup
#: The Click command group for registration of CLI commands
#: on the application and associated blueprints. These commands
#: are accessible via the :command:`flask` command once the
#: application has been discovered and blueprints registered.
self.cli = AppGroup()
def _get_static_folder(self):
if self._static_folder is not None:
return os.path.join(self.root_path, self._static_folder)

View File

@ -23,7 +23,7 @@ import pytest
from _pytest.monkeypatch import notset
from click.testing import CliRunner
from flask import Flask, current_app
from flask import Flask, current_app, Blueprint
from flask.cli import (
AppGroup,
FlaskGroup,
@ -609,3 +609,46 @@ def test_run_cert_import(monkeypatch):
# no --key with SSLContext
with pytest.raises(click.BadParameter):
run_command.make_context("run", ["--cert", "ssl_context", "--key", __file__])
def test_cli_blueprints(app):
"""Test blueprint commands register correctly to the application"""
custom = Blueprint("custom", __name__, cli_group="customized")
nested = Blueprint("nested", __name__)
merged = Blueprint("merged", __name__, cli_group=None)
late = Blueprint("late", __name__)
@custom.cli.command("custom")
def custom_command():
click.echo("custom_result")
@nested.cli.command("nested")
def nested_command():
click.echo("nested_result")
@merged.cli.command("merged")
def merged_command():
click.echo("merged_result")
@late.cli.command("late")
def late_command():
click.echo("late_result")
app.register_blueprint(custom)
app.register_blueprint(nested)
app.register_blueprint(merged)
app.register_blueprint(late, cli_group="late_registration")
app_runner = app.test_cli_runner()
result = app_runner.invoke(args=["customized", "custom"])
assert "custom_result" in result.output
result = app_runner.invoke(args=["nested", "nested"])
assert "nested_result" in result.output
result = app_runner.invoke(args=["merged"])
assert "merged_result" in result.output
result = app_runner.invoke(args=["late_registration", "late"])
assert "late_result" in result.output