Merge branch 'master' into sphinx-docs
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,13 +1,15 @@ | |||||||
| *~ | *~ | ||||||
| *.egg-info | *.egg-info | ||||||
| *.egg | *.egg | ||||||
|  | *.eggs | ||||||
|  | *.pyc | ||||||
| .coverage | .coverage | ||||||
| .coverage.* | .coverage.* | ||||||
| coverage | coverage | ||||||
| .tox | .tox | ||||||
| settings.py | settings.py | ||||||
| *.pyc |  | ||||||
| .idea/* | .idea/* | ||||||
| .cache/* | .cache/* | ||||||
|  | .python-version | ||||||
| docs/_build/ | docs/_build/ | ||||||
| docs/_api/ | docs/_api/ | ||||||
							
								
								
									
										11
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,15 +1,10 @@ | |||||||
|  | sudo: false | ||||||
| language: python | language: python | ||||||
| python: | python: | ||||||
|   - '3.5' |   - '3.5' | ||||||
|   - '3.6' |   - '3.6' | ||||||
| install: | install: pip install tox-travis | ||||||
|   - pip install -r requirements.txt | script: tox | ||||||
|   - pip install -r requirements-dev.txt |  | ||||||
|   - python setup.py install |  | ||||||
|   - pip install flake8 |  | ||||||
|   - pip install pytest |  | ||||||
| before_script: flake8 sanic |  | ||||||
| script: py.test -v tests |  | ||||||
| deploy: | deploy: | ||||||
|   provider: pypi |   provider: pypi | ||||||
|   user: channelcat |   user: channelcat | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								README.rst
									
									
									
									
									
								
							| @@ -74,3 +74,37 @@ Documentation can be found in the ``docs`` directory. | |||||||
|    :target: https://pypi.python.org/pypi/sanic/ |    :target: https://pypi.python.org/pypi/sanic/ | ||||||
| .. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg | .. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg | ||||||
|    :target: https://pypi.python.org/pypi/sanic/ |    :target: https://pypi.python.org/pypi/sanic/ | ||||||
|  |  | ||||||
|  | TODO | ||||||
|  | ---- | ||||||
|  | * Streamed file processing | ||||||
|  | * File output | ||||||
|  | * Examples of integrations with 3rd-party modules | ||||||
|  | * RESTful router | ||||||
|  |  | ||||||
|  | Limitations | ||||||
|  | ----------- | ||||||
|  | * No wheels for uvloop and httptools on Windows :( | ||||||
|  |  | ||||||
|  | Final Thoughts | ||||||
|  | -------------- | ||||||
|  |  | ||||||
|  |                      ▄▄▄▄▄ | ||||||
|  |             ▀▀▀██████▄▄▄       _______________ | ||||||
|  |           ▄▄▄▄▄  █████████▄  /                 \ | ||||||
|  |          ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  | | ||||||
|  |        ▀▀█████▄▄ ▀██████▄██ | _________________/ | ||||||
|  |        ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/ | ||||||
|  |             ▀▀▀▄  ▀▀███ ▀       ▄▄ | ||||||
|  |          ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌ | ||||||
|  |        ██▀▄▄▄██▀▄███▀ ▀▀████      ▄██ | ||||||
|  |     ▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███     ▌▄▄▀ | ||||||
|  |     ▌    ▐▀████▐███▒▒▒▒▒▐██▌ | ||||||
|  |     ▀▄▄▄▄▀   ▀▀████▒▒▒▒▄██▀ | ||||||
|  |               ▀▀█████████▀ | ||||||
|  |             ▄▄██▀██████▀█ | ||||||
|  |           ▄██▀     ▀▀▀  █ | ||||||
|  |          ▄█             ▐▌ | ||||||
|  |      ▄▄▄▄█▌              ▀█▄▄▄▄▀▀▄ | ||||||
|  |     ▌     ▐                ▀▀▄▄▄▀ | ||||||
|  |      ▀▀▄▄▀ | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ class SimpleView(HTTPMethodView): | |||||||
|   def delete(self, request): |   def delete(self, request): | ||||||
|       return text('I am delete method') |       return text('I am delete method') | ||||||
|  |  | ||||||
| app.add_route(SimpleView(), '/') | app.add_route(SimpleView.as_view(), '/') | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| @@ -40,6 +40,19 @@ class NameView(HTTPMethodView): | |||||||
|   def get(self, request, name): |   def get(self, request, name): | ||||||
|     return text('Hello {}'.format(name)) |     return text('Hello {}'.format(name)) | ||||||
|  |  | ||||||
| app.add_route(NameView(), '/<name>') | app.add_route(NameView.as_view(), '/<name>') | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | If you want to add decorator for class, you could set decorators variable | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | class ViewWithDecorator(HTTPMethodView): | ||||||
|  |   decorators = [some_decorator_here] | ||||||
|  |  | ||||||
|  |   def get(self, request, name): | ||||||
|  |     return text('Hello I have a decorator') | ||||||
|  |  | ||||||
|  | app.add_route(ViewWithDecorator.as_view(), '/url') | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								docs/custom_protocol.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								docs/custom_protocol.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | # Custom Protocol | ||||||
|  |  | ||||||
|  | You can change the behavior of protocol by using custom protocol.   | ||||||
|  | If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic. | ||||||
|  |  | ||||||
|  | * loop   | ||||||
|  | `loop` is an asyncio compatible event loop. | ||||||
|  |  | ||||||
|  | * connections   | ||||||
|  | `connections` is a `set object`  to  store protocol objects. | ||||||
|  | When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections. | ||||||
|  |  | ||||||
|  | * signal   | ||||||
|  | `signal` is a `sanic.server.Signal object` with `stopped attribute`. | ||||||
|  | When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`. | ||||||
|  |  | ||||||
|  | * request_handler   | ||||||
|  | `request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments. | ||||||
|  |  | ||||||
|  | * error_handler   | ||||||
|  | `error_handler` is a `sanic.exceptions.Handler` object. | ||||||
|  |  | ||||||
|  | * request_timeout   | ||||||
|  | `request_timeout` is seconds for timeout. | ||||||
|  |  | ||||||
|  | * request_max_size   | ||||||
|  | `request_max_size` is bytes of max request size. | ||||||
|  |  | ||||||
|  | ## Example | ||||||
|  | By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`.   | ||||||
|  | In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`. | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.server import HttpProtocol | ||||||
|  | from sanic.response import text | ||||||
|  |  | ||||||
|  | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomHttpProtocol(HttpProtocol): | ||||||
|  |  | ||||||
|  |     def __init__(self, *, loop, request_handler, error_handler, | ||||||
|  |                  signal, connections, request_timeout, request_max_size): | ||||||
|  |         super().__init__( | ||||||
|  |             loop=loop, request_handler=request_handler, | ||||||
|  |             error_handler=error_handler, signal=signal, | ||||||
|  |             connections=connections, request_timeout=request_timeout, | ||||||
|  |             request_max_size=request_max_size) | ||||||
|  |  | ||||||
|  |     def write_response(self, response): | ||||||
|  |         if isinstance(response, str): | ||||||
|  |             response = text(response) | ||||||
|  |         self.transport.write( | ||||||
|  |             response.output(self.request.version) | ||||||
|  |         ) | ||||||
|  |         self.transport.close() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route('/') | ||||||
|  | async def string(request): | ||||||
|  |     return 'string' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route('/1') | ||||||
|  | async def response(request): | ||||||
|  |     return text('response') | ||||||
|  |  | ||||||
|  | app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) | ||||||
|  | ``` | ||||||
							
								
								
									
										6
									
								
								docs/extensions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/extensions.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | # Sanic Extensions | ||||||
|  |  | ||||||
|  | A list of Sanic extensions created by the community. | ||||||
|  |  | ||||||
|  |  * [Sessions](https://github.com/subyraman/sanic_session) — Support for sessions. Allows using redis, memcache or an in memory store. | ||||||
|  |  * [CORS](https://github.com/ashleysommer/sanic-cors) — A port of flask-cors. | ||||||
| @@ -9,6 +9,7 @@ The following request variables are accessible as properties: | |||||||
| `request.args` (dict) - Query String variables.  Use getlist to get multiple of the same name   | `request.args` (dict) - Query String variables.  Use getlist to get multiple of the same name   | ||||||
| `request.form` (dict) - Posted form variables.  Use getlist to get multiple of the same name   | `request.form` (dict) - Posted form variables.  Use getlist to get multiple of the same name   | ||||||
| `request.body` (bytes) - Posted raw body.  To get the raw data, regardless of content type   | `request.body` (bytes) - Posted raw body.  To get the raw data, regardless of content type   | ||||||
|  | `request.ip` (str) - IP address of the requester | ||||||
|  |  | ||||||
| See request.py for more information | See request.py for more information | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,12 +33,12 @@ async def handler1(request): | |||||||
| 	return text('OK') | 	return text('OK') | ||||||
| app.add_route(handler1, '/test') | app.add_route(handler1, '/test') | ||||||
|  |  | ||||||
| async def handler(request, name): | async def handler2(request, name): | ||||||
| 	return text('Folder - {}'.format(name)) | 	return text('Folder - {}'.format(name)) | ||||||
| app.add_route(handler, '/folder/<name>') | app.add_route(handler2, '/folder/<name>') | ||||||
|  |  | ||||||
| async def person_handler(request, name): | async def person_handler2(request, name): | ||||||
| 	return text('Person - {}'.format(name)) | 	return text('Person - {}'.format(name)) | ||||||
| app.add_route(handler, '/person/<name:[A-z]>') | app.add_route(person_handler2, '/person/<name:[A-z]>') | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								examples/override_logging.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								examples/override_logging.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import text | ||||||
|  | import json | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | logging_format = "[%(asctime)s] %(process)d-%(levelname)s " | ||||||
|  | logging_format += "%(module)s::%(funcName)s():l%(lineno)d: " | ||||||
|  | logging_format += "%(message)s" | ||||||
|  |  | ||||||
|  | logging.basicConfig( | ||||||
|  |     format=logging_format, | ||||||
|  |     level=logging.DEBUG | ||||||
|  | ) | ||||||
|  | log = logging.getLogger() | ||||||
|  |  | ||||||
|  | # Set logger to override default basicConfig | ||||||
|  | sanic = Sanic() | ||||||
|  | @sanic.route("/") | ||||||
|  | def test(request): | ||||||
|  |     log.info("received request; responding with 'hey'") | ||||||
|  |     return text("hey") | ||||||
|  |  | ||||||
|  | sanic.run(host="0.0.0.0", port=8000) | ||||||
							
								
								
									
										65
									
								
								examples/sanic_asyncpg_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								examples/sanic_asyncpg_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | """ To run this example you need additional asyncpg package | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | import os | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
|  | import uvloop | ||||||
|  | from asyncpg import create_pool | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import json | ||||||
|  |  | ||||||
|  | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) | ||||||
|  |  | ||||||
|  | DB_CONFIG = { | ||||||
|  |     'host': '<host>', | ||||||
|  |     'user': '<username>', | ||||||
|  |     'password': '<password>', | ||||||
|  |     'port': '<port>', | ||||||
|  |     'database': '<database>' | ||||||
|  | } | ||||||
|  |  | ||||||
|  | def jsonify(records): | ||||||
|  |     """ Parse asyncpg record response into JSON format | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |     return [{key: value for key, value in | ||||||
|  |              zip(r.keys(), r.values())} for r in records] | ||||||
|  |  | ||||||
|  | loop = asyncio.get_event_loop() | ||||||
|  |  | ||||||
|  | async def make_pool(): | ||||||
|  |     return await create_pool(**DB_CONFIG) | ||||||
|  |  | ||||||
|  | app = Sanic(__name__) | ||||||
|  | pool = loop.run_until_complete(make_pool()) | ||||||
|  |  | ||||||
|  | async def create_db(): | ||||||
|  |     """ Create some table and add some data | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |     async with pool.acquire() as connection: | ||||||
|  |         async with connection.transaction(): | ||||||
|  |             await connection.execute('DROP TABLE IF EXISTS sanic_post') | ||||||
|  |             await connection.execute("""CREATE TABLE sanic_post ( | ||||||
|  |                                     id serial primary key, | ||||||
|  |                                     content varchar(50), | ||||||
|  |                                     post_date timestamp | ||||||
|  |                                 );""") | ||||||
|  |             for i in range(0, 100): | ||||||
|  |                 await connection.execute(f"""INSERT INTO sanic_post | ||||||
|  |                     (id, content, post_date) VALUES ({i}, {i}, now())""") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route("/") | ||||||
|  | async def handler(request): | ||||||
|  |     async with pool.acquire() as connection: | ||||||
|  |         async with connection.transaction(): | ||||||
|  |             results = await connection.fetch('SELECT * FROM sanic_post') | ||||||
|  |             return json({'posts': jsonify(results)}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     loop.run_until_complete(create_db()) | ||||||
|  |     app.run(host='0.0.0.0', port=8000, loop=loop) | ||||||
| @@ -64,11 +64,11 @@ def query_string(request): | |||||||
| # Run Server | # Run Server | ||||||
| # ----------------------------------------------- # | # ----------------------------------------------- # | ||||||
|  |  | ||||||
| def after_start(loop): | def after_start(app, loop): | ||||||
|     log.info("OH OH OH OH OHHHHHHHH") |     log.info("OH OH OH OH OHHHHHHHH") | ||||||
|  |  | ||||||
|  |  | ||||||
| def before_stop(loop): | def before_stop(app, loop): | ||||||
|     log.info("TRIED EVERYTHING") |     log.info("TRIED EVERYTHING") | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								examples/vhosts.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								examples/vhosts.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | from sanic.response import text | ||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.blueprints import Blueprint | ||||||
|  |  | ||||||
|  | # Usage | ||||||
|  | # curl -H "Host: example.com" localhost:8000 | ||||||
|  | # curl -H "Host: sub.example.com" localhost:8000 | ||||||
|  | # curl -H "Host: bp.example.com" localhost:8000/question | ||||||
|  | # curl -H "Host: bp.example.com" localhost:8000/answer | ||||||
|  |  | ||||||
|  | app = Sanic() | ||||||
|  | bp = Blueprint("bp", host="bp.example.com") | ||||||
|  |  | ||||||
|  | @app.route('/', host="example.com") | ||||||
|  | async def hello(request): | ||||||
|  |     return text("Answer") | ||||||
|  | @app.route('/', host="sub.example.com") | ||||||
|  | async def hello(request): | ||||||
|  |     return text("42") | ||||||
|  |  | ||||||
|  | @bp.route("/question") | ||||||
|  | async def hello(request): | ||||||
|  |     return text("What is the meaning of life?") | ||||||
|  |  | ||||||
|  | @bp.route("/answer") | ||||||
|  | async def hello(request): | ||||||
|  |     return text("42") | ||||||
|  |  | ||||||
|  | app.register_blueprint(bp) | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     app.run(host="0.0.0.0", port=8000) | ||||||
| @@ -14,3 +14,4 @@ tornado | |||||||
| aiofiles | aiofiles | ||||||
| sphinx | sphinx | ||||||
| recommonmark | recommonmark | ||||||
|  | beautifulsoup4 | ||||||
|   | |||||||
| @@ -2,4 +2,3 @@ httptools | |||||||
| ujson | ujson | ||||||
| uvloop | uvloop | ||||||
| aiofiles | aiofiles | ||||||
| multidict |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from .sanic import Sanic | from .sanic import Sanic | ||||||
| from .blueprints import Blueprint | from .blueprints import Blueprint | ||||||
|  |  | ||||||
| __version__ = '0.1.9' | __version__ = '0.2.0' | ||||||
|  |  | ||||||
| __all__ = ['Sanic', 'Blueprint'] | __all__ = ['Sanic', 'Blueprint'] | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ if __name__ == "__main__": | |||||||
|  |  | ||||||
|         module = import_module(module_name) |         module = import_module(module_name) | ||||||
|         app = getattr(module, app_name, None) |         app = getattr(module, app_name, None) | ||||||
|         if type(app) is not Sanic: |         if not isinstance(app, Sanic): | ||||||
|             raise ValueError("Module is not a Sanic app, it is a {}.  " |             raise ValueError("Module is not a Sanic app, it is a {}.  " | ||||||
|                              "Perhaps you meant {}.app?" |                              "Perhaps you meant {}.app?" | ||||||
|                              .format(type(app).__name__, args.module)) |                              .format(type(app).__name__, args.module)) | ||||||
|   | |||||||
| @@ -18,14 +18,17 @@ class BlueprintSetup: | |||||||
|         #: blueprint. |         #: blueprint. | ||||||
|         self.url_prefix = url_prefix |         self.url_prefix = url_prefix | ||||||
|  |  | ||||||
|     def add_route(self, handler, uri, methods): |     def add_route(self, handler, uri, methods, host=None): | ||||||
|         """ |         """ | ||||||
|         A helper method to register a handler to the application url routes. |         A helper method to register a handler to the application url routes. | ||||||
|         """ |         """ | ||||||
|         if self.url_prefix: |         if self.url_prefix: | ||||||
|             uri = self.url_prefix + uri |             uri = self.url_prefix + uri | ||||||
|  |  | ||||||
|         self.app.route(uri=uri, methods=methods)(handler) |         if host is None: | ||||||
|  |             host = self.blueprint.host | ||||||
|  |  | ||||||
|  |         self.app.route(uri=uri, methods=methods, host=host)(handler) | ||||||
|  |  | ||||||
|     def add_exception(self, handler, *args, **kwargs): |     def add_exception(self, handler, *args, **kwargs): | ||||||
|         """ |         """ | ||||||
| @@ -53,7 +56,7 @@ class BlueprintSetup: | |||||||
|  |  | ||||||
|  |  | ||||||
| class Blueprint: | class Blueprint: | ||||||
|     def __init__(self, name, url_prefix=None): |     def __init__(self, name, url_prefix=None, host=None): | ||||||
|         """ |         """ | ||||||
|         Creates a new blueprint |         Creates a new blueprint | ||||||
|         :param name: Unique name of the blueprint |         :param name: Unique name of the blueprint | ||||||
| @@ -63,6 +66,7 @@ class Blueprint: | |||||||
|         self.url_prefix = url_prefix |         self.url_prefix = url_prefix | ||||||
|         self.deferred_functions = [] |         self.deferred_functions = [] | ||||||
|         self.listeners = defaultdict(list) |         self.listeners = defaultdict(list) | ||||||
|  |         self.host = host | ||||||
|  |  | ||||||
|     def record(self, func): |     def record(self, func): | ||||||
|         """ |         """ | ||||||
| @@ -83,18 +87,18 @@ class Blueprint: | |||||||
|         for deferred in self.deferred_functions: |         for deferred in self.deferred_functions: | ||||||
|             deferred(state) |             deferred(state) | ||||||
|  |  | ||||||
|     def route(self, uri, methods=None): |     def route(self, uri, methods=None, host=None): | ||||||
|         """ |         """ | ||||||
|         """ |         """ | ||||||
|         def decorator(handler): |         def decorator(handler): | ||||||
|             self.record(lambda s: s.add_route(handler, uri, methods)) |             self.record(lambda s: s.add_route(handler, uri, methods, host)) | ||||||
|             return handler |             return handler | ||||||
|         return decorator |         return decorator | ||||||
|  |  | ||||||
|     def add_route(self, handler, uri, methods=None): |     def add_route(self, handler, uri, methods=None, host=None): | ||||||
|         """ |         """ | ||||||
|         """ |         """ | ||||||
|         self.record(lambda s: s.add_route(handler, uri, methods)) |         self.record(lambda s: s.add_route(handler, uri, methods, host)) | ||||||
|         return handler |         return handler | ||||||
|  |  | ||||||
|     def listener(self, event): |     def listener(self, event): | ||||||
|   | |||||||
| @@ -1,5 +1,104 @@ | |||||||
| from .response import text | from .response import text, html | ||||||
| from traceback import format_exc | from .log import log | ||||||
|  | from traceback import format_exc, extract_tb | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  | TRACEBACK_STYLE = ''' | ||||||
|  |     <style> | ||||||
|  |         body { | ||||||
|  |             padding: 20px; | ||||||
|  |             font-family: Arial, sans-serif; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         p { | ||||||
|  |             margin: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .summary { | ||||||
|  |             padding: 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         h1 { | ||||||
|  |             margin-bottom: 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         h3 { | ||||||
|  |             margin-top: 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         h3 code { | ||||||
|  |             font-size: 24px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .frame-line > * { | ||||||
|  |             padding: 5px 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .frame-line { | ||||||
|  |             margin-bottom: 5px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .frame-code { | ||||||
|  |             font-size: 16px; | ||||||
|  |             padding-left: 30px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .tb-wrapper { | ||||||
|  |             border: 1px solid #f3f3f3; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .tb-header { | ||||||
|  |             background-color: #f3f3f3; | ||||||
|  |             padding: 5px 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .frame-descriptor { | ||||||
|  |             background-color: #e2eafb; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .frame-descriptor { | ||||||
|  |             font-size: 14px; | ||||||
|  |         } | ||||||
|  |     </style> | ||||||
|  | ''' | ||||||
|  |  | ||||||
|  | TRACEBACK_WRAPPER_HTML = ''' | ||||||
|  |     <html> | ||||||
|  |         <head> | ||||||
|  |             {style} | ||||||
|  |         </head> | ||||||
|  |         <body> | ||||||
|  |             <h1>{exc_name}</h1> | ||||||
|  |             <h3><code>{exc_value}</code></h3> | ||||||
|  |             <div class="tb-wrapper"> | ||||||
|  |                 <p class="tb-header">Traceback (most recent call last):</p> | ||||||
|  |                 {frame_html} | ||||||
|  |                 <p class="summary"> | ||||||
|  |                 <b>{exc_name}: {exc_value}</b> | ||||||
|  |                     while handling uri <code>{uri}</code> | ||||||
|  |                 </p> | ||||||
|  |             </div> | ||||||
|  |         </body> | ||||||
|  |     </html> | ||||||
|  | ''' | ||||||
|  |  | ||||||
|  | TRACEBACK_LINE_HTML = ''' | ||||||
|  |     <div class="frame-line"> | ||||||
|  |         <p class="frame-descriptor"> | ||||||
|  |             File {0.filename}, line <i>{0.lineno}</i>, | ||||||
|  |             in <code><b>{0.name}</b></code> | ||||||
|  |         </p> | ||||||
|  |         <p class="frame-code"><code>{0.line}</code></p> | ||||||
|  |     </div> | ||||||
|  | ''' | ||||||
|  |  | ||||||
|  | INTERNAL_SERVER_ERROR_HTML = ''' | ||||||
|  |     <h1>Internal Server Error</h1> | ||||||
|  |     <p> | ||||||
|  |         The server encountered an internal error and cannot complete | ||||||
|  |         your request. | ||||||
|  |     </p> | ||||||
|  | ''' | ||||||
|  |  | ||||||
|  |  | ||||||
| class SanicException(Exception): | class SanicException(Exception): | ||||||
| @@ -45,6 +144,21 @@ class Handler: | |||||||
|         self.handlers = {} |         self.handlers = {} | ||||||
|         self.sanic = sanic |         self.sanic = sanic | ||||||
|  |  | ||||||
|  |     def _render_traceback_html(self, exception, request): | ||||||
|  |         exc_type, exc_value, tb = sys.exc_info() | ||||||
|  |         frames = extract_tb(tb) | ||||||
|  |  | ||||||
|  |         frame_html = [] | ||||||
|  |         for frame in frames: | ||||||
|  |             frame_html.append(TRACEBACK_LINE_HTML.format(frame)) | ||||||
|  |  | ||||||
|  |         return TRACEBACK_WRAPPER_HTML.format( | ||||||
|  |             style=TRACEBACK_STYLE, | ||||||
|  |             exc_name=exc_type.__name__, | ||||||
|  |             exc_value=exc_value, | ||||||
|  |             frame_html=''.join(frame_html), | ||||||
|  |             uri=request.url) | ||||||
|  |  | ||||||
|     def add(self, exception, handler): |     def add(self, exception, handler): | ||||||
|         self.handlers[exception] = handler |         self.handlers[exception] = handler | ||||||
|  |  | ||||||
| @@ -57,18 +171,32 @@ class Handler: | |||||||
|         :return: Response object |         :return: Response object | ||||||
|         """ |         """ | ||||||
|         handler = self.handlers.get(type(exception), self.default) |         handler = self.handlers.get(type(exception), self.default) | ||||||
|         response = handler(request=request, exception=exception) |         try: | ||||||
|  |             response = handler(request=request, exception=exception) | ||||||
|  |         except: | ||||||
|  |             if self.sanic.debug: | ||||||
|  |                 response_message = ( | ||||||
|  |                     'Exception raised in exception handler "{}" ' | ||||||
|  |                     'for uri: "{}"\n{}').format( | ||||||
|  |                         handler.__name__, request.url, format_exc()) | ||||||
|  |                 log.error(response_message) | ||||||
|  |                 return text(response_message, 500) | ||||||
|  |             else: | ||||||
|  |                 return text('An error occurred while handling an error', 500) | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|     def default(self, request, exception): |     def default(self, request, exception): | ||||||
|         if issubclass(type(exception), SanicException): |         if issubclass(type(exception), SanicException): | ||||||
|             return text( |             return text( | ||||||
|                 "Error: {}".format(exception), |                 'Error: {}'.format(exception), | ||||||
|                 status=getattr(exception, 'status_code', 500)) |                 status=getattr(exception, 'status_code', 500)) | ||||||
|         elif self.sanic.debug: |         elif self.sanic.debug: | ||||||
|             return text( |             html_output = self._render_traceback_html(exception, request) | ||||||
|                 "Error: {}\nException: {}".format( |  | ||||||
|                     exception, format_exc()), status=500) |             response_message = ( | ||||||
|  |                 'Exception occurred while handling uri: "{}"\n{}'.format( | ||||||
|  |                     request.url, format_exc())) | ||||||
|  |             log.error(response_message) | ||||||
|  |             return html(html_output, status=500) | ||||||
|         else: |         else: | ||||||
|             return text( |             return html(INTERNAL_SERVER_ERROR_HTML, status=500) | ||||||
|                 "An error occurred while generating the request", status=500) |  | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logging.basicConfig( |  | ||||||
|     level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") |  | ||||||
| log = logging.getLogger(__name__) | log = logging.getLogger(__name__) | ||||||
|   | |||||||
| @@ -25,6 +25,9 @@ class RequestParameters(dict): | |||||||
|         self.super = super() |         self.super = super() | ||||||
|         self.super.__init__(*args, **kwargs) |         self.super.__init__(*args, **kwargs) | ||||||
|  |  | ||||||
|  |     def __getitem__(self, name): | ||||||
|  |         return self.get(name) | ||||||
|  |  | ||||||
|     def get(self, name, default=None): |     def get(self, name, default=None): | ||||||
|         values = self.super.get(name) |         values = self.super.get(name) | ||||||
|         return values[0] if values else default |         return values[0] if values else default | ||||||
| @@ -38,18 +41,20 @@ class Request(dict): | |||||||
|     Properties of an HTTP request such as URL, headers, etc. |     Properties of an HTTP request such as URL, headers, etc. | ||||||
|     """ |     """ | ||||||
|     __slots__ = ( |     __slots__ = ( | ||||||
|         'url', 'headers', 'version', 'method', '_cookies', |         'url', 'headers', 'version', 'method', '_cookies', 'transport', | ||||||
|         'query_string', 'body', |         'query_string', 'body', | ||||||
|         'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', |         'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', | ||||||
|  |         '_ip', | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     def __init__(self, url_bytes, headers, version, method): |     def __init__(self, url_bytes, headers, version, method, transport): | ||||||
|         # TODO: Content-Encoding detection |         # TODO: Content-Encoding detection | ||||||
|         url_parsed = parse_url(url_bytes) |         url_parsed = parse_url(url_bytes) | ||||||
|         self.url = url_parsed.path.decode('utf-8') |         self.url = url_parsed.path.decode('utf-8') | ||||||
|         self.headers = headers |         self.headers = headers | ||||||
|         self.version = version |         self.version = version | ||||||
|         self.method = method |         self.method = method | ||||||
|  |         self.transport = transport | ||||||
|         self.query_string = None |         self.query_string = None | ||||||
|         if url_parsed.query: |         if url_parsed.query: | ||||||
|             self.query_string = url_parsed.query.decode('utf-8') |             self.query_string = url_parsed.query.decode('utf-8') | ||||||
| @@ -64,7 +69,7 @@ class Request(dict): | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def json(self): |     def json(self): | ||||||
|         if not self.parsed_json: |         if self.parsed_json is None: | ||||||
|             try: |             try: | ||||||
|                 self.parsed_json = json_loads(self.body) |                 self.parsed_json = json_loads(self.body) | ||||||
|             except Exception: |             except Exception: | ||||||
| @@ -72,6 +77,17 @@ class Request(dict): | |||||||
|  |  | ||||||
|         return self.parsed_json |         return self.parsed_json | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def token(self): | ||||||
|  |         """ | ||||||
|  |         Attempts to return the auth header token. | ||||||
|  |         :return: token related to request | ||||||
|  |         """ | ||||||
|  |         auth_header = self.headers.get('Authorization') | ||||||
|  |         if auth_header is not None: | ||||||
|  |             return auth_header.split()[1] | ||||||
|  |         return auth_header | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def form(self): |     def form(self): | ||||||
|         if self.parsed_form is None: |         if self.parsed_form is None: | ||||||
| @@ -125,6 +141,12 @@ class Request(dict): | |||||||
|                 self._cookies = {} |                 self._cookies = {} | ||||||
|         return self._cookies |         return self._cookies | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def ip(self): | ||||||
|  |         if not hasattr(self, '_ip'): | ||||||
|  |             self._ip = self.transport.get_extra_info('peername') | ||||||
|  |         return self._ip | ||||||
|  |  | ||||||
|  |  | ||||||
| File = namedtuple('File', ['type', 'body', 'name']) | File = namedtuple('File', ['type', 'body', 'name']) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -83,10 +83,10 @@ class HTTPResponse: | |||||||
|         if body is not None: |         if body is not None: | ||||||
|             try: |             try: | ||||||
|                 # Try to encode it regularly |                 # Try to encode it regularly | ||||||
|                 self.body = body.encode('utf-8') |                 self.body = body.encode() | ||||||
|             except AttributeError: |             except AttributeError: | ||||||
|                 # Convert it to a str if you can't |                 # Convert it to a str if you can't | ||||||
|                 self.body = str(body).encode('utf-8') |                 self.body = str(body).encode() | ||||||
|         else: |         else: | ||||||
|             self.body = body_bytes |             self.body = body_bytes | ||||||
|  |  | ||||||
| @@ -103,10 +103,14 @@ class HTTPResponse: | |||||||
|  |  | ||||||
|         headers = b'' |         headers = b'' | ||||||
|         if self.headers: |         if self.headers: | ||||||
|             headers = b''.join( |             for name, value in self.headers.items(): | ||||||
|                 b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) |                 try: | ||||||
|                 for name, value in self.headers.items() |                     headers += ( | ||||||
|             ) |                         b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))) | ||||||
|  |                 except AttributeError: | ||||||
|  |                     headers += ( | ||||||
|  |                         b'%b: %b\r\n' % ( | ||||||
|  |                             str(name).encode(), str(value).encode('utf-8'))) | ||||||
|  |  | ||||||
|         # Try to pull from the common codes first |         # Try to pull from the common codes first | ||||||
|         # Speeds up response rate 6% over pulling from all |         # Speeds up response rate 6% over pulling from all | ||||||
| @@ -165,3 +169,26 @@ async def file(location, mime_type=None, headers=None): | |||||||
|                         headers=headers, |                         headers=headers, | ||||||
|                         content_type=mime_type, |                         content_type=mime_type, | ||||||
|                         body_bytes=out_stream) |                         body_bytes=out_stream) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def redirect(to, headers=None, status=302, | ||||||
|  |              content_type="text/html; charset=utf-8"): | ||||||
|  |     """ | ||||||
|  |     Aborts execution and causes a 302 redirect (by default). | ||||||
|  |  | ||||||
|  |     :param to: path or fully qualified URL to redirect to | ||||||
|  |     :param headers: optional dict of headers to include in the new request | ||||||
|  |     :param status: status code (int) of the new request, defaults to 302 | ||||||
|  |     :param content_type: | ||||||
|  |         the content type (string) of the response | ||||||
|  |     :returns: the redirecting Response | ||||||
|  |     """ | ||||||
|  |     headers = headers or {} | ||||||
|  |  | ||||||
|  |     # According to RFC 7231, a relative URI is now permitted. | ||||||
|  |     headers['Location'] = to | ||||||
|  |  | ||||||
|  |     return HTTPResponse( | ||||||
|  |         status=status, | ||||||
|  |         headers=headers, | ||||||
|  |         content_type=content_type) | ||||||
|   | |||||||
| @@ -23,6 +23,10 @@ class RouteExists(Exception): | |||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RouteDoesNotExist(Exception): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| class Router: | class Router: | ||||||
|     """ |     """ | ||||||
|     Router supports basic routing with parameters and method checks |     Router supports basic routing with parameters and method checks | ||||||
| @@ -31,16 +35,16 @@ class Router: | |||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         @sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...]) |         @sanic.route('/my/url/<my_param>', methods=['GET', 'POST', ...]) | ||||||
|         def my_route(request, my_parameter): |         def my_route(request, my_param): | ||||||
|             do stuff... |             do stuff... | ||||||
|  |  | ||||||
|     or |     or | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         @sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...]) |         @sanic.route('/my/url/<my_param:my_type>', methods['GET', 'POST', ...]) | ||||||
|         def my_route_with_type(request, my_parameter): |         def my_route_with_type(request, my_param: my_type): | ||||||
|             do stuff... |             do stuff... | ||||||
|  |  | ||||||
|     Parameters will be passed as keyword arguments to the request handling |     Parameters will be passed as keyword arguments to the request handling | ||||||
| @@ -59,8 +63,9 @@ class Router: | |||||||
|         self.routes_static = {} |         self.routes_static = {} | ||||||
|         self.routes_dynamic = defaultdict(list) |         self.routes_dynamic = defaultdict(list) | ||||||
|         self.routes_always_check = [] |         self.routes_always_check = [] | ||||||
|  |         self.hosts = None | ||||||
|  |  | ||||||
|     def add(self, uri, methods, handler): |     def add(self, uri, methods, handler, host=None): | ||||||
|         """ |         """ | ||||||
|         Adds a handler to the route list |         Adds a handler to the route list | ||||||
|  |  | ||||||
| @@ -71,6 +76,17 @@ class Router: | |||||||
|                         When executed, it should provide a response object. |                         When executed, it should provide a response object. | ||||||
|         :return: Nothing |         :return: Nothing | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|  |         if host is not None: | ||||||
|  |             # we want to track if there are any | ||||||
|  |             # vhosts on the Router instance so that we can | ||||||
|  |             # default to the behavior without vhosts | ||||||
|  |             if self.hosts is None: | ||||||
|  |                 self.hosts = set(host) | ||||||
|  |             else: | ||||||
|  |                 self.hosts.add(host) | ||||||
|  |             uri = host + uri | ||||||
|  |  | ||||||
|         if uri in self.routes_all: |         if uri in self.routes_all: | ||||||
|             raise RouteExists("Route already registered: {}".format(uri)) |             raise RouteExists("Route already registered: {}".format(uri)) | ||||||
|  |  | ||||||
| @@ -118,6 +134,25 @@ class Router: | |||||||
|         else: |         else: | ||||||
|             self.routes_static[uri] = route |             self.routes_static[uri] = route | ||||||
|  |  | ||||||
|  |     def remove(self, uri, clean_cache=True, host=None): | ||||||
|  |         if host is not None: | ||||||
|  |             uri = host + uri | ||||||
|  |         try: | ||||||
|  |             route = self.routes_all.pop(uri) | ||||||
|  |         except KeyError: | ||||||
|  |             raise RouteDoesNotExist("Route was not registered: {}".format(uri)) | ||||||
|  |  | ||||||
|  |         if route in self.routes_always_check: | ||||||
|  |             self.routes_always_check.remove(route) | ||||||
|  |         elif url_hash(uri) in self.routes_dynamic \ | ||||||
|  |                 and route in self.routes_dynamic[url_hash(uri)]: | ||||||
|  |             self.routes_dynamic[url_hash(uri)].remove(route) | ||||||
|  |         else: | ||||||
|  |             self.routes_static.pop(uri) | ||||||
|  |  | ||||||
|  |         if clean_cache: | ||||||
|  |             self._get.cache_clear() | ||||||
|  |  | ||||||
|     def get(self, request): |     def get(self, request): | ||||||
|         """ |         """ | ||||||
|         Gets a request handler based on the URL of the request, or raises an |         Gets a request handler based on the URL of the request, or raises an | ||||||
| @@ -126,10 +161,14 @@ class Router: | |||||||
|         :param request: Request object |         :param request: Request object | ||||||
|         :return: handler, arguments, keyword arguments |         :return: handler, arguments, keyword arguments | ||||||
|         """ |         """ | ||||||
|         return self._get(request.url, request.method) |         if self.hosts is None: | ||||||
|  |             return self._get(request.url, request.method, '') | ||||||
|  |         else: | ||||||
|  |             return self._get(request.url, request.method, | ||||||
|  |                              request.headers.get("Host", '')) | ||||||
|  |  | ||||||
|     @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) |     @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) | ||||||
|     def _get(self, url, method): |     def _get(self, url, method, host): | ||||||
|         """ |         """ | ||||||
|         Gets a request handler based on the URL of the request, or raises an |         Gets a request handler based on the URL of the request, or raises an | ||||||
|         error.  Internal method for caching. |         error.  Internal method for caching. | ||||||
| @@ -137,6 +176,7 @@ class Router: | |||||||
|         :param method: Request method |         :param method: Request method | ||||||
|         :return: handler, arguments, keyword arguments |         :return: handler, arguments, keyword arguments | ||||||
|         """ |         """ | ||||||
|  |         url = host + url | ||||||
|         # Check against known static routes |         # Check against known static routes | ||||||
|         route = self.routes_static.get(url) |         route = self.routes_static.get(url) | ||||||
|         if route: |         if route: | ||||||
|   | |||||||
| @@ -4,21 +4,24 @@ from functools import partial | |||||||
| from inspect import isawaitable, stack, getmodulename | from inspect import isawaitable, stack, getmodulename | ||||||
| from multiprocessing import Process, Event | from multiprocessing import Process, Event | ||||||
| from signal import signal, SIGTERM, SIGINT | from signal import signal, SIGTERM, SIGINT | ||||||
| from time import sleep |  | ||||||
| from traceback import format_exc | from traceback import format_exc | ||||||
|  | import logging | ||||||
|  |  | ||||||
| from .config import Config | from .config import Config | ||||||
| from .exceptions import Handler | from .exceptions import Handler | ||||||
| from .log import log, logging | from .log import log | ||||||
| from .response import HTTPResponse | from .response import HTTPResponse | ||||||
| from .router import Router | from .router import Router | ||||||
| from .server import serve | from .server import serve, HttpProtocol | ||||||
| from .static import register as static_register | from .static import register as static_register | ||||||
| from .exceptions import ServerError | from .exceptions import ServerError | ||||||
|  | from socket import socket, SOL_SOCKET, SO_REUSEADDR | ||||||
|  | from os import set_inheritable | ||||||
|  |  | ||||||
|  |  | ||||||
| class Sanic: | class Sanic: | ||||||
|     def __init__(self, name=None, router=None, error_handler=None): |     def __init__(self, name=None, router=None, | ||||||
|  |                  error_handler=None): | ||||||
|         if name is None: |         if name is None: | ||||||
|             frame_records = stack()[1] |             frame_records = stack()[1] | ||||||
|             name = getmodulename(frame_records[1]) |             name = getmodulename(frame_records[1]) | ||||||
| @@ -32,6 +35,8 @@ class Sanic: | |||||||
|         self._blueprint_order = [] |         self._blueprint_order = [] | ||||||
|         self.loop = None |         self.loop = None | ||||||
|         self.debug = None |         self.debug = None | ||||||
|  |         self.sock = None | ||||||
|  |         self.processes = None | ||||||
|  |  | ||||||
|         # Register alternative method names |         # Register alternative method names | ||||||
|         self.go_fast = self.run |         self.go_fast = self.run | ||||||
| @@ -41,7 +46,7 @@ class Sanic: | |||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def route(self, uri, methods=None): |     def route(self, uri, methods=None, host=None): | ||||||
|         """ |         """ | ||||||
|         Decorates a function to be registered as a route |         Decorates a function to be registered as a route | ||||||
|  |  | ||||||
| @@ -56,12 +61,13 @@ class Sanic: | |||||||
|             uri = '/' + uri |             uri = '/' + uri | ||||||
|  |  | ||||||
|         def response(handler): |         def response(handler): | ||||||
|             self.router.add(uri=uri, methods=methods, handler=handler) |             self.router.add(uri=uri, methods=methods, handler=handler, | ||||||
|  |                             host=host) | ||||||
|             return handler |             return handler | ||||||
|  |  | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|     def add_route(self, handler, uri, methods=None): |     def add_route(self, handler, uri, methods=None, host=None): | ||||||
|         """ |         """ | ||||||
|         A helper method to register class instance or |         A helper method to register class instance or | ||||||
|         functions as a handler to the application url |         functions as a handler to the application url | ||||||
| @@ -72,9 +78,12 @@ class Sanic: | |||||||
|         :param methods: list or tuple of methods allowed |         :param methods: list or tuple of methods allowed | ||||||
|         :return: function or class instance |         :return: function or class instance | ||||||
|         """ |         """ | ||||||
|         self.route(uri=uri, methods=methods)(handler) |         self.route(uri=uri, methods=methods, host=host)(handler) | ||||||
|         return handler |         return handler | ||||||
|  |  | ||||||
|  |     def remove_route(self, uri, clean_cache=True, host=None): | ||||||
|  |         self.router.remove(uri, clean_cache, host) | ||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def exception(self, *exceptions): |     def exception(self, *exceptions): | ||||||
|         """ |         """ | ||||||
| @@ -144,7 +153,8 @@ class Sanic: | |||||||
|     def register_blueprint(self, *args, **kwargs): |     def register_blueprint(self, *args, **kwargs): | ||||||
|         # TODO: deprecate 1.0 |         # TODO: deprecate 1.0 | ||||||
|         log.warning("Use of register_blueprint will be deprecated in " |         log.warning("Use of register_blueprint will be deprecated in " | ||||||
|                     "version 1.0.  Please use the blueprint method instead") |                     "version 1.0.  Please use the blueprint method instead", | ||||||
|  |                     DeprecationWarning) | ||||||
|         return self.blueprint(*args, **kwargs) |         return self.blueprint(*args, **kwargs) | ||||||
|  |  | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
| @@ -237,7 +247,8 @@ class Sanic: | |||||||
|  |  | ||||||
|     def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, |     def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, | ||||||
|             after_start=None, before_stop=None, after_stop=None, sock=None, |             after_start=None, before_stop=None, after_stop=None, sock=None, | ||||||
|             workers=1, loop=None): |             workers=1, loop=None, protocol=HttpProtocol, backlog=100, | ||||||
|  |             stop_event=None, logger=None): | ||||||
|         """ |         """ | ||||||
|         Runs the HTTP Server and listens until keyboard interrupt or term |         Runs the HTTP Server and listens until keyboard interrupt or term | ||||||
|         signal. On termination, drains connections before closing. |         signal. On termination, drains connections before closing. | ||||||
| @@ -245,25 +256,32 @@ class Sanic: | |||||||
|         :param host: Address to host on |         :param host: Address to host on | ||||||
|         :param port: Port to host on |         :param port: Port to host on | ||||||
|         :param debug: Enables debug output (slows server) |         :param debug: Enables debug output (slows server) | ||||||
|         :param before_start: Function to be executed before the server starts |         :param before_start: Functions to be executed before the server starts | ||||||
|                              accepting connections |                              accepting connections | ||||||
|         :param after_start: Function to be executed after the server starts |         :param after_start: Functions to be executed after the server starts | ||||||
|                             accepting connections |                             accepting connections | ||||||
|         :param before_stop: Function to be executed when a stop signal is |         :param before_stop: Functions to be executed when a stop signal is | ||||||
|                             received before it is respected |                             received before it is respected | ||||||
|         :param after_stop: Function to be executed when all requests are |         :param after_stop: Functions to be executed when all requests are | ||||||
|                            complete |                            complete | ||||||
|  |  | ||||||
|         :param sock: Socket for the server to accept connections from |         :param sock: Socket for the server to accept connections from | ||||||
|         :param workers: Number of processes |         :param workers: Number of processes | ||||||
|                         received before it is respected |                         received before it is respected | ||||||
|         :param loop: asyncio compatible event loop |         :param loop: asyncio compatible event loop | ||||||
|  |         :param protocol: Subclass of asyncio protocol class | ||||||
|         :return: Nothing |         :return: Nothing | ||||||
|         """ |         """ | ||||||
|  |         logging.basicConfig( | ||||||
|  |             level=logging.INFO, | ||||||
|  |             format="%(asctime)s: %(levelname)s: %(message)s" | ||||||
|  |         ) | ||||||
|         self.error_handler.debug = True |         self.error_handler.debug = True | ||||||
|         self.debug = debug |         self.debug = debug | ||||||
|         self.loop = loop |         self.loop = loop | ||||||
|  |  | ||||||
|         server_settings = { |         server_settings = { | ||||||
|  |             'protocol': protocol, | ||||||
|             'host': host, |             'host': host, | ||||||
|             'port': port, |             'port': port, | ||||||
|             'sock': sock, |             'sock': sock, | ||||||
| @@ -272,7 +290,8 @@ class Sanic: | |||||||
|             'error_handler': self.error_handler, |             'error_handler': self.error_handler, | ||||||
|             'request_timeout': self.config.REQUEST_TIMEOUT, |             'request_timeout': self.config.REQUEST_TIMEOUT, | ||||||
|             'request_max_size': self.config.REQUEST_MAX_SIZE, |             'request_max_size': self.config.REQUEST_MAX_SIZE, | ||||||
|             'loop': loop |             'loop': loop, | ||||||
|  |             'backlog': backlog | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         # -------------------------------------------- # |         # -------------------------------------------- # | ||||||
| @@ -289,7 +308,7 @@ class Sanic: | |||||||
|             for blueprint in self.blueprints.values(): |             for blueprint in self.blueprints.values(): | ||||||
|                 listeners += blueprint.listeners[event_name] |                 listeners += blueprint.listeners[event_name] | ||||||
|             if args: |             if args: | ||||||
|                 if type(args) is not list: |                 if callable(args): | ||||||
|                     args = [args] |                     args = [args] | ||||||
|                 listeners += args |                 listeners += args | ||||||
|             if reverse: |             if reverse: | ||||||
| @@ -311,7 +330,7 @@ class Sanic: | |||||||
|             else: |             else: | ||||||
|                 log.info('Spinning up {} workers...'.format(workers)) |                 log.info('Spinning up {} workers...'.format(workers)) | ||||||
|  |  | ||||||
|                 self.serve_multiple(server_settings, workers) |                 self.serve_multiple(server_settings, workers, stop_event) | ||||||
|  |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             log.exception( |             log.exception( | ||||||
| @@ -323,10 +342,13 @@ class Sanic: | |||||||
|         """ |         """ | ||||||
|         This kills the Sanic |         This kills the Sanic | ||||||
|         """ |         """ | ||||||
|  |         if self.processes is not None: | ||||||
|  |             for process in self.processes: | ||||||
|  |                 process.terminate() | ||||||
|  |             self.sock.close() | ||||||
|         get_event_loop().stop() |         get_event_loop().stop() | ||||||
|  |  | ||||||
|     @staticmethod |     def serve_multiple(self, server_settings, workers, stop_event=None): | ||||||
|     def serve_multiple(server_settings, workers, stop_event=None): |  | ||||||
|         """ |         """ | ||||||
|         Starts multiple server processes simultaneously.  Stops on interrupt |         Starts multiple server processes simultaneously.  Stops on interrupt | ||||||
|         and terminate signals, and drains connections when complete. |         and terminate signals, and drains connections when complete. | ||||||
| @@ -336,29 +358,37 @@ class Sanic: | |||||||
|         :param stop_event: if provided, is used as a stop signal |         :param stop_event: if provided, is used as a stop signal | ||||||
|         :return: |         :return: | ||||||
|         """ |         """ | ||||||
|  |         # In case this is called directly, we configure logging here too. | ||||||
|  |         # This won't interfere with the same call from run() | ||||||
|  |         logging.basicConfig( | ||||||
|  |             level=logging.INFO, | ||||||
|  |             format="%(asctime)s: %(levelname)s: %(message)s" | ||||||
|  |         ) | ||||||
|         server_settings['reuse_port'] = True |         server_settings['reuse_port'] = True | ||||||
|  |  | ||||||
|         # Create a stop event to be triggered by a signal |         # Create a stop event to be triggered by a signal | ||||||
|         if not stop_event: |         if stop_event is None: | ||||||
|             stop_event = Event() |             stop_event = Event() | ||||||
|         signal(SIGINT, lambda s, f: stop_event.set()) |         signal(SIGINT, lambda s, f: stop_event.set()) | ||||||
|         signal(SIGTERM, lambda s, f: stop_event.set()) |         signal(SIGTERM, lambda s, f: stop_event.set()) | ||||||
|  |  | ||||||
|         processes = [] |         self.sock = socket() | ||||||
|  |         self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) | ||||||
|  |         self.sock.bind((server_settings['host'], server_settings['port'])) | ||||||
|  |         set_inheritable(self.sock.fileno(), True) | ||||||
|  |         server_settings['sock'] = self.sock | ||||||
|  |         server_settings['host'] = None | ||||||
|  |         server_settings['port'] = None | ||||||
|  |  | ||||||
|  |         self.processes = [] | ||||||
|         for _ in range(workers): |         for _ in range(workers): | ||||||
|             process = Process(target=serve, kwargs=server_settings) |             process = Process(target=serve, kwargs=server_settings) | ||||||
|  |             process.daemon = True | ||||||
|             process.start() |             process.start() | ||||||
|             processes.append(process) |             self.processes.append(process) | ||||||
|  |  | ||||||
|         # Infinitely wait for the stop event |         for process in self.processes: | ||||||
|         try: |  | ||||||
|             while not stop_event.is_set(): |  | ||||||
|                 sleep(0.3) |  | ||||||
|         except: |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
|         log.info('Spinning down workers...') |  | ||||||
|         for process in processes: |  | ||||||
|             process.terminate() |  | ||||||
|         for process in processes: |  | ||||||
|             process.join() |             process.join() | ||||||
|  |  | ||||||
|  |         # the above processes will block this until they're stopped | ||||||
|  |         self.stop() | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import asyncio | import asyncio | ||||||
|  | import traceback | ||||||
| from functools import partial | from functools import partial | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from multidict import CIMultiDict |  | ||||||
| from signal import SIGINT, SIGTERM | from signal import SIGINT, SIGTERM | ||||||
| from time import time | from time import time | ||||||
| from httptools import HttpRequestParser | from httptools import HttpRequestParser | ||||||
| @@ -18,11 +18,30 @@ from .request import Request | |||||||
| from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage | from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | current_time = None | ||||||
|  |  | ||||||
|  |  | ||||||
| class Signal: | class Signal: | ||||||
|     stopped = False |     stopped = False | ||||||
|  |  | ||||||
|  |  | ||||||
| current_time = None | class CIDict(dict): | ||||||
|  |     """ | ||||||
|  |     Case Insensitive dict where all keys are converted to lowercase | ||||||
|  |     This does not maintain the inputted case when calling items() or keys() | ||||||
|  |     in favor of speed, since headers are case insensitive | ||||||
|  |     """ | ||||||
|  |     def get(self, key, default=None): | ||||||
|  |         return super().get(key.casefold(), default) | ||||||
|  |  | ||||||
|  |     def __getitem__(self, key): | ||||||
|  |         return super().__getitem__(key.casefold()) | ||||||
|  |  | ||||||
|  |     def __setitem__(self, key, value): | ||||||
|  |         return super().__setitem__(key.casefold(), value) | ||||||
|  |  | ||||||
|  |     def __contains__(self, key): | ||||||
|  |         return super().__contains__(key.casefold()) | ||||||
|  |  | ||||||
|  |  | ||||||
| class HttpProtocol(asyncio.Protocol): | class HttpProtocol(asyncio.Protocol): | ||||||
| @@ -118,18 +137,15 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             exception = PayloadTooLarge('Payload Too Large') |             exception = PayloadTooLarge('Payload Too Large') | ||||||
|             self.write_error(exception) |             self.write_error(exception) | ||||||
|  |  | ||||||
|         self.headers.append((name.decode(), value.decode('utf-8'))) |         self.headers.append((name.decode().casefold(), value.decode())) | ||||||
|  |  | ||||||
|     def on_headers_complete(self): |     def on_headers_complete(self): | ||||||
|         remote_addr = self.transport.get_extra_info('peername') |  | ||||||
|         if remote_addr: |  | ||||||
|             self.headers.append(('Remote-Addr', '%s:%s' % remote_addr)) |  | ||||||
|  |  | ||||||
|         self.request = Request( |         self.request = Request( | ||||||
|             url_bytes=self.url, |             url_bytes=self.url, | ||||||
|             headers=CIMultiDict(self.headers), |             headers=CIDict(self.headers), | ||||||
|             version=self.parser.get_http_version(), |             version=self.parser.get_http_version(), | ||||||
|             method=self.parser.get_method().decode() |             method=self.parser.get_method().decode(), | ||||||
|  |             transport=self.transport | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def on_body(self, body): |     def on_body(self, body): | ||||||
| @@ -174,9 +190,15 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|                 "Writing error failed, connection closed {}".format(e)) |                 "Writing error failed, connection closed {}".format(e)) | ||||||
|  |  | ||||||
|     def bail_out(self, message): |     def bail_out(self, message): | ||||||
|         exception = ServerError(message) |         if self.transport.is_closing(): | ||||||
|         self.write_error(exception) |             log.error( | ||||||
|         log.error(message) |                 "Connection closed before error was sent to user @ {}".format( | ||||||
|  |                     self.transport.get_extra_info('peername'))) | ||||||
|  |             log.debug('Error experienced:\n{}'.format(traceback.format_exc())) | ||||||
|  |         else: | ||||||
|  |             exception = ServerError(message) | ||||||
|  |             self.write_error(exception) | ||||||
|  |             log.error(message) | ||||||
|  |  | ||||||
|     def cleanup(self): |     def cleanup(self): | ||||||
|         self.parser = None |         self.parser = None | ||||||
| @@ -225,26 +247,33 @@ def trigger_events(events, loop): | |||||||
|  |  | ||||||
|  |  | ||||||
| def serve(host, port, request_handler, error_handler, before_start=None, | def serve(host, port, request_handler, error_handler, before_start=None, | ||||||
|           after_start=None, before_stop=None, after_stop=None, |           after_start=None, before_stop=None, after_stop=None, debug=False, | ||||||
|           debug=False, request_timeout=60, sock=None, |           request_timeout=60, sock=None, request_max_size=None, | ||||||
|           request_max_size=None, reuse_port=False, loop=None): |           reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100): | ||||||
|     """ |     """ | ||||||
|     Starts asynchronous HTTP Server on an individual process. |     Starts asynchronous HTTP Server on an individual process. | ||||||
|  |  | ||||||
|     :param host: Address to host on |     :param host: Address to host on | ||||||
|     :param port: Port to host on |     :param port: Port to host on | ||||||
|     :param request_handler: Sanic request handler with middleware |     :param request_handler: Sanic request handler with middleware | ||||||
|  |     :param error_handler: Sanic error handler with middleware | ||||||
|  |     :param before_start: Function to be executed before the server starts | ||||||
|  |                          listening. Takes single argument `loop` | ||||||
|     :param after_start: Function to be executed after the server starts |     :param after_start: Function to be executed after the server starts | ||||||
|                         listening. Takes single argument `loop` |                         listening. Takes single argument `loop` | ||||||
|     :param before_stop: Function to be executed when a stop signal is |     :param before_stop: Function to be executed when a stop signal is | ||||||
|                         received before it is respected. Takes single |                         received before it is respected. Takes single | ||||||
|                         argumenet `loop` |                         argument `loop` | ||||||
|  |     :param after_stop: Function to be executed when a stop signal is | ||||||
|  |                        received after it is respected. Takes single | ||||||
|  |                        argument `loop` | ||||||
|     :param debug: Enables debug output (slows server) |     :param debug: Enables debug output (slows server) | ||||||
|     :param request_timeout: time in seconds |     :param request_timeout: time in seconds | ||||||
|     :param sock: Socket for the server to accept connections from |     :param sock: Socket for the server to accept connections from | ||||||
|     :param request_max_size: size in bytes, `None` for no limit |     :param request_max_size: size in bytes, `None` for no limit | ||||||
|     :param reuse_port: `True` for multiple workers |     :param reuse_port: `True` for multiple workers | ||||||
|     :param loop: asyncio compatible event loop |     :param loop: asyncio compatible event loop | ||||||
|  |     :param protocol: Subclass of asyncio protocol class | ||||||
|     :return: Nothing |     :return: Nothing | ||||||
|     """ |     """ | ||||||
|     loop = loop or async_loop.new_event_loop() |     loop = loop or async_loop.new_event_loop() | ||||||
| @@ -258,7 +287,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, | |||||||
|     connections = set() |     connections = set() | ||||||
|     signal = Signal() |     signal = Signal() | ||||||
|     server = partial( |     server = partial( | ||||||
|         HttpProtocol, |         protocol, | ||||||
|         loop=loop, |         loop=loop, | ||||||
|         connections=connections, |         connections=connections, | ||||||
|         signal=signal, |         signal=signal, | ||||||
| @@ -273,7 +302,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, | |||||||
|         host, |         host, | ||||||
|         port, |         port, | ||||||
|         reuse_port=reuse_port, |         reuse_port=reuse_port, | ||||||
|         sock=sock |         sock=sock, | ||||||
|  |         backlog=backlog | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     # Instead of pulling time at the end of every request, |     # Instead of pulling time at the end of every request, | ||||||
|   | |||||||
| @@ -16,15 +16,15 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): | |||||||
|  |  | ||||||
|  |  | ||||||
| def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | ||||||
|                         loop=None, debug=False, *request_args, |                         loop=None, debug=False, server_kwargs={}, | ||||||
|                         **request_kwargs): |                         *request_args, **request_kwargs): | ||||||
|     results = [] |     results = [] | ||||||
|     exceptions = [] |     exceptions = [] | ||||||
|  |  | ||||||
|     if gather_request: |     if gather_request: | ||||||
|         @app.middleware |  | ||||||
|         def _collect_request(request): |         def _collect_request(request): | ||||||
|             results.append(request) |             results.append(request) | ||||||
|  |         app.request_middleware.appendleft(_collect_request) | ||||||
|  |  | ||||||
|     async def _collect_response(sanic, loop): |     async def _collect_response(sanic, loop): | ||||||
|         try: |         try: | ||||||
| @@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | |||||||
|             exceptions.append(e) |             exceptions.append(e) | ||||||
|         app.stop() |         app.stop() | ||||||
|  |  | ||||||
|     app.run(host=HOST, debug=debug, port=42101, |     app.run(host=HOST, debug=debug, port=PORT, | ||||||
|             after_start=_collect_response, loop=loop) |             after_start=_collect_response, loop=loop, **server_kwargs) | ||||||
|  |  | ||||||
|     if exceptions: |     if exceptions: | ||||||
|         raise ValueError("Exception during request: {}".format(exceptions)) |         raise ValueError("Exception during request: {}".format(exceptions)) | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ class HTTPMethodView: | |||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class DummyView(View): |         class DummyView(HTTPMethodView): | ||||||
|             def get(self, request, *args, **kwargs): |             def get(self, request, *args, **kwargs): | ||||||
|                 return text('I am get method') |                 return text('I am get method') | ||||||
|             def put(self, request, *args, **kwargs): |             def put(self, request, *args, **kwargs): | ||||||
| @@ -25,19 +25,43 @@ class HTTPMethodView: | |||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class DummyView(View): |         class DummyView(HTTPMethodView): | ||||||
|             def get(self, request, my_param_here, *args, **kwargs): |             def get(self, request, my_param_here, *args, **kwargs): | ||||||
|                 return text('I am get method with %s' % my_param_here) |                 return text('I am get method with %s' % my_param_here) | ||||||
|  |  | ||||||
|     To add the view into the routing you could use |     To add the view into the routing you could use | ||||||
|         1) app.add_route(DummyView(), '/') |         1) app.add_route(DummyView.as_view(), '/') | ||||||
|         2) app.route('/')(DummyView()) |         2) app.route('/')(DummyView.as_view()) | ||||||
|  |  | ||||||
|  |     To add any decorator you could set it into decorators variable | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def __call__(self, request, *args, **kwargs): |     decorators = [] | ||||||
|  |  | ||||||
|  |     def dispatch_request(self, request, *args, **kwargs): | ||||||
|         handler = getattr(self, request.method.lower(), None) |         handler = getattr(self, request.method.lower(), None) | ||||||
|         if handler: |         if handler: | ||||||
|             return handler(request, *args, **kwargs) |             return handler(request, *args, **kwargs) | ||||||
|         raise InvalidUsage( |         raise InvalidUsage( | ||||||
|             'Method {} not allowed for URL {}'.format( |             'Method {} not allowed for URL {}'.format( | ||||||
|                 request.method, request.url), status_code=405) |                 request.method, request.url), status_code=405) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def as_view(cls, *class_args, **class_kwargs): | ||||||
|  |         """ Converts the class into an actual view function that can be used | ||||||
|  |         with the routing system. | ||||||
|  |  | ||||||
|  |         """ | ||||||
|  |         def view(*args, **kwargs): | ||||||
|  |             self = view.view_class(*class_args, **class_kwargs) | ||||||
|  |             return self.dispatch_request(*args, **kwargs) | ||||||
|  |  | ||||||
|  |         if cls.decorators: | ||||||
|  |             view.__module__ = cls.__module__ | ||||||
|  |             for decorator in cls.decorators: | ||||||
|  |                 view = decorator(view) | ||||||
|  |  | ||||||
|  |         view.view_class = cls | ||||||
|  |         view.__doc__ = cls.__doc__ | ||||||
|  |         view.__module__ = cls.__module__ | ||||||
|  |         return view | ||||||
|   | |||||||
| @@ -59,6 +59,71 @@ def test_several_bp_with_url_prefix(): | |||||||
|     request, response = sanic_endpoint_test(app, uri='/test2/') |     request, response = sanic_endpoint_test(app, uri='/test2/') | ||||||
|     assert response.text == 'Hello2' |     assert response.text == 'Hello2' | ||||||
|  |  | ||||||
|  | def test_bp_with_host(): | ||||||
|  |     app = Sanic('test_bp_host') | ||||||
|  |     bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com") | ||||||
|  |  | ||||||
|  |     @bp.route('/') | ||||||
|  |     def handler(request): | ||||||
|  |         return text('Hello') | ||||||
|  |  | ||||||
|  |     @bp.route('/', host="sub.example.com") | ||||||
|  |     def handler(request): | ||||||
|  |         return text('Hello subdomain!') | ||||||
|  |  | ||||||
|  |     app.blueprint(bp) | ||||||
|  |     headers = {"Host": "example.com"} | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test1/', | ||||||
|  |                                             headers=headers) | ||||||
|  |     assert response.text == 'Hello' | ||||||
|  |  | ||||||
|  |     headers = {"Host": "sub.example.com"} | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test1/', | ||||||
|  |                                             headers=headers) | ||||||
|  |  | ||||||
|  |     assert response.text == 'Hello subdomain!' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_several_bp_with_host(): | ||||||
|  |     app = Sanic('test_text') | ||||||
|  |     bp = Blueprint('test_text', | ||||||
|  |                    url_prefix='/test', | ||||||
|  |                    host="example.com") | ||||||
|  |     bp2 = Blueprint('test_text2', | ||||||
|  |                     url_prefix='/test', | ||||||
|  |                     host="sub.example.com") | ||||||
|  |  | ||||||
|  |     @bp.route('/') | ||||||
|  |     def handler(request): | ||||||
|  |         return text('Hello') | ||||||
|  |  | ||||||
|  |     @bp2.route('/') | ||||||
|  |     def handler2(request): | ||||||
|  |         return text('Hello2') | ||||||
|  |  | ||||||
|  |     @bp2.route('/other/') | ||||||
|  |     def handler2(request): | ||||||
|  |         return text('Hello3') | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     app.blueprint(bp) | ||||||
|  |     app.blueprint(bp2) | ||||||
|  |  | ||||||
|  |     assert bp.host == "example.com" | ||||||
|  |     headers = {"Host": "example.com"} | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test/', | ||||||
|  |                                             headers=headers) | ||||||
|  |     assert response.text == 'Hello' | ||||||
|  |  | ||||||
|  |     assert bp2.host == "sub.example.com" | ||||||
|  |     headers = {"Host": "sub.example.com"} | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test/', | ||||||
|  |                                             headers=headers) | ||||||
|  |  | ||||||
|  |     assert response.text == 'Hello2' | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test/other/', | ||||||
|  |                                             headers=headers) | ||||||
|  |     assert response.text == 'Hello3' | ||||||
|  |  | ||||||
| def test_bp_middleware(): | def test_bp_middleware(): | ||||||
|     app = Sanic('test_middleware') |     app = Sanic('test_middleware') | ||||||
| @@ -162,4 +227,4 @@ def test_bp_static(): | |||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, uri='/testing.file') |     request, response = sanic_endpoint_test(app, uri='/testing.file') | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.body == current_file_contents |     assert response.body == current_file_contents | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								tests/test_custom_protocol.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								tests/test_custom_protocol.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.server import HttpProtocol | ||||||
|  | from sanic.response import text | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
|  | app = Sanic('test_custom_porotocol') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomHttpProtocol(HttpProtocol): | ||||||
|  |  | ||||||
|  |     def write_response(self, response): | ||||||
|  |         if isinstance(response, str): | ||||||
|  |             response = text(response) | ||||||
|  |         self.transport.write( | ||||||
|  |             response.output(self.request.version) | ||||||
|  |         ) | ||||||
|  |         self.transport.close() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.route('/1') | ||||||
|  | async def handler_1(request): | ||||||
|  |     return 'OK' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_use_custom_protocol(): | ||||||
|  |     server_kwargs = { | ||||||
|  |         'protocol': CustomHttpProtocol | ||||||
|  |     } | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/1', | ||||||
|  |                                             server_kwargs=server_kwargs) | ||||||
|  |     assert response.status == 200 | ||||||
|  |     assert response.text == 'OK' | ||||||
| @@ -1,51 +1,93 @@ | |||||||
|  | import pytest | ||||||
|  | from bs4 import BeautifulSoup | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
| from sanic.exceptions import InvalidUsage, ServerError, NotFound | from sanic.exceptions import InvalidUsage, ServerError, NotFound | ||||||
| from sanic.utils import sanic_endpoint_test | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # |  | ||||||
| #  GET |  | ||||||
| # ------------------------------------------------------------ # |  | ||||||
|  |  | ||||||
| exception_app = Sanic('test_exceptions') | class SanicExceptionTestException(Exception): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| @exception_app.route('/') | @pytest.fixture(scope='module') | ||||||
| def handler(request): | def exception_app(): | ||||||
|     return text('OK') |     app = Sanic('test_exceptions') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     def handler(request): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     @app.route('/error') | ||||||
|  |     def handler_error(request): | ||||||
|  |         raise ServerError("OK") | ||||||
|  |  | ||||||
|  |     @app.route('/404') | ||||||
|  |     def handler_404(request): | ||||||
|  |         raise NotFound("OK") | ||||||
|  |  | ||||||
|  |     @app.route('/invalid') | ||||||
|  |     def handler_invalid(request): | ||||||
|  |         raise InvalidUsage("OK") | ||||||
|  |  | ||||||
|  |     @app.route('/divide_by_zero') | ||||||
|  |     def handle_unhandled_exception(request): | ||||||
|  |         1 / 0 | ||||||
|  |  | ||||||
|  |     @app.route('/error_in_error_handler_handler') | ||||||
|  |     def custom_error_handler(request): | ||||||
|  |         raise SanicExceptionTestException('Dummy message!') | ||||||
|  |  | ||||||
|  |     @app.exception(SanicExceptionTestException) | ||||||
|  |     def error_in_error_handler_handler(request, exception): | ||||||
|  |         1 / 0 | ||||||
|  |  | ||||||
|  |     return app | ||||||
|  |  | ||||||
|  |  | ||||||
| @exception_app.route('/error') | def test_no_exception(exception_app): | ||||||
| def handler_error(request): |     """Test that a route works without an exception""" | ||||||
|     raise ServerError("OK") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @exception_app.route('/404') |  | ||||||
| def handler_404(request): |  | ||||||
|     raise NotFound("OK") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @exception_app.route('/invalid') |  | ||||||
| def handler_invalid(request): |  | ||||||
|     raise InvalidUsage("OK") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_no_exception(): |  | ||||||
|     request, response = sanic_endpoint_test(exception_app) |     request, response = sanic_endpoint_test(exception_app) | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.text == 'OK' |     assert response.text == 'OK' | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_server_error_exception(): | def test_server_error_exception(exception_app): | ||||||
|  |     """Test the built-in ServerError exception works""" | ||||||
|     request, response = sanic_endpoint_test(exception_app, uri='/error') |     request, response = sanic_endpoint_test(exception_app, uri='/error') | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_invalid_usage_exception(): | def test_invalid_usage_exception(exception_app): | ||||||
|  |     """Test the built-in InvalidUsage exception works""" | ||||||
|     request, response = sanic_endpoint_test(exception_app, uri='/invalid') |     request, response = sanic_endpoint_test(exception_app, uri='/invalid') | ||||||
|     assert response.status == 400 |     assert response.status == 400 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_not_found_exception(): | def test_not_found_exception(exception_app): | ||||||
|  |     """Test the built-in NotFound exception works""" | ||||||
|     request, response = sanic_endpoint_test(exception_app, uri='/404') |     request, response = sanic_endpoint_test(exception_app, uri='/404') | ||||||
|     assert response.status == 404 |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_handled_unhandled_exception(exception_app): | ||||||
|  |     """Test that an exception not built into sanic is handled""" | ||||||
|  |     request, response = sanic_endpoint_test( | ||||||
|  |         exception_app, uri='/divide_by_zero') | ||||||
|  |     assert response.status == 500 | ||||||
|  |     soup = BeautifulSoup(response.body, 'html.parser') | ||||||
|  |     assert soup.h1.text == 'Internal Server Error' | ||||||
|  |  | ||||||
|  |     message = " ".join(soup.p.text.split()) | ||||||
|  |     assert message == ( | ||||||
|  |         "The server encountered an internal error and " | ||||||
|  |         "cannot complete your request.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_exception_in_exception_handler(exception_app): | ||||||
|  |     """Test that an exception thrown in an error handler is handled""" | ||||||
|  |     request, response = sanic_endpoint_test( | ||||||
|  |         exception_app, uri='/error_in_error_handler_handler') | ||||||
|  |     assert response.status == 500 | ||||||
|  |     assert response.body == b'An error occurred while handling an error' | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ from sanic import Sanic | |||||||
| from sanic.response import text | from sanic.response import text | ||||||
| from sanic.exceptions import InvalidUsage, ServerError, NotFound | from sanic.exceptions import InvalidUsage, ServerError, NotFound | ||||||
| from sanic.utils import sanic_endpoint_test | from sanic.utils import sanic_endpoint_test | ||||||
|  | from bs4 import BeautifulSoup | ||||||
|  |  | ||||||
| exception_handler_app = Sanic('test_exception_handler') | exception_handler_app = Sanic('test_exception_handler') | ||||||
|  |  | ||||||
| @@ -21,6 +22,12 @@ def handler_3(request): | |||||||
|     raise NotFound("OK") |     raise NotFound("OK") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @exception_handler_app.route('/4') | ||||||
|  | def handler_4(request): | ||||||
|  |     foo = bar | ||||||
|  |     return text(foo) | ||||||
|  |  | ||||||
|  |  | ||||||
| @exception_handler_app.exception(NotFound, ServerError) | @exception_handler_app.exception(NotFound, ServerError) | ||||||
| def handler_exception(request, exception): | def handler_exception(request, exception): | ||||||
|     return text("OK") |     return text("OK") | ||||||
| @@ -47,3 +54,20 @@ def test_text_exception__handler(): | |||||||
|         exception_handler_app, uri='/random') |         exception_handler_app, uri='/random') | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.text == 'OK' |     assert response.text == 'OK' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_html_traceback_output_in_debug_mode(): | ||||||
|  |     request, response = sanic_endpoint_test( | ||||||
|  |         exception_handler_app, uri='/4', debug=True) | ||||||
|  |     assert response.status == 500 | ||||||
|  |     soup = BeautifulSoup(response.body, 'html.parser') | ||||||
|  |     html = str(soup) | ||||||
|  |  | ||||||
|  |     assert 'response = handler(request, *args, **kwargs)' in html | ||||||
|  |     assert 'handler_4' in html | ||||||
|  |     assert 'foo = bar' in html | ||||||
|  |  | ||||||
|  |     summary_text = " ".join(soup.select('.summary')[0].text.split()) | ||||||
|  |     assert ( | ||||||
|  |         "NameError: name 'bar' " | ||||||
|  |         "is not defined while handling uri /4") == summary_text | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								tests/test_logging.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								tests/test_logging.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | import asyncio | ||||||
|  | from sanic.response import text | ||||||
|  | from sanic import Sanic | ||||||
|  | from io import StringIO | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | logging_format = '''module: %(module)s; \ | ||||||
|  | function: %(funcName)s(); \ | ||||||
|  | message: %(message)s''' | ||||||
|  |  | ||||||
|  | def test_log(): | ||||||
|  |     log_stream = StringIO() | ||||||
|  |     for handler in logging.root.handlers[:]: | ||||||
|  |             logging.root.removeHandler(handler) | ||||||
|  |     logging.basicConfig( | ||||||
|  |         format=logging_format, | ||||||
|  |         level=logging.DEBUG, | ||||||
|  |         stream=log_stream | ||||||
|  |     ) | ||||||
|  |     log = logging.getLogger() | ||||||
|  |     app = Sanic('test_logging') | ||||||
|  |     @app.route('/') | ||||||
|  |     def handler(request): | ||||||
|  |         log.info('hello world') | ||||||
|  |         return text('hello') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |     log_text = log_stream.getvalue().strip().split('\n')[-3] | ||||||
|  |     assert log_text == "module: test_logging; function: handler(); message: hello world" | ||||||
|  |  | ||||||
|  | if __name__ =="__main__": | ||||||
|  |     test_log() | ||||||
| @@ -1,7 +1,9 @@ | |||||||
| from multiprocessing import Array, Event, Process | from multiprocessing import Array, Event, Process | ||||||
| from time import sleep | from time import sleep, time | ||||||
| from ujson import loads as json_loads | from ujson import loads as json_loads | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json | from sanic.response import json | ||||||
| from sanic.utils import local_request, HOST, PORT | from sanic.utils import local_request, HOST, PORT | ||||||
| @@ -13,8 +15,9 @@ from sanic.utils import local_request, HOST, PORT | |||||||
|  |  | ||||||
| # TODO: Figure out why this freezes on pytest but not when | # TODO: Figure out why this freezes on pytest but not when | ||||||
| # executed via interpreter | # executed via interpreter | ||||||
|  | @pytest.mark.skip( | ||||||
| def skip_test_multiprocessing(): |     reason="Freezes with pytest not on interpreter") | ||||||
|  | def test_multiprocessing(): | ||||||
|     app = Sanic('test_json') |     app = Sanic('test_json') | ||||||
|  |  | ||||||
|     response = Array('c', 50) |     response = Array('c', 50) | ||||||
| @@ -51,3 +54,28 @@ def skip_test_multiprocessing(): | |||||||
|         raise ValueError("Expected JSON response but got '{}'".format(response)) |         raise ValueError("Expected JSON response but got '{}'".format(response)) | ||||||
|  |  | ||||||
|     assert results.get('test') == True |     assert results.get('test') == True | ||||||
|  |  | ||||||
|  | @pytest.mark.skip( | ||||||
|  |     reason="Freezes with pytest not on interpreter") | ||||||
|  | def test_drain_connections(): | ||||||
|  |     app = Sanic('test_json') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     async def handler(request): | ||||||
|  |         return json({"test": True}) | ||||||
|  |  | ||||||
|  |     stop_event = Event() | ||||||
|  |     async def after_start(*args, **kwargs): | ||||||
|  |         http_response = await local_request('get', '/') | ||||||
|  |         stop_event.set() | ||||||
|  |  | ||||||
|  |     start = time() | ||||||
|  |     app.serve_multiple({ | ||||||
|  |         'host': HOST, | ||||||
|  |         'port': PORT, | ||||||
|  |         'after_start': after_start, | ||||||
|  |         'request_handler': app.handle_request, | ||||||
|  |     }, workers=2, stop_event=stop_event) | ||||||
|  |     end = time() | ||||||
|  |  | ||||||
|  |     assert end - start < 0.05 | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| from json import loads as json_loads, dumps as json_dumps | from json import loads as json_loads, dumps as json_dumps | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json, text | from sanic.response import json, text, redirect | ||||||
| from sanic.utils import sanic_endpoint_test | from sanic.utils import sanic_endpoint_test | ||||||
| from sanic.exceptions import ServerError | from sanic.exceptions import ServerError | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
| #  GET | #  GET | ||||||
| @@ -33,6 +34,31 @@ def test_text(): | |||||||
|     assert response.text == 'Hello' |     assert response.text == 'Hello' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_headers(): | ||||||
|  |     app = Sanic('test_text') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     async def handler(request): | ||||||
|  |         headers = {"spam": "great"} | ||||||
|  |         return text('Hello', headers=headers) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
|  |     assert response.headers.get('spam') == 'great' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_non_str_headers(): | ||||||
|  |     app = Sanic('test_text') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     async def handler(request): | ||||||
|  |         headers = {"answer": 42} | ||||||
|  |         return text('Hello', headers=headers) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
|  |     assert response.headers.get('answer') == '42' | ||||||
|  |      | ||||||
| def test_invalid_response(): | def test_invalid_response(): | ||||||
|     app = Sanic('test_invalid_response') |     app = Sanic('test_invalid_response') | ||||||
|  |  | ||||||
| @@ -47,8 +73,8 @@ def test_invalid_response(): | |||||||
|     request, response = sanic_endpoint_test(app) |     request, response = sanic_endpoint_test(app) | ||||||
|     assert response.status == 500 |     assert response.status == 500 | ||||||
|     assert response.text == "Internal Server Error." |     assert response.text == "Internal Server Error." | ||||||
|  |      | ||||||
|  |      | ||||||
| def test_json(): | def test_json(): | ||||||
|     app = Sanic('test_json') |     app = Sanic('test_json') | ||||||
|  |  | ||||||
| @@ -92,6 +118,24 @@ def test_query_string(): | |||||||
|     assert request.args.get('test2') == 'false' |     assert request.args.get('test2') == 'false' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_token(): | ||||||
|  |     app = Sanic('test_post_token') | ||||||
|  |  | ||||||
|  |     @app.route('/') | ||||||
|  |     async def handler(request): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     # uuid4 generated token. | ||||||
|  |     token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' | ||||||
|  |     headers = { | ||||||
|  |         'content-type': 'application/json', | ||||||
|  |         'Authorization': 'Token {}'.format(token) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, headers=headers) | ||||||
|  |  | ||||||
|  |     assert request.token == token | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
| #  POST | #  POST | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
| @@ -145,3 +189,73 @@ def test_post_form_multipart_form_data(): | |||||||
|     request, response = sanic_endpoint_test(app, data=payload, headers=headers) |     request, response = sanic_endpoint_test(app, data=payload, headers=headers) | ||||||
|  |  | ||||||
|     assert request.form.get('test') == 'OK' |     assert request.form.get('test') == 'OK' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture | ||||||
|  | def redirect_app(): | ||||||
|  |     app = Sanic('test_redirection') | ||||||
|  |  | ||||||
|  |     @app.route('/redirect_init') | ||||||
|  |     async def redirect_init(request): | ||||||
|  |         return redirect("/redirect_target") | ||||||
|  |  | ||||||
|  |     @app.route('/redirect_init_with_301') | ||||||
|  |     async def redirect_init_with_301(request): | ||||||
|  |         return redirect("/redirect_target", status=301) | ||||||
|  |  | ||||||
|  |     @app.route('/redirect_target') | ||||||
|  |     async def redirect_target(request): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     return app | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_redirect_default_302(redirect_app): | ||||||
|  |     """ | ||||||
|  |     We expect a 302 default status code and the headers to be set. | ||||||
|  |     """ | ||||||
|  |     request, response = sanic_endpoint_test( | ||||||
|  |         redirect_app, method="get", | ||||||
|  |         uri="/redirect_init", | ||||||
|  |         allow_redirects=False) | ||||||
|  |  | ||||||
|  |     assert response.status == 302 | ||||||
|  |     assert response.headers["Location"] == "/redirect_target" | ||||||
|  |     assert response.headers["Content-Type"] == 'text/html; charset=utf-8' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_redirect_headers_none(redirect_app): | ||||||
|  |     request, response = sanic_endpoint_test( | ||||||
|  |         redirect_app, method="get", | ||||||
|  |         uri="/redirect_init", | ||||||
|  |         headers=None, | ||||||
|  |         allow_redirects=False) | ||||||
|  |  | ||||||
|  |     assert response.status == 302 | ||||||
|  |     assert response.headers["Location"] == "/redirect_target" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_redirect_with_301(redirect_app): | ||||||
|  |     """ | ||||||
|  |     Test redirection with a different status code. | ||||||
|  |     """ | ||||||
|  |     request, response = sanic_endpoint_test( | ||||||
|  |         redirect_app, method="get", | ||||||
|  |         uri="/redirect_init_with_301", | ||||||
|  |         allow_redirects=False) | ||||||
|  |  | ||||||
|  |     assert response.status == 301 | ||||||
|  |     assert response.headers["Location"] == "/redirect_target" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_get_then_redirect_follow_redirect(redirect_app): | ||||||
|  |     """ | ||||||
|  |     With `allow_redirects` we expect a 200. | ||||||
|  |     """ | ||||||
|  |     response = sanic_endpoint_test( | ||||||
|  |         redirect_app, method="get", | ||||||
|  |         uri="/redirect_init", gather_request=False, | ||||||
|  |         allow_redirects=True) | ||||||
|  |  | ||||||
|  |     assert response.status == 200 | ||||||
|  |     assert response.text == 'OK' | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import pytest | |||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
| from sanic.router import RouteExists | from sanic.router import RouteExists, RouteDoesNotExist | ||||||
| from sanic.utils import sanic_endpoint_test | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -356,3 +356,110 @@ def test_add_route_method_not_allowed(): | |||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, method='post', uri='/test') |     request, response = sanic_endpoint_test(app, method='post', uri='/test') | ||||||
|     assert response.status == 405 |     assert response.status == 405 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_static_route(): | ||||||
|  |     app = Sanic('test_remove_static_route') | ||||||
|  |  | ||||||
|  |     async def handler1(request): | ||||||
|  |         return text('OK1') | ||||||
|  |  | ||||||
|  |     async def handler2(request): | ||||||
|  |         return text('OK2') | ||||||
|  |  | ||||||
|  |     app.add_route(handler1, '/test') | ||||||
|  |     app.add_route(handler2, '/test2') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test2') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route('/test') | ||||||
|  |     app.remove_route('/test2') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test2') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_dynamic_route(): | ||||||
|  |     app = Sanic('test_remove_dynamic_route') | ||||||
|  |  | ||||||
|  |     async def handler(request, name): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<name>') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test123') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route('/folder/<name>') | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test123') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_inexistent_route(): | ||||||
|  |     app = Sanic('test_remove_inexistent_route') | ||||||
|  |  | ||||||
|  |     with pytest.raises(RouteDoesNotExist): | ||||||
|  |         app.remove_route('/test') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_unhashable_route(): | ||||||
|  |     app = Sanic('test_remove_unhashable_route') | ||||||
|  |  | ||||||
|  |     async def handler(request, unhashable): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/end/') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_remove_route_without_clean_cache(): | ||||||
|  |     app = Sanic('test_remove_static_route') | ||||||
|  |  | ||||||
|  |     async def handler(request): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/test') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route('/test', clean_cache=True) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/test') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     app.remove_route('/test', clean_cache=False) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 200 | ||||||
|   | |||||||
							
								
								
									
										59
									
								
								tests/test_server_events.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								tests/test_server_events.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | from io import StringIO | ||||||
|  | from random import choice | ||||||
|  | from string import ascii_letters | ||||||
|  | import signal | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from sanic import Sanic | ||||||
|  |  | ||||||
|  | AVAILABLE_LISTENERS = [ | ||||||
|  |     'before_start', | ||||||
|  |     'after_start', | ||||||
|  |     'before_stop', | ||||||
|  |     'after_stop' | ||||||
|  | ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_listener(listener_name, in_list): | ||||||
|  |     async def _listener(app, loop): | ||||||
|  |         print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name)) | ||||||
|  |         in_list.insert(0, app.name + listener_name) | ||||||
|  |     return _listener | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def start_stop_app(random_name_app, **run_kwargs): | ||||||
|  |  | ||||||
|  |     def stop_on_alarm(signum, frame): | ||||||
|  |         raise KeyboardInterrupt('SIGINT for sanic to stop gracefully') | ||||||
|  |  | ||||||
|  |     signal.signal(signal.SIGALRM, stop_on_alarm) | ||||||
|  |     signal.alarm(1) | ||||||
|  |     try: | ||||||
|  |         random_name_app.run(**run_kwargs) | ||||||
|  |     except KeyboardInterrupt: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS) | ||||||
|  | def test_single_listener(listener_name): | ||||||
|  |     """Test that listeners on their own work""" | ||||||
|  |     random_name_app = Sanic(''.join( | ||||||
|  |         [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) | ||||||
|  |     output = list() | ||||||
|  |     start_stop_app( | ||||||
|  |         random_name_app, | ||||||
|  |         **{listener_name: create_listener(listener_name, output)}) | ||||||
|  |     assert random_name_app.name + listener_name == output.pop() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_all_listeners(): | ||||||
|  |     random_name_app = Sanic(''.join( | ||||||
|  |         [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) | ||||||
|  |     output = list() | ||||||
|  |     start_stop_app( | ||||||
|  |         random_name_app, | ||||||
|  |         **{listener_name: create_listener(listener_name, output) | ||||||
|  |            for listener_name in AVAILABLE_LISTENERS}) | ||||||
|  |     for listener_name in AVAILABLE_LISTENERS: | ||||||
|  |         assert random_name_app.name + listener_name == output.pop() | ||||||
							
								
								
									
										23
									
								
								tests/test_vhosts.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								tests/test_vhosts.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | from sanic import Sanic | ||||||
|  | from sanic.response import json, text | ||||||
|  | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_vhosts(): | ||||||
|  |     app = Sanic('test_text') | ||||||
|  |  | ||||||
|  |     @app.route('/', host="example.com") | ||||||
|  |     async def handler(request): | ||||||
|  |         return text("You're at example.com!") | ||||||
|  |  | ||||||
|  |     @app.route('/', host="subdomain.example.com") | ||||||
|  |     async def handler(request): | ||||||
|  |         return text("You're at subdomain.example.com!") | ||||||
|  |  | ||||||
|  |     headers = {"Host": "example.com"} | ||||||
|  |     request, response = sanic_endpoint_test(app, headers=headers) | ||||||
|  |     assert response.text == "You're at example.com!" | ||||||
|  |  | ||||||
|  |     headers = {"Host": "subdomain.example.com"} | ||||||
|  |     request, response = sanic_endpoint_test(app, headers=headers) | ||||||
|  |     assert response.text == "You're at subdomain.example.com!" | ||||||
| @@ -26,7 +26,7 @@ def test_methods(): | |||||||
|         def delete(self, request): |         def delete(self, request): | ||||||
|             return text('I am delete method') |             return text('I am delete method') | ||||||
|  |  | ||||||
|     app.add_route(DummyView(), '/') |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, method="get") |     request, response = sanic_endpoint_test(app, method="get") | ||||||
|     assert response.text == 'I am get method' |     assert response.text == 'I am get method' | ||||||
| @@ -48,7 +48,7 @@ def test_unexisting_methods(): | |||||||
|         def get(self, request): |         def get(self, request): | ||||||
|             return text('I am get method') |             return text('I am get method') | ||||||
|  |  | ||||||
|     app.add_route(DummyView(), '/') |     app.add_route(DummyView.as_view(), '/') | ||||||
|     request, response = sanic_endpoint_test(app, method="get") |     request, response = sanic_endpoint_test(app, method="get") | ||||||
|     assert response.text == 'I am get method' |     assert response.text == 'I am get method' | ||||||
|     request, response = sanic_endpoint_test(app, method="post") |     request, response = sanic_endpoint_test(app, method="post") | ||||||
| @@ -63,7 +63,7 @@ def test_argument_methods(): | |||||||
|         def get(self, request, my_param_here): |         def get(self, request, my_param_here): | ||||||
|             return text('I am get method with %s' % my_param_here) |             return text('I am get method with %s' % my_param_here) | ||||||
|  |  | ||||||
|     app.add_route(DummyView(), '/<my_param_here>') |     app.add_route(DummyView.as_view(), '/<my_param_here>') | ||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, uri='/test123') |     request, response = sanic_endpoint_test(app, uri='/test123') | ||||||
|  |  | ||||||
| @@ -79,7 +79,7 @@ def test_with_bp(): | |||||||
|         def get(self, request): |         def get(self, request): | ||||||
|             return text('I am get method') |             return text('I am get method') | ||||||
|  |  | ||||||
|     bp.add_route(DummyView(), '/') |     bp.add_route(DummyView.as_view(), '/') | ||||||
|  |  | ||||||
|     app.blueprint(bp) |     app.blueprint(bp) | ||||||
|     request, response = sanic_endpoint_test(app) |     request, response = sanic_endpoint_test(app) | ||||||
| @@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix(): | |||||||
|         def get(self, request): |         def get(self, request): | ||||||
|             return text('I am get method') |             return text('I am get method') | ||||||
|  |  | ||||||
|     bp.add_route(DummyView(), '/') |     bp.add_route(DummyView.as_view(), '/') | ||||||
|  |  | ||||||
|     app.blueprint(bp) |     app.blueprint(bp) | ||||||
|     request, response = sanic_endpoint_test(app, uri='/test1/') |     request, response = sanic_endpoint_test(app, uri='/test1/') | ||||||
| @@ -112,7 +112,7 @@ def test_with_middleware(): | |||||||
|         def get(self, request): |         def get(self, request): | ||||||
|             return text('I am get method') |             return text('I am get method') | ||||||
|  |  | ||||||
|     app.add_route(DummyView(), '/') |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |  | ||||||
|     results = [] |     results = [] | ||||||
|  |  | ||||||
| @@ -145,7 +145,7 @@ def test_with_middleware_response(): | |||||||
|         def get(self, request): |         def get(self, request): | ||||||
|             return text('I am get method') |             return text('I am get method') | ||||||
|  |  | ||||||
|     app.add_route(DummyView(), '/') |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app) |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
| @@ -153,3 +153,44 @@ def test_with_middleware_response(): | |||||||
|     assert type(results[0]) is Request |     assert type(results[0]) is Request | ||||||
|     assert type(results[1]) is Request |     assert type(results[1]) is Request | ||||||
|     assert issubclass(type(results[2]), HTTPResponse) |     assert issubclass(type(results[2]), HTTPResponse) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_with_custom_class_methods(): | ||||||
|  |     app = Sanic('test_with_custom_class_methods') | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |         global_var = 0 | ||||||
|  |  | ||||||
|  |         def _iternal_method(self): | ||||||
|  |             self.global_var += 10 | ||||||
|  |  | ||||||
|  |         def get(self, request): | ||||||
|  |             self._iternal_method() | ||||||
|  |             return text('I am get method and global var is {}'.format(self.global_var)) | ||||||
|  |  | ||||||
|  |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |     request, response = sanic_endpoint_test(app, method="get") | ||||||
|  |     assert response.text == 'I am get method and global var is 10' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_with_decorator(): | ||||||
|  |     app = Sanic('test_with_decorator') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     def stupid_decorator(view): | ||||||
|  |         def decorator(*args, **kwargs): | ||||||
|  |             results.append(1) | ||||||
|  |             return view(*args, **kwargs) | ||||||
|  |         return decorator | ||||||
|  |  | ||||||
|  |     class DummyView(HTTPMethodView): | ||||||
|  |         decorators = [stupid_decorator] | ||||||
|  |  | ||||||
|  |         def get(self, request): | ||||||
|  |             return text('I am get method') | ||||||
|  |  | ||||||
|  |     app.add_route(DummyView.as_view(), '/') | ||||||
|  |     request, response = sanic_endpoint_test(app, method="get") | ||||||
|  |     assert response.text == 'I am get method' | ||||||
|  |     assert results[0] == 1 | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,22 +1,22 @@ | |||||||
| [tox] | [tox] | ||||||
|  |  | ||||||
| envlist = py35, py36 | envlist = py35, py36, flake8 | ||||||
|  |  | ||||||
|  | [travis] | ||||||
|  |  | ||||||
|  | python = | ||||||
|  |     3.5: py35, flake8 | ||||||
|  |     3.6: py36, flake8 | ||||||
|  |  | ||||||
| [testenv] | [testenv] | ||||||
|  |  | ||||||
| deps = | deps = | ||||||
|     aiohttp |     aiohttp | ||||||
|     pytest |     pytest | ||||||
|     coverage |     beautifulsoup4 | ||||||
|  |  | ||||||
| commands = | commands = | ||||||
|     coverage run -m pytest -v tests {posargs} |     pytest tests {posargs} | ||||||
|     mv .coverage .coverage.{envname} |  | ||||||
|  |  | ||||||
| whitelist_externals = |  | ||||||
|     coverage |  | ||||||
|     mv |  | ||||||
|     echo |  | ||||||
|  |  | ||||||
| [testenv:flake8] | [testenv:flake8] | ||||||
| deps = | deps = | ||||||
| @@ -24,11 +24,3 @@ deps = | |||||||
|  |  | ||||||
| commands = | commands = | ||||||
|     flake8 sanic |     flake8 sanic | ||||||
|  |  | ||||||
| [testenv:report] |  | ||||||
|  |  | ||||||
| commands = |  | ||||||
|     coverage combine |  | ||||||
|     coverage report |  | ||||||
|     coverage html |  | ||||||
|     echo "Open file://{toxinidir}/coverage/index.html" |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Cadel Watson
					Cadel Watson