Merge branch 'master' into asgi-refactor-attempt
This commit is contained in:
commit
c68523150f
|
@ -2,11 +2,6 @@ version: "{branch}.{build}"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
matrix:
|
matrix:
|
||||||
- TOXENV: py35-no-ext
|
|
||||||
PYTHON: "C:\\Python35-x64"
|
|
||||||
PYTHON_VERSION: "3.5.x"
|
|
||||||
PYTHON_ARCH: "64"
|
|
||||||
|
|
||||||
- TOXENV: py36-no-ext
|
- TOXENV: py36-no-ext
|
||||||
PYTHON: "C:\\Python36-x64"
|
PYTHON: "C:\\Python36-x64"
|
||||||
PYTHON_VERSION: "3.6.x"
|
PYTHON_VERSION: "3.6.x"
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -16,3 +16,4 @@ docs/_api/
|
||||||
build/*
|
build/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist/*
|
dist/*
|
||||||
|
pip-wheel-metadata/
|
||||||
|
|
|
@ -5,10 +5,6 @@ cache:
|
||||||
- $HOME/.cache/pip
|
- $HOME/.cache/pip
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- env: TOX_ENV=py35
|
|
||||||
python: 3.5
|
|
||||||
- env: TOX_ENV=py35-no-ext
|
|
||||||
python: 3.5
|
|
||||||
- env: TOX_ENV=py36
|
- env: TOX_ENV=py36
|
||||||
python: 3.6
|
python: 3.6
|
||||||
- env: TOX_ENV=py36-no-ext
|
- env: TOX_ENV=py36-no-ext
|
||||||
|
@ -33,9 +29,9 @@ after_success:
|
||||||
- codecov
|
- codecov
|
||||||
deploy:
|
deploy:
|
||||||
provider: pypi
|
provider: pypi
|
||||||
user: channelcat
|
user: brewmaster
|
||||||
password:
|
password:
|
||||||
secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc=
|
secure: "GoawLwmbtJOgKB6AJ0ZSYUUnNwIoonseHBxaAUH3zu79TS/Afrq+yB3lsVaMSG0CbyDgN4FrfD1phT1NzbvZ1VcLIOTDtCrmpQ1kLDw+zwgF40ab8sp8fPkKVHHHfCCs1mjltHIpxQa5lZTJcAs6Bpi/lbUWWwYxFzSV8pHw4W4hY09EHUd2o+evLTSVxaploetSt725DJUYKICUr2eAtCC11IDnIW4CzBJEx6krVV3uhzfTJW0Ls17x0c6sdZ9icMnV/G9xO/eQH6RIHe4xcrWJ6cmLDNKoGAkJp+BKr1CeVVg7Jw/MzPjvZKL2/ki6Beue1y6GUIy7lOS7jPVaOEhJ23b0zQwFcLMZw+Tt+E3v6QfHk+B/WBBBnM3zUZed9UI+QyW8+lqLLt39sQX0FO0P3eaDh8qTXtUuon2jTyFMMAMTFRTNpJmpAzuBH9yeMmDeALPTh0HphI+BkoUl5q1QbWFYjjnZMH2CatApxpLybt9A7rwm//PbOG0TSI93GEKNQ4w5DYryKTfwHzRBptNSephJSuxZYEfJsmUtas5es1D7Fe0PkyjxNNSU+eO+8wsTlitLUsJO4k0jAgy+cEKdU7YJ3J0GZVXocSkrNnUfd2hQPcJ3UtEJx3hLqqr8EM7EZBAasc1yGHh36NFetclzFY24YPih0G1+XurhTys="
|
||||||
on:
|
on:
|
||||||
tags: true
|
tags: true
|
||||||
distributions: "sdist bdist_wheel"
|
distributions: "sdist bdist_wheel"
|
||||||
|
|
126
CHANGELOG.md
126
CHANGELOG.md
|
@ -1,3 +1,129 @@
|
||||||
|
Version 19.6
|
||||||
|
------------
|
||||||
|
19.6.0
|
||||||
|
- Changes:
|
||||||
|
- Remove `aiohttp` dependencey and create new `SanicTestClient` based upon
|
||||||
|
[`requests-async`](https://github.com/encode/requests-async).
|
||||||
|
|
||||||
|
- Deprecation:
|
||||||
|
- Support for Python 3.5
|
||||||
|
|
||||||
|
Note: Sanic will not support Python 3.5 from version 19.6 and forward. However,
|
||||||
|
version 18.12LTS will have its support period extended thru December 2020, and
|
||||||
|
therefore passing Python's official support version 3.5, which is set to expire
|
||||||
|
in September 2020.
|
||||||
|
|
||||||
|
Version 19.3
|
||||||
|
-------------
|
||||||
|
19.3.1
|
||||||
|
- Changes:
|
||||||
|
- [#1497](https://github.com/huge-success/sanic/pull/1497)
|
||||||
|
Add support for zero-length and RFC 5987 encoded filename for
|
||||||
|
multipart/form-data requests.
|
||||||
|
|
||||||
|
- [#1484](https://github.com/huge-success/sanic/pull/1484)
|
||||||
|
The type of `expires` attribute of `sanic.cookies.Cookie` is now
|
||||||
|
enforced to be of type `datetime`.
|
||||||
|
|
||||||
|
- [#1482](https://github.com/huge-success/sanic/pull/1482)
|
||||||
|
Add support for the `stream` parameter of `sanic.Sanic.add_route()`
|
||||||
|
available to `sanic.Blueprint.add_route()`.
|
||||||
|
|
||||||
|
- [#1481](https://github.com/huge-success/sanic/pull/1481)
|
||||||
|
Accept negative values for route parameters with type `int` or `number`.
|
||||||
|
|
||||||
|
- [#1476](https://github.com/huge-success/sanic/pull/1476)
|
||||||
|
Deprecated the use of `sanic.request.Request.raw_args` - it has a
|
||||||
|
fundamental flaw in which is drops repeated query string parameters.
|
||||||
|
Added `sanic.request.Request.query_args` as a replacement for the
|
||||||
|
original use-case.
|
||||||
|
|
||||||
|
- [#1472](https://github.com/huge-success/sanic/pull/1472)
|
||||||
|
Remove an unwanted `None` check in Request class `repr` implementation.
|
||||||
|
This changes the default `repr` of a Request from `<Request>` to
|
||||||
|
`<Request: None />`
|
||||||
|
|
||||||
|
- [#1470](https://github.com/huge-success/sanic/pull/1470)
|
||||||
|
Added 2 new parameters to `sanic.app.Sanic.create_server`:
|
||||||
|
- `return_asyncio_server` - whether to return an asyncio.Server.
|
||||||
|
- `asyncio_server_kwargs` - kwargs to pass to `loop.create_server` for
|
||||||
|
the event loop that sanic is using.
|
||||||
|
|
||||||
|
This is a breaking change.
|
||||||
|
|
||||||
|
- [#1499](https://github.com/huge-success/sanic/pull/1499)
|
||||||
|
Added a set of test cases that test and benchmark route resolution.
|
||||||
|
|
||||||
|
- [#1457](https://github.com/huge-success/sanic/pull/1457)
|
||||||
|
The type of the `"max-age"` value in a `sanic.cookies.Cookie` is now
|
||||||
|
enforced to be an integer. Non-integer values are replaced with `0`.
|
||||||
|
|
||||||
|
- [#1445](https://github.com/huge-success/sanic/pull/1445)
|
||||||
|
Added the `endpoint` attribute to an incoming `request`, containing the
|
||||||
|
name of the handler function.
|
||||||
|
|
||||||
|
- [#1423](https://github.com/huge-success/sanic/pull/1423)
|
||||||
|
Improved request streaming. `request.stream` is now a bounded-size buffer
|
||||||
|
instead of an unbounded queue. Callers must now call
|
||||||
|
`await request.stream.read()` instead of `await request.stream.get()`
|
||||||
|
to read each portion of the body.
|
||||||
|
|
||||||
|
This is a breaking change.
|
||||||
|
|
||||||
|
- Fixes:
|
||||||
|
- [#1502](https://github.com/huge-success/sanic/pull/1502)
|
||||||
|
Sanic was prefetching `time.time()` and updating it once per second to
|
||||||
|
avoid excessive `time.time()` calls. The implementation was observed to
|
||||||
|
cause memory leaks in some cases. The benefit of the prefetch appeared
|
||||||
|
to negligible, so this has been removed. Fixes
|
||||||
|
[#1500](https://github.com/huge-success/sanic/pull/1500)
|
||||||
|
|
||||||
|
- [#1501](https://github.com/huge-success/sanic/pull/1501)
|
||||||
|
Fix a bug in the auto-reloader when the process was launched as a module
|
||||||
|
i.e. `python -m init0.mod1` where the sanic server is started
|
||||||
|
in `init0/mod1.py` with `debug` enabled and imports another module in
|
||||||
|
`init0`.
|
||||||
|
|
||||||
|
- [#1376](https://github.com/huge-success/sanic/pull/1376)
|
||||||
|
Allow sanic test client to bind to a random port by specifying
|
||||||
|
`port=None` when constructing a `SanicTestClient`
|
||||||
|
|
||||||
|
- [#1399](https://github.com/huge-success/sanic/pull/1399)
|
||||||
|
Added the ability to specify middleware on a blueprint group, so that all
|
||||||
|
routes produced from the blueprints in the group have the middleware
|
||||||
|
applied.
|
||||||
|
|
||||||
|
- [#1442](https://github.com/huge-success/sanic/pull/1442)
|
||||||
|
Allow the the use the `SANIC_ACCESS_LOG` environment variable to
|
||||||
|
enable/disable the access log when not explicitly passed to `app.run()`.
|
||||||
|
This allows the access log to be disabled for example when running via
|
||||||
|
gunicorn.
|
||||||
|
|
||||||
|
- Developer infrastructure:
|
||||||
|
- [#1529](https://github.com/huge-success/sanic/pull/1529) Update project PyPI credentials
|
||||||
|
- [#1515](https://github.com/huge-success/sanic/pull/1515) fix linter issue causing travis build failures (fix #1514)
|
||||||
|
- [#1490](https://github.com/huge-success/sanic/pull/1490) Fix python version in doc build
|
||||||
|
- [#1478](https://github.com/huge-success/sanic/pull/1478) Upgrade setuptools version and use native docutils in doc build
|
||||||
|
- [#1464](https://github.com/huge-success/sanic/pull/1464) Upgrade pytest, and fix caplog unit tests
|
||||||
|
|
||||||
|
- Typos and Documentation:
|
||||||
|
- [#1516](https://github.com/huge-success/sanic/pull/1516) Fix typo at the exception documentation
|
||||||
|
- [#1510](https://github.com/huge-success/sanic/pull/1510) fix typo in Asyncio example
|
||||||
|
- [#1486](https://github.com/huge-success/sanic/pull/1486) Documentation typo
|
||||||
|
- [#1477](https://github.com/huge-success/sanic/pull/1477) Fix grammar in README.md
|
||||||
|
- [#1489](https://github.com/huge-success/sanic/pull/1489) Added "databases" to the extensions list
|
||||||
|
- [#1483](https://github.com/huge-success/sanic/pull/1483) Add sanic-zipkin to extensions list
|
||||||
|
- [#1487](https://github.com/huge-success/sanic/pull/1487) Removed link to deleted repo, Sanic-OAuth, from the extensions list
|
||||||
|
- [#1460](https://github.com/huge-success/sanic/pull/1460) 18.12 changelog
|
||||||
|
- [#1449](https://github.com/huge-success/sanic/pull/1449) Add example of amending request object
|
||||||
|
- [#1446](https://github.com/huge-success/sanic/pull/1446) Update README
|
||||||
|
- [#1444](https://github.com/huge-success/sanic/pull/1444) Update README
|
||||||
|
- [#1443](https://github.com/huge-success/sanic/pull/1443) Update README, including new logo
|
||||||
|
- [#1440](https://github.com/huge-success/sanic/pull/1440) fix minor type and pip install instruction mismatch
|
||||||
|
- [#1424](https://github.com/huge-success/sanic/pull/1424) Documentation Enhancements
|
||||||
|
|
||||||
|
Note: 19.3.0 was skipped for packagement purposes and not released on PyPI
|
||||||
|
|
||||||
Version 18.12
|
Version 18.12
|
||||||
-------------
|
-------------
|
||||||
18.12.0
|
18.12.0
|
||||||
|
|
|
@ -18,7 +18,7 @@ So assume you have already cloned the repo and are in the working directory with
|
||||||
a virtual environment already set up, then run:
|
a virtual environment already set up, then run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip3 install -e . "[.dev]"
|
pip3 install -e . ".[dev]"
|
||||||
```
|
```
|
||||||
|
|
||||||
# Dependency Changes
|
# Dependency Changes
|
||||||
|
@ -30,9 +30,9 @@ the document that explains the way `sanic` manages dependencies inside the `setu
|
||||||
| Dependency Type | Usage | Installation |
|
| Dependency Type | Usage | Installation |
|
||||||
| ------------------------------------------| -------------------------------------------------------------------------- | --------------------------- |
|
| ------------------------------------------| -------------------------------------------------------------------------- | --------------------------- |
|
||||||
| requirements | Bare minimum dependencies required for sanic to function | pip3 install -e . |
|
| requirements | Bare minimum dependencies required for sanic to function | pip3 install -e . |
|
||||||
| tests_require / extras_require['test'] | Dependencies required to run the Unit Tests for `sanic` | pip3 install -e '[.test]' |
|
| tests_require / extras_require['test'] | Dependencies required to run the Unit Tests for `sanic` | pip3 install -e '.[test]' |
|
||||||
| extras_require['dev'] | Additional Development requirements to add contributing | pip3 install -e '[.dev]' |
|
| extras_require['dev'] | Additional Development requirements to add contributing | pip3 install -e '.[dev]' |
|
||||||
| extras_require['docs'] | Dependencies required to enable building and enhancing sanic documentation | pip3 install -e '[.docs]' |
|
| extras_require['docs'] | Dependencies required to enable building and enhancing sanic documentation | pip3 install -e '.[docs]' |
|
||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2016-present Channel Cat
|
Copyright (c) 2016-present Sanic Community
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
6
Makefile
6
Makefile
|
@ -47,12 +47,12 @@ ifdef include_tests
|
||||||
isort -rc sanic tests
|
isort -rc sanic tests
|
||||||
else
|
else
|
||||||
$(info Sorting Imports)
|
$(info Sorting Imports)
|
||||||
isort -rc sanic
|
isort -rc sanic tests
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
black:
|
black:
|
||||||
black --config ./pyproject.toml sanic tests
|
black --config ./.black.toml sanic tests
|
||||||
|
|
||||||
fix-import: black
|
fix-import: black
|
||||||
isort -rc sanic
|
isort -rc sanic tests
|
||||||
|
|
|
@ -17,6 +17,8 @@ Sanic | Build fast. Run fast.
|
||||||
- | |PyPI| |PyPI version| |Wheel| |Supported implementations| |Code style black|
|
- | |PyPI| |PyPI version| |Wheel| |Supported implementations| |Code style black|
|
||||||
* - Support
|
* - Support
|
||||||
- | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby|
|
- | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby|
|
||||||
|
* - Stats
|
||||||
|
- | |Downloads|
|
||||||
|
|
||||||
.. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg
|
.. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg
|
||||||
:target: https://community.sanicframework.org/
|
:target: https://community.sanicframework.org/
|
||||||
|
@ -42,10 +44,13 @@ Sanic | Build fast. Run fast.
|
||||||
.. |Supported implementations| image:: https://img.shields.io/pypi/implementation/sanic.svg
|
.. |Supported implementations| image:: https://img.shields.io/pypi/implementation/sanic.svg
|
||||||
:alt: Supported implementations
|
:alt: Supported implementations
|
||||||
:target: https://pypi.python.org/pypi/sanic
|
:target: https://pypi.python.org/pypi/sanic
|
||||||
|
.. |Downloads| image:: https://pepy.tech/badge/sanic/month
|
||||||
|
:alt: Downloads
|
||||||
|
:target: https://pepy.tech/project/sanic
|
||||||
|
|
||||||
.. end-badges
|
.. 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 3.6+** 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/>`_.
|
`Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_.
|
||||||
|
|
||||||
|
@ -64,7 +69,7 @@ Installation
|
||||||
|
|
||||||
$ export SANIC_NO_UVLOOP=true
|
$ export SANIC_NO_UVLOOP=true
|
||||||
$ export SANIC_NO_UJSON=true
|
$ export SANIC_NO_UJSON=true
|
||||||
$ pip3 install sanic
|
$ pip3 install --no-binary :all: sanic
|
||||||
|
|
||||||
|
|
||||||
Hello World Example
|
Hello World Example
|
||||||
|
|
|
@ -33,6 +33,7 @@ Guides
|
||||||
sanic/changelog
|
sanic/changelog
|
||||||
sanic/contributing
|
sanic/contributing
|
||||||
sanic/api_reference
|
sanic/api_reference
|
||||||
|
sanic/asyncio_python37
|
||||||
|
|
||||||
|
|
||||||
Module Documentation
|
Module Documentation
|
||||||
|
|
|
@ -20,6 +20,15 @@ sanic.blueprints module
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
sanic.blueprint_group module
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: sanic.blueprint_group
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
sanic.config module
|
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.AbstractServer
|
||||||
|
|
||||||
|
|
||||||
|
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.remove(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_kwargs=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`
|
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
|
||||||
decorator or `bp.add_websocket_route` method.
|
decorator or `bp.add_websocket_route` method.
|
||||||
|
|
||||||
### Middleware
|
### Blueprint Middleware
|
||||||
|
|
||||||
Using blueprints allows you to also register middleware globally.
|
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')
|
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
|
||||||
|
|
||||||
Exceptions can be applied exclusively to blueprints globally.
|
Exceptions can be applied exclusively to blueprints globally.
|
||||||
|
|
|
@ -1,3 +1,129 @@
|
||||||
|
Version 19.6
|
||||||
|
------------
|
||||||
|
19.6.0
|
||||||
|
- Changes:
|
||||||
|
- Remove `aiohttp` dependencey and create new `SanicTestClient` based upon
|
||||||
|
[`requests-async`](https://github.com/encode/requests-async).
|
||||||
|
|
||||||
|
- Deprecation:
|
||||||
|
- Support for Python 3.5
|
||||||
|
|
||||||
|
Note: Sanic will not support Python 3.5 from version 19.6 and forward. However,
|
||||||
|
version 18.12LTS will have its support period extended thru December 2020, and
|
||||||
|
therefore passing Python's official support version 3.5, which is set to expire
|
||||||
|
in September 2020.
|
||||||
|
|
||||||
|
Version 19.3
|
||||||
|
-------------
|
||||||
|
19.3.1
|
||||||
|
- Changes:
|
||||||
|
- [#1497](https://github.com/huge-success/sanic/pull/1497)
|
||||||
|
Add support for zero-length and RFC 5987 encoded filename for
|
||||||
|
multipart/form-data requests.
|
||||||
|
|
||||||
|
- [#1484](https://github.com/huge-success/sanic/pull/1484)
|
||||||
|
The type of `expires` attribute of `sanic.cookies.Cookie` is now
|
||||||
|
enforced to be of type `datetime`.
|
||||||
|
|
||||||
|
- [#1482](https://github.com/huge-success/sanic/pull/1482)
|
||||||
|
Add support for the `stream` parameter of `sanic.Sanic.add_route()`
|
||||||
|
available to `sanic.Blueprint.add_route()`.
|
||||||
|
|
||||||
|
- [#1481](https://github.com/huge-success/sanic/pull/1481)
|
||||||
|
Accept negative values for route parameters with type `int` or `number`.
|
||||||
|
|
||||||
|
- [#1476](https://github.com/huge-success/sanic/pull/1476)
|
||||||
|
Deprecated the use of `sanic.request.Request.raw_args` - it has a
|
||||||
|
fundamental flaw in which is drops repeated query string parameters.
|
||||||
|
Added `sanic.request.Request.query_args` as a replacement for the
|
||||||
|
original use-case.
|
||||||
|
|
||||||
|
- [#1472](https://github.com/huge-success/sanic/pull/1472)
|
||||||
|
Remove an unwanted `None` check in Request class `repr` implementation.
|
||||||
|
This changes the default `repr` of a Request from `<Request>` to
|
||||||
|
`<Request: None />`
|
||||||
|
|
||||||
|
- [#1470](https://github.com/huge-success/sanic/pull/1470)
|
||||||
|
Added 2 new parameters to `sanic.app.Sanic.create_server`:
|
||||||
|
- `return_asyncio_server` - whether to return an asyncio.Server.
|
||||||
|
- `asyncio_server_kwargs` - kwargs to pass to `loop.create_server` for
|
||||||
|
the event loop that sanic is using.
|
||||||
|
|
||||||
|
This is a breaking change.
|
||||||
|
|
||||||
|
- [#1499](https://github.com/huge-success/sanic/pull/1499)
|
||||||
|
Added a set of test cases that test and benchmark route resolution.
|
||||||
|
|
||||||
|
- [#1457](https://github.com/huge-success/sanic/pull/1457)
|
||||||
|
The type of the `"max-age"` value in a `sanic.cookies.Cookie` is now
|
||||||
|
enforced to be an integer. Non-integer values are replaced with `0`.
|
||||||
|
|
||||||
|
- [#1445](https://github.com/huge-success/sanic/pull/1445)
|
||||||
|
Added the `endpoint` attribute to an incoming `request`, containing the
|
||||||
|
name of the handler function.
|
||||||
|
|
||||||
|
- [#1423](https://github.com/huge-success/sanic/pull/1423)
|
||||||
|
Improved request streaming. `request.stream` is now a bounded-size buffer
|
||||||
|
instead of an unbounded queue. Callers must now call
|
||||||
|
`await request.stream.read()` instead of `await request.stream.get()`
|
||||||
|
to read each portion of the body.
|
||||||
|
|
||||||
|
This is a breaking change.
|
||||||
|
|
||||||
|
- Fixes:
|
||||||
|
- [#1502](https://github.com/huge-success/sanic/pull/1502)
|
||||||
|
Sanic was prefetching `time.time()` and updating it once per second to
|
||||||
|
avoid excessive `time.time()` calls. The implementation was observed to
|
||||||
|
cause memory leaks in some cases. The benefit of the prefetch appeared
|
||||||
|
to negligible, so this has been removed. Fixes
|
||||||
|
[#1500](https://github.com/huge-success/sanic/pull/1500)
|
||||||
|
|
||||||
|
- [#1501](https://github.com/huge-success/sanic/pull/1501)
|
||||||
|
Fix a bug in the auto-reloader when the process was launched as a module
|
||||||
|
i.e. `python -m init0.mod1` where the sanic server is started
|
||||||
|
in `init0/mod1.py` with `debug` enabled and imports another module in
|
||||||
|
`init0`.
|
||||||
|
|
||||||
|
- [#1376](https://github.com/huge-success/sanic/pull/1376)
|
||||||
|
Allow sanic test client to bind to a random port by specifying
|
||||||
|
`port=None` when constructing a `SanicTestClient`
|
||||||
|
|
||||||
|
- [#1399](https://github.com/huge-success/sanic/pull/1399)
|
||||||
|
Added the ability to specify middleware on a blueprint group, so that all
|
||||||
|
routes produced from the blueprints in the group have the middleware
|
||||||
|
applied.
|
||||||
|
|
||||||
|
- [#1442](https://github.com/huge-success/sanic/pull/1442)
|
||||||
|
Allow the the use the `SANIC_ACCESS_LOG` environment variable to
|
||||||
|
enable/disable the access log when not explicitly passed to `app.run()`.
|
||||||
|
This allows the access log to be disabled for example when running via
|
||||||
|
gunicorn.
|
||||||
|
|
||||||
|
- Developer infrastructure:
|
||||||
|
- [#1529](https://github.com/huge-success/sanic/pull/1529) Update project PyPI credentials
|
||||||
|
- [#1515](https://github.com/huge-success/sanic/pull/1515) fix linter issue causing travis build failures (fix #1514)
|
||||||
|
- [#1490](https://github.com/huge-success/sanic/pull/1490) Fix python version in doc build
|
||||||
|
- [#1478](https://github.com/huge-success/sanic/pull/1478) Upgrade setuptools version and use native docutils in doc build
|
||||||
|
- [#1464](https://github.com/huge-success/sanic/pull/1464) Upgrade pytest, and fix caplog unit tests
|
||||||
|
|
||||||
|
- Typos and Documentation:
|
||||||
|
- [#1516](https://github.com/huge-success/sanic/pull/1516) Fix typo at the exception documentation
|
||||||
|
- [#1510](https://github.com/huge-success/sanic/pull/1510) fix typo in Asyncio example
|
||||||
|
- [#1486](https://github.com/huge-success/sanic/pull/1486) Documentation typo
|
||||||
|
- [#1477](https://github.com/huge-success/sanic/pull/1477) Fix grammar in README.md
|
||||||
|
- [#1489](https://github.com/huge-success/sanic/pull/1489) Added "databases" to the extensions list
|
||||||
|
- [#1483](https://github.com/huge-success/sanic/pull/1483) Add sanic-zipkin to extensions list
|
||||||
|
- [#1487](https://github.com/huge-success/sanic/pull/1487) Removed link to deleted repo, Sanic-OAuth, from the extensions list
|
||||||
|
- [#1460](https://github.com/huge-success/sanic/pull/1460) 18.12 changelog
|
||||||
|
- [#1449](https://github.com/huge-success/sanic/pull/1449) Add example of amending request object
|
||||||
|
- [#1446](https://github.com/huge-success/sanic/pull/1446) Update README
|
||||||
|
- [#1444](https://github.com/huge-success/sanic/pull/1444) Update README
|
||||||
|
- [#1443](https://github.com/huge-success/sanic/pull/1443) Update README, including new logo
|
||||||
|
- [#1440](https://github.com/huge-success/sanic/pull/1440) fix minor type and pip install instruction mismatch
|
||||||
|
- [#1424](https://github.com/huge-success/sanic/pull/1424) Documentation Enhancements
|
||||||
|
|
||||||
|
Note: 19.3.0 was skipped for packagement purposes and not released on PyPI
|
||||||
|
|
||||||
Version 18.12
|
Version 18.12
|
||||||
-------------
|
-------------
|
||||||
18.12.0
|
18.12.0
|
||||||
|
|
|
@ -86,7 +86,7 @@ DB_USER = 'appuser'
|
||||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
| ------------------------- | --------- | --------------------------------------------------------- |
|
| ------------------------- | ----------------- | --------------------------------------------------------------------------- |
|
||||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||||
| REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size |
|
| REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size |
|
||||||
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
|
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
|
||||||
|
@ -95,6 +95,9 @@ Out of the box there are just a few predefined values which can be overwritten w
|
||||||
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
|
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
|
||||||
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
|
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long to wait to force close non-idle connection (sec) |
|
||||||
| ACCESS_LOG | True | Disable or enable access log |
|
| ACCESS_LOG | True | Disable or enable access log |
|
||||||
|
| PROXIES_COUNT | -1 | The number of proxy servers in front of the app (e.g. nginx; see below) |
|
||||||
|
| FORWARDED_FOR_HEADER | "X-Forwarded-For" | The name of "X-Forwarded-For" HTTP header that contains client and proxy ip |
|
||||||
|
| REAL_IP_HEADER | "X-Real-IP" | The name of "X-Real-IP" HTTP header that contains real client ip |
|
||||||
|
|
||||||
### The different Timeout variables:
|
### The different Timeout variables:
|
||||||
|
|
||||||
|
@ -143,3 +146,17 @@ Firefox client hard keepalive limit = 115 seconds
|
||||||
Opera 11 client hard keepalive limit = 120 seconds
|
Opera 11 client hard keepalive limit = 120 seconds
|
||||||
Chrome 13+ client keepalive limit > 300+ seconds
|
Chrome 13+ client keepalive limit > 300+ seconds
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### About proxy servers and client ip
|
||||||
|
|
||||||
|
When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain ip of a proxy, typically `127.0.0.1`. To determine the real client ip, `X-Forwarded-For` and `X-Real-IP` HTTP headers are used. But client can fake these headers if they have not been overridden by a proxy. Sanic has a set of options to determine the level of confidence in these headers.
|
||||||
|
|
||||||
|
* If you have a single proxy, set `PROXIES_COUNT` to `1`. Then Sanic will use `X-Real-IP` if available or the last ip from `X-Forwarded-For`.
|
||||||
|
|
||||||
|
* If you have multiple proxies, set `PROXIES_COUNT` equal to their number to allow Sanic to select the correct ip from `X-Forwarded-For`.
|
||||||
|
|
||||||
|
* If you don't use a proxy, set `PROXIES_COUNT` to `0` to ignore these headers and prevent ip falsification.
|
||||||
|
|
||||||
|
* If you don't use `X-Real-IP` (e.g. your proxy sends only `X-Forwarded-For`), set `REAL_IP_HEADER` to an empty string.
|
||||||
|
|
||||||
|
The real ip will be available in `request.remote_addr`. If HTTP headers are unavailable or untrusted, `request.remote_addr` will be an empty string; in this case use `request.ip` instead.
|
||||||
|
|
|
@ -64,6 +64,26 @@ of the memory leak.
|
||||||
|
|
||||||
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
|
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
|
||||||
|
|
||||||
|
## Running behind a reverse proxy
|
||||||
|
|
||||||
|
Sanic can be used with a reverse proxy (e.g. nginx). There's a simple example of nginx configuration:
|
||||||
|
|
||||||
|
```
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name example.org;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to get real client ip, you should configure `X-Real-IP` and `X-Forwarded-For` HTTP headers and set `app.config.PROXIES_COUNT` to `1`; see the configuration page for more information.
|
||||||
|
|
||||||
## Disable debug logging
|
## Disable debug logging
|
||||||
|
|
||||||
To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
|
To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
|
||||||
|
@ -92,7 +112,7 @@ to run the app in general.
|
||||||
Here is an incomplete example (please see `run_async.py` in examples for something more practical):
|
Here is an incomplete example (please see `run_async.py` in examples for something more practical):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
server = app.create_server(host="0.0.0.0", port=8000)
|
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
task = asyncio.ensure_future(server)
|
task = asyncio.ensure_future(server)
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
|
|
|
@ -160,7 +160,7 @@ execution support provided by the ``pytest-xdist`` plugin.
|
||||||
Amending Request Object
|
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.
|
The ``request`` object in ``Sanic`` is a kind of ``dict`` object, this means that ``request`` object can be manipulated as a regular ``dict`` object.
|
||||||
|
|
||||||
.. literalinclude:: ../../examples/amending_request_object.py
|
.. literalinclude:: ../../examples/amending_request_object.py
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ app = Sanic()
|
||||||
app.error_handler.add(Exception, server_error_handler)
|
app.error_handler.add(Exception, server_error_handler)
|
||||||
```
|
```
|
||||||
|
|
||||||
In some cases, you might want want to add some more error handling
|
In some cases, you might want to add some more error handling
|
||||||
functionality to what is provided by default. In that case, you
|
functionality to what is provided by default. In that case, you
|
||||||
can subclass Sanic's default error handler as such:
|
can subclass Sanic's default error handler as such:
|
||||||
|
|
||||||
|
|
|
@ -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
|
- [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
|
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
|
||||||
- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic.
|
- [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-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-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.
|
- [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 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
|
- [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/))
|
- [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
|
## Unit Testing
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ A list of Sanic extensions created by the community.
|
||||||
## Monitoring and Reporting
|
## Monitoring and Reporting
|
||||||
|
|
||||||
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
|
- [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
|
## Sample Applications
|
||||||
|
|
|
@ -19,6 +19,8 @@ The following variables are accessible as properties on `Request` objects:
|
||||||
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
||||||
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
|
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
|
||||||
The request's `query_string` variable holds the unparsed string value.
|
The request's `query_string` variable holds the unparsed string value.
|
||||||
|
Property is providing the default parsing strategy. If you would like to change it look to the section below
|
||||||
|
(`Changing the default parsing rules of the queryset`).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
@ -28,9 +30,54 @@ The following variables are accessible as properties on `Request` objects:
|
||||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||||
```
|
```
|
||||||
|
|
||||||
- `raw_args` (dict) - On many cases you would need to access the url arguments in
|
- `query_args` (list) - On many cases you would need to access the url arguments in
|
||||||
a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the
|
a less packed form. `query_args` is the list of `(key, value)` tuples.
|
||||||
`raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
Property is providing the default parsing strategy. If you would like to change it look to the section below
|
||||||
|
(`Changing the default parsing rules of the queryset`).
|
||||||
|
For the same previous URL queryset `?key1=value1&key2=value2`, the
|
||||||
|
`query_args` list would look like `[('key1', 'value1'), ('key2', 'value2')]`.
|
||||||
|
And in case of the multiple params with the same key like `?key1=value1&key2=value2&key1=value3`
|
||||||
|
the `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2'), ('key1', 'value3')]`.
|
||||||
|
|
||||||
|
The difference between Request.args and Request.query_args
|
||||||
|
for the queryset `?key1=value1&key2=value2&key1=value3`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/test_request_args")
|
||||||
|
async def test_request_args(request):
|
||||||
|
return json({
|
||||||
|
"parsed": True,
|
||||||
|
"url": request.url,
|
||||||
|
"query_string": request.query_string,
|
||||||
|
"args": request.args,
|
||||||
|
"raw_args": request.raw_args,
|
||||||
|
"query_args": request.query_args,
|
||||||
|
})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"parsed":true,
|
||||||
|
"url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3",
|
||||||
|
"query_string":"key1=value1&key2=value2&key1=value3",
|
||||||
|
"args":{"key1":["value1","value3"],"key2":["value2"]},
|
||||||
|
"raw_args":{"key1":"value1","key2":"value2"},
|
||||||
|
"query_args":[["key1","value1"],["key2","value2"],["key1","value3"]]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`raw_args` contains only the first entry of `key1`. Will be deprecated in the future versions.
|
||||||
|
|
||||||
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
||||||
|
|
||||||
|
@ -106,6 +153,51 @@ The following variables are accessible as properties on `Request` objects:
|
||||||
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
||||||
|
|
||||||
|
|
||||||
|
## Changing the default parsing rules of the queryset
|
||||||
|
|
||||||
|
The default parameters that are using internally in `args` and `query_args` properties to parse queryset:
|
||||||
|
|
||||||
|
- `keep_blank_values` (bool): `False` - flag indicating whether blank values in
|
||||||
|
percent-encoded queries should be treated as blank strings.
|
||||||
|
A true value indicates that blanks should be retained as blank
|
||||||
|
strings. The default false value indicates that blank values
|
||||||
|
are to be ignored and treated as if they were not included.
|
||||||
|
- `strict_parsing` (bool): `False` - flag indicating what to do with parsing errors. If
|
||||||
|
false (the default), errors are silently ignored. If true,
|
||||||
|
errors raise a ValueError exception.
|
||||||
|
- `encoding` and `errors` (str): 'utf-8' and 'replace' - specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
|
||||||
|
If you would like to change that default parameters you could call `get_args` and `get_query_args` methods
|
||||||
|
with the new values.
|
||||||
|
|
||||||
|
For the queryset `/?test1=value1&test2=&test3=value3`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
@app.route("/query_string")
|
||||||
|
def query_string(request):
|
||||||
|
args_with_blank_values = request.get_args(keep_blank_values=True)
|
||||||
|
return json({
|
||||||
|
"parsed": True,
|
||||||
|
"url": request.url,
|
||||||
|
"args_with_blank_values": args_with_blank_values,
|
||||||
|
"query_string": request.query_string
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The output will be:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"parsed": true,
|
||||||
|
"url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3",
|
||||||
|
"args_with_blank_values": {"test1": ["value1""], "test2": "", "test3": ["value3"]},
|
||||||
|
"query_string": "test1=value1&test2=&test3=value3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Accessing values using `get` and `getlist`
|
## Accessing values using `get` and `getlist`
|
||||||
|
|
||||||
The request properties which return a dictionary actually return a subclass of
|
The request properties which return a dictionary actually return a subclass of
|
||||||
|
|
|
@ -55,11 +55,13 @@ from sanic import response
|
||||||
@app.route("/streaming")
|
@app.route("/streaming")
|
||||||
async def index(request):
|
async def index(request):
|
||||||
async def streaming_fn(response):
|
async def streaming_fn(response):
|
||||||
response.write('foo')
|
await response.write('foo')
|
||||||
response.write('bar')
|
await response.write('bar')
|
||||||
return response.stream(streaming_fn, content_type='text/plain')
|
return response.stream(streaming_fn, content_type='text/plain')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See [Streaming](streaming.md) for more information.
|
||||||
|
|
||||||
## File Streaming
|
## File Streaming
|
||||||
For large files, a combination of File and Streaming above
|
For large files, a combination of File and Streaming above
|
||||||
```python
|
```python
|
||||||
|
|
|
@ -48,7 +48,7 @@ app.run(host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
## Virtual Host
|
## 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
|
```python
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
|
|
@ -42,7 +42,7 @@ async def handler(request):
|
||||||
|
|
||||||
|
|
||||||
@bp.put('/bp_stream', stream=True)
|
@bp.put('/bp_stream', stream=True)
|
||||||
async def bp_handler(request):
|
async def bp_put_handler(request):
|
||||||
result = ''
|
result = ''
|
||||||
while True:
|
while True:
|
||||||
body = await request.stream.read()
|
body = await request.stream.read()
|
||||||
|
@ -52,6 +52,19 @@ async def bp_handler(request):
|
||||||
return text(result)
|
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):
|
async def post_handler(request):
|
||||||
result = ''
|
result = ''
|
||||||
while True:
|
while True:
|
||||||
|
@ -104,3 +117,27 @@ async def index(request):
|
||||||
|
|
||||||
return stream(stream_from_db)
|
return stream(stream_from_db)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If a client supports HTTP/1.1, Sanic will use [chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding); you can explicitly enable or disable it using `chunked` option of the `stream` function.
|
||||||
|
|
||||||
|
## File Streaming
|
||||||
|
|
||||||
|
Sanic provides `sanic.response.file_stream` function that is useful when you want to send a large file. It returns a `StreamingHTTPResponse` object and will use chunked transfer encoding by default; for this reason Sanic doesn't add `Content-Length` HTTP header in the response. If you want to use this header, you can disable chunked transfer encoding and add it manually:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from aiofiles import os as async_os
|
||||||
|
from sanic.response import file_stream
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def index(request):
|
||||||
|
file_path = "/srv/www/whatever.png"
|
||||||
|
|
||||||
|
file_stat = await async_os.stat(file_path)
|
||||||
|
headers = {"Content-Length": str(file_stat.st_size)}
|
||||||
|
|
||||||
|
return await file_stream(
|
||||||
|
file_path,
|
||||||
|
headers=headers,
|
||||||
|
chunked=False,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# Testing
|
# Testing
|
||||||
|
|
||||||
Sanic endpoints can be tested locally using the `test_client` object, which
|
Sanic endpoints can be tested locally using the `test_client` object, which
|
||||||
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
|
depends on the additional [`requests-async`](https://github.com/encode/requests-async)
|
||||||
library.
|
library, which implements an API that mirrors the `requests` library.
|
||||||
|
|
||||||
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
|
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
|
||||||
for you to run against your application. A simple example (using pytest) is like follows:
|
for you to run against your application. A simple example (using pytest) is like follows:
|
||||||
|
@ -21,7 +21,7 @@ def test_index_put_not_allowed():
|
||||||
```
|
```
|
||||||
|
|
||||||
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
|
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
|
||||||
your test request is executed against your application, using `aiohttp`.
|
your test request is executed against your application, using `requests-async`.
|
||||||
|
|
||||||
The `test_client` methods accept the following arguments and keyword arguments:
|
The `test_client` methods accept the following arguments and keyword arguments:
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ The `test_client` methods accept the following arguments and keyword arguments:
|
||||||
- `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run.
|
- `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run.
|
||||||
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
|
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
|
||||||
|
|
||||||
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the aiohttp ClientSession request.
|
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the request.
|
||||||
|
|
||||||
For example, to supply data to a GET request, you would do the following:
|
For example, to supply data to a GET request, you would do the following:
|
||||||
|
|
||||||
|
@ -55,8 +55,25 @@ def test_post_json_request_includes_data():
|
||||||
|
|
||||||
|
|
||||||
More information about
|
More information about
|
||||||
the available arguments to aiohttp can be found
|
the available arguments to `requests-async` can be found
|
||||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
[in the documentation for `requests`](https://2.python-requests.org/en/master/).
|
||||||
|
|
||||||
|
|
||||||
|
## Using a random port
|
||||||
|
|
||||||
|
If you need to test using a free unpriveleged port chosen by the kernel
|
||||||
|
instead of the default with `SanicTestClient`, you can do so by specifying
|
||||||
|
`port=None`. On most systems the port will be in the range 1024 to 65535.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Import the Sanic app, usually created with Sanic(__name__)
|
||||||
|
from external_server import app
|
||||||
|
from sanic.testing import SanicTestClient
|
||||||
|
|
||||||
|
def test_index_returns_200():
|
||||||
|
request, response = SanicTestClient(app, port=None).get('/')
|
||||||
|
assert response.status == 200
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## pytest-sanic
|
## pytest-sanic
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
name: py35
|
name: py36
|
||||||
dependencies:
|
dependencies:
|
||||||
- openssl=1.0.2g=0
|
- pip=18.1=py36_0
|
||||||
- pip=8.1.1=py35_0
|
- python=3.6=0
|
||||||
- python=3.5.1=0
|
- setuptools=40.4.3=py36_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:
|
- pip:
|
||||||
- uvloop>=0.5.3
|
|
||||||
- httptools>=0.0.10
|
- httptools>=0.0.10
|
||||||
|
- uvloop>=0.5.3
|
||||||
- ujson>=1.35
|
- ujson>=1.35
|
||||||
- aiofiles>=0.3.0
|
- aiofiles>=0.3.0
|
||||||
- websockets>=6.0
|
- websockets>=6.0,<7.0
|
||||||
- sphinxcontrib-asyncio>=0.2.0
|
|
||||||
- multidict>=4.0,<5.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
|
||||||
|
|
|
@ -76,7 +76,7 @@ async def test(request):
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||||
server = app.create_server(host="0.0.0.0", port=8000)
|
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.set_task_factory(context.task_factory)
|
loop.set_task_factory(context.task_factory)
|
||||||
task = asyncio.ensure_future(server)
|
task = asyncio.ensure_future(server)
|
||||||
|
|
|
@ -12,7 +12,7 @@ async def test(request):
|
||||||
return response.json({"answer": "42"})
|
return response.json({"answer": "42"})
|
||||||
|
|
||||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||||
server = app.create_server(host="0.0.0.0", port=8000)
|
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
task = asyncio.ensure_future(server)
|
task = asyncio.ensure_future(server)
|
||||||
signal(SIGINT, lambda s, f: loop.stop())
|
signal(SIGINT, lambda s, f: loop.stop())
|
||||||
|
|
|
@ -2,6 +2,6 @@ from sanic.app import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
|
|
||||||
__version__ = "18.12.0"
|
__version__ = "19.03.1"
|
||||||
|
|
||||||
__all__ = ["Sanic", "Blueprint"]
|
__all__ = ["Sanic", "Blueprint"]
|
||||||
|
|
40
sanic/app.py
40
sanic/app.py
|
@ -16,6 +16,7 @@ from typing import Any, Optional, Type, Union
|
||||||
from urllib.parse import urlencode, urlunparse
|
from urllib.parse import urlencode, urlunparse
|
||||||
|
|
||||||
from sanic import reloader_helpers
|
from sanic import reloader_helpers
|
||||||
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.config import BASE_LOGO, Config
|
from sanic.config import BASE_LOGO, Config
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.exceptions import SanicException, ServerError, URLBuildError
|
from sanic.exceptions import SanicException, ServerError, URLBuildError
|
||||||
|
@ -181,8 +182,14 @@ class Sanic:
|
||||||
strict_slashes = self.strict_slashes
|
strict_slashes = self.strict_slashes
|
||||||
|
|
||||||
def response(handler):
|
def response(handler):
|
||||||
args = [key for key in signature(handler).parameters.keys()]
|
args = list(signature(handler).parameters.keys())
|
||||||
if args:
|
|
||||||
|
if not args:
|
||||||
|
raise ValueError(
|
||||||
|
"Required parameter `request` missing "
|
||||||
|
"in the {0}() route?".format(handler.__name__)
|
||||||
|
)
|
||||||
|
|
||||||
if stream:
|
if stream:
|
||||||
handler.is_stream = stream
|
handler.is_stream = stream
|
||||||
|
|
||||||
|
@ -196,11 +203,6 @@ class Sanic:
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
return handler
|
return handler
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
"Required parameter `request` missing "
|
|
||||||
"in the {0}() route?".format(handler.__name__)
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -333,7 +335,7 @@ class Sanic:
|
||||||
name=None,
|
name=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **DELETE** *HTTP* method
|
Add an API URL under the **PATCH** *HTTP* method
|
||||||
|
|
||||||
:param uri: URL to be tagged to **PATCH** method of *HTTP*
|
:param uri: URL to be tagged to **PATCH** method of *HTTP*
|
||||||
:param host: Host IP or FQDN for the service to use
|
:param host: Host IP or FQDN for the service to use
|
||||||
|
@ -599,8 +601,10 @@ class Sanic:
|
||||||
:return: decorated method
|
:return: decorated method
|
||||||
"""
|
"""
|
||||||
if attach_to == "request":
|
if attach_to == "request":
|
||||||
|
if middleware not in self.request_middleware:
|
||||||
self.request_middleware.append(middleware)
|
self.request_middleware.append(middleware)
|
||||||
if attach_to == "response":
|
if attach_to == "response":
|
||||||
|
if middleware not in self.response_middleware:
|
||||||
self.response_middleware.appendleft(middleware)
|
self.response_middleware.appendleft(middleware)
|
||||||
return middleware
|
return middleware
|
||||||
|
|
||||||
|
@ -683,7 +687,7 @@ class Sanic:
|
||||||
:param options: option dictionary with blueprint defaults
|
:param options: option dictionary with blueprint defaults
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if isinstance(blueprint, (list, tuple)):
|
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
|
||||||
for item in blueprint:
|
for item in blueprint:
|
||||||
self.blueprint(item, **options)
|
self.blueprint(item, **options)
|
||||||
return
|
return
|
||||||
|
@ -879,8 +883,6 @@ class Sanic:
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Request Middleware
|
# Request Middleware
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
|
||||||
request.app = self
|
|
||||||
response = await self._run_request_middleware(request)
|
response = await self._run_request_middleware(request)
|
||||||
# No middleware results
|
# No middleware results
|
||||||
if not response:
|
if not response:
|
||||||
|
@ -1122,6 +1124,8 @@ class Sanic:
|
||||||
backlog: int = 100,
|
backlog: int = 100,
|
||||||
stop_event: Any = None,
|
stop_event: Any = None,
|
||||||
access_log: Optional[bool] = None,
|
access_log: Optional[bool] = None,
|
||||||
|
return_asyncio_server=False,
|
||||||
|
asyncio_server_kwargs=None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Asynchronous version of :func:`run`.
|
Asynchronous version of :func:`run`.
|
||||||
|
@ -1155,6 +1159,13 @@ class Sanic:
|
||||||
:type stop_event: None
|
:type stop_event: None
|
||||||
:param access_log: Enables writing access logs (slows server)
|
:param access_log: Enables writing access logs (slows server)
|
||||||
:type access_log: bool
|
: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
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1185,7 +1196,7 @@ class Sanic:
|
||||||
loop=get_event_loop(),
|
loop=get_event_loop(),
|
||||||
protocol=protocol,
|
protocol=protocol,
|
||||||
backlog=backlog,
|
backlog=backlog,
|
||||||
run_async=True,
|
run_async=return_asyncio_server,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Trigger before_start events
|
# Trigger before_start events
|
||||||
|
@ -1194,7 +1205,9 @@ class Sanic:
|
||||||
server_settings.get("loop"),
|
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):
|
async def trigger_events(self, events, loop):
|
||||||
"""Trigger events (functions or async)
|
"""Trigger events (functions or async)
|
||||||
|
@ -1274,6 +1287,7 @@ class Sanic:
|
||||||
"port": port,
|
"port": port,
|
||||||
"sock": sock,
|
"sock": sock,
|
||||||
"ssl": ssl,
|
"ssl": ssl,
|
||||||
|
"app": self,
|
||||||
"signal": Signal(),
|
"signal": Signal(),
|
||||||
"debug": debug,
|
"debug": debug,
|
||||||
"request_handler": self.handle_request,
|
"request_handler": self.handle_request,
|
||||||
|
|
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 collections import defaultdict, namedtuple
|
||||||
|
|
||||||
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.views import CompositionView
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
|
@ -78,10 +79,12 @@ class Blueprint:
|
||||||
for i in nested:
|
for i in nested:
|
||||||
if isinstance(i, (list, tuple)):
|
if isinstance(i, (list, tuple)):
|
||||||
yield from chain(i)
|
yield from chain(i)
|
||||||
|
elif isinstance(i, BlueprintGroup):
|
||||||
|
yield from i.blueprints
|
||||||
else:
|
else:
|
||||||
yield i
|
yield i
|
||||||
|
|
||||||
bps = []
|
bps = BlueprintGroup(url_prefix=url_prefix)
|
||||||
for bp in chain(blueprints):
|
for bp in chain(blueprints):
|
||||||
if bp.url_prefix is None:
|
if bp.url_prefix is None:
|
||||||
bp.url_prefix = ""
|
bp.url_prefix = ""
|
||||||
|
@ -212,6 +215,7 @@ class Blueprint:
|
||||||
strict_slashes=None,
|
strict_slashes=None,
|
||||||
version=None,
|
version=None,
|
||||||
name=None,
|
name=None,
|
||||||
|
stream=False,
|
||||||
):
|
):
|
||||||
"""Create a blueprint route from a function.
|
"""Create a blueprint route from a function.
|
||||||
|
|
||||||
|
@ -224,6 +228,7 @@ class Blueprint:
|
||||||
training */*
|
training */*
|
||||||
:param version: Blueprint Version
|
:param version: Blueprint Version
|
||||||
:param name: user defined route name for url_for
|
: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
|
:return: function or class instance
|
||||||
"""
|
"""
|
||||||
# Handle HTTPMethodView differently
|
# Handle HTTPMethodView differently
|
||||||
|
@ -246,6 +251,7 @@ class Blueprint:
|
||||||
methods=methods,
|
methods=methods,
|
||||||
host=host,
|
host=host,
|
||||||
strict_slashes=strict_slashes,
|
strict_slashes=strict_slashes,
|
||||||
|
stream=stream,
|
||||||
version=version,
|
version=version,
|
||||||
name=name,
|
name=name,
|
||||||
)(handler)
|
)(handler)
|
||||||
|
@ -323,6 +329,12 @@ class Blueprint:
|
||||||
middleware = args[0]
|
middleware = args[0]
|
||||||
args = []
|
args = []
|
||||||
return register_middleware(middleware)
|
return register_middleware(middleware)
|
||||||
|
else:
|
||||||
|
if kwargs.get("bp_group") and callable(args[0]):
|
||||||
|
middleware = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
kwargs.pop("bp_group")
|
||||||
|
return register_middleware(middleware)
|
||||||
else:
|
else:
|
||||||
return register_middleware
|
return register_middleware
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import types
|
import types
|
||||||
|
|
||||||
from distutils.util import strtobool
|
|
||||||
|
|
||||||
from sanic.exceptions import PyFileError
|
from sanic.exceptions import PyFileError
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,6 +25,9 @@ DEFAULT_CONFIG = {
|
||||||
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||||
"ACCESS_LOG": True,
|
"ACCESS_LOG": True,
|
||||||
|
"PROXIES_COUNT": -1,
|
||||||
|
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||||
|
"REAL_IP_HEADER": "X-Real-IP",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -127,6 +128,23 @@ class Config(dict):
|
||||||
self[config_key] = float(v)
|
self[config_key] = float(v)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
try:
|
try:
|
||||||
self[config_key] = bool(strtobool(v))
|
self[config_key] = strtobool(v)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self[config_key] = v
|
self[config_key] = v
|
||||||
|
|
||||||
|
|
||||||
|
def strtobool(val):
|
||||||
|
"""
|
||||||
|
This function was borrowed from distutils.utils. While distutils
|
||||||
|
is part of stdlib, it feels odd to use distutils in main application code.
|
||||||
|
|
||||||
|
The function was modified to walk its talk and actually return bool
|
||||||
|
and not int.
|
||||||
|
"""
|
||||||
|
val = val.lower()
|
||||||
|
if val in ("y", "yes", "t", "true", "on", "1"):
|
||||||
|
return True
|
||||||
|
elif val in ("n", "no", "f", "false", "off", "0"):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid truth value %r" % (val,))
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_MAX_AGE = 0
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# SimpleCookie
|
# SimpleCookie
|
||||||
|
@ -103,6 +107,14 @@ class Cookie(dict):
|
||||||
if key not in self._keys:
|
if key not in self._keys:
|
||||||
raise KeyError("Unknown cookie property")
|
raise KeyError("Unknown cookie property")
|
||||||
if value is not False:
|
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)
|
return super().__setitem__(key, value)
|
||||||
|
|
||||||
def encode(self, encoding):
|
def encode(self, encoding):
|
||||||
|
@ -126,16 +138,10 @@ class Cookie(dict):
|
||||||
except TypeError:
|
except TypeError:
|
||||||
output.append("%s=%s" % (self._keys[key], value))
|
output.append("%s=%s" % (self._keys[key], value))
|
||||||
elif key == "expires":
|
elif key == "expires":
|
||||||
try:
|
|
||||||
output.append(
|
output.append(
|
||||||
"%s=%s"
|
"%s=%s"
|
||||||
% (
|
% (self._keys[key], value.strftime("%a, %d-%b-%Y %T GMT"))
|
||||||
self._keys[key],
|
|
||||||
value.strftime("%a, %d-%b-%Y %T GMT"),
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
output.append("%s=%s" % (self._keys[key], value))
|
|
||||||
elif key in self._flags and self[key]:
|
elif key in self._flags and self[key]:
|
||||||
output.append(self._keys[key])
|
output.append(self._keys[key])
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -36,6 +36,14 @@ def _iter_module_files():
|
||||||
def _get_args_for_reloading():
|
def _get_args_for_reloading():
|
||||||
"""Returns the executable."""
|
"""Returns the executable."""
|
||||||
rv = [sys.executable]
|
rv = [sys.executable]
|
||||||
|
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)
|
rv.extend(sys.argv)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
@ -44,6 +52,7 @@ def restart_with_reloader():
|
||||||
"""Create a new process and a subprocess in it with the same arguments as
|
"""Create a new process and a subprocess in it with the same arguments as
|
||||||
this one.
|
this one.
|
||||||
"""
|
"""
|
||||||
|
cwd = os.getcwd()
|
||||||
args = _get_args_for_reloading()
|
args = _get_args_for_reloading()
|
||||||
new_environ = os.environ.copy()
|
new_environ = os.environ.copy()
|
||||||
new_environ["SANIC_SERVER_RUNNING"] = "true"
|
new_environ["SANIC_SERVER_RUNNING"] = "true"
|
||||||
|
@ -51,7 +60,7 @@ def restart_with_reloader():
|
||||||
worker_process = Process(
|
worker_process = Process(
|
||||||
target=subprocess.call,
|
target=subprocess.call,
|
||||||
args=(cmd,),
|
args=(cmd,),
|
||||||
kwargs=dict(shell=True, env=new_environ),
|
kwargs={"cwd": cwd, "shell": True, "env": new_environ},
|
||||||
)
|
)
|
||||||
worker_process.start()
|
worker_process.start()
|
||||||
return worker_process
|
return worker_process
|
||||||
|
|
178
sanic/request.py
178
sanic/request.py
|
@ -1,11 +1,13 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import email.utils
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
from cgi import parse_header
|
from cgi import parse_header
|
||||||
from collections import namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
from urllib.parse import parse_qs, urlunparse
|
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
||||||
|
|
||||||
from httptools import parse_url
|
from httptools import parse_url
|
||||||
|
|
||||||
|
@ -82,6 +84,7 @@ class Request(dict):
|
||||||
"headers",
|
"headers",
|
||||||
"method",
|
"method",
|
||||||
"parsed_args",
|
"parsed_args",
|
||||||
|
"parsed_not_grouped_args",
|
||||||
"parsed_files",
|
"parsed_files",
|
||||||
"parsed_form",
|
"parsed_form",
|
||||||
"parsed_json",
|
"parsed_json",
|
||||||
|
@ -92,11 +95,11 @@ class Request(dict):
|
||||||
"version",
|
"version",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, url_bytes, headers, version, method, transport):
|
def __init__(self, url_bytes, headers, version, method, transport, app):
|
||||||
self.raw_url = url_bytes
|
self.raw_url = url_bytes
|
||||||
# TODO: Content-Encoding detection
|
# TODO: Content-Encoding detection
|
||||||
self._parsed_url = parse_url(url_bytes)
|
self._parsed_url = parse_url(url_bytes)
|
||||||
self.app = None
|
self.app = app
|
||||||
|
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
self.version = version
|
self.version = version
|
||||||
|
@ -108,15 +111,14 @@ class Request(dict):
|
||||||
self.parsed_json = None
|
self.parsed_json = None
|
||||||
self.parsed_form = None
|
self.parsed_form = None
|
||||||
self.parsed_files = None
|
self.parsed_files = None
|
||||||
self.parsed_args = None
|
self.parsed_args = defaultdict(RequestParameters)
|
||||||
|
self.parsed_not_grouped_args = defaultdict(list)
|
||||||
self.uri_template = None
|
self.uri_template = None
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
self.stream = None
|
self.stream = None
|
||||||
self.endpoint = None
|
self.endpoint = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.method is None or not self.path:
|
|
||||||
return "<{0}>".format(self.__class__.__name__)
|
|
||||||
return "<{0}: {1} {2}>".format(
|
return "<{0}: {1} {2}>".format(
|
||||||
self.__class__.__name__, self.method, self.path
|
self.__class__.__name__, self.method, self.path
|
||||||
)
|
)
|
||||||
|
@ -200,21 +202,117 @@ class Request(dict):
|
||||||
|
|
||||||
return self.parsed_files
|
return self.parsed_files
|
||||||
|
|
||||||
@property
|
def get_args(
|
||||||
def args(self):
|
self,
|
||||||
if self.parsed_args is None:
|
keep_blank_values: bool = False,
|
||||||
|
strict_parsing: bool = False,
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
errors: str = "replace",
|
||||||
|
) -> RequestParameters:
|
||||||
|
"""
|
||||||
|
Method to parse `query_string` using `urllib.parse.parse_qs`.
|
||||||
|
This methods is used by `args` property.
|
||||||
|
Can be used directly if you need to change default parameters.
|
||||||
|
:param keep_blank_values: flag indicating whether blank values in
|
||||||
|
percent-encoded queries should be treated as blank strings.
|
||||||
|
A true value indicates that blanks should be retained as blank
|
||||||
|
strings. The default false value indicates that blank values
|
||||||
|
are to be ignored and treated as if they were not included.
|
||||||
|
:type keep_blank_values: bool
|
||||||
|
:param strict_parsing: flag indicating what to do with parsing errors.
|
||||||
|
If false (the default), errors are silently ignored. If true,
|
||||||
|
errors raise a ValueError exception.
|
||||||
|
:type strict_parsing: bool
|
||||||
|
:param encoding: specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type encoding: str
|
||||||
|
:param errors: specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type errors: str
|
||||||
|
:return: RequestParameters
|
||||||
|
"""
|
||||||
|
if not self.parsed_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]:
|
||||||
if self.query_string:
|
if self.query_string:
|
||||||
self.parsed_args = RequestParameters(
|
self.parsed_args[
|
||||||
parse_qs(self.query_string)
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
] = RequestParameters(
|
||||||
|
parse_qs(
|
||||||
|
qs=self.query_string,
|
||||||
|
keep_blank_values=keep_blank_values,
|
||||||
|
strict_parsing=strict_parsing,
|
||||||
|
encoding=encoding,
|
||||||
|
errors=errors,
|
||||||
)
|
)
|
||||||
else:
|
)
|
||||||
self.parsed_args = RequestParameters()
|
|
||||||
return self.parsed_args
|
return self.parsed_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]
|
||||||
|
|
||||||
|
args = property(get_args)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def raw_args(self):
|
def raw_args(self) -> dict:
|
||||||
|
if self.app.debug: # pragma: no cover
|
||||||
|
warnings.simplefilter("default")
|
||||||
|
warnings.warn(
|
||||||
|
"Use of raw_args will be deprecated in "
|
||||||
|
"the future versions. Please use args or query_args "
|
||||||
|
"properties instead",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
return {k: v[0] for k, v in self.args.items()}
|
return {k: v[0] for k, v in self.args.items()}
|
||||||
|
|
||||||
|
def get_query_args(
|
||||||
|
self,
|
||||||
|
keep_blank_values: bool = False,
|
||||||
|
strict_parsing: bool = False,
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
errors: str = "replace",
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
Method to parse `query_string` using `urllib.parse.parse_qsl`.
|
||||||
|
This methods is used by `query_args` property.
|
||||||
|
Can be used directly if you need to change default parameters.
|
||||||
|
:param keep_blank_values: flag indicating whether blank values in
|
||||||
|
percent-encoded queries should be treated as blank strings.
|
||||||
|
A true value indicates that blanks should be retained as blank
|
||||||
|
strings. The default false value indicates that blank values
|
||||||
|
are to be ignored and treated as if they were not included.
|
||||||
|
:type keep_blank_values: bool
|
||||||
|
:param strict_parsing: flag indicating what to do with parsing errors.
|
||||||
|
If false (the default), errors are silently ignored. If true,
|
||||||
|
errors raise a ValueError exception.
|
||||||
|
:type strict_parsing: bool
|
||||||
|
:param encoding: specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type encoding: str
|
||||||
|
:param errors: specify how to decode percent-encoded sequences
|
||||||
|
into Unicode characters, as accepted by the bytes.decode() method.
|
||||||
|
:type errors: str
|
||||||
|
:return: list
|
||||||
|
"""
|
||||||
|
if not self.parsed_not_grouped_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]:
|
||||||
|
if self.query_string:
|
||||||
|
self.parsed_not_grouped_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
] = parse_qsl(
|
||||||
|
qs=self.query_string,
|
||||||
|
keep_blank_values=keep_blank_values,
|
||||||
|
strict_parsing=strict_parsing,
|
||||||
|
encoding=encoding,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
return self.parsed_not_grouped_args[
|
||||||
|
(keep_blank_values, strict_parsing, encoding, errors)
|
||||||
|
]
|
||||||
|
|
||||||
|
query_args = property(get_query_args)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookies(self):
|
def cookies(self):
|
||||||
if self._cookies is None:
|
if self._cookies is None:
|
||||||
|
@ -257,19 +355,38 @@ class Request(dict):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def remote_addr(self):
|
def remote_addr(self):
|
||||||
"""Attempt to return the original client ip based on X-Forwarded-For.
|
"""Attempt to return the original client ip based on X-Forwarded-For
|
||||||
|
or X-Real-IP. If HTTP headers are unavailable or untrusted, returns
|
||||||
|
an empty string.
|
||||||
|
|
||||||
:return: original client ip.
|
:return: original client ip.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, "_remote_addr"):
|
if not hasattr(self, "_remote_addr"):
|
||||||
forwarded_for = self.headers.get("X-Forwarded-For", "").split(",")
|
if self.app.config.PROXIES_COUNT == 0:
|
||||||
|
self._remote_addr = ""
|
||||||
|
elif self.app.config.REAL_IP_HEADER and self.headers.get(
|
||||||
|
self.app.config.REAL_IP_HEADER
|
||||||
|
):
|
||||||
|
self._remote_addr = self.headers[
|
||||||
|
self.app.config.REAL_IP_HEADER
|
||||||
|
]
|
||||||
|
elif self.app.config.FORWARDED_FOR_HEADER:
|
||||||
|
forwarded_for = self.headers.get(
|
||||||
|
self.app.config.FORWARDED_FOR_HEADER, ""
|
||||||
|
).split(",")
|
||||||
remote_addrs = [
|
remote_addrs = [
|
||||||
addr
|
addr
|
||||||
for addr in [addr.strip() for addr in forwarded_for]
|
for addr in [addr.strip() for addr in forwarded_for]
|
||||||
if addr
|
if addr
|
||||||
]
|
]
|
||||||
if len(remote_addrs) > 0:
|
if self.app.config.PROXIES_COUNT == -1:
|
||||||
self._remote_addr = remote_addrs[0]
|
self._remote_addr = remote_addrs[0]
|
||||||
|
elif len(remote_addrs) >= self.app.config.PROXIES_COUNT:
|
||||||
|
self._remote_addr = remote_addrs[
|
||||||
|
-self.app.config.PROXIES_COUNT
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self._remote_addr = ""
|
||||||
else:
|
else:
|
||||||
self._remote_addr = ""
|
self._remote_addr = ""
|
||||||
return self._remote_addr
|
return self._remote_addr
|
||||||
|
@ -358,15 +475,28 @@ def parse_multipart_form(body, boundary):
|
||||||
)
|
)
|
||||||
|
|
||||||
if form_header_field == "content-disposition":
|
if form_header_field == "content-disposition":
|
||||||
file_name = form_parameters.get("filename")
|
|
||||||
field_name = form_parameters.get("name")
|
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":
|
elif form_header_field == "content-type":
|
||||||
content_type = form_header_value
|
content_type = form_header_value
|
||||||
content_charset = form_parameters.get("charset", "utf-8")
|
content_charset = form_parameters.get("charset", "utf-8")
|
||||||
|
|
||||||
if field_name:
|
if field_name:
|
||||||
post_data = form_part[line_index:-4]
|
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(
|
form_file = File(
|
||||||
type=content_type, name=file_name, body=post_data
|
type=content_type, name=file_name, body=post_data
|
||||||
)
|
)
|
||||||
|
@ -374,12 +504,6 @@ def parse_multipart_form(body, boundary):
|
||||||
files[field_name].append(form_file)
|
files[field_name].append(form_file)
|
||||||
else:
|
else:
|
||||||
files[field_name] = [form_file]
|
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:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Form-data field does not have a 'name' parameter "
|
"Form-data field does not have a 'name' parameter "
|
||||||
|
|
|
@ -59,16 +59,23 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||||
"status",
|
"status",
|
||||||
"content_type",
|
"content_type",
|
||||||
"headers",
|
"headers",
|
||||||
|
"chunked",
|
||||||
"_cookies",
|
"_cookies",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, streaming_fn, status=200, headers=None, content_type="text/plain"
|
self,
|
||||||
|
streaming_fn,
|
||||||
|
status=200,
|
||||||
|
headers=None,
|
||||||
|
content_type="text/plain",
|
||||||
|
chunked=True,
|
||||||
):
|
):
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
self.streaming_fn = streaming_fn
|
self.streaming_fn = streaming_fn
|
||||||
self.status = status
|
self.status = status
|
||||||
self.headers = CIMultiDict(headers or {})
|
self.headers = CIMultiDict(headers or {})
|
||||||
|
self.chunked = chunked
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
|
|
||||||
async def write(self, data):
|
async def write(self, data):
|
||||||
|
@ -79,7 +86,10 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||||
if type(data) != bytes:
|
if type(data) != bytes:
|
||||||
data = self._encode_body(data)
|
data = self._encode_body(data)
|
||||||
|
|
||||||
|
if self.chunked:
|
||||||
self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
||||||
|
else:
|
||||||
|
self.protocol.push_data(data)
|
||||||
await self.protocol.drain()
|
await self.protocol.drain()
|
||||||
|
|
||||||
async def stream(
|
async def stream(
|
||||||
|
@ -88,6 +98,8 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||||
"""Streams headers, runs the `streaming_fn` callback that writes
|
"""Streams headers, runs the `streaming_fn` callback that writes
|
||||||
content to the response body, then finalizes the response body.
|
content to the response body, then finalizes the response body.
|
||||||
"""
|
"""
|
||||||
|
if version != "1.1":
|
||||||
|
self.chunked = False
|
||||||
headers = self.get_headers(
|
headers = self.get_headers(
|
||||||
version,
|
version,
|
||||||
keep_alive=keep_alive,
|
keep_alive=keep_alive,
|
||||||
|
@ -96,6 +108,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||||
self.protocol.push_data(headers)
|
self.protocol.push_data(headers)
|
||||||
await self.protocol.drain()
|
await self.protocol.drain()
|
||||||
await self.streaming_fn(self)
|
await self.streaming_fn(self)
|
||||||
|
if self.chunked:
|
||||||
self.protocol.push_data(b"0\r\n\r\n")
|
self.protocol.push_data(b"0\r\n\r\n")
|
||||||
# no need to await drain here after this write, because it is the
|
# no need to await drain here after this write, because it is the
|
||||||
# very last thing we write and nothing needs to wait for it.
|
# very last thing we write and nothing needs to wait for it.
|
||||||
|
@ -109,6 +122,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||||
if keep_alive and keep_alive_timeout is not None:
|
if keep_alive and keep_alive_timeout is not None:
|
||||||
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
|
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
|
||||||
|
|
||||||
|
if self.chunked and version == "1.1":
|
||||||
self.headers["Transfer-Encoding"] = "chunked"
|
self.headers["Transfer-Encoding"] = "chunked"
|
||||||
self.headers.pop("Content-Length", None)
|
self.headers.pop("Content-Length", None)
|
||||||
self.headers["Content-Type"] = self.headers.get(
|
self.headers["Content-Type"] = self.headers.get(
|
||||||
|
@ -117,7 +131,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||||
|
|
||||||
headers = self._parse_headers()
|
headers = self._parse_headers()
|
||||||
|
|
||||||
if self.status is 200:
|
if self.status == 200:
|
||||||
status = b"OK"
|
status = b"OK"
|
||||||
else:
|
else:
|
||||||
status = STATUS_CODES.get(self.status)
|
status = STATUS_CODES.get(self.status)
|
||||||
|
@ -176,7 +190,7 @@ class HTTPResponse(BaseHTTPResponse):
|
||||||
|
|
||||||
headers = self._parse_headers()
|
headers = self._parse_headers()
|
||||||
|
|
||||||
if self.status is 200:
|
if self.status == 200:
|
||||||
status = b"OK"
|
status = b"OK"
|
||||||
else:
|
else:
|
||||||
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
|
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
|
||||||
|
@ -327,6 +341,7 @@ async def file_stream(
|
||||||
mime_type=None,
|
mime_type=None,
|
||||||
headers=None,
|
headers=None,
|
||||||
filename=None,
|
filename=None,
|
||||||
|
chunked=True,
|
||||||
_range=None,
|
_range=None,
|
||||||
):
|
):
|
||||||
"""Return a streaming response object with file data.
|
"""Return a streaming response object with file data.
|
||||||
|
@ -336,6 +351,7 @@ async def file_stream(
|
||||||
:param mime_type: Specific mime_type.
|
:param mime_type: Specific mime_type.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param filename: Override filename.
|
:param filename: Override filename.
|
||||||
|
:param chunked: Enable or disable chunked transfer-encoding
|
||||||
:param _range:
|
:param _range:
|
||||||
"""
|
"""
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
|
@ -383,6 +399,7 @@ async def file_stream(
|
||||||
status=status,
|
status=status,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
content_type=mime_type,
|
content_type=mime_type,
|
||||||
|
chunked=chunked,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -391,6 +408,7 @@ def stream(
|
||||||
status=200,
|
status=200,
|
||||||
headers=None,
|
headers=None,
|
||||||
content_type="text/plain; charset=utf-8",
|
content_type="text/plain; charset=utf-8",
|
||||||
|
chunked=True,
|
||||||
):
|
):
|
||||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||||
|
@ -409,9 +427,14 @@ def stream(
|
||||||
writes content to that response.
|
writes content to that response.
|
||||||
:param mime_type: Specific mime_type.
|
:param mime_type: Specific mime_type.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
|
:param chunked: Enable or disable chunked transfer-encoding
|
||||||
"""
|
"""
|
||||||
return StreamingHTTPResponse(
|
return StreamingHTTPResponse(
|
||||||
streaming_fn, headers=headers, content_type=content_type, status=status
|
streaming_fn,
|
||||||
|
headers=headers,
|
||||||
|
content_type=content_type,
|
||||||
|
status=status,
|
||||||
|
chunked=chunked,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ Parameter = namedtuple("Parameter", ["name", "cast"])
|
||||||
|
|
||||||
REGEX_TYPES = {
|
REGEX_TYPES = {
|
||||||
"string": (str, r"[^/]+"),
|
"string": (str, r"[^/]+"),
|
||||||
"int": (int, r"\d+"),
|
"int": (int, r"-?\d+"),
|
||||||
"number": (float, r"[0-9\\.]+"),
|
"number": (float, r"-?(?:\d+(?:\.\d*)?|\.\d+)"),
|
||||||
"alpha": (str, r"[A-Za-z]+"),
|
"alpha": (str, r"[A-Za-z]+"),
|
||||||
"path": (str, r"[^/].*?"),
|
"path": (str, r"[^/].*?"),
|
||||||
"uuid": (
|
"uuid": (
|
||||||
|
|
|
@ -34,9 +34,6 @@ except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
current_time = None
|
|
||||||
|
|
||||||
|
|
||||||
class Signal:
|
class Signal:
|
||||||
stopped = False
|
stopped = False
|
||||||
|
|
||||||
|
@ -47,6 +44,8 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
|
# app
|
||||||
|
"app",
|
||||||
# event loop, connection
|
# event loop, connection
|
||||||
"loop",
|
"loop",
|
||||||
"transport",
|
"transport",
|
||||||
|
@ -91,6 +90,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
loop,
|
loop,
|
||||||
|
app,
|
||||||
request_handler,
|
request_handler,
|
||||||
error_handler,
|
error_handler,
|
||||||
signal=Signal(),
|
signal=Signal(),
|
||||||
|
@ -110,6 +110,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
|
self.app = app
|
||||||
self.transport = None
|
self.transport = None
|
||||||
self.request = None
|
self.request = None
|
||||||
self.parser = None
|
self.parser = None
|
||||||
|
@ -118,7 +119,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.router = router
|
self.router = router
|
||||||
self.signal = signal
|
self.signal = signal
|
||||||
self.access_log = access_log
|
self.access_log = access_log
|
||||||
self.connections = connections or set()
|
self.connections = connections if connections is not None else set()
|
||||||
self.request_handler = request_handler
|
self.request_handler = request_handler
|
||||||
self.error_handler = error_handler
|
self.error_handler = error_handler
|
||||||
self.request_timeout = request_timeout
|
self.request_timeout = request_timeout
|
||||||
|
@ -171,7 +172,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.request_timeout, self.request_timeout_callback
|
self.request_timeout, self.request_timeout_callback
|
||||||
)
|
)
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
self._last_request_time = current_time
|
self._last_request_time = time()
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
def connection_lost(self, exc):
|
||||||
self.connections.discard(self)
|
self.connections.discard(self)
|
||||||
|
@ -197,7 +198,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
# exactly what this timeout is checking for.
|
# exactly what this timeout is checking for.
|
||||||
# Check if elapsed time since request initiated exceeds our
|
# Check if elapsed time since request initiated exceeds our
|
||||||
# configured maximum request timeout value
|
# 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:
|
if time_elapsed < self.request_timeout:
|
||||||
time_left = self.request_timeout - time_elapsed
|
time_left = self.request_timeout - time_elapsed
|
||||||
self._request_timeout_handler = self.loop.call_later(
|
self._request_timeout_handler = self.loop.call_later(
|
||||||
|
@ -213,7 +214,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
def response_timeout_callback(self):
|
def response_timeout_callback(self):
|
||||||
# Check if elapsed time since response was initiated exceeds our
|
# Check if elapsed time since response was initiated exceeds our
|
||||||
# configured maximum request timeout value
|
# 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:
|
if time_elapsed < self.response_timeout:
|
||||||
time_left = self.response_timeout - time_elapsed
|
time_left = self.response_timeout - time_elapsed
|
||||||
self._response_timeout_handler = self.loop.call_later(
|
self._response_timeout_handler = self.loop.call_later(
|
||||||
|
@ -234,7 +235,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
time_elapsed = current_time - self._last_response_time
|
time_elapsed = time() - self._last_response_time
|
||||||
if time_elapsed < self.keep_alive_timeout:
|
if time_elapsed < self.keep_alive_timeout:
|
||||||
time_left = self.keep_alive_timeout - time_elapsed
|
time_left = self.keep_alive_timeout - time_elapsed
|
||||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||||
|
@ -306,6 +307,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
version=self.parser.get_http_version(),
|
version=self.parser.get_http_version(),
|
||||||
method=self.parser.get_method().decode(),
|
method=self.parser.get_method().decode(),
|
||||||
transport=self.transport,
|
transport=self.transport,
|
||||||
|
app=self.app,
|
||||||
)
|
)
|
||||||
# Remove any existing KeepAlive handler here,
|
# Remove any existing KeepAlive handler here,
|
||||||
# It will be recreated if required on the new request.
|
# It will be recreated if required on the new request.
|
||||||
|
@ -362,7 +364,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self._response_timeout_handler = self.loop.call_later(
|
self._response_timeout_handler = self.loop.call_later(
|
||||||
self.response_timeout, self.response_timeout_callback
|
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_task = self.loop.create_task(
|
||||||
self.request_handler(
|
self.request_handler(
|
||||||
self.request, self.write_response, self.stream_response
|
self.request, self.write_response, self.stream_response
|
||||||
|
@ -449,7 +451,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||||
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
||||||
)
|
)
|
||||||
self._last_response_time = current_time
|
self._last_response_time = time()
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
|
||||||
async def drain(self):
|
async def drain(self):
|
||||||
|
@ -502,7 +504,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||||
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
self.keep_alive_timeout, self.keep_alive_timeout_callback
|
||||||
)
|
)
|
||||||
self._last_response_time = current_time
|
self._last_response_time = time()
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
|
||||||
def write_error(self, exception):
|
def write_error(self, exception):
|
||||||
|
@ -552,11 +554,15 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if from_error or self.transport.is_closing():
|
if from_error or self.transport is None or self.transport.is_closing():
|
||||||
logger.error(
|
logger.error(
|
||||||
"Transport closed @ %s and exception "
|
"Transport closed @ %s and exception "
|
||||||
"experienced during error handling",
|
"experienced during error handling",
|
||||||
self.transport.get_extra_info("peername"),
|
(
|
||||||
|
self.transport.get_extra_info("peername")
|
||||||
|
if self.transport is not None
|
||||||
|
else "N/A"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
logger.debug("Exception:", exc_info=True)
|
logger.debug("Exception:", exc_info=True)
|
||||||
else:
|
else:
|
||||||
|
@ -595,18 +601,6 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.transport = None
|
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):
|
def trigger_events(events, loop):
|
||||||
"""Trigger event callbacks (functions or async)
|
"""Trigger event callbacks (functions or async)
|
||||||
|
|
||||||
|
@ -622,6 +616,7 @@ def trigger_events(events, loop):
|
||||||
def serve(
|
def serve(
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
|
app,
|
||||||
request_handler,
|
request_handler,
|
||||||
error_handler,
|
error_handler,
|
||||||
before_start=None,
|
before_start=None,
|
||||||
|
@ -656,6 +651,7 @@ def serve(
|
||||||
websocket_write_limit=2 ** 16,
|
websocket_write_limit=2 ** 16,
|
||||||
state=None,
|
state=None,
|
||||||
graceful_shutdown_timeout=15.0,
|
graceful_shutdown_timeout=15.0,
|
||||||
|
asyncio_server_kwargs=None,
|
||||||
):
|
):
|
||||||
"""Start asynchronous HTTP Server on an individual process.
|
"""Start asynchronous HTTP Server on an individual process.
|
||||||
|
|
||||||
|
@ -700,6 +696,8 @@ def serve(
|
||||||
:param router: Router object
|
:param router: Router object
|
||||||
:param graceful_shutdown_timeout: How long take to Force close non-idle
|
:param graceful_shutdown_timeout: How long take to Force close non-idle
|
||||||
connection
|
connection
|
||||||
|
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
|
||||||
|
create_server method
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if not run_async:
|
if not run_async:
|
||||||
|
@ -716,6 +714,7 @@ def serve(
|
||||||
loop=loop,
|
loop=loop,
|
||||||
connections=connections,
|
connections=connections,
|
||||||
signal=signal,
|
signal=signal,
|
||||||
|
app=app,
|
||||||
request_handler=request_handler,
|
request_handler=request_handler,
|
||||||
error_handler=error_handler,
|
error_handler=error_handler,
|
||||||
request_timeout=request_timeout,
|
request_timeout=request_timeout,
|
||||||
|
@ -734,7 +733,9 @@ def serve(
|
||||||
state=state,
|
state=state,
|
||||||
debug=debug,
|
debug=debug,
|
||||||
)
|
)
|
||||||
|
asyncio_server_kwargs = (
|
||||||
|
asyncio_server_kwargs if asyncio_server_kwargs else {}
|
||||||
|
)
|
||||||
server_coroutine = loop.create_server(
|
server_coroutine = loop.create_server(
|
||||||
server,
|
server,
|
||||||
host,
|
host,
|
||||||
|
@ -743,12 +744,9 @@ def serve(
|
||||||
reuse_port=reuse_port,
|
reuse_port=reuse_port,
|
||||||
sock=sock,
|
sock=sock,
|
||||||
backlog=backlog,
|
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:
|
if run_async:
|
||||||
return server_coroutine
|
return server_coroutine
|
||||||
|
|
||||||
|
|
170
sanic/testing.py
170
sanic/testing.py
|
@ -1,4 +1,10 @@
|
||||||
|
|
||||||
|
from json import JSONDecodeError
|
||||||
|
from socket import socket
|
||||||
|
|
||||||
|
import requests_async as requests
|
||||||
|
import websockets
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import http
|
import http
|
||||||
import io
|
import io
|
||||||
|
@ -18,54 +24,10 @@ HOST = "127.0.0.1"
|
||||||
PORT = 42101
|
PORT = 42101
|
||||||
|
|
||||||
|
|
||||||
# Annotations for `Session.request()`
|
|
||||||
Cookies = typing.Union[
|
|
||||||
typing.MutableMapping[str, str], requests.cookies.RequestsCookieJar
|
|
||||||
]
|
|
||||||
Params = typing.Union[bytes, typing.MutableMapping[str, str]]
|
|
||||||
DataType = typing.Union[bytes, typing.MutableMapping[str, str], typing.IO]
|
|
||||||
TimeOut = typing.Union[float, typing.Tuple[float, float]]
|
|
||||||
FileType = typing.MutableMapping[str, typing.IO]
|
|
||||||
AuthType = typing.Union[
|
|
||||||
typing.Tuple[str, str],
|
|
||||||
requests.auth.AuthBase,
|
|
||||||
typing.Callable[[requests.Request], requests.Request],
|
|
||||||
]
|
|
||||||
|
|
||||||
|
class SanicTestClient:
|
||||||
class _HeaderDict(requests.packages.urllib3._collections.HTTPHeaderDict):
|
def __init__(self, app, port=PORT):
|
||||||
def get_all(self, key: str, default: str) -> str:
|
"""Use port=None to bind to a random port"""
|
||||||
return self.getheaders(key)
|
|
||||||
|
|
||||||
|
|
||||||
class _MockOriginalResponse:
|
|
||||||
"""
|
|
||||||
We have to jump through some hoops to present the response as if
|
|
||||||
it was made using urllib3.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, headers: typing.List[typing.Tuple[bytes, bytes]]) -> None:
|
|
||||||
self.msg = _HeaderDict(headers)
|
|
||||||
self.closed = False
|
|
||||||
|
|
||||||
def isclosed(self) -> bool:
|
|
||||||
return self.closed
|
|
||||||
|
|
||||||
|
|
||||||
class _Upgrade(Exception):
|
|
||||||
def __init__(self, session: "WebSocketTestSession") -> None:
|
|
||||||
self.session = session
|
|
||||||
|
|
||||||
|
|
||||||
def _get_reason_phrase(status_code: int) -> str:
|
|
||||||
try:
|
|
||||||
return http.HTTPStatus(status_code).phrase
|
|
||||||
except ValueError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
class _ASGIAdapter(requests.adapters.HTTPAdapter):
|
|
||||||
def __init__(self, app: ASGIApp, raise_server_exceptions: bool = True) -> None:
|
|
||||||
self.app = app
|
self.app = app
|
||||||
self.raise_server_exceptions = raise_server_exceptions
|
self.raise_server_exceptions = raise_server_exceptions
|
||||||
|
|
||||||
|
@ -76,22 +38,55 @@ class _ASGIAdapter(requests.adapters.HTTPAdapter):
|
||||||
request.url
|
request.url
|
||||||
)
|
)
|
||||||
|
|
||||||
default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme]
|
def get_new_session(self):
|
||||||
|
return requests.Session()
|
||||||
|
|
||||||
if ":" in netloc:
|
async def _local_request(self, method, url, *args, **kwargs):
|
||||||
host, port_string = netloc.split(":", 1)
|
logger.info(url)
|
||||||
port = int(port_string)
|
raw_cookies = kwargs.pop("raw_cookies", None)
|
||||||
else:
|
|
||||||
host = netloc
|
|
||||||
port = default_port
|
|
||||||
|
|
||||||
# Include the 'host' header.
|
if method == "websocket":
|
||||||
if "host" in request.headers:
|
async with websockets.connect(url, *args, **kwargs) as websocket:
|
||||||
headers = [] # type: typing.List[typing.Tuple[bytes, bytes]]
|
websocket.opened = websocket.open
|
||||||
elif port == default_port:
|
return websocket
|
||||||
headers = [(b"host", host.encode())]
|
|
||||||
else:
|
else:
|
||||||
headers = [(b"host", ("%s:%d" % (host, port)).encode())]
|
async with self.get_new_session() as session:
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await getattr(session, method.lower())(
|
||||||
|
url, verify=False, *args, **kwargs
|
||||||
|
)
|
||||||
|
except NameError:
|
||||||
|
raise Exception(response.status_code)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response.json = response.json()
|
||||||
|
except (JSONDecodeError, UnicodeDecodeError):
|
||||||
|
response.json = None
|
||||||
|
|
||||||
|
response.body = await response.read()
|
||||||
|
response.status = response.status_code
|
||||||
|
response.content_type = response.headers.get("content-type")
|
||||||
|
|
||||||
|
if raw_cookies:
|
||||||
|
response.raw_cookies = {}
|
||||||
|
for cookie in response.cookies:
|
||||||
|
response.raw_cookies[cookie.name] = cookie
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _sanic_endpoint_test(
|
||||||
|
self,
|
||||||
|
method="get",
|
||||||
|
uri="/",
|
||||||
|
gather_request=True,
|
||||||
|
debug=False,
|
||||||
|
server_kwargs={"auto_reload": False},
|
||||||
|
*request_args,
|
||||||
|
**request_kwargs
|
||||||
|
):
|
||||||
|
results = [None, None]
|
||||||
|
exceptions = []
|
||||||
|
|
||||||
# Include other request headers.
|
# Include other request headers.
|
||||||
headers += [
|
headers += [
|
||||||
|
@ -158,25 +153,31 @@ class _ASGIAdapter(requests.adapters.HTTPAdapter):
|
||||||
else:
|
else:
|
||||||
body_bytes = body
|
body_bytes = body
|
||||||
|
|
||||||
request_complete = True
|
if self.port:
|
||||||
return {"type": "http.request", "body": body_bytes}
|
server_kwargs = dict(host=HOST, port=self.port, **server_kwargs)
|
||||||
|
host, port = HOST, self.port
|
||||||
|
else:
|
||||||
|
sock = socket()
|
||||||
|
sock.bind((HOST, 0))
|
||||||
|
server_kwargs = dict(sock=sock, **server_kwargs)
|
||||||
|
host, port = sock.getsockname()
|
||||||
|
|
||||||
async def send(message: Message) -> None:
|
if uri.startswith(
|
||||||
nonlocal raw_kwargs, response_started, response_complete, template, context
|
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
|
||||||
|
):
|
||||||
|
url = uri
|
||||||
|
else:
|
||||||
|
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
|
||||||
|
scheme = "ws" if method == "websocket" else "http"
|
||||||
|
url = "{scheme}://{host}:{port}{uri}".format(
|
||||||
|
scheme=scheme, host=host, port=port, uri=uri
|
||||||
|
)
|
||||||
|
|
||||||
if message["type"] == "http.response.start":
|
@self.app.listener("after_server_start")
|
||||||
assert (
|
async def _collect_response(sanic, loop):
|
||||||
not response_started
|
try:
|
||||||
), 'Received multiple "http.response.start" messages.'
|
response = await self._local_request(
|
||||||
raw_kwargs["version"] = 11
|
method, url, *request_args, **request_kwargs
|
||||||
raw_kwargs["status"] = message["status"]
|
|
||||||
raw_kwargs["reason"] = _get_reason_phrase(message["status"])
|
|
||||||
raw_kwargs["headers"] = [
|
|
||||||
(key.decode(), value.decode()) for key, value in message["headers"]
|
|
||||||
]
|
|
||||||
raw_kwargs["preload_content"] = False
|
|
||||||
raw_kwargs["original_response"] = _MockOriginalResponse(
|
|
||||||
raw_kwargs["headers"]
|
|
||||||
)
|
)
|
||||||
response_started = True
|
response_started = True
|
||||||
elif message["type"] == "http.response.body":
|
elif message["type"] == "http.response.body":
|
||||||
|
@ -204,11 +205,9 @@ class _ASGIAdapter(requests.adapters.HTTPAdapter):
|
||||||
template = None
|
template = None
|
||||||
context = None
|
context = None
|
||||||
|
|
||||||
try:
|
|
||||||
loop = asyncio.get_event_loop()
|
self.app.run(debug=debug, **server_kwargs)
|
||||||
except RuntimeError:
|
self.app.listeners["after_server_start"].pop()
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
|
|
||||||
self.app.is_running = True
|
self.app.is_running = True
|
||||||
try:
|
try:
|
||||||
|
@ -350,6 +349,7 @@ class SanicTestClient(requests.Session):
|
||||||
return self.request("options", *args, **kwargs)
|
return self.request("options", *args, **kwargs)
|
||||||
|
|
||||||
def head(self, *args, **kwargs):
|
def head(self, *args, **kwargs):
|
||||||
if 'uri' in kwargs:
|
return self._sanic_endpoint_test("head", *args, **kwargs)
|
||||||
kwargs['url'] = kwargs.pop('uri')
|
|
||||||
return self.request("head", *args, **kwargs)
|
def websocket(self, *args, **kwargs):
|
||||||
|
return self._sanic_endpoint_test("websocket", *args, **kwargs)
|
||||||
|
|
|
@ -14,7 +14,7 @@ multi_line_output = 3
|
||||||
not_skip = __init__.py
|
not_skip = __init__.py
|
||||||
|
|
||||||
[version]
|
[version]
|
||||||
current_version = 0.8.3
|
current_version = 19.3.1
|
||||||
file = sanic/__init__.py
|
file = sanic/__init__.py
|
||||||
current_version_pattern = __version__ = "{current_version}"
|
current_version_pattern = __version__ = "{current_version}"
|
||||||
new_version_pattern = __version__ = "{new_version}"
|
new_version_pattern = __version__ = "{new_version}"
|
||||||
|
|
15
setup.py
15
setup.py
|
@ -50,12 +50,12 @@ with open_local(["README.rst"]) as rm:
|
||||||
setup_kwargs = {
|
setup_kwargs = {
|
||||||
"name": "sanic",
|
"name": "sanic",
|
||||||
"version": version,
|
"version": version,
|
||||||
"url": "http://github.com/channelcat/sanic/",
|
"url": "http://github.com/huge-success/sanic/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Channel Cat",
|
"author": "Sanic Community",
|
||||||
"author_email": "channelcat@gmail.com",
|
"author_email": "admhpkns@gmail.com",
|
||||||
"description": (
|
"description": (
|
||||||
"A microframework based on uvloop, httptools, and learnings of flask"
|
"A web server and web framework that's written to go fast. Build fast. Run fast."
|
||||||
),
|
),
|
||||||
"long_description": long_description,
|
"long_description": long_description,
|
||||||
"packages": ["sanic"],
|
"packages": ["sanic"],
|
||||||
|
@ -64,7 +64,6 @@ setup_kwargs = {
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Programming Language :: Python :: 3.5",
|
|
||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
],
|
],
|
||||||
|
@ -90,12 +89,14 @@ tests_require = [
|
||||||
"multidict>=4.0,<5.0",
|
"multidict>=4.0,<5.0",
|
||||||
"gunicorn",
|
"gunicorn",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"aiohttp>=2.3.0,<=3.2.1",
|
"httpcore==0.1.1",
|
||||||
|
"requests-async==0.4.0",
|
||||||
"beautifulsoup4",
|
"beautifulsoup4",
|
||||||
uvloop,
|
uvloop,
|
||||||
ujson,
|
ujson,
|
||||||
"pytest-sanic",
|
"pytest-sanic",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
|
"pytest-benchmark",
|
||||||
]
|
]
|
||||||
|
|
||||||
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
||||||
|
@ -118,7 +119,7 @@ extras_require = {
|
||||||
"recommonmark",
|
"recommonmark",
|
||||||
"sphinxcontrib-asyncio",
|
"sphinxcontrib-asyncio",
|
||||||
"docutils",
|
"docutils",
|
||||||
"pygments"
|
"pygments",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
55
tests/benchmark/test_route_resolution_benchmark.py
Normal file
55
tests/benchmark/test_route_resolution_benchmark.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
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,131 @@
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import string
|
||||||
import sys
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
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"]:
|
if sys.platform in ["win32", "cygwin"]:
|
||||||
collect_ignore = ["test_worker.py"]
|
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
|
@pytest.fixture
|
||||||
def app(request):
|
def app(request):
|
||||||
return Sanic(request.node.name)
|
return Sanic(request.node.name)
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
# Run with python3 simple_server.py PORT
|
# Run with python3 simple_server.py PORT
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
import uvloop
|
|
||||||
import ujson as json
|
import ujson as json
|
||||||
|
import uvloop
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
|
||||||
loop = uvloop.new_event_loop()
|
loop = uvloop.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
# Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker -b :8000 simple_server:app
|
# Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker -b :8000 simple_server:app
|
||||||
import bottle
|
import bottle
|
||||||
from bottle import route, run
|
|
||||||
import ujson
|
import ujson
|
||||||
|
|
||||||
|
from bottle import route, run
|
||||||
|
|
||||||
|
|
||||||
@route("/")
|
@route("/")
|
||||||
def index():
|
def index():
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
# Run with: python3 -O simple_server.py
|
# Run with: python3 -O simple_server.py
|
||||||
import asyncio
|
import asyncio
|
||||||
from kyoukai import Kyoukai, HTTPRequestContext
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import ujson
|
import ujson
|
||||||
import uvloop
|
import uvloop
|
||||||
|
|
||||||
|
from kyoukai import HTTPRequestContext, Kyoukai
|
||||||
|
|
||||||
|
|
||||||
loop = uvloop.new_event_loop()
|
loop = uvloop.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import asyncpg
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import timeit
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
currentdir = os.path.dirname(
|
currentdir = os.path.dirname(
|
||||||
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
||||||
)
|
)
|
||||||
sys.path.insert(0, currentdir + "/../../../")
|
sys.path.insert(0, currentdir + "/../../../")
|
||||||
|
|
||||||
import timeit
|
|
||||||
|
|
||||||
from sanic.response import json
|
|
||||||
|
|
||||||
print(json({"test": True}).output())
|
print(json({"test": True}).output())
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
currentdir = os.path.dirname(
|
currentdir = os.path.dirname(
|
||||||
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
||||||
)
|
)
|
||||||
sys.path.insert(0, currentdir + "/../../../")
|
sys.path.insert(0, currentdir + "/../../../")
|
||||||
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import json
|
|
||||||
|
|
||||||
app = Sanic("test")
|
app = Sanic("test")
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.exceptions import ServerError
|
||||||
|
from sanic.response import json, text
|
||||||
|
|
||||||
|
|
||||||
currentdir = os.path.dirname(
|
currentdir = os.path.dirname(
|
||||||
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
||||||
)
|
)
|
||||||
sys.path.insert(0, currentdir + "/../../../")
|
sys.path.insert(0, currentdir + "/../../../")
|
||||||
|
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import json, text
|
|
||||||
from sanic.exceptions import ServerError
|
|
||||||
|
|
||||||
app = Sanic("test")
|
app = Sanic("test")
|
||||||
|
|
||||||
|
@ -56,8 +58,6 @@ def query_string(request):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=sys.argv[1])
|
app.run(host="0.0.0.0", port=sys.argv[1])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# Run with: python simple_server.py
|
# Run with: python simple_server.py
|
||||||
import ujson
|
import ujson
|
||||||
|
|
||||||
from tornado import ioloop, web
|
from tornado import ioloop, web
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
""" Minimal helloworld application.
|
""" Minimal helloworld application.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from wheezy.http import HTTPResponse
|
import ujson
|
||||||
from wheezy.http import WSGIApplication
|
|
||||||
|
from wheezy.http import HTTPResponse, WSGIApplication
|
||||||
from wheezy.http.response import json_response
|
from wheezy.http.response import json_response
|
||||||
from wheezy.routing import url
|
from wheezy.routing import url
|
||||||
from wheezy.web.handlers import BaseHandler
|
from wheezy.web.handlers import BaseHandler
|
||||||
from wheezy.web.middleware import bootstrap_defaults
|
from wheezy.web.middleware import (
|
||||||
from wheezy.web.middleware import path_routing_middleware_factory
|
bootstrap_defaults,
|
||||||
|
path_routing_middleware_factory,
|
||||||
import ujson
|
)
|
||||||
|
|
||||||
|
|
||||||
class WelcomeHandler(BaseHandler):
|
class WelcomeHandler(BaseHandler):
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from inspect import isawaitable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -7,6 +10,15 @@ from sanic.exceptions import SanicException
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
|
def uvloop_installed():
|
||||||
|
try:
|
||||||
|
import uvloop # noqa
|
||||||
|
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_app_loop_running(app):
|
def test_app_loop_running(app):
|
||||||
@app.get("/test")
|
@app.get("/test")
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
|
@ -17,9 +29,35 @@ def test_app_loop_running(app):
|
||||||
assert response.text == "pass"
|
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):
|
def test_app_loop_not_running(app):
|
||||||
with pytest.raises(SanicException) as excinfo:
|
with pytest.raises(SanicException) as excinfo:
|
||||||
app.loop
|
_ = app.loop
|
||||||
|
|
||||||
assert str(excinfo.value) == (
|
assert str(excinfo.value) == (
|
||||||
"Loop can only be retrieved after the app has started "
|
"Loop can only be retrieved after the app has started "
|
||||||
|
@ -103,7 +141,6 @@ def test_handle_request_with_nested_exception(app, monkeypatch):
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def handler(request):
|
def handler(request):
|
||||||
raise Exception
|
raise Exception
|
||||||
return text("OK")
|
|
||||||
|
|
||||||
request, response = app.test_client.get("/")
|
request, response = app.test_client.get("/")
|
||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
|
@ -125,7 +162,6 @@ def test_handle_request_with_nested_exception_debug(app, monkeypatch):
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def handler(request):
|
def handler(request):
|
||||||
raise Exception
|
raise Exception
|
||||||
return text("OK")
|
|
||||||
|
|
||||||
request, response = app.test_client.get("/", debug=True)
|
request, response = app.test_client.get("/", debug=True)
|
||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
|
@ -149,7 +185,6 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def handler(request):
|
def handler(request):
|
||||||
raise Exception
|
raise Exception
|
||||||
return text("OK")
|
|
||||||
|
|
||||||
with caplog.at_level(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
request, response = app.test_client.get("/")
|
request, response = app.test_client.get("/")
|
||||||
|
|
181
tests/test_blueprint_group.py
Normal file
181
tests/test_blueprint_group.py
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
from sanic.app import Sanic
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
from sanic.request import Request
|
||||||
|
from sanic.response import HTTPResponse, text
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
|
@ -7,9 +7,9 @@ import pytest
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.exceptions import NotFound, ServerError, InvalidUsage
|
from sanic.exceptions import InvalidUsage, NotFound, ServerError
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import text, json
|
from sanic.response import json, text
|
||||||
from sanic.views import CompositionView
|
from sanic.views import CompositionView
|
||||||
|
|
||||||
|
|
||||||
|
@ -467,16 +467,8 @@ def test_bp_shorthand(app):
|
||||||
request, response = app.test_client.get("/delete")
|
request, response = app.test_client.get("/delete")
|
||||||
assert response.status == 405
|
assert response.status == 405
|
||||||
|
|
||||||
request, response = app.test_client.get(
|
request, response = app.test_client.websocket("/ws/")
|
||||||
"/ws/",
|
assert response.opened is True
|
||||||
headers={
|
|
||||||
"Upgrade": "websocket",
|
|
||||||
"Connection": "upgrade",
|
|
||||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
|
||||||
"Sec-WebSocket-Version": "13",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert response.status == 101
|
|
||||||
assert ev.is_set()
|
assert ev.is_set()
|
||||||
|
|
||||||
|
|
||||||
|
@ -595,14 +587,13 @@ def test_blueprint_middleware_with_args(app: Sanic):
|
||||||
"/wa", headers={"content-type": "plain/text"}
|
"/wa", headers={"content-type": "plain/text"}
|
||||||
)
|
)
|
||||||
assert response.json.get("test") == "value"
|
assert response.json.get("test") == "value"
|
||||||
d = {}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("file_name", ["test.file"])
|
@pytest.mark.parametrize("file_name", ["test.file"])
|
||||||
def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
|
def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
|
||||||
current_file = inspect.getfile(inspect.currentframe())
|
current_file = inspect.getfile(inspect.currentframe())
|
||||||
with open(current_file, "rb") as file:
|
with open(current_file, "rb") as file:
|
||||||
current_file_contents = file.read()
|
file.read()
|
||||||
|
|
||||||
bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False)
|
bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False)
|
||||||
|
|
||||||
|
@ -662,16 +653,8 @@ def test_websocket_route(app: Sanic):
|
||||||
|
|
||||||
app.blueprint(bp)
|
app.blueprint(bp)
|
||||||
|
|
||||||
_, response = app.test_client.get(
|
_, response = app.test_client.websocket("/ws/test")
|
||||||
"/ws/test",
|
assert response.opened is True
|
||||||
headers={
|
|
||||||
"Upgrade": "websocket",
|
|
||||||
"Connection": "upgrade",
|
|
||||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
|
||||||
"Sec-WebSocket-Version": "13",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert response.status == 101
|
|
||||||
assert event.is_set()
|
assert event.is_set()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
from contextlib import contextmanager
|
||||||
from os import environ
|
from os import environ
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from contextlib import contextmanager
|
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.config import Config, DEFAULT_CONFIG
|
from sanic.config import DEFAULT_CONFIG, Config
|
||||||
from sanic.exceptions import PyFileError
|
from sanic.exceptions import PyFileError
|
||||||
|
|
||||||
|
|
||||||
|
@ -144,9 +145,10 @@ def test_overwrite_exisiting_config_ignore_lowercase(app):
|
||||||
|
|
||||||
|
|
||||||
def test_missing_config(app):
|
def test_missing_config(app):
|
||||||
with pytest.raises(AttributeError) as e:
|
with pytest.raises(
|
||||||
app.config.NON_EXISTENT
|
AttributeError, match="Config has no 'NON_EXISTENT'"
|
||||||
assert str(e.value) == ("Config has no 'NON_EXISTENT'")
|
) as e:
|
||||||
|
_ = app.config.NON_EXISTENT
|
||||||
|
|
||||||
|
|
||||||
def test_config_defaults():
|
def test_config_defaults():
|
||||||
|
@ -166,7 +168,7 @@ def test_config_custom_defaults():
|
||||||
custom_defaults = {
|
custom_defaults = {
|
||||||
"REQUEST_MAX_SIZE": 1,
|
"REQUEST_MAX_SIZE": 1,
|
||||||
"KEEP_ALIVE": False,
|
"KEEP_ALIVE": False,
|
||||||
"ACCESS_LOG": False
|
"ACCESS_LOG": False,
|
||||||
}
|
}
|
||||||
conf = Config(defaults=custom_defaults)
|
conf = Config(defaults=custom_defaults)
|
||||||
for key, value in DEFAULT_CONFIG.items():
|
for key, value in DEFAULT_CONFIG.items():
|
||||||
|
@ -182,13 +184,13 @@ def test_config_custom_defaults_with_env():
|
||||||
custom_defaults = {
|
custom_defaults = {
|
||||||
"REQUEST_MAX_SIZE123": 1,
|
"REQUEST_MAX_SIZE123": 1,
|
||||||
"KEEP_ALIVE123": False,
|
"KEEP_ALIVE123": False,
|
||||||
"ACCESS_LOG123": False
|
"ACCESS_LOG123": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
environ_defaults = {
|
environ_defaults = {
|
||||||
"SANIC_REQUEST_MAX_SIZE123": "2",
|
"SANIC_REQUEST_MAX_SIZE123": "2",
|
||||||
"SANIC_KEEP_ALIVE123": "True",
|
"SANIC_KEEP_ALIVE123": "True",
|
||||||
"SANIC_ACCESS_LOG123": "False"
|
"SANIC_ACCESS_LOG123": "False",
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value in environ_defaults.items():
|
for key, value in environ_defaults.items():
|
||||||
|
@ -201,8 +203,8 @@ def test_config_custom_defaults_with_env():
|
||||||
try:
|
try:
|
||||||
value = int(value)
|
value = int(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if value in ['True', 'False']:
|
if value in ["True", "False"]:
|
||||||
value = value == 'True'
|
value = value == "True"
|
||||||
|
|
||||||
assert getattr(conf, key) == value
|
assert getattr(conf, key) == value
|
||||||
|
|
||||||
|
@ -213,7 +215,7 @@ def test_config_custom_defaults_with_env():
|
||||||
def test_config_access_log_passing_in_run(app):
|
def test_config_access_log_passing_in_run(app):
|
||||||
assert app.config.ACCESS_LOG == True
|
assert app.config.ACCESS_LOG == True
|
||||||
|
|
||||||
@app.listener('after_server_start')
|
@app.listener("after_server_start")
|
||||||
async def _request(sanic, loop):
|
async def _request(sanic, loop):
|
||||||
app.stop()
|
app.stop()
|
||||||
|
|
||||||
|
@ -227,14 +229,18 @@ def test_config_access_log_passing_in_run(app):
|
||||||
async def test_config_access_log_passing_in_create_server(app):
|
async def test_config_access_log_passing_in_create_server(app):
|
||||||
assert app.config.ACCESS_LOG == True
|
assert app.config.ACCESS_LOG == True
|
||||||
|
|
||||||
@app.listener('after_server_start')
|
@app.listener("after_server_start")
|
||||||
async def _request(sanic, loop):
|
async def _request(sanic, loop):
|
||||||
app.stop()
|
app.stop()
|
||||||
|
|
||||||
await app.create_server(port=1341, access_log=False)
|
await app.create_server(
|
||||||
|
port=1341, access_log=False, return_asyncio_server=True
|
||||||
|
)
|
||||||
assert app.config.ACCESS_LOG == False
|
assert app.config.ACCESS_LOG == False
|
||||||
|
|
||||||
await app.create_server(port=1342, access_log=True)
|
await app.create_server(
|
||||||
|
port=1342, access_log=True, return_asyncio_server=True
|
||||||
|
)
|
||||||
assert app.config.ACCESS_LOG == True
|
assert app.config.ACCESS_LOG == True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
from sanic.response import text
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic.cookies import Cookie
|
from sanic.cookies import Cookie
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# GET
|
# GET
|
||||||
|
@ -135,10 +138,10 @@ def test_cookie_set_same_key(app):
|
||||||
|
|
||||||
request, response = app.test_client.get("/", cookies=cookies)
|
request, response = app.test_client.get("/", cookies=cookies)
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.cookies["test"].value == "pass"
|
assert response.cookies["test"] == "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):
|
def test_cookie_max_age(app, max_age):
|
||||||
cookies = {"test": "wait"}
|
cookies = {"test": "wait"}
|
||||||
|
|
||||||
|
@ -149,18 +152,42 @@ def test_cookie_max_age(app, max_age):
|
||||||
response.cookies["test"]["max-age"] = max_age
|
response.cookies["test"]["max-age"] = max_age
|
||||||
return response
|
return response
|
||||||
|
|
||||||
request, response = app.test_client.get("/", cookies=cookies)
|
request, response = app.test_client.get(
|
||||||
|
"/", cookies=cookies, raw_cookies=True
|
||||||
|
)
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
assert response.cookies["test"].value == "pass"
|
cookie = response.cookies.get("test")
|
||||||
assert response.cookies["test"]["max-age"] == str(max_age)
|
if (
|
||||||
|
str(max_age).isdigit()
|
||||||
|
and int(max_age) == float(max_age)
|
||||||
|
and int(max_age) != 0
|
||||||
|
):
|
||||||
|
cookie_expires = datetime.utcfromtimestamp(
|
||||||
|
response.raw_cookies["test"].expires
|
||||||
|
).replace(microsecond=0)
|
||||||
|
|
||||||
|
# Grabbing utcnow after the response may lead to it being off slightly.
|
||||||
|
# Therefore, we 0 out the microseconds, and accept the test if there
|
||||||
|
# is a 1 second difference.
|
||||||
|
expires = datetime.utcnow().replace(microsecond=0) + timedelta(
|
||||||
|
seconds=int(max_age)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert cookie == "pass"
|
||||||
|
assert (
|
||||||
|
cookie_expires == expires
|
||||||
|
or cookie_expires == expires + timedelta(seconds=-1)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert cookie is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"expires",
|
"expires", [datetime.utcnow() + timedelta(seconds=60)]
|
||||||
[datetime.now() + timedelta(seconds=60), "Fri, 21-Dec-2018 15:30:00 GMT"],
|
|
||||||
)
|
)
|
||||||
def test_cookie_expires(app, expires):
|
def test_cookie_expires(app, expires):
|
||||||
|
expires = expires.replace(microsecond=0)
|
||||||
cookies = {"test": "wait"}
|
cookies = {"test": "wait"}
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
@ -170,12 +197,21 @@ def test_cookie_expires(app, expires):
|
||||||
response.cookies["test"]["expires"] = expires
|
response.cookies["test"]["expires"] = expires
|
||||||
return response
|
return response
|
||||||
|
|
||||||
request, response = app.test_client.get("/", cookies=cookies)
|
request, response = app.test_client.get(
|
||||||
|
"/", cookies=cookies, raw_cookies=True
|
||||||
|
)
|
||||||
|
cookie_expires = datetime.utcfromtimestamp(
|
||||||
|
response.raw_cookies["test"].expires
|
||||||
|
).replace(microsecond=0)
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
assert response.cookies["test"] == "pass"
|
||||||
|
assert cookie_expires == expires
|
||||||
|
|
||||||
assert response.cookies["test"].value == "pass"
|
|
||||||
|
|
||||||
if isinstance(expires, datetime):
|
@pytest.mark.parametrize("expires", ["Fri, 21-Dec-2018 15:30:00 GMT"])
|
||||||
expires = expires.strftime("%a, %d-%b-%Y %T GMT")
|
def test_cookie_expires_illegal_instance_type(expires):
|
||||||
|
c = Cookie("test_cookie", "value")
|
||||||
assert response.cookies["test"]["expires"] == expires
|
with pytest.raises(expected_exception=TypeError) as e:
|
||||||
|
c["expires"] = expires
|
||||||
|
assert e.message == "Cookie 'expires' property must be a datetime"
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
from sanic.response import text
|
|
||||||
from threading import Event
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
from threading import Event
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
def test_create_task(app):
|
def test_create_task(app):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from sanic.server import HttpProtocol
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
from sanic.server import HttpProtocol
|
||||||
|
|
||||||
|
|
||||||
class CustomHttpProtocol(HttpProtocol):
|
class CustomHttpProtocol(HttpProtocol):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.router import RouteExists
|
from sanic.router import RouteExists
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -39,5 +40,5 @@ def test_overload_dynamic_routes_exist(app):
|
||||||
with pytest.raises(RouteExists):
|
with pytest.raises(RouteExists):
|
||||||
|
|
||||||
@app.route("/overload/<param>", methods=["PUT", "DELETE"])
|
@app.route("/overload/<param>", methods=["PUT", "DELETE"])
|
||||||
async def handler3(request):
|
async def handler3(request, param):
|
||||||
return text("Duplicated")
|
return text("Duplicated")
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
from sanic.exceptions import (
|
||||||
|
Forbidden,
|
||||||
|
InvalidUsage,
|
||||||
|
NotFound,
|
||||||
|
ServerError,
|
||||||
|
Unauthorized,
|
||||||
|
abort,
|
||||||
|
)
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized
|
|
||||||
from sanic.exceptions import Forbidden, abort
|
|
||||||
|
|
||||||
|
|
||||||
class SanicExceptionTestException(Exception):
|
class SanicExceptionTestException(Exception):
|
||||||
|
@ -74,7 +81,7 @@ def exception_app():
|
||||||
|
|
||||||
@app.route("/divide_by_zero")
|
@app.route("/divide_by_zero")
|
||||||
def handle_unhandled_exception(request):
|
def handle_unhandled_exception(request):
|
||||||
1 / 0
|
_ = 1 / 0
|
||||||
|
|
||||||
@app.route("/error_in_error_handler_handler")
|
@app.route("/error_in_error_handler_handler")
|
||||||
def custom_error_handler(request):
|
def custom_error_handler(request):
|
||||||
|
@ -82,7 +89,7 @@ def exception_app():
|
||||||
|
|
||||||
@app.exception(SanicExceptionTestException)
|
@app.exception(SanicExceptionTestException)
|
||||||
def error_in_error_handler_handler(request, exception):
|
def error_in_error_handler_handler(request, exception):
|
||||||
1 / 0
|
_ = 1 / 0
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from sanic import Sanic
|
|
||||||
from sanic.response import text
|
|
||||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
|
||||||
from sanic.handlers import ErrorHandler
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.exceptions import InvalidUsage, NotFound, ServerError
|
||||||
|
from sanic.handlers import ErrorHandler
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
exception_handler_app = Sanic("test_exception_handler")
|
exception_handler_app = Sanic("test_exception_handler")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,41 +1,143 @@
|
||||||
from json import JSONDecodeError
|
|
||||||
from sanic import Sanic
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import functools
|
||||||
|
import socket
|
||||||
|
|
||||||
from asyncio import sleep as aio_sleep
|
from asyncio import sleep as aio_sleep
|
||||||
|
from http.client import _encode
|
||||||
|
from json import JSONDecodeError
|
||||||
|
|
||||||
|
import httpcore
|
||||||
|
import requests_async as requests
|
||||||
|
|
||||||
|
from httpcore import PoolTimeout
|
||||||
|
|
||||||
|
from sanic import Sanic, server
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic import server
|
from sanic.testing import HOST, PORT, SanicTestClient
|
||||||
import aiohttp
|
|
||||||
from aiohttp import TCPConnector
|
|
||||||
from sanic.testing import SanicTestClient, HOST, PORT
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_FOR_TESTS = {
|
# import traceback
|
||||||
"KEEP_ALIVE_TIMEOUT": 2,
|
|
||||||
"KEEP_ALIVE": True
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReuseableTCPConnector(TCPConnector):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
|
|
||||||
self.old_proto = None
|
|
||||||
|
|
||||||
async def connect(self, req, *args, **kwargs):
|
|
||||||
new_conn = await super(ReuseableTCPConnector, self).connect(
|
|
||||||
req, *args, **kwargs
|
|
||||||
|
|
||||||
|
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
|
||||||
|
|
||||||
|
old_conn = None
|
||||||
|
|
||||||
|
|
||||||
|
class ReusableSanicConnectionPool(httpcore.ConnectionPool):
|
||||||
|
async def acquire_connection(self, url, ssl, timeout):
|
||||||
|
global old_conn
|
||||||
|
if timeout.connect_timeout and not timeout.pool_timeout:
|
||||||
|
timeout.pool_timeout = timeout.connect_timeout
|
||||||
|
key = (url.scheme, url.hostname, url.port, ssl, timeout)
|
||||||
|
try:
|
||||||
|
connection = self._keepalive_connections[key].pop()
|
||||||
|
if not self._keepalive_connections[key]:
|
||||||
|
del self._keepalive_connections[key]
|
||||||
|
self.num_keepalive_connections -= 1
|
||||||
|
self.num_active_connections += 1
|
||||||
|
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
ssl_context = await self.get_ssl_context(url, ssl)
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(
|
||||||
|
self._max_connections.acquire(), timeout.pool_timeout
|
||||||
)
|
)
|
||||||
if self.old_proto is not None:
|
except asyncio.TimeoutError:
|
||||||
if self.old_proto != new_conn._protocol:
|
raise PoolTimeout()
|
||||||
|
release = functools.partial(self.release_connection, key=key)
|
||||||
|
connection = httpcore.connections.Connection(
|
||||||
|
timeout=timeout, on_release=release
|
||||||
|
)
|
||||||
|
self.num_active_connections += 1
|
||||||
|
await connection.open(url.hostname, url.port, ssl=ssl_context)
|
||||||
|
|
||||||
|
if old_conn is not None:
|
||||||
|
if old_conn != connection:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"We got a new connection, wanted the same one!"
|
"We got a new connection, wanted the same one!"
|
||||||
)
|
)
|
||||||
print(new_conn.__dict__)
|
old_conn = connection
|
||||||
self.old_proto = new_conn._protocol
|
|
||||||
return new_conn
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
class ReusableSanicAdapter(requests.adapters.HTTPAdapter):
|
||||||
|
def __init__(self):
|
||||||
|
self.pool = ReusableSanicConnectionPool()
|
||||||
|
|
||||||
|
async def send(
|
||||||
|
self,
|
||||||
|
request,
|
||||||
|
stream=False,
|
||||||
|
timeout=None,
|
||||||
|
verify=True,
|
||||||
|
cert=None,
|
||||||
|
proxies=None,
|
||||||
|
):
|
||||||
|
|
||||||
|
method = request.method
|
||||||
|
url = request.url
|
||||||
|
headers = [
|
||||||
|
(_encode(k), _encode(v)) for k, v in request.headers.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
if not request.body:
|
||||||
|
body = b""
|
||||||
|
elif isinstance(request.body, str):
|
||||||
|
body = _encode(request.body)
|
||||||
|
else:
|
||||||
|
body = request.body
|
||||||
|
|
||||||
|
if isinstance(timeout, tuple):
|
||||||
|
timeout_kwargs = {
|
||||||
|
"connect_timeout": timeout[0],
|
||||||
|
"read_timeout": timeout[1],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
timeout_kwargs = {
|
||||||
|
"connect_timeout": timeout,
|
||||||
|
"read_timeout": timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl = httpcore.SSLConfig(cert=cert, verify=verify)
|
||||||
|
timeout = httpcore.TimeoutConfig(**timeout_kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self.pool.request(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
headers=headers,
|
||||||
|
body=body,
|
||||||
|
stream=stream,
|
||||||
|
ssl=ssl,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
except (httpcore.BadResponse, socket.error) as err:
|
||||||
|
raise ConnectionError(err, request=request)
|
||||||
|
except httpcore.ConnectTimeout as err:
|
||||||
|
raise requests.exceptions.ConnectTimeout(err, request=request)
|
||||||
|
except httpcore.ReadTimeout as err:
|
||||||
|
raise requests.exceptions.ReadTimeout(err, request=request)
|
||||||
|
|
||||||
|
return self.build_response(request, response)
|
||||||
|
|
||||||
|
|
||||||
|
class ResusableSanicSession(requests.Session):
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
adapter = ReusableSanicAdapter()
|
||||||
|
self.mount("http://", adapter)
|
||||||
|
self.mount("https://", adapter)
|
||||||
|
|
||||||
|
|
||||||
class ReuseableSanicTestClient(SanicTestClient):
|
class ReuseableSanicTestClient(SanicTestClient):
|
||||||
def __init__(self, app, loop=None):
|
def __init__(self, app, loop=None):
|
||||||
super(ReuseableSanicTestClient, self).__init__(app)
|
super().__init__(app)
|
||||||
if loop is None:
|
if loop is None:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
self._loop = loop
|
self._loop = loop
|
||||||
|
@ -51,14 +153,13 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||||
uri="/",
|
uri="/",
|
||||||
gather_request=True,
|
gather_request=True,
|
||||||
debug=False,
|
debug=False,
|
||||||
server_kwargs={},
|
server_kwargs={"return_asyncio_server": True},
|
||||||
*request_args,
|
*request_args,
|
||||||
**request_kwargs
|
**request_kwargs,
|
||||||
):
|
):
|
||||||
loop = self._loop
|
loop = self._loop
|
||||||
results = [None, None]
|
results = [None, None]
|
||||||
exceptions = []
|
exceptions = []
|
||||||
do_kill_server = request_kwargs.pop("end_server", False)
|
|
||||||
if gather_request:
|
if gather_request:
|
||||||
|
|
||||||
def _collect_request(request):
|
def _collect_request(request):
|
||||||
|
@ -67,21 +168,27 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||||
|
|
||||||
self.app.request_middleware.appendleft(_collect_request)
|
self.app.request_middleware.appendleft(_collect_request)
|
||||||
|
|
||||||
|
if uri.startswith(
|
||||||
|
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
|
||||||
|
):
|
||||||
|
url = uri
|
||||||
|
else:
|
||||||
|
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
|
||||||
|
scheme = "http"
|
||||||
|
url = "{scheme}://{host}:{port}{uri}".format(
|
||||||
|
scheme=scheme, host=HOST, port=PORT, uri=uri
|
||||||
|
)
|
||||||
|
|
||||||
@self.app.listener("after_server_start")
|
@self.app.listener("after_server_start")
|
||||||
async def _collect_response(loop):
|
async def _collect_response(loop):
|
||||||
try:
|
try:
|
||||||
if do_kill_server:
|
|
||||||
request_kwargs["end_session"] = True
|
|
||||||
response = await self._local_request(
|
response = await self._local_request(
|
||||||
method, uri, *request_args, **request_kwargs
|
method, url, *request_args, **request_kwargs
|
||||||
)
|
)
|
||||||
results[-1] = response
|
results[-1] = response
|
||||||
except Exception as e2:
|
except Exception as e2:
|
||||||
import traceback
|
# traceback.print_tb(e2.__traceback__)
|
||||||
|
|
||||||
traceback.print_tb(e2.__traceback__)
|
|
||||||
exceptions.append(e2)
|
exceptions.append(e2)
|
||||||
# Don't stop here! self.app.stop()
|
|
||||||
|
|
||||||
if self._server is not None:
|
if self._server is not None:
|
||||||
_server = self._server
|
_server = self._server
|
||||||
|
@ -96,27 +203,14 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop._stopping = False
|
loop._stopping = False
|
||||||
http_server = loop.run_until_complete(_server_co)
|
_server = loop.run_until_complete(_server_co)
|
||||||
except Exception as e1:
|
except Exception as e1:
|
||||||
import traceback
|
# traceback.print_tb(e1.__traceback__)
|
||||||
|
|
||||||
traceback.print_tb(e1.__traceback__)
|
|
||||||
raise e1
|
raise e1
|
||||||
self._server = _server = http_server
|
self._server = _server
|
||||||
server.trigger_events(self.app.listeners["after_server_start"], loop)
|
server.trigger_events(self.app.listeners["after_server_start"], loop)
|
||||||
self.app.listeners["after_server_start"].pop()
|
self.app.listeners["after_server_start"].pop()
|
||||||
|
|
||||||
if do_kill_server:
|
|
||||||
try:
|
|
||||||
_server.close()
|
|
||||||
self._server = None
|
|
||||||
loop.run_until_complete(_server.wait_closed())
|
|
||||||
self.app.stop()
|
|
||||||
except Exception as e3:
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
traceback.print_tb(e3.__traceback__)
|
|
||||||
exceptions.append(e3)
|
|
||||||
if exceptions:
|
if exceptions:
|
||||||
raise ValueError("Exception during request: {}".format(exceptions))
|
raise ValueError("Exception during request: {}".format(exceptions))
|
||||||
|
|
||||||
|
@ -139,59 +233,61 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||||
"Request object expected, got ({})".format(results)
|
"Request object expected, got ({})".format(results)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def kill_server(self):
|
||||||
|
try:
|
||||||
|
if self._server:
|
||||||
|
self._server.close()
|
||||||
|
self._loop.run_until_complete(self._server.wait_closed())
|
||||||
|
self._server = None
|
||||||
|
self.app.stop()
|
||||||
|
|
||||||
|
if self._session:
|
||||||
|
self._loop.run_until_complete(self._session.close())
|
||||||
|
self._session = None
|
||||||
|
|
||||||
|
except Exception as e3:
|
||||||
|
raise e3
|
||||||
|
|
||||||
# Copied from SanicTestClient, but with some changes to reuse the
|
# Copied from SanicTestClient, but with some changes to reuse the
|
||||||
# same TCPConnection and the sane ClientSession more than once.
|
# same TCPConnection and the sane ClientSession more than once.
|
||||||
# Note, you cannot use the same session if you are in a _different_
|
# Note, you cannot use the same session if you are in a _different_
|
||||||
# loop, so the changes above are required too.
|
# loop, so the changes above are required too.
|
||||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
async def _local_request(self, method, url, *args, **kwargs):
|
||||||
|
raw_cookies = kwargs.pop("raw_cookies", None)
|
||||||
request_keepalive = kwargs.pop(
|
request_keepalive = kwargs.pop(
|
||||||
"request_keepalive", CONFIG_FOR_TESTS['KEEP_ALIVE_TIMEOUT']
|
"request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"]
|
||||||
)
|
)
|
||||||
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
|
|
||||||
url = uri
|
|
||||||
else:
|
|
||||||
url = "http://{host}:{port}{uri}".format(
|
|
||||||
host=HOST, port=self.port, uri=uri
|
|
||||||
)
|
|
||||||
do_kill_session = kwargs.pop("end_session", False)
|
|
||||||
if self._session:
|
if self._session:
|
||||||
session = self._session
|
_session = self._session
|
||||||
else:
|
else:
|
||||||
if self._tcp_connector:
|
_session = ResusableSanicSession()
|
||||||
conn = self._tcp_connector
|
self._session = _session
|
||||||
else:
|
async with _session as session:
|
||||||
conn = ReuseableTCPConnector(
|
|
||||||
ssl=False,
|
|
||||||
loop=self._loop,
|
|
||||||
keepalive_timeout=request_keepalive,
|
|
||||||
)
|
|
||||||
self._tcp_connector = conn
|
|
||||||
session = aiohttp.ClientSession(
|
|
||||||
cookies=cookies, connector=conn, loop=self._loop
|
|
||||||
)
|
|
||||||
self._session = session
|
|
||||||
|
|
||||||
async with getattr(session, method.lower())(
|
|
||||||
url, *args, **kwargs
|
|
||||||
) as response:
|
|
||||||
try:
|
try:
|
||||||
response.text = await response.text()
|
response = await getattr(session, method.lower())(
|
||||||
except UnicodeDecodeError:
|
url,
|
||||||
response.text = None
|
verify=False,
|
||||||
|
timeout=request_keepalive,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
except NameError:
|
||||||
|
raise Exception(response.status_code)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response.json = await response.json()
|
response.json = response.json()
|
||||||
except (
|
except (JSONDecodeError, UnicodeDecodeError):
|
||||||
JSONDecodeError,
|
|
||||||
UnicodeDecodeError,
|
|
||||||
aiohttp.ClientResponseError,
|
|
||||||
):
|
|
||||||
response.json = None
|
response.json = None
|
||||||
|
|
||||||
response.body = await response.read()
|
response.body = await response.read()
|
||||||
if do_kill_session:
|
response.status = response.status_code
|
||||||
await session.close()
|
response.content_type = response.headers.get("content-type")
|
||||||
self._session = None
|
|
||||||
|
if raw_cookies:
|
||||||
|
response.raw_cookies = {}
|
||||||
|
for cookie in response.cookies:
|
||||||
|
response.raw_cookies[cookie.name] = cookie
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -231,9 +327,10 @@ def test_keep_alive_timeout_reuse():
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
loop.run_until_complete(aio_sleep(1))
|
loop.run_until_complete(aio_sleep(1))
|
||||||
request, response = client.get("/1", end_server=True)
|
request, response = client.get("/1")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
|
client.kill_server()
|
||||||
|
|
||||||
|
|
||||||
def test_keep_alive_client_timeout():
|
def test_keep_alive_client_timeout():
|
||||||
|
@ -243,20 +340,21 @@ def test_keep_alive_client_timeout():
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
|
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
|
||||||
headers = {"Connection": "keep-alive"}
|
headers = {"Connection": "keep-alive"}
|
||||||
request, response = client.get("/1", headers=headers, request_keepalive=1)
|
try:
|
||||||
|
request, response = client.get(
|
||||||
|
"/1", headers=headers, request_keepalive=1
|
||||||
|
)
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
loop.run_until_complete(aio_sleep(2))
|
loop.run_until_complete(aio_sleep(2))
|
||||||
exception = None
|
exception = None
|
||||||
try:
|
request, response = client.get("/1", request_keepalive=1)
|
||||||
request, response = client.get(
|
|
||||||
"/1", end_server=True, request_keepalive=1
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
exception = e
|
exception = e
|
||||||
assert exception is not None
|
assert exception is not None
|
||||||
assert isinstance(exception, ValueError)
|
assert isinstance(exception, ValueError)
|
||||||
assert "got a new connection" in exception.args[0]
|
assert "got a new connection" in exception.args[0]
|
||||||
|
client.kill_server()
|
||||||
|
|
||||||
|
|
||||||
def test_keep_alive_server_timeout():
|
def test_keep_alive_server_timeout():
|
||||||
|
@ -268,15 +366,15 @@ def test_keep_alive_server_timeout():
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
|
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
|
||||||
headers = {"Connection": "keep-alive"}
|
headers = {"Connection": "keep-alive"}
|
||||||
request, response = client.get("/1", headers=headers, request_keepalive=60)
|
try:
|
||||||
|
request, response = client.get(
|
||||||
|
"/1", headers=headers, request_keepalive=60
|
||||||
|
)
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
loop.run_until_complete(aio_sleep(3))
|
loop.run_until_complete(aio_sleep(3))
|
||||||
exception = None
|
exception = None
|
||||||
try:
|
request, response = client.get("/1", request_keepalive=60)
|
||||||
request, response = client.get(
|
|
||||||
"/1", request_keepalive=60, end_server=True
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
exception = e
|
exception = e
|
||||||
assert exception is not None
|
assert exception is not None
|
||||||
|
@ -285,3 +383,4 @@ def test_keep_alive_server_timeout():
|
||||||
"Connection reset" in exception.args[0]
|
"Connection reset" in exception.args[0]
|
||||||
or "got a new connection" in exception.args[0]
|
or "got a new connection" in exception.args[0]
|
||||||
)
|
)
|
||||||
|
client.kill_server()
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import uuid
|
|
||||||
import logging
|
import logging
|
||||||
|
import uuid
|
||||||
|
|
||||||
from io import StringIO
|
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
|
from io import StringIO
|
||||||
import pytest
|
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import sanic
|
import sanic
|
||||||
from sanic.response import text
|
|
||||||
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.log import logger
|
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
logging_format = """module: %(module)s; \
|
logging_format = """module: %(module)s; \
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import logging
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
from sanic.config import BASE_LOGO
|
from sanic.config import BASE_LOGO
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import uvloop # noqa
|
import uvloop # noqa
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@ except BaseException:
|
||||||
|
|
||||||
|
|
||||||
def test_logo_base(app, caplog):
|
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()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
loop._stopping = False
|
loop._stopping = False
|
||||||
|
@ -31,7 +32,7 @@ def test_logo_base(app, caplog):
|
||||||
def test_logo_false(app, caplog):
|
def test_logo_false(app, caplog):
|
||||||
app.config.LOGO = False
|
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()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
loop._stopping = False
|
loop._stopping = False
|
||||||
|
@ -50,7 +51,7 @@ def test_logo_false(app, caplog):
|
||||||
def test_logo_true(app, caplog):
|
def test_logo_true(app, caplog):
|
||||||
app.config.LOGO = True
|
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()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
loop._stopping = False
|
loop._stopping = False
|
||||||
|
@ -69,7 +70,7 @@ def test_logo_true(app, caplog):
|
||||||
def test_logo_custom(app, caplog):
|
def test_logo_custom(app, caplog):
|
||||||
app.config.LOGO = "My Custom Logo"
|
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()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
loop._stopping = False
|
loop._stopping = False
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from asyncio import CancelledError
|
from asyncio import CancelledError
|
||||||
|
|
||||||
from sanic.exceptions import NotFound
|
from sanic.exceptions import NotFound
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import HTTPResponse, text
|
from sanic.response import HTTPResponse, text
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# GET
|
# GET
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
import pickle
|
||||||
import random
|
import random
|
||||||
import signal
|
import signal
|
||||||
import pickle
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic.testing import HOST, PORT
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
from sanic.testing import HOST, PORT
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.response import text
|
|
||||||
from sanic.exceptions import URLBuildError
|
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
|
from sanic.exceptions import URLBuildError
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import pytest
|
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from sanic.response import text, redirect
|
import pytest
|
||||||
|
|
||||||
|
from sanic.response import redirect, text
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
from sanic.response import text, stream
|
from sanic.response import stream, text
|
||||||
|
|
||||||
|
|
||||||
async def test_request_cancel_when_connection_lost(loop, app, test_client):
|
async def test_request_cancel_when_connection_lost(loop, app, test_client):
|
||||||
|
|
|
@ -2,6 +2,7 @@ import random
|
||||||
|
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ujson import loads
|
from ujson import loads
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import asyncio
|
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.views import CompositionView
|
|
||||||
from sanic.views import HTTPMethodView
|
|
||||||
from sanic.views import stream as stream_decorator
|
|
||||||
from sanic.response import stream, text
|
|
||||||
from sanic.request import StreamBuffer
|
from sanic.request import StreamBuffer
|
||||||
|
from sanic.response import stream, text
|
||||||
|
from sanic.views import CompositionView, HTTPMethodView
|
||||||
|
from sanic.views import stream as stream_decorator
|
||||||
|
|
||||||
|
|
||||||
data = "abc" * 10000000
|
data = "abc" * 10000000
|
||||||
|
@ -270,6 +268,21 @@ def test_request_stream_blueprint(app):
|
||||||
|
|
||||||
return stream(streaming)
|
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)
|
app.blueprint(bp)
|
||||||
|
|
||||||
assert app.is_request_stream is True
|
assert app.is_request_stream is True
|
||||||
|
@ -314,6 +327,10 @@ def test_request_stream_blueprint(app):
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == data
|
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):
|
def test_request_stream_composition_view(app):
|
||||||
"""for self.is_request_stream = True"""
|
"""for self.is_request_stream = True"""
|
||||||
|
|
|
@ -1,185 +1,73 @@
|
||||||
from json import JSONDecodeError
|
import asyncio
|
||||||
|
|
||||||
|
import httpcore
|
||||||
|
import requests_async as requests
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
import asyncio
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
import aiohttp
|
from sanic.testing import SanicTestClient
|
||||||
from aiohttp import TCPConnector
|
|
||||||
from sanic.testing import SanicTestClient, HOST
|
|
||||||
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
# direct use
|
|
||||||
import packaging
|
|
||||||
|
|
||||||
version = packaging.version
|
|
||||||
except (ImportError, AttributeError):
|
|
||||||
# setuptools v39.0 and above.
|
|
||||||
try:
|
|
||||||
from setuptools.extern import packaging
|
|
||||||
except ImportError:
|
|
||||||
# Before setuptools v39.0
|
|
||||||
from pkg_resources.extern import packaging
|
|
||||||
version = packaging.version
|
|
||||||
except ImportError:
|
|
||||||
raise RuntimeError("The 'packaging' library is missing.")
|
|
||||||
|
|
||||||
|
|
||||||
aiohttp_version = version.parse(aiohttp.__version__)
|
class DelayableSanicConnectionPool(httpcore.ConnectionPool):
|
||||||
|
def __init__(self, request_delay=None, *args, **kwargs):
|
||||||
|
self._request_delay = request_delay
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
async def request(
|
||||||
|
self,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
headers=(),
|
||||||
|
body=b"",
|
||||||
|
stream=False,
|
||||||
|
ssl=None,
|
||||||
|
timeout=None,
|
||||||
|
):
|
||||||
|
if ssl is None:
|
||||||
|
ssl = self.ssl_config
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.timeout
|
||||||
|
|
||||||
class DelayableTCPConnector(TCPConnector):
|
parsed_url = httpcore.URL(url)
|
||||||
class RequestContextManager(object):
|
request = httpcore.Request(
|
||||||
def __new__(cls, req, delay):
|
method, parsed_url, headers=headers, body=body
|
||||||
cls = super(
|
|
||||||
DelayableTCPConnector.RequestContextManager, cls
|
|
||||||
).__new__(cls)
|
|
||||||
cls.req = req
|
|
||||||
cls.send_task = None
|
|
||||||
cls.resp = None
|
|
||||||
cls.orig_send = getattr(req, "send")
|
|
||||||
cls.orig_start = None
|
|
||||||
cls.delay = delay
|
|
||||||
cls._acting_as = req
|
|
||||||
return cls
|
|
||||||
|
|
||||||
def __getattr__(self, item):
|
|
||||||
acting_as = self._acting_as
|
|
||||||
return getattr(acting_as, item)
|
|
||||||
|
|
||||||
async def start(self, connection, read_until_eof=False):
|
|
||||||
if self.send_task is None:
|
|
||||||
raise RuntimeError("do a send() before you do a start()")
|
|
||||||
resp = await self.send_task
|
|
||||||
self.send_task = None
|
|
||||||
self.resp = resp
|
|
||||||
self._acting_as = self.resp
|
|
||||||
self.orig_start = getattr(resp, "start")
|
|
||||||
|
|
||||||
try:
|
|
||||||
if aiohttp_version >= version.parse("3.3.0"):
|
|
||||||
ret = await self.orig_start(connection)
|
|
||||||
else:
|
|
||||||
ret = await self.orig_start(connection, read_until_eof)
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.resp is not None:
|
|
||||||
self.resp.close()
|
|
||||||
if self.send_task is not None:
|
|
||||||
self.send_task.cancel()
|
|
||||||
|
|
||||||
async def delayed_send(self, *args, **kwargs):
|
|
||||||
req = self.req
|
|
||||||
if self.delay and self.delay > 0:
|
|
||||||
# sync_sleep(self.delay)
|
|
||||||
await asyncio.sleep(self.delay)
|
|
||||||
t = req.loop.time()
|
|
||||||
print("sending at {}".format(t), flush=True)
|
|
||||||
next(iter(args)) # first arg is connection
|
|
||||||
|
|
||||||
try:
|
|
||||||
return await self.orig_send(*args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
if aiohttp_version < version.parse("3.1.0"):
|
|
||||||
return aiohttp.ClientResponse(req.method, req.url)
|
|
||||||
kw = dict(
|
|
||||||
writer=None,
|
|
||||||
continue100=None,
|
|
||||||
timer=None,
|
|
||||||
request_info=None,
|
|
||||||
traces=[],
|
|
||||||
loop=req.loop,
|
|
||||||
session=None,
|
|
||||||
)
|
)
|
||||||
if aiohttp_version < version.parse("3.3.0"):
|
connection = await self.acquire_connection(
|
||||||
kw["auto_decompress"] = None
|
parsed_url, ssl=ssl, timeout=timeout
|
||||||
return aiohttp.ClientResponse(req.method, req.url, **kw)
|
|
||||||
|
|
||||||
def _send(self, *args, **kwargs):
|
|
||||||
gen = self.delayed_send(*args, **kwargs)
|
|
||||||
task = self.req.loop.create_task(gen)
|
|
||||||
self.send_task = task
|
|
||||||
self._acting_as = task
|
|
||||||
return self
|
|
||||||
|
|
||||||
if aiohttp_version >= version.parse("3.1.0"):
|
|
||||||
# aiohttp changed the request.send method to async
|
|
||||||
async def send(self, *args, **kwargs):
|
|
||||||
return self._send(*args, **kwargs)
|
|
||||||
|
|
||||||
else:
|
|
||||||
send = _send
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
_post_connect_delay = kwargs.pop("post_connect_delay", 0)
|
|
||||||
_pre_request_delay = kwargs.pop("pre_request_delay", 0)
|
|
||||||
super(DelayableTCPConnector, self).__init__(*args, **kwargs)
|
|
||||||
self._post_connect_delay = _post_connect_delay
|
|
||||||
self._pre_request_delay = _pre_request_delay
|
|
||||||
|
|
||||||
async def connect(self, req, *args, **kwargs):
|
|
||||||
d_req = DelayableTCPConnector.RequestContextManager(
|
|
||||||
req, self._pre_request_delay
|
|
||||||
)
|
)
|
||||||
conn = await super(DelayableTCPConnector, self).connect(
|
if self._request_delay:
|
||||||
req, *args, **kwargs
|
print(f"\t>> Sleeping ({self._request_delay})")
|
||||||
)
|
await asyncio.sleep(self._request_delay)
|
||||||
if self._post_connect_delay and self._post_connect_delay > 0:
|
response = await connection.send(request)
|
||||||
await asyncio.sleep(self._post_connect_delay, loop=self._loop)
|
if not stream:
|
||||||
req.send = d_req.send
|
try:
|
||||||
t = req.loop.time()
|
await response.read()
|
||||||
print("Connected at {}".format(t), flush=True)
|
finally:
|
||||||
return conn
|
await response.close()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class DelayableSanicAdapter(requests.adapters.HTTPAdapter):
|
||||||
|
def __init__(self, request_delay=None):
|
||||||
|
self.pool = DelayableSanicConnectionPool(request_delay=request_delay)
|
||||||
|
|
||||||
|
|
||||||
|
class DelayableSanicSession(requests.Session):
|
||||||
|
def __init__(self, request_delay=None, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
adapter = DelayableSanicAdapter(request_delay=request_delay)
|
||||||
|
self.mount("http://", adapter)
|
||||||
|
self.mount("https://", adapter)
|
||||||
|
|
||||||
|
|
||||||
class DelayableSanicTestClient(SanicTestClient):
|
class DelayableSanicTestClient(SanicTestClient):
|
||||||
def __init__(self, app, loop, request_delay=1):
|
def __init__(self, app, request_delay=None):
|
||||||
super(DelayableSanicTestClient, self).__init__(app)
|
super().__init__(app)
|
||||||
self._request_delay = request_delay
|
self._request_delay = request_delay
|
||||||
self._loop = None
|
self._loop = None
|
||||||
|
|
||||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
def get_new_session(self):
|
||||||
if self._loop is None:
|
return DelayableSanicSession(request_delay=self._request_delay)
|
||||||
self._loop = asyncio.get_event_loop()
|
|
||||||
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
|
|
||||||
url = uri
|
|
||||||
else:
|
|
||||||
url = "http://{host}:{port}{uri}".format(
|
|
||||||
host=HOST, port=self.port, uri=uri
|
|
||||||
)
|
|
||||||
conn = DelayableTCPConnector(
|
|
||||||
pre_request_delay=self._request_delay,
|
|
||||||
ssl=False,
|
|
||||||
loop=self._loop,
|
|
||||||
)
|
|
||||||
async with aiohttp.ClientSession(
|
|
||||||
cookies=cookies, connector=conn, loop=self._loop
|
|
||||||
) as session:
|
|
||||||
# Insert a delay after creating the connection
|
|
||||||
# But before sending the request.
|
|
||||||
|
|
||||||
async with getattr(session, method.lower())(
|
|
||||||
url, *args, **kwargs
|
|
||||||
) as response:
|
|
||||||
try:
|
|
||||||
response.text = await response.text()
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
response.text = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
response.json = await response.json()
|
|
||||||
except (
|
|
||||||
JSONDecodeError,
|
|
||||||
UnicodeDecodeError,
|
|
||||||
aiohttp.ClientResponseError,
|
|
||||||
):
|
|
||||||
response.json = None
|
|
||||||
|
|
||||||
response.body = await response.read()
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
request_timeout_default_app = Sanic("test_request_timeout_default")
|
request_timeout_default_app = Sanic("test_request_timeout_default")
|
||||||
|
@ -204,14 +92,14 @@ async def ws_handler1(request, ws):
|
||||||
|
|
||||||
|
|
||||||
def test_default_server_error_request_timeout():
|
def test_default_server_error_request_timeout():
|
||||||
client = DelayableSanicTestClient(request_timeout_default_app, None, 2)
|
client = DelayableSanicTestClient(request_timeout_default_app, 2)
|
||||||
request, response = client.get("/1")
|
request, response = client.get("/1")
|
||||||
assert response.status == 408
|
assert response.status == 408
|
||||||
assert response.text == "Error: Request Timeout"
|
assert response.text == "Error: Request Timeout"
|
||||||
|
|
||||||
|
|
||||||
def test_default_server_error_request_dont_timeout():
|
def test_default_server_error_request_dont_timeout():
|
||||||
client = DelayableSanicTestClient(request_no_timeout_app, None, 0.2)
|
client = DelayableSanicTestClient(request_no_timeout_app, 0.2)
|
||||||
request, response = client.get("/1")
|
request, response = client.get("/1")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == "OK"
|
assert response.text == "OK"
|
||||||
|
@ -226,7 +114,7 @@ def test_default_server_error_websocket_request_timeout():
|
||||||
"Sec-WebSocket-Version": "13",
|
"Sec-WebSocket-Version": "13",
|
||||||
}
|
}
|
||||||
|
|
||||||
client = DelayableSanicTestClient(request_timeout_default_app, None, 2)
|
client = DelayableSanicTestClient(request_timeout_default_app, 2)
|
||||||
request, response = client.get("/ws1", headers=headers)
|
request, response = client.get("/ws1", headers=headers)
|
||||||
|
|
||||||
assert response.status == 408
|
assert response.status == 408
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
from json import dumps as json_dumps
|
from json import dumps as json_dumps
|
||||||
from json import loads as json_loads
|
from json import loads as json_loads
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Blueprint, Sanic
|
||||||
from sanic import Blueprint
|
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE
|
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
from sanic.testing import HOST, PORT
|
from sanic.testing import HOST, PORT
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# GET
|
# GET
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
@ -29,7 +30,7 @@ def test_sync(app):
|
||||||
assert response.text == "Hello"
|
assert response.text == "Hello"
|
||||||
|
|
||||||
|
|
||||||
def test_remote_address(app):
|
def test_ip(app):
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def handler(request):
|
def handler(request):
|
||||||
return text("{}".format(request.ip))
|
return text("{}".format(request.ip))
|
||||||
|
@ -130,11 +131,14 @@ def test_query_string(app):
|
||||||
|
|
||||||
assert request.args.get("test1") == "1"
|
assert request.args.get("test1") == "1"
|
||||||
assert request.args.get("test2") == "false"
|
assert request.args.get("test2") == "false"
|
||||||
|
assert request.args.getlist("test2") == ["false", "true"]
|
||||||
|
assert request.args.getlist("test1") == ["1"]
|
||||||
|
assert request.args.get("test3", default="My value") == "My value"
|
||||||
|
|
||||||
|
|
||||||
def test_uri_template(app):
|
def test_uri_template(app):
|
||||||
@app.route("/foo/<id:int>/bar/<name:[A-z]+>")
|
@app.route("/foo/<id:int>/bar/<name:[A-z]+>")
|
||||||
async def handler(request):
|
async def handler(request, id, name):
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
|
||||||
request, response = app.test_client.get("/foo/123/bar/baz")
|
request, response = app.test_client.get("/foo/123/bar/baz")
|
||||||
|
@ -200,11 +204,23 @@ def test_content_type(app):
|
||||||
assert response.text == "application/json"
|
assert response.text == "application/json"
|
||||||
|
|
||||||
|
|
||||||
def test_remote_addr(app):
|
def test_remote_addr_with_two_proxies(app):
|
||||||
|
app.config.PROXIES_COUNT = 2
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
return text(request.remote_addr)
|
return text(request.remote_addr)
|
||||||
|
|
||||||
|
headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == "127.0.0.2"
|
||||||
|
assert response.text == "127.0.0.2"
|
||||||
|
|
||||||
|
headers = {"X-Forwarded-For": "127.0.1.1"}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == ""
|
||||||
|
assert response.text == ""
|
||||||
|
|
||||||
headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
|
headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
|
||||||
request, response = app.test_client.get("/", headers=headers)
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
assert request.remote_addr == "127.0.0.1"
|
assert request.remote_addr == "127.0.0.1"
|
||||||
|
@ -219,6 +235,86 @@ def test_remote_addr(app):
|
||||||
assert request.remote_addr == "127.0.0.1"
|
assert request.remote_addr == "127.0.0.1"
|
||||||
assert response.text == "127.0.0.1"
|
assert response.text == "127.0.0.1"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-Forwarded-For": ", 127.0.2.2, , ,127.0.0.1, , ,,127.0.1.2"
|
||||||
|
}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == "127.0.0.1"
|
||||||
|
assert response.text == "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_addr_with_infinite_number_of_proxies(app):
|
||||||
|
app.config.PROXIES_COUNT = -1
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def handler(request):
|
||||||
|
return text(request.remote_addr)
|
||||||
|
|
||||||
|
headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == "127.0.0.2"
|
||||||
|
assert response.text == "127.0.0.2"
|
||||||
|
|
||||||
|
headers = {"X-Forwarded-For": "127.0.1.1"}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == "127.0.1.1"
|
||||||
|
assert response.text == "127.0.1.1"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-Forwarded-For": "127.0.0.5, 127.0.0.4, 127.0.0.3, 127.0.0.2, 127.0.0.1"
|
||||||
|
}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == "127.0.0.5"
|
||||||
|
assert response.text == "127.0.0.5"
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_addr_without_proxy(app):
|
||||||
|
app.config.PROXIES_COUNT = 0
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def handler(request):
|
||||||
|
return text(request.remote_addr)
|
||||||
|
|
||||||
|
headers = {"X-Real-IP": "127.0.0.2", "X-Forwarded-For": "127.0.1.1"}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == ""
|
||||||
|
assert response.text == ""
|
||||||
|
|
||||||
|
headers = {"X-Forwarded-For": "127.0.1.1"}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == ""
|
||||||
|
assert response.text == ""
|
||||||
|
|
||||||
|
headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == ""
|
||||||
|
assert response.text == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_addr_custom_headers(app):
|
||||||
|
app.config.PROXIES_COUNT = 1
|
||||||
|
app.config.REAL_IP_HEADER = "Client-IP"
|
||||||
|
app.config.FORWARDED_FOR_HEADER = "Forwarded"
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def handler(request):
|
||||||
|
return text(request.remote_addr)
|
||||||
|
|
||||||
|
headers = {"X-Real-IP": "127.0.0.2", "Forwarded": "127.0.1.1"}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == "127.0.1.1"
|
||||||
|
assert response.text == "127.0.1.1"
|
||||||
|
|
||||||
|
headers = {"X-Forwarded-For": "127.0.1.1"}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == ""
|
||||||
|
assert response.text == ""
|
||||||
|
|
||||||
|
headers = {"Client-IP": "127.0.0.2", "Forwarded": "127.0.1.1"}
|
||||||
|
request, response = app.test_client.get("/", headers=headers)
|
||||||
|
assert request.remote_addr == "127.0.0.2"
|
||||||
|
assert response.text == "127.0.0.2"
|
||||||
|
|
||||||
|
|
||||||
def test_match_info(app):
|
def test_match_info(app):
|
||||||
@app.route("/api/v1/user/<user_id>/")
|
@app.route("/api/v1/user/<user_id>/")
|
||||||
|
@ -432,21 +528,59 @@ def test_request_string_representation(app):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"payload",
|
"payload,filename",
|
||||||
[
|
[
|
||||||
|
(
|
||||||
"------sanic\r\n"
|
"------sanic\r\n"
|
||||||
'Content-Disposition: form-data; filename="filename"; name="test"\r\n'
|
'Content-Disposition: form-data; filename="filename"; name="test"\r\n'
|
||||||
"\r\n"
|
"\r\n"
|
||||||
"OK\r\n"
|
"OK\r\n"
|
||||||
"------sanic--\r\n",
|
"------sanic--\r\n",
|
||||||
|
"filename",
|
||||||
|
),
|
||||||
|
(
|
||||||
"------sanic\r\n"
|
"------sanic\r\n"
|
||||||
'content-disposition: form-data; filename="filename"; name="test"\r\n'
|
'content-disposition: form-data; filename="filename"; name="test"\r\n'
|
||||||
"\r\n"
|
"\r\n"
|
||||||
'content-type: application/json; {"field": "value"}\r\n'
|
'content-type: application/json; {"field": "value"}\r\n'
|
||||||
"------sanic--\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"])
|
@app.route("/", methods=["POST"])
|
||||||
async def post(request):
|
async def post(request):
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
@ -454,7 +588,7 @@ def test_request_multipart_files(app, payload):
|
||||||
headers = {"content-type": "multipart/form-data; boundary=----sanic"}
|
headers = {"content-type": "multipart/form-data; boundary=----sanic"}
|
||||||
|
|
||||||
request, _ = app.test_client.post(data=payload, headers=headers)
|
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):
|
def test_request_multipart_file_with_json_content_type(app):
|
||||||
|
@ -566,7 +700,7 @@ def test_request_repr(app):
|
||||||
assert repr(request) == "<Request: GET />"
|
assert repr(request) == "<Request: GET />"
|
||||||
|
|
||||||
request.method = None
|
request.method = None
|
||||||
assert repr(request) == "<Request>"
|
assert repr(request) == "<Request: None />"
|
||||||
|
|
||||||
|
|
||||||
def test_request_bool(app):
|
def test_request_bool(app):
|
||||||
|
@ -626,6 +760,75 @@ def test_request_raw_args(app):
|
||||||
assert request.raw_args == params
|
assert request.raw_args == params
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_query_args(app):
|
||||||
|
# test multiple params with the same key
|
||||||
|
params = [("test", "value1"), ("test", "value2")]
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def handler(request):
|
||||||
|
return text("pass")
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/", params=params)
|
||||||
|
|
||||||
|
assert request.query_args == params
|
||||||
|
|
||||||
|
# test cached value
|
||||||
|
assert (
|
||||||
|
request.parsed_not_grouped_args[(False, False, "utf-8", "replace")]
|
||||||
|
== request.query_args
|
||||||
|
)
|
||||||
|
|
||||||
|
# test params directly in the url
|
||||||
|
request, response = app.test_client.get("/?test=value1&test=value2")
|
||||||
|
|
||||||
|
assert request.query_args == params
|
||||||
|
|
||||||
|
# test unique params
|
||||||
|
params = [("test1", "value1"), ("test2", "value2")]
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/", params=params)
|
||||||
|
|
||||||
|
assert request.query_args == params
|
||||||
|
|
||||||
|
# test no params
|
||||||
|
request, response = app.test_client.get("/")
|
||||||
|
|
||||||
|
assert not request.query_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_query_args_custom_parsing(app):
|
||||||
|
@app.get("/")
|
||||||
|
def handler(request):
|
||||||
|
return text("pass")
|
||||||
|
|
||||||
|
request, response = app.test_client.get(
|
||||||
|
"/?test1=value1&test2=&test3=value3"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert request.get_query_args(keep_blank_values=True) == [
|
||||||
|
("test1", "value1"),
|
||||||
|
("test2", ""),
|
||||||
|
("test3", "value3"),
|
||||||
|
]
|
||||||
|
assert request.query_args == [("test1", "value1"), ("test3", "value3")]
|
||||||
|
assert request.get_query_args(keep_blank_values=False) == [
|
||||||
|
("test1", "value1"),
|
||||||
|
("test3", "value3"),
|
||||||
|
]
|
||||||
|
|
||||||
|
assert request.get_args(keep_blank_values=True) == RequestParameters(
|
||||||
|
{"test1": ["value1"], "test2": [""], "test3": ["value3"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert request.args == RequestParameters(
|
||||||
|
{"test1": ["value1"], "test3": ["value3"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert request.get_args(keep_blank_values=False) == RequestParameters(
|
||||||
|
{"test1": ["value1"], "test3": ["value3"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_request_cookies(app):
|
def test_request_cookies(app):
|
||||||
|
|
||||||
cookies = {"test": "OK"}
|
cookies = {"test": "OK"}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from random import choice
|
from random import choice
|
||||||
|
@ -8,6 +9,7 @@ from unittest.mock import MagicMock
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiofiles import os as async_os
|
from aiofiles import os as async_os
|
||||||
|
|
||||||
from sanic.response import (
|
from sanic.response import (
|
||||||
|
@ -18,11 +20,11 @@ from sanic.response import (
|
||||||
json,
|
json,
|
||||||
raw,
|
raw,
|
||||||
stream,
|
stream,
|
||||||
text,
|
|
||||||
)
|
)
|
||||||
from sanic.server import HttpProtocol
|
from sanic.server import HttpProtocol
|
||||||
from sanic.testing import HOST, PORT
|
from sanic.testing import HOST, PORT
|
||||||
|
|
||||||
|
|
||||||
JSON_DATA = {"ok": True}
|
JSON_DATA = {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,10 +79,10 @@ def test_response_header(app):
|
||||||
|
|
||||||
request, response = app.test_client.get("/")
|
request, response = app.test_client.get("/")
|
||||||
assert dict(response.headers) == {
|
assert dict(response.headers) == {
|
||||||
"Connection": "keep-alive",
|
"connection": "keep-alive",
|
||||||
"Keep-Alive": str(app.config.KEEP_ALIVE_TIMEOUT),
|
"keep-alive": str(app.config.KEEP_ALIVE_TIMEOUT),
|
||||||
"Content-Length": "11",
|
"content-length": "11",
|
||||||
"Content-Type": "application/json",
|
"content-type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -192,22 +194,59 @@ def test_no_content(json_app):
|
||||||
def streaming_app(app):
|
def streaming_app(app):
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return stream(sample_streaming_fn, content_type="text/csv")
|
return stream(
|
||||||
|
sample_streaming_fn,
|
||||||
|
headers={"Content-Length": "7"},
|
||||||
|
content_type="text/csv",
|
||||||
|
)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def test_streaming_adds_correct_headers(streaming_app):
|
@pytest.fixture
|
||||||
|
def non_chunked_streaming_app(app):
|
||||||
|
@app.route("/")
|
||||||
|
async def test(request):
|
||||||
|
return stream(
|
||||||
|
sample_streaming_fn,
|
||||||
|
headers={"Content-Length": "7"},
|
||||||
|
content_type="text/csv",
|
||||||
|
chunked=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def test_chunked_streaming_adds_correct_headers(streaming_app):
|
||||||
request, response = streaming_app.test_client.get("/")
|
request, response = streaming_app.test_client.get("/")
|
||||||
assert response.headers["Transfer-Encoding"] == "chunked"
|
assert response.headers["Transfer-Encoding"] == "chunked"
|
||||||
assert response.headers["Content-Type"] == "text/csv"
|
assert response.headers["Content-Type"] == "text/csv"
|
||||||
|
# Content-Length is not allowed by HTTP/1.1 specification
|
||||||
|
# when "Transfer-Encoding: chunked" is used
|
||||||
|
assert "Content-Length" not in response.headers
|
||||||
|
|
||||||
|
|
||||||
def test_streaming_returns_correct_content(streaming_app):
|
def test_chunked_streaming_returns_correct_content(streaming_app):
|
||||||
request, response = streaming_app.test_client.get("/")
|
request, response = streaming_app.test_client.get("/")
|
||||||
assert response.text == "foo,bar"
|
assert response.text == "foo,bar"
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_chunked_streaming_adds_correct_headers(
|
||||||
|
non_chunked_streaming_app
|
||||||
|
):
|
||||||
|
request, response = non_chunked_streaming_app.test_client.get("/")
|
||||||
|
assert "Transfer-Encoding" not in response.headers
|
||||||
|
assert response.headers["Content-Type"] == "text/csv"
|
||||||
|
assert response.headers["Content-Length"] == "7"
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_chunked_streaming_returns_correct_content(
|
||||||
|
non_chunked_streaming_app
|
||||||
|
):
|
||||||
|
request, response = non_chunked_streaming_app.test_client.get("/")
|
||||||
|
assert response.text == "foo,bar"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("status", [200, 201, 400, 401])
|
@pytest.mark.parametrize("status", [200, 201, 400, 401])
|
||||||
def test_stream_response_status_returns_correct_headers(status):
|
def test_stream_response_status_returns_correct_headers(status):
|
||||||
response = StreamingHTTPResponse(sample_streaming_fn, status=status)
|
response = StreamingHTTPResponse(sample_streaming_fn, status=status)
|
||||||
|
@ -227,13 +266,27 @@ def test_stream_response_keep_alive_returns_correct_headers(
|
||||||
assert b"Keep-Alive: %s\r\n" % str(keep_alive_timeout).encode() in headers
|
assert b"Keep-Alive: %s\r\n" % str(keep_alive_timeout).encode() in headers
|
||||||
|
|
||||||
|
|
||||||
def test_stream_response_includes_chunked_header():
|
def test_stream_response_includes_chunked_header_http11():
|
||||||
response = StreamingHTTPResponse(sample_streaming_fn)
|
response = StreamingHTTPResponse(sample_streaming_fn)
|
||||||
headers = response.get_headers()
|
headers = response.get_headers(version="1.1")
|
||||||
assert b"Transfer-Encoding: chunked\r\n" in headers
|
assert b"Transfer-Encoding: chunked\r\n" in headers
|
||||||
|
|
||||||
|
|
||||||
def test_stream_response_writes_correct_content_to_transport(streaming_app):
|
def test_stream_response_does_not_include_chunked_header_http10():
|
||||||
|
response = StreamingHTTPResponse(sample_streaming_fn)
|
||||||
|
headers = response.get_headers(version="1.0")
|
||||||
|
assert b"Transfer-Encoding: chunked\r\n" not in headers
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream_response_does_not_include_chunked_header_if_disabled():
|
||||||
|
response = StreamingHTTPResponse(sample_streaming_fn, chunked=False)
|
||||||
|
headers = response.get_headers(version="1.1")
|
||||||
|
assert b"Transfer-Encoding: chunked\r\n" not in headers
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream_response_writes_correct_content_to_transport_when_chunked(
|
||||||
|
streaming_app
|
||||||
|
):
|
||||||
response = StreamingHTTPResponse(sample_streaming_fn)
|
response = StreamingHTTPResponse(sample_streaming_fn)
|
||||||
response.protocol = MagicMock(HttpProtocol)
|
response.protocol = MagicMock(HttpProtocol)
|
||||||
response.protocol.transport = MagicMock(asyncio.Transport)
|
response.protocol.transport = MagicMock(asyncio.Transport)
|
||||||
|
@ -262,6 +315,42 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app):
|
||||||
b"0\r\n\r\n"
|
b"0\r\n\r\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert len(response.protocol.transport.write.call_args_list) == 4
|
||||||
|
|
||||||
|
app.stop()
|
||||||
|
|
||||||
|
streaming_app.run(host=HOST, port=PORT)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream_response_writes_correct_content_to_transport_when_not_chunked(
|
||||||
|
streaming_app,
|
||||||
|
):
|
||||||
|
response = StreamingHTTPResponse(sample_streaming_fn)
|
||||||
|
response.protocol = MagicMock(HttpProtocol)
|
||||||
|
response.protocol.transport = MagicMock(asyncio.Transport)
|
||||||
|
|
||||||
|
async def mock_drain():
|
||||||
|
pass
|
||||||
|
|
||||||
|
def mock_push_data(data):
|
||||||
|
response.protocol.transport.write(data)
|
||||||
|
|
||||||
|
response.protocol.push_data = mock_push_data
|
||||||
|
response.protocol.drain = mock_drain
|
||||||
|
|
||||||
|
@streaming_app.listener("after_server_start")
|
||||||
|
async def run_stream(app, loop):
|
||||||
|
await response.stream(version="1.0")
|
||||||
|
assert response.protocol.transport.write.call_args_list[1][0][0] == (
|
||||||
|
b"foo,"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.protocol.transport.write.call_args_list[2][0][0] == (
|
||||||
|
b"bar"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(response.protocol.transport.write.call_args_list) == 3
|
||||||
|
|
||||||
app.stop()
|
app.stop()
|
||||||
|
|
||||||
streaming_app.run(host=HOST, port=PORT)
|
streaming_app.run(host=HOST, port=PORT)
|
||||||
|
@ -276,7 +365,7 @@ def test_stream_response_with_cookies(app):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
request, response = app.test_client.get("/")
|
request, response = app.test_client.get("/")
|
||||||
assert response.cookies["test"].value == "pass"
|
assert response.cookies["test"] == "pass"
|
||||||
|
|
||||||
|
|
||||||
def test_stream_response_without_cookies(app):
|
def test_stream_response_without_cookies(app):
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
from sanic import Sanic
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from sanic.response import text
|
|
||||||
|
from sanic import Sanic
|
||||||
from sanic.exceptions import ServiceUnavailable
|
from sanic.exceptions import ServiceUnavailable
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
response_timeout_app = Sanic("test_response_timeout")
|
response_timeout_app = Sanic("test_response_timeout")
|
||||||
response_timeout_default_app = Sanic("test_response_timeout_default")
|
response_timeout_default_app = Sanic("test_response_timeout_default")
|
||||||
|
|
|
@ -7,6 +7,7 @@ from sanic.constants import HTTP_METHODS
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists
|
from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# UTF-8
|
# UTF-8
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
@ -365,9 +366,18 @@ def test_dynamic_route_number(app):
|
||||||
request, response = app.test_client.get("/weight/1234.56")
|
request, response = app.test_client.get("/weight/1234.56")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/weight/.12")
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/weight/12.")
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
request, response = app.test_client.get("/weight/1234-56")
|
request, response = app.test_client.get("/weight/1234-56")
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/weight/12.34.56")
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
def test_dynamic_route_regex(app):
|
def test_dynamic_route_regex(app):
|
||||||
@app.route("/folder/<folder_id:[A-Za-z0-9]{0,4}>")
|
@app.route("/folder/<folder_id:[A-Za-z0-9]{0,4}>")
|
||||||
|
@ -459,16 +469,8 @@ def test_websocket_route(app, url):
|
||||||
assert ws.subprotocol is None
|
assert ws.subprotocol is None
|
||||||
ev.set()
|
ev.set()
|
||||||
|
|
||||||
request, response = app.test_client.get(
|
request, response = app.test_client.websocket(url)
|
||||||
"/ws",
|
assert response.opened is True
|
||||||
headers={
|
|
||||||
"Upgrade": "websocket",
|
|
||||||
"Connection": "upgrade",
|
|
||||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
|
||||||
"Sec-WebSocket-Version": "13",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert response.status == 101
|
|
||||||
assert ev.is_set()
|
assert ev.is_set()
|
||||||
|
|
||||||
|
|
||||||
|
@ -478,54 +480,24 @@ def test_websocket_route_with_subprotocols(app):
|
||||||
@app.websocket("/ws", subprotocols=["foo", "bar"])
|
@app.websocket("/ws", subprotocols=["foo", "bar"])
|
||||||
async def handler(request, ws):
|
async def handler(request, ws):
|
||||||
results.append(ws.subprotocol)
|
results.append(ws.subprotocol)
|
||||||
|
assert ws.subprotocol is not None
|
||||||
|
|
||||||
request, response = app.test_client.get(
|
request, response = app.test_client.websocket("/ws", subprotocols=["bar"])
|
||||||
"/ws",
|
assert response.opened is True
|
||||||
headers={
|
assert results == ["bar"]
|
||||||
"Upgrade": "websocket",
|
|
||||||
"Connection": "upgrade",
|
request, response = app.test_client.websocket(
|
||||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
"/ws", subprotocols=["bar", "foo"]
|
||||||
"Sec-WebSocket-Version": "13",
|
|
||||||
"Sec-WebSocket-Protocol": "bar",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
assert response.status == 101
|
assert response.opened is True
|
||||||
|
assert results == ["bar", "bar"]
|
||||||
|
|
||||||
request, response = app.test_client.get(
|
request, response = app.test_client.websocket("/ws", subprotocols=["baz"])
|
||||||
"/ws",
|
assert response.opened is True
|
||||||
headers={
|
assert results == ["bar", "bar", None]
|
||||||
"Upgrade": "websocket",
|
|
||||||
"Connection": "upgrade",
|
|
||||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
|
||||||
"Sec-WebSocket-Version": "13",
|
|
||||||
"Sec-WebSocket-Protocol": "bar, foo",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert response.status == 101
|
|
||||||
|
|
||||||
request, response = app.test_client.get(
|
|
||||||
"/ws",
|
|
||||||
headers={
|
|
||||||
"Upgrade": "websocket",
|
|
||||||
"Connection": "upgrade",
|
|
||||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
|
||||||
"Sec-WebSocket-Version": "13",
|
|
||||||
"Sec-WebSocket-Protocol": "baz",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert response.status == 101
|
|
||||||
|
|
||||||
request, response = app.test_client.get(
|
|
||||||
"/ws",
|
|
||||||
headers={
|
|
||||||
"Upgrade": "websocket",
|
|
||||||
"Connection": "upgrade",
|
|
||||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
|
||||||
"Sec-WebSocket-Version": "13",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert response.status == 101
|
|
||||||
|
|
||||||
|
request, response = app.test_client.websocket("/ws")
|
||||||
|
assert response.opened is True
|
||||||
assert results == ["bar", "bar", None, None]
|
assert results == ["bar", "bar", None, None]
|
||||||
|
|
||||||
|
|
||||||
|
@ -538,16 +510,8 @@ def test_add_webscoket_route(app, strict_slashes):
|
||||||
ev.set()
|
ev.set()
|
||||||
|
|
||||||
app.add_websocket_route(handler, "/ws", strict_slashes=strict_slashes)
|
app.add_websocket_route(handler, "/ws", strict_slashes=strict_slashes)
|
||||||
request, response = app.test_client.get(
|
request, response = app.test_client.websocket("/ws")
|
||||||
"/ws",
|
assert response.opened is True
|
||||||
headers={
|
|
||||||
"Upgrade": "websocket",
|
|
||||||
"Connection": "upgrade",
|
|
||||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
|
||||||
"Sec-WebSocket-Version": "13",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert response.status == 101
|
|
||||||
assert ev.is_set()
|
assert ev.is_set()
|
||||||
|
|
||||||
|
|
||||||
|
@ -672,9 +636,18 @@ def test_dynamic_add_route_number(app):
|
||||||
request, response = app.test_client.get("/weight/1234.56")
|
request, response = app.test_client.get("/weight/1234.56")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/weight/.12")
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/weight/12.")
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
request, response = app.test_client.get("/weight/1234-56")
|
request, response = app.test_client.get("/weight/1234-56")
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = app.test_client.get("/weight/12.34.56")
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
def test_dynamic_add_route_regex(app):
|
def test_dynamic_add_route_regex(app):
|
||||||
async def handler(request, folder_id):
|
async def handler(request, folder_id):
|
||||||
|
|
|
@ -4,6 +4,7 @@ import pytest
|
||||||
|
|
||||||
from sanic.testing import HOST, PORT
|
from sanic.testing import HOST, PORT
|
||||||
|
|
||||||
|
|
||||||
AVAILABLE_LISTENERS = [
|
AVAILABLE_LISTENERS = [
|
||||||
"before_server_start",
|
"before_server_start",
|
||||||
"after_server_start",
|
"after_server_start",
|
||||||
|
@ -83,7 +84,7 @@ async def test_trigger_before_events_create_server(app):
|
||||||
async def init_db(app, loop):
|
async def init_db(app, loop):
|
||||||
app.db = MySanicDb()
|
app.db = MySanicDb()
|
||||||
|
|
||||||
await app.create_server()
|
await app.create_server(debug=True, return_asyncio_server=True)
|
||||||
|
|
||||||
assert hasattr(app, "db")
|
assert hasattr(app, "db")
|
||||||
assert isinstance(app.db, MySanicDb)
|
assert isinstance(app.db, MySanicDb)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from queue import Queue
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from sanic.response import HTTPResponse
|
from sanic.response import HTTPResponse
|
||||||
from sanic.testing import HOST, PORT
|
from sanic.testing import HOST, PORT
|
||||||
from unittest.mock import MagicMock
|
|
||||||
import asyncio
|
|
||||||
from queue import Queue
|
|
||||||
|
|
||||||
|
|
||||||
async def stop(app, loop):
|
async def stop(app, loop):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
35
tests/test_test_client_port.py
Normal file
35
tests/test_test_client_port.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from sanic.response import json, text
|
||||||
|
from sanic.testing import PORT, SanicTestClient
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
# UTF-8
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
|
||||||
|
def test_test_client_port_none(app):
|
||||||
|
@app.get("/get")
|
||||||
|
def handler(request):
|
||||||
|
return text("OK")
|
||||||
|
|
||||||
|
test_client = SanicTestClient(app, port=None)
|
||||||
|
|
||||||
|
request, response = test_client.get("/get")
|
||||||
|
assert response.text == "OK"
|
||||||
|
|
||||||
|
request, response = test_client.post("/get")
|
||||||
|
assert response.status == 405
|
||||||
|
|
||||||
|
|
||||||
|
def test_test_client_port_default(app):
|
||||||
|
@app.get("/get")
|
||||||
|
def handler(request):
|
||||||
|
return json(request.transport.get_extra_info("sockname")[1])
|
||||||
|
|
||||||
|
test_client = SanicTestClient(app)
|
||||||
|
assert test_client.port == PORT
|
||||||
|
|
||||||
|
request, response = test_client.get("/get")
|
||||||
|
assert response.json == PORT
|
|
@ -1,14 +1,17 @@
|
||||||
import pytest as pytest
|
|
||||||
from urllib.parse import urlsplit, parse_qsl
|
|
||||||
|
|
||||||
from sanic.response import text
|
|
||||||
from sanic.views import HTTPMethodView
|
|
||||||
from sanic.blueprints import Blueprint
|
|
||||||
from sanic.testing import PORT as test_port, HOST as test_host
|
|
||||||
from sanic.exceptions import URLBuildError
|
|
||||||
|
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
from urllib.parse import parse_qsl, urlsplit
|
||||||
|
|
||||||
|
import pytest as pytest
|
||||||
|
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
from sanic.exceptions import URLBuildError
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic.testing import HOST as test_host
|
||||||
|
from sanic.testing import PORT as test_port
|
||||||
|
from sanic.views import HTTPMethodView
|
||||||
|
|
||||||
|
|
||||||
URL_FOR_ARGS1 = dict(arg1=["v1", "v2"])
|
URL_FOR_ARGS1 = dict(arg1=["v1", "v2"])
|
||||||
URL_FOR_VALUE1 = "/myurl?arg1=v1&arg1=v2"
|
URL_FOR_VALUE1 = "/myurl?arg1=v1&arg1=v2"
|
||||||
URL_FOR_ARGS2 = dict(arg1=["v1", "v2"], _anchor="anchor")
|
URL_FOR_ARGS2 = dict(arg1=["v1", "v2"], _anchor="anchor")
|
||||||
|
@ -169,12 +172,28 @@ def test_fails_with_int_message(app):
|
||||||
app.url_for("fail", **failing_kwargs)
|
app.url_for("fail", **failing_kwargs)
|
||||||
|
|
||||||
expected_error = (
|
expected_error = (
|
||||||
'Value "not_int" for parameter `foo` '
|
r'Value "not_int" for parameter `foo` '
|
||||||
"does not match pattern for type `int`: \d+"
|
r"does not match pattern for type `int`: -?\d+"
|
||||||
)
|
)
|
||||||
assert str(e.value) == expected_error
|
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):
|
def test_fails_with_two_letter_string_message(app):
|
||||||
@app.route(COMPLEX_PARAM_URL)
|
@app.route(COMPLEX_PARAM_URL)
|
||||||
def fail(request):
|
def fail(request):
|
||||||
|
@ -207,12 +226,26 @@ def test_fails_with_number_message(app):
|
||||||
|
|
||||||
expected_error = (
|
expected_error = (
|
||||||
'Value "foo" for parameter `some_number` '
|
'Value "foo" for parameter `some_number` '
|
||||||
"does not match pattern for type `float`: [0-9\\\\.]+"
|
r"does not match pattern for type `float`: -?(?:\d+(?:\.\d*)?|\.\d+)"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert str(e.value) == expected_error
|
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):
|
def test_adds_other_supplied_values_as_query_string(app):
|
||||||
@app.route(COMPLEX_PARAM_URL)
|
@app.route(COMPLEX_PARAM_URL)
|
||||||
def passes(request):
|
def passes(request):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from json import dumps as json_dumps
|
from json import dumps as json_dumps
|
||||||
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import pytest as pytest
|
import pytest as pytest
|
||||||
|
|
||||||
from sanic.exceptions import InvalidUsage
|
|
||||||
from sanic.response import text, HTTPResponse
|
|
||||||
from sanic.views import HTTPMethodView, CompositionView
|
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.request import Request
|
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
|
from sanic.exceptions import InvalidUsage
|
||||||
|
from sanic.request import Request
|
||||||
|
from sanic.response import HTTPResponse, text
|
||||||
|
from sanic.views import CompositionView, HTTPMethodView
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("method", HTTP_METHODS)
|
@pytest.mark.parametrize("method", HTTP_METHODS)
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import time
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from sanic.worker import GunicornWorker
|
|
||||||
from sanic.app import Sanic
|
|
||||||
import asyncio
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sanic.app import Sanic
|
||||||
|
from sanic.worker import GunicornWorker
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def gunicorn_worker():
|
def gunicorn_worker():
|
||||||
|
@ -24,28 +27,28 @@ def gunicorn_worker():
|
||||||
worker.kill()
|
worker.kill()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope="module")
|
||||||
def gunicorn_worker_with_access_logs():
|
def gunicorn_worker_with_access_logs():
|
||||||
command = (
|
command = (
|
||||||
'gunicorn '
|
"gunicorn "
|
||||||
'--bind 127.0.0.1:1338 '
|
"--bind 127.0.0.1:1338 "
|
||||||
'--worker-class sanic.worker.GunicornWorker '
|
"--worker-class sanic.worker.GunicornWorker "
|
||||||
'examples.simple_server:app'
|
"examples.simple_server:app"
|
||||||
)
|
)
|
||||||
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
|
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
return worker
|
return worker
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope="module")
|
||||||
def gunicorn_worker_with_env_var():
|
def gunicorn_worker_with_env_var():
|
||||||
command = (
|
command = (
|
||||||
'env SANIC_ACCESS_LOG="False" '
|
'env SANIC_ACCESS_LOG="False" '
|
||||||
'gunicorn '
|
"gunicorn "
|
||||||
'--bind 127.0.0.1:1339 '
|
"--bind 127.0.0.1:1339 "
|
||||||
'--worker-class sanic.worker.GunicornWorker '
|
"--worker-class sanic.worker.GunicornWorker "
|
||||||
'--log-level info '
|
"--log-level info "
|
||||||
'examples.simple_server:app'
|
"examples.simple_server:app"
|
||||||
)
|
)
|
||||||
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
|
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
@ -62,7 +65,7 @@ def test_gunicorn_worker_no_logs(gunicorn_worker_with_env_var):
|
||||||
"""
|
"""
|
||||||
if SANIC_ACCESS_LOG was set to False do not show access logs
|
if SANIC_ACCESS_LOG was set to False do not show access logs
|
||||||
"""
|
"""
|
||||||
with urllib.request.urlopen('http://localhost:1339/') as _:
|
with urllib.request.urlopen("http://localhost:1339/") as _:
|
||||||
gunicorn_worker_with_env_var.kill()
|
gunicorn_worker_with_env_var.kill()
|
||||||
assert not gunicorn_worker_with_env_var.stdout.read()
|
assert not gunicorn_worker_with_env_var.stdout.read()
|
||||||
|
|
||||||
|
@ -71,9 +74,12 @@ def test_gunicorn_worker_with_logs(gunicorn_worker_with_access_logs):
|
||||||
"""
|
"""
|
||||||
default - show access logs
|
default - show access logs
|
||||||
"""
|
"""
|
||||||
with urllib.request.urlopen('http://localhost:1338/') as _:
|
with urllib.request.urlopen("http://localhost:1338/") as _:
|
||||||
gunicorn_worker_with_access_logs.kill()
|
gunicorn_worker_with_access_logs.kill()
|
||||||
assert b"(sanic.access)[INFO][127.0.0.1" in gunicorn_worker_with_access_logs.stdout.read()
|
assert (
|
||||||
|
b"(sanic.access)[INFO][127.0.0.1"
|
||||||
|
in gunicorn_worker_with_access_logs.stdout.read()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GunicornTestWorker(GunicornWorker):
|
class GunicornTestWorker(GunicornWorker):
|
||||||
|
|
18
tox.ini
18
tox.ini
|
@ -1,23 +1,25 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py35, py36, py37, {py35,py36,py37}-no-ext, lint, check
|
envlist = py36, py37, {py36,py37}-no-ext, lint, check
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
usedevelop = True
|
usedevelop = True
|
||||||
setenv =
|
setenv =
|
||||||
{py35,py36,py37}-no-ext: SANIC_NO_UJSON=1
|
{py36,py37}-no-ext: SANIC_NO_UJSON=1
|
||||||
{py35,py36,py37}-no-ext: SANIC_NO_UVLOOP=1
|
{py36,py37}-no-ext: SANIC_NO_UVLOOP=1
|
||||||
deps =
|
deps =
|
||||||
coverage
|
coverage
|
||||||
pytest==4.1.0
|
pytest==4.1.0
|
||||||
pytest-cov
|
pytest-cov
|
||||||
pytest-sanic
|
pytest-sanic
|
||||||
pytest-sugar
|
pytest-sugar
|
||||||
aiohttp>=2.3,<=3.2.1
|
httpcore==0.1.1
|
||||||
|
requests-async==0.4.0
|
||||||
chardet<=2.3.0
|
chardet<=2.3.0
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
gunicorn
|
gunicorn
|
||||||
|
pytest-benchmark
|
||||||
commands =
|
commands =
|
||||||
pytest tests --cov sanic --cov-report= {posargs}
|
pytest {posargs:tests --cov sanic}
|
||||||
- coverage combine --append
|
- coverage combine --append
|
||||||
coverage report -m
|
coverage report -m
|
||||||
coverage html -i
|
coverage html -i
|
||||||
|
@ -30,7 +32,7 @@ deps =
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
flake8 sanic
|
flake8 sanic
|
||||||
black --check --verbose sanic
|
black --config ./.black.toml --check --verbose sanic
|
||||||
isort --check-only --recursive sanic
|
isort --check-only --recursive sanic
|
||||||
|
|
||||||
[testenv:check]
|
[testenv:check]
|
||||||
|
@ -39,3 +41,7 @@ deps =
|
||||||
pygments
|
pygments
|
||||||
commands =
|
commands =
|
||||||
python setup.py check -r -s
|
python setup.py check -r -s
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
filterwarnings =
|
||||||
|
ignore:.*async with lock.* instead:DeprecationWarning
|
||||||
|
|
Loading…
Reference in New Issue
Block a user