From a10d7469cdeeaa4bdd94d508520e84255bdddb0b Mon Sep 17 00:00:00 2001 From: Eli Date: Thu, 18 Jan 2018 17:20:51 -0800 Subject: [PATCH] Add blueprint groups + nesting --- docs/sanic/blueprints.md | 67 ++++++++++++++++++++++++++++++++++++++++ sanic/app.py | 6 +++- sanic/blueprints.py | 22 ++++++++++++- tests/test_blueprints.py | 41 ++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md index 1a7c5293..53aef5fd 100644 --- a/docs/sanic/blueprints.md +++ b/docs/sanic/blueprints.md @@ -51,6 +51,73 @@ will look like: [Route(handler=, methods=None, pattern=re.compile('^/$'), parameters=[])] ``` +## Blueprint groups and nesting + +Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example: + +``` +api/ +├──content/ +│ ├──authors.py +│ ├──static.py +│ └──__init__.py +├──info.py +└──__init__.py +app.py +``` + +Initialization of this app's blueprint hierarchy could go as follows: + +```python +# api/content/authors.py +from sanic import Blueprint + +authors = Blueprint('content_authors', url_prefix='/authors') +``` +```python +# api/content/static.py +from sanic import Blueprint + +static = Blueprint('content_static', url_prefix='/static') +``` +```python +# api/content/__init__.py +from sanic import Blueprint + +from .static import static +from .authors import authors + +content = Blueprint.group(assets, authors, url_prefix='/content') +``` +```python +# api/info.py +from sanic import Blueprint + +info = Blueprint('info', url_prefix='/info') +``` +```python +# api/__init__.py +from sanic import Blueprint + +from .content import content +from .info import info + +api = Blueprint.group(content, info, url_prefix='/api') +``` + +And registering these blueprints in `app.py` can now be done like so: + +```python +# app.py +from sanic import Sanic + +from .api import api + +app = Sanic(__name__) + +app.blueprint(api) +``` + ## Using blueprints Blueprints have much the same functionality as an application instance. diff --git a/sanic/app.py b/sanic/app.py index 26ef83b0..92a4dfb2 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -372,10 +372,14 @@ class Sanic: def blueprint(self, blueprint, **options): """Register a blueprint on the application. - :param blueprint: Blueprint object + :param blueprint: Blueprint object or (list, tuple) thereof :param options: option dictionary with blueprint defaults :return: Nothing """ + if isinstance(blueprint, (list, tuple)): + for item in blueprint: + self.blueprint(item, **options) + return if blueprint.name in self.blueprints: assert self.blueprints[blueprint.name] is blueprint, \ 'A blueprint with the name "%s" is already registered. ' \ diff --git a/sanic/blueprints.py b/sanic/blueprints.py index f9159168..084013e1 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -14,7 +14,6 @@ FutureStatic = namedtuple('Route', class Blueprint: - def __init__(self, name, url_prefix=None, host=None, version=None, @@ -38,6 +37,27 @@ class Blueprint: self.version = version self.strict_slashes = strict_slashes + @staticmethod + def group(*blueprints, url_prefix=''): + """Create a list of blueprints, optionally + grouping them under a general URL prefix. + + :param blueprints: blueprints to be registered as a group + :param url_prefix: URL route to be prepended to all sub-prefixes + """ + def chain(nested): + """itertools.chain() but leaves strings untouched""" + for i in nested: + if isinstance(i, (list, tuple)): + yield from chain(i) + else: + yield i + bps = [] + for bp in chain(blueprints): + bp.url_prefix = url_prefix + bp.url_prefix + bps.append(bp) + return bps + def register(self, app, options): """Register the blueprint to the sanic app.""" diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 7e713da6..4c321646 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -446,3 +446,44 @@ def test_bp_shorthand(): 'Sec-WebSocket-Version': '13'}) assert response.status == 101 assert ev.is_set() + +def test_bp_group(): + app = Sanic('test_nested_bp_groups') + + deep_0 = Blueprint('deep_0', url_prefix='/deep') + deep_1 = Blueprint('deep_1', url_prefix = '/deep1') + + @deep_0.route('/') + def handler(request): + return text('D0_OK') + + @deep_1.route('/bottom') + def handler(request): + return text('D1B_OK') + + mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid') + mid_1 = Blueprint('mid_tier', url_prefix='/mid1') + + @mid_1.route('/') + def handler(request): + return text('M1_OK') + + top = Blueprint.group(mid_0, mid_1) + + app.blueprint(top) + + @app.route('/') + def handler(request): + return text('TOP_OK') + + request, response = app.test_client.get('/') + assert response.text == 'TOP_OK' + + request, response = app.test_client.get('/mid1') + assert response.text == 'M1_OK' + + request, response = app.test_client.get('/mid/deep') + assert response.text == 'D0_OK' + + request, response = app.test_client.get('/mid/deep1/bottom') + assert response.text == 'D1B_OK'