Merge pull request #2784 from doobeh/master
Add Blueprint level cli registration-- #1357
This commit is contained in:
commit
b83760675d
6 changed files with 136 additions and 8 deletions
|
|
@ -56,6 +56,9 @@ Unreleased
|
||||||
returning a string will produce a ``text/html`` response, returning
|
returning a string will produce a ``text/html`` response, returning
|
||||||
a dict will call ``jsonify`` to produce a ``application/json``
|
a dict will call ``jsonify`` to produce a ``application/json``
|
||||||
response. :pr:`3111`
|
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
|
.. _#2935: https://github.com/pallets/flask/issues/2935
|
||||||
.. _#2957: https://github.com/pallets/flask/issues/2957
|
.. _#2957: https://github.com/pallets/flask/issues/2957
|
||||||
|
|
|
||||||
56
docs/cli.rst
56
docs/cli.rst
|
|
@ -310,10 +310,66 @@ group. This is useful if you want to organize multiple related commands. ::
|
||||||
|
|
||||||
$ flask user create demo
|
$ flask user create demo
|
||||||
|
|
||||||
|
|
||||||
See :ref:`testing-cli` for an overview of how to test your custom
|
See :ref:`testing-cli` for an overview of how to test your custom
|
||||||
commands.
|
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
|
Application Context
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
||||||
10
flask/app.py
10
flask/app.py
|
|
@ -600,13 +600,9 @@ class Flask(_PackageBoundObject):
|
||||||
view_func=self.send_static_file,
|
view_func=self.send_static_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
#: The click command line context for this application. Commands
|
# Set the name of the Click group in case someone wants to add
|
||||||
#: registered here show up in the :command:`flask` command once the
|
# the app's commands to another CLI tool.
|
||||||
#: application has been discovered. The default commands are
|
self.cli.name = self.name
|
||||||
#: provided by Flask itself and can be overridden.
|
|
||||||
#:
|
|
||||||
#: This is an instance of a :class:`click.Group` object.
|
|
||||||
self.cli = cli.AppGroup(self.name)
|
|
||||||
|
|
||||||
@locked_cached_property
|
@locked_cached_property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ from functools import update_wrapper
|
||||||
|
|
||||||
from .helpers import _PackageBoundObject, _endpoint_from_view_func
|
from .helpers import _PackageBoundObject, _endpoint_from_view_func
|
||||||
|
|
||||||
|
# a singleton sentinel value for parameter defaults
|
||||||
|
_sentinel = object()
|
||||||
|
|
||||||
|
|
||||||
class BlueprintSetupState(object):
|
class BlueprintSetupState(object):
|
||||||
"""Temporary holder object for registering a blueprint with the
|
"""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
|
or other things on the main application. See :ref:`blueprints` for more
|
||||||
information.
|
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
|
.. versionadded:: 0.7
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -129,6 +137,7 @@ class Blueprint(_PackageBoundObject):
|
||||||
subdomain=None,
|
subdomain=None,
|
||||||
url_defaults=None,
|
url_defaults=None,
|
||||||
root_path=None,
|
root_path=None,
|
||||||
|
cli_group=_sentinel,
|
||||||
):
|
):
|
||||||
_PackageBoundObject.__init__(
|
_PackageBoundObject.__init__(
|
||||||
self, import_name, template_folder, root_path=root_path
|
self, import_name, template_folder, root_path=root_path
|
||||||
|
|
@ -142,6 +151,7 @@ class Blueprint(_PackageBoundObject):
|
||||||
if url_defaults is None:
|
if url_defaults is None:
|
||||||
url_defaults = {}
|
url_defaults = {}
|
||||||
self.url_values_defaults = url_defaults
|
self.url_values_defaults = url_defaults
|
||||||
|
self.cli_group = cli_group
|
||||||
|
|
||||||
def record(self, func):
|
def record(self, func):
|
||||||
"""Registers a function that is called when the blueprint is
|
"""Registers a function that is called when the blueprint is
|
||||||
|
|
@ -206,6 +216,17 @@ class Blueprint(_PackageBoundObject):
|
||||||
for deferred in self.deferred_functions:
|
for deferred in self.deferred_functions:
|
||||||
deferred(state)
|
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):
|
def route(self, rule, **options):
|
||||||
"""Like :meth:`Flask.route` but for a blueprint. The endpoint for the
|
"""Like :meth:`Flask.route` but for a blueprint. The endpoint for the
|
||||||
:func:`url_for` function is prefixed with the name of the blueprint.
|
:func:`url_for` function is prefixed with the name of the blueprint.
|
||||||
|
|
|
||||||
|
|
@ -942,6 +942,15 @@ class _PackageBoundObject(object):
|
||||||
self._static_folder = None
|
self._static_folder = None
|
||||||
self._static_url_path = 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):
|
def _get_static_folder(self):
|
||||||
if self._static_folder is not None:
|
if self._static_folder is not None:
|
||||||
return os.path.join(self.root_path, self._static_folder)
|
return os.path.join(self.root_path, self._static_folder)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import pytest
|
||||||
from _pytest.monkeypatch import notset
|
from _pytest.monkeypatch import notset
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from flask import Flask, current_app
|
from flask import Flask, current_app, Blueprint
|
||||||
from flask.cli import (
|
from flask.cli import (
|
||||||
AppGroup,
|
AppGroup,
|
||||||
FlaskGroup,
|
FlaskGroup,
|
||||||
|
|
@ -609,3 +609,46 @@ def test_run_cert_import(monkeypatch):
|
||||||
# no --key with SSLContext
|
# no --key with SSLContext
|
||||||
with pytest.raises(click.BadParameter):
|
with pytest.raises(click.BadParameter):
|
||||||
run_command.make_context("run", ["--cert", "ssl_context", "--key", __file__])
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue