From 3a64a2533164a438908dc5066d2f1ddaa2c3e2b2 Mon Sep 17 00:00:00 2001 From: Harsha Narayana Date: Tue, 6 Nov 2018 15:13:49 +0530 Subject: [PATCH] enable blueprint group middleware support This commit will enable the users to implement a middleware at the blueprint group level whereby enforcing the middleware automatically to each of the available Blueprints that are part of the group. This will eanble a simple way in which a certain set of common features and criteria can be enforced on a Blueprint group. i.e. authentication and authorization This commit will address the feature request raised as part of Issue #1386 Signed-off-by: Harsha Narayana --- sanic/app.py | 9 ++-- sanic/blueprint_grpoup.py | 79 +++++++++++++++++++++++++++++++++++ sanic/blueprints.py | 12 +++++- setup.py | 1 + tests/test_blueprint_group.py | 63 ++++++++++++++++++++++++++++ tests/test_blueprints.py | 2 +- 6 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 sanic/blueprint_grpoup.py create mode 100644 tests/test_blueprint_group.py diff --git a/sanic/app.py b/sanic/app.py index c2a24464a1..6735d5f0a8 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -13,6 +13,7 @@ from urllib.parse import urlencode, urlunparse from sanic import reloader_helpers +from sanic.blueprint_grpoup import BlueprintGroup from sanic.config import Config from sanic.constants import HTTP_METHODS from sanic.exceptions import SanicException, ServerError, URLBuildError @@ -482,9 +483,11 @@ def response(handler): def register_middleware(self, middleware, attach_to="request"): if attach_to == "request": - self.request_middleware.append(middleware) + if middleware not in self.request_middleware: + self.request_middleware.append(middleware) if attach_to == "response": - self.response_middleware.appendleft(middleware) + if middleware not in self.response_middleware: + self.response_middleware.appendleft(middleware) return middleware # Decorator @@ -540,7 +543,7 @@ def blueprint(self, blueprint, **options): :param options: option dictionary with blueprint defaults :return: Nothing """ - if isinstance(blueprint, (list, tuple)): + if isinstance(blueprint, BlueprintGroup): for item in blueprint: self.blueprint(item, **options) return diff --git a/sanic/blueprint_grpoup.py b/sanic/blueprint_grpoup.py new file mode 100644 index 0000000000..3e85dfdf54 --- /dev/null +++ b/sanic/blueprint_grpoup.py @@ -0,0 +1,79 @@ +class BlueprintGroup(object): + """ + This class provides a mechanism to implement a Blueprint Group + using the `Blueprint.group` method. To avoid having to re-write + some of the existing implementation, this class provides a custom + iterator implementation that will let you use the object of this + class as a list/tuple inside the existing implementation. + """ + + def __init__(self, url_prefix=None): + """ + Create a new Blueprint Group + + :param url_prefix: URL: to be prefixed before all the Blueprint Prefix + """ + self._blueprints = list() + self._iter_position = 0 + self._url_prefix = url_prefix + + @property + def url_prefix(self): + """ + Retrieve the URL prefix being used for the Current Blueprint Group + :return: string with url prefix + """ + return self._url_prefix + + @property + def blueprints(self): + """ + Retrieve a list of all the available blueprints under this group. + :return: List of Blueprint instance + """ + return self._blueprints + + @blueprints.setter + def blueprints(self, blueprint): + """ + Add a new Blueprint to the Group under consideration. + :param blueprint: Instance of Blueprint + :return: None + """ + self._blueprints.append(blueprint) + + def __iter__(self): + """Tun the class Blueprint Group into an Iterable item""" + return self + + def __next__(self): + """ + A Custom method to iterate over the Blueprint Objects in the + group under consideration + """ + if not len(self._blueprints) or self._iter_position >= len( + self._blueprints + ): + raise StopIteration + else: + self._iter_position += 1 + return self._blueprints[self._iter_position - 1] + + def middleware(self, *args, **kwargs): + """ + A decorator that can be used to implement a Middleware plugin to + all of the Blueprints that belongs to this specific Blueprint Group. + + In case of nested Blueprint Groups, the same middleware is applied + across each of the Blueprints recursively. + + :param args: Optional positional Parameters to be use middleware + :param kwargs: Optional Keyword arg to use with Middleware + :return: Partial function to apply the middleware + """ + + def register_middleware_for_blueprints(fn): + for blueprint in self.blueprints: + blueprint.middleware(fn, *args, **kwargs) + + return register_middleware_for_blueprints diff --git a/sanic/blueprints.py b/sanic/blueprints.py index f7f6cfb76a..b330e58d80 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -1,5 +1,6 @@ from collections import defaultdict, namedtuple +from sanic.blueprint_grpoup import BlueprintGroup from sanic.constants import HTTP_METHODS from sanic.views import CompositionView @@ -71,15 +72,17 @@ def chain(nested): for i in nested: if isinstance(i, (list, tuple)): yield from chain(i) + elif isinstance(i, (BlueprintGroup)): + yield from i.blueprints else: yield i - bps = [] + bps = BlueprintGroup(url_prefix=url_prefix) for bp in chain(blueprints): if bp.url_prefix is None: bp.url_prefix = "" bp.url_prefix = url_prefix + bp.url_prefix - bps.append(bp) + bps.blueprints = bp return bps def register(self, app, options): @@ -284,6 +287,11 @@ def register_middleware(_middleware): middleware = args[0] args = [] return register_middleware(middleware) + elif len(args) == 2 and len(kwargs) == 0 and callable(args[0]): + # This will be used in case of Blueprint Group + middleware = args[0] + args = args[1:] + return register_middleware(middleware) else: return register_middleware diff --git a/setup.py b/setup.py index 3716433648..923226be0e 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ import codecs import os import re + from distutils.errors import DistutilsPlatformError from distutils.util import strtobool diff --git a/tests/test_blueprint_group.py b/tests/test_blueprint_group.py new file mode 100644 index 0000000000..0a4fe90a06 --- /dev/null +++ b/tests/test_blueprint_group.py @@ -0,0 +1,63 @@ +from sanic.app import Sanic +from sanic.blueprints import Blueprint +from sanic.response import text + +MIDDLEWARE_INVOKE_COUNTER = { + 'request': 0, + 'response': 0 + } + + +def test_bp_group(app: Sanic): + blueprint_1 = Blueprint('blueprint_1', url_prefix="/bp1") + blueprint_2 = Blueprint('blueprint_2', url_prefix='/bp2') + + @blueprint_1.route('/') + def blueprint_1_default_route(request): + return text("BP1_OK") + + @blueprint_2.route("/") + def blueprint_2_default_route(request): + return text("BP2_OK") + + blueprint_group_1 = Blueprint.group( + blueprint_1, blueprint_2, url_prefix="/bp") + + blueprint_3 = Blueprint('blueprint_3', url_prefix="/bp3") + + @blueprint_group_1.middleware('request') + def blueprint_group_1_middleware(request): + global MIDDLEWARE_INVOKE_COUNTER + MIDDLEWARE_INVOKE_COUNTER['request'] += 1 + + @blueprint_3.route("/") + def blueprint_3_default_route(request): + return text("BP3_OK") + + blueprint_group_2 = Blueprint.group(blueprint_group_1, blueprint_3, url_prefix="/api") + + @blueprint_group_2.middleware('response') + def blueprint_group_2_middleware(request, response): + global MIDDLEWARE_INVOKE_COUNTER + MIDDLEWARE_INVOKE_COUNTER['response'] += 1 + + app.blueprint(blueprint_group_2) + + @app.route("/") + def app_default_route(request): + return text("APP_OK") + + _, response = app.test_client.get("/") + assert response.text == 'APP_OK' + + _, response = app.test_client.get("/api/bp/bp1") + assert response.text == 'BP1_OK' + + _, response = app.test_client.get("/api/bp/bp2") + assert response.text == 'BP2_OK' + + _, response = app.test_client.get('/api/bp3') + assert response.text == 'BP3_OK' + + assert MIDDLEWARE_INVOKE_COUNTER['response'] == 4 + assert MIDDLEWARE_INVOKE_COUNTER['request'] == 4 diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 039a354424..ef36a7014c 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -36,7 +36,7 @@ def handler(request): return text('OK') else: print(func) - raise + raise InvalidUsage("Blueprint is missing a callable method: {}".format(method)) app.blueprint(bp)