Skip to content

Commit

Permalink
enable blueprint group middleware support
Browse files Browse the repository at this point in the history
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 sanic-org#1386

Signed-off-by: Harsha Narayana <[email protected]>
  • Loading branch information
harshanarayana committed Feb 23, 2019
1 parent 52deeba commit e0912ef
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 5 deletions.
9 changes: 6 additions & 3 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from urllib.parse import urlencode, urlunparse

from sanic import reloader_helpers
from sanic.blueprint_grpoup import BlueprintGroup
from sanic.config import BASE_LOGO, Config
from sanic.constants import HTTP_METHODS
from sanic.exceptions import SanicException, ServerError, URLBuildError
Expand Down Expand Up @@ -597,9 +598,11 @@ def register_middleware(self, middleware, attach_to="request"):
:return: decorated method
"""
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
Expand Down Expand Up @@ -681,7 +684,7 @@ def blueprint(self, blueprint, **options):
:param options: option dictionary with blueprint defaults
:return: Nothing
"""
if isinstance(blueprint, (list, tuple)):
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
for item in blueprint:
self.blueprint(item, **options)
return
Expand Down
79 changes: 79 additions & 0 deletions sanic/blueprint_grpoup.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 10 additions & 2 deletions sanic/blueprints.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -78,15 +79,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):
Expand Down Expand Up @@ -326,6 +329,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

Expand Down
63 changes: 63 additions & 0 deletions tests/test_blueprint_group.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit e0912ef

Please sign in to comment.