Merge branch 'master' into sphinx-docs
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,13 +1,15 @@ | ||||
| *~ | ||||
| *.egg-info | ||||
| *.egg | ||||
| *.eggs | ||||
| *.pyc | ||||
| .coverage | ||||
| .coverage.* | ||||
| coverage | ||||
| .tox | ||||
| settings.py | ||||
| *.pyc | ||||
| .idea/* | ||||
| .cache/* | ||||
| .python-version | ||||
| docs/_build/ | ||||
| docs/_api/ | ||||
| docs/_api/ | ||||
							
								
								
									
										11
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,15 +1,10 @@ | ||||
| sudo: false | ||||
| language: python | ||||
| python: | ||||
|   - '3.5' | ||||
|   - '3.6' | ||||
| install: | ||||
|   - pip install -r requirements.txt | ||||
|   - 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 | ||||
| install: pip install tox-travis | ||||
| script: tox | ||||
| deploy: | ||||
|   provider: pypi | ||||
|   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/ | ||||
| .. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg | ||||
|    :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): | ||||
|       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): | ||||
|     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.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.ip` (str) - IP address of the requester | ||||
|  | ||||
| See request.py for more information | ||||
|  | ||||
|   | ||||
| @@ -33,12 +33,12 @@ async def handler1(request): | ||||
| 	return text('OK') | ||||
| app.add_route(handler1, '/test') | ||||
|  | ||||
| async def handler(request, name): | ||||
| async def handler2(request, 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)) | ||||
| 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 | ||||
| # ----------------------------------------------- # | ||||
|  | ||||
| def after_start(loop): | ||||
| def after_start(app, loop): | ||||
|     log.info("OH OH OH OH OHHHHHHHH") | ||||
|  | ||||
|  | ||||
| def before_stop(loop): | ||||
| def before_stop(app, loop): | ||||
|     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 | ||||
| sphinx | ||||
| recommonmark | ||||
| beautifulsoup4 | ||||
|   | ||||
| @@ -2,4 +2,3 @@ httptools | ||||
| ujson | ||||
| uvloop | ||||
| aiofiles | ||||
| multidict | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| from .sanic import Sanic | ||||
| from .blueprints import Blueprint | ||||
|  | ||||
| __version__ = '0.1.9' | ||||
| __version__ = '0.2.0' | ||||
|  | ||||
| __all__ = ['Sanic', 'Blueprint'] | ||||
|   | ||||
| @@ -20,7 +20,7 @@ if __name__ == "__main__": | ||||
|  | ||||
|         module = import_module(module_name) | ||||
|         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 {}.  " | ||||
|                              "Perhaps you meant {}.app?" | ||||
|                              .format(type(app).__name__, args.module)) | ||||
|   | ||||
| @@ -18,14 +18,17 @@ class BlueprintSetup: | ||||
|         #: blueprint. | ||||
|         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. | ||||
|         """ | ||||
|         if self.url_prefix: | ||||
|             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): | ||||
|         """ | ||||
| @@ -53,7 +56,7 @@ class BlueprintSetup: | ||||
|  | ||||
|  | ||||
| class Blueprint: | ||||
|     def __init__(self, name, url_prefix=None): | ||||
|     def __init__(self, name, url_prefix=None, host=None): | ||||
|         """ | ||||
|         Creates a new blueprint | ||||
|         :param name: Unique name of the blueprint | ||||
| @@ -63,6 +66,7 @@ class Blueprint: | ||||
|         self.url_prefix = url_prefix | ||||
|         self.deferred_functions = [] | ||||
|         self.listeners = defaultdict(list) | ||||
|         self.host = host | ||||
|  | ||||
|     def record(self, func): | ||||
|         """ | ||||
| @@ -83,18 +87,18 @@ class Blueprint: | ||||
|         for deferred in self.deferred_functions: | ||||
|             deferred(state) | ||||
|  | ||||
|     def route(self, uri, methods=None): | ||||
|     def route(self, uri, methods=None, host=None): | ||||
|         """ | ||||
|         """ | ||||
|         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 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 | ||||
|  | ||||
|     def listener(self, event): | ||||
|   | ||||
| @@ -1,5 +1,104 @@ | ||||
| from .response import text | ||||
| from traceback import format_exc | ||||
| from .response import text, html | ||||
| 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): | ||||
| @@ -45,6 +144,21 @@ class Handler: | ||||
|         self.handlers = {} | ||||
|         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): | ||||
|         self.handlers[exception] = handler | ||||
|  | ||||
| @@ -57,18 +171,32 @@ class Handler: | ||||
|         :return: Response object | ||||
|         """ | ||||
|         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 | ||||
|  | ||||
|     def default(self, request, exception): | ||||
|         if issubclass(type(exception), SanicException): | ||||
|             return text( | ||||
|                 "Error: {}".format(exception), | ||||
|                 'Error: {}'.format(exception), | ||||
|                 status=getattr(exception, 'status_code', 500)) | ||||
|         elif self.sanic.debug: | ||||
|             return text( | ||||
|                 "Error: {}\nException: {}".format( | ||||
|                     exception, format_exc()), status=500) | ||||
|             html_output = self._render_traceback_html(exception, request) | ||||
|  | ||||
|             response_message = ( | ||||
|                 'Exception occurred while handling uri: "{}"\n{}'.format( | ||||
|                     request.url, format_exc())) | ||||
|             log.error(response_message) | ||||
|             return html(html_output, status=500) | ||||
|         else: | ||||
|             return text( | ||||
|                 "An error occurred while generating the request", status=500) | ||||
|             return html(INTERNAL_SERVER_ERROR_HTML, status=500) | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| import logging | ||||
|  | ||||
| logging.basicConfig( | ||||
|     level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") | ||||
| log = logging.getLogger(__name__) | ||||
|   | ||||
| @@ -25,6 +25,9 @@ class RequestParameters(dict): | ||||
|         self.super = super() | ||||
|         self.super.__init__(*args, **kwargs) | ||||
|  | ||||
|     def __getitem__(self, name): | ||||
|         return self.get(name) | ||||
|  | ||||
|     def get(self, name, default=None): | ||||
|         values = self.super.get(name) | ||||
|         return values[0] if values else default | ||||
| @@ -38,18 +41,20 @@ class Request(dict): | ||||
|     Properties of an HTTP request such as URL, headers, etc. | ||||
|     """ | ||||
|     __slots__ = ( | ||||
|         'url', 'headers', 'version', 'method', '_cookies', | ||||
|         'url', 'headers', 'version', 'method', '_cookies', 'transport', | ||||
|         'query_string', 'body', | ||||
|         '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 | ||||
|         url_parsed = parse_url(url_bytes) | ||||
|         self.url = url_parsed.path.decode('utf-8') | ||||
|         self.headers = headers | ||||
|         self.version = version | ||||
|         self.method = method | ||||
|         self.transport = transport | ||||
|         self.query_string = None | ||||
|         if url_parsed.query: | ||||
|             self.query_string = url_parsed.query.decode('utf-8') | ||||
| @@ -64,7 +69,7 @@ class Request(dict): | ||||
|  | ||||
|     @property | ||||
|     def json(self): | ||||
|         if not self.parsed_json: | ||||
|         if self.parsed_json is None: | ||||
|             try: | ||||
|                 self.parsed_json = json_loads(self.body) | ||||
|             except Exception: | ||||
| @@ -72,6 +77,17 @@ class Request(dict): | ||||
|  | ||||
|         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 | ||||
|     def form(self): | ||||
|         if self.parsed_form is None: | ||||
| @@ -125,6 +141,12 @@ class Request(dict): | ||||
|                 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']) | ||||
|  | ||||
|   | ||||
| @@ -83,10 +83,10 @@ class HTTPResponse: | ||||
|         if body is not None: | ||||
|             try: | ||||
|                 # Try to encode it regularly | ||||
|                 self.body = body.encode('utf-8') | ||||
|                 self.body = body.encode() | ||||
|             except AttributeError: | ||||
|                 # Convert it to a str if you can't | ||||
|                 self.body = str(body).encode('utf-8') | ||||
|                 self.body = str(body).encode() | ||||
|         else: | ||||
|             self.body = body_bytes | ||||
|  | ||||
| @@ -103,10 +103,14 @@ class HTTPResponse: | ||||
|  | ||||
|         headers = b'' | ||||
|         if self.headers: | ||||
|             headers = b''.join( | ||||
|                 b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) | ||||
|                 for name, value in self.headers.items() | ||||
|             ) | ||||
|             for name, value in self.headers.items(): | ||||
|                 try: | ||||
|                     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 | ||||
|         # Speeds up response rate 6% over pulling from all | ||||
| @@ -165,3 +169,26 @@ async def file(location, mime_type=None, headers=None): | ||||
|                         headers=headers, | ||||
|                         content_type=mime_type, | ||||
|                         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 | ||||
|  | ||||
|  | ||||
| class RouteDoesNotExist(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class Router: | ||||
|     """ | ||||
|     Router supports basic routing with parameters and method checks | ||||
| @@ -31,16 +35,16 @@ class Router: | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         @sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...]) | ||||
|         def my_route(request, my_parameter): | ||||
|         @sanic.route('/my/url/<my_param>', methods=['GET', 'POST', ...]) | ||||
|         def my_route(request, my_param): | ||||
|             do stuff... | ||||
|  | ||||
|     or | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         @sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...]) | ||||
|         def my_route_with_type(request, my_parameter): | ||||
|         @sanic.route('/my/url/<my_param:my_type>', methods['GET', 'POST', ...]) | ||||
|         def my_route_with_type(request, my_param: my_type): | ||||
|             do stuff... | ||||
|  | ||||
|     Parameters will be passed as keyword arguments to the request handling | ||||
| @@ -59,8 +63,9 @@ class Router: | ||||
|         self.routes_static = {} | ||||
|         self.routes_dynamic = defaultdict(list) | ||||
|         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 | ||||
|  | ||||
| @@ -71,6 +76,17 @@ class Router: | ||||
|                         When executed, it should provide a response object. | ||||
|         :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: | ||||
|             raise RouteExists("Route already registered: {}".format(uri)) | ||||
|  | ||||
| @@ -118,6 +134,25 @@ class Router: | ||||
|         else: | ||||
|             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): | ||||
|         """ | ||||
|         Gets a request handler based on the URL of the request, or raises an | ||||
| @@ -126,10 +161,14 @@ class Router: | ||||
|         :param request: Request object | ||||
|         :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) | ||||
|     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 | ||||
|         error.  Internal method for caching. | ||||
| @@ -137,6 +176,7 @@ class Router: | ||||
|         :param method: Request method | ||||
|         :return: handler, arguments, keyword arguments | ||||
|         """ | ||||
|         url = host + url | ||||
|         # Check against known static routes | ||||
|         route = self.routes_static.get(url) | ||||
|         if route: | ||||
|   | ||||
| @@ -4,21 +4,24 @@ from functools import partial | ||||
| from inspect import isawaitable, stack, getmodulename | ||||
| from multiprocessing import Process, Event | ||||
| from signal import signal, SIGTERM, SIGINT | ||||
| from time import sleep | ||||
| from traceback import format_exc | ||||
| import logging | ||||
|  | ||||
| from .config import Config | ||||
| from .exceptions import Handler | ||||
| from .log import log, logging | ||||
| from .log import log | ||||
| from .response import HTTPResponse | ||||
| from .router import Router | ||||
| from .server import serve | ||||
| from .server import serve, HttpProtocol | ||||
| from .static import register as static_register | ||||
| from .exceptions import ServerError | ||||
| from socket import socket, SOL_SOCKET, SO_REUSEADDR | ||||
| from os import set_inheritable | ||||
|  | ||||
|  | ||||
| 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: | ||||
|             frame_records = stack()[1] | ||||
|             name = getmodulename(frame_records[1]) | ||||
| @@ -32,6 +35,8 @@ class Sanic: | ||||
|         self._blueprint_order = [] | ||||
|         self.loop = None | ||||
|         self.debug = None | ||||
|         self.sock = None | ||||
|         self.processes = None | ||||
|  | ||||
|         # Register alternative method names | ||||
|         self.go_fast = self.run | ||||
| @@ -41,7 +46,7 @@ class Sanic: | ||||
|     # -------------------------------------------------------------------- # | ||||
|  | ||||
|     # Decorator | ||||
|     def route(self, uri, methods=None): | ||||
|     def route(self, uri, methods=None, host=None): | ||||
|         """ | ||||
|         Decorates a function to be registered as a route | ||||
|  | ||||
| @@ -56,12 +61,13 @@ class Sanic: | ||||
|             uri = '/' + uri | ||||
|  | ||||
|         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 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 | ||||
|         functions as a handler to the application url | ||||
| @@ -72,9 +78,12 @@ class Sanic: | ||||
|         :param methods: list or tuple of methods allowed | ||||
|         :return: function or class instance | ||||
|         """ | ||||
|         self.route(uri=uri, methods=methods)(handler) | ||||
|         self.route(uri=uri, methods=methods, host=host)(handler) | ||||
|         return handler | ||||
|  | ||||
|     def remove_route(self, uri, clean_cache=True, host=None): | ||||
|         self.router.remove(uri, clean_cache, host) | ||||
|  | ||||
|     # Decorator | ||||
|     def exception(self, *exceptions): | ||||
|         """ | ||||
| @@ -144,7 +153,8 @@ class Sanic: | ||||
|     def register_blueprint(self, *args, **kwargs): | ||||
|         # TODO: deprecate 1.0 | ||||
|         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) | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
| @@ -237,7 +247,8 @@ class Sanic: | ||||
|  | ||||
|     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, | ||||
|             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 | ||||
|         signal. On termination, drains connections before closing. | ||||
| @@ -245,25 +256,32 @@ class Sanic: | ||||
|         :param host: Address to host on | ||||
|         :param port: Port to host on | ||||
|         :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 | ||||
|         :param after_start: Function to be executed after the server starts | ||||
|         :param after_start: Functions to be executed after the server starts | ||||
|                             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 | ||||
|         :param after_stop: Function to be executed when all requests are | ||||
|         :param after_stop: Functions to be executed when all requests are | ||||
|                            complete | ||||
|  | ||||
|         :param sock: Socket for the server to accept connections from | ||||
|         :param workers: Number of processes | ||||
|                         received before it is respected | ||||
|         :param loop: asyncio compatible event loop | ||||
|         :param protocol: Subclass of asyncio protocol class | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         logging.basicConfig( | ||||
|             level=logging.INFO, | ||||
|             format="%(asctime)s: %(levelname)s: %(message)s" | ||||
|         ) | ||||
|         self.error_handler.debug = True | ||||
|         self.debug = debug | ||||
|         self.loop = loop | ||||
|  | ||||
|         server_settings = { | ||||
|             'protocol': protocol, | ||||
|             'host': host, | ||||
|             'port': port, | ||||
|             'sock': sock, | ||||
| @@ -272,7 +290,8 @@ class Sanic: | ||||
|             'error_handler': self.error_handler, | ||||
|             'request_timeout': self.config.REQUEST_TIMEOUT, | ||||
|             '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(): | ||||
|                 listeners += blueprint.listeners[event_name] | ||||
|             if args: | ||||
|                 if type(args) is not list: | ||||
|                 if callable(args): | ||||
|                     args = [args] | ||||
|                 listeners += args | ||||
|             if reverse: | ||||
| @@ -311,7 +330,7 @@ class Sanic: | ||||
|             else: | ||||
|                 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: | ||||
|             log.exception( | ||||
| @@ -323,10 +342,13 @@ class 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() | ||||
|  | ||||
|     @staticmethod | ||||
|     def serve_multiple(server_settings, workers, stop_event=None): | ||||
|     def serve_multiple(self, server_settings, workers, stop_event=None): | ||||
|         """ | ||||
|         Starts multiple server processes simultaneously.  Stops on interrupt | ||||
|         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 | ||||
|         :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 | ||||
|  | ||||
|         # Create a stop event to be triggered by a signal | ||||
|         if not stop_event: | ||||
|         if stop_event is None: | ||||
|             stop_event = Event() | ||||
|         signal(SIGINT, 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): | ||||
|             process = Process(target=serve, kwargs=server_settings) | ||||
|             process.daemon = True | ||||
|             process.start() | ||||
|             processes.append(process) | ||||
|             self.processes.append(process) | ||||
|  | ||||
|         # Infinitely wait for the stop event | ||||
|         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: | ||||
|         for process in self.processes: | ||||
|             process.join() | ||||
|  | ||||
|         # the above processes will block this until they're stopped | ||||
|         self.stop() | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import asyncio | ||||
| import traceback | ||||
| from functools import partial | ||||
| from inspect import isawaitable | ||||
| from multidict import CIMultiDict | ||||
| from signal import SIGINT, SIGTERM | ||||
| from time import time | ||||
| from httptools import HttpRequestParser | ||||
| @@ -18,11 +18,30 @@ from .request import Request | ||||
| from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage | ||||
|  | ||||
|  | ||||
| current_time = None | ||||
|  | ||||
|  | ||||
| class Signal: | ||||
|     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): | ||||
| @@ -118,18 +137,15 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             exception = PayloadTooLarge('Payload Too Large') | ||||
|             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): | ||||
|         remote_addr = self.transport.get_extra_info('peername') | ||||
|         if remote_addr: | ||||
|             self.headers.append(('Remote-Addr', '%s:%s' % remote_addr)) | ||||
|  | ||||
|         self.request = Request( | ||||
|             url_bytes=self.url, | ||||
|             headers=CIMultiDict(self.headers), | ||||
|             headers=CIDict(self.headers), | ||||
|             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): | ||||
| @@ -174,9 +190,15 @@ class HttpProtocol(asyncio.Protocol): | ||||
|                 "Writing error failed, connection closed {}".format(e)) | ||||
|  | ||||
|     def bail_out(self, message): | ||||
|         exception = ServerError(message) | ||||
|         self.write_error(exception) | ||||
|         log.error(message) | ||||
|         if self.transport.is_closing(): | ||||
|             log.error( | ||||
|                 "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): | ||||
|         self.parser = None | ||||
| @@ -225,26 +247,33 @@ def trigger_events(events, loop): | ||||
|  | ||||
|  | ||||
| def serve(host, port, request_handler, error_handler, before_start=None, | ||||
|           after_start=None, before_stop=None, after_stop=None, | ||||
|           debug=False, request_timeout=60, sock=None, | ||||
|           request_max_size=None, reuse_port=False, loop=None): | ||||
|           after_start=None, before_stop=None, after_stop=None, debug=False, | ||||
|           request_timeout=60, sock=None, request_max_size=None, | ||||
|           reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100): | ||||
|     """ | ||||
|     Starts asynchronous HTTP Server on an individual process. | ||||
|  | ||||
|     :param host: Address to host on | ||||
|     :param port: Port to host on | ||||
|     :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 | ||||
|                         listening. Takes single argument `loop` | ||||
|     :param before_stop: Function to be executed when a stop signal is | ||||
|                         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 request_timeout: time in seconds | ||||
|     :param sock: Socket for the server to accept connections from | ||||
|     :param request_max_size: size in bytes, `None` for no limit | ||||
|     :param reuse_port: `True` for multiple workers | ||||
|     :param loop: asyncio compatible event loop | ||||
|     :param protocol: Subclass of asyncio protocol class | ||||
|     :return: Nothing | ||||
|     """ | ||||
|     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() | ||||
|     signal = Signal() | ||||
|     server = partial( | ||||
|         HttpProtocol, | ||||
|         protocol, | ||||
|         loop=loop, | ||||
|         connections=connections, | ||||
|         signal=signal, | ||||
| @@ -273,7 +302,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, | ||||
|         host, | ||||
|         port, | ||||
|         reuse_port=reuse_port, | ||||
|         sock=sock | ||||
|         sock=sock, | ||||
|         backlog=backlog | ||||
|     ) | ||||
|  | ||||
|     # 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, | ||||
|                         loop=None, debug=False, *request_args, | ||||
|                         **request_kwargs): | ||||
|                         loop=None, debug=False, server_kwargs={}, | ||||
|                         *request_args, **request_kwargs): | ||||
|     results = [] | ||||
|     exceptions = [] | ||||
|  | ||||
|     if gather_request: | ||||
|         @app.middleware | ||||
|         def _collect_request(request): | ||||
|             results.append(request) | ||||
|         app.request_middleware.appendleft(_collect_request) | ||||
|  | ||||
|     async def _collect_response(sanic, loop): | ||||
|         try: | ||||
| @@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | ||||
|             exceptions.append(e) | ||||
|         app.stop() | ||||
|  | ||||
|     app.run(host=HOST, debug=debug, port=42101, | ||||
|             after_start=_collect_response, loop=loop) | ||||
|     app.run(host=HOST, debug=debug, port=PORT, | ||||
|             after_start=_collect_response, loop=loop, **server_kwargs) | ||||
|  | ||||
|     if exceptions: | ||||
|         raise ValueError("Exception during request: {}".format(exceptions)) | ||||
|   | ||||
| @@ -10,7 +10,7 @@ class HTTPMethodView: | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         class DummyView(View): | ||||
|         class DummyView(HTTPMethodView): | ||||
|             def get(self, request, *args, **kwargs): | ||||
|                 return text('I am get method') | ||||
|             def put(self, request, *args, **kwargs): | ||||
| @@ -25,19 +25,43 @@ class HTTPMethodView: | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         class DummyView(View): | ||||
|         class DummyView(HTTPMethodView): | ||||
|             def get(self, request, my_param_here, *args, **kwargs): | ||||
|                 return text('I am get method with %s' % my_param_here) | ||||
|  | ||||
|     To add the view into the routing you could use | ||||
|         1) app.add_route(DummyView(), '/') | ||||
|         2) app.route('/')(DummyView()) | ||||
|         1) app.add_route(DummyView.as_view(), '/') | ||||
|         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) | ||||
|         if handler: | ||||
|             return handler(request, *args, **kwargs) | ||||
|         raise InvalidUsage( | ||||
|             'Method {} not allowed for URL {}'.format( | ||||
|                 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/') | ||||
|     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(): | ||||
|     app = Sanic('test_middleware') | ||||
| @@ -162,4 +227,4 @@ def test_bp_static(): | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/testing.file') | ||||
|     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.response import text | ||||
| from sanic.exceptions import InvalidUsage, ServerError, NotFound | ||||
| from sanic.utils import sanic_endpoint_test | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  GET | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
| exception_app = Sanic('test_exceptions') | ||||
| class SanicExceptionTestException(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| @exception_app.route('/') | ||||
| def handler(request): | ||||
|     return text('OK') | ||||
| @pytest.fixture(scope='module') | ||||
| def exception_app(): | ||||
|     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 handler_error(request): | ||||
|     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(): | ||||
| def test_no_exception(exception_app): | ||||
|     """Test that a route works without an exception""" | ||||
|     request, response = sanic_endpoint_test(exception_app) | ||||
|     assert response.status == 200 | ||||
|     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') | ||||
|     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') | ||||
|     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') | ||||
|     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.exceptions import InvalidUsage, ServerError, NotFound | ||||
| from sanic.utils import sanic_endpoint_test | ||||
| from bs4 import BeautifulSoup | ||||
|  | ||||
| exception_handler_app = Sanic('test_exception_handler') | ||||
|  | ||||
| @@ -21,6 +22,12 @@ def handler_3(request): | ||||
|     raise NotFound("OK") | ||||
|  | ||||
|  | ||||
| @exception_handler_app.route('/4') | ||||
| def handler_4(request): | ||||
|     foo = bar | ||||
|     return text(foo) | ||||
|  | ||||
|  | ||||
| @exception_handler_app.exception(NotFound, ServerError) | ||||
| def handler_exception(request, exception): | ||||
|     return text("OK") | ||||
| @@ -47,3 +54,20 @@ def test_text_exception__handler(): | ||||
|         exception_handler_app, uri='/random') | ||||
|     assert response.status == 200 | ||||
|     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 time import sleep | ||||
| from time import sleep, time | ||||
| from ujson import loads as json_loads | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
| 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 | ||||
| # executed via interpreter | ||||
|  | ||||
| def skip_test_multiprocessing(): | ||||
| @pytest.mark.skip( | ||||
|     reason="Freezes with pytest not on interpreter") | ||||
| def test_multiprocessing(): | ||||
|     app = Sanic('test_json') | ||||
|  | ||||
|     response = Array('c', 50) | ||||
| @@ -51,3 +54,28 @@ def skip_test_multiprocessing(): | ||||
|         raise ValueError("Expected JSON response but got '{}'".format(response)) | ||||
|  | ||||
|     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 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.exceptions import ServerError | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  GET | ||||
| @@ -33,6 +34,31 @@ def test_text(): | ||||
|     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(): | ||||
|     app = Sanic('test_invalid_response') | ||||
|  | ||||
| @@ -47,8 +73,8 @@ def test_invalid_response(): | ||||
|     request, response = sanic_endpoint_test(app) | ||||
|     assert response.status == 500 | ||||
|     assert response.text == "Internal Server Error." | ||||
|  | ||||
|  | ||||
|      | ||||
|      | ||||
| def test_json(): | ||||
|     app = Sanic('test_json') | ||||
|  | ||||
| @@ -92,6 +118,24 @@ def test_query_string(): | ||||
|     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 | ||||
| # ------------------------------------------------------------ # | ||||
| @@ -145,3 +189,73 @@ def test_post_form_multipart_form_data(): | ||||
|     request, response = sanic_endpoint_test(app, data=payload, headers=headers) | ||||
|  | ||||
|     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.response import text | ||||
| from sanic.router import RouteExists | ||||
| from sanic.router import RouteExists, RouteDoesNotExist | ||||
| 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') | ||||
|     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): | ||||
|             return text('I am delete method') | ||||
|  | ||||
|     app.add_route(DummyView(), '/') | ||||
|     app.add_route(DummyView.as_view(), '/') | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, method="get") | ||||
|     assert response.text == 'I am get method' | ||||
| @@ -48,7 +48,7 @@ def test_unexisting_methods(): | ||||
|         def get(self, request): | ||||
|             return text('I am get method') | ||||
|  | ||||
|     app.add_route(DummyView(), '/') | ||||
|     app.add_route(DummyView.as_view(), '/') | ||||
|     request, response = sanic_endpoint_test(app, method="get") | ||||
|     assert response.text == 'I am get method' | ||||
|     request, response = sanic_endpoint_test(app, method="post") | ||||
| @@ -63,7 +63,7 @@ def test_argument_methods(): | ||||
|         def get(self, request, 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') | ||||
|  | ||||
| @@ -79,7 +79,7 @@ def test_with_bp(): | ||||
|         def get(self, request): | ||||
|             return text('I am get method') | ||||
|  | ||||
|     bp.add_route(DummyView(), '/') | ||||
|     bp.add_route(DummyView.as_view(), '/') | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|     request, response = sanic_endpoint_test(app) | ||||
| @@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix(): | ||||
|         def get(self, request): | ||||
|             return text('I am get method') | ||||
|  | ||||
|     bp.add_route(DummyView(), '/') | ||||
|     bp.add_route(DummyView.as_view(), '/') | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|     request, response = sanic_endpoint_test(app, uri='/test1/') | ||||
| @@ -112,7 +112,7 @@ def test_with_middleware(): | ||||
|         def get(self, request): | ||||
|             return text('I am get method') | ||||
|  | ||||
|     app.add_route(DummyView(), '/') | ||||
|     app.add_route(DummyView.as_view(), '/') | ||||
|  | ||||
|     results = [] | ||||
|  | ||||
| @@ -145,7 +145,7 @@ def test_with_middleware_response(): | ||||
|         def get(self, request): | ||||
|             return text('I am get method') | ||||
|  | ||||
|     app.add_route(DummyView(), '/') | ||||
|     app.add_route(DummyView.as_view(), '/') | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app) | ||||
|  | ||||
| @@ -153,3 +153,44 @@ def test_with_middleware_response(): | ||||
|     assert type(results[0]) is Request | ||||
|     assert type(results[1]) is Request | ||||
|     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] | ||||
|  | ||||
| envlist = py35, py36 | ||||
| envlist = py35, py36, flake8 | ||||
|  | ||||
| [travis] | ||||
|  | ||||
| python = | ||||
|     3.5: py35, flake8 | ||||
|     3.6: py36, flake8 | ||||
|  | ||||
| [testenv] | ||||
|  | ||||
| deps = | ||||
|     aiohttp | ||||
|     pytest | ||||
|     coverage | ||||
|     beautifulsoup4 | ||||
|  | ||||
| commands = | ||||
|     coverage run -m pytest -v tests {posargs} | ||||
|     mv .coverage .coverage.{envname} | ||||
|  | ||||
| whitelist_externals = | ||||
|     coverage | ||||
|     mv | ||||
|     echo | ||||
|     pytest tests {posargs} | ||||
|  | ||||
| [testenv:flake8] | ||||
| deps = | ||||
| @@ -24,11 +24,3 @@ deps = | ||||
|  | ||||
| commands = | ||||
|     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