diff --git a/README.md b/README.md index 76fba594..1d8a2a54 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ app.run(host="0.0.0.0", port=8000) * [Routing](docs/routing.md) * [Middleware](docs/middleware.md) * [Exceptions](docs/exceptions.md) + * [Blueprints](docs/blueprints.md) * [Contributing](docs/contributing.md) * [License](LICENSE) @@ -50,7 +51,6 @@ app.run(host="0.0.0.0", port=8000) * File output * Examples of integrations with 3rd-party modules * RESTful router - * Blueprints? ## Limitations: * No wheels for uvloop and httptools on Windows :( diff --git a/docs/blueprints.md b/docs/blueprints.md new file mode 100644 index 00000000..1880ae88 --- /dev/null +++ b/docs/blueprints.md @@ -0,0 +1,82 @@ +# Blueprints + +Blueprints are objects that can be used for sub-routing within an application. +Instead of adding routes to the application object, blueprints define similar +methods for adding routes, which are then registered with the application in a +flexible and plugable manner. + +## Why? + +Blueprints are especially useful for larger applications, where your application +logic can be broken down into several groups or areas of responsibility. + +It is also useful for API versioning, where one blueprint may point at +`/v1/`, and another pointing at `/v2/`. + + +## My First Blueprint + +The following shows a very simple blueprint that registers a handler-function at +the root `/` of your application. + +Suppose you save this file as `my_blueprint.py`, this can be imported in your +main application later. + +```python +from sanic.response import json +from sanic import Blueprint + +bp = Blueprint('my_blueprint') + +@bp.route('/') +async def bp_root(): + return json({'my': 'blueprint'}) + +``` + +## Registering Blueprints +Blueprints must be registered with the application. + +```python +from sanic import Sanic +from my_blueprint import bp + +app = Sanic(__name__) +app.register_blueprint(bp) + +app.run(host='0.0.0.0', port=8000, debug=True) +``` + +This will add the blueprint to the application and register any routes defined +by that blueprint. +In this example, the registered routes in the `app.router` will look like: + +```python +[Route(handler=, methods=None, pattern=re.compile('^/$'), parameters=[])] +``` + +## Middleware +Blueprints must be registered with the application. + +```python +@bp.middleware +async def halt_request(request): + print("I am a spy") + +@bp.middleware('request') +async def halt_request(request): + return text('I halted the request') + +@bp.middleware('response') +async def halt_response(request, response): + return text('I halted the response') +``` + +## Exceptions +Blueprints must be registered with the application. + +```python +@bp.exception(NotFound) +def ignore_404s(request, exception): + return text("Yep, I totally found the page: {}".format(request.url)) +``` \ No newline at end of file diff --git a/examples/blueprints.py b/examples/blueprints.py new file mode 100644 index 00000000..f612185c --- /dev/null +++ b/examples/blueprints.py @@ -0,0 +1,24 @@ +from sanic import Sanic +from sanic import Blueprint +from sanic.response import json, text + + +app = Sanic(__name__) +blueprint = Blueprint('name', url_prefix='/my_blueprint') +blueprint2 = Blueprint('name', url_prefix='/my_blueprint2') + + +@blueprint.route('/foo') +async def foo(request): + return json({'msg': 'hi from blueprint'}) + + +@blueprint2.route('/foo') +async def foo2(request): + return json({'msg': 'hi from blueprint2'}) + + +app.register_blueprint(blueprint) +app.register_blueprint(blueprint2) + +app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/sanic/__init__.py b/sanic/__init__.py index 52dc810a..5b893df4 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1 +1,2 @@ from .sanic import Sanic +from .blueprints import Blueprint diff --git a/sanic/blueprints.py b/sanic/blueprints.py new file mode 100644 index 00000000..506c2e90 --- /dev/null +++ b/sanic/blueprints.py @@ -0,0 +1,99 @@ +class BlueprintSetup: + """ + """ + + def __init__(self, blueprint, app, options): + self.app = app + self.blueprint = blueprint + self.options = options + + url_prefix = self.options.get('url_prefix') + if url_prefix is None: + url_prefix = self.blueprint.url_prefix + + #: The prefix that should be used for all URLs defined on the + #: blueprint. + self.url_prefix = url_prefix + + def add_route(self, handler, uri, methods): + """ + A helper method to register a handler to the application url routes. + """ + if self.url_prefix: + uri = self.url_prefix + uri + + self.app.router.add(uri, methods, handler) + + def add_exception(self, handler, *args, **kwargs): + """ + Registers exceptions to sanic + """ + self.app.exception(*args, **kwargs)(handler) + + def add_middleware(self, middleware, *args, **kwargs): + """ + Registers middleware to sanic + """ + if args or kwargs: + self.app.middleware(*args, **kwargs)(middleware) + else: + self.app.middleware(middleware) + + +class Blueprint: + def __init__(self, name, url_prefix=None): + self.name = name + self.url_prefix = url_prefix + self.deferred_functions = [] + + def record(self, func): + """ + Registers a callback function that is invoked when the blueprint is + registered on the application. + """ + self.deferred_functions.append(func) + + + def make_setup_state(self, app, options): + """ + """ + return BlueprintSetup(self, app, options) + + def register(self, app, options): + """ + """ + state = self.make_setup_state(app, options) + for deferred in self.deferred_functions: + deferred(state) + + def route(self, uri, methods=None): + """ + """ + def decorator(handler): + self.record(lambda s: s.add_route(handler, uri, methods)) + return handler + return decorator + + def middleware(self, *args, **kwargs): + """ + """ + + def register_middleware(middleware): + self.record(lambda s: s.add_middleware(middleware, *args, **kwargs)) + return middleware + + # Detect which way this was called, @middleware or @middleware('AT') + if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): + args = [] + return register_middleware(args[0]) + else: + return register_middleware + + def exception(self, *args, **kwargs): + """ + """ + def decorator(handler): + self.record(lambda s: s.add_exception(handler, *args, **kwargs)) + return handler + return decorator + diff --git a/sanic/sanic.py b/sanic/sanic.py index f7ed0dc0..45b00aeb 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -21,6 +21,8 @@ class Sanic: self.config = Config() self.request_middleware = [] self.response_middleware = [] + self.blueprints = {} + self._blueprint_order = [] # -------------------------------------------------------------------- # # Registration @@ -85,6 +87,23 @@ class Sanic: return middleware + def register_blueprint(self, blueprint, **options): + """ + Registers a blueprint on the application. + :param blueprint: Blueprint object + :param options: option dictionary with blueprint defaults + :return: Nothing + """ + if blueprint.name in self.blueprints: + assert self.blueprints[blueprint.name] is blueprint, \ + 'A blueprint with the name "%s" is already registered. ' \ + 'Blueprint names must be unique.' % \ + (blueprint.name,) + else: + self.blueprints[blueprint.name] = blueprint + self._blueprint_order.append(blueprint) + blueprint.register(self, options) + # -------------------------------------------------------------------- # # Request Handling # -------------------------------------------------------------------- # diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py new file mode 100644 index 00000000..8068160f --- /dev/null +++ b/tests/test_blueprints.py @@ -0,0 +1,111 @@ +from sanic import Sanic +from sanic.blueprints import Blueprint +from sanic.response import json, text +from sanic.utils import sanic_endpoint_test +from sanic.exceptions import NotFound, ServerError, InvalidUsage + + +# ------------------------------------------------------------ # +# GET +# ------------------------------------------------------------ # + +def test_bp(): + app = Sanic('test_text') + bp = Blueprint('test_text') + + @bp.route('/') + def handler(request): + return text('Hello') + + app.register_blueprint(bp) + request, response = sanic_endpoint_test(app) + + assert response.text == 'Hello' + +def test_bp_with_url_prefix(): + app = Sanic('test_text') + bp = Blueprint('test_text', url_prefix='/test1') + + @bp.route('/') + def handler(request): + return text('Hello') + + app.register_blueprint(bp) + request, response = sanic_endpoint_test(app, uri='/test1/') + + assert response.text == 'Hello' + + +def test_several_bp_with_url_prefix(): + app = Sanic('test_text') + bp = Blueprint('test_text', url_prefix='/test1') + bp2 = Blueprint('test_text2', url_prefix='/test2') + + @bp.route('/') + def handler(request): + return text('Hello') + + @bp2.route('/') + def handler2(request): + return text('Hello2') + + app.register_blueprint(bp) + app.register_blueprint(bp2) + request, response = sanic_endpoint_test(app, uri='/test1/') + assert response.text == 'Hello' + + request, response = sanic_endpoint_test(app, uri='/test2/') + assert response.text == 'Hello2' + + +def test_bp_middleware(): + app = Sanic('test_middleware') + blueprint = Blueprint('test_middleware') + + @blueprint.middleware('response') + async def process_response(request, response): + return text('OK') + + @app.route('/') + async def handler(request): + return text('FAIL') + + app.register_blueprint(blueprint) + + request, response = sanic_endpoint_test(app) + + assert response.status == 200 + assert response.text == 'OK' + +def test_bp_exception_handler(): + app = Sanic('test_middleware') + blueprint = Blueprint('test_middleware') + + @blueprint.route('/1') + def handler_1(request): + raise InvalidUsage("OK") + + @blueprint.route('/2') + def handler_2(request): + raise ServerError("OK") + + @blueprint.route('/3') + def handler_3(request): + raise NotFound("OK") + + @blueprint.exception(NotFound, ServerError) + def handler_exception(request, exception): + return text("OK") + + app.register_blueprint(blueprint) + + request, response = sanic_endpoint_test(app, uri='/1') + assert response.status == 400 + + + request, response = sanic_endpoint_test(app, uri='/2') + assert response.status == 200 + assert response.text == 'OK' + + request, response = sanic_endpoint_test(app, uri='/3') + assert response.status == 200 \ No newline at end of file