Skip to content

Commit

Permalink
Enable Middleware Support for Blueprint Groups (#1399)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* enable indexing of BlueprintGroup object

Signed-off-by: Harsha Narayana <[email protected]>

* rename blueprint group file to fix spelling error

Signed-off-by: Harsha Narayana <[email protected]>

* add documentation and additional unit tests

Signed-off-by: Harsha Narayana <[email protected]>

* cleanup and optimize headers in unit test file

Signed-off-by: Harsha Narayana <[email protected]>

* fix Bluprint Group iteratable method

Signed-off-by: Harsha Narayana <[email protected]>

* add additional unit test to check StopIteration condition

Signed-off-by: Harsha Narayana <[email protected]>

* cleanup iter protocol implemenation for blueprint group and add slots

Signed-off-by: Harsha Narayana <[email protected]>

* fix blueprint group middleware invocation identification

Signed-off-by: Harsha Narayana <[email protected]>

* feat: enable list behavior on blueprint group object and use append instead of properly to add blueprint to group

Signed-off-by: Harsha Narayana <[email protected]>
  • Loading branch information
harshanarayana authored and sjsadowski committed Mar 3, 2019
1 parent e5c7589 commit 348964f
Show file tree
Hide file tree
Showing 6 changed files with 357 additions and 6 deletions.
9 changes: 9 additions & 0 deletions docs/sanic/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ sanic.blueprints module
:undoc-members:
:show-inheritance:

sanic.blueprint_group module
----------------------------

.. automodule:: sanic.blueprint_group
:members:
:undoc-members:
:show-inheritance:


sanic.config module
-------------------

Expand Down
32 changes: 31 additions & 1 deletion docs/sanic/blueprints.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Blueprints have almost the same functionality as an application instance.
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
decorator or `bp.add_websocket_route` method.

### Middleware
### Blueprint Middleware

Using blueprints allows you to also register middleware globally.

Expand All @@ -145,6 +145,36 @@ async def halt_response(request, response):
return text('I halted the response')
```

### Blueprint Group Middleware
Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the
current blueprint group under consideration.

```python
bp1 = Blueprint('bp1', url_prefix='/bp1')
bp2 = Blueprint('bp2', url_prefix='/bp2')

@bp1.middleware('request')
async def bp1_only_middleware(request):
print('applied on Blueprint : bp1 Only')

@bp1.route('/')
async def bp1_route(request):
return text('bp1')

@bp2.route('/<param>')
async def bp2_route(request, param):
return text(param)

group = Blueprint.group(bp1, bp2)

@group.middleware('request')
async def group_middleware(request):
print('common middleware applied for both bp1 and bp2')

# Register Blueprint group under the app
app.blueprint(group)
```

### Exceptions

Exceptions can be applied exclusively to blueprints globally.
Expand Down
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_group 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
120 changes: 120 additions & 0 deletions sanic/blueprint_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from collections import MutableSequence


class BlueprintGroup(MutableSequence):
"""
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.
"""

__slots__ = ("_blueprints", "_url_prefix")

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 = []
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

def __iter__(self):
"""Tun the class Blueprint Group into an Iterable item"""
return iter(self._blueprints)

def __getitem__(self, item):
"""
This method returns a blueprint inside the group specified by
an index value. This will enable indexing, splice and slicing
of the blueprint group like we can do with regular list/tuple.
This method is provided to ensure backward compatibility with
any of the pre-existing usage that might break.
:param item: Index of the Blueprint item in the group
:return: Blueprint object
"""
return self._blueprints[item]

def __setitem__(self, index: int, item: object) -> None:
"""
Abstract method implemented to turn the `BlueprintGroup` class
into a list like object to support all the existing behavior.
This method is used to perform the list's indexed setter operation.
:param index: Index to use for inserting a new Blueprint item
:param item: New `Blueprint` object.
:return: None
"""
self._blueprints[index] = item

def __delitem__(self, index: int) -> None:
"""
Abstract method implemented to turn the `BlueprintGroup` class
into a list like object to support all the existing behavior.
This method is used to delete an item from the list of blueprint
groups like it can be done on a regular list with index.
:param index: Index to use for removing a new Blueprint item
:return: None
"""
del self._blueprints[index]

def __len__(self) -> int:
"""
Get the Length of the blueprint group object.
:return: Length of Blueprint group object
"""
return len(self._blueprints)

def insert(self, index: int, item: object) -> None:
"""
The Abstract class `MutableSequence` leverages this insert method to
perform the `BlueprintGroup.append` operation.
:param index: Index to use for removing a new Blueprint item
:param item: New `Blueprint` object.
:return: None
"""
self._blueprints.insert(index, item)

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
"""
kwargs["bp_group"] = True

def register_middleware_for_blueprints(fn):
for blueprint in self.blueprints:
blueprint.middleware(fn, *args, **kwargs)

return register_middleware_for_blueprints
13 changes: 11 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_group import BlueprintGroup
from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView

Expand Down Expand Up @@ -78,10 +79,12 @@ 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 = ""
Expand Down Expand Up @@ -327,7 +330,13 @@ def register_middleware(_middleware):
args = []
return register_middleware(middleware)
else:
return register_middleware
if kwargs.get("bp_group") and callable(args[0]):
middleware = args[0]
args = args[1:]
kwargs.pop("bp_group")
return register_middleware(middleware)
else:
return register_middleware

def exception(self, *args, **kwargs):
"""
Expand Down
Loading

0 comments on commit 348964f

Please sign in to comment.