Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68d5039c5f | ||
|
|
9d07988d75 | ||
|
|
1eaa2e3a5f | ||
|
|
c7f2399ded | ||
|
|
650ab61c2e | ||
|
|
b7df86e7dd | ||
|
|
72b445621b | ||
|
|
ba0e9baffa | ||
|
|
503622438a | ||
|
|
d5e9aae425 | ||
|
|
a2666a2b8a | ||
|
|
966b05b47e | ||
|
|
78fe97b9cb | ||
|
|
d2094fed38 | ||
|
|
e2d65ba57c | ||
|
|
c9d8ab4b27 | ||
|
|
891f99d71d | ||
|
|
3f47fa9f99 | ||
|
|
b1c23fdbaa | ||
|
|
62e0e5b9ec | ||
|
|
fb61834a2e | ||
|
|
8fbbe94fe1 | ||
|
|
ab706dda7d | ||
|
|
b2d4132a14 | ||
|
|
322cf89c92 | ||
|
|
09acd64ba1 | ||
|
|
072fcfe03e | ||
|
|
13079c6e30 | ||
|
|
1b1a51c1bb | ||
|
|
39d134994d | ||
|
|
5f9e98554f | ||
|
|
0d9a21718f | ||
|
|
daf42c5f43 | ||
|
|
3685b4de85 | ||
|
|
2631f10c5e | ||
|
|
f21db60859 | ||
|
|
c15158224b | ||
|
|
a57c14c70b | ||
|
|
bb2bd2fe53 | ||
|
|
aebe2b5809 | ||
|
|
9172399b8c | ||
|
|
22c0d97783 | ||
|
|
3ead529693 | ||
|
|
e36f398aa6 | ||
|
|
18cd4caf70 | ||
|
|
80df45ba6d | ||
|
|
16d262e3e5 | ||
|
|
83e3d4ca1f | ||
|
|
1c9141bd5d | ||
|
|
1b984422db | ||
|
|
b6453e9fac | ||
|
|
7b8e3624b8 | ||
|
|
8a56da84e6 | ||
|
|
14a00490e2 | ||
|
|
29bf967a7e | ||
|
|
eeb79f2587 | ||
|
|
6d1741694d | ||
|
|
746dccf8f9 | ||
|
|
28a897e599 | ||
|
|
21ebf6d777 | ||
|
|
a2dbbb25a1 | ||
|
|
2a64dabe82 | ||
|
|
046ca6eaf1 | ||
|
|
3661afa461 | ||
|
|
12f1985375 | ||
|
|
bb800c9db8 | ||
|
|
262048df95 | ||
|
|
9255eb6902 | ||
|
|
56d386f152 | ||
|
|
193dbe89cd | ||
|
|
601e158ffe | ||
|
|
42b9fa3779 | ||
|
|
4767a67acd | ||
|
|
4c8cc84b64 | ||
|
|
c5efc4b64b | ||
|
|
25e2151fdf | ||
|
|
cb10e261a2 | ||
|
|
bd89c7f269 | ||
|
|
d4ef151cc4 | ||
|
|
669cfa33df | ||
|
|
f70ab2f68a | ||
|
|
900020ddc9 | ||
|
|
ec428135ce | ||
|
|
8e2a1a61a5 | ||
|
|
5fb8f5d3e7 | ||
|
|
c68523150f | ||
|
|
ae2b8f0056 | ||
|
|
ef6d78c580 | ||
|
|
ccd4c9615c | ||
|
|
7d6e60ab7d | ||
|
|
9615e37ef9 | ||
|
|
6be12ba773 | ||
|
|
03855d316b | ||
|
|
9f07109616 | ||
|
|
6a4a3f617f | ||
|
|
f32c9be41f | ||
|
|
d83d829e0a | ||
|
|
99e56ef74b | ||
|
|
df23692802 | ||
|
|
b68a7fe7ae | ||
|
|
5c9ba189bc | ||
|
|
5631a31099 | ||
|
|
f4bc0efc31 | ||
|
|
53f45810ff | ||
|
|
d58151a0eb | ||
|
|
de582d2fc7 | ||
|
|
653ac7ee14 | ||
|
|
0b4769289a | ||
|
|
3bedb223fc | ||
|
|
94a1720e04 | ||
|
|
d0c8808340 | ||
|
|
dd32d81726 | ||
|
|
378a732968 | ||
|
|
b2e82685b4 | ||
|
|
566940e052 | ||
|
|
dab802fbf4 | ||
|
|
7bca95205d | ||
|
|
669e2ed5b0 | ||
|
|
783eb1a6e8 | ||
|
|
9b9599b12f | ||
|
|
6ed0d3def7 | ||
|
|
c42731a55c | ||
|
|
b534df242b | ||
|
|
734730640a | ||
|
|
bee7cfa6aa | ||
|
|
eacf78b83c | ||
|
|
95526a82de | ||
|
|
62420e0f40 | ||
|
|
b36dc22b45 | ||
|
|
f2a55d01ea | ||
|
|
bf029c1b9d | ||
|
|
375ebd39f0 | ||
|
|
a33ebbaf11 | ||
|
|
19b304b0fc | ||
|
|
0b64fe6746 | ||
|
|
e978121d58 |
@@ -2,11 +2,6 @@ version: "{branch}.{build}"
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- TOXENV: py35-no-ext
|
||||
PYTHON: "C:\\Python35-x64"
|
||||
PYTHON_VERSION: "3.5.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
- TOXENV: py36-no-ext
|
||||
PYTHON: "C:\\Python36-x64"
|
||||
PYTHON_VERSION: "3.6.x"
|
||||
|
||||
@@ -5,3 +5,11 @@ omit = site-packages, sanic/utils.py, sanic/__main__.py
|
||||
|
||||
[html]
|
||||
directory = coverage
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
no cov
|
||||
no qa
|
||||
noqa
|
||||
NOQA
|
||||
pragma: no cover
|
||||
|
||||
19
.github/stale.yml
vendored
Normal file
19
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 90
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 30
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- bug
|
||||
- urgent
|
||||
- necessary
|
||||
- help wanted
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. If this
|
||||
is incorrect, please respond with an update. Thank you for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,4 +15,5 @@ docs/_build/
|
||||
docs/_api/
|
||||
build/*
|
||||
.DS_Store
|
||||
dist/*
|
||||
dist/*
|
||||
pip-wheel-metadata/
|
||||
|
||||
12
.travis.yml
12
.travis.yml
@@ -5,10 +5,6 @@ cache:
|
||||
- $HOME/.cache/pip
|
||||
matrix:
|
||||
include:
|
||||
- env: TOX_ENV=py35
|
||||
python: 3.5
|
||||
- env: TOX_ENV=py35-no-ext
|
||||
python: 3.5
|
||||
- env: TOX_ENV=py36
|
||||
python: 3.6
|
||||
- env: TOX_ENV=py36-no-ext
|
||||
@@ -25,6 +21,10 @@ matrix:
|
||||
python: 3.6
|
||||
- env: TOX_ENV=check
|
||||
python: 3.6
|
||||
- env: TOX_ENV=security
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
install:
|
||||
- pip install -U tox
|
||||
- pip install codecov
|
||||
@@ -33,9 +33,9 @@ after_success:
|
||||
- codecov
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: channelcat
|
||||
user: brewmaster
|
||||
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:
|
||||
tags: true
|
||||
distributions: "sdist bdist_wheel"
|
||||
|
||||
153
CHANGELOG.md
153
CHANGELOG.md
@@ -1,3 +1,156 @@
|
||||
Version 19.6
|
||||
------------
|
||||
19.6.0
|
||||
- Changes:
|
||||
- [#1562](https://github.com/huge-success/sanic/pull/1562)
|
||||
Remove `aiohttp` dependencey and create new `SanicTestClient` based upon
|
||||
[`requests-async`](https://github.com/encode/requests-async).
|
||||
|
||||
- [#1475](https://github.com/huge-success/sanic/pull/1475)
|
||||
Added ASGI support (Beta)
|
||||
|
||||
- [#1436](https://github.com/huge-success/sanic/pull/1436)
|
||||
Add Configure support from object string
|
||||
|
||||
- [#1544](https://github.com/huge-success/sanic/pull/1544)
|
||||
Drop dependency on distutil
|
||||
|
||||
- Fixes:
|
||||
- [#1587](https://github.com/huge-success/sanic/pull/1587)
|
||||
Add missing handle for Expect header.
|
||||
|
||||
- [#1560](https://github.com/huge-success/sanic/pull/1560)
|
||||
Allow to disable Transfer-Encoding: chunked.
|
||||
|
||||
- [#1558](https://github.com/huge-success/sanic/pull/1558)
|
||||
Fix graceful shutdown.
|
||||
|
||||
- [#1594](https://github.com/huge-success/sanic/pull/1594)
|
||||
Strict Slashes behavior fix
|
||||
|
||||
- Deprecation:
|
||||
- [#1562](https://github.com/huge-success/sanic/pull/1562)
|
||||
Drop support for Python 3.5
|
||||
|
||||
- [#1568](https://github.com/huge-success/sanic/pull/1568)
|
||||
Deprecate route removal.
|
||||
|
||||
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
|
||||
-------------
|
||||
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:
|
||||
|
||||
```bash
|
||||
pip3 install -e . "[.dev]"
|
||||
pip3 install -e . ".[dev]"
|
||||
```
|
||||
|
||||
# Dependency Changes
|
||||
@@ -30,32 +30,88 @@ the document that explains the way `sanic` manages dependencies inside the `setu
|
||||
| Dependency Type | Usage | Installation |
|
||||
| ------------------------------------------| -------------------------------------------------------------------------- | --------------------------- |
|
||||
| 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]' |
|
||||
| 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]' |
|
||||
| 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['docs'] | Dependencies required to enable building and enhancing sanic documentation | pip3 install -e '.[docs]' |
|
||||
|
||||
## Running tests
|
||||
|
||||
To run the tests for sanic it is recommended to use tox like so:
|
||||
## Running all tests
|
||||
To run the tests for Sanic it is recommended to use tox like so:
|
||||
|
||||
```bash
|
||||
tox
|
||||
```
|
||||
|
||||
See it's that simple!
|
||||
|
||||
`tox.ini` contains different environments. Running `tox` without any arguments will
|
||||
run all unittests, perform lint and other checks.
|
||||
|
||||
## Run unittests :
|
||||
`tox` environment -> `[testenv]`
|
||||
|
||||
To execute only unittests, run `tox` with environment like so:
|
||||
|
||||
```bash
|
||||
tox -e py36 -v -- tests/test_config.py
|
||||
# or
|
||||
tox -e py37 -v -- tests/test_config.py
|
||||
```
|
||||
|
||||
## Run lint checks :
|
||||
`tox` environment -> `[testenv:lint]`
|
||||
|
||||
Permform `flake8`, `black` and `isort` checks.
|
||||
```bash
|
||||
tox -e lint
|
||||
```
|
||||
|
||||
## Run other checks :
|
||||
`tox` environment -> `[testenv:check]`
|
||||
|
||||
Perform other checks.
|
||||
```bash
|
||||
tox -e check
|
||||
```
|
||||
|
||||
# Code Style
|
||||
To maintain the code consistency, Sanic uses following tools.
|
||||
|
||||
1. [isort](https://github.com/timothycrosley/isort)
|
||||
2. [black](https://github.com/python/black)
|
||||
2. [flake8](https://github.com/PyCQA/flake8)
|
||||
|
||||
|
||||
## isort
|
||||
`isort` sorts Python imports. It divides imports into three
|
||||
categories sorted each in alphabetical order.
|
||||
1. built-in
|
||||
2. third-party
|
||||
3. project-specific
|
||||
|
||||
## black
|
||||
`black` is a Python code formatter.
|
||||
|
||||
## flake8
|
||||
`flake8` is a Python style guide that wraps following tools into one.
|
||||
1. PyFlakes
|
||||
2. pycodestyle
|
||||
3. Ned Batchelder's McCabe script
|
||||
|
||||
`isort`, `black` and `flake8` checks are performed during `tox` lint checks.
|
||||
|
||||
Refer [tox](https://tox.readthedocs.io/en/latest/index.html) documentation for more details.
|
||||
|
||||
## Pull requests!
|
||||
|
||||
So the pull request approval rules are pretty simple:
|
||||
1. All pull requests must pass unit tests.
|
||||
2. All pull requests must be reviewed and approved by at least
|
||||
one current collaborator on the project.
|
||||
one current collaborator on the project.
|
||||
3. All pull requests must pass flake8 checks.
|
||||
4. All pull requests must be consistent with the existing code.
|
||||
5. If you decide to remove/change anything from any common interface
|
||||
a deprecation message should accompany it.
|
||||
a deprecation message should accompany it.
|
||||
6. If you implement a new feature you should have at least one unit
|
||||
test to accompany it.
|
||||
test to accompany it.
|
||||
7. An example must be one of the following:
|
||||
* Example of how to use Sanic
|
||||
* Example of how to use Sanic extensions
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
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
|
||||
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
|
||||
else
|
||||
$(info Sorting Imports)
|
||||
isort -rc sanic
|
||||
isort -rc sanic tests
|
||||
endif
|
||||
endif
|
||||
|
||||
black:
|
||||
black --config ./pyproject.toml sanic tests
|
||||
black --config ./.black.toml sanic tests
|
||||
|
||||
fix-import: black
|
||||
isort -rc sanic
|
||||
isort -rc sanic tests
|
||||
|
||||
25
README.rst
25
README.rst
@@ -16,7 +16,9 @@ Sanic | Build fast. Run fast.
|
||||
* - Package
|
||||
- | |PyPI| |PyPI version| |Wheel| |Supported implementations| |Code style black|
|
||||
* - Support
|
||||
- | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby|
|
||||
- | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby| |Awesome|
|
||||
* - Stats
|
||||
- | |Downloads| |Conda downloads|
|
||||
|
||||
.. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg
|
||||
:target: https://community.sanicframework.org/
|
||||
@@ -42,14 +44,23 @@ Sanic | Build fast. Run fast.
|
||||
.. |Supported implementations| image:: https://img.shields.io/pypi/implementation/sanic.svg
|
||||
:alt: Supported implementations
|
||||
:target: https://pypi.python.org/pypi/sanic
|
||||
.. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg
|
||||
:alt: Awesome Sanic List
|
||||
:target: https://github.com/mekicha/awesome-sanic
|
||||
.. |Downloads| image:: https://pepy.tech/badge/sanic/month
|
||||
:alt: Downloads
|
||||
:target: https://pepy.tech/project/sanic
|
||||
.. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg
|
||||
:alt: Downloads
|
||||
:target: https://anaconda.org/conda-forge/sanic
|
||||
|
||||
.. end-badges
|
||||
|
||||
Sanic is a Python web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
|
||||
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/>`_.
|
||||
|
||||
The project is maintained by the community, for the community **Contributions are welcome!**
|
||||
The project is maintained by the community, for the community. **Contributions are welcome!**
|
||||
|
||||
The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.
|
||||
|
||||
@@ -64,7 +75,13 @@ Installation
|
||||
|
||||
$ export SANIC_NO_UVLOOP=true
|
||||
$ export SANIC_NO_UJSON=true
|
||||
$ pip3 install sanic
|
||||
$ pip3 install --no-binary :all: sanic
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to
|
||||
use ``sanic`` with ``ujson`` dependency.
|
||||
|
||||
|
||||
Hello World Example
|
||||
|
||||
25
SECURITY.md
Normal file
25
SECURITY.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release.
|
||||
|
||||
| Version | LTS | Supported |
|
||||
| ------- | ------------------ | ------------------ |
|
||||
| 19.6.0 | | :white_check_mark: |
|
||||
| 19.3.1 | | :heavy_check_mark: |
|
||||
| 18.12.0 | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| 0.8.3 | | :x: |
|
||||
| 0.7.0 | | :x: |
|
||||
| 0.6.0 | | :x: |
|
||||
| 0.5.4 | | :x: |
|
||||
| 0.4.1 | | :x: |
|
||||
| 0.3.1 | | :x: |
|
||||
| 0.2.0 | | :x: |
|
||||
| 0.1.9 | | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button.
|
||||
|
||||
This will help to not publicize the issue until the team can address it and resolve it.
|
||||
@@ -4,7 +4,7 @@ Python 3.7 AsyncIO examples
|
||||
With Python 3.7 AsyncIO got major update for the following types:
|
||||
|
||||
- asyncio.AbstractEventLoop
|
||||
- asyncio.AnstractServer
|
||||
- asyncio.AbstractServer
|
||||
|
||||
|
||||
This example shows how to use sanic with Python 3.7, to be precise: how to retrieve an asyncio server instance:
|
||||
|
||||
@@ -1,6 +1,159 @@
|
||||
Version 18.12
|
||||
-------------
|
||||
18.12.0
|
||||
# Changelog
|
||||
|
||||
## Version 19.6
|
||||
|
||||
- Changes:
|
||||
- [#1562](https://github.com/huge-success/sanic/pull/1562)
|
||||
Remove `aiohttp` dependencey and create new `SanicTestClient` based upon
|
||||
[`requests-async`](https://github.com/encode/requests-async).
|
||||
|
||||
- [#1475](https://github.com/huge-success/sanic/pull/1475)
|
||||
Added ASGI support (Beta)
|
||||
|
||||
- [#1436](https://github.com/huge-success/sanic/pull/1436)
|
||||
Add Configure support from object string
|
||||
|
||||
- [#1544](https://github.com/huge-success/sanic/pull/1544)
|
||||
Drop dependency on distutil
|
||||
|
||||
- Fixes:
|
||||
- [#1587](https://github.com/huge-success/sanic/pull/1587)
|
||||
Add missing handle for Expect header.
|
||||
|
||||
- [#1560](https://github.com/huge-success/sanic/pull/1560)
|
||||
Allow to disable Transfer-Encoding: chunked.
|
||||
|
||||
- [#1558](https://github.com/huge-success/sanic/pull/1558)
|
||||
Fix graceful shutdown.
|
||||
|
||||
- [#1594](https://github.com/huge-success/sanic/pull/1594)
|
||||
Strict Slashes behavior fix
|
||||
|
||||
- Deprecation:
|
||||
- [#1562](https://github.com/huge-success/sanic/pull/1562)
|
||||
Drop support for Python 3.5
|
||||
|
||||
- [#1568](https://github.com/huge-success/sanic/pull/1568)
|
||||
Deprecate route removal.
|
||||
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- Changes:
|
||||
- Improved codebase test coverage from 81% to 91%.
|
||||
- Added stream_large_files and host examples in static_file document
|
||||
@@ -34,8 +187,8 @@ Version 18.12
|
||||
- Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
|
||||
- Fix document for logging
|
||||
|
||||
Version 0.8
|
||||
-----------
|
||||
## Version 0.8
|
||||
|
||||
0.8.3
|
||||
- Changes:
|
||||
- Ownership changed to org 'huge-success'
|
||||
@@ -111,8 +264,8 @@ Version 0.8
|
||||
|
||||
Note: Changelog was unmaintained between 0.1 and 0.7
|
||||
|
||||
Version 0.1
|
||||
-----------
|
||||
## Version 0.1
|
||||
|
||||
- 0.1.7
|
||||
- Reversed static url and directory arguments to meet spec
|
||||
- 0.1.6
|
||||
|
||||
@@ -53,6 +53,13 @@ import myapp.default_settings
|
||||
app = Sanic('myapp')
|
||||
app.config.from_object(myapp.default_settings)
|
||||
```
|
||||
or also by path to config:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
app.config.from_object('config.path.config.Class')
|
||||
```
|
||||
|
||||
|
||||
You could use a class or any other object as well.
|
||||
|
||||
@@ -85,16 +92,19 @@ DB_USER = 'appuser'
|
||||
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------- | --------- | --------------------------------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
|
||||
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
| 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) |
|
||||
| ACCESS_LOG | True | Disable or enable access log |
|
||||
| Variable | Default | Description |
|
||||
| ------------------------- | ----------------- | --------------------------------------------------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_BUFFER_QUEUE_SIZE | 100 | Request streaming buffer queue size |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
|
||||
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
| 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) |
|
||||
| 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:
|
||||
|
||||
@@ -143,3 +153,17 @@ Firefox client hard keepalive limit = 115 seconds
|
||||
Opera 11 client hard keepalive limit = 120 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.
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# Deploying
|
||||
|
||||
Deploying Sanic is made simple by the inbuilt webserver. After defining an
|
||||
instance of `sanic.Sanic`, we can call the `run` method with the following
|
||||
Deploying Sanic is very simple using one of three options: the inbuilt webserver,
|
||||
an [ASGI webserver](https://asgi.readthedocs.io/en/latest/implementations.html), or `gunicorn`.
|
||||
It is also very common to place Sanic behind a reverse proxy, like `nginx`.
|
||||
|
||||
## Running via Sanic webserver
|
||||
|
||||
After defining an instance of `sanic.Sanic`, we can call the `run` method with the following
|
||||
keyword arguments:
|
||||
|
||||
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
|
||||
@@ -17,7 +22,13 @@ keyword arguments:
|
||||
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
|
||||
- `access_log` *(default `True`)*: Enables log on handling requests (significantly slows server).
|
||||
|
||||
## Workers
|
||||
```python
|
||||
app.run(host='0.0.0.0', port=1337, access_log=False)
|
||||
```
|
||||
|
||||
In the above example, we decided to turn off the access log in order to increase performance.
|
||||
|
||||
### Workers
|
||||
|
||||
By default, Sanic listens in the main process using only one CPU core. To crank
|
||||
up the juice, just specify the number of workers in the `run` arguments.
|
||||
@@ -29,9 +40,9 @@ app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
Sanic will automatically spin up multiple processes and route traffic between
|
||||
them. We recommend as many workers as you have available cores.
|
||||
|
||||
## Running via command
|
||||
### Running via command
|
||||
|
||||
If you like using command line arguments, you can launch a Sanic server by
|
||||
If you like using command line arguments, you can launch a Sanic webserver by
|
||||
executing the module. For example, if you initialized Sanic as `app` in a file
|
||||
named `server.py`, you could run the server like so:
|
||||
|
||||
@@ -46,6 +57,33 @@ if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
|
||||
## Running via ASGI
|
||||
|
||||
Sanic is also ASGI-compliant. This means you can use your preferred ASGI webserver
|
||||
to run Sanic. The three main implementations of ASGI are
|
||||
[Daphne](http://github.com/django/daphne), [Uvicorn](https://www.uvicorn.org/),
|
||||
and [Hypercorn](https://pgjones.gitlab.io/hypercorn/index.html).
|
||||
|
||||
Follow their documentation for the proper way to run them, but it should look
|
||||
something like:
|
||||
|
||||
```
|
||||
daphne myapp:app
|
||||
uvicorn myapp:app
|
||||
hypercorn myapp:app
|
||||
```
|
||||
|
||||
A couple things to note when using ASGI:
|
||||
|
||||
1. When using the Sanic webserver, websockets will run using the [`websockets`](https://websockets.readthedocs.io/) package. In ASGI mode, there is no need for this package since websockets are managed in the ASGI server.
|
||||
1. The ASGI [lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) supports
|
||||
only two server events: startup and shutdown. Sanic has four: before startup, after startup,
|
||||
before shutdown, and after shutdown. Therefore, in ASGI mode, the startup and shutdown events will
|
||||
run consecutively and not actually around the server process beginning and ending (since that
|
||||
is now controlled by the ASGI server). Therefore, it is best to use `after_server_start` and
|
||||
`before_server_stop`.
|
||||
1. ASGI mode is still in "beta" as of Sanic v19.6.
|
||||
|
||||
## Running via Gunicorn
|
||||
|
||||
[Gunicorn](http://gunicorn.org/) ‘Green Unicorn’ is a WSGI HTTP Server for UNIX.
|
||||
@@ -64,7 +102,29 @@ of the memory leak.
|
||||
|
||||
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
|
||||
|
||||
## Disable debug logging
|
||||
## Other deployment considerations
|
||||
|
||||
### 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 for performance
|
||||
|
||||
To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
|
||||
|
||||
@@ -84,16 +144,17 @@ Or you can rewrite app config directly
|
||||
app.config.ACCESS_LOG = False
|
||||
```
|
||||
|
||||
## Asynchronous support
|
||||
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
|
||||
However be advised that this method does not support using multiple processes, and is not the preferred way
|
||||
### Asynchronous support and sharing the loop
|
||||
|
||||
This is suitable if you *need* to share the Sanic process with other applications, in particular the `loop`.
|
||||
However, be advised that this method does not support using multiple processes, and is not the preferred way
|
||||
to run the app in general.
|
||||
|
||||
Here is an incomplete example (please see `run_async.py` in examples for something more practical):
|
||||
|
||||
```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()
|
||||
task = asyncio.ensure_future(server)
|
||||
loop.run_forever()
|
||||
```
|
||||
```
|
||||
@@ -160,7 +160,7 @@ execution support provided by the ``pytest-xdist`` plugin.
|
||||
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
|
||||
|
||||
|
||||
@@ -1,75 +1 @@
|
||||
# Extensions
|
||||
|
||||
A list of Sanic extensions created by the community.
|
||||
|
||||
|
||||
## Extension and Plugin Development
|
||||
|
||||
- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins.
|
||||
- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application.
|
||||
|
||||
## Security
|
||||
|
||||
- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic.
|
||||
- [Secure](https://github.com/cakinney/secure): Secure 🔒 is a lightweight package that adds optional security headers and cookie attributes for Python web frameworks.
|
||||
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. Allows using redis, memcache or an in memory store.
|
||||
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
||||
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for
|
||||
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
|
||||
- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic.
|
||||
- [sanic-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support.
|
||||
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
|
||||
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||
- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation.
|
||||
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
|
||||
|
||||
## ORM and Database Integration
|
||||
|
||||
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
|
||||
- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models.
|
||||
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
|
||||
- [GINO](https://github.com/fantix/gino): An asyncio ORM on top of SQLAlchemy core, delivered with a Sanic extension. ([Documentation](https://python-gino.readthedocs.io/))
|
||||
- [Databases](https://github.com/encode/databases): Async database access for SQLAlchemy core, with support for PostgreSQL, MySQL, and SQLite.
|
||||
|
||||
## Unit Testing
|
||||
|
||||
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
|
||||
|
||||
## Project Creation Template
|
||||
|
||||
- [cookiecutter-sanic](https://github.com/harshanarayana/cookiecutter-sanic): Get your sanic application up and running in a matter of second in a well defined project structure.
|
||||
Batteries included for deployment, unit testing, automated release management and changelog generation.
|
||||
|
||||
## Templating
|
||||
|
||||
- [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/))
|
||||
|
||||
## API Helper Utilities
|
||||
|
||||
- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.
|
||||
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
|
||||
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
|
||||
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
|
||||
|
||||
## i18n/l10n Support
|
||||
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the `Babel` library
|
||||
|
||||
## Custom Middlewares
|
||||
|
||||
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
|
||||
|
||||
## Monitoring and Reporting
|
||||
|
||||
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
|
||||
- [sanic-zipkin](https://github.com/kevinqqnj/sanic-zipkin): Easily report request/function/RPC traces to zipkin/jaeger, through aiozipkin.
|
||||
|
||||
|
||||
## Sample Applications
|
||||
|
||||
- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.
|
||||
Moved to the [`awesome-sanic`](https://github.com/mekicha/awesome-sanic) list.
|
||||
@@ -1,14 +1,16 @@
|
||||
# Getting Started
|
||||
|
||||
Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
|
||||
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
|
||||
least version 3.6 of Python before starting. Sanic uses the new `async`/`await`
|
||||
syntax, so earlier versions of python won't work.
|
||||
|
||||
## 1. Install Sanic
|
||||
|
||||
```
|
||||
pip3 install sanic
|
||||
```
|
||||
> If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to use ``sanic`` with ``ujson`` dependency.
|
||||
|
||||
```bash
|
||||
pip3 install sanic
|
||||
```
|
||||
|
||||
To install sanic without `uvloop` or `ujson` using bash, you can provide either or both of these environmental variables
|
||||
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the `SANIC_NO_X` (`X` = `UVLOOP`/`UJSON`)
|
||||
@@ -18,6 +20,13 @@ to true will stop that features installation.
|
||||
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic
|
||||
```
|
||||
|
||||
You can also install Sanic from [`conda-forge`](https://anaconda.org/conda-forge/sanic)
|
||||
|
||||
```bash
|
||||
conda config --add channels conda-forge
|
||||
conda install sanic
|
||||
```
|
||||
|
||||
## 2. Create a file called `main.py`
|
||||
|
||||
```python
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
Sanic
|
||||
=================================
|
||||
|
||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
|
||||
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.
|
||||
|
||||
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
|
||||
The goal of the project is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
|
||||
@@ -22,4 +22,8 @@ Sanic aspires to be simple
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
.. note::
|
||||
|
||||
Sanic does not support Python 3.5 from version 19.6 and forward. However, version 18.12LTS is supported thru December 2020. Official Python support for version 3.5 is set to expire in September 2020.
|
||||
@@ -145,13 +145,17 @@ The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
```
|
||||
- `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar`
|
||||
- `scheme`: The URL scheme associated with the request: `http` or `https`
|
||||
- `host`: The host associated with the request: `localhost:8080`
|
||||
- `scheme`: The URL scheme associated with the request: 'http|https|ws|wss' or arbitrary value given by the headers.
|
||||
- `host`: The host associated with the request(which in the `Host` header): `localhost:8080`
|
||||
- `server_name`: The hostname of the server, without port number. the value is seeked in this order: `config.SERVER_NAME`, `x-forwarded-host` header, :func:`Request.host`
|
||||
- `server_port`: Like `server_name`. Seeked in this order: `x-forwarded-port` header, :func:`Request.host`, actual port used by the transport layer socket.
|
||||
- `path`: The path of the request: `/posts/1/`
|
||||
- `query_string`: The query string of the request: `foo=bar` or a blank string `''`
|
||||
- `uri_template`: Template for matching route handler: `/posts/<id>/`
|
||||
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
||||
|
||||
- `url_for`: Just like `sanic.Sanic.url_for`, but automatically determine `scheme` and `netloc` base on the request. Since this method is aiming to generate correct schema & netloc, `_external` is implied.
|
||||
|
||||
|
||||
## Changing the default parsing rules of the queryset
|
||||
|
||||
@@ -193,7 +197,7 @@ 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"]},
|
||||
"args_with_blank_values": {"test1": ["value1"], "test2": "", "test3": ["value3"]},
|
||||
"query_string": "test1=value1&test2=&test3=value3"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -55,11 +55,13 @@ from sanic import response
|
||||
@app.route("/streaming")
|
||||
async def index(request):
|
||||
async def streaming_fn(response):
|
||||
response.write('foo')
|
||||
response.write('bar')
|
||||
await response.write('foo')
|
||||
await response.write('bar')
|
||||
return response.stream(streaming_fn, content_type='text/plain')
|
||||
```
|
||||
|
||||
See [Streaming](streaming.md) for more information.
|
||||
|
||||
## File Streaming
|
||||
For large files, a combination of File and Streaming above
|
||||
```python
|
||||
|
||||
@@ -41,27 +41,75 @@ inside the quotes. If the parameter does not match the specified type, Sanic
|
||||
will throw a `NotFound` exception, resulting in a `404: Page not found` error
|
||||
on the URL.
|
||||
|
||||
### Supported types
|
||||
|
||||
* `string`
|
||||
* "Bob"
|
||||
* "Python 3"
|
||||
* `int`
|
||||
* 10
|
||||
* 20
|
||||
* 30
|
||||
* -10
|
||||
* (No floats work here)
|
||||
* `number`
|
||||
* 1
|
||||
* 1.5
|
||||
* 10
|
||||
* -10
|
||||
* `alpha`
|
||||
* "Bob"
|
||||
* "Python"
|
||||
* (If it contains a symbol or a non alphanumeric character it will fail)
|
||||
* `path`
|
||||
* "hello"
|
||||
* "hello.text"
|
||||
* "hello world"
|
||||
* `uuid`
|
||||
* 123a123a-a12a-1a1a-a1a1-1a12a1a12345 (UUIDv4 Support)
|
||||
* `regex expression`
|
||||
|
||||
If no type is set then a string is expected. The argument given to the function will always be a string, independent of the type.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/number/<integer_arg:int>')
|
||||
@app.route('/string/<string_arg:string>')
|
||||
async def string_handler(request, string_arg):
|
||||
return text('String - {}'.format(string_arg))
|
||||
|
||||
@app.route('/int/<integer_arg:int>')
|
||||
async def integer_handler(request, integer_arg):
|
||||
return text('Integer - {}'.format(integer_arg))
|
||||
return text('Integer - {}'.format(integer_arg))
|
||||
|
||||
@app.route('/number/<number_arg:number>')
|
||||
async def number_handler(request, number_arg):
|
||||
return text('Number - {}'.format(number_arg))
|
||||
return text('Number - {}'.format(number_arg))
|
||||
|
||||
@app.route('/alpha/<alpha_arg:alpha>')
|
||||
async def number_handler(request, alpha_arg):
|
||||
return text('Alpha - {}'.format(alpha_arg))
|
||||
|
||||
@app.route('/path/<path_arg:path>')
|
||||
async def number_handler(request, path_arg):
|
||||
return text('Path - {}'.format(path_arg))
|
||||
|
||||
@app.route('/uuid/<uuid_arg:uuid>')
|
||||
async def number_handler(request, uuid_arg):
|
||||
return text('Uuid - {}'.format(uuid_arg))
|
||||
|
||||
@app.route('/person/<name:[A-z]+>')
|
||||
async def person_handler(request, name):
|
||||
return text('Person - {}'.format(name))
|
||||
return text('Person - {}'.format(name))
|
||||
|
||||
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
|
||||
async def folder_handler(request, folder_id):
|
||||
return text('Folder - {}'.format(folder_id))
|
||||
return text('Folder - {}'.format(folder_id))
|
||||
|
||||
```
|
||||
|
||||
**Warning** `str` is not a valid type tag. If you want `str` recognition then you must use `string`
|
||||
|
||||
## HTTP request types
|
||||
|
||||
By default, a route defined on a URL will be available for only GET requests to that URL.
|
||||
@@ -241,6 +289,45 @@ def handler(request):
|
||||
app.blueprint(bp)
|
||||
```
|
||||
|
||||
The behavior of how the `strict_slashes` flag follows a defined hierarchy which decides if a specific route
|
||||
falls under the `strict_slashes` behavior.
|
||||
|
||||
```bash
|
||||
|___ Route
|
||||
|___ Blueprint
|
||||
|___ Application
|
||||
```
|
||||
|
||||
Above hierarchy defines how the `strict_slashes` flag will behave. The first non `None` value of the `strict_slashes`
|
||||
found in the above order will be applied to the route in question.
|
||||
|
||||
```python
|
||||
from sanic import Sanic, Blueprint
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic("sample_strict_slashes", strict_slashes=True)
|
||||
|
||||
@app.get("/r1")
|
||||
def r1(request):
|
||||
return text("strict_slashes is applicable from App level")
|
||||
|
||||
@app.get("/r2", strict_slashes=False)
|
||||
def r2(request):
|
||||
return text("strict_slashes is not applicable due to False value set in route level")
|
||||
|
||||
bp = Blueprint("bp", strict_slashes=False)
|
||||
|
||||
@bp.get("/r3", strict_slashes=True)
|
||||
def r3(request):
|
||||
return text("strict_slashes applicable from blueprint route level")
|
||||
|
||||
bp1 = Blueprint("bp1", strict_slashes=True)
|
||||
|
||||
@bp.get("/r4")
|
||||
def r3(request):
|
||||
return text("strict_slashes applicable from blueprint level")
|
||||
```
|
||||
|
||||
## User defined route name
|
||||
|
||||
A custom route name can be used by passing a `name` argument while registering the route which will
|
||||
|
||||
@@ -117,3 +117,27 @@ async def index(request):
|
||||
|
||||
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
|
||||
|
||||
Sanic endpoints can be tested locally using the `test_client` object, which
|
||||
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
|
||||
library.
|
||||
depends on the additional [`requests-async`](https://github.com/encode/requests-async)
|
||||
library, which implements an API that mirrors the `requests` library.
|
||||
|
||||
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:
|
||||
@@ -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
|
||||
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:
|
||||
|
||||
@@ -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.
|
||||
- `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:
|
||||
|
||||
@@ -55,8 +55,8 @@ def test_post_json_request_includes_data():
|
||||
|
||||
|
||||
More information about
|
||||
the available arguments to aiohttp can be found
|
||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||
the available arguments to `requests-async` can be found
|
||||
[in the documentation for `requests`](https://2.python-requests.org/en/master/).
|
||||
|
||||
|
||||
## Using a random port
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
WebSocket
|
||||
=========
|
||||
|
||||
Sanic provides an easy to user abstraction on top of `websockets`. To setup a WebSocket:
|
||||
Sanic provides an easy to use abstraction on top of `websockets`. To setup a WebSocket:
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ dependencies:
|
||||
- sphinx==1.8.3
|
||||
- sphinx_rtd_theme==0.4.2
|
||||
- recommonmark==0.5.0
|
||||
- requests-async==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__':
|
||||
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.set_task_factory(context.task_factory)
|
||||
task = asyncio.ensure_future(server)
|
||||
|
||||
88
examples/run_asgi.py
Normal file
88
examples/run_asgi.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
1. Create a simple Sanic app
|
||||
0. Run with an ASGI server:
|
||||
$ uvicorn run_asgi:app
|
||||
or
|
||||
$ hypercorn run_asgi:app
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from sanic import Sanic, response
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route("/text")
|
||||
def handler_text(request):
|
||||
return response.text("Hello")
|
||||
|
||||
|
||||
@app.route("/json")
|
||||
def handler_json(request):
|
||||
return response.json({"foo": "bar"})
|
||||
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def handler_ws(request, ws):
|
||||
name = "<someone>"
|
||||
while True:
|
||||
data = f"Hello {name}"
|
||||
await ws.send(data)
|
||||
name = await ws.recv()
|
||||
|
||||
if not name:
|
||||
break
|
||||
|
||||
|
||||
@app.route("/file")
|
||||
async def handler_file(request):
|
||||
return await response.file(Path("../") / "setup.py")
|
||||
|
||||
|
||||
@app.route("/file_stream")
|
||||
async def handler_file_stream(request):
|
||||
return await response.file_stream(
|
||||
Path("../") / "setup.py", chunk_size=1024
|
||||
)
|
||||
|
||||
|
||||
@app.route("/stream", stream=True)
|
||||
async def handler_stream(request):
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
body = body.decode("utf-8").replace("1", "A")
|
||||
# await response.write(body)
|
||||
return response.stream(body)
|
||||
|
||||
|
||||
@app.listener("before_server_start")
|
||||
async def listener_before_server_start(*args, **kwargs):
|
||||
print("before_server_start")
|
||||
|
||||
|
||||
@app.listener("after_server_start")
|
||||
async def listener_after_server_start(*args, **kwargs):
|
||||
print("after_server_start")
|
||||
|
||||
|
||||
@app.listener("before_server_stop")
|
||||
async def listener_before_server_stop(*args, **kwargs):
|
||||
print("before_server_stop")
|
||||
|
||||
|
||||
@app.listener("after_server_stop")
|
||||
async def listener_after_server_stop(*args, **kwargs):
|
||||
print("after_server_stop")
|
||||
|
||||
|
||||
@app.middleware("request")
|
||||
async def print_on_request(request):
|
||||
print("print_on_request")
|
||||
|
||||
|
||||
@app.middleware("response")
|
||||
async def print_on_response(request, response):
|
||||
print("print_on_response")
|
||||
@@ -12,7 +12,7 @@ async def test(request):
|
||||
return response.json({"answer": "42"})
|
||||
|
||||
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()
|
||||
task = asyncio.ensure_future(server)
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
|
||||
@@ -2,6 +2,6 @@ from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
|
||||
__version__ = "18.12.0"
|
||||
__version__ = "19.6.2"
|
||||
|
||||
__all__ = ["Sanic", "Blueprint"]
|
||||
|
||||
114
sanic/app.py
114
sanic/app.py
@@ -15,6 +15,7 @@ from typing import Any, Optional, Type, Union
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
|
||||
from sanic import reloader_helpers
|
||||
from sanic.asgi import ASGIApp
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.config import BASE_LOGO, Config
|
||||
from sanic.constants import HTTP_METHODS
|
||||
@@ -25,7 +26,7 @@ from sanic.response import HTTPResponse, StreamingHTTPResponse
|
||||
from sanic.router import Router
|
||||
from sanic.server import HttpProtocol, Signal, serve, serve_multiple
|
||||
from sanic.static import register as static_register
|
||||
from sanic.testing import SanicTestClient
|
||||
from sanic.testing import SanicASGITestClient, SanicTestClient
|
||||
from sanic.views import CompositionView
|
||||
from sanic.websocket import ConnectionClosed, WebSocketProtocol
|
||||
|
||||
@@ -53,6 +54,7 @@ class Sanic:
|
||||
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
||||
|
||||
self.name = name
|
||||
self.asgi = False
|
||||
self.router = router or Router()
|
||||
self.request_class = request_class
|
||||
self.error_handler = error_handler or ErrorHandler()
|
||||
@@ -80,7 +82,7 @@ class Sanic:
|
||||
|
||||
Only supported when using the `app.run` method.
|
||||
"""
|
||||
if not self.is_running:
|
||||
if not self.is_running and self.asgi is False:
|
||||
raise SanicException(
|
||||
"Loop can only be retrieved after the app has started "
|
||||
"running. Not supported with `create_server` function"
|
||||
@@ -180,27 +182,28 @@ class Sanic:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def response(handler):
|
||||
args = [key for key in signature(handler).parameters.keys()]
|
||||
if args:
|
||||
if stream:
|
||||
handler.is_stream = stream
|
||||
args = list(signature(handler).parameters.keys())
|
||||
|
||||
self.router.add(
|
||||
uri=uri,
|
||||
methods=methods,
|
||||
handler=handler,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
return handler
|
||||
else:
|
||||
if not args:
|
||||
raise ValueError(
|
||||
"Required parameter `request` missing "
|
||||
"in the {0}() route?".format(handler.__name__)
|
||||
)
|
||||
|
||||
if stream:
|
||||
handler.is_stream = stream
|
||||
|
||||
self.router.add(
|
||||
uri=uri,
|
||||
methods=methods,
|
||||
handler=handler,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# Shorthand method decorators
|
||||
@@ -332,7 +335,7 @@ class Sanic:
|
||||
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 host: Host IP or FQDN for the service to use
|
||||
@@ -440,8 +443,12 @@ class Sanic:
|
||||
):
|
||||
"""Decorate a function to be registered as a websocket route
|
||||
:param uri: path of the URL
|
||||
:param host: Host IP or FQDN details
|
||||
:param strict_slashes: If the API endpoint needs to terminate
|
||||
with a "/" or not
|
||||
:param subprotocols: optional list of str with supported subprotocols
|
||||
:param host:
|
||||
:param name: A unique name assigned to the URL so that it can
|
||||
be used with :func:`url_for`
|
||||
:return: decorated function
|
||||
"""
|
||||
self.enable_websocket()
|
||||
@@ -464,13 +471,23 @@ class Sanic:
|
||||
getattr(handler, "__blueprintname__", "")
|
||||
+ handler.__name__
|
||||
)
|
||||
try:
|
||||
protocol = request.transport.get_protocol()
|
||||
except AttributeError:
|
||||
# On Python3.5 the Transport classes in asyncio do not
|
||||
# have a get_protocol() method as in uvloop
|
||||
protocol = request.transport._protocol
|
||||
ws = await protocol.websocket_handshake(request, subprotocols)
|
||||
|
||||
pass
|
||||
|
||||
if self.asgi:
|
||||
ws = request.transport.get_websocket_connection()
|
||||
else:
|
||||
try:
|
||||
protocol = request.transport.get_protocol()
|
||||
except AttributeError:
|
||||
# On Python3.5 the Transport classes in asyncio do not
|
||||
# have a get_protocol() method as in uvloop
|
||||
protocol = request.transport._protocol
|
||||
protocol.app = self
|
||||
|
||||
ws = await protocol.websocket_handshake(
|
||||
request, subprotocols
|
||||
)
|
||||
|
||||
# schedule the application handler
|
||||
# its future is kept in self.websocket_tasks in case it
|
||||
@@ -554,12 +571,22 @@ class Sanic:
|
||||
This method provides the app user a mechanism by which an already
|
||||
existing route can be removed from the :class:`Sanic` object
|
||||
|
||||
.. warning::
|
||||
remove_route is deprecated in v19.06 and will be removed
|
||||
from future versions.
|
||||
|
||||
:param uri: URL Path to be removed from the app
|
||||
:param clean_cache: Instruct sanic if it needs to clean up the LRU
|
||||
route cache
|
||||
:param host: IP address or FQDN specific to the host
|
||||
:return: None
|
||||
"""
|
||||
warnings.warn(
|
||||
"remove_route is deprecated and will be removed "
|
||||
"from future versions.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.router.remove(uri, clean_cache, host)
|
||||
|
||||
# Decorator
|
||||
@@ -880,8 +907,6 @@ class Sanic:
|
||||
# -------------------------------------------- #
|
||||
# Request Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
request.app = self
|
||||
response = await self._run_request_middleware(request)
|
||||
# No middleware results
|
||||
if not response:
|
||||
@@ -970,8 +995,16 @@ class Sanic:
|
||||
raise CancelledError()
|
||||
|
||||
# pass the response to the correct callback
|
||||
if isinstance(response, StreamingHTTPResponse):
|
||||
await stream_callback(response)
|
||||
if write_callback is None or isinstance(
|
||||
response, StreamingHTTPResponse
|
||||
):
|
||||
if stream_callback:
|
||||
await stream_callback(response)
|
||||
else:
|
||||
# Should only end here IF it is an ASGI websocket.
|
||||
# TODO:
|
||||
# - Add exception handling
|
||||
pass
|
||||
else:
|
||||
write_callback(response)
|
||||
|
||||
@@ -983,6 +1016,10 @@ class Sanic:
|
||||
def test_client(self):
|
||||
return SanicTestClient(self)
|
||||
|
||||
@property
|
||||
def asgi_client(self):
|
||||
return SanicASGITestClient(self)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Execution
|
||||
# -------------------------------------------------------------------- #
|
||||
@@ -1109,10 +1146,6 @@ class Sanic:
|
||||
"""This kills the Sanic"""
|
||||
get_event_loop().stop()
|
||||
|
||||
def __call__(self):
|
||||
"""gunicorn compatibility"""
|
||||
return self
|
||||
|
||||
async def create_server(
|
||||
self,
|
||||
host: Optional[str] = None,
|
||||
@@ -1287,6 +1320,7 @@ class Sanic:
|
||||
"port": port,
|
||||
"sock": sock,
|
||||
"ssl": ssl,
|
||||
"app": self,
|
||||
"signal": Signal(),
|
||||
"debug": debug,
|
||||
"request_handler": self.handle_request,
|
||||
@@ -1353,3 +1387,15 @@ class Sanic:
|
||||
def _build_endpoint_name(self, *parts):
|
||||
parts = [self.name, *parts]
|
||||
return ".".join(parts)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# ASGI
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
"""To be ASGI compliant, our instance must be a callable that accepts
|
||||
three arguments: scope, receive, send. See the ASGI reference for more
|
||||
details: https://asgi.readthedocs.io/en/latest/"""
|
||||
self.asgi = True
|
||||
asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||
await asgi_app()
|
||||
|
||||
349
sanic/asgi.py
Normal file
349
sanic/asgi.py
Normal file
@@ -0,0 +1,349 @@
|
||||
import asyncio
|
||||
import warnings
|
||||
|
||||
from http.cookies import SimpleCookie
|
||||
from inspect import isawaitable
|
||||
from typing import Any, Awaitable, Callable, MutableMapping, Union
|
||||
from urllib.parse import quote
|
||||
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from sanic.exceptions import InvalidUsage, ServerError
|
||||
from sanic.log import logger
|
||||
from sanic.request import Request
|
||||
from sanic.response import HTTPResponse, StreamingHTTPResponse
|
||||
from sanic.server import StreamBuffer
|
||||
from sanic.websocket import WebSocketConnection
|
||||
|
||||
|
||||
ASGIScope = MutableMapping[str, Any]
|
||||
ASGIMessage = MutableMapping[str, Any]
|
||||
ASGISend = Callable[[ASGIMessage], Awaitable[None]]
|
||||
ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
|
||||
|
||||
|
||||
class MockProtocol:
|
||||
def __init__(self, transport: "MockTransport", loop):
|
||||
self.transport = transport
|
||||
self._not_paused = asyncio.Event(loop=loop)
|
||||
self._not_paused.set()
|
||||
self._complete = asyncio.Event(loop=loop)
|
||||
|
||||
def pause_writing(self) -> None:
|
||||
self._not_paused.clear()
|
||||
|
||||
def resume_writing(self) -> None:
|
||||
self._not_paused.set()
|
||||
|
||||
async def complete(self) -> None:
|
||||
self._not_paused.set()
|
||||
await self.transport.send(
|
||||
{"type": "http.response.body", "body": b"", "more_body": False}
|
||||
)
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
return self._complete.is_set()
|
||||
|
||||
async def push_data(self, data: bytes) -> None:
|
||||
if not self.is_complete:
|
||||
await self.transport.send(
|
||||
{"type": "http.response.body", "body": data, "more_body": True}
|
||||
)
|
||||
|
||||
async def drain(self) -> None:
|
||||
await self._not_paused.wait()
|
||||
|
||||
|
||||
class MockTransport:
|
||||
def __init__(
|
||||
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||
) -> None:
|
||||
self.scope = scope
|
||||
self._receive = receive
|
||||
self._send = send
|
||||
self._protocol = None
|
||||
self.loop = None
|
||||
|
||||
def get_protocol(self) -> MockProtocol:
|
||||
if not self._protocol:
|
||||
self._protocol = MockProtocol(self, self.loop)
|
||||
return self._protocol
|
||||
|
||||
def get_extra_info(self, info: str) -> Union[str, bool]:
|
||||
if info == "peername":
|
||||
return self.scope.get("server")
|
||||
elif info == "sslcontext":
|
||||
return self.scope.get("scheme") in ["https", "wss"]
|
||||
|
||||
def get_websocket_connection(self) -> WebSocketConnection:
|
||||
try:
|
||||
return self._websocket_connection
|
||||
except AttributeError:
|
||||
raise InvalidUsage("Improper websocket connection.")
|
||||
|
||||
def create_websocket_connection(
|
||||
self, send: ASGISend, receive: ASGIReceive
|
||||
) -> WebSocketConnection:
|
||||
self._websocket_connection = WebSocketConnection(send, receive)
|
||||
return self._websocket_connection
|
||||
|
||||
def add_task(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def send(self, data) -> None:
|
||||
# TODO:
|
||||
# - Validation on data and that it is formatted properly and is valid
|
||||
await self._send(data)
|
||||
|
||||
async def receive(self) -> ASGIMessage:
|
||||
return await self._receive()
|
||||
|
||||
|
||||
class Lifespan:
|
||||
def __init__(self, asgi_app: "ASGIApp") -> None:
|
||||
self.asgi_app = asgi_app
|
||||
|
||||
if "before_server_start" in self.asgi_app.sanic_app.listeners:
|
||||
warnings.warn(
|
||||
'You have set a listener for "before_server_start" '
|
||||
"in ASGI mode. "
|
||||
"It will be executed as early as possible, but not before "
|
||||
"the ASGI server is started."
|
||||
)
|
||||
if "after_server_stop" in self.asgi_app.sanic_app.listeners:
|
||||
warnings.warn(
|
||||
'You have set a listener for "after_server_stop" '
|
||||
"in ASGI mode. "
|
||||
"It will be executed as late as possible, but not after "
|
||||
"the ASGI server is stopped."
|
||||
)
|
||||
|
||||
async def startup(self) -> None:
|
||||
"""
|
||||
Gather the listeners to fire on server start.
|
||||
Because we are using a third-party server and not Sanic server, we do
|
||||
not have access to fire anything BEFORE the server starts.
|
||||
Therefore, we fire before_server_start and after_server_start
|
||||
in sequence since the ASGI lifespan protocol only supports a single
|
||||
startup event.
|
||||
"""
|
||||
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||
"before_server_start", []
|
||||
) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])
|
||||
|
||||
for handler in listeners:
|
||||
response = handler(
|
||||
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
|
||||
)
|
||||
if isawaitable(response):
|
||||
await response
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
Gather the listeners to fire on server stop.
|
||||
Because we are using a third-party server and not Sanic server, we do
|
||||
not have access to fire anything AFTER the server stops.
|
||||
Therefore, we fire before_server_stop and after_server_stop
|
||||
in sequence since the ASGI lifespan protocol only supports a single
|
||||
shutdown event.
|
||||
"""
|
||||
listeners = self.asgi_app.sanic_app.listeners.get(
|
||||
"before_server_stop", []
|
||||
) + self.asgi_app.sanic_app.listeners.get("after_server_stop", [])
|
||||
|
||||
for handler in listeners:
|
||||
response = handler(
|
||||
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
|
||||
)
|
||||
if isawaitable(response):
|
||||
await response
|
||||
|
||||
async def __call__(
|
||||
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||
) -> None:
|
||||
message = await receive()
|
||||
if message["type"] == "lifespan.startup":
|
||||
await self.startup()
|
||||
await send({"type": "lifespan.startup.complete"})
|
||||
|
||||
message = await receive()
|
||||
if message["type"] == "lifespan.shutdown":
|
||||
await self.shutdown()
|
||||
await send({"type": "lifespan.shutdown.complete"})
|
||||
|
||||
|
||||
class ASGIApp:
|
||||
def __init__(self) -> None:
|
||||
self.ws = None
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
cls, sanic_app, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||
) -> "ASGIApp":
|
||||
instance = cls()
|
||||
instance.sanic_app = sanic_app
|
||||
instance.transport = MockTransport(scope, receive, send)
|
||||
instance.transport.add_task = sanic_app.loop.create_task
|
||||
instance.transport.loop = sanic_app.loop
|
||||
|
||||
headers = CIMultiDict(
|
||||
[
|
||||
(key.decode("latin-1"), value.decode("latin-1"))
|
||||
for key, value in scope.get("headers", [])
|
||||
]
|
||||
)
|
||||
instance.do_stream = (
|
||||
True if headers.get("expect") == "100-continue" else False
|
||||
)
|
||||
instance.lifespan = Lifespan(instance)
|
||||
|
||||
if scope["type"] == "lifespan":
|
||||
await instance.lifespan(scope, receive, send)
|
||||
else:
|
||||
url_bytes = scope.get("root_path", "") + quote(scope["path"])
|
||||
url_bytes = url_bytes.encode("latin-1")
|
||||
url_bytes += b"?" + scope["query_string"]
|
||||
|
||||
if scope["type"] == "http":
|
||||
version = scope["http_version"]
|
||||
method = scope["method"]
|
||||
elif scope["type"] == "websocket":
|
||||
version = "1.1"
|
||||
method = "GET"
|
||||
|
||||
instance.ws = instance.transport.create_websocket_connection(
|
||||
send, receive
|
||||
)
|
||||
await instance.ws.accept()
|
||||
else:
|
||||
pass
|
||||
# TODO:
|
||||
# - close connection
|
||||
|
||||
request_class = sanic_app.request_class or Request
|
||||
instance.request = request_class(
|
||||
url_bytes,
|
||||
headers,
|
||||
version,
|
||||
method,
|
||||
instance.transport,
|
||||
sanic_app,
|
||||
)
|
||||
|
||||
if sanic_app.is_request_stream:
|
||||
is_stream_handler = sanic_app.router.is_stream_handler(
|
||||
instance.request
|
||||
)
|
||||
if is_stream_handler:
|
||||
instance.request.stream = StreamBuffer(
|
||||
sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE
|
||||
)
|
||||
instance.do_stream = True
|
||||
|
||||
return instance
|
||||
|
||||
async def read_body(self) -> bytes:
|
||||
"""
|
||||
Read and return the entire body from an incoming ASGI message.
|
||||
"""
|
||||
body = b""
|
||||
more_body = True
|
||||
while more_body:
|
||||
message = await self.transport.receive()
|
||||
body += message.get("body", b"")
|
||||
more_body = message.get("more_body", False)
|
||||
|
||||
return body
|
||||
|
||||
async def stream_body(self) -> None:
|
||||
"""
|
||||
Read and stream the body in chunks from an incoming ASGI message.
|
||||
"""
|
||||
more_body = True
|
||||
|
||||
while more_body:
|
||||
message = await self.transport.receive()
|
||||
chunk = message.get("body", b"")
|
||||
await self.request.stream.put(chunk)
|
||||
|
||||
more_body = message.get("more_body", False)
|
||||
|
||||
await self.request.stream.put(None)
|
||||
|
||||
async def __call__(self) -> None:
|
||||
"""
|
||||
Handle the incoming request.
|
||||
"""
|
||||
if not self.do_stream:
|
||||
self.request.body = await self.read_body()
|
||||
else:
|
||||
self.sanic_app.loop.create_task(self.stream_body())
|
||||
|
||||
handler = self.sanic_app.handle_request
|
||||
callback = None if self.ws else self.stream_callback
|
||||
await handler(self.request, None, callback)
|
||||
|
||||
async def stream_callback(self, response: HTTPResponse) -> None:
|
||||
"""
|
||||
Write the response.
|
||||
"""
|
||||
|
||||
try:
|
||||
headers = [
|
||||
(str(name).encode("latin-1"), str(value).encode("latin-1"))
|
||||
for name, value in response.headers.items()
|
||||
]
|
||||
except AttributeError:
|
||||
logger.error(
|
||||
"Invalid response object for url %s, "
|
||||
"Expected Type: HTTPResponse, Actual Type: %s",
|
||||
self.request.url,
|
||||
type(response),
|
||||
)
|
||||
exception = ServerError("Invalid response type")
|
||||
response = self.sanic_app.error_handler.response(
|
||||
self.request, exception
|
||||
)
|
||||
headers = [
|
||||
(str(name).encode("latin-1"), str(value).encode("latin-1"))
|
||||
for name, value in response.headers.items()
|
||||
if name not in (b"Set-Cookie",)
|
||||
]
|
||||
|
||||
if "content-length" not in response.headers and not isinstance(
|
||||
response, StreamingHTTPResponse
|
||||
):
|
||||
headers += [
|
||||
(b"content-length", str(len(response.body)).encode("latin-1"))
|
||||
]
|
||||
|
||||
if response.cookies:
|
||||
cookies = SimpleCookie()
|
||||
cookies.load(response.cookies)
|
||||
headers += [
|
||||
(b"set-cookie", cookie.encode("utf-8"))
|
||||
for name, cookie in response.cookies.items()
|
||||
]
|
||||
|
||||
await self.transport.send(
|
||||
{
|
||||
"type": "http.response.start",
|
||||
"status": response.status,
|
||||
"headers": headers,
|
||||
}
|
||||
)
|
||||
|
||||
if isinstance(response, StreamingHTTPResponse):
|
||||
response.protocol = self.transport.get_protocol()
|
||||
await response.stream()
|
||||
await response.protocol.complete()
|
||||
|
||||
else:
|
||||
await self.transport.send(
|
||||
{
|
||||
"type": "http.response.body",
|
||||
"body": response.body,
|
||||
"more_body": False,
|
||||
}
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
from collections import MutableSequence
|
||||
from collections.abc import MutableSequence
|
||||
|
||||
|
||||
class BlueprintGroup(MutableSequence):
|
||||
|
||||
@@ -37,7 +37,7 @@ class Blueprint:
|
||||
url_prefix=None,
|
||||
host=None,
|
||||
version=None,
|
||||
strict_slashes=False,
|
||||
strict_slashes=None,
|
||||
):
|
||||
"""
|
||||
In *Sanic* terminology, a **Blueprint** is a logical collection of
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import os
|
||||
import types
|
||||
|
||||
from distutils.util import strtobool
|
||||
|
||||
from sanic.exceptions import PyFileError
|
||||
from sanic.helpers import import_string
|
||||
|
||||
|
||||
SANIC_PREFIX = "SANIC_"
|
||||
@@ -27,6 +26,9 @@ DEFAULT_CONFIG = {
|
||||
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
|
||||
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
|
||||
"ACCESS_LOG": True,
|
||||
"PROXIES_COUNT": -1,
|
||||
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
|
||||
"REAL_IP_HEADER": "X-Real-IP",
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +80,7 @@ class Config(dict):
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec(
|
||||
exec( # nosec
|
||||
compile(config_file.read(), filename, "exec"),
|
||||
module.__dict__,
|
||||
)
|
||||
@@ -101,6 +103,9 @@ class Config(dict):
|
||||
from yourapplication import default_config
|
||||
app.config.from_object(default_config)
|
||||
|
||||
or also:
|
||||
app.config.from_object('myproject.config.MyConfigClass')
|
||||
|
||||
You should not use this function to load the actual configuration but
|
||||
rather configuration defaults. The actual config should be loaded
|
||||
with :meth:`from_pyfile` and ideally from a location not within the
|
||||
@@ -108,6 +113,8 @@ class Config(dict):
|
||||
|
||||
:param obj: an object holding the configuration
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
obj = import_string(obj)
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
@@ -127,6 +134,23 @@ class Config(dict):
|
||||
self[config_key] = float(v)
|
||||
except ValueError:
|
||||
try:
|
||||
self[config_key] = bool(strtobool(v))
|
||||
self[config_key] = strtobool(v)
|
||||
except ValueError:
|
||||
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,))
|
||||
|
||||
@@ -218,6 +218,11 @@ class ContentRangeError(SanicException):
|
||||
}
|
||||
|
||||
|
||||
@add_status_code(417)
|
||||
class HeaderExpectationFailed(SanicException):
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(403)
|
||||
class Forbidden(SanicException):
|
||||
pass
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Defines basics of HTTP standard."""
|
||||
|
||||
from importlib import import_module
|
||||
from inspect import ismodule
|
||||
|
||||
|
||||
STATUS_CODES = {
|
||||
100: b"Continue",
|
||||
101: b"Switching Protocols",
|
||||
@@ -131,3 +135,21 @@ def remove_entity_headers(headers, allowed=("content-location", "expires")):
|
||||
if not is_entity_header(header) or header.lower() in allowed
|
||||
}
|
||||
return headers
|
||||
|
||||
|
||||
def import_string(module_name, package=None):
|
||||
"""
|
||||
import a module or class by string path.
|
||||
|
||||
:module_name: str with path of module or path to import and
|
||||
instanciate a class
|
||||
:returns: a module object or one instance from class if
|
||||
module_name is a valid path to class
|
||||
|
||||
"""
|
||||
module, klass = module_name.rsplit(".", 1)
|
||||
module = import_module(module, package=package)
|
||||
obj = getattr(module, klass)
|
||||
if ismodule(obj):
|
||||
return obj
|
||||
return obj()
|
||||
|
||||
126
sanic/request.py
126
sanic/request.py
@@ -27,9 +27,8 @@ except ImportError:
|
||||
else:
|
||||
json_loads = json.loads
|
||||
|
||||
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
EXPECT_HEADER = "EXPECT"
|
||||
|
||||
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
|
||||
# > If the media type remains unknown, the recipient SHOULD treat it
|
||||
@@ -95,11 +94,11 @@ class Request(dict):
|
||||
"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
|
||||
# TODO: Content-Encoding detection
|
||||
self._parsed_url = parse_url(url_bytes)
|
||||
self.app = None
|
||||
self.app = app
|
||||
|
||||
self.headers = headers
|
||||
self.version = version
|
||||
@@ -329,12 +328,18 @@ class Request(dict):
|
||||
|
||||
@property
|
||||
def ip(self):
|
||||
"""
|
||||
:return: peer ip of the socket
|
||||
"""
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._ip
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
"""
|
||||
:return: peer port of the socket
|
||||
"""
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._port
|
||||
@@ -353,27 +358,93 @@ class Request(dict):
|
||||
self._ip = self._socket[0]
|
||||
self._port = self._socket[1]
|
||||
|
||||
@property
|
||||
def server_name(self):
|
||||
"""
|
||||
Attempt to get the server's hostname in this order:
|
||||
`config.SERVER_NAME`, `x-forwarded-host` header, :func:`Request.host`
|
||||
|
||||
:return: the server name without port number
|
||||
:rtype: str
|
||||
"""
|
||||
return (
|
||||
self.app.config.get("SERVER_NAME")
|
||||
or self.headers.get("x-forwarded-host")
|
||||
or self.host.split(":")[0]
|
||||
)
|
||||
|
||||
@property
|
||||
def server_port(self):
|
||||
"""
|
||||
Attempt to get the server's port in this order:
|
||||
`x-forwarded-port` header, :func:`Request.host`, actual port used by
|
||||
the transport layer socket.
|
||||
:return: server port
|
||||
:rtype: int
|
||||
"""
|
||||
forwarded_port = self.headers.get("x-forwarded-port") or (
|
||||
self.host.split(":")[1] if ":" in self.host else None
|
||||
)
|
||||
if forwarded_port:
|
||||
return int(forwarded_port)
|
||||
else:
|
||||
_, port = self.transport.get_extra_info("sockname")
|
||||
return port
|
||||
|
||||
@property
|
||||
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.
|
||||
"""
|
||||
if not hasattr(self, "_remote_addr"):
|
||||
forwarded_for = self.headers.get("X-Forwarded-For", "").split(",")
|
||||
remote_addrs = [
|
||||
addr
|
||||
for addr in [addr.strip() for addr in forwarded_for]
|
||||
if addr
|
||||
]
|
||||
if len(remote_addrs) > 0:
|
||||
self._remote_addr = remote_addrs[0]
|
||||
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 = [
|
||||
addr
|
||||
for addr in [addr.strip() for addr in forwarded_for]
|
||||
if addr
|
||||
]
|
||||
if self.app.config.PROXIES_COUNT == -1:
|
||||
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:
|
||||
self._remote_addr = ""
|
||||
return self._remote_addr
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
"""
|
||||
Attempt to get the request scheme.
|
||||
Seeking the value in this order:
|
||||
`x-forwarded-proto` header, `x-scheme` header, the sanic app itself.
|
||||
|
||||
:return: http|https|ws|wss or arbitrary value given by the headers.
|
||||
:rtype: str
|
||||
"""
|
||||
forwarded_proto = self.headers.get(
|
||||
"x-forwarded-proto"
|
||||
) or self.headers.get("x-scheme")
|
||||
if forwarded_proto:
|
||||
return forwarded_proto
|
||||
|
||||
if (
|
||||
self.app.websocket_enabled
|
||||
and self.headers.get("upgrade") == "websocket"
|
||||
@@ -389,8 +460,12 @@ class Request(dict):
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
"""
|
||||
:return: the Host specified in the header, may contains port number.
|
||||
"""
|
||||
# it appears that httptools doesn't return the host
|
||||
# so pull it from the headers
|
||||
|
||||
return self.headers.get("Host", "")
|
||||
|
||||
@property
|
||||
@@ -419,6 +494,31 @@ class Request(dict):
|
||||
(self.scheme, self.host, self.path, None, self.query_string, None)
|
||||
)
|
||||
|
||||
def url_for(self, view_name, **kwargs):
|
||||
"""
|
||||
Same as :func:`sanic.Sanic.url_for`, but automatically determine
|
||||
`scheme` and `netloc` base on the request. Since this method is aiming
|
||||
to generate correct schema & netloc, `_external` is implied.
|
||||
|
||||
:param kwargs: takes same parameters as in :func:`sanic.Sanic.url_for`
|
||||
:return: an absolute url to the given view
|
||||
:rtype: str
|
||||
"""
|
||||
scheme = self.scheme
|
||||
host = self.server_name
|
||||
port = self.server_port
|
||||
|
||||
if (scheme.lower() in ("http", "ws") and port == 80) or (
|
||||
scheme.lower() in ("https", "wss") and port == 443
|
||||
):
|
||||
netloc = host
|
||||
else:
|
||||
netloc = "{}:{}".format(host, port)
|
||||
|
||||
return self.app.url_for(
|
||||
view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs
|
||||
)
|
||||
|
||||
|
||||
File = namedtuple("File", ["type", "body", "name"])
|
||||
|
||||
|
||||
@@ -59,16 +59,23 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
"status",
|
||||
"content_type",
|
||||
"headers",
|
||||
"chunked",
|
||||
"_cookies",
|
||||
)
|
||||
|
||||
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.streaming_fn = streaming_fn
|
||||
self.status = status
|
||||
self.headers = CIMultiDict(headers or {})
|
||||
self.chunked = chunked
|
||||
self._cookies = None
|
||||
|
||||
async def write(self, data):
|
||||
@@ -79,7 +86,10 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
if type(data) != bytes:
|
||||
data = self._encode_body(data)
|
||||
|
||||
self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
||||
if self.chunked:
|
||||
await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
||||
else:
|
||||
await self.protocol.push_data(data)
|
||||
await self.protocol.drain()
|
||||
|
||||
async def stream(
|
||||
@@ -88,15 +98,18 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
"""Streams headers, runs the `streaming_fn` callback that writes
|
||||
content to the response body, then finalizes the response body.
|
||||
"""
|
||||
if version != "1.1":
|
||||
self.chunked = False
|
||||
headers = self.get_headers(
|
||||
version,
|
||||
keep_alive=keep_alive,
|
||||
keep_alive_timeout=keep_alive_timeout,
|
||||
)
|
||||
self.protocol.push_data(headers)
|
||||
await self.protocol.push_data(headers)
|
||||
await self.protocol.drain()
|
||||
await self.streaming_fn(self)
|
||||
self.protocol.push_data(b"0\r\n\r\n")
|
||||
if self.chunked:
|
||||
await self.protocol.push_data(b"0\r\n\r\n")
|
||||
# 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.
|
||||
|
||||
@@ -109,8 +122,9 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
if keep_alive and keep_alive_timeout is not None:
|
||||
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
|
||||
|
||||
self.headers["Transfer-Encoding"] = "chunked"
|
||||
self.headers.pop("Content-Length", None)
|
||||
if self.chunked and version == "1.1":
|
||||
self.headers["Transfer-Encoding"] = "chunked"
|
||||
self.headers.pop("Content-Length", None)
|
||||
self.headers["Content-Type"] = self.headers.get(
|
||||
"Content-Type", self.content_type
|
||||
)
|
||||
@@ -327,6 +341,7 @@ async def file_stream(
|
||||
mime_type=None,
|
||||
headers=None,
|
||||
filename=None,
|
||||
chunked=True,
|
||||
_range=None,
|
||||
):
|
||||
"""Return a streaming response object with file data.
|
||||
@@ -336,6 +351,7 @@ async def file_stream(
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param filename: Override filename.
|
||||
:param chunked: Enable or disable chunked transfer-encoding
|
||||
:param _range:
|
||||
"""
|
||||
headers = headers or {}
|
||||
@@ -383,6 +399,7 @@ async def file_stream(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
chunked=chunked,
|
||||
)
|
||||
|
||||
|
||||
@@ -391,6 +408,7 @@ def stream(
|
||||
status=200,
|
||||
headers=None,
|
||||
content_type="text/plain; charset=utf-8",
|
||||
chunked=True,
|
||||
):
|
||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||
@@ -409,9 +427,14 @@ def stream(
|
||||
writes content to that response.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param chunked: Enable or disable chunked transfer-encoding
|
||||
"""
|
||||
return StreamingHTTPResponse(
|
||||
streaming_fn, headers=headers, content_type=content_type, status=status
|
||||
streaming_fn,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
status=status,
|
||||
chunked=chunked,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Parameter = namedtuple("Parameter", ["name", "cast"])
|
||||
REGEX_TYPES = {
|
||||
"string": (str, r"[^/]+"),
|
||||
"int": (int, r"-?\d+"),
|
||||
"number": (float, r"-?[0-9\\.]+"),
|
||||
"number": (float, r"-?(?:\d+(?:\.\d*)?|\.\d+)"),
|
||||
"alpha": (str, r"[A-Za-z]+"),
|
||||
"path": (str, r"[^/].*?"),
|
||||
"uuid": (
|
||||
@@ -406,6 +406,7 @@ class Router:
|
||||
if not self.hosts:
|
||||
return self._get(request.path, request.method, "")
|
||||
# virtual hosts specified; try to match route to the host header
|
||||
|
||||
try:
|
||||
return self._get(
|
||||
request.path, request.method, request.headers.get("Host", "")
|
||||
|
||||
@@ -15,6 +15,7 @@ from httptools.parser.errors import HttpParserError
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from sanic.exceptions import (
|
||||
HeaderExpectationFailed,
|
||||
InvalidUsage,
|
||||
PayloadTooLarge,
|
||||
RequestTimeout,
|
||||
@@ -22,7 +23,7 @@ from sanic.exceptions import (
|
||||
ServiceUnavailable,
|
||||
)
|
||||
from sanic.log import access_logger, logger
|
||||
from sanic.request import Request, StreamBuffer
|
||||
from sanic.request import EXPECT_HEADER, Request, StreamBuffer
|
||||
from sanic.response import HTTPResponse
|
||||
|
||||
|
||||
@@ -44,6 +45,8 @@ class HttpProtocol(asyncio.Protocol):
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
# app
|
||||
"app",
|
||||
# event loop, connection
|
||||
"loop",
|
||||
"transport",
|
||||
@@ -88,6 +91,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self,
|
||||
*,
|
||||
loop,
|
||||
app,
|
||||
request_handler,
|
||||
error_handler,
|
||||
signal=Signal(),
|
||||
@@ -107,6 +111,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
**kwargs
|
||||
):
|
||||
self.loop = loop
|
||||
self.app = app
|
||||
self.transport = None
|
||||
self.request = None
|
||||
self.parser = None
|
||||
@@ -115,7 +120,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.router = router
|
||||
self.signal = signal
|
||||
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.error_handler = error_handler
|
||||
self.request_timeout = request_timeout
|
||||
@@ -303,12 +308,17 @@ class HttpProtocol(asyncio.Protocol):
|
||||
version=self.parser.get_http_version(),
|
||||
method=self.parser.get_method().decode(),
|
||||
transport=self.transport,
|
||||
app=self.app,
|
||||
)
|
||||
# Remove any existing KeepAlive handler here,
|
||||
# It will be recreated if required on the new request.
|
||||
if self._keep_alive_timeout_handler:
|
||||
self._keep_alive_timeout_handler.cancel()
|
||||
self._keep_alive_timeout_handler = None
|
||||
|
||||
if self.request.headers.get(EXPECT_HEADER):
|
||||
self.expect_handler()
|
||||
|
||||
if self.is_request_stream:
|
||||
self._is_stream_handler = self.router.is_stream_handler(
|
||||
self.request
|
||||
@@ -319,6 +329,21 @@ class HttpProtocol(asyncio.Protocol):
|
||||
)
|
||||
self.execute_request_handler()
|
||||
|
||||
def expect_handler(self):
|
||||
"""
|
||||
Handler for Expect Header.
|
||||
"""
|
||||
expect = self.request.headers.get(EXPECT_HEADER)
|
||||
if self.request.version == "1.1":
|
||||
if expect.lower() == "100-continue":
|
||||
self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
|
||||
else:
|
||||
self.write_error(
|
||||
HeaderExpectationFailed(
|
||||
"Unknown Expect: {expect}".format(expect=expect)
|
||||
)
|
||||
)
|
||||
|
||||
def on_body(self, body):
|
||||
if self.is_request_stream and self._is_stream_handler:
|
||||
self._request_stream_task = self.loop.create_task(
|
||||
@@ -452,7 +477,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||
async def drain(self):
|
||||
await self._not_paused.wait()
|
||||
|
||||
def push_data(self, data):
|
||||
async def push_data(self, data):
|
||||
self.transport.write(data)
|
||||
|
||||
async def stream_response(self, response):
|
||||
@@ -549,11 +574,15 @@ class HttpProtocol(asyncio.Protocol):
|
||||
|
||||
: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(
|
||||
"Transport closed @ %s and exception "
|
||||
"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)
|
||||
else:
|
||||
@@ -607,6 +636,7 @@ def trigger_events(events, loop):
|
||||
def serve(
|
||||
host,
|
||||
port,
|
||||
app,
|
||||
request_handler,
|
||||
error_handler,
|
||||
before_start=None,
|
||||
@@ -698,12 +728,15 @@ def serve(
|
||||
if debug:
|
||||
loop.set_debug(debug)
|
||||
|
||||
app.asgi = False
|
||||
|
||||
connections = connections if connections is not None else set()
|
||||
server = partial(
|
||||
protocol,
|
||||
loop=loop,
|
||||
connections=connections,
|
||||
signal=signal,
|
||||
app=app,
|
||||
request_handler=request_handler,
|
||||
error_handler=error_handler,
|
||||
request_timeout=request_timeout,
|
||||
@@ -831,6 +864,8 @@ def serve_multiple(server_settings, workers):
|
||||
server_settings["host"] = None
|
||||
server_settings["port"] = None
|
||||
|
||||
processes = []
|
||||
|
||||
def sig_handler(signal, frame):
|
||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
||||
for process in processes:
|
||||
@@ -839,8 +874,6 @@ def serve_multiple(server_settings, workers):
|
||||
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
|
||||
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
|
||||
|
||||
processes = []
|
||||
|
||||
for _ in range(workers):
|
||||
process = Process(target=serve, kwargs=server_settings)
|
||||
process.daemon = True
|
||||
|
||||
325
sanic/testing.py
325
sanic/testing.py
@@ -1,11 +1,22 @@
|
||||
import asyncio
|
||||
import types
|
||||
import typing
|
||||
|
||||
from json import JSONDecodeError
|
||||
from socket import socket
|
||||
from urllib.parse import unquote, urlsplit
|
||||
|
||||
import httpcore
|
||||
import requests_async as requests
|
||||
import websockets
|
||||
|
||||
from sanic.asgi import ASGIApp
|
||||
from sanic.exceptions import MethodNotSupported
|
||||
from sanic.log import logger
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
ASGI_HOST = "mockserver"
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 42101
|
||||
|
||||
@@ -16,32 +27,41 @@ class SanicTestClient:
|
||||
self.app = app
|
||||
self.port = port
|
||||
|
||||
async def _local_request(self, method, url, cookies=None, *args, **kwargs):
|
||||
import aiohttp
|
||||
def get_new_session(self):
|
||||
return requests.Session()
|
||||
|
||||
async def _local_request(self, method, url, *args, **kwargs):
|
||||
logger.info(url)
|
||||
conn = aiohttp.TCPConnector(ssl=False)
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, connector=conn
|
||||
) as session:
|
||||
async with getattr(session, method.lower())(
|
||||
url, *args, **kwargs
|
||||
) as response:
|
||||
try:
|
||||
response.text = await response.text()
|
||||
except UnicodeDecodeError:
|
||||
response.text = None
|
||||
raw_cookies = kwargs.pop("raw_cookies", None)
|
||||
|
||||
if method == "websocket":
|
||||
async with websockets.connect(url, *args, **kwargs) as websocket:
|
||||
websocket.opened = websocket.open
|
||||
return websocket
|
||||
else:
|
||||
async with self.get_new_session() as session:
|
||||
|
||||
try:
|
||||
response.json = await response.json()
|
||||
except (
|
||||
JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError,
|
||||
):
|
||||
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(
|
||||
@@ -52,7 +72,7 @@ class SanicTestClient:
|
||||
debug=False,
|
||||
server_kwargs={"auto_reload": False},
|
||||
*request_args,
|
||||
**request_kwargs
|
||||
**request_kwargs,
|
||||
):
|
||||
results = [None, None]
|
||||
exceptions = []
|
||||
@@ -83,11 +103,15 @@ class SanicTestClient:
|
||||
server_kwargs = dict(sock=sock, **server_kwargs)
|
||||
host, port = sock.getsockname()
|
||||
|
||||
if uri.startswith(("http:", "https:", "ftp:", "ftps://", "//")):
|
||||
if uri.startswith(
|
||||
("http:", "https:", "ftp:", "ftps://", "//", "ws:", "wss:")
|
||||
):
|
||||
url = uri
|
||||
else:
|
||||
url = "http://{host}:{port}{uri}".format(
|
||||
host=host, port=port, uri=uri
|
||||
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
|
||||
)
|
||||
|
||||
@self.app.listener("after_server_start")
|
||||
@@ -112,7 +136,7 @@ class SanicTestClient:
|
||||
try:
|
||||
request, response = results
|
||||
return request, response
|
||||
except BaseException:
|
||||
except BaseException: # noqa
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results
|
||||
@@ -121,7 +145,7 @@ class SanicTestClient:
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except BaseException:
|
||||
except BaseException: # noqa
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results)
|
||||
)
|
||||
@@ -146,3 +170,254 @@ class SanicTestClient:
|
||||
|
||||
def head(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test("head", *args, **kwargs)
|
||||
|
||||
def websocket(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test("websocket", *args, **kwargs)
|
||||
|
||||
|
||||
class SanicASGIAdapter(requests.asgi.ASGIAdapter): # noqa
|
||||
async def send( # type: ignore
|
||||
self,
|
||||
request: requests.PreparedRequest,
|
||||
gather_return: bool = False,
|
||||
*args: typing.Any,
|
||||
**kwargs: typing.Any,
|
||||
) -> requests.Response:
|
||||
"""This method is taken MOSTLY verbatim from requests-asyn. The
|
||||
difference is the capturing of a response on the ASGI call and then
|
||||
returning it on the response object. This is implemented to achieve:
|
||||
|
||||
request, response = await app.asgi_client.get("/")
|
||||
|
||||
You can see the original code here:
|
||||
https://github.com/encode/requests-async/blob/614f40f77f19e6c6da8a212ae799107b0384dbf9/requests_async/asgi.py#L51""" # noqa
|
||||
scheme, netloc, path, query, fragment = urlsplit(
|
||||
request.url
|
||||
) # type: ignore
|
||||
|
||||
default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme]
|
||||
|
||||
if ":" in netloc:
|
||||
host, port_string = netloc.split(":", 1)
|
||||
port = int(port_string)
|
||||
else:
|
||||
host = netloc
|
||||
port = default_port
|
||||
|
||||
# Include the 'host' header.
|
||||
if "host" in request.headers:
|
||||
headers = [] # type: typing.List[typing.Tuple[bytes, bytes]]
|
||||
elif port == default_port:
|
||||
headers = [(b"host", host.encode())]
|
||||
else:
|
||||
headers = [(b"host", (f"{host}:{port}").encode())]
|
||||
|
||||
# Include other request headers.
|
||||
headers += [
|
||||
(key.lower().encode(), value.encode())
|
||||
for key, value in request.headers.items()
|
||||
]
|
||||
|
||||
no_response = False
|
||||
if scheme in {"ws", "wss"}:
|
||||
subprotocol = request.headers.get("sec-websocket-protocol", None)
|
||||
if subprotocol is None:
|
||||
subprotocols = [] # type: typing.Sequence[str]
|
||||
else:
|
||||
subprotocols = [
|
||||
value.strip() for value in subprotocol.split(",")
|
||||
]
|
||||
|
||||
scope = {
|
||||
"type": "websocket",
|
||||
"path": unquote(path),
|
||||
"root_path": "",
|
||||
"scheme": scheme,
|
||||
"query_string": query.encode(),
|
||||
"headers": headers,
|
||||
"client": ["testclient", 50000],
|
||||
"server": [host, port],
|
||||
"subprotocols": subprotocols,
|
||||
}
|
||||
no_response = True
|
||||
|
||||
else:
|
||||
scope = {
|
||||
"type": "http",
|
||||
"http_version": "1.1",
|
||||
"method": request.method,
|
||||
"path": unquote(path),
|
||||
"root_path": "",
|
||||
"scheme": scheme,
|
||||
"query_string": query.encode(),
|
||||
"headers": headers,
|
||||
"client": ["testclient", 50000],
|
||||
"server": [host, port],
|
||||
"extensions": {"http.response.template": {}},
|
||||
}
|
||||
|
||||
async def receive():
|
||||
nonlocal request_complete, response_complete
|
||||
|
||||
if request_complete:
|
||||
while not response_complete:
|
||||
await asyncio.sleep(0.0001)
|
||||
return {"type": "http.disconnect"}
|
||||
|
||||
body = request.body
|
||||
if isinstance(body, str):
|
||||
body_bytes = body.encode("utf-8") # type: bytes
|
||||
elif body is None:
|
||||
body_bytes = b""
|
||||
elif isinstance(body, types.GeneratorType):
|
||||
try:
|
||||
chunk = body.send(None)
|
||||
if isinstance(chunk, str):
|
||||
chunk = chunk.encode("utf-8")
|
||||
return {
|
||||
"type": "http.request",
|
||||
"body": chunk,
|
||||
"more_body": True,
|
||||
}
|
||||
except StopIteration:
|
||||
request_complete = True
|
||||
return {"type": "http.request", "body": b""}
|
||||
else:
|
||||
body_bytes = body
|
||||
|
||||
request_complete = True
|
||||
return {"type": "http.request", "body": body_bytes}
|
||||
|
||||
async def send(message) -> None:
|
||||
nonlocal raw_kwargs, response_started, response_complete, template, context # noqa
|
||||
|
||||
if message["type"] == "http.response.start":
|
||||
assert (
|
||||
not response_started
|
||||
), 'Received multiple "http.response.start" messages.'
|
||||
raw_kwargs["status_code"] = message["status"]
|
||||
raw_kwargs["headers"] = message["headers"]
|
||||
response_started = True
|
||||
elif message["type"] == "http.response.body":
|
||||
assert response_started, (
|
||||
'Received "http.response.body" '
|
||||
'without "http.response.start".'
|
||||
)
|
||||
assert (
|
||||
not response_complete
|
||||
), 'Received "http.response.body" after response completed.'
|
||||
body = message.get("body", b"")
|
||||
more_body = message.get("more_body", False)
|
||||
if request.method != "HEAD":
|
||||
raw_kwargs["content"] += body
|
||||
if not more_body:
|
||||
response_complete = True
|
||||
elif message["type"] == "http.response.template":
|
||||
template = message["template"]
|
||||
context = message["context"]
|
||||
|
||||
request_complete = False
|
||||
response_started = False
|
||||
response_complete = False
|
||||
raw_kwargs = {"content": b""} # type: typing.Dict[str, typing.Any]
|
||||
template = None
|
||||
context = None
|
||||
return_value = None
|
||||
|
||||
try:
|
||||
return_value = await self.app(scope, receive, send)
|
||||
except BaseException as exc:
|
||||
if not self.suppress_exceptions:
|
||||
raise exc from None
|
||||
|
||||
if no_response:
|
||||
response_started = True
|
||||
raw_kwargs = {"status_code": 204, "headers": []}
|
||||
|
||||
if not self.suppress_exceptions:
|
||||
assert response_started, "TestClient did not receive any response."
|
||||
elif not response_started:
|
||||
raw_kwargs = {"status_code": 500, "headers": []}
|
||||
|
||||
raw = httpcore.Response(**raw_kwargs)
|
||||
response = self.build_response(request, raw)
|
||||
if template is not None:
|
||||
response.template = template
|
||||
response.context = context
|
||||
|
||||
if gather_return:
|
||||
response.return_value = return_value
|
||||
return response
|
||||
|
||||
|
||||
class TestASGIApp(ASGIApp):
|
||||
async def __call__(self):
|
||||
await super().__call__()
|
||||
return self.request
|
||||
|
||||
|
||||
async def app_call_with_return(self, scope, receive, send):
|
||||
asgi_app = await TestASGIApp.create(self, scope, receive, send)
|
||||
return await asgi_app()
|
||||
|
||||
|
||||
class SanicASGITestClient(requests.ASGISession):
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
base_url: str = "http://{}".format(ASGI_HOST),
|
||||
suppress_exceptions: bool = False,
|
||||
) -> None:
|
||||
app.__class__.__call__ = app_call_with_return
|
||||
app.asgi = True
|
||||
super().__init__(app)
|
||||
|
||||
adapter = SanicASGIAdapter(
|
||||
app, suppress_exceptions=suppress_exceptions
|
||||
)
|
||||
self.mount("http://", adapter)
|
||||
self.mount("https://", adapter)
|
||||
self.mount("ws://", adapter)
|
||||
self.mount("wss://", adapter)
|
||||
self.headers.update({"user-agent": "testclient"})
|
||||
self.app = app
|
||||
self.base_url = base_url
|
||||
|
||||
async def request(self, method, url, gather_request=True, *args, **kwargs):
|
||||
|
||||
self.gather_request = gather_request
|
||||
response = await super().request(method, url, *args, **kwargs)
|
||||
response.status = response.status_code
|
||||
response.body = response.content
|
||||
response.content_type = response.headers.get("content-type")
|
||||
|
||||
if hasattr(response, "return_value"):
|
||||
request = response.return_value
|
||||
del response.return_value
|
||||
return request, response
|
||||
|
||||
return response
|
||||
|
||||
def merge_environment_settings(self, *args, **kwargs):
|
||||
settings = super().merge_environment_settings(*args, **kwargs)
|
||||
settings.update({"gather_return": self.gather_request})
|
||||
return settings
|
||||
|
||||
async def websocket(self, uri, subprotocols=None, *args, **kwargs):
|
||||
if uri.startswith(("ws:", "wss:")):
|
||||
url = uri
|
||||
else:
|
||||
uri = uri if uri.startswith("/") else "/{uri}".format(uri=uri)
|
||||
url = "ws://testserver{uri}".format(uri=uri)
|
||||
|
||||
headers = kwargs.get("headers", {})
|
||||
headers.setdefault("connection", "upgrade")
|
||||
headers.setdefault("sec-websocket-key", "testserver==")
|
||||
headers.setdefault("sec-websocket-version", "13")
|
||||
if subprotocols is not None:
|
||||
headers.setdefault(
|
||||
"sec-websocket-protocol", ", ".join(subprotocols)
|
||||
)
|
||||
kwargs["headers"] = headers
|
||||
|
||||
return await self.request("websocket", url, **kwargs)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
|
||||
|
||||
from httptools import HttpParserUpgrade
|
||||
from websockets import ConnectionClosed # noqa
|
||||
from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake
|
||||
@@ -6,6 +8,9 @@ from sanic.exceptions import InvalidUsage
|
||||
from sanic.server import HttpProtocol
|
||||
|
||||
|
||||
ASIMessage = MutableMapping[str, Any]
|
||||
|
||||
|
||||
class WebSocketProtocol(HttpProtocol):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -19,6 +24,7 @@ class WebSocketProtocol(HttpProtocol):
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.websocket = None
|
||||
# self.app = None
|
||||
self.websocket_timeout = websocket_timeout
|
||||
self.websocket_max_size = websocket_max_size
|
||||
self.websocket_max_queue = websocket_max_queue
|
||||
@@ -103,3 +109,45 @@ class WebSocketProtocol(HttpProtocol):
|
||||
self.websocket.connection_made(request.transport)
|
||||
self.websocket.connection_open()
|
||||
return self.websocket
|
||||
|
||||
|
||||
class WebSocketConnection:
|
||||
|
||||
# TODO
|
||||
# - Implement ping/pong
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
send: Callable[[ASIMessage], Awaitable[None]],
|
||||
receive: Callable[[], Awaitable[ASIMessage]],
|
||||
) -> None:
|
||||
self._send = send
|
||||
self._receive = receive
|
||||
|
||||
async def send(self, data: Union[str, bytes], *args, **kwargs) -> None:
|
||||
message = {"type": "websocket.send"}
|
||||
|
||||
try:
|
||||
data.decode()
|
||||
except AttributeError:
|
||||
message.update({"text": str(data)})
|
||||
else:
|
||||
message.update({"bytes": data})
|
||||
|
||||
await self._send(message)
|
||||
|
||||
async def recv(self, *args, **kwargs) -> Optional[str]:
|
||||
message = await self._receive()
|
||||
|
||||
if message["type"] == "websocket.receive":
|
||||
return message["text"]
|
||||
elif message["type"] == "websocket.disconnect":
|
||||
pass
|
||||
|
||||
receive = recv
|
||||
|
||||
async def accept(self) -> None:
|
||||
await self._send({"type": "websocket.accept", "subprotocol": ""})
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -14,7 +14,7 @@ multi_line_output = 3
|
||||
not_skip = __init__.py
|
||||
|
||||
[version]
|
||||
current_version = 0.8.3
|
||||
current_version = 19.3.1
|
||||
file = sanic/__init__.py
|
||||
current_version_pattern = __version__ = "{current_version}"
|
||||
new_version_pattern = __version__ = "{new_version}"
|
||||
|
||||
17
setup.py
17
setup.py
@@ -50,12 +50,12 @@ with open_local(["README.rst"]) as rm:
|
||||
setup_kwargs = {
|
||||
"name": "sanic",
|
||||
"version": version,
|
||||
"url": "http://github.com/channelcat/sanic/",
|
||||
"url": "http://github.com/huge-success/sanic/",
|
||||
"license": "MIT",
|
||||
"author": "Channel Cat",
|
||||
"author_email": "channelcat@gmail.com",
|
||||
"author": "Sanic Community",
|
||||
"author_email": "admhpkns@gmail.com",
|
||||
"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,
|
||||
"packages": ["sanic"],
|
||||
@@ -64,7 +64,6 @@ setup_kwargs = {
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
],
|
||||
@@ -83,6 +82,7 @@ requirements = [
|
||||
"aiofiles>=0.3.0",
|
||||
"websockets>=6.0,<7.0",
|
||||
"multidict>=4.0,<5.0",
|
||||
"requests-async==0.5.0",
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
@@ -90,12 +90,13 @@ tests_require = [
|
||||
"multidict>=4.0,<5.0",
|
||||
"gunicorn",
|
||||
"pytest-cov",
|
||||
"aiohttp>=2.3.0,<=3.2.1",
|
||||
"httpcore==0.3.0",
|
||||
"beautifulsoup4",
|
||||
uvloop,
|
||||
ujson,
|
||||
"pytest-sanic",
|
||||
"pytest-sugar",
|
||||
"pytest-benchmark",
|
||||
]
|
||||
|
||||
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
||||
@@ -111,14 +112,14 @@ if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
|
||||
|
||||
extras_require = {
|
||||
"test": tests_require,
|
||||
"dev": tests_require + ["aiofiles", "tox", "black", "flake8"],
|
||||
"dev": tests_require + ["aiofiles", "tox", "black", "flake8", "bandit"],
|
||||
"docs": [
|
||||
"sphinx",
|
||||
"sphinx_rtd_theme",
|
||||
"recommonmark",
|
||||
"sphinxcontrib-asyncio",
|
||||
"docutils",
|
||||
"pygments"
|
||||
"pygments",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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
|
||||
|
||||
@@ -9,6 +9,7 @@ import pytest
|
||||
from sanic import Sanic
|
||||
from sanic.router import RouteExists, Router
|
||||
|
||||
|
||||
random.seed("Pack my box with five dozen liquor jugs.")
|
||||
|
||||
if sys.platform in ["win32", "cygwin"]:
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Run with python3 simple_server.py PORT
|
||||
|
||||
from aiohttp import web
|
||||
import asyncio
|
||||
import sys
|
||||
import uvloop
|
||||
|
||||
import ujson as json
|
||||
import uvloop
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
loop = uvloop.new_event_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
|
||||
import bottle
|
||||
from bottle import route, run
|
||||
import ujson
|
||||
|
||||
from bottle import route, run
|
||||
|
||||
|
||||
@route("/")
|
||||
def index():
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Run with: python3 -O simple_server.py
|
||||
import asyncio
|
||||
from kyoukai import Kyoukai, HTTPRequestContext
|
||||
import logging
|
||||
|
||||
import ujson
|
||||
import uvloop
|
||||
|
||||
from kyoukai import HTTPRequestContext, Kyoukai
|
||||
|
||||
|
||||
loop = uvloop.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import asyncpg
|
||||
import sys
|
||||
import os
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import timeit
|
||||
|
||||
import asyncpg
|
||||
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
currentdir = os.path.dirname(
|
||||
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
||||
)
|
||||
sys.path.insert(0, currentdir + "/../../../")
|
||||
|
||||
import timeit
|
||||
|
||||
from sanic.response import json
|
||||
|
||||
print(json({"test": True}).output())
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import sys
|
||||
import os
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
currentdir = os.path.dirname(
|
||||
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
||||
)
|
||||
sys.path.insert(0, currentdir + "/../../../")
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic("test")
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import sys
|
||||
import os
|
||||
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(
|
||||
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
||||
)
|
||||
sys.path.insert(0, currentdir + "/../../../")
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
app = Sanic("test")
|
||||
|
||||
@@ -56,8 +58,6 @@ def query_string(request):
|
||||
)
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
app.run(host="0.0.0.0", port=sys.argv[1])
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Run with: python simple_server.py
|
||||
import ujson
|
||||
|
||||
from tornado import ioloop, web
|
||||
|
||||
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
""" Minimal helloworld application.
|
||||
"""
|
||||
|
||||
from wheezy.http import HTTPResponse
|
||||
from wheezy.http import WSGIApplication
|
||||
import ujson
|
||||
|
||||
from wheezy.http import HTTPResponse, WSGIApplication
|
||||
from wheezy.http.response import json_response
|
||||
from wheezy.routing import url
|
||||
from wheezy.web.handlers import BaseHandler
|
||||
from wheezy.web.middleware import bootstrap_defaults
|
||||
from wheezy.web.middleware import path_routing_middleware_factory
|
||||
|
||||
import ujson
|
||||
from wheezy.web.middleware import (
|
||||
bootstrap_defaults,
|
||||
path_routing_middleware_factory,
|
||||
)
|
||||
|
||||
|
||||
class WelcomeHandler(BaseHandler):
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
import sys
|
||||
|
||||
from inspect import isawaitable
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.exceptions import SanicException
|
||||
@@ -11,7 +12,7 @@ from sanic.response import text
|
||||
|
||||
def uvloop_installed():
|
||||
try:
|
||||
import uvloop
|
||||
import uvloop # noqa
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
@@ -56,7 +57,7 @@ def test_asyncio_server_start_serving(app):
|
||||
|
||||
def test_app_loop_not_running(app):
|
||||
with pytest.raises(SanicException) as excinfo:
|
||||
_ = app.loop
|
||||
app.loop
|
||||
|
||||
assert str(excinfo.value) == (
|
||||
"Loop can only be retrieved after the app has started "
|
||||
|
||||
231
tests/test_asgi.py
Normal file
231
tests/test_asgi.py
Normal file
@@ -0,0 +1,231 @@
|
||||
import asyncio
|
||||
|
||||
from collections import deque
|
||||
|
||||
import pytest
|
||||
import uvicorn
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.asgi import MockTransport
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.request import Request
|
||||
from sanic.response import text
|
||||
from sanic.websocket import WebSocketConnection
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def message_stack():
|
||||
return deque()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def receive(message_stack):
|
||||
async def _receive():
|
||||
return message_stack.popleft()
|
||||
|
||||
return _receive
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def send(message_stack):
|
||||
async def _send(message):
|
||||
message_stack.append(message)
|
||||
|
||||
return _send
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def transport(message_stack, receive, send):
|
||||
return MockTransport({}, receive, send)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
# @pytest.mark.asyncio
|
||||
def protocol(transport, loop):
|
||||
return transport.get_protocol()
|
||||
|
||||
|
||||
def test_listeners_triggered(app):
|
||||
before_server_start = False
|
||||
after_server_start = False
|
||||
before_server_stop = False
|
||||
after_server_stop = False
|
||||
|
||||
@app.listener("before_server_start")
|
||||
def do_before_server_start(*args, **kwargs):
|
||||
nonlocal before_server_start
|
||||
before_server_start = True
|
||||
|
||||
@app.listener("after_server_start")
|
||||
def do_after_server_start(*args, **kwargs):
|
||||
nonlocal after_server_start
|
||||
after_server_start = True
|
||||
|
||||
@app.listener("before_server_stop")
|
||||
def do_before_server_stop(*args, **kwargs):
|
||||
nonlocal before_server_stop
|
||||
before_server_stop = True
|
||||
|
||||
@app.listener("after_server_stop")
|
||||
def do_after_server_stop(*args, **kwargs):
|
||||
nonlocal after_server_stop
|
||||
after_server_stop = True
|
||||
|
||||
class CustomServer(uvicorn.Server):
|
||||
def install_signal_handlers(self):
|
||||
pass
|
||||
|
||||
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
|
||||
server = CustomServer(config=config)
|
||||
|
||||
with pytest.warns(UserWarning):
|
||||
server.run()
|
||||
|
||||
for task in asyncio.Task.all_tasks():
|
||||
task.cancel()
|
||||
|
||||
assert before_server_start
|
||||
assert after_server_start
|
||||
assert before_server_stop
|
||||
assert after_server_stop
|
||||
|
||||
|
||||
def test_listeners_triggered_async(app):
|
||||
before_server_start = False
|
||||
after_server_start = False
|
||||
before_server_stop = False
|
||||
after_server_stop = False
|
||||
|
||||
@app.listener("before_server_start")
|
||||
async def do_before_server_start(*args, **kwargs):
|
||||
nonlocal before_server_start
|
||||
before_server_start = True
|
||||
|
||||
@app.listener("after_server_start")
|
||||
async def do_after_server_start(*args, **kwargs):
|
||||
nonlocal after_server_start
|
||||
after_server_start = True
|
||||
|
||||
@app.listener("before_server_stop")
|
||||
async def do_before_server_stop(*args, **kwargs):
|
||||
nonlocal before_server_stop
|
||||
before_server_stop = True
|
||||
|
||||
@app.listener("after_server_stop")
|
||||
async def do_after_server_stop(*args, **kwargs):
|
||||
nonlocal after_server_stop
|
||||
after_server_stop = True
|
||||
|
||||
class CustomServer(uvicorn.Server):
|
||||
def install_signal_handlers(self):
|
||||
pass
|
||||
|
||||
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
|
||||
server = CustomServer(config=config)
|
||||
|
||||
with pytest.warns(UserWarning):
|
||||
server.run()
|
||||
|
||||
for task in asyncio.Task.all_tasks():
|
||||
task.cancel()
|
||||
|
||||
assert before_server_start
|
||||
assert after_server_start
|
||||
assert before_server_stop
|
||||
assert after_server_stop
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mockprotocol_events(protocol):
|
||||
assert protocol._not_paused.is_set()
|
||||
protocol.pause_writing()
|
||||
assert not protocol._not_paused.is_set()
|
||||
protocol.resume_writing()
|
||||
assert protocol._not_paused.is_set()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_protocol_push_data(protocol, message_stack):
|
||||
text = b"hello"
|
||||
|
||||
await protocol.push_data(text)
|
||||
await protocol.complete()
|
||||
|
||||
assert len(message_stack) == 2
|
||||
|
||||
message = message_stack.popleft()
|
||||
assert message["type"] == "http.response.body"
|
||||
assert message["more_body"]
|
||||
assert message["body"] == text
|
||||
|
||||
message = message_stack.popleft()
|
||||
assert message["type"] == "http.response.body"
|
||||
assert not message["more_body"]
|
||||
assert message["body"] == b""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_send(send, receive, message_stack):
|
||||
text_string = "hello"
|
||||
text_bytes = b"hello"
|
||||
|
||||
ws = WebSocketConnection(send, receive)
|
||||
await ws.send(text_string)
|
||||
await ws.send(text_bytes)
|
||||
|
||||
assert len(message_stack) == 2
|
||||
|
||||
message = message_stack.popleft()
|
||||
assert message["type"] == "websocket.send"
|
||||
assert message["text"] == text_string
|
||||
assert "bytes" not in message
|
||||
|
||||
message = message_stack.popleft()
|
||||
assert message["type"] == "websocket.send"
|
||||
assert message["bytes"] == text_bytes
|
||||
assert "text" not in message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_receive(send, receive, message_stack):
|
||||
msg = {"text": "hello", "type": "websocket.receive"}
|
||||
message_stack.append(msg)
|
||||
|
||||
ws = WebSocketConnection(send, receive)
|
||||
text = await ws.receive()
|
||||
|
||||
assert text == msg["text"]
|
||||
|
||||
|
||||
def test_improper_websocket_connection(transport, send, receive):
|
||||
with pytest.raises(InvalidUsage):
|
||||
transport.get_websocket_connection()
|
||||
|
||||
transport.create_websocket_connection(send, receive)
|
||||
connection = transport.get_websocket_connection()
|
||||
assert isinstance(connection, WebSocketConnection)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_class_regular(app):
|
||||
@app.get("/regular")
|
||||
def regular_request(request):
|
||||
return text(request.__class__.__name__)
|
||||
|
||||
_, response = await app.asgi_client.get("/regular")
|
||||
assert response.body == b"Request"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_class_custom():
|
||||
class MyCustomRequest(Request):
|
||||
pass
|
||||
|
||||
app = Sanic(request_class=MyCustomRequest)
|
||||
|
||||
@app.get("/custom")
|
||||
def custom_request(request):
|
||||
return text(request.__class__.__name__)
|
||||
|
||||
_, response = await app.asgi_client.get("/custom")
|
||||
assert response.body == b"MyCustomRequest"
|
||||
5
tests/test_asgi_client.py
Normal file
5
tests/test_asgi_client.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from sanic.testing import SanicASGITestClient
|
||||
|
||||
|
||||
def test_asgi_client_instantiation(app):
|
||||
assert isinstance(app.asgi_client, SanicASGITestClient)
|
||||
@@ -3,7 +3,8 @@ from pytest import raises
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.request import Request
|
||||
from sanic.response import text, HTTPResponse
|
||||
from sanic.response import HTTPResponse, text
|
||||
|
||||
|
||||
MIDDLEWARE_INVOKE_COUNTER = {"request": 0, "response": 0}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import pytest
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
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.response import text, json
|
||||
from sanic.response import json, text
|
||||
from sanic.views import CompositionView
|
||||
|
||||
|
||||
@@ -467,16 +467,8 @@ def test_bp_shorthand(app):
|
||||
request, response = app.test_client.get("/delete")
|
||||
assert response.status == 405
|
||||
|
||||
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 ev.is_set()
|
||||
|
||||
|
||||
@@ -595,14 +587,13 @@ def test_blueprint_middleware_with_args(app: Sanic):
|
||||
"/wa", headers={"content-type": "plain/text"}
|
||||
)
|
||||
assert response.json.get("test") == "value"
|
||||
d = {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("file_name", ["test.file"])
|
||||
def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
with open(current_file, "rb") as file:
|
||||
current_file_contents = file.read()
|
||||
file.read()
|
||||
|
||||
bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False)
|
||||
|
||||
@@ -662,16 +653,8 @@ def test_websocket_route(app: Sanic):
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
_, response = app.test_client.get(
|
||||
"/ws/test",
|
||||
headers={
|
||||
"Upgrade": "websocket",
|
||||
"Connection": "upgrade",
|
||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
||||
"Sec-WebSocket-Version": "13",
|
||||
},
|
||||
)
|
||||
assert response.status == 101
|
||||
_, response = app.test_client.websocket("/ws/test")
|
||||
assert response.opened is True
|
||||
assert event.is_set()
|
||||
|
||||
|
||||
@@ -704,3 +687,49 @@ def test_register_blueprint(app, debug):
|
||||
"version 1.0. Please use the blueprint method"
|
||||
" instead"
|
||||
)
|
||||
|
||||
|
||||
def test_strict_slashes_behavior_adoption(app):
|
||||
app.strict_slashes = True
|
||||
|
||||
@app.get("/test")
|
||||
def handler_test(request):
|
||||
return text("Test")
|
||||
|
||||
assert app.test_client.get("/test")[1].status == 200
|
||||
assert app.test_client.get("/test/")[1].status == 404
|
||||
|
||||
bp = Blueprint("bp")
|
||||
|
||||
@bp.get("/one", strict_slashes=False)
|
||||
def one(request):
|
||||
return text("one")
|
||||
|
||||
@bp.get("/second")
|
||||
def second(request):
|
||||
return text("second")
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
assert app.test_client.get("/one")[1].status == 200
|
||||
assert app.test_client.get("/one/")[1].status == 200
|
||||
|
||||
assert app.test_client.get("/second")[1].status == 200
|
||||
assert app.test_client.get("/second/")[1].status == 404
|
||||
|
||||
bp2 = Blueprint("bp2", strict_slashes=False)
|
||||
|
||||
@bp2.get("/third")
|
||||
def third(request):
|
||||
return text("third")
|
||||
|
||||
app.blueprint(bp2)
|
||||
assert app.test_client.get("/third")[1].status == 200
|
||||
assert app.test_client.get("/third/")[1].status == 200
|
||||
|
||||
@app.get("/f1", strict_slashes=False)
|
||||
def f1(request):
|
||||
return text("f1")
|
||||
|
||||
assert app.test_client.get("/f1")[1].status == 200
|
||||
assert app.test_client.get("/f1/")[1].status == 200
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from contextlib import contextmanager
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
from tempfile import TemporaryDirectory
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.config import Config, DEFAULT_CONFIG
|
||||
from sanic.config import DEFAULT_CONFIG, Config
|
||||
from sanic.exceptions import PyFileError
|
||||
|
||||
|
||||
@@ -17,17 +18,30 @@ def temp_path():
|
||||
yield Path(td, "file")
|
||||
|
||||
|
||||
def test_load_from_object(app):
|
||||
class Config:
|
||||
not_for_config = "should not be used"
|
||||
CONFIG_VALUE = "should be used"
|
||||
class ConfigTest:
|
||||
not_for_config = "should not be used"
|
||||
CONFIG_VALUE = "should be used"
|
||||
|
||||
app.config.from_object(Config)
|
||||
|
||||
def test_load_from_object(app):
|
||||
app.config.from_object(ConfigTest)
|
||||
assert "CONFIG_VALUE" in app.config
|
||||
assert app.config.CONFIG_VALUE == "should be used"
|
||||
assert "not_for_config" not in app.config
|
||||
|
||||
|
||||
def test_load_from_object_string(app):
|
||||
app.config.from_object("test_config.ConfigTest")
|
||||
assert "CONFIG_VALUE" in app.config
|
||||
assert app.config.CONFIG_VALUE == "should be used"
|
||||
assert "not_for_config" not in app.config
|
||||
|
||||
|
||||
def test_load_from_object_string_exception(app):
|
||||
with pytest.raises(ImportError):
|
||||
app.config.from_object("test_config.Config.test")
|
||||
|
||||
|
||||
def test_auto_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic()
|
||||
@@ -225,6 +239,7 @@ def test_config_access_log_passing_in_run(app):
|
||||
assert app.config.ACCESS_LOG == True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_access_log_passing_in_create_server(app):
|
||||
assert app.config.ACCESS_LOG == True
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from datetime import datetime, timedelta
|
||||
from http.cookies import SimpleCookie
|
||||
from sanic.response import text
|
||||
|
||||
import pytest
|
||||
from sanic.cookies import Cookie, DEFAULT_MAX_AGE
|
||||
|
||||
from sanic.cookies import Cookie
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
@@ -24,6 +27,24 @@ def test_cookies(app):
|
||||
assert response_cookies["right_back"].value == "at you"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cookies_asgi(app):
|
||||
@app.route("/")
|
||||
def handler(request):
|
||||
response = text("Cookies are: {}".format(request.cookies["test"]))
|
||||
response.cookies["right_back"] = "at you"
|
||||
return response
|
||||
|
||||
request, response = await app.asgi_client.get(
|
||||
"/", cookies={"test": "working!"}
|
||||
)
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get("set-cookie", {}))
|
||||
|
||||
assert response.text == "Cookies are: working!"
|
||||
assert response_cookies["right_back"].value == "at you"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
|
||||
def test_false_cookies_encoded(app, httponly, expected):
|
||||
@app.route("/")
|
||||
@@ -100,7 +121,7 @@ def test_cookie_deletion(app):
|
||||
|
||||
assert int(response_cookies["i_want_to_die"]["max-age"]) == 0
|
||||
with pytest.raises(KeyError):
|
||||
_ = response.cookies["i_never_existed"]
|
||||
response.cookies["i_never_existed"]
|
||||
|
||||
|
||||
def test_cookie_reserved_cookie():
|
||||
@@ -135,7 +156,7 @@ def test_cookie_set_same_key(app):
|
||||
|
||||
request, response = app.test_client.get("/", cookies=cookies)
|
||||
assert response.status == 200
|
||||
assert response.cookies["test"].value == "pass"
|
||||
assert response.cookies["test"] == "pass"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("max_age", ["0", 30, 30.0, 30.1, "30", "test"])
|
||||
@@ -149,19 +170,42 @@ def test_cookie_max_age(app, max_age):
|
||||
response.cookies["test"]["max-age"] = max_age
|
||||
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.cookies["test"].value == "pass"
|
||||
cookie = response.cookies.get("test")
|
||||
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)
|
||||
|
||||
if str(max_age).isdigit() and int(max_age) == float(max_age):
|
||||
assert response.cookies["test"]["max-age"] == str(max_age)
|
||||
# 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 response.cookies["test"]["max-age"] == str(DEFAULT_MAX_AGE)
|
||||
assert cookie is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expires", [datetime.now() + timedelta(seconds=60)])
|
||||
@pytest.mark.parametrize(
|
||||
"expires", [datetime.utcnow() + timedelta(seconds=60)]
|
||||
)
|
||||
def test_cookie_expires(app, expires):
|
||||
expires = expires.replace(microsecond=0)
|
||||
cookies = {"test": "wait"}
|
||||
|
||||
@app.get("/")
|
||||
@@ -171,15 +215,16 @@ def test_cookie_expires(app, expires):
|
||||
response.cookies["test"]["expires"] = expires
|
||||
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.cookies["test"].value == "pass"
|
||||
|
||||
if isinstance(expires, datetime):
|
||||
expires = expires.strftime("%a, %d-%b-%Y %T GMT")
|
||||
|
||||
assert response.cookies["test"]["expires"] == expires
|
||||
assert response.cookies["test"] == "pass"
|
||||
assert cookie_expires == expires
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expires", ["Fri, 21-Dec-2018 15:30:00 GMT"])
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from sanic.response import text
|
||||
from threading import Event
|
||||
import asyncio
|
||||
|
||||
from queue import Queue
|
||||
from threading import Event
|
||||
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
def test_create_task(app):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from sanic.server import HttpProtocol
|
||||
from sanic.response import text
|
||||
from sanic.server import HttpProtocol
|
||||
|
||||
|
||||
class CustomHttpProtocol(HttpProtocol):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from sanic.response import text
|
||||
from sanic.router import RouteExists
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import pytest
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import (
|
||||
Forbidden,
|
||||
InvalidUsage,
|
||||
NotFound,
|
||||
ServerError,
|
||||
Unauthorized,
|
||||
abort,
|
||||
)
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized
|
||||
from sanic.exceptions import Forbidden, abort
|
||||
|
||||
|
||||
class SanicExceptionTestException(Exception):
|
||||
|
||||
@@ -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 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")
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import inspect
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import helpers
|
||||
from sanic.config import Config
|
||||
|
||||
|
||||
def test_has_message_body():
|
||||
@@ -56,3 +61,18 @@ def test_remove_entity_headers():
|
||||
|
||||
for header, expected in tests:
|
||||
assert helpers.remove_entity_headers(header) == expected
|
||||
|
||||
|
||||
def test_import_string_class():
|
||||
obj = helpers.import_string("sanic.config.Config")
|
||||
assert isinstance(obj, Config)
|
||||
|
||||
|
||||
def test_import_string_module():
|
||||
module = helpers.import_string("sanic.config")
|
||||
assert inspect.ismodule(module)
|
||||
|
||||
|
||||
def test_import_string_exception():
|
||||
with pytest.raises(ImportError):
|
||||
helpers.import_string("test.test.test")
|
||||
|
||||
@@ -1,39 +1,72 @@
|
||||
from json import JSONDecodeError
|
||||
from sanic import Sanic
|
||||
import asyncio
|
||||
import functools
|
||||
import socket
|
||||
|
||||
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 import server
|
||||
import aiohttp
|
||||
from aiohttp import TCPConnector
|
||||
from sanic.testing import SanicTestClient, HOST, PORT
|
||||
from sanic.testing import HOST, PORT, SanicTestClient
|
||||
|
||||
|
||||
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
|
||||
|
||||
old_conn = None
|
||||
|
||||
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
|
||||
class ReusableSanicConnectionPool(httpcore.ConnectionPool):
|
||||
async def acquire_connection(self, origin):
|
||||
global old_conn
|
||||
connection = self.active_connections.pop_by_origin(
|
||||
origin, http2_only=True
|
||||
)
|
||||
if self.old_proto is not None:
|
||||
if self.old_proto != new_conn._protocol:
|
||||
if connection is None:
|
||||
connection = self.keepalive_connections.pop_by_origin(origin)
|
||||
|
||||
if connection is None:
|
||||
await self.max_connections.acquire()
|
||||
connection = httpcore.HTTPConnection(
|
||||
origin,
|
||||
ssl=self.ssl,
|
||||
timeout=self.timeout,
|
||||
backend=self.backend,
|
||||
release_func=self.release_connection,
|
||||
)
|
||||
self.active_connections.add(connection)
|
||||
|
||||
if old_conn is not None:
|
||||
if old_conn != connection:
|
||||
raise RuntimeError(
|
||||
"We got a new connection, wanted the same one!"
|
||||
)
|
||||
print(new_conn.__dict__)
|
||||
self.old_proto = new_conn._protocol
|
||||
return new_conn
|
||||
old_conn = connection
|
||||
|
||||
return connection
|
||||
|
||||
|
||||
class ReusableSanicAdapter(requests.adapters.HTTPAdapter):
|
||||
def __init__(self):
|
||||
self.pool = ReusableSanicConnectionPool()
|
||||
|
||||
|
||||
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):
|
||||
def __init__(self, app, loop=None):
|
||||
super(ReuseableSanicTestClient, self).__init__(app)
|
||||
super().__init__(app)
|
||||
if loop is None:
|
||||
loop = asyncio.get_event_loop()
|
||||
self._loop = loop
|
||||
@@ -49,14 +82,14 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
uri="/",
|
||||
gather_request=True,
|
||||
debug=False,
|
||||
server_kwargs={"return_asyncio_server": True},
|
||||
server_kwargs=None,
|
||||
*request_args,
|
||||
**request_kwargs
|
||||
**request_kwargs,
|
||||
):
|
||||
loop = self._loop
|
||||
results = [None, None]
|
||||
exceptions = []
|
||||
do_kill_server = request_kwargs.pop("end_server", False)
|
||||
server_kwargs = server_kwargs or {"return_asyncio_server": True}
|
||||
if gather_request:
|
||||
|
||||
def _collect_request(request):
|
||||
@@ -65,21 +98,26 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
|
||||
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")
|
||||
async def _collect_response(loop):
|
||||
try:
|
||||
if do_kill_server:
|
||||
request_kwargs["end_session"] = True
|
||||
response = await self._local_request(
|
||||
method, uri, *request_args, **request_kwargs
|
||||
method, url, *request_args, **request_kwargs
|
||||
)
|
||||
results[-1] = response
|
||||
except Exception as e2:
|
||||
import traceback
|
||||
|
||||
traceback.print_tb(e2.__traceback__)
|
||||
exceptions.append(e2)
|
||||
# Don't stop here! self.app.stop()
|
||||
|
||||
if self._server is not None:
|
||||
_server = self._server
|
||||
@@ -94,27 +132,13 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
|
||||
try:
|
||||
loop._stopping = False
|
||||
http_server = loop.run_until_complete(_server_co)
|
||||
_server = loop.run_until_complete(_server_co)
|
||||
except Exception as e1:
|
||||
import traceback
|
||||
|
||||
traceback.print_tb(e1.__traceback__)
|
||||
raise e1
|
||||
self._server = _server = http_server
|
||||
self._server = _server
|
||||
server.trigger_events(self.app.listeners["after_server_start"], loop)
|
||||
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:
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
|
||||
@@ -137,59 +161,53 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||
"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
|
||||
# same TCPConnection and the sane ClientSession more than once.
|
||||
# Note, you cannot use the same session if you are in a _different_
|
||||
# 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", 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
|
||||
if not self._session:
|
||||
self._session = ResusableSanicSession()
|
||||
try:
|
||||
response = await getattr(self._session, method.lower())(
|
||||
url, verify=False, timeout=request_keepalive, *args, **kwargs
|
||||
)
|
||||
do_kill_session = kwargs.pop("end_session", False)
|
||||
if self._session:
|
||||
session = self._session
|
||||
else:
|
||||
if self._tcp_connector:
|
||||
conn = self._tcp_connector
|
||||
else:
|
||||
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
|
||||
except NameError:
|
||||
raise Exception(response.status_code)
|
||||
|
||||
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 = response.json()
|
||||
except (JSONDecodeError, UnicodeDecodeError):
|
||||
response.json = None
|
||||
|
||||
try:
|
||||
response.json = await response.json()
|
||||
except (
|
||||
JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError,
|
||||
):
|
||||
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
|
||||
|
||||
response.body = await response.read()
|
||||
if do_kill_session:
|
||||
await session.close()
|
||||
self._session = None
|
||||
return response
|
||||
|
||||
|
||||
@@ -221,40 +239,46 @@ def test_keep_alive_timeout_reuse():
|
||||
"""If the server keep-alive timeout and client keep-alive timeout are
|
||||
both longer than the delay, the client _and_ server will successfully
|
||||
reuse the existing connection."""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop)
|
||||
headers = {"Connection": "keep-alive"}
|
||||
request, response = client.get("/1", headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.text == "OK"
|
||||
loop.run_until_complete(aio_sleep(1))
|
||||
request, response = client.get("/1", end_server=True)
|
||||
assert response.status == 200
|
||||
assert response.text == "OK"
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop)
|
||||
headers = {"Connection": "keep-alive"}
|
||||
request, response = client.get("/1", headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.text == "OK"
|
||||
loop.run_until_complete(aio_sleep(1))
|
||||
request, response = client.get("/1")
|
||||
assert response.status == 200
|
||||
assert response.text == "OK"
|
||||
finally:
|
||||
client.kill_server()
|
||||
|
||||
|
||||
def test_keep_alive_client_timeout():
|
||||
"""If the server keep-alive timeout is longer than the client
|
||||
keep-alive timeout, client will try to create a new connection here."""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
|
||||
headers = {"Connection": "keep-alive"}
|
||||
request, response = client.get("/1", headers=headers, request_keepalive=1)
|
||||
assert response.status == 200
|
||||
assert response.text == "OK"
|
||||
loop.run_until_complete(aio_sleep(2))
|
||||
exception = None
|
||||
try:
|
||||
request, response = client.get(
|
||||
"/1", end_server=True, request_keepalive=1
|
||||
)
|
||||
except ValueError as e:
|
||||
exception = e
|
||||
assert exception is not None
|
||||
assert isinstance(exception, ValueError)
|
||||
assert "got a new connection" in exception.args[0]
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
|
||||
headers = {"Connection": "keep-alive"}
|
||||
try:
|
||||
request, response = client.get(
|
||||
"/1", headers=headers, request_keepalive=1
|
||||
)
|
||||
assert response.status == 200
|
||||
assert response.text == "OK"
|
||||
loop.run_until_complete(aio_sleep(2))
|
||||
exception = None
|
||||
request, response = client.get("/1", request_keepalive=1)
|
||||
except ValueError as e:
|
||||
exception = e
|
||||
assert exception is not None
|
||||
assert isinstance(exception, ValueError)
|
||||
assert "got a new connection" in exception.args[0]
|
||||
finally:
|
||||
client.kill_server()
|
||||
|
||||
|
||||
def test_keep_alive_server_timeout():
|
||||
@@ -262,24 +286,27 @@ def test_keep_alive_server_timeout():
|
||||
keep-alive timeout, the client will either a 'Connection reset' error
|
||||
_or_ a new connection. Depending on how the event-loop handles the
|
||||
broken server connection."""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
|
||||
headers = {"Connection": "keep-alive"}
|
||||
request, response = client.get("/1", headers=headers, request_keepalive=60)
|
||||
assert response.status == 200
|
||||
assert response.text == "OK"
|
||||
loop.run_until_complete(aio_sleep(3))
|
||||
exception = None
|
||||
try:
|
||||
request, response = client.get(
|
||||
"/1", request_keepalive=60, end_server=True
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
|
||||
headers = {"Connection": "keep-alive"}
|
||||
try:
|
||||
request, response = client.get(
|
||||
"/1", headers=headers, request_keepalive=60
|
||||
)
|
||||
assert response.status == 200
|
||||
assert response.text == "OK"
|
||||
loop.run_until_complete(aio_sleep(3))
|
||||
exception = None
|
||||
request, response = client.get("/1", request_keepalive=60)
|
||||
except ValueError as e:
|
||||
exception = e
|
||||
assert exception is not None
|
||||
assert isinstance(exception, ValueError)
|
||||
assert (
|
||||
"Connection reset" in exception.args[0]
|
||||
or "got a new connection" in exception.args[0]
|
||||
)
|
||||
except ValueError as e:
|
||||
exception = e
|
||||
assert exception is not None
|
||||
assert isinstance(exception, ValueError)
|
||||
assert (
|
||||
"Connection reset" in exception.args[0]
|
||||
or "got a new connection" in exception.args[0]
|
||||
)
|
||||
finally:
|
||||
client.kill_server()
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import uuid
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from io import StringIO
|
||||
from importlib import reload
|
||||
|
||||
import pytest
|
||||
from io import StringIO
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
import sanic
|
||||
from sanic.response import text
|
||||
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
||||
|
||||
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; \
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from sanic.config import BASE_LOGO
|
||||
from sanic.testing import PORT
|
||||
|
||||
|
||||
try:
|
||||
import uvloop # noqa
|
||||
@@ -12,7 +14,9 @@ except BaseException:
|
||||
|
||||
|
||||
def test_logo_base(app, caplog):
|
||||
server = app.create_server(debug=True, return_asyncio_server=True)
|
||||
server = app.create_server(
|
||||
debug=True, return_asyncio_server=True, port=PORT
|
||||
)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop._stopping = False
|
||||
@@ -31,7 +35,9 @@ def test_logo_base(app, caplog):
|
||||
def test_logo_false(app, caplog):
|
||||
app.config.LOGO = False
|
||||
|
||||
server = app.create_server(debug=True, return_asyncio_server=True)
|
||||
server = app.create_server(
|
||||
debug=True, return_asyncio_server=True, port=PORT
|
||||
)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop._stopping = False
|
||||
@@ -44,13 +50,17 @@ def test_logo_false(app, caplog):
|
||||
app.stop()
|
||||
|
||||
assert caplog.record_tuples[ROW][1] == logging.INFO
|
||||
assert caplog.record_tuples[ROW][2] == "Goin' Fast @ http://127.0.0.1:8000"
|
||||
assert caplog.record_tuples[ROW][
|
||||
2
|
||||
] == "Goin' Fast @ http://127.0.0.1:{}".format(PORT)
|
||||
|
||||
|
||||
def test_logo_true(app, caplog):
|
||||
app.config.LOGO = True
|
||||
|
||||
server = app.create_server(debug=True, return_asyncio_server=True)
|
||||
server = app.create_server(
|
||||
debug=True, return_asyncio_server=True, port=PORT
|
||||
)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop._stopping = False
|
||||
@@ -69,7 +79,9 @@ def test_logo_true(app, caplog):
|
||||
def test_logo_custom(app, caplog):
|
||||
app.config.LOGO = "My Custom Logo"
|
||||
|
||||
server = app.create_server(debug=True, return_asyncio_server=True)
|
||||
server = app.create_server(
|
||||
debug=True, return_asyncio_server=True, port=PORT
|
||||
)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop._stopping = False
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
|
||||
from asyncio import CancelledError
|
||||
|
||||
from sanic.exceptions import NotFound
|
||||
from sanic.request import Request
|
||||
from sanic.response import HTTPResponse, text
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import multiprocessing
|
||||
import pickle
|
||||
import random
|
||||
import signal
|
||||
import pickle
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.testing import HOST, PORT
|
||||
from sanic.response import text
|
||||
from sanic.testing import HOST, PORT
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import URLBuildError
|
||||
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 sanic.response import text, redirect
|
||||
import pytest
|
||||
|
||||
from sanic.response import redirect, text
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -109,21 +110,19 @@ def test_redirect_with_header_injection(redirect_app):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("test_str", ["sanic-test", "sanictest", "sanic test"])
|
||||
async def test_redirect_with_params(app, test_client, test_str):
|
||||
def test_redirect_with_params(app, test_str):
|
||||
use_in_uri = quote(test_str)
|
||||
|
||||
@app.route("/api/v1/test/<test>/")
|
||||
async def init_handler(request, test):
|
||||
assert test == test_str
|
||||
return redirect("/api/v2/test/{}/".format(quote(test)))
|
||||
return redirect("/api/v2/test/{}/".format(use_in_uri))
|
||||
|
||||
@app.route("/api/v2/test/<test>/")
|
||||
async def target_handler(request, test):
|
||||
assert test == test_str
|
||||
return text("OK")
|
||||
|
||||
test_cli = await test_client(app)
|
||||
|
||||
response = await test_cli.get("/api/v1/test/{}/".format(quote(test_str)))
|
||||
_, response = app.test_client.get("/api/v1/test/{}/".format(use_in_uri))
|
||||
assert response.status == 200
|
||||
|
||||
txt = await response.text()
|
||||
assert txt == "OK"
|
||||
assert response.content == b"OK"
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
|
||||
from sanic.response import text, stream
|
||||
import pytest
|
||||
|
||||
from sanic.response import stream, text
|
||||
|
||||
|
||||
async def test_request_cancel_when_connection_lost(loop, app, test_client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_cancel_when_connection_lost(app):
|
||||
app.still_serving_cancelled_request = False
|
||||
|
||||
@app.get("/")
|
||||
@@ -14,10 +17,9 @@ async def test_request_cancel_when_connection_lost(loop, app, test_client):
|
||||
app.still_serving_cancelled_request = True
|
||||
return text("OK")
|
||||
|
||||
test_cli = await test_client(app)
|
||||
|
||||
# schedule client call
|
||||
task = loop.create_task(test_cli.get("/"))
|
||||
loop = asyncio.get_event_loop()
|
||||
task = loop.create_task(app.asgi_client.get("/"))
|
||||
loop.call_later(0.01, task)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
@@ -33,7 +35,8 @@ async def test_request_cancel_when_connection_lost(loop, app, test_client):
|
||||
assert app.still_serving_cancelled_request is False
|
||||
|
||||
|
||||
async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_request_cancel_when_conn_lost(app):
|
||||
app.still_serving_cancelled_request = False
|
||||
|
||||
@app.post("/post/<id>", stream=True)
|
||||
@@ -53,10 +56,9 @@ async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
|
||||
|
||||
return stream(streaming)
|
||||
|
||||
test_cli = await test_client(app)
|
||||
|
||||
# schedule client call
|
||||
task = loop.create_task(test_cli.post("/post/1"))
|
||||
loop = asyncio.get_event_loop()
|
||||
task = loop.create_task(app.asgi_client.post("/post/1"))
|
||||
loop.call_later(0.01, task)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import random
|
||||
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
try:
|
||||
from ujson import loads
|
||||
except ImportError:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
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.exceptions import HeaderExpectationFailed
|
||||
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
|
||||
@@ -42,6 +43,46 @@ def test_request_stream_method_view(app):
|
||||
assert response.text == data
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"headers, expect_raise_exception",
|
||||
[
|
||||
({"EXPECT": "100-continue"}, False),
|
||||
({"EXPECT": "100-continue-extra"}, True),
|
||||
],
|
||||
)
|
||||
def test_request_stream_100_continue(app, headers, expect_raise_exception):
|
||||
class SimpleView(HTTPMethodView):
|
||||
@stream_decorator
|
||||
async def post(self, request):
|
||||
assert isinstance(request.stream, StreamBuffer)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
app.add_route(SimpleView.as_view(), "/method_view")
|
||||
|
||||
assert app.is_request_stream is True
|
||||
|
||||
if not expect_raise_exception:
|
||||
request, response = app.test_client.post(
|
||||
"/method_view", data=data, headers={"EXPECT": "100-continue"}
|
||||
)
|
||||
assert response.status == 200
|
||||
assert response.text == data
|
||||
else:
|
||||
with pytest.raises(ValueError) as e:
|
||||
app.test_client.post(
|
||||
"/method_view",
|
||||
data=data,
|
||||
headers={"EXPECT": "100-continue-extra"},
|
||||
)
|
||||
assert "Unknown Expect: 100-continue-extra" in str(e)
|
||||
|
||||
|
||||
def test_request_stream_app(app):
|
||||
"""for self.is_request_stream = True and decorators"""
|
||||
|
||||
@@ -73,15 +114,13 @@ def test_request_stream_app(app):
|
||||
@app.post("/post/<id>", stream=True)
|
||||
async def post(request, id):
|
||||
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)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
@app.put("/_put")
|
||||
async def _put(request):
|
||||
@@ -91,15 +130,13 @@ def test_request_stream_app(app):
|
||||
@app.put("/put", stream=True)
|
||||
async def put(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)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
@app.patch("/_patch")
|
||||
async def _patch(request):
|
||||
@@ -109,15 +146,13 @@ def test_request_stream_app(app):
|
||||
@app.patch("/patch", stream=True)
|
||||
async def patch(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)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
assert app.is_request_stream is True
|
||||
|
||||
@@ -162,21 +197,134 @@ def test_request_stream_app(app):
|
||||
assert response.text == data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_stream_app_asgi(app):
|
||||
"""for self.is_request_stream = True and decorators"""
|
||||
|
||||
@app.get("/get")
|
||||
async def get(request):
|
||||
assert request.stream is None
|
||||
return text("GET")
|
||||
|
||||
@app.head("/head")
|
||||
async def head(request):
|
||||
assert request.stream is None
|
||||
return text("HEAD")
|
||||
|
||||
@app.delete("/delete")
|
||||
async def delete(request):
|
||||
assert request.stream is None
|
||||
return text("DELETE")
|
||||
|
||||
@app.options("/options")
|
||||
async def options(request):
|
||||
assert request.stream is None
|
||||
return text("OPTIONS")
|
||||
|
||||
@app.post("/_post/<id>")
|
||||
async def _post(request, id):
|
||||
assert request.stream is None
|
||||
return text("_POST")
|
||||
|
||||
@app.post("/post/<id>", stream=True)
|
||||
async def post(request, id):
|
||||
assert isinstance(request.stream, StreamBuffer)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
@app.put("/_put")
|
||||
async def _put(request):
|
||||
assert request.stream is None
|
||||
return text("_PUT")
|
||||
|
||||
@app.put("/put", stream=True)
|
||||
async def put(request):
|
||||
assert isinstance(request.stream, StreamBuffer)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
@app.patch("/_patch")
|
||||
async def _patch(request):
|
||||
assert request.stream is None
|
||||
return text("_PATCH")
|
||||
|
||||
@app.patch("/patch", stream=True)
|
||||
async def patch(request):
|
||||
assert isinstance(request.stream, StreamBuffer)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
assert app.is_request_stream is True
|
||||
|
||||
request, response = await app.asgi_client.get("/get")
|
||||
assert response.status == 200
|
||||
assert response.text == "GET"
|
||||
|
||||
request, response = await app.asgi_client.head("/head")
|
||||
assert response.status == 200
|
||||
assert response.text == ""
|
||||
|
||||
request, response = await app.asgi_client.delete("/delete")
|
||||
assert response.status == 200
|
||||
assert response.text == "DELETE"
|
||||
|
||||
request, response = await app.asgi_client.options("/options")
|
||||
assert response.status == 200
|
||||
assert response.text == "OPTIONS"
|
||||
|
||||
request, response = await app.asgi_client.post("/_post/1", data=data)
|
||||
assert response.status == 200
|
||||
assert response.text == "_POST"
|
||||
|
||||
request, response = await app.asgi_client.post("/post/1", data=data)
|
||||
assert response.status == 200
|
||||
assert response.text == data
|
||||
|
||||
request, response = await app.asgi_client.put("/_put", data=data)
|
||||
assert response.status == 200
|
||||
assert response.text == "_PUT"
|
||||
|
||||
request, response = await app.asgi_client.put("/put", data=data)
|
||||
assert response.status == 200
|
||||
assert response.text == data
|
||||
|
||||
request, response = await app.asgi_client.patch("/_patch", data=data)
|
||||
assert response.status == 200
|
||||
assert response.text == "_PATCH"
|
||||
|
||||
request, response = await app.asgi_client.patch("/patch", data=data)
|
||||
assert response.status == 200
|
||||
assert response.text == data
|
||||
|
||||
|
||||
def test_request_stream_handle_exception(app):
|
||||
"""for handling exceptions properly"""
|
||||
|
||||
@app.post("/post/<id>", stream=True)
|
||||
async def post(request, id):
|
||||
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)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
# 404
|
||||
request, response = app.test_client.post("/in_valid_post", data=data)
|
||||
@@ -224,15 +372,13 @@ def test_request_stream_blueprint(app):
|
||||
@bp.post("/post/<id>", stream=True)
|
||||
async def post(request, id):
|
||||
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)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
@bp.put("/_put")
|
||||
async def _put(request):
|
||||
@@ -242,15 +388,13 @@ def test_request_stream_blueprint(app):
|
||||
@bp.put("/put", stream=True)
|
||||
async def put(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)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
@bp.patch("/_patch")
|
||||
async def _patch(request):
|
||||
@@ -260,27 +404,23 @@ def test_request_stream_blueprint(app):
|
||||
@bp.patch("/patch", stream=True)
|
||||
async def patch(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)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
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)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
bp.add_route(
|
||||
post_add_route, "/post/add_route", methods=["POST"], stream=True
|
||||
@@ -390,15 +530,13 @@ def test_request_stream(app):
|
||||
@app.post("/stream", stream=True)
|
||||
async def handler(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)
|
||||
result = ""
|
||||
while True:
|
||||
body = await request.stream.read()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode("utf-8")
|
||||
return text(result)
|
||||
|
||||
@app.get("/get")
|
||||
async def get(request):
|
||||
|
||||
@@ -1,183 +1,59 @@
|
||||
from json import JSONDecodeError
|
||||
import asyncio
|
||||
|
||||
import httpcore
|
||||
import requests_async as requests
|
||||
|
||||
from sanic import Sanic
|
||||
import asyncio
|
||||
from sanic.response import text
|
||||
import aiohttp
|
||||
from aiohttp import TCPConnector
|
||||
from sanic.testing import SanicTestClient, HOST
|
||||
from sanic.testing import SanicTestClient
|
||||
|
||||
try:
|
||||
try:
|
||||
# direct use
|
||||
import packaging
|
||||
|
||||
version = packaging.version
|
||||
except (ImportError, AttributeError):
|
||||
# setuptools v39.0 and above.
|
||||
class DelayableSanicConnectionPool(httpcore.ConnectionPool):
|
||||
def __init__(self, request_delay=None, *args, **kwargs):
|
||||
self._request_delay = request_delay
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def send(self, request, stream=False, ssl=None, timeout=None):
|
||||
connection = await self.acquire_connection(request.url.origin)
|
||||
if (
|
||||
connection.h11_connection is None
|
||||
and connection.h2_connection is None
|
||||
):
|
||||
await connection.connect(ssl=ssl, timeout=timeout)
|
||||
if self._request_delay:
|
||||
await asyncio.sleep(self._request_delay)
|
||||
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.")
|
||||
response = await connection.send(
|
||||
request, stream=stream, ssl=ssl, timeout=timeout
|
||||
)
|
||||
except BaseException as exc:
|
||||
self.active_connections.remove(connection)
|
||||
self.max_connections.release()
|
||||
raise exc
|
||||
return response
|
||||
|
||||
|
||||
aiohttp_version = version.parse(aiohttp.__version__)
|
||||
class DelayableSanicAdapter(requests.adapters.HTTPAdapter):
|
||||
def __init__(self, request_delay=None):
|
||||
self.pool = DelayableSanicConnectionPool(request_delay=request_delay)
|
||||
|
||||
|
||||
class DelayableTCPConnector(TCPConnector):
|
||||
class RequestContextManager(object):
|
||||
def __new__(cls, req, delay):
|
||||
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"):
|
||||
kw["auto_decompress"] = None
|
||||
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(
|
||||
req, *args, **kwargs
|
||||
)
|
||||
if self._post_connect_delay and self._post_connect_delay > 0:
|
||||
await asyncio.sleep(self._post_connect_delay, loop=self._loop)
|
||||
req.send = d_req.send
|
||||
t = req.loop.time()
|
||||
print("Connected at {}".format(t), flush=True)
|
||||
return conn
|
||||
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):
|
||||
def __init__(self, app, loop, request_delay=1):
|
||||
super(DelayableSanicTestClient, self).__init__(app)
|
||||
def __init__(self, app, request_delay=None):
|
||||
super().__init__(app)
|
||||
self._request_delay = request_delay
|
||||
self._loop = None
|
||||
|
||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
||||
if self._loop is None:
|
||||
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
|
||||
def get_new_session(self):
|
||||
return DelayableSanicSession(request_delay=self._request_delay)
|
||||
|
||||
|
||||
request_timeout_default_app = Sanic("test_request_timeout_default")
|
||||
@@ -202,14 +78,14 @@ async def ws_handler1(request, ws):
|
||||
|
||||
|
||||
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")
|
||||
assert response.status == 408
|
||||
assert response.text == "Error: Request 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")
|
||||
assert response.status == 200
|
||||
assert response.text == "OK"
|
||||
@@ -224,7 +100,7 @@ def test_default_server_error_websocket_request_timeout():
|
||||
"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)
|
||||
|
||||
assert response.status == 408
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
|
||||
from collections import namedtuple
|
||||
from mimetypes import guess_type
|
||||
from random import choice
|
||||
@@ -8,6 +9,7 @@ from unittest.mock import MagicMock
|
||||
from urllib.parse import unquote
|
||||
|
||||
import pytest
|
||||
|
||||
from aiofiles import os as async_os
|
||||
|
||||
from sanic.response import (
|
||||
@@ -18,11 +20,11 @@ from sanic.response import (
|
||||
json,
|
||||
raw,
|
||||
stream,
|
||||
text,
|
||||
)
|
||||
from sanic.server import HttpProtocol
|
||||
from sanic.testing import HOST, PORT
|
||||
|
||||
|
||||
JSON_DATA = {"ok": True}
|
||||
|
||||
|
||||
@@ -77,10 +79,10 @@ def test_response_header(app):
|
||||
|
||||
request, response = app.test_client.get("/")
|
||||
assert dict(response.headers) == {
|
||||
"Connection": "keep-alive",
|
||||
"Keep-Alive": str(app.config.KEEP_ALIVE_TIMEOUT),
|
||||
"Content-Length": "11",
|
||||
"Content-Type": "application/json",
|
||||
"connection": "keep-alive",
|
||||
"keep-alive": str(app.config.KEEP_ALIVE_TIMEOUT),
|
||||
"content-length": "11",
|
||||
"content-type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
@@ -192,22 +194,57 @@ def test_no_content(json_app):
|
||||
def streaming_app(app):
|
||||
@app.route("/")
|
||||
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
|
||||
|
||||
|
||||
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("/")
|
||||
assert response.headers["Transfer-Encoding"] == "chunked"
|
||||
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("/")
|
||||
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])
|
||||
def test_stream_response_status_returns_correct_headers(status):
|
||||
response = StreamingHTTPResponse(sample_streaming_fn, status=status)
|
||||
@@ -227,13 +264,27 @@ def test_stream_response_keep_alive_returns_correct_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)
|
||||
headers = response.get_headers()
|
||||
headers = response.get_headers(version="1.1")
|
||||
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.protocol = MagicMock(HttpProtocol)
|
||||
response.protocol.transport = MagicMock(asyncio.Transport)
|
||||
@@ -241,7 +292,7 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app):
|
||||
async def mock_drain():
|
||||
pass
|
||||
|
||||
def mock_push_data(data):
|
||||
async def mock_push_data(data):
|
||||
response.protocol.transport.write(data)
|
||||
|
||||
response.protocol.push_data = mock_push_data
|
||||
@@ -262,6 +313,42 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app):
|
||||
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
|
||||
|
||||
async 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()
|
||||
|
||||
streaming_app.run(host=HOST, port=PORT)
|
||||
@@ -276,7 +363,7 @@ def test_stream_response_with_cookies(app):
|
||||
return response
|
||||
|
||||
request, response = app.test_client.get("/")
|
||||
assert response.cookies["test"].value == "pass"
|
||||
assert response.cookies["test"] == "pass"
|
||||
|
||||
|
||||
def test_stream_response_without_cookies(app):
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from sanic import Sanic
|
||||
import asyncio
|
||||
from sanic.response import text
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.exceptions import ServiceUnavailable
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
response_timeout_app = Sanic("test_response_timeout")
|
||||
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.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# UTF-8
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -365,9 +366,18 @@ def test_dynamic_route_number(app):
|
||||
request, response = app.test_client.get("/weight/1234.56")
|
||||
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")
|
||||
assert response.status == 404
|
||||
|
||||
request, response = app.test_client.get("/weight/12.34.56")
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_dynamic_route_regex(app):
|
||||
@app.route("/folder/<folder_id:[A-Za-z0-9]{0,4}>")
|
||||
@@ -459,16 +469,21 @@ def test_websocket_route(app, url):
|
||||
assert ws.subprotocol is None
|
||||
ev.set()
|
||||
|
||||
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(url)
|
||||
assert response.opened is True
|
||||
assert ev.is_set()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("url", ["/ws", "ws"])
|
||||
async def test_websocket_route_asgi(app, url):
|
||||
ev = asyncio.Event()
|
||||
|
||||
@app.websocket(url)
|
||||
async def handler(request, ws):
|
||||
ev.set()
|
||||
|
||||
request, response = await app.asgi_client.websocket(url)
|
||||
assert ev.is_set()
|
||||
|
||||
|
||||
@@ -478,54 +493,24 @@ def test_websocket_route_with_subprotocols(app):
|
||||
@app.websocket("/ws", subprotocols=["foo", "bar"])
|
||||
async def handler(request, ws):
|
||||
results.append(ws.subprotocol)
|
||||
assert ws.subprotocol is not None
|
||||
|
||||
request, response = app.test_client.get(
|
||||
"/ws",
|
||||
headers={
|
||||
"Upgrade": "websocket",
|
||||
"Connection": "upgrade",
|
||||
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
||||
"Sec-WebSocket-Version": "13",
|
||||
"Sec-WebSocket-Protocol": "bar",
|
||||
},
|
||||
request, response = app.test_client.websocket("/ws", subprotocols=["bar"])
|
||||
assert response.opened is True
|
||||
assert results == ["bar"]
|
||||
|
||||
request, response = app.test_client.websocket(
|
||||
"/ws", subprotocols=["bar", "foo"]
|
||||
)
|
||||
assert response.status == 101
|
||||
assert response.opened is True
|
||||
assert results == ["bar", "bar"]
|
||||
|
||||
request, response = app.test_client.get(
|
||||
"/ws",
|
||||
headers={
|
||||
"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", subprotocols=["baz"])
|
||||
assert response.opened is True
|
||||
assert results == ["bar", "bar", None]
|
||||
|
||||
request, response = app.test_client.websocket("/ws")
|
||||
assert response.opened is True
|
||||
assert results == ["bar", "bar", None, None]
|
||||
|
||||
|
||||
@@ -538,16 +523,8 @@ def test_add_webscoket_route(app, strict_slashes):
|
||||
ev.set()
|
||||
|
||||
app.add_websocket_route(handler, "/ws", strict_slashes=strict_slashes)
|
||||
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 ev.is_set()
|
||||
|
||||
|
||||
@@ -672,9 +649,18 @@ def test_dynamic_add_route_number(app):
|
||||
request, response = app.test_client.get("/weight/1234.56")
|
||||
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")
|
||||
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):
|
||||
async def handler(request, folder_id):
|
||||
|
||||
@@ -4,6 +4,7 @@ import pytest
|
||||
|
||||
from sanic.testing import HOST, PORT
|
||||
|
||||
|
||||
AVAILABLE_LISTENERS = [
|
||||
"before_server_start",
|
||||
"after_server_start",
|
||||
@@ -75,6 +76,7 @@ def test_all_listeners(app):
|
||||
assert app.name + listener_name == output.pop()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_before_events_create_server(app):
|
||||
class MySanicDb:
|
||||
pass
|
||||
@@ -83,7 +85,7 @@ async def test_trigger_before_events_create_server(app):
|
||||
async def init_db(app, loop):
|
||||
app.db = MySanicDb()
|
||||
|
||||
await app.create_server(debug=True, return_asyncio_server=True)
|
||||
await app.create_server(debug=True, return_asyncio_server=True, port=PORT)
|
||||
|
||||
assert hasattr(app, "db")
|
||||
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.testing import HOST, PORT
|
||||
from unittest.mock import MagicMock
|
||||
import asyncio
|
||||
from queue import Queue
|
||||
|
||||
|
||||
async def stop(app, loop):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import inspect
|
||||
import os
|
||||
|
||||
from time import gmtime, strftime
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import socket
|
||||
|
||||
from sanic.testing import PORT, SanicTestClient
|
||||
from sanic.response import json, text
|
||||
from sanic.testing import PORT, SanicTestClient
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# UTF-8
|
||||
@@ -9,26 +10,26 @@ from sanic.response import json, text
|
||||
|
||||
|
||||
def test_test_client_port_none(app):
|
||||
@app.get('/get')
|
||||
@app.get("/get")
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
return text("OK")
|
||||
|
||||
test_client = SanicTestClient(app, port=None)
|
||||
|
||||
request, response = test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
request, response = test_client.get("/get")
|
||||
assert response.text == "OK"
|
||||
|
||||
request, response = test_client.post('/get')
|
||||
request, response = test_client.post("/get")
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_test_client_port_default(app):
|
||||
@app.get('/get')
|
||||
@app.get("/get")
|
||||
def handler(request):
|
||||
return json(request.transport.get_extra_info('sockname')[1])
|
||||
return json(request.transport.get_extra_info("sockname")[1])
|
||||
|
||||
test_client = SanicTestClient(app)
|
||||
assert test_client.port == PORT
|
||||
|
||||
request, response = test_client.get('/get')
|
||||
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
|
||||
|
||||
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_VALUE1 = "/myurl?arg1=v1&arg1=v2"
|
||||
URL_FOR_ARGS2 = dict(arg1=["v1", "v2"], _anchor="anchor")
|
||||
@@ -170,7 +173,7 @@ def test_fails_with_int_message(app):
|
||||
|
||||
expected_error = (
|
||||
r'Value "not_int" for parameter `foo` '
|
||||
r'does not match pattern for type `int`: -?\d+'
|
||||
r"does not match pattern for type `int`: -?\d+"
|
||||
)
|
||||
assert str(e.value) == expected_error
|
||||
|
||||
@@ -223,7 +226,7 @@ def test_fails_with_number_message(app):
|
||||
|
||||
expected_error = (
|
||||
'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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from json import dumps as json_dumps
|
||||
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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.request import Request
|
||||
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)
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import time
|
||||
import asyncio
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from unittest import mock
|
||||
from sanic.worker import GunicornWorker
|
||||
from sanic.app import Sanic
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.worker import GunicornWorker
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def gunicorn_worker():
|
||||
|
||||
23
tox.ini
23
tox.ini
@@ -1,24 +1,27 @@
|
||||
[tox]
|
||||
envlist = py35, py36, py37, {py35,py36,py37}-no-ext, lint, check
|
||||
envlist = py36, py37, {py36,py37}-no-ext, lint, check, security
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
setenv =
|
||||
{py35,py36,py37}-no-ext: SANIC_NO_UJSON=1
|
||||
{py35,py36,py37}-no-ext: SANIC_NO_UVLOOP=1
|
||||
{py36,py37}-no-ext: SANIC_NO_UJSON=1
|
||||
{py36,py37}-no-ext: SANIC_NO_UVLOOP=1
|
||||
deps =
|
||||
coverage
|
||||
pytest==4.1.0
|
||||
pytest-cov
|
||||
pytest-sanic
|
||||
pytest-sugar
|
||||
aiohttp>=2.3,<=3.2.1
|
||||
httpcore==0.3.0
|
||||
requests-async==0.5.0
|
||||
chardet<=2.3.0
|
||||
beautifulsoup4
|
||||
gunicorn
|
||||
pytest-benchmark
|
||||
uvicorn
|
||||
websockets>=6.0,<7.0
|
||||
commands =
|
||||
pytest tests --cov sanic --cov-report= {posargs}
|
||||
pytest {posargs:tests --cov sanic}
|
||||
- coverage combine --append
|
||||
coverage report -m
|
||||
coverage html -i
|
||||
@@ -28,10 +31,11 @@ deps =
|
||||
flake8
|
||||
black
|
||||
isort
|
||||
bandit
|
||||
|
||||
commands =
|
||||
flake8 sanic
|
||||
black --check --verbose sanic
|
||||
black --config ./.black.toml --check --verbose sanic/
|
||||
isort --check-only --recursive sanic
|
||||
|
||||
[testenv:check]
|
||||
@@ -44,3 +48,10 @@ commands =
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore:.*async with lock.* instead:DeprecationWarning
|
||||
|
||||
[testenv:security]
|
||||
deps =
|
||||
bandit
|
||||
|
||||
commands =
|
||||
bandit --recursive sanic --skip B404,B101 --exclude sanic/reloader_helpers.py
|
||||
|
||||
Reference in New Issue
Block a user