Merge branch 'master' of github.com:huge-success/sanic into config_from_object_string
This commit is contained in:
commit
eacf78b83c
36
CHANGELOG.md
36
CHANGELOG.md
|
@ -1,3 +1,39 @@
|
|||
Version 18.12
|
||||
-------------
|
||||
18.12.0
|
||||
- Changes:
|
||||
- Improved codebase test coverage from 81% to 91%.
|
||||
- Added stream_large_files and host examples in static_file document
|
||||
- Added methods to append and finish body content on Request (#1379)
|
||||
- Integrated with .appveyor.yml for windows ci support
|
||||
- Added documentation for AF_INET6 and AF_UNIX socket usage
|
||||
- Adopt black/isort for codestyle
|
||||
- Cancel task when connection_lost
|
||||
- Simplify request ip and port retrieval logic
|
||||
- Handle config error in load config file.
|
||||
- Integrate with codecov for CI
|
||||
- Add missed documentation for config section.
|
||||
- Deprecate Handler.log
|
||||
- Pinned httptools requirement to version 0.0.10+
|
||||
|
||||
- Fixes:
|
||||
- Fix `remove_entity_headers` helper function (#1415)
|
||||
- Fix TypeError when use Blueprint.group() to group blueprint with default url_prefix, Use os.path.normpath to avoid invalid url_prefix like api//v1
|
||||
f8a6af1 Rename the `http` module to `helpers` to prevent conflicts with the built-in Python http library (fixes #1323)
|
||||
- Fix unittests on windows
|
||||
- Fix Namespacing of sanic logger
|
||||
- Fix missing quotes in decorator example
|
||||
- Fix redirect with quoted param
|
||||
- Fix doc for latest blueprint code
|
||||
- Fix build of latex documentation relating to markdown lists
|
||||
- Fix loop exception handling in app.py
|
||||
- Fix content length mismatch in windows and other platform
|
||||
- Fix Range header handling for static files (#1402)
|
||||
- Fix the logger and make it work (#1397)
|
||||
- Fix type pikcle->pickle in multiprocessing test
|
||||
- Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
|
||||
- Fix document for logging
|
||||
|
||||
Version 0.8
|
||||
-----------
|
||||
0.8.3
|
||||
|
|
|
@ -45,7 +45,7 @@ Sanic | Build fast. Run fast.
|
|||
|
||||
.. end-badges
|
||||
|
||||
Sanic is a Python web server and web framework that's written to go fast. It allows usage the `async` and `await` syntax added in Python 3.5, which makes your code non-blocking and speedy.
|
||||
Sanic is a Python web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
|
||||
|
||||
`Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_.
|
||||
|
||||
|
@ -111,6 +111,11 @@ Documentation
|
|||
|
||||
`Documentation on Readthedocs <http://sanic.readthedocs.io/>`_.
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
`Release Changelogs <https://github.com/huge-success/sanic/blob/master/CHANGELOG.md>`_.
|
||||
|
||||
|
||||
Questions and Discussion
|
||||
------------------------
|
||||
|
|
|
@ -29,9 +29,11 @@ Guides
|
|||
sanic/testing
|
||||
sanic/deploying
|
||||
sanic/extensions
|
||||
sanic/examples
|
||||
sanic/changelog
|
||||
sanic/contributing
|
||||
sanic/api_reference
|
||||
sanic/examples
|
||||
sanic/asyncio_python37
|
||||
|
||||
|
||||
Module Documentation
|
||||
|
|
|
@ -20,6 +20,15 @@ sanic.blueprints module
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.blueprint_group module
|
||||
----------------------------
|
||||
|
||||
.. automodule:: sanic.blueprint_group
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
sanic.config module
|
||||
-------------------
|
||||
|
||||
|
|
58
docs/sanic/asyncio_python37.rst
Normal file
58
docs/sanic/asyncio_python37.rst
Normal file
|
@ -0,0 +1,58 @@
|
|||
Python 3.7 AsyncIO examples
|
||||
###########################
|
||||
|
||||
With Python 3.7 AsyncIO got major update for the following types:
|
||||
|
||||
- asyncio.AbstractEventLoop
|
||||
- asyncio.AnstractServer
|
||||
|
||||
|
||||
This example shows how to use sanic with Python 3.7, to be precise: how to retrieve an asyncio server instance:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
import os
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
|
||||
server_socket = '/tmp/sanic.sock'
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
|
||||
try:
|
||||
os.remote(server_socket)
|
||||
finally:
|
||||
sock.bind(server_socket)
|
||||
|
||||
if __name__ == "__main__":
|
||||
loop = asyncio.get_event_loop()
|
||||
srv_coro = app.create_server(
|
||||
sock=sock,
|
||||
return_asyncio_server=True,
|
||||
asyncio_server_args=dict(
|
||||
start_serving=False
|
||||
)
|
||||
)
|
||||
srv = loop.run_until_complete(srv_coro)
|
||||
try:
|
||||
assert srv.is_serving() is False
|
||||
loop.run_until_complete(srv.start_serving())
|
||||
assert srv.is_serving() is True
|
||||
loop.run_until_complete(srv.serve_forever())
|
||||
except KeyboardInterrupt:
|
||||
srv.close()
|
||||
loop.close()
|
||||
|
||||
|
||||
Please note that uvloop does not support these features yet.
|
|
@ -127,7 +127,7 @@ Blueprints have almost the same functionality as an application instance.
|
|||
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
|
||||
decorator or `bp.add_websocket_route` method.
|
||||
|
||||
### Middleware
|
||||
### Blueprint Middleware
|
||||
|
||||
Using blueprints allows you to also register middleware globally.
|
||||
|
||||
|
@ -145,6 +145,36 @@ async def halt_response(request, response):
|
|||
return text('I halted the response')
|
||||
```
|
||||
|
||||
### Blueprint Group Middleware
|
||||
Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the
|
||||
current blueprint group under consideration.
|
||||
|
||||
```python
|
||||
bp1 = Blueprint('bp1', url_prefix='/bp1')
|
||||
bp2 = Blueprint('bp2', url_prefix='/bp2')
|
||||
|
||||
@bp1.middleware('request')
|
||||
async def bp1_only_middleware(request):
|
||||
print('applied on Blueprint : bp1 Only')
|
||||
|
||||
@bp1.route('/')
|
||||
async def bp1_route(request):
|
||||
return text('bp1')
|
||||
|
||||
@bp2.route('/<param>')
|
||||
async def bp2_route(request, param):
|
||||
return text(param)
|
||||
|
||||
group = Blueprint.group(bp1, bp2)
|
||||
|
||||
@group.middleware('request')
|
||||
async def group_middleware(request):
|
||||
print('common middleware applied for both bp1 and bp2')
|
||||
|
||||
# Register Blueprint group under the app
|
||||
app.blueprint(group)
|
||||
```
|
||||
|
||||
### Exceptions
|
||||
|
||||
Exceptions can be applied exclusively to blueprints globally.
|
||||
|
|
135
docs/sanic/changelog.md
Normal file
135
docs/sanic/changelog.md
Normal file
|
@ -0,0 +1,135 @@
|
|||
Version 18.12
|
||||
-------------
|
||||
18.12.0
|
||||
- Changes:
|
||||
- Improved codebase test coverage from 81% to 91%.
|
||||
- Added stream_large_files and host examples in static_file document
|
||||
- Added methods to append and finish body content on Request (#1379)
|
||||
- Integrated with .appveyor.yml for windows ci support
|
||||
- Added documentation for AF_INET6 and AF_UNIX socket usage
|
||||
- Adopt black/isort for codestyle
|
||||
- Cancel task when connection_lost
|
||||
- Simplify request ip and port retrieval logic
|
||||
- Handle config error in load config file.
|
||||
- Integrate with codecov for CI
|
||||
- Add missed documentation for config section.
|
||||
- Deprecate Handler.log
|
||||
- Pinned httptools requirement to version 0.0.10+
|
||||
|
||||
- Fixes:
|
||||
- Fix `remove_entity_headers` helper function (#1415)
|
||||
- Fix TypeError when use Blueprint.group() to group blueprint with default url_prefix, Use os.path.normpath to avoid invalid url_prefix like api//v1
|
||||
f8a6af1 Rename the `http` module to `helpers` to prevent conflicts with the built-in Python http library (fixes #1323)
|
||||
- Fix unittests on windows
|
||||
- Fix Namespacing of sanic logger
|
||||
- Fix missing quotes in decorator example
|
||||
- Fix redirect with quoted param
|
||||
- Fix doc for latest blueprint code
|
||||
- Fix build of latex documentation relating to markdown lists
|
||||
- Fix loop exception handling in app.py
|
||||
- Fix content length mismatch in windows and other platform
|
||||
- Fix Range header handling for static files (#1402)
|
||||
- Fix the logger and make it work (#1397)
|
||||
- Fix type pikcle->pickle in multiprocessing test
|
||||
- Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
|
||||
- Fix document for logging
|
||||
|
||||
Version 0.8
|
||||
-----------
|
||||
0.8.3
|
||||
- Changes:
|
||||
- Ownership changed to org 'huge-success'
|
||||
|
||||
0.8.0
|
||||
- Changes:
|
||||
- Add Server-Sent Events extension (Innokenty Lebedev)
|
||||
- Graceful handling of request_handler_task cancellation (Ashley Sommer)
|
||||
- Sanitize URL before redirection (aveao)
|
||||
- Add url_bytes to request (johndoe46)
|
||||
- py37 support for travisci (yunstanford)
|
||||
- Auto reloader support for OSX (garyo)
|
||||
- Add UUID route support (Volodymyr Maksymiv)
|
||||
- Add pausable response streams (Ashley Sommer)
|
||||
- Add weakref to request slots (vopankov)
|
||||
- remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
|
||||
- Allow streaming handlers in add_route (kinware)
|
||||
- use travis_retry for tox (Raphael Deem)
|
||||
- update aiohttp version for test client (yunstanford)
|
||||
- add redirect import for clarity (yingshaoxo)
|
||||
- Update HTTP Entity headers (Arnulfo Solís)
|
||||
- Add register_listener method (Stephan Fitzpatrick)
|
||||
- Remove uvloop/ujson dependencies for Windows (abuckenheimer)
|
||||
- Content-length header on 204/304 responses (Arnulfo Solís)
|
||||
- Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
|
||||
- Update development status from pre-alpha to beta (Maksim Anisenkov)
|
||||
- KeepAlive Timout log level changed to debug (Arnulfo Solís)
|
||||
- Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
|
||||
- Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
|
||||
- Add support for blueprint groups and nesting (Elias Tarhini)
|
||||
- Remove uvloop for windows setup (Aleksandr Kurlov)
|
||||
- Auto Reload (Yaser Amari)
|
||||
- Documentation updates/fixups (multiple contributors)
|
||||
|
||||
- Fixes:
|
||||
- Fix: auto_reload in Linux (Ashley Sommer)
|
||||
- Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
|
||||
- Fix: disable auto_reload by default on windows (abuckenheimer)
|
||||
- Fix (1143): Turn off access log with gunicorn (hqy)
|
||||
- Fix (1268): Support status code for file response (Cosmo Borsky)
|
||||
- Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
|
||||
- Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
|
||||
- Fix (1242): Responses for CI header (yunstanford)
|
||||
- Fix (1237): add version constraint for websockets (yunstanford)
|
||||
- Fix (1231): memory leak - always release resource (Phillip Xu)
|
||||
- Fix (1221): make request truthy if transport exists (Raphael Deem)
|
||||
- Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
|
||||
- Fix try_everything examples (PyManiacGR, kot83)
|
||||
- Fix (1158): default to auto_reload in debug mode (Raphael Deem)
|
||||
- Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
|
||||
- Fix: raw requires bytes-like object (cloudship)
|
||||
- Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
|
||||
- Fix: Bug in multipart/form-data parser (DirkGuijt)
|
||||
- Fix: Exception for missing parameter when value is null (NyanKiyoshi)
|
||||
- Fix: Parameter check (Howie Hu)
|
||||
- Fix (1089): Routing issue with named parameters and different methods (yunstanford)
|
||||
- Fix (1085): Signal handling in multi-worker mode (yunstanford)
|
||||
- Fix: single quote in readme.rst (Cosven)
|
||||
- Fix: method typos (Dmitry Dygalo)
|
||||
- Fix: log_response correct output for ip and port (Wibowo Arindrarto)
|
||||
- Fix (1042): Exception Handling (Raphael Deem)
|
||||
- Fix: Chinese URIs (Howie Hu)
|
||||
- Fix (1079): timeout bug when self.transport is None (Raphael Deem)
|
||||
- Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
|
||||
- Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
|
||||
- Fix (1065): allow add_task after server starts (Raphael Deem)
|
||||
- Fix (1061): double quotes in unauthorized exception (Raphael Deem)
|
||||
- Fix (1062): inject the app in add_task method (Raphael Deem)
|
||||
- Fix: update environment.yml for readthedocs (Eli Uriegas)
|
||||
- Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
|
||||
- Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
|
||||
- Fix: IPv6 Address and Socket Data Format (Dan Palmer)
|
||||
|
||||
Note: Changelog was unmaintained between 0.1 and 0.7
|
||||
|
||||
Version 0.1
|
||||
-----------
|
||||
- 0.1.7
|
||||
- Reversed static url and directory arguments to meet spec
|
||||
- 0.1.6
|
||||
- Static files
|
||||
- Lazy Cookie Loading
|
||||
- 0.1.5
|
||||
- Cookies
|
||||
- Blueprint listeners and ordering
|
||||
- Faster Router
|
||||
- Fix: Incomplete file reads on medium+ sized post requests
|
||||
- Breaking: after_start and before_stop now pass sanic as their first argument
|
||||
- 0.1.4
|
||||
- Multiprocessing
|
||||
- 0.1.3
|
||||
- Blueprint support
|
||||
- Faster Response processing
|
||||
- 0.1.1 - 0.1.2
|
||||
- Struggling to update pypi via CI
|
||||
- 0.1.0
|
||||
- Released to public
|
|
@ -15,6 +15,7 @@ keyword arguments:
|
|||
- `protocol` *(default `HttpProtocol`)*: Subclass
|
||||
of
|
||||
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
||||
- `access_log` *(default `True`)*: Enables log on handling requests (significantly slows server).
|
||||
|
||||
## Workers
|
||||
|
||||
|
@ -63,6 +64,26 @@ of the memory leak.
|
|||
|
||||
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
|
||||
|
||||
## Disable debug logging
|
||||
|
||||
To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
|
||||
|
||||
```python
|
||||
app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False)
|
||||
```
|
||||
|
||||
Running via Gunicorn you can set Environment variable `SANIC_ACCESS_LOG="False"`
|
||||
|
||||
```
|
||||
env SANIC_ACCESS_LOG="False" gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning
|
||||
```
|
||||
|
||||
Or you can rewrite app config directly
|
||||
|
||||
```python
|
||||
app.config.ACCESS_LOG = False
|
||||
```
|
||||
|
||||
## Asynchronous support
|
||||
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
|
||||
However be advised that this method does not support using multiple processes, and is not the preferred way
|
||||
|
|
|
@ -156,4 +156,12 @@ execution support provided by the ``pytest-xdist`` plugin.
|
|||
|
||||
.. literalinclude:: ../../examples/pytest_xdist.py
|
||||
|
||||
|
||||
Amending Request Object
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``request`` object in ``Sanic`` is a kind of ``dict`` object, this means that ``reqeust`` object can be manipulated as a regular ``dict`` object.
|
||||
|
||||
.. literalinclude:: ../../examples/amending_request_object.py
|
||||
|
||||
For more examples and useful samples please visit the `Huge-Sanic's GitHub Page <https://github.com/huge-success/sanic/tree/master/examples>`_
|
||||
|
|
|
@ -17,7 +17,6 @@ A list of Sanic extensions created by the community.
|
|||
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for
|
||||
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
|
||||
- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic.
|
||||
- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
|
||||
- [sanic-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support.
|
||||
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
|
||||
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
|
||||
|
@ -34,6 +33,7 @@ A list of Sanic extensions created by the community.
|
|||
- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models.
|
||||
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
|
||||
- [GINO](https://github.com/fantix/gino): An asyncio ORM on top of SQLAlchemy core, delivered with a Sanic extension. ([Documentation](https://python-gino.readthedocs.io/))
|
||||
- [Databases](https://github.com/encode/databases): Async database access for SQLAlchemy core, with support for PostgreSQL, MySQL, and SQLite.
|
||||
|
||||
## Unit Testing
|
||||
|
||||
|
@ -41,7 +41,7 @@ A list of Sanic extensions created by the community.
|
|||
|
||||
## Project Creation Template
|
||||
|
||||
- [cookiecutter-sanic](https://github.com/harshanarayana/cookiecutter-sanic) Get your sanic application up and running in a matter of second in a well defined project structure.
|
||||
- [cookiecutter-sanic](https://github.com/harshanarayana/cookiecutter-sanic): Get your sanic application up and running in a matter of second in a well defined project structure.
|
||||
Batteries included for deployment, unit testing, automated release management and changelog generation.
|
||||
|
||||
## Templating
|
||||
|
@ -67,6 +67,7 @@ A list of Sanic extensions created by the community.
|
|||
## Monitoring and Reporting
|
||||
|
||||
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
|
||||
- [sanic-zipkin](https://github.com/kevinqqnj/sanic-zipkin): Easily report request/function/RPC traces to zipkin/jaeger, through aiozipkin.
|
||||
|
||||
|
||||
## Sample Applications
|
||||
|
|
|
@ -36,20 +36,31 @@ this.
|
|||
```
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.middleware('request')
|
||||
async def add_key(request):
|
||||
# Add a key to request object like dict object
|
||||
request['foo'] = 'bar'
|
||||
|
||||
|
||||
@app.middleware('response')
|
||||
async def custom_banner(request, response):
|
||||
response.headers["Server"] = "Fake-Server"
|
||||
|
||||
|
||||
@app.middleware('response')
|
||||
async def prevent_xss(request, response):
|
||||
response.headers["x-xss-protection"] = "1; mode=block"
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
The above code will apply the two middleware in order. First, the middleware
|
||||
The above code will apply the three middleware in order. The first middleware
|
||||
**add_key** will add a new key `foo` into `request` object. This worked because
|
||||
`request` object can be manipulated like `dict` object. Then, the second middleware
|
||||
**custom_banner** will change the HTTP response header *Server* to
|
||||
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
|
||||
*Fake-Server*, and the last middleware **prevent_xss** will add the HTTP
|
||||
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
|
||||
are invoked *after* a user function returns a response.
|
||||
|
||||
|
|
|
@ -126,3 +126,40 @@ args.get('titles') # => 'Post 1'
|
|||
|
||||
args.getlist('titles') # => ['Post 1', 'Post 2']
|
||||
```
|
||||
|
||||
## Accessing the handler name with the request.endpoint attribute
|
||||
|
||||
The `request.endpoint` attribute holds the handler's name. For instance, the below
|
||||
route will return "hello".
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@app.get("/")
|
||||
def hello(request):
|
||||
return text(request.endpoint)
|
||||
```
|
||||
|
||||
Or, with a blueprint it will be include both, separated by a period. For example,
|
||||
the below route would return foo.bar:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic import Blueprint
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
blueprint = Blueprint('foo')
|
||||
|
||||
@blueprint.get('/')
|
||||
async def bar(request):
|
||||
return text(request.endpoint)
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
```
|
||||
|
|
|
@ -48,7 +48,7 @@ app.run(host="0.0.0.0", port=8000)
|
|||
|
||||
## Virtual Host
|
||||
|
||||
The `app.static()` method also support **virtual host**. You can serve your static files with spefic **virtual host** with `host` argument. For example:
|
||||
The `app.static()` method also support **virtual host**. You can serve your static files with specific **virtual host** with `host` argument. For example:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
|
|
@ -42,7 +42,7 @@ async def handler(request):
|
|||
|
||||
|
||||
@bp.put('/bp_stream', stream=True)
|
||||
async def bp_handler(request):
|
||||
async def bp_put_handler(request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
|
@ -52,6 +52,19 @@ async def bp_handler(request):
|
|||
return text(result)
|
||||
|
||||
|
||||
# You can also use `bp.add_route()` with stream argument
|
||||
async def bp_post_handler(request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8').replace('1', 'A')
|
||||
return text(result)
|
||||
|
||||
bp.add_route(bp_post_handler, '/bp_stream', methods=['POST'], stream=True)
|
||||
|
||||
|
||||
async def post_handler(request):
|
||||
result = ''
|
||||
while True:
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
name: py35
|
||||
name: py36
|
||||
dependencies:
|
||||
- openssl=1.0.2g=0
|
||||
- pip=8.1.1=py35_0
|
||||
- python=3.5.1=0
|
||||
- readline=6.2=2
|
||||
- setuptools=20.3=py35_0
|
||||
- sqlite=3.9.2=0
|
||||
- tk=8.5.18=0
|
||||
- wheel=0.29.0=py35_0
|
||||
- xz=5.0.5=1
|
||||
- zlib=1.2.8=0
|
||||
- pip=18.1=py36_0
|
||||
- python=3.6=0
|
||||
- setuptools=40.4.3=py36_0
|
||||
- pip:
|
||||
- uvloop>=0.5.3
|
||||
- httptools>=0.0.10
|
||||
- uvloop>=0.5.3
|
||||
- ujson>=1.35
|
||||
- aiofiles>=0.3.0
|
||||
- websockets>=6.0
|
||||
- sphinxcontrib-asyncio>=0.2.0
|
||||
- websockets>=6.0,<7.0
|
||||
- multidict>=4.0,<5.0
|
||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
||||
- sphinx==1.8.3
|
||||
- sphinx_rtd_theme==0.4.2
|
||||
- recommonmark==0.5.0
|
||||
- sphinxcontrib-asyncio>=0.2.0
|
||||
- docutils==0.14
|
||||
- pygments==2.3.1
|
||||
|
|
30
examples/amending_request_object.py
Normal file
30
examples/amending_request_object.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from random import randint
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.middleware('request')
|
||||
def append_request(request):
|
||||
# Add new key with random value
|
||||
request['num'] = randint(0, 100)
|
||||
|
||||
|
||||
@app.get('/pop')
|
||||
def pop_handler(request):
|
||||
# Pop key from request object
|
||||
num = request.pop('num')
|
||||
return text(num)
|
||||
|
||||
|
||||
@app.get('/key_exist')
|
||||
def key_exist_handler(request):
|
||||
# Check the key is exist or not
|
||||
if 'num' in request:
|
||||
return text('num exist in request')
|
||||
|
||||
return text('num does not exist in reqeust')
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
149
sanic/app.py
149
sanic/app.py
|
@ -4,15 +4,18 @@ import os
|
|||
import re
|
||||
import warnings
|
||||
|
||||
from asyncio import CancelledError, ensure_future, get_event_loop
|
||||
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
|
||||
from collections import defaultdict, deque
|
||||
from functools import partial
|
||||
from inspect import getmodulename, isawaitable, signature, stack
|
||||
from ssl import Purpose, create_default_context
|
||||
from socket import socket
|
||||
from ssl import Purpose, SSLContext, create_default_context
|
||||
from traceback import format_exc
|
||||
from typing import Any, Optional, Type, Union
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
|
||||
from sanic import reloader_helpers
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.config import BASE_LOGO, Config
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import SanicException, ServerError, URLBuildError
|
||||
|
@ -454,6 +457,13 @@ class Sanic:
|
|||
def response(handler):
|
||||
async def websocket_handler(request, *args, **kwargs):
|
||||
request.app = self
|
||||
if not getattr(handler, "__blueprintname__", False):
|
||||
request.endpoint = handler.__name__
|
||||
else:
|
||||
request.endpoint = (
|
||||
getattr(handler, "__blueprintname__", "")
|
||||
+ handler.__name__
|
||||
)
|
||||
try:
|
||||
protocol = request.transport.get_protocol()
|
||||
except AttributeError:
|
||||
|
@ -588,9 +598,11 @@ class Sanic:
|
|||
:return: decorated method
|
||||
"""
|
||||
if attach_to == "request":
|
||||
self.request_middleware.append(middleware)
|
||||
if middleware not in self.request_middleware:
|
||||
self.request_middleware.append(middleware)
|
||||
if attach_to == "response":
|
||||
self.response_middleware.appendleft(middleware)
|
||||
if middleware not in self.response_middleware:
|
||||
self.response_middleware.appendleft(middleware)
|
||||
return middleware
|
||||
|
||||
# Decorator
|
||||
|
@ -672,7 +684,7 @@ class Sanic:
|
|||
:param options: option dictionary with blueprint defaults
|
||||
:return: Nothing
|
||||
"""
|
||||
if isinstance(blueprint, (list, tuple)):
|
||||
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
|
||||
for item in blueprint:
|
||||
self.blueprint(item, **options)
|
||||
return
|
||||
|
@ -888,6 +900,16 @@ class Sanic:
|
|||
"handler from the router"
|
||||
)
|
||||
)
|
||||
else:
|
||||
if not getattr(handler, "__blueprintname__", False):
|
||||
request.endpoint = self._build_endpoint_name(
|
||||
handler.__name__
|
||||
)
|
||||
else:
|
||||
request.endpoint = self._build_endpoint_name(
|
||||
getattr(handler, "__blueprintname__", ""),
|
||||
handler.__name__,
|
||||
)
|
||||
|
||||
# Run response handler
|
||||
response = handler(request, *args, **kwargs)
|
||||
|
@ -967,34 +989,47 @@ class Sanic:
|
|||
|
||||
def run(
|
||||
self,
|
||||
host=None,
|
||||
port=None,
|
||||
debug=False,
|
||||
ssl=None,
|
||||
sock=None,
|
||||
workers=1,
|
||||
protocol=None,
|
||||
backlog=100,
|
||||
stop_event=None,
|
||||
register_sys_signals=True,
|
||||
access_log=True,
|
||||
**kwargs
|
||||
):
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
debug: bool = False,
|
||||
ssl: Union[dict, SSLContext, None] = None,
|
||||
sock: Optional[socket] = None,
|
||||
workers: int = 1,
|
||||
protocol: Type[Protocol] = None,
|
||||
backlog: int = 100,
|
||||
stop_event: Any = None,
|
||||
register_sys_signals: bool = True,
|
||||
access_log: Optional[bool] = None,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Run the HTTP Server and listen until keyboard interrupt or term
|
||||
signal. On termination, drain connections before closing.
|
||||
|
||||
:param host: Address to host on
|
||||
:type host: str
|
||||
:param port: Port to host on
|
||||
:type port: int
|
||||
:param debug: Enables debug output (slows server)
|
||||
:type debug: bool
|
||||
:param ssl: SSLContext, or location of certificate and key
|
||||
for SSL encryption of worker(s)
|
||||
:type ssl:SSLContext or dict
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:type sock: socket
|
||||
:param workers: Number of processes received before it is respected
|
||||
:type workers: int
|
||||
:param protocol: Subclass of asyncio Protocol class
|
||||
:type protocol: type[Protocol]
|
||||
:param backlog: a number of unaccepted connections that the system
|
||||
will allow before refusing new connections
|
||||
:param stop_event: event to be triggered before stopping the app
|
||||
:type backlog: int
|
||||
:param stop_event: event to be triggered
|
||||
before stopping the app - deprecated
|
||||
:type stop_event: None
|
||||
:param register_sys_signals: Register SIG* events
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:type register_sys_signals: bool
|
||||
:param access_log: Enables writing access logs (slows server)
|
||||
:type access_log: bool
|
||||
:return: Nothing
|
||||
"""
|
||||
if "loop" in kwargs:
|
||||
|
@ -1027,8 +1062,10 @@ class Sanic:
|
|||
"stop_event will be removed from future versions.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
# compatibility old access_log params
|
||||
self.config.ACCESS_LOG = access_log
|
||||
# if access_log is passed explicitly change config.ACCESS_LOG
|
||||
if access_log is not None:
|
||||
self.config.ACCESS_LOG = access_log
|
||||
|
||||
server_settings = self._helper(
|
||||
host=host,
|
||||
port=port,
|
||||
|
@ -1078,16 +1115,18 @@ class Sanic:
|
|||
|
||||
async def create_server(
|
||||
self,
|
||||
host=None,
|
||||
port=None,
|
||||
debug=False,
|
||||
ssl=None,
|
||||
sock=None,
|
||||
protocol=None,
|
||||
backlog=100,
|
||||
stop_event=None,
|
||||
access_log=True,
|
||||
):
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
debug: bool = False,
|
||||
ssl: Union[dict, SSLContext, None] = None,
|
||||
sock: Optional[socket] = None,
|
||||
protocol: Type[Protocol] = None,
|
||||
backlog: int = 100,
|
||||
stop_event: Any = None,
|
||||
access_log: Optional[bool] = None,
|
||||
return_asyncio_server=False,
|
||||
asyncio_server_kwargs=None,
|
||||
) -> None:
|
||||
"""
|
||||
Asynchronous version of :func:`run`.
|
||||
|
||||
|
@ -1098,6 +1137,36 @@ class Sanic:
|
|||
.. note::
|
||||
This does not support multiprocessing and is not the preferred
|
||||
way to run a :class:`Sanic` application.
|
||||
|
||||
:param host: Address to host on
|
||||
:type host: str
|
||||
:param port: Port to host on
|
||||
:type port: int
|
||||
:param debug: Enables debug output (slows server)
|
||||
:type debug: bool
|
||||
:param ssl: SSLContext, or location of certificate and key
|
||||
for SSL encryption of worker(s)
|
||||
:type ssl:SSLContext or dict
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:type sock: socket
|
||||
:param protocol: Subclass of asyncio Protocol class
|
||||
:type protocol: type[Protocol]
|
||||
:param backlog: a number of unaccepted connections that the system
|
||||
will allow before refusing new connections
|
||||
:type backlog: int
|
||||
:param stop_event: event to be triggered
|
||||
before stopping the app - deprecated
|
||||
:type stop_event: None
|
||||
:param access_log: Enables writing access logs (slows server)
|
||||
:type access_log: bool
|
||||
:param return_asyncio_server: flag that defines whether there's a need
|
||||
to return asyncio.Server or
|
||||
start it serving right away
|
||||
:type return_asyncio_server: bool
|
||||
:param asyncio_server_kwargs: key-value arguments for
|
||||
asyncio/uvloop create_server method
|
||||
:type asyncio_server_kwargs: dict
|
||||
:return: Nothing
|
||||
"""
|
||||
|
||||
if sock is None:
|
||||
|
@ -1114,8 +1183,10 @@ class Sanic:
|
|||
"stop_event will be removed from future versions.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
# compatibility old access_log params
|
||||
self.config.ACCESS_LOG = access_log
|
||||
# if access_log is passed explicitly change config.ACCESS_LOG
|
||||
if access_log is not None:
|
||||
self.config.ACCESS_LOG = access_log
|
||||
|
||||
server_settings = self._helper(
|
||||
host=host,
|
||||
port=port,
|
||||
|
@ -1125,7 +1196,7 @@ class Sanic:
|
|||
loop=get_event_loop(),
|
||||
protocol=protocol,
|
||||
backlog=backlog,
|
||||
run_async=True,
|
||||
run_async=return_asyncio_server,
|
||||
)
|
||||
|
||||
# Trigger before_start events
|
||||
|
@ -1134,7 +1205,9 @@ class Sanic:
|
|||
server_settings.get("loop"),
|
||||
)
|
||||
|
||||
return await serve(**server_settings)
|
||||
return await serve(
|
||||
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
|
||||
)
|
||||
|
||||
async def trigger_events(self, events, loop):
|
||||
"""Trigger events (functions or async)
|
||||
|
@ -1276,3 +1349,7 @@ class Sanic:
|
|||
logger.info("Goin' Fast @ {}://{}:{}".format(proto, host, port))
|
||||
|
||||
return server_settings
|
||||
|
||||
def _build_endpoint_name(self, *parts):
|
||||
parts = [self.name, *parts]
|
||||
return ".".join(parts)
|
||||
|
|
120
sanic/blueprint_group.py
Normal file
120
sanic/blueprint_group.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
from collections import MutableSequence
|
||||
|
||||
|
||||
class BlueprintGroup(MutableSequence):
|
||||
"""
|
||||
This class provides a mechanism to implement a Blueprint Group
|
||||
using the `Blueprint.group` method. To avoid having to re-write
|
||||
some of the existing implementation, this class provides a custom
|
||||
iterator implementation that will let you use the object of this
|
||||
class as a list/tuple inside the existing implementation.
|
||||
"""
|
||||
|
||||
__slots__ = ("_blueprints", "_url_prefix")
|
||||
|
||||
def __init__(self, url_prefix=None):
|
||||
"""
|
||||
Create a new Blueprint Group
|
||||
|
||||
:param url_prefix: URL: to be prefixed before all the Blueprint Prefix
|
||||
"""
|
||||
self._blueprints = []
|
||||
self._url_prefix = url_prefix
|
||||
|
||||
@property
|
||||
def url_prefix(self):
|
||||
"""
|
||||
Retrieve the URL prefix being used for the Current Blueprint Group
|
||||
:return: string with url prefix
|
||||
"""
|
||||
return self._url_prefix
|
||||
|
||||
@property
|
||||
def blueprints(self):
|
||||
"""
|
||||
Retrieve a list of all the available blueprints under this group.
|
||||
:return: List of Blueprint instance
|
||||
"""
|
||||
return self._blueprints
|
||||
|
||||
def __iter__(self):
|
||||
"""Tun the class Blueprint Group into an Iterable item"""
|
||||
return iter(self._blueprints)
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""
|
||||
This method returns a blueprint inside the group specified by
|
||||
an index value. This will enable indexing, splice and slicing
|
||||
of the blueprint group like we can do with regular list/tuple.
|
||||
|
||||
This method is provided to ensure backward compatibility with
|
||||
any of the pre-existing usage that might break.
|
||||
|
||||
:param item: Index of the Blueprint item in the group
|
||||
:return: Blueprint object
|
||||
"""
|
||||
return self._blueprints[item]
|
||||
|
||||
def __setitem__(self, index: int, item: object) -> None:
|
||||
"""
|
||||
Abstract method implemented to turn the `BlueprintGroup` class
|
||||
into a list like object to support all the existing behavior.
|
||||
|
||||
This method is used to perform the list's indexed setter operation.
|
||||
|
||||
:param index: Index to use for inserting a new Blueprint item
|
||||
:param item: New `Blueprint` object.
|
||||
:return: None
|
||||
"""
|
||||
self._blueprints[index] = item
|
||||
|
||||
def __delitem__(self, index: int) -> None:
|
||||
"""
|
||||
Abstract method implemented to turn the `BlueprintGroup` class
|
||||
into a list like object to support all the existing behavior.
|
||||
|
||||
This method is used to delete an item from the list of blueprint
|
||||
groups like it can be done on a regular list with index.
|
||||
|
||||
:param index: Index to use for removing a new Blueprint item
|
||||
:return: None
|
||||
"""
|
||||
del self._blueprints[index]
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
Get the Length of the blueprint group object.
|
||||
:return: Length of Blueprint group object
|
||||
"""
|
||||
return len(self._blueprints)
|
||||
|
||||
def insert(self, index: int, item: object) -> None:
|
||||
"""
|
||||
The Abstract class `MutableSequence` leverages this insert method to
|
||||
perform the `BlueprintGroup.append` operation.
|
||||
|
||||
:param index: Index to use for removing a new Blueprint item
|
||||
:param item: New `Blueprint` object.
|
||||
:return: None
|
||||
"""
|
||||
self._blueprints.insert(index, item)
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
A decorator that can be used to implement a Middleware plugin to
|
||||
all of the Blueprints that belongs to this specific Blueprint Group.
|
||||
|
||||
In case of nested Blueprint Groups, the same middleware is applied
|
||||
across each of the Blueprints recursively.
|
||||
|
||||
:param args: Optional positional Parameters to be use middleware
|
||||
:param kwargs: Optional Keyword arg to use with Middleware
|
||||
:return: Partial function to apply the middleware
|
||||
"""
|
||||
kwargs["bp_group"] = True
|
||||
|
||||
def register_middleware_for_blueprints(fn):
|
||||
for blueprint in self.blueprints:
|
||||
blueprint.middleware(fn, *args, **kwargs)
|
||||
|
||||
return register_middleware_for_blueprints
|
|
@ -1,5 +1,6 @@
|
|||
from collections import defaultdict, namedtuple
|
||||
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.views import CompositionView
|
||||
|
||||
|
@ -78,10 +79,12 @@ class Blueprint:
|
|||
for i in nested:
|
||||
if isinstance(i, (list, tuple)):
|
||||
yield from chain(i)
|
||||
elif isinstance(i, BlueprintGroup):
|
||||
yield from i.blueprints
|
||||
else:
|
||||
yield i
|
||||
|
||||
bps = []
|
||||
bps = BlueprintGroup(url_prefix=url_prefix)
|
||||
for bp in chain(blueprints):
|
||||
if bp.url_prefix is None:
|
||||
bp.url_prefix = ""
|
||||
|
@ -212,6 +215,7 @@ class Blueprint:
|
|||
strict_slashes=None,
|
||||
version=None,
|
||||
name=None,
|
||||
stream=False,
|
||||
):
|
||||
"""Create a blueprint route from a function.
|
||||
|
||||
|
@ -224,6 +228,7 @@ class Blueprint:
|
|||
training */*
|
||||
:param version: Blueprint Version
|
||||
:param name: user defined route name for url_for
|
||||
:param stream: boolean specifying if the handler is a stream handler
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
|
@ -246,6 +251,7 @@ class Blueprint:
|
|||
methods=methods,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)(handler)
|
||||
|
@ -324,7 +330,13 @@ class Blueprint:
|
|||
args = []
|
||||
return register_middleware(middleware)
|
||||
else:
|
||||
return register_middleware
|
||||
if kwargs.get("bp_group") and callable(args[0]):
|
||||
middleware = args[0]
|
||||
args = args[1:]
|
||||
kwargs.pop("bp_group")
|
||||
return register_middleware(middleware)
|
||||
else:
|
||||
return register_middleware
|
||||
|
||||
def exception(self, *args, **kwargs):
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
import types
|
||||
|
||||
from distutils.util import strtobool
|
||||
|
||||
from sanic.exceptions import PyFileError
|
||||
from sanic.helpers import import_string
|
||||
|
||||
|
@ -13,23 +15,31 @@ BASE_LOGO = """
|
|||
|
||||
"""
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
|
||||
"REQUEST_BUFFER_QUEUE_SIZE": 100,
|
||||
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
||||
"KEEP_ALIVE": True,
|
||||
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
|
||||
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabytes
|
||||
"WEBSOCKET_MAX_QUEUE": 32,
|
||||
"WEBSOCKET_READ_LIMIT": 2 ** 16,
|
||||
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||
"ACCESS_LOG": True,
|
||||
}
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=True):
|
||||
super().__init__(defaults or {})
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=None):
|
||||
defaults = defaults or {}
|
||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||
|
||||
self.LOGO = BASE_LOGO
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
|
||||
self.REQUEST_BUFFER_QUEUE_SIZE = 100
|
||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
self.RESPONSE_TIMEOUT = 60 # 60 seconds
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
|
||||
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
||||
self.WEBSOCKET_MAX_QUEUE = 32
|
||||
self.WEBSOCKET_READ_LIMIT = 2 ** 16
|
||||
self.WEBSOCKET_WRITE_LIMIT = 2 ** 16
|
||||
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
|
||||
self.ACCESS_LOG = True
|
||||
|
||||
if keep_alive is not None:
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
|
||||
if load_env:
|
||||
prefix = SANIC_PREFIX if load_env is True else load_env
|
||||
|
@ -122,4 +132,7 @@ class Config(dict):
|
|||
try:
|
||||
self[config_key] = float(v)
|
||||
except ValueError:
|
||||
self[config_key] = v
|
||||
try:
|
||||
self[config_key] = bool(strtobool(v))
|
||||
except ValueError:
|
||||
self[config_key] = v
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import re
|
||||
import string
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
DEFAULT_MAX_AGE = 0
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# SimpleCookie
|
||||
|
@ -103,6 +107,14 @@ class Cookie(dict):
|
|||
if key not in self._keys:
|
||||
raise KeyError("Unknown cookie property")
|
||||
if value is not False:
|
||||
if key.lower() == "max-age":
|
||||
if not str(value).isdigit():
|
||||
value = DEFAULT_MAX_AGE
|
||||
elif key.lower() == "expires":
|
||||
if not isinstance(value, datetime):
|
||||
raise TypeError(
|
||||
"Cookie 'expires' property must be a datetime"
|
||||
)
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def encode(self, encoding):
|
||||
|
@ -126,16 +138,10 @@ class Cookie(dict):
|
|||
except TypeError:
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
elif key == "expires":
|
||||
try:
|
||||
output.append(
|
||||
"%s=%s"
|
||||
% (
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT"),
|
||||
)
|
||||
)
|
||||
except AttributeError:
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
output.append(
|
||||
"%s=%s"
|
||||
% (self._keys[key], value.strftime("%a, %d-%b-%Y %T GMT"))
|
||||
)
|
||||
elif key in self._flags and self[key]:
|
||||
output.append(self._keys[key])
|
||||
else:
|
||||
|
|
|
@ -36,7 +36,15 @@ def _iter_module_files():
|
|||
def _get_args_for_reloading():
|
||||
"""Returns the executable."""
|
||||
rv = [sys.executable]
|
||||
rv.extend(sys.argv)
|
||||
main_module = sys.modules["__main__"]
|
||||
mod_spec = getattr(main_module, "__spec__", None)
|
||||
if mod_spec:
|
||||
# Parent exe was launched as a module rather than a script
|
||||
rv.extend(["-m", mod_spec.name])
|
||||
if len(sys.argv) > 1:
|
||||
rv.extend(sys.argv[1:])
|
||||
else:
|
||||
rv.extend(sys.argv)
|
||||
return rv
|
||||
|
||||
|
||||
|
@ -44,6 +52,7 @@ def restart_with_reloader():
|
|||
"""Create a new process and a subprocess in it with the same arguments as
|
||||
this one.
|
||||
"""
|
||||
cwd = os.getcwd()
|
||||
args = _get_args_for_reloading()
|
||||
new_environ = os.environ.copy()
|
||||
new_environ["SANIC_SERVER_RUNNING"] = "true"
|
||||
|
@ -51,7 +60,7 @@ def restart_with_reloader():
|
|||
worker_process = Process(
|
||||
target=subprocess.call,
|
||||
args=(cmd,),
|
||||
kwargs=dict(shell=True, env=new_environ),
|
||||
kwargs={"cwd": cwd, "shell": True, "env": new_environ},
|
||||
)
|
||||
worker_process.start()
|
||||
return worker_process
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import asyncio
|
||||
import email.utils
|
||||
import json
|
||||
import sys
|
||||
|
||||
from cgi import parse_header
|
||||
from collections import namedtuple
|
||||
from http.cookies import SimpleCookie
|
||||
from urllib.parse import parse_qs, urlunparse
|
||||
from urllib.parse import parse_qs, unquote, urlunparse
|
||||
|
||||
from httptools import parse_url
|
||||
|
||||
|
@ -69,26 +70,27 @@ class Request(dict):
|
|||
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||
|
||||
__slots__ = (
|
||||
"app",
|
||||
"headers",
|
||||
"version",
|
||||
"method",
|
||||
"__weakref__",
|
||||
"_cookies",
|
||||
"transport",
|
||||
"body",
|
||||
"parsed_json",
|
||||
"parsed_args",
|
||||
"parsed_form",
|
||||
"parsed_files",
|
||||
"_ip",
|
||||
"_parsed_url",
|
||||
"uri_template",
|
||||
"stream",
|
||||
"_port",
|
||||
"_remote_addr",
|
||||
"_socket",
|
||||
"_port",
|
||||
"__weakref__",
|
||||
"app",
|
||||
"body",
|
||||
"endpoint",
|
||||
"headers",
|
||||
"method",
|
||||
"parsed_args",
|
||||
"parsed_files",
|
||||
"parsed_form",
|
||||
"parsed_json",
|
||||
"raw_url",
|
||||
"stream",
|
||||
"transport",
|
||||
"uri_template",
|
||||
"version",
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
|
@ -111,10 +113,9 @@ class Request(dict):
|
|||
self.uri_template = None
|
||||
self._cookies = None
|
||||
self.stream = None
|
||||
self.endpoint = None
|
||||
|
||||
def __repr__(self):
|
||||
if self.method is None or not self.path:
|
||||
return "<{0}>".format(self.__class__.__name__)
|
||||
return "<{0}: {1} {2}>".format(
|
||||
self.__class__.__name__, self.method, self.path
|
||||
)
|
||||
|
@ -356,15 +357,28 @@ def parse_multipart_form(body, boundary):
|
|||
)
|
||||
|
||||
if form_header_field == "content-disposition":
|
||||
file_name = form_parameters.get("filename")
|
||||
field_name = form_parameters.get("name")
|
||||
file_name = form_parameters.get("filename")
|
||||
|
||||
# non-ASCII filenames in RFC2231, "filename*" format
|
||||
if file_name is None and form_parameters.get("filename*"):
|
||||
encoding, _, value = email.utils.decode_rfc2231(
|
||||
form_parameters["filename*"]
|
||||
)
|
||||
file_name = unquote(value, encoding=encoding)
|
||||
elif form_header_field == "content-type":
|
||||
content_type = form_header_value
|
||||
content_charset = form_parameters.get("charset", "utf-8")
|
||||
|
||||
if field_name:
|
||||
post_data = form_part[line_index:-4]
|
||||
if file_name:
|
||||
if file_name is None:
|
||||
value = post_data.decode(content_charset)
|
||||
if field_name in fields:
|
||||
fields[field_name].append(value)
|
||||
else:
|
||||
fields[field_name] = [value]
|
||||
else:
|
||||
form_file = File(
|
||||
type=content_type, name=file_name, body=post_data
|
||||
)
|
||||
|
@ -372,12 +386,6 @@ def parse_multipart_form(body, boundary):
|
|||
files[field_name].append(form_file)
|
||||
else:
|
||||
files[field_name] = [form_file]
|
||||
else:
|
||||
value = post_data.decode(content_charset)
|
||||
if field_name in fields:
|
||||
fields[field_name].append(value)
|
||||
else:
|
||||
fields[field_name] = [value]
|
||||
else:
|
||||
logger.debug(
|
||||
"Form-data field does not have a 'name' parameter "
|
||||
|
|
|
@ -117,7 +117,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||
|
||||
headers = self._parse_headers()
|
||||
|
||||
if self.status is 200:
|
||||
if self.status == 200:
|
||||
status = b"OK"
|
||||
else:
|
||||
status = STATUS_CODES.get(self.status)
|
||||
|
@ -176,7 +176,7 @@ class HTTPResponse(BaseHTTPResponse):
|
|||
|
||||
headers = self._parse_headers()
|
||||
|
||||
if self.status is 200:
|
||||
if self.status == 200:
|
||||
status = b"OK"
|
||||
else:
|
||||
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
|
||||
|
|
|
@ -17,8 +17,8 @@ Parameter = namedtuple("Parameter", ["name", "cast"])
|
|||
|
||||
REGEX_TYPES = {
|
||||
"string": (str, r"[^/]+"),
|
||||
"int": (int, r"\d+"),
|
||||
"number": (float, r"[0-9\\.]+"),
|
||||
"int": (int, r"-?\d+"),
|
||||
"number": (float, r"-?[0-9\\.]+"),
|
||||
"alpha": (str, r"[A-Za-z]+"),
|
||||
"path": (str, r"[^/].*?"),
|
||||
"uuid": (
|
||||
|
|
|
@ -34,9 +34,6 @@ except ImportError:
|
|||
pass
|
||||
|
||||
|
||||
current_time = None
|
||||
|
||||
|
||||
class Signal:
|
||||
stopped = False
|
||||
|
||||
|
@ -171,7 +168,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
self.request_timeout, self.request_timeout_callback
|
||||
)
|
||||
self.transport = transport
|
||||
self._last_request_time = current_time
|
||||
self._last_request_time = time()
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.connections.discard(self)
|
||||
|
@ -197,7 +194,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
# exactly what this timeout is checking for.
|
||||
# Check if elapsed time since request initiated exceeds our
|
||||
# configured maximum request timeout value
|
||||
time_elapsed = current_time - self._last_request_time
|
||||
time_elapsed = time() - self._last_request_time
|
||||
if time_elapsed < self.request_timeout:
|
||||
time_left = self.request_timeout - time_elapsed
|
||||
self._request_timeout_handler = self.loop.call_later(
|
||||
|
@ -213,7 +210,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
def response_timeout_callback(self):
|
||||
# Check if elapsed time since response was initiated exceeds our
|
||||
# configured maximum request timeout value
|
||||
time_elapsed = current_time - self._last_request_time
|
||||
time_elapsed = time() - self._last_request_time
|
||||
if time_elapsed < self.response_timeout:
|
||||
time_left = self.response_timeout - time_elapsed
|
||||
self._response_timeout_handler = self.loop.call_later(
|
||||
|
@ -234,7 +231,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
|
||||
:return: None
|
||||
"""
|
||||
time_elapsed = current_time - self._last_response_time
|
||||
time_elapsed = time() - self._last_response_time
|
||||
if time_elapsed < self.keep_alive_timeout:
|
||||
time_left = self.keep_alive_timeout - time_elapsed
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
|
@ -362,7 +359,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
self._response_timeout_handler = self.loop.call_later(
|
||||
self.response_timeout, self.response_timeout_callback
|
||||
)
|
||||
self._last_request_time = current_time
|
||||
self._last_request_time = time()
|
||||
self._request_handler_task = self.loop.create_task(
|
||||
self.request_handler(
|
||||
self.request, self.write_response, self.stream_response
|
||||
|
@ -449,7 +446,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
||||
)
|
||||
self._last_response_time = current_time
|
||||
self._last_response_time = time()
|
||||
self.cleanup()
|
||||
|
||||
async def drain(self):
|
||||
|
@ -502,7 +499,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
||||
)
|
||||
self._last_response_time = current_time
|
||||
self._last_response_time = time()
|
||||
self.cleanup()
|
||||
|
||||
def write_error(self, exception):
|
||||
|
@ -595,18 +592,6 @@ class HttpProtocol(asyncio.Protocol):
|
|||
self.transport = None
|
||||
|
||||
|
||||
def update_current_time(loop):
|
||||
"""Cache the current time, since it is needed at the end of every
|
||||
keep-alive request to update the request timeout time
|
||||
|
||||
:param loop:
|
||||
:return:
|
||||
"""
|
||||
global current_time
|
||||
current_time = time()
|
||||
loop.call_later(1, partial(update_current_time, loop))
|
||||
|
||||
|
||||
def trigger_events(events, loop):
|
||||
"""Trigger event callbacks (functions or async)
|
||||
|
||||
|
@ -656,6 +641,7 @@ def serve(
|
|||
websocket_write_limit=2 ** 16,
|
||||
state=None,
|
||||
graceful_shutdown_timeout=15.0,
|
||||
asyncio_server_kwargs=None,
|
||||
):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
|
@ -700,6 +686,8 @@ def serve(
|
|||
:param router: Router object
|
||||
:param graceful_shutdown_timeout: How long take to Force close non-idle
|
||||
connection
|
||||
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
|
||||
create_server method
|
||||
:return: Nothing
|
||||
"""
|
||||
if not run_async:
|
||||
|
@ -734,7 +722,9 @@ def serve(
|
|||
state=state,
|
||||
debug=debug,
|
||||
)
|
||||
|
||||
asyncio_server_kwargs = (
|
||||
asyncio_server_kwargs if asyncio_server_kwargs else {}
|
||||
)
|
||||
server_coroutine = loop.create_server(
|
||||
server,
|
||||
host,
|
||||
|
@ -743,12 +733,9 @@ def serve(
|
|||
reuse_port=reuse_port,
|
||||
sock=sock,
|
||||
backlog=backlog,
|
||||
**asyncio_server_kwargs
|
||||
)
|
||||
|
||||
# Instead of pulling time at the end of every request,
|
||||
# pull it once per minute
|
||||
loop.call_soon(partial(update_current_time, loop))
|
||||
|
||||
if run_async:
|
||||
return server_coroutine
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ class GunicornWorker(base.Worker):
|
|||
if self.app.callable.websocket_enabled
|
||||
else self.http_protocol
|
||||
)
|
||||
|
||||
self._server_settings = self.app.callable._helper(
|
||||
loop=self.loop,
|
||||
debug=is_debug,
|
||||
|
|
2
setup.py
2
setup.py
|
@ -86,7 +86,7 @@ requirements = [
|
|||
]
|
||||
|
||||
tests_require = [
|
||||
"pytest==3.3.2",
|
||||
"pytest==4.1.0",
|
||||
"multidict>=4.0,<5.0",
|
||||
"gunicorn",
|
||||
"pytest-cov",
|
||||
|
|
53
tests/benchmark/test_route_resolution_benchmark.py
Normal file
53
tests/benchmark/test_route_resolution_benchmark.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
from random import choice, seed
|
||||
from pytest import mark
|
||||
|
||||
import sanic.router
|
||||
|
||||
seed("Pack my box with five dozen liquor jugs.")
|
||||
|
||||
# Disable Caching for testing purpose
|
||||
sanic.router.ROUTER_CACHE_SIZE = 0
|
||||
|
||||
|
||||
class TestSanicRouteResolution:
|
||||
@mark.asyncio
|
||||
async def test_resolve_route_no_arg_string_path(
|
||||
self, sanic_router, route_generator, benchmark
|
||||
):
|
||||
simple_routes = route_generator.generate_random_direct_route(
|
||||
max_route_depth=4
|
||||
)
|
||||
router, simple_routes = sanic_router(route_details=simple_routes)
|
||||
route_to_call = choice(simple_routes)
|
||||
|
||||
result = benchmark.pedantic(
|
||||
router._get,
|
||||
("/{}".format(route_to_call[-1]), route_to_call[0], "localhost"),
|
||||
iterations=1000,
|
||||
rounds=1000,
|
||||
)
|
||||
assert await result[0](None) == 1
|
||||
|
||||
@mark.asyncio
|
||||
async def test_resolve_route_with_typed_args(
|
||||
self, sanic_router, route_generator, benchmark
|
||||
):
|
||||
typed_routes = route_generator.add_typed_parameters(
|
||||
route_generator.generate_random_direct_route(max_route_depth=4),
|
||||
max_route_depth=8,
|
||||
)
|
||||
router, typed_routes = sanic_router(route_details=typed_routes)
|
||||
route_to_call = choice(typed_routes)
|
||||
url = route_generator.generate_url_for_template(
|
||||
template=route_to_call[-1]
|
||||
)
|
||||
|
||||
print("{} -> {}".format(route_to_call[-1], url))
|
||||
|
||||
result = benchmark.pedantic(
|
||||
router._get,
|
||||
("/{}".format(url), route_to_call[0], "localhost"),
|
||||
iterations=1000,
|
||||
rounds=1000,
|
||||
)
|
||||
assert await result[0](None) == 1
|
|
@ -1,12 +1,130 @@
|
|||
import random
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.router import RouteExists, Router
|
||||
|
||||
random.seed("Pack my box with five dozen liquor jugs.")
|
||||
|
||||
if sys.platform in ["win32", "cygwin"]:
|
||||
collect_ignore = ["test_worker.py"]
|
||||
|
||||
|
||||
async def _handler(request):
|
||||
"""
|
||||
Dummy placeholder method used for route resolver when creating a new
|
||||
route into the sanic router. This router is not actually called by the
|
||||
sanic app. So do not worry about the arguments to this method.
|
||||
|
||||
If you change the return value of this method, make sure to propagate the
|
||||
change to any test case that leverages RouteStringGenerator.
|
||||
"""
|
||||
return 1
|
||||
|
||||
|
||||
TYPE_TO_GENERATOR_MAP = {
|
||||
"string": lambda: "".join(
|
||||
[random.choice(string.ascii_letters + string.digits) for _ in range(4)]
|
||||
),
|
||||
"int": lambda: random.choice(range(1000000)),
|
||||
"number": lambda: random.random(),
|
||||
"alpha": lambda: "".join(
|
||||
[random.choice(string.ascii_letters) for _ in range(4)]
|
||||
),
|
||||
"uuid": lambda: str(uuid.uuid1()),
|
||||
}
|
||||
|
||||
|
||||
class RouteStringGenerator:
|
||||
|
||||
ROUTE_COUNT_PER_DEPTH = 100
|
||||
HTTP_METHODS = ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTION"]
|
||||
ROUTE_PARAM_TYPES = ["string", "int", "number", "alpha", "uuid"]
|
||||
|
||||
def generate_random_direct_route(self, max_route_depth=4):
|
||||
routes = []
|
||||
for depth in range(1, max_route_depth + 1):
|
||||
for _ in range(self.ROUTE_COUNT_PER_DEPTH):
|
||||
route = "/".join(
|
||||
[
|
||||
TYPE_TO_GENERATOR_MAP.get("string")()
|
||||
for _ in range(depth)
|
||||
]
|
||||
)
|
||||
route = route.replace(".", "", -1)
|
||||
route_detail = (random.choice(self.HTTP_METHODS), route)
|
||||
|
||||
if route_detail not in routes:
|
||||
routes.append(route_detail)
|
||||
return routes
|
||||
|
||||
def add_typed_parameters(self, current_routes, max_route_depth=8):
|
||||
routes = []
|
||||
for method, route in current_routes:
|
||||
current_length = len(route.split("/"))
|
||||
new_route_part = "/".join(
|
||||
[
|
||||
"<{}:{}>".format(
|
||||
TYPE_TO_GENERATOR_MAP.get("string")(),
|
||||
random.choice(self.ROUTE_PARAM_TYPES),
|
||||
)
|
||||
for _ in range(max_route_depth - current_length)
|
||||
]
|
||||
)
|
||||
route = "/".join([route, new_route_part])
|
||||
route = route.replace(".", "", -1)
|
||||
routes.append((method, route))
|
||||
return routes
|
||||
|
||||
@staticmethod
|
||||
def generate_url_for_template(template):
|
||||
url = template
|
||||
for pattern, param_type in re.findall(
|
||||
re.compile(r"((?:<\w+:(string|int|number|alpha|uuid)>)+)"),
|
||||
template,
|
||||
):
|
||||
value = TYPE_TO_GENERATOR_MAP.get(param_type)()
|
||||
url = url.replace(pattern, str(value), -1)
|
||||
return url
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def sanic_router():
|
||||
# noinspection PyProtectedMember
|
||||
def _setup(route_details: tuple) -> (Router, tuple):
|
||||
router = Router()
|
||||
added_router = []
|
||||
for method, route in route_details:
|
||||
try:
|
||||
router._add(
|
||||
uri="/{}".format(route),
|
||||
methods=frozenset({method}),
|
||||
host="localhost",
|
||||
handler=_handler,
|
||||
)
|
||||
added_router.append((method, route))
|
||||
except RouteExists:
|
||||
pass
|
||||
return router, added_router
|
||||
|
||||
return _setup
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def route_generator() -> RouteStringGenerator:
|
||||
return RouteStringGenerator()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def url_param_generator():
|
||||
return TYPE_TO_GENERATOR_MAP
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(request):
|
||||
return Sanic(request.node.name)
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from inspect import isawaitable
|
||||
import pytest
|
||||
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
def uvloop_installed():
|
||||
try:
|
||||
import uvloop
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def test_app_loop_running(app):
|
||||
@app.get("/test")
|
||||
async def handler(request):
|
||||
|
@ -17,9 +28,35 @@ def test_app_loop_running(app):
|
|||
assert response.text == "pass"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 7), reason="requires python3.7 or higher"
|
||||
)
|
||||
def test_create_asyncio_server(app):
|
||||
if not uvloop_installed():
|
||||
loop = asyncio.get_event_loop()
|
||||
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
|
||||
assert isawaitable(asyncio_srv_coro)
|
||||
srv = loop.run_until_complete(asyncio_srv_coro)
|
||||
assert srv.is_serving() is True
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 7), reason="requires python3.7 or higher"
|
||||
)
|
||||
def test_asyncio_server_start_serving(app):
|
||||
if not uvloop_installed():
|
||||
loop = asyncio.get_event_loop()
|
||||
asyncio_srv_coro = app.create_server(
|
||||
return_asyncio_server=True,
|
||||
asyncio_server_kwargs=dict(start_serving=False),
|
||||
)
|
||||
srv = loop.run_until_complete(asyncio_srv_coro)
|
||||
assert srv.is_serving() is False
|
||||
|
||||
|
||||
def test_app_loop_not_running(app):
|
||||
with pytest.raises(SanicException) as excinfo:
|
||||
app.loop
|
||||
_ = app.loop
|
||||
|
||||
assert str(excinfo.value) == (
|
||||
"Loop can only be retrieved after the app has started "
|
||||
|
@ -103,7 +140,6 @@ def test_handle_request_with_nested_exception(app, monkeypatch):
|
|||
@app.get("/")
|
||||
def handler(request):
|
||||
raise Exception
|
||||
return text("OK")
|
||||
|
||||
request, response = app.test_client.get("/")
|
||||
assert response.status == 500
|
||||
|
@ -125,7 +161,6 @@ def test_handle_request_with_nested_exception_debug(app, monkeypatch):
|
|||
@app.get("/")
|
||||
def handler(request):
|
||||
raise Exception
|
||||
return text("OK")
|
||||
|
||||
request, response = app.test_client.get("/", debug=True)
|
||||
assert response.status == 500
|
||||
|
@ -149,14 +184,13 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
|
|||
@app.get("/")
|
||||
def handler(request):
|
||||
raise Exception
|
||||
return text("OK")
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
request, response = app.test_client.get("/")
|
||||
assert response.status == 500
|
||||
assert response.text == "Error: Mock SanicException"
|
||||
assert caplog.record_tuples[0] == (
|
||||
assert (
|
||||
"sanic.root",
|
||||
logging.ERROR,
|
||||
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
|
||||
)
|
||||
) in caplog.record_tuples
|
||||
|
|
180
tests/test_blueprint_group.py
Normal file
180
tests/test_blueprint_group.py
Normal file
|
@ -0,0 +1,180 @@
|
|||
from pytest import raises
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.request import Request
|
||||
from sanic.response import text, HTTPResponse
|
||||
|
||||
MIDDLEWARE_INVOKE_COUNTER = {"request": 0, "response": 0}
|
||||
|
||||
AUTH = "dGVzdDp0ZXN0Cg=="
|
||||
|
||||
|
||||
def test_bp_group_indexing(app: Sanic):
|
||||
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||
|
||||
group = Blueprint.group(blueprint_1, blueprint_2)
|
||||
assert group[0] == blueprint_1
|
||||
|
||||
with raises(expected_exception=IndexError) as e:
|
||||
_ = group[3]
|
||||
|
||||
|
||||
def test_bp_group_with_additional_route_params(app: Sanic):
|
||||
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||
|
||||
@blueprint_1.route(
|
||||
"/request_path", methods=frozenset({"PUT", "POST"}), version=2
|
||||
)
|
||||
def blueprint_1_v2_method_with_put_and_post(request: Request):
|
||||
if request.method == "PUT":
|
||||
return text("PUT_OK")
|
||||
elif request.method == "POST":
|
||||
return text("POST_OK")
|
||||
|
||||
@blueprint_2.route(
|
||||
"/route/<param>", methods=frozenset({"DELETE", "PATCH"}), name="test"
|
||||
)
|
||||
def blueprint_2_named_method(request: Request, param):
|
||||
if request.method == "DELETE":
|
||||
return text("DELETE_{}".format(param))
|
||||
elif request.method == "PATCH":
|
||||
return text("PATCH_{}".format(param))
|
||||
|
||||
blueprint_group = Blueprint.group(
|
||||
blueprint_1, blueprint_2, url_prefix="/api"
|
||||
)
|
||||
|
||||
@blueprint_group.middleware("request")
|
||||
def authenticate_request(request: Request):
|
||||
global AUTH
|
||||
auth = request.headers.get("authorization")
|
||||
if auth:
|
||||
# Dummy auth check. We can have anything here and it's fine.
|
||||
if AUTH not in auth:
|
||||
return text("Unauthorized", status=401)
|
||||
else:
|
||||
return text("Unauthorized", status=401)
|
||||
|
||||
@blueprint_group.middleware("response")
|
||||
def enhance_response_middleware(request: Request, response: HTTPResponse):
|
||||
response.headers.add("x-test-middleware", "value")
|
||||
|
||||
app.blueprint(blueprint_group)
|
||||
|
||||
header = {"authorization": " ".join(["Basic", AUTH])}
|
||||
_, response = app.test_client.put(
|
||||
"/v2/api/bp1/request_path", headers=header
|
||||
)
|
||||
assert response.text == "PUT_OK"
|
||||
assert response.headers.get("x-test-middleware") == "value"
|
||||
|
||||
_, response = app.test_client.post(
|
||||
"/v2/api/bp1/request_path", headers=header
|
||||
)
|
||||
assert response.text == "POST_OK"
|
||||
|
||||
_, response = app.test_client.delete("/api/bp2/route/bp2", headers=header)
|
||||
assert response.text == "DELETE_bp2"
|
||||
|
||||
_, response = app.test_client.patch("/api/bp2/route/bp2", headers=header)
|
||||
assert response.text == "PATCH_bp2"
|
||||
|
||||
_, response = app.test_client.get("/v2/api/bp1/request_path")
|
||||
assert response.status == 401
|
||||
|
||||
|
||||
def test_bp_group(app: Sanic):
|
||||
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||
|
||||
@blueprint_1.route("/")
|
||||
def blueprint_1_default_route(request):
|
||||
return text("BP1_OK")
|
||||
|
||||
@blueprint_2.route("/")
|
||||
def blueprint_2_default_route(request):
|
||||
return text("BP2_OK")
|
||||
|
||||
blueprint_group_1 = Blueprint.group(
|
||||
blueprint_1, blueprint_2, url_prefix="/bp"
|
||||
)
|
||||
|
||||
blueprint_3 = Blueprint("blueprint_3", url_prefix="/bp3")
|
||||
|
||||
@blueprint_group_1.middleware("request")
|
||||
def blueprint_group_1_middleware(request):
|
||||
global MIDDLEWARE_INVOKE_COUNTER
|
||||
MIDDLEWARE_INVOKE_COUNTER["request"] += 1
|
||||
|
||||
@blueprint_3.route("/")
|
||||
def blueprint_3_default_route(request):
|
||||
return text("BP3_OK")
|
||||
|
||||
blueprint_group_2 = Blueprint.group(
|
||||
blueprint_group_1, blueprint_3, url_prefix="/api"
|
||||
)
|
||||
|
||||
@blueprint_group_2.middleware("response")
|
||||
def blueprint_group_2_middleware(request, response):
|
||||
global MIDDLEWARE_INVOKE_COUNTER
|
||||
MIDDLEWARE_INVOKE_COUNTER["response"] += 1
|
||||
|
||||
app.blueprint(blueprint_group_2)
|
||||
|
||||
@app.route("/")
|
||||
def app_default_route(request):
|
||||
return text("APP_OK")
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
assert response.text == "APP_OK"
|
||||
|
||||
_, response = app.test_client.get("/api/bp/bp1")
|
||||
assert response.text == "BP1_OK"
|
||||
|
||||
_, response = app.test_client.get("/api/bp/bp2")
|
||||
assert response.text == "BP2_OK"
|
||||
|
||||
_, response = app.test_client.get("/api/bp3")
|
||||
assert response.text == "BP3_OK"
|
||||
|
||||
assert MIDDLEWARE_INVOKE_COUNTER["response"] == 4
|
||||
assert MIDDLEWARE_INVOKE_COUNTER["request"] == 4
|
||||
|
||||
|
||||
def test_bp_group_list_operations(app: Sanic):
|
||||
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
|
||||
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
|
||||
|
||||
@blueprint_1.route("/")
|
||||
def blueprint_1_default_route(request):
|
||||
return text("BP1_OK")
|
||||
|
||||
@blueprint_2.route("/")
|
||||
def blueprint_2_default_route(request):
|
||||
return text("BP2_OK")
|
||||
|
||||
blueprint_group_1 = Blueprint.group(
|
||||
blueprint_1, blueprint_2, url_prefix="/bp"
|
||||
)
|
||||
|
||||
blueprint_3 = Blueprint("blueprint_2", url_prefix="/bp3")
|
||||
|
||||
@blueprint_3.route("/second")
|
||||
def blueprint_3_second_route(request):
|
||||
return text("BP3_OK")
|
||||
|
||||
assert len(blueprint_group_1) == 2
|
||||
|
||||
blueprint_group_1.append(blueprint_3)
|
||||
assert len(blueprint_group_1) == 3
|
||||
|
||||
del blueprint_group_1[2]
|
||||
assert len(blueprint_group_1) == 2
|
||||
|
||||
blueprint_group_1[1] = blueprint_3
|
||||
assert len(blueprint_group_1) == 2
|
||||
|
||||
assert blueprint_group_1.url_prefix == "/bp"
|
|
@ -6,6 +6,7 @@ from textwrap import dedent
|
|||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.config import Config, DEFAULT_CONFIG
|
||||
from sanic.exceptions import PyFileError
|
||||
|
||||
|
||||
|
@ -47,6 +48,13 @@ def test_auto_load_env():
|
|||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
|
||||
def test_auto_load_bool_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "True"
|
||||
app = Sanic()
|
||||
assert app.config.TEST_ANSWER == True
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
|
||||
def test_dont_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic(load_env=False)
|
||||
|
@ -149,6 +157,115 @@ def test_overwrite_exisiting_config_ignore_lowercase(app):
|
|||
|
||||
|
||||
def test_missing_config(app):
|
||||
with pytest.raises(AttributeError) as e:
|
||||
app.config.NON_EXISTENT
|
||||
assert str(e.value) == ("Config has no 'NON_EXISTENT'")
|
||||
with pytest.raises(
|
||||
AttributeError, match="Config has no 'NON_EXISTENT'"
|
||||
) as e:
|
||||
_ = app.config.NON_EXISTENT
|
||||
|
||||
|
||||
def test_config_defaults():
|
||||
"""
|
||||
load DEFAULT_CONFIG
|
||||
"""
|
||||
conf = Config()
|
||||
for key, value in DEFAULT_CONFIG.items():
|
||||
assert getattr(conf, key) == value
|
||||
|
||||
|
||||
def test_config_custom_defaults():
|
||||
"""
|
||||
we should have all the variables from defaults rewriting them with custom defaults passed in
|
||||
Config
|
||||
"""
|
||||
custom_defaults = {
|
||||
"REQUEST_MAX_SIZE": 1,
|
||||
"KEEP_ALIVE": False,
|
||||
"ACCESS_LOG": False,
|
||||
}
|
||||
conf = Config(defaults=custom_defaults)
|
||||
for key, value in DEFAULT_CONFIG.items():
|
||||
if key in custom_defaults.keys():
|
||||
value = custom_defaults[key]
|
||||
assert getattr(conf, key) == value
|
||||
|
||||
|
||||
def test_config_custom_defaults_with_env():
|
||||
"""
|
||||
test that environment variables has higher priority than DEFAULT_CONFIG and passed defaults dict
|
||||
"""
|
||||
custom_defaults = {
|
||||
"REQUEST_MAX_SIZE123": 1,
|
||||
"KEEP_ALIVE123": False,
|
||||
"ACCESS_LOG123": False,
|
||||
}
|
||||
|
||||
environ_defaults = {
|
||||
"SANIC_REQUEST_MAX_SIZE123": "2",
|
||||
"SANIC_KEEP_ALIVE123": "True",
|
||||
"SANIC_ACCESS_LOG123": "False",
|
||||
}
|
||||
|
||||
for key, value in environ_defaults.items():
|
||||
environ[key] = value
|
||||
|
||||
conf = Config(defaults=custom_defaults)
|
||||
for key, value in DEFAULT_CONFIG.items():
|
||||
if "SANIC_" + key in environ_defaults.keys():
|
||||
value = environ_defaults["SANIC_" + key]
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
if value in ["True", "False"]:
|
||||
value = value == "True"
|
||||
|
||||
assert getattr(conf, key) == value
|
||||
|
||||
for key, value in environ_defaults.items():
|
||||
del environ[key]
|
||||
|
||||
|
||||
def test_config_access_log_passing_in_run(app):
|
||||
assert app.config.ACCESS_LOG == True
|
||||
|
||||
@app.listener("after_server_start")
|
||||
async def _request(sanic, loop):
|
||||
app.stop()
|
||||
|
||||
app.run(port=1340, access_log=False)
|
||||
assert app.config.ACCESS_LOG == False
|
||||
|
||||
app.run(port=1340, access_log=True)
|
||||
assert app.config.ACCESS_LOG == True
|
||||
|
||||
|
||||
async def test_config_access_log_passing_in_create_server(app):
|
||||
assert app.config.ACCESS_LOG == True
|
||||
|
||||
@app.listener("after_server_start")
|
||||
async def _request(sanic, loop):
|
||||
app.stop()
|
||||
|
||||
await app.create_server(
|
||||
port=1341, access_log=False, return_asyncio_server=True
|
||||
)
|
||||
assert app.config.ACCESS_LOG == False
|
||||
|
||||
await app.create_server(
|
||||
port=1342, access_log=True, return_asyncio_server=True
|
||||
)
|
||||
assert app.config.ACCESS_LOG == True
|
||||
|
||||
|
||||
def test_config_rewrite_keep_alive():
|
||||
config = Config()
|
||||
assert config.KEEP_ALIVE == DEFAULT_CONFIG["KEEP_ALIVE"]
|
||||
config = Config(keep_alive=True)
|
||||
assert config.KEEP_ALIVE == True
|
||||
config = Config(keep_alive=False)
|
||||
assert config.KEEP_ALIVE == False
|
||||
|
||||
# use defaults
|
||||
config = Config(defaults={"KEEP_ALIVE": False})
|
||||
assert config.KEEP_ALIVE == False
|
||||
config = Config(defaults={"KEEP_ALIVE": True})
|
||||
assert config.KEEP_ALIVE == True
|
||||
|
|
|
@ -2,7 +2,7 @@ from datetime import datetime, timedelta
|
|||
from http.cookies import SimpleCookie
|
||||
from sanic.response import text
|
||||
import pytest
|
||||
from sanic.cookies import Cookie
|
||||
from sanic.cookies import Cookie, DEFAULT_MAX_AGE
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
|
@ -100,7 +100,7 @@ def test_cookie_deletion(app):
|
|||
|
||||
assert int(response_cookies["i_want_to_die"]["max-age"]) == 0
|
||||
with pytest.raises(KeyError):
|
||||
response.cookies["i_never_existed"]
|
||||
_ = response.cookies["i_never_existed"]
|
||||
|
||||
|
||||
def test_cookie_reserved_cookie():
|
||||
|
@ -138,7 +138,7 @@ def test_cookie_set_same_key(app):
|
|||
assert response.cookies["test"].value == "pass"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("max_age", ["0", 30, "30"])
|
||||
@pytest.mark.parametrize("max_age", ["0", 30, 30.0, 30.1, "30", "test"])
|
||||
def test_cookie_max_age(app, max_age):
|
||||
cookies = {"test": "wait"}
|
||||
|
||||
|
@ -153,13 +153,14 @@ def test_cookie_max_age(app, max_age):
|
|||
assert response.status == 200
|
||||
|
||||
assert response.cookies["test"].value == "pass"
|
||||
assert response.cookies["test"]["max-age"] == str(max_age)
|
||||
|
||||
if str(max_age).isdigit() and int(max_age) == float(max_age):
|
||||
assert response.cookies["test"]["max-age"] == str(max_age)
|
||||
else:
|
||||
assert response.cookies["test"]["max-age"] == str(DEFAULT_MAX_AGE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expires",
|
||||
[datetime.now() + timedelta(seconds=60), "Fri, 21-Dec-2018 15:30:00 GMT"],
|
||||
)
|
||||
@pytest.mark.parametrize("expires", [datetime.now() + timedelta(seconds=60)])
|
||||
def test_cookie_expires(app, expires):
|
||||
cookies = {"test": "wait"}
|
||||
|
||||
|
@ -179,3 +180,11 @@ def test_cookie_expires(app, expires):
|
|||
expires = expires.strftime("%a, %d-%b-%Y %T GMT")
|
||||
|
||||
assert response.cookies["test"]["expires"] == expires
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expires", ["Fri, 21-Dec-2018 15:30:00 GMT"])
|
||||
def test_cookie_expires_illegal_instance_type(expires):
|
||||
c = Cookie("test_cookie", "value")
|
||||
with pytest.raises(expected_exception=TypeError) as e:
|
||||
c["expires"] = expires
|
||||
assert e.message == "Cookie 'expires' property must be a datetime"
|
||||
|
|
|
@ -39,5 +39,5 @@ def test_overload_dynamic_routes_exist(app):
|
|||
with pytest.raises(RouteExists):
|
||||
|
||||
@app.route("/overload/<param>", methods=["PUT", "DELETE"])
|
||||
async def handler3(request):
|
||||
async def handler3(request, param):
|
||||
return text("Duplicated")
|
||||
|
|
|
@ -74,7 +74,7 @@ def exception_app():
|
|||
|
||||
@app.route("/divide_by_zero")
|
||||
def handle_unhandled_exception(request):
|
||||
1 / 0
|
||||
_ = 1 / 0
|
||||
|
||||
@app.route("/error_in_error_handler_handler")
|
||||
def custom_error_handler(request):
|
||||
|
@ -82,7 +82,7 @@ def exception_app():
|
|||
|
||||
@app.exception(SanicExceptionTestException)
|
||||
def error_in_error_handler_handler(request, exception):
|
||||
1 / 0
|
||||
_ = 1 / 0
|
||||
|
||||
return app
|
||||
|
||||
|
|
|
@ -3,13 +3,15 @@ from sanic import Sanic
|
|||
import asyncio
|
||||
from asyncio import sleep as aio_sleep
|
||||
from sanic.response import text
|
||||
from sanic.config import Config
|
||||
from sanic import server
|
||||
import aiohttp
|
||||
from aiohttp import TCPConnector
|
||||
from sanic.testing import SanicTestClient, HOST, PORT
|
||||
|
||||
|
||||
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
|
||||
|
||||
|
||||
class ReuseableTCPConnector(TCPConnector):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
|
||||
|
@ -47,7 +49,7 @@ class ReuseableSanicTestClient(SanicTestClient):
|
|||
uri="/",
|
||||
gather_request=True,
|
||||
debug=False,
|
||||
server_kwargs={},
|
||||
server_kwargs={"return_asyncio_server": True},
|
||||
*request_args,
|
||||
**request_kwargs
|
||||
):
|
||||
|
@ -141,7 +143,7 @@ class ReuseableSanicTestClient(SanicTestClient):
|
|||
# loop, so the changes above are required too.
|
||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
||||
request_keepalive = kwargs.pop(
|
||||
"request_keepalive", Config.KEEP_ALIVE_TIMEOUT
|
||||
"request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"]
|
||||
)
|
||||
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
|
||||
url = uri
|
||||
|
@ -157,7 +159,7 @@ class ReuseableSanicTestClient(SanicTestClient):
|
|||
conn = self._tcp_connector
|
||||
else:
|
||||
conn = ReuseableTCPConnector(
|
||||
verify_ssl=False,
|
||||
ssl=False,
|
||||
loop=self._loop,
|
||||
keepalive_timeout=request_keepalive,
|
||||
)
|
||||
|
@ -191,12 +193,14 @@ class ReuseableSanicTestClient(SanicTestClient):
|
|||
return response
|
||||
|
||||
|
||||
Config.KEEP_ALIVE_TIMEOUT = 2
|
||||
Config.KEEP_ALIVE = True
|
||||
keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse")
|
||||
keep_alive_app_client_timeout = Sanic("test_ka_client_timeout")
|
||||
keep_alive_app_server_timeout = Sanic("test_ka_server_timeout")
|
||||
|
||||
keep_alive_timeout_app_reuse.config.update(CONFIG_FOR_TESTS)
|
||||
keep_alive_app_client_timeout.config.update(CONFIG_FOR_TESTS)
|
||||
keep_alive_app_server_timeout.config.update(CONFIG_FOR_TESTS)
|
||||
|
||||
|
||||
@keep_alive_timeout_app_reuse.route("/1")
|
||||
async def handler1(request):
|
||||
|
|
|
@ -5,13 +5,14 @@ from sanic.config import BASE_LOGO
|
|||
|
||||
try:
|
||||
import uvloop # noqa
|
||||
|
||||
ROW = 0
|
||||
except BaseException:
|
||||
ROW = 1
|
||||
|
||||
|
||||
def test_logo_base(app, caplog):
|
||||
server = app.create_server(debug=True)
|
||||
server = app.create_server(debug=True, return_asyncio_server=True)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop._stopping = False
|
||||
|
@ -30,7 +31,7 @@ def test_logo_base(app, caplog):
|
|||
def test_logo_false(app, caplog):
|
||||
app.config.LOGO = False
|
||||
|
||||
server = app.create_server(debug=True)
|
||||
server = app.create_server(debug=True, return_asyncio_server=True)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop._stopping = False
|
||||
|
@ -49,7 +50,7 @@ def test_logo_false(app, caplog):
|
|||
def test_logo_true(app, caplog):
|
||||
app.config.LOGO = True
|
||||
|
||||
server = app.create_server(debug=True)
|
||||
server = app.create_server(debug=True, return_asyncio_server=True)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop._stopping = False
|
||||
|
@ -68,7 +69,7 @@ def test_logo_true(app, caplog):
|
|||
def test_logo_custom(app, caplog):
|
||||
app.config.LOGO = "My Custom Logo"
|
||||
|
||||
server = app.create_server(debug=True)
|
||||
server = app.create_server(debug=True, return_asyncio_server=True)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop._stopping = False
|
||||
|
|
|
@ -73,6 +73,8 @@ def test_middleware_response_exception(app):
|
|||
|
||||
|
||||
def test_middleware_response_raise_cancelled_error(app, caplog):
|
||||
app.config.RESPONSE_TIMEOUT = 1
|
||||
|
||||
@app.middleware("response")
|
||||
async def process_response(request, response):
|
||||
raise CancelledError("CancelledError at response middleware")
|
||||
|
@ -84,12 +86,12 @@ def test_middleware_response_raise_cancelled_error(app, caplog):
|
|||
with caplog.at_level(logging.ERROR):
|
||||
reqrequest, response = app.test_client.get("/")
|
||||
|
||||
assert response.status == 503
|
||||
assert caplog.record_tuples[0] == (
|
||||
"sanic.root",
|
||||
logging.ERROR,
|
||||
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
|
||||
)
|
||||
assert response.status == 503
|
||||
assert (
|
||||
"sanic.root",
|
||||
logging.ERROR,
|
||||
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
def test_middleware_response_raise_exception(app, caplog):
|
||||
|
@ -101,16 +103,16 @@ def test_middleware_response_raise_exception(app, caplog):
|
|||
reqrequest, response = app.test_client.get("/")
|
||||
|
||||
assert response.status == 404
|
||||
assert caplog.record_tuples[0] == (
|
||||
assert (
|
||||
"sanic.root",
|
||||
logging.ERROR,
|
||||
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
|
||||
)
|
||||
assert caplog.record_tuples[1] == (
|
||||
) in caplog.record_tuples
|
||||
assert (
|
||||
"sanic.error",
|
||||
logging.ERROR,
|
||||
"Exception occurred in one of response middleware handlers",
|
||||
)
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
def test_middleware_override_request(app):
|
||||
|
|
|
@ -270,6 +270,21 @@ def test_request_stream_blueprint(app):
|
|||
|
||||
return stream(streaming)
|
||||
|
||||
async def post_add_route(request):
|
||||
assert isinstance(request.stream, StreamBuffer)
|
||||
|
||||
async def streaming(response):
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
await response.write(body.decode("utf-8"))
|
||||
|
||||
return stream(streaming)
|
||||
|
||||
bp.add_route(
|
||||
post_add_route, "/post/add_route", methods=["POST"], stream=True
|
||||
)
|
||||
app.blueprint(bp)
|
||||
|
||||
assert app.is_request_stream is True
|
||||
|
@ -314,6 +329,10 @@ def test_request_stream_blueprint(app):
|
|||
assert response.status == 200
|
||||
assert response.text == data
|
||||
|
||||
request, response = app.test_client.post("/post/add_route", data=data)
|
||||
assert response.status == 200
|
||||
assert response.text == data
|
||||
|
||||
|
||||
def test_request_stream_composition_view(app):
|
||||
"""for self.is_request_stream = True"""
|
||||
|
|
|
@ -3,7 +3,6 @@ from json import JSONDecodeError
|
|||
from sanic import Sanic
|
||||
import asyncio
|
||||
from sanic.response import text
|
||||
from sanic.config import Config
|
||||
import aiohttp
|
||||
from aiohttp import TCPConnector
|
||||
from sanic.testing import SanicTestClient, HOST
|
||||
|
@ -152,9 +151,7 @@ class DelayableSanicTestClient(SanicTestClient):
|
|||
host=HOST, port=self.port, uri=uri
|
||||
)
|
||||
conn = DelayableTCPConnector(
|
||||
pre_request_delay=self._request_delay,
|
||||
verify_ssl=False,
|
||||
loop=self._loop,
|
||||
pre_request_delay=self._request_delay, ssl=False, loop=self._loop
|
||||
)
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, connector=conn, loop=self._loop
|
||||
|
@ -183,9 +180,10 @@ class DelayableSanicTestClient(SanicTestClient):
|
|||
return response
|
||||
|
||||
|
||||
Config.REQUEST_TIMEOUT = 0.6
|
||||
request_timeout_default_app = Sanic("test_request_timeout_default")
|
||||
request_no_timeout_app = Sanic("test_request_no_timeout")
|
||||
request_timeout_default_app.config.REQUEST_TIMEOUT = 0.6
|
||||
request_no_timeout_app.config.REQUEST_TIMEOUT = 0.6
|
||||
|
||||
|
||||
@request_timeout_default_app.route("/1")
|
||||
|
|
|
@ -7,6 +7,8 @@ from urllib.parse import urlparse
|
|||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic import Blueprint
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE
|
||||
from sanic.response import json, text
|
||||
|
@ -132,7 +134,7 @@ def test_query_string(app):
|
|||
|
||||
def test_uri_template(app):
|
||||
@app.route("/foo/<id:int>/bar/<name:[A-z]+>")
|
||||
async def handler(request):
|
||||
async def handler(request, id, name):
|
||||
return text("OK")
|
||||
|
||||
request, response = app.test_client.get("/foo/123/bar/baz")
|
||||
|
@ -430,21 +432,41 @@ def test_request_string_representation(app):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
"payload,filename",
|
||||
[
|
||||
"------sanic\r\n"
|
||||
'Content-Disposition: form-data; filename="filename"; name="test"\r\n'
|
||||
"\r\n"
|
||||
"OK\r\n"
|
||||
"------sanic--\r\n",
|
||||
"------sanic\r\n"
|
||||
'content-disposition: form-data; filename="filename"; name="test"\r\n'
|
||||
"\r\n"
|
||||
'content-type: application/json; {"field": "value"}\r\n'
|
||||
"------sanic--\r\n",
|
||||
("------sanic\r\n"
|
||||
'Content-Disposition: form-data; filename="filename"; name="test"\r\n'
|
||||
"\r\n"
|
||||
"OK\r\n"
|
||||
"------sanic--\r\n", "filename"),
|
||||
("------sanic\r\n"
|
||||
'content-disposition: form-data; filename="filename"; name="test"\r\n'
|
||||
"\r\n"
|
||||
'content-type: application/json; {"field": "value"}\r\n'
|
||||
"------sanic--\r\n", "filename"),
|
||||
("------sanic\r\n"
|
||||
'Content-Disposition: form-data; filename=""; name="test"\r\n'
|
||||
"\r\n"
|
||||
"OK\r\n"
|
||||
"------sanic--\r\n", ""),
|
||||
("------sanic\r\n"
|
||||
'content-disposition: form-data; filename=""; name="test"\r\n'
|
||||
"\r\n"
|
||||
'content-type: application/json; {"field": "value"}\r\n'
|
||||
"------sanic--\r\n", ""),
|
||||
("------sanic\r\n"
|
||||
'Content-Disposition: form-data; filename*="utf-8\'\'filename_%C2%A0_test"; name="test"\r\n'
|
||||
"\r\n"
|
||||
"OK\r\n"
|
||||
"------sanic--\r\n", "filename_\u00A0_test"),
|
||||
("------sanic\r\n"
|
||||
'content-disposition: form-data; filename*="utf-8\'\'filename_%C2%A0_test"; name="test"\r\n'
|
||||
"\r\n"
|
||||
'content-type: application/json; {"field": "value"}\r\n'
|
||||
"------sanic--\r\n", "filename_\u00A0_test"),
|
||||
],
|
||||
)
|
||||
def test_request_multipart_files(app, payload):
|
||||
def test_request_multipart_files(app, payload, filename):
|
||||
@app.route("/", methods=["POST"])
|
||||
async def post(request):
|
||||
return text("OK")
|
||||
|
@ -452,7 +474,7 @@ def test_request_multipart_files(app, payload):
|
|||
headers = {"content-type": "multipart/form-data; boundary=----sanic"}
|
||||
|
||||
request, _ = app.test_client.post(data=payload, headers=headers)
|
||||
assert request.files.get("test").name == "filename"
|
||||
assert request.files.get("test").name == filename
|
||||
|
||||
|
||||
def test_request_multipart_file_with_json_content_type(app):
|
||||
|
@ -564,7 +586,7 @@ def test_request_repr(app):
|
|||
assert repr(request) == "<Request: GET />"
|
||||
|
||||
request.method = None
|
||||
assert repr(request) == "<Request>"
|
||||
assert repr(request) == "<Request: None />"
|
||||
|
||||
|
||||
def test_request_bool(app):
|
||||
|
@ -698,3 +720,42 @@ def test_request_form_invalid_content_type(app):
|
|||
request, response = app.test_client.post("/", json={"test": "OK"})
|
||||
|
||||
assert request.form == {}
|
||||
|
||||
|
||||
def test_endpoint_basic():
|
||||
app = Sanic()
|
||||
|
||||
@app.route("/")
|
||||
def my_unique_handler(request):
|
||||
return text("Hello")
|
||||
|
||||
request, response = app.test_client.get("/")
|
||||
|
||||
assert request.endpoint == "test_requests.my_unique_handler"
|
||||
|
||||
|
||||
def test_endpoint_named_app():
|
||||
app = Sanic("named")
|
||||
|
||||
@app.route("/")
|
||||
def my_unique_handler(request):
|
||||
return text("Hello")
|
||||
|
||||
request, response = app.test_client.get("/")
|
||||
|
||||
assert request.endpoint == "named.my_unique_handler"
|
||||
|
||||
|
||||
def test_endpoint_blueprint():
|
||||
bp = Blueprint("my_blueprint", url_prefix="/bp")
|
||||
|
||||
@bp.route("/")
|
||||
async def bp_root(request):
|
||||
return text("Hello")
|
||||
|
||||
app = Sanic("named")
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get("/bp")
|
||||
|
||||
assert request.endpoint == "named.my_blueprint.bp_root"
|
||||
|
|
|
@ -2,13 +2,15 @@ from sanic import Sanic
|
|||
import asyncio
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import ServiceUnavailable
|
||||
from sanic.config import Config
|
||||
|
||||
Config.RESPONSE_TIMEOUT = 1
|
||||
response_timeout_app = Sanic("test_response_timeout")
|
||||
response_timeout_default_app = Sanic("test_response_timeout_default")
|
||||
response_handler_cancelled_app = Sanic("test_response_handler_cancelled")
|
||||
|
||||
response_timeout_app.config.RESPONSE_TIMEOUT = 1
|
||||
response_timeout_default_app.config.RESPONSE_TIMEOUT = 1
|
||||
response_handler_cancelled_app.config.RESPONSE_TIMEOUT = 1
|
||||
|
||||
|
||||
@response_timeout_app.route("/1")
|
||||
async def handler_1(request):
|
||||
|
|
|
@ -83,7 +83,7 @@ async def test_trigger_before_events_create_server(app):
|
|||
async def init_db(app, loop):
|
||||
app.db = MySanicDb()
|
||||
|
||||
await app.create_server()
|
||||
await app.create_server(debug=True, return_asyncio_server=True)
|
||||
|
||||
assert hasattr(app, "db")
|
||||
assert isinstance(app.db, MySanicDb)
|
||||
|
|
|
@ -169,12 +169,28 @@ def test_fails_with_int_message(app):
|
|||
app.url_for("fail", **failing_kwargs)
|
||||
|
||||
expected_error = (
|
||||
'Value "not_int" for parameter `foo` '
|
||||
"does not match pattern for type `int`: \d+"
|
||||
r'Value "not_int" for parameter `foo` '
|
||||
r'does not match pattern for type `int`: -?\d+'
|
||||
)
|
||||
assert str(e.value) == expected_error
|
||||
|
||||
|
||||
def test_passes_with_negative_int_message(app):
|
||||
@app.route("path/<possibly_neg:int>/another-word")
|
||||
def good(request, possibly_neg):
|
||||
assert isinstance(possibly_neg, int)
|
||||
return text("this should pass with `{}`".format(possibly_neg))
|
||||
|
||||
u_plus_3 = app.url_for("good", possibly_neg=3)
|
||||
assert u_plus_3 == "/path/3/another-word", u_plus_3
|
||||
request, response = app.test_client.get(u_plus_3)
|
||||
assert response.text == "this should pass with `3`"
|
||||
u_neg_3 = app.url_for("good", possibly_neg=-3)
|
||||
assert u_neg_3 == "/path/-3/another-word", u_neg_3
|
||||
request, response = app.test_client.get(u_neg_3)
|
||||
assert response.text == "this should pass with `-3`"
|
||||
|
||||
|
||||
def test_fails_with_two_letter_string_message(app):
|
||||
@app.route(COMPLEX_PARAM_URL)
|
||||
def fail(request):
|
||||
|
@ -207,12 +223,26 @@ def test_fails_with_number_message(app):
|
|||
|
||||
expected_error = (
|
||||
'Value "foo" for parameter `some_number` '
|
||||
"does not match pattern for type `float`: [0-9\\\\.]+"
|
||||
"does not match pattern for type `float`: -?[0-9\\\\.]+"
|
||||
)
|
||||
|
||||
assert str(e.value) == expected_error
|
||||
|
||||
|
||||
@pytest.mark.parametrize("number", [3, -3, 13.123, -13.123])
|
||||
def test_passes_with_negative_number_message(app, number):
|
||||
@app.route("path/<possibly_neg:number>/another-word")
|
||||
def good(request, possibly_neg):
|
||||
assert isinstance(possibly_neg, (int, float))
|
||||
return text("this should pass with `{}`".format(possibly_neg))
|
||||
|
||||
u = app.url_for("good", possibly_neg=number)
|
||||
assert u == "/path/{}/another-word".format(number), u
|
||||
request, response = app.test_client.get(u)
|
||||
# For ``number``, it has been cast to a float - so a ``3`` becomes a ``3.0``
|
||||
assert response.text == "this should pass with `{}`".format(float(number))
|
||||
|
||||
|
||||
def test_adds_other_supplied_values_as_query_string(app):
|
||||
@app.route(COMPLEX_PARAM_URL)
|
||||
def passes(request):
|
||||
|
|
|
@ -24,12 +24,61 @@ def gunicorn_worker():
|
|||
worker.kill()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def gunicorn_worker_with_access_logs():
|
||||
command = (
|
||||
"gunicorn "
|
||||
"--bind 127.0.0.1:1338 "
|
||||
"--worker-class sanic.worker.GunicornWorker "
|
||||
"examples.simple_server:app"
|
||||
)
|
||||
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
|
||||
time.sleep(2)
|
||||
return worker
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def gunicorn_worker_with_env_var():
|
||||
command = (
|
||||
'env SANIC_ACCESS_LOG="False" '
|
||||
"gunicorn "
|
||||
"--bind 127.0.0.1:1339 "
|
||||
"--worker-class sanic.worker.GunicornWorker "
|
||||
"--log-level info "
|
||||
"examples.simple_server:app"
|
||||
)
|
||||
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
|
||||
time.sleep(2)
|
||||
return worker
|
||||
|
||||
|
||||
def test_gunicorn_worker(gunicorn_worker):
|
||||
with urllib.request.urlopen("http://localhost:1337/") as f:
|
||||
res = json.loads(f.read(100).decode())
|
||||
assert res["test"]
|
||||
|
||||
|
||||
def test_gunicorn_worker_no_logs(gunicorn_worker_with_env_var):
|
||||
"""
|
||||
if SANIC_ACCESS_LOG was set to False do not show access logs
|
||||
"""
|
||||
with urllib.request.urlopen("http://localhost:1339/") as _:
|
||||
gunicorn_worker_with_env_var.kill()
|
||||
assert not gunicorn_worker_with_env_var.stdout.read()
|
||||
|
||||
|
||||
def test_gunicorn_worker_with_logs(gunicorn_worker_with_access_logs):
|
||||
"""
|
||||
default - show access logs
|
||||
"""
|
||||
with urllib.request.urlopen("http://localhost:1338/") as _:
|
||||
gunicorn_worker_with_access_logs.kill()
|
||||
assert (
|
||||
b"(sanic.access)[INFO][127.0.0.1"
|
||||
in gunicorn_worker_with_access_logs.stdout.read()
|
||||
)
|
||||
|
||||
|
||||
class GunicornTestWorker(GunicornWorker):
|
||||
def __init__(self):
|
||||
self.app = mock.Mock()
|
||||
|
|
7
tox.ini
7
tox.ini
|
@ -8,7 +8,7 @@ setenv =
|
|||
{py35,py36,py37}-no-ext: SANIC_NO_UVLOOP=1
|
||||
deps =
|
||||
coverage
|
||||
pytest==3.3.2
|
||||
pytest==4.1.0
|
||||
pytest-cov
|
||||
pytest-sanic
|
||||
pytest-sugar
|
||||
|
@ -16,6 +16,7 @@ deps =
|
|||
chardet<=2.3.0
|
||||
beautifulsoup4
|
||||
gunicorn
|
||||
pytest-benchmark
|
||||
commands =
|
||||
pytest tests --cov sanic --cov-report= {posargs}
|
||||
- coverage combine --append
|
||||
|
@ -39,3 +40,7 @@ deps =
|
|||
pygments
|
||||
commands =
|
||||
python setup.py check -r -s
|
||||
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore:.*async with lock.* instead:DeprecationWarning
|
||||
|
|
Loading…
Reference in New Issue
Block a user