Compare commits

..

1 Commits

Author SHA1 Message Date
Harsha Narayana
d758f7c6df GIT1505: Backport changes from #1502 to 18.12LTS (#1507)
* GIT1505: backport changes from #1502 to 18.12LTS

* GIT1505: fix pytest version to address UT failures

* fix: GIT1505: fix unittests with caplog and some other warnings

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-03-06 08:36:03 -06:00
106 changed files with 3540 additions and 6746 deletions

1
.gitignore vendored
View File

@@ -15,4 +15,3 @@ docs/_build/
docs/_api/
build/*
.DS_Store
dist/*

View File

@@ -1,39 +1,3 @@
Version 18.12
-------------
18.12.0
- Changes:
- Improved codebase test coverage from 81% to 91%.
- Added stream_large_files and host examples in static_file document
- Added methods to append and finish body content on Request (#1379)
- Integrated with .appveyor.yml for windows ci support
- Added documentation for AF_INET6 and AF_UNIX socket usage
- Adopt black/isort for codestyle
- Cancel task when connection_lost
- Simplify request ip and port retrieval logic
- Handle config error in load config file.
- Integrate with codecov for CI
- Add missed documentation for config section.
- Deprecate Handler.log
- Pinned httptools requirement to version 0.0.10+
- Fixes:
- Fix `remove_entity_headers` helper function (#1415)
- Fix TypeError when use Blueprint.group() to group blueprint with default url_prefix, Use os.path.normpath to avoid invalid url_prefix like api//v1
f8a6af1 Rename the `http` module to `helpers` to prevent conflicts with the built-in Python http library (fixes #1323)
- Fix unittests on windows
- Fix Namespacing of sanic logger
- Fix missing quotes in decorator example
- Fix redirect with quoted param
- Fix doc for latest blueprint code
- Fix build of latex documentation relating to markdown lists
- Fix loop exception handling in app.py
- Fix content length mismatch in windows and other platform
- Fix Range header handling for static files (#1402)
- Fix the logger and make it work (#1397)
- Fix type pikcle->pickle in multiprocessing test
- Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
- Fix document for logging
Version 0.8
-----------
0.8.3

View File

@@ -18,22 +18,9 @@ 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]"
python setup.py develop && pip install -r requirements-dev.txt
```
# Dependency Changes
`Sanic` doesn't use `requirements*.txt` files to manage any kind of dependencies related to it in order to simplify the
effort required in managing the dependencies. Please make sure you have read and understood the following section of
the document that explains the way `sanic` manages dependencies inside the `setup.py` file.
| 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]' |
## Running tests
To run the tests for sanic it is recommended to use tox like so:

View File

@@ -1,15 +1,7 @@
# Non Code related contents
include LICENSE
include README.rst
include pyproject.toml
# Setup
include MANIFEST.in
include LICENSE
include setup.py
include Makefile
# Tests
include .coveragerc
graft tests
global-exclude __pycache__
global-exclude *.py[co]
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

View File

@@ -1,58 +1,4 @@
.PHONY: help test test-coverage install docker-test black fix-import beautify
.DEFAULT: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo "test"
@echo " Run Sanic Unit Tests"
@echo "test-coverage"
@echo " Run Sanic Unit Tests with Coverage"
@echo "install"
@echo " Install Sanic"
@echo "docker-test"
@echo " Run Sanic Unit Tests using Docker"
@echo "black"
@echo " Analyze and fix linting issues using Black"
@echo "fix-import"
@echo " Analyze and fix import order using isort"
@echo "beautify [sort_imports=1] [include_tests=1]"
@echo " Analyze and fix linting issue using black and optionally fix import sort using isort"
@echo ""
clean:
find . ! -path "./.eggs/*" -name "*.pyc" -exec rm {} \;
find . ! -path "./.eggs/*" -name "*.pyo" -exec rm {} \;
find . ! -path "./.eggs/*" -name ".coverage" -exec rm {} \;
rm -rf build/* > /dev/null 2>&1
rm -rf dist/* > /dev/null 2>&1
test: clean
python setup.py test
test-coverage: clean
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
install:
python setup.py install
docker-test: clean
test:
find . -name "*.pyc" -delete
docker build -t sanic/test-image -f docker/Dockerfile .
docker run -t sanic/test-image tox
beautify: black
ifdef sort_imports
ifdef include_tests
$(warning It is suggested that you do not run sort import on tests)
isort -rc sanic tests
else
$(info Sorting Imports)
isort -rc sanic
endif
endif
black:
black --config ./pyproject.toml sanic tests
fix-import: black
isort -rc sanic

View File

@@ -1,71 +1,15 @@
.. image:: https://raw.githubusercontent.com/huge-success/sanic-assets/master/png/sanic-framework-logo-400x97.png
:alt: Sanic | Build fast. Run fast.
Sanic
=====
Sanic | Build fast. Run fast.
=============================
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |AppVeyor Build Status| |Documentation| |Codecov| |PyPI| |PyPI version| |Code style black|
.. start-badges
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/>`_.
.. list-table::
:stub-columns: 1
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.
* - Build
- | |Build Status| |AppVeyor Build Status| |Codecov|
* - Docs
- |Documentation|
* - Package
- | |PyPI| |PyPI version| |Wheel| |Supported implementations| |Code style black|
* - Support
- | |Forums| |Join the chat at https://gitter.im/sanic-python/Lobby|
.. |Forums| image:: https://img.shields.io/badge/forums-community-ff0068.svg
:target: https://community.sanicframework.org/
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/huge-success/sanic
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
:target: https://travis-ci.org/huge-success/sanic
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
:target: https://ci.appveyor.com/project/huge-success/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
.. |Wheel| image:: https://img.shields.io/pypi/wheel/sanic.svg
:alt: PyPI Wheel
:target: https://pypi.python.org/pypi/sanic
.. |Supported implementations| image:: https://img.shields.io/pypi/implementation/sanic.svg
:alt: Supported implementations
:target: https://pypi.python.org/pypi/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.
`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 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.
Installation
------------
``pip3 install sanic``
Sanic makes use of ``uvloop`` and ``ujson`` to help with performance. If you do not want to use those packages, simply add an environmental variable ``SANIC_NO_UVLOOP=true`` or ``SANIC_NO_UJSON=true`` at install time.
.. code:: shell
$ export SANIC_NO_UVLOOP=true
$ export SANIC_NO_UJSON=true
$ pip3 install sanic
Sanic is developed `on GitHub <https://github.com/huge-success/sanic/>`_. We also have `a community discussion board <https://community.sanicframework.org/>`_. Contributions are welcome!
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/huge-success/sanic/issues/396>`_ that we use to track those projects!
Hello World Example
-------------------
@@ -84,26 +28,16 @@ Hello World Example
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
Sanic can now be easily run using ``python3 hello.py``.
Installation
------------
.. code::
- ``pip install sanic``
[2018-12-30 11:37:41 +0200] [13564] [INFO] Goin' Fast @ http://0.0.0.0:8000
[2018-12-30 11:37:41 +0200] [13564] [INFO] Starting worker [13564]
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 NO_X to true will stop that features
installation.
And, we can verify it is working: ``curl localhost:8000 -i``
.. code::
HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: 5
Content-Length: 17
Content-Type: application/json
{"hello":"world"}
**Now, let's go build something fast!**
- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip install sanic``
Documentation
@@ -111,18 +45,59 @@ Documentation
`Documentation on Readthedocs <http://sanic.readthedocs.io/>`_.
Changelog
---------
`Release Changelogs <https://github.com/huge-success/sanic/blob/master/CHANGELOG.md>`_.
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/huge-success/sanic
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
:target: https://travis-ci.org/huge-success/sanic
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
:target: https://ci.appveyor.com/project/huge-success/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
Questions and Discussion
------------------------
`Ask a question or join the conversation <https://community.sanicframework.org/>`_.
Contribution
------------
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/huge-success/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/huge-success/sanic/blob/master/CONTRIBUTING.md>`_.
Examples
--------
`Non-Core examples <https://github.com/huge-success/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
`Extensions <https://github.com/huge-success/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
`Projects <https://github.com/huge-success/sanic/wiki/Projects/>`_. Sanic in production use.
Final Thoughts
--------------
::
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀

View File

@@ -38,7 +38,7 @@ master_doc = 'index'
# General information about the project.
project = 'Sanic'
copyright = '2018, Sanic contributors'
copyright = '2016, Sanic contributors'
author = 'Sanic contributors'
# The version info for the project you're documenting, acts as replacement for

View File

@@ -7,33 +7,30 @@ Guides
:maxdepth: 2
sanic/getting_started
sanic/config
sanic/logging
sanic/routing
sanic/request_data
sanic/response
sanic/cookies
sanic/routing
sanic/blueprints
sanic/static_files
sanic/versioning
sanic/exceptions
sanic/middleware
sanic/blueprints
sanic/websocket
sanic/config
sanic/cookies
sanic/decorators
sanic/streaming
sanic/class_based_views
sanic/custom_protocol
sanic/sockets
sanic/ssl
sanic/logging
sanic/versioning
sanic/debug_mode
sanic/testing
sanic/deploying
sanic/extensions
sanic/examples
sanic/changelog
sanic/contributing
sanic/api_reference
sanic/asyncio_python37
Module Documentation

View File

@@ -20,15 +20,6 @@ sanic.blueprints module
:undoc-members:
:show-inheritance:
sanic.blueprint_group module
----------------------------
.. automodule:: sanic.blueprint_group
:members:
:undoc-members:
:show-inheritance:
sanic.config module
-------------------

View File

@@ -1,58 +0,0 @@
Python 3.7 AsyncIO examples
###########################
With Python 3.7 AsyncIO got major update for the following types:
- asyncio.AbstractEventLoop
- asyncio.AnstractServer
This example shows how to use sanic with Python 3.7, to be precise: how to retrieve an asyncio server instance:
.. code:: python
import asyncio
import socket
import os
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route("/")
async def test(request):
return json({"hello": "world"})
server_socket = '/tmp/sanic.sock'
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
os.remove(server_socket)
finally:
sock.bind(server_socket)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
srv_coro = app.create_server(
sock=sock,
return_asyncio_server=True,
asyncio_server_kwargs=dict(
start_serving=False
)
)
srv = loop.run_until_complete(srv_coro)
try:
assert srv.is_serving() is False
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
loop.run_until_complete(srv.serve_forever())
except KeyboardInterrupt:
srv.close()
loop.close()
Please note that uvloop does not support these features yet.

View File

@@ -118,16 +118,16 @@ app = Sanic(__name__)
app.blueprint(api)
```
## Using Blueprints
## Using blueprints
Blueprints have almost the same functionality as an application instance.
Blueprints have much the same functionality as an application instance.
### WebSocket routes
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
decorator or `bp.add_websocket_route` method.
### Blueprint Middleware
### Middleware
Using blueprints allows you to also register middleware globally.
@@ -145,36 +145,6 @@ async def halt_response(request, response):
return text('I halted the response')
```
### Blueprint Group Middleware
Using this middleware will ensure that you can apply a common middleware to all the blueprints that form the
current blueprint group under consideration.
```python
bp1 = Blueprint('bp1', url_prefix='/bp1')
bp2 = Blueprint('bp2', url_prefix='/bp2')
@bp1.middleware('request')
async def bp1_only_middleware(request):
print('applied on Blueprint : bp1 Only')
@bp1.route('/')
async def bp1_route(request):
return text('bp1')
@bp2.route('/<param>')
async def bp2_route(request, param):
return text(param)
group = Blueprint.group(bp1, bp2)
@group.middleware('request')
async def group_middleware(request):
print('common middleware applied for both bp1 and bp2')
# Register Blueprint group under the app
app.blueprint(group)
```
### Exceptions
Exceptions can be applied exclusively to blueprints globally.
@@ -231,7 +201,7 @@ async def close_connection(app, loop):
Blueprints can be very useful for API versioning, where one blueprint may point
at `/v1/<routes>`, and another pointing at `/v2/<routes>`.
When a blueprint is initialised, it can take an optional `version` argument,
When a blueprint is initialised, it can take an optional `url_prefix` argument,
which will be prepended to all routes defined on the blueprint. This feature
can be used to implement our API versioning scheme.
@@ -240,8 +210,8 @@ can be used to implement our API versioning scheme.
from sanic.response import text
from sanic import Blueprint
blueprint_v1 = Blueprint('v1', url_prefix='/api', version="v1")
blueprint_v2 = Blueprint('v2', url_prefix='/api', version="v2")
blueprint_v1 = Blueprint('v1', url_prefix='/v1')
blueprint_v2 = Blueprint('v2', url_prefix='/v2')
@blueprint_v1.route('/')
async def api_v1_root(request):
@@ -252,7 +222,7 @@ async def api_v2_root(request):
return text('Welcome to version 2 of our documentation')
```
When we register our blueprints on the app, the routes `/v1/api` and `/v2/api` will now
When we register our blueprints on the app, the routes `/v1` and `/v2` will now
point to the individual blueprints, which allows the creation of *sub-sites*
for each API version.
@@ -262,8 +232,8 @@ from sanic import Sanic
from blueprints import blueprint_v1, blueprint_v2
app = Sanic(__name__)
app.blueprint(blueprint_v1)
app.blueprint(blueprint_v2)
app.blueprint(blueprint_v1, url_prefix='/v1')
app.blueprint(blueprint_v2, url_prefix='/v2')
app.run(host='0.0.0.0', port=8000, debug=True)
```
@@ -276,7 +246,7 @@ takes the format `<blueprint_name>.<handler_name>`. For example:
```python
@blueprint_v1.route('/')
async def root(request):
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/api/post/5'
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
return redirect(url)

View File

@@ -1,135 +0,0 @@
Version 18.12
-------------
18.12.0
- Changes:
- Improved codebase test coverage from 81% to 91%.
- Added stream_large_files and host examples in static_file document
- Added methods to append and finish body content on Request (#1379)
- Integrated with .appveyor.yml for windows ci support
- Added documentation for AF_INET6 and AF_UNIX socket usage
- Adopt black/isort for codestyle
- Cancel task when connection_lost
- Simplify request ip and port retrieval logic
- Handle config error in load config file.
- Integrate with codecov for CI
- Add missed documentation for config section.
- Deprecate Handler.log
- Pinned httptools requirement to version 0.0.10+
- Fixes:
- Fix `remove_entity_headers` helper function (#1415)
- Fix TypeError when use Blueprint.group() to group blueprint with default url_prefix, Use os.path.normpath to avoid invalid url_prefix like api//v1
f8a6af1 Rename the `http` module to `helpers` to prevent conflicts with the built-in Python http library (fixes #1323)
- Fix unittests on windows
- Fix Namespacing of sanic logger
- Fix missing quotes in decorator example
- Fix redirect with quoted param
- Fix doc for latest blueprint code
- Fix build of latex documentation relating to markdown lists
- Fix loop exception handling in app.py
- Fix content length mismatch in windows and other platform
- Fix Range header handling for static files (#1402)
- Fix the logger and make it work (#1397)
- Fix type pikcle->pickle in multiprocessing test
- Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
- Fix document for logging
Version 0.8
-----------
0.8.3
- Changes:
- Ownership changed to org 'huge-success'
0.8.0
- Changes:
- Add Server-Sent Events extension (Innokenty Lebedev)
- Graceful handling of request_handler_task cancellation (Ashley Sommer)
- Sanitize URL before redirection (aveao)
- Add url_bytes to request (johndoe46)
- py37 support for travisci (yunstanford)
- Auto reloader support for OSX (garyo)
- Add UUID route support (Volodymyr Maksymiv)
- Add pausable response streams (Ashley Sommer)
- Add weakref to request slots (vopankov)
- remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
- Allow streaming handlers in add_route (kinware)
- use travis_retry for tox (Raphael Deem)
- update aiohttp version for test client (yunstanford)
- add redirect import for clarity (yingshaoxo)
- Update HTTP Entity headers (Arnulfo Solís)
- Add register_listener method (Stephan Fitzpatrick)
- Remove uvloop/ujson dependencies for Windows (abuckenheimer)
- Content-length header on 204/304 responses (Arnulfo Solís)
- Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
- Update development status from pre-alpha to beta (Maksim Anisenkov)
- KeepAlive Timout log level changed to debug (Arnulfo Solís)
- Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
- Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
- Add support for blueprint groups and nesting (Elias Tarhini)
- Remove uvloop for windows setup (Aleksandr Kurlov)
- Auto Reload (Yaser Amari)
- Documentation updates/fixups (multiple contributors)
- Fixes:
- Fix: auto_reload in Linux (Ashley Sommer)
- Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
- Fix: disable auto_reload by default on windows (abuckenheimer)
- Fix (1143): Turn off access log with gunicorn (hqy)
- Fix (1268): Support status code for file response (Cosmo Borsky)
- Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
- Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
- Fix (1242): Responses for CI header (yunstanford)
- Fix (1237): add version constraint for websockets (yunstanford)
- Fix (1231): memory leak - always release resource (Phillip Xu)
- Fix (1221): make request truthy if transport exists (Raphael Deem)
- Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
- Fix try_everything examples (PyManiacGR, kot83)
- Fix (1158): default to auto_reload in debug mode (Raphael Deem)
- Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
- Fix: raw requires bytes-like object (cloudship)
- Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
- Fix: Bug in multipart/form-data parser (DirkGuijt)
- Fix: Exception for missing parameter when value is null (NyanKiyoshi)
- Fix: Parameter check (Howie Hu)
- Fix (1089): Routing issue with named parameters and different methods (yunstanford)
- Fix (1085): Signal handling in multi-worker mode (yunstanford)
- Fix: single quote in readme.rst (Cosven)
- Fix: method typos (Dmitry Dygalo)
- Fix: log_response correct output for ip and port (Wibowo Arindrarto)
- Fix (1042): Exception Handling (Raphael Deem)
- Fix: Chinese URIs (Howie Hu)
- Fix (1079): timeout bug when self.transport is None (Raphael Deem)
- Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
- Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
- Fix (1065): allow add_task after server starts (Raphael Deem)
- Fix (1061): double quotes in unauthorized exception (Raphael Deem)
- Fix (1062): inject the app in add_task method (Raphael Deem)
- Fix: update environment.yml for readthedocs (Eli Uriegas)
- Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
- Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
- Fix: IPv6 Address and Socket Data Format (Dan Palmer)
Note: Changelog was unmaintained between 0.1 and 0.7
Version 0.1
-----------
- 0.1.7
- Reversed static url and directory arguments to meet spec
- 0.1.6
- Static files
- Lazy Cookie Loading
- 0.1.5
- Cookies
- Blueprint listeners and ordering
- Faster Router
- Fix: Incomplete file reads on medium+ sized post requests
- Breaking: after_start and before_stop now pass sanic as their first argument
- 0.1.4
- Multiprocessing
- 0.1.3
- Blueprint support
- Faster Response processing
- 0.1.1 - 0.1.2
- Struggling to update pypi via CI
- 0.1.0
- Released to public

View File

@@ -86,52 +86,28 @@ 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) |
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long take to force close non-idle connection (sec) |
| ACCESS_LOG | True | Disable or enable access log |
### The different Timeout variables:
#### `REQUEST_TIMEOUT`
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the `REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates a HTTP 408 response and sends that to the client. Adjust this value higher if your clients routinely pass very large request payloads or upload requests very slowly.
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the
Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the
`REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates an `HTTP 408` response
and sends that to the client. Set this parameter's value higher if your clients routinely pass very large request payloads
or upload requests very slowly.
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` value (in seconds), this is considered a Server Error so Sanic generates a HTTP 503 response and sets that to the client. Adjust this value higher if your application is likely to have long-running process that delay the generation of a response.
#### `RESPONSE_TIMEOUT`
### What is Keep Alive? And what does the Keep Alive Timeout value do?
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the
Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT`
value (in seconds), this is considered a Server Error so Sanic generates an `HTTP 503` response and sends that to the
client. Set this parameter's value higher if your application is likely to have long-running process that delay the
generation of a response.
Keep-Alive is a HTTP feature indroduced in HTTP 1.1. When sending a HTTP request, the client (usually a web browser application) can set a Keep-Alive header to indicate for the http server (Sanic) to not close the TCP connection after it has send the response. This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient network traffic for both the client and the server.
#### `KEEP_ALIVE_TIMEOUT`
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, set it to `False` to cause all client connections to close immediately after a response is sent, regardless of the Keep-Alive header on the request.
##### What is Keep Alive? And what does the Keep Alive Timeout value do?
`Keep-Alive` is a HTTP feature introduced in `HTTP 1.1`. When sending a HTTP request, the client (usually a web browser application)
can set a `Keep-Alive` header to indicate the http server (Sanic) to not close the TCP connection after it has send the response.
This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient
network traffic for both the client and the server.
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application,
set it to `False` to cause all client connections to close immediately after a response is sent, regardless of
the `Keep-Alive` header on the request.
The amount of time the server holds the TCP connection open is decided by the server itself.
In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds.
This is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for
the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless
you know your clients are using a browser which supports TCP connections held open for that long.
The amount of time the server holds the TCP connection open is decided by the server itself. In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds, this is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless you know your clients are using a browser which supports TCP connections held open for that long.
For reference:
```

View File

@@ -0,0 +1,62 @@
# Contributing
Thank you for your interest! Sanic is always looking for contributors. If you
don't feel comfortable contributing code, adding docstrings to the source files
is very appreciated.
## Installation
To develop on sanic (and mainly to just run the tests) it is highly recommend to
install from sources.
So assume you have already cloned the repo and are in the working directory with
a virtual environment already set up, then run:
```bash
python setup.py develop && pip install -r requirements-dev.txt
```
## Running tests
To run the tests for sanic it is recommended to use tox like so:
```bash
tox
```
See it's that simple!
## Pull requests!
So the pull request approval rules are pretty simple:
* All pull requests must pass unit tests
* All pull requests must be reviewed and approved by at least
one current collaborator on the project
* All pull requests must pass flake8 checks
* If you decide to remove/change anything from any common interface
a deprecation message should accompany it.
* If you implement a new feature you should have at least one unit
test to accompany it.
## Documentation
Sanic's documentation is built
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
Markdown and can be found in the `docs` folder, while the module reference is
automatically generated using `sphinx-apidoc`.
To generate the documentation from scratch:
```bash
sphinx-apidoc -fo docs/_api/ sanic
sphinx-build -b html docs docs/_build
```
The HTML documentation will be created in the `docs/_build` folder.
## Warning
One of the main goals of Sanic is speed. Code that lowers the performance of
Sanic without significant gains in usability, security, or features may not be
merged. Please don't let this intimidate you! If you have any concerns about an
idea, open an issue for discussion and help.

View File

@@ -1,89 +0,0 @@
Contributing
============
Thank you for your interest! Sanic is always looking for contributors.
If you dont feel comfortable contributing code, adding docstrings to
the source files is very appreciated.
Installation
------------
To develop on sanic (and mainly to just run the tests) it is highly
recommend to install from sources.
So assume you have already cloned the repo and are in the working
directory with a virtual environment already set up, then run:
.. code:: bash
pip3 install -e '.[dev]'
Dependency Changes
------------------
``Sanic`` doesn't use ``requirements*.txt`` files to manage any kind of dependencies related to it in order to simplify the
effort required in managing the dependencies. Please make sure you have read and understood the following section of
the document that explains the way ``sanic`` manages dependencies inside the ``setup.py`` file.
+------------------------+-----------------------------------------------+--------------------------------+
| Dependency Type | Usage | Installation |
+========================+===============================================+================================+
| requirements | Bare minimum dependencies required for sanic | ``pip3 install -e .`` |
| | to function | |
+------------------------+-----------------------------------------------+--------------------------------+
| tests_require / | Dependencies required to run the Unit Tests | ``pip3 install -e '.[test]'`` |
| extras_require['test'] | for ``sanic`` | |
+------------------------+-----------------------------------------------+--------------------------------+
| extras_require['dev'] | Additional Development requirements to add | ``pip3 install -e '.[dev]'`` |
| | for contributing | |
+------------------------+-----------------------------------------------+--------------------------------+
| extras_require['docs'] | Dependencies required to enable building and | ``pip3 install -e '.[docs]'`` |
| | enhancing sanic documentation | |
+------------------------+-----------------------------------------------+--------------------------------+
Running tests
-------------
To run the tests for sanic it is recommended to use tox like so:
.. code:: bash
tox
See its that simple!
Pull requests!
--------------
So the pull request approval rules are pretty simple:
* All pull requests must pass unit tests
* All pull requests must be reviewed and approved by at least one current collaborator on the project
* All pull requests must pass flake8 checks
* If you decide to remove/change anything from any common interface a deprecation message should accompany it.
* If you implement a new feature you should have at least one unit test to accompany it.
Documentation
-------------
Sanics documentation is built using `sphinx`_. Guides are written in
Markdown and can be found in the ``docs`` folder, while the module
reference is automatically generated using ``sphinx-apidoc``.
To generate the documentation from scratch:
.. code:: bash
sphinx-apidoc -fo docs/_api/ sanic
sphinx-build -b html docs docs/_build
The HTML documentation will be created in the ``docs/_build`` folder.
.. warning::
One of the main goals of Sanic is speed. Code that lowers the
performance of Sanic without significant gains in usability, security,
or features may not be merged. Please dont let this intimidate you! If
you have any concerns about an idea, open an issue for discussion and
help.
.. _sphinx: http://www.sphinx-doc.org/en/1.5.1/

View File

@@ -0,0 +1,72 @@
# Custom Protocols
*Note: this is advanced usage, and most readers will not need such functionality.*
You can change the behavior of Sanic's protocol by specifying a custom
protocol, which should be a subclass
of
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
This protocol can then be passed as the keyword argument `protocol` to the `sanic.run` method.
The constructor of the custom protocol class receives the following keyword
arguments from Sanic.
- `loop`: an `asyncio`-compatible event loop.
- `connections`: a `set` to store protocol objects. When Sanic receives
`SIGINT` or `SIGTERM`, it executes `protocol.close_if_idle` for all protocol
objects stored in this set.
- `signal`: a `sanic.server.Signal` object with the `stopped` attribute. When
Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` is assigned `True`.
- `request_handler`: a coroutine that takes a `sanic.request.Request` object
and a `response` callback as arguments.
- `error_handler`: a `sanic.exceptions.Handler` which is called when exceptions
are raised.
- `request_timeout`: the number of seconds before a request times out.
- `request_max_size`: an integer specifying the maximum size of a request, in bytes.
## Example
An error occurs in the default protocol if a handler function does not return
an `HTTPResponse` object.
By overriding the `write_response` protocol method, if a handler returns a
string it will be converted to an `HTTPResponse object`.
```python
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
app = Sanic(__name__)
class CustomHttpProtocol(HttpProtocol):
def __init__(self, *, loop, request_handler, error_handler,
signal, connections, request_timeout, request_max_size):
super().__init__(
loop=loop, request_handler=request_handler,
error_handler=error_handler, signal=signal,
connections=connections, request_timeout=request_timeout,
request_max_size=request_max_size)
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/')
async def string(request):
return 'string'
@app.route('/1')
async def response(request):
return text('response')
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
```

View File

@@ -1,76 +0,0 @@
Custom Protocols
================
.. note::
This is advanced usage, and most readers will not need such functionality.
You can change the behavior of Sanic's protocol by specifying a custom
protocol, which should be a subclass
of `asyncio.protocol <https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes>`_.
This protocol can then be passed as the keyword argument ``protocol`` to the ``sanic.run`` method.
The constructor of the custom protocol class receives the following keyword
arguments from Sanic.
- ``loop``: an ``asyncio``-compatible event loop.
- ``connections``: a ``set`` to store protocol objects. When Sanic receives
``SIGINT`` or ``SIGTERM``, it executes ``protocol.close_if_idle`` for all protocol
objects stored in this set.
- ``signal``: a ``sanic.server.Signal`` object with the ``stopped`` attribute. When
Sanic receives ``SIGINT`` or ``SIGTERM``, ``signal.stopped`` is assigned ``True``.
- ``request_handler``: a coroutine that takes a ``sanic.request.Request`` object
and a ``response`` callback as arguments.
- ``error_handler``: a ``sanic.exceptions.Handler`` which is called when exceptions
are raised.
- ``request_timeout``: the number of seconds before a request times out.
- ``request_max_size``: an integer specifying the maximum size of a request, in bytes.
Example
-------
An error occurs in the default protocol if a handler function does not return
an ``HTTPResponse`` object.
By overriding the ``write_response`` protocol method, if a handler returns a
string it will be converted to an ``HTTPResponse object``.
.. code:: python
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
app = Sanic(__name__)
class CustomHttpProtocol(HttpProtocol):
def __init__(self, *, loop, request_handler, error_handler,
signal, connections, request_timeout, request_max_size):
super().__init__(
loop=loop, request_handler=request_handler,
error_handler=error_handler, signal=signal,
connections=connections, request_timeout=request_timeout,
request_max_size=request_max_size)
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/')
async def string(request):
return 'string'
@app.route('/1')
async def response(request):
return text('response')
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)

View File

@@ -15,7 +15,6 @@ keyword arguments:
- `protocol` *(default `HttpProtocol`)*: Subclass
of
[asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes).
- `access_log` *(default `True`)*: Enables log on handling requests (significantly slows server).
## Workers
@@ -64,26 +63,6 @@ of the memory leak.
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
## Disable debug logging
To improve the performance add `debug=False` and `access_log=False` in the `run` arguments.
```python
app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False)
```
Running via Gunicorn you can set Environment variable `SANIC_ACCESS_LOG="False"`
```
env SANIC_ACCESS_LOG="False" gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning
```
Or you can rewrite app config directly
```python
app.config.ACCESS_LOG = False
```
## Asynchronous support
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
However be advised that this method does not support using multiple processes, and is not the preferred way

View File

@@ -1,167 +0,0 @@
Examples
========
This section of the documentation is a simple collection of example code that can help you get a quick start
on your application development. Most of these examples are categorized and provide you with a link to the
working code example in the `Sanic Repository <https://github.com/huge-success/sanic/tree/master/examples>`_
Basic Examples
--------------
This section of the examples are a collection of code that provide a simple use case example of the sanic application.
Simple Apps
~~~~~~~~~~~~
A simple sanic application with a single ``async`` method with ``text`` and ``json`` type response.
.. literalinclude:: ../../examples/teapot.py
.. literalinclude:: ../../examples/simple_server.py
Simple App with ``Sanic Views``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Showcasing the simple mechanism of using :class:`sanic.viewes.HTTPMethodView` as well as a way to extend the same
into providing a custom ``async`` behavior for ``view``.
.. literalinclude:: ../../examples/simple_async_view.py
URL Redirect
~~~~~~~~~~~~
.. literalinclude:: ../../examples/redirect_example.py
Named URL redirection
~~~~~~~~~~~~~~~~~~~~~
``Sanic`` provides an easy to use way of redirecting the requests via a helper method called ``url_for`` that takes a
unique url name as argument and returns you the actual route assigned for it. This will help in simplifying the
efforts required in redirecting the user between different section of the application.
.. literalinclude:: ../../examples/url_for_example.py
Blueprints
~~~~~~~~~~
``Sanic`` provides an amazing feature to group your APIs and routes under a logical collection that can easily be
imported and plugged into any of your sanic application and it's called ``blueprints``
.. literalinclude:: ../../examples/blueprints.py
Logging Enhancements
~~~~~~~~~~~~~~~~~~~~
Even though ``Sanic`` comes with a battery of Logging support it allows the end users to customize the way logging
is handled in the application runtime.
.. literalinclude:: ../../examples/override_logging.py
The following sample provides an example code that demonstrates the usage of :func:`sanic.app.Sanic.middleware` in order
to provide a mechanism to assign a unique request ID for each of the incoming requests and log them via
`aiotask-context <https://github.com/Skyscanner/aiotask-context>`_.
.. literalinclude:: ../../examples/log_request_id.py
Sanic Streaming Support
~~~~~~~~~~~~~~~~~~~~~~~
``Sanic`` framework comes with in-built support for streaming large files and the following code explains the process
to setup a ``Sanic`` application with streaming support.
.. literalinclude:: ../../examples/request_stream/server.py
Sample Client app to show the usage of streaming application by a client code.
.. literalinclude:: ../../examples/request_stream/client.py
Sanic Concurrency Support
~~~~~~~~~~~~~~~~~~~~~~~~~
``Sanic`` supports the ability to start an app with multiple worker support. However, it's important to be able to limit
the concurrency per process/loop in order to ensure an efficient execution. The following section of the code provides a
brief example of how to limit the concurrency with the help of :class:`asyncio.Semaphore`
.. literalinclude:: ../../examples/limit_concurrency.py
Sanic Deployment via Docker
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Deploying a ``sanic`` app via ``docker`` and ``docker-compose`` is an easy task to achieve and the following example
provides a deployment of the sample ``simple_server.py``
.. literalinclude:: ../../examples/Dockerfile
.. literalinclude:: ../../examples/docker-compose.yml
Monitoring and Error Handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``Sanic`` provides an extendable bare minimum implementation of a global exception handler via
:class:`sanic.handlers.ErrorHandler`. This example shows how to extend it to enable some custom behaviors.
.. literalinclude:: ../../examples/exception_monitoring.py
Monitoring using external Service Providers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* `LogDNA <https://logdna.com/>`_
.. literalinclude:: ../../examples/logdna_example.py
* `RayGun <https://raygun.com/>`_
.. literalinclude:: ../../examples/raygun_example.py
* `Rollbar <https://rollbar.com>`_
.. literalinclude:: ../../examples/rollbar_example.py
* `Sentry <http://sentry.io>`_
.. literalinclude:: ../../examples/sentry_example.py
Security
~~~~~~~~
The following sample code shows a simple decorator based authentication and authorization mechanism that can be setup
to secure your ``sanic`` api endpoints.
.. literalinclude:: ../../examples/authorized_sanic.py
Sanic Websocket
~~~~~~~~~~~~~~~
``Sanic`` provides an ability to easily add a route and map it to a ``websocket`` handlers.
.. literalinclude:: ../../examples/websocket.html
.. literalinclude:: ../../examples/websocket.py
vhost Suppport
~~~~~~~~~~~~~~
.. literalinclude:: ../../examples/vhosts.py
Unit Testing With Parallel Test Run Support
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following example shows you how to get up and running with unit testing ``sanic`` application with parallel test
execution support provided by the ``pytest-xdist`` plugin.
.. literalinclude:: ../../examples/pytest_xdist.py
Amending Request Object
~~~~~~~~~~~~~~~~~~~~~~~
The ``request`` object in ``Sanic`` is a kind of ``dict`` object, this means that ``reqeust`` object can be manipulated as a regular ``dict`` object.
.. literalinclude:: ../../examples/amending_request_object.py
For more examples and useful samples please visit the `Huge-Sanic's GitHub Page <https://github.com/huge-success/sanic/tree/master/examples>`_

View File

@@ -59,7 +59,7 @@ app = Sanic()
app.error_handler.add(Exception, server_error_handler)
```
In some cases, you might want to add some more error handling
In some cases, you might want want to add some more error handling
functionality to what is provided by default. In that case, you
can subclass Sanic's default error handler as such:

View File

@@ -1,75 +1,36 @@
# 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.
- [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
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic.
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for Sanic
- [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
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
- [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.
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic.
- [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
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
`Babel` library
- [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-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
- [sanic-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support.
- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
- [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.
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/))
- [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/))
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
- [Sanic-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.
- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application.
- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.

View File

@@ -7,17 +7,9 @@ syntax, so earlier versions of python won't work.
## 1. Install Sanic
```
pip3 install sanic
python3 -m pip 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`)
to true will stop that features installation.
```bash
SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip3 install sanic
```
## 2. Create a file called `main.py`
```python

100
docs/sanic/logging.md Normal file
View File

@@ -0,0 +1,100 @@
# Logging
Sanic allows you to do different types of logging (access log, error log) on the requests based on the [python3 logging API](https://docs.python.org/3/howto/logging.html). You should have some basic knowledge on python3 logging if you want to create a new configuration.
### Quick Start
A simple example using default settings would be like this:
```python
from sanic import Sanic
from sanic.log import logger
from sanic.response import text
app = Sanic('test')
@app.route('/')
async def test(request):
logger.info('Here is your log')
return text('Hello World!')
if __name__ == "__main__":
app.run(debug=True, access_log=True)
```
After the server is running, you can see some messages looks like:
```
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
```
You can send a request to server and it will print the log messages:
```
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
```
To use your own logging config, simply use `logging.config.dictConfig`, or
pass `log_config` when you initialize `Sanic` app:
```python
app = Sanic('test', log_config=LOGGING_CONFIG)
```
And to close logging, simply assign access_log=False:
```python
if __name__ == "__main__":
app.run(access_log=False)
```
This would skip calling logging functions when handling requests.
And you could even do further in production to gain extra speed:
```python
if __name__ == "__main__":
# disable debug messages
app.run(debug=False, access_log=False)
```
### Configuration
By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS dictionary for configuration.
There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
- sanic.root:<br>
Used to log internal messages.
- sanic.error:<br>
Used to log error logs.
- sanic.access:<br>
Used to log access logs.
#### Log format:
In addition to default parameters provided by python (asctime, levelname, message),
Sanic provides additional parameters for access logger with:
- host (str)<br>
request.ip
- request (str)<br>
request.method + " " + request.url
- status (int)<br>
response.status
- byte (int)<br>
len(response.body)
The default access log format is
```python
%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d
```

View File

@@ -1,103 +0,0 @@
Logging
=======
Sanic allows you to do different types of logging (access log, error
log) on the requests based on the `python3 logging API`_. You should
have some basic knowledge on python3 logging if you want to create a new
configuration.
Quick Start
~~~~~~~~~~~
A simple example using default settings would be like this:
.. code:: python
from sanic import Sanic
from sanic.log import logger
from sanic.response import text
app = Sanic('test')
@app.route('/')
async def test(request):
logger.info('Here is your log')
return text('Hello World!')
if __name__ == "__main__":
app.run(debug=True, access_log=True)
After the server is running, you can see some messages looks like:
::
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
You can send a request to server and it will print the log messages:
::
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
To use your own logging config, simply use
``logging.config.dictConfig``, or pass ``log_config`` when you
initialize ``Sanic`` app:
.. code:: python
app = Sanic('test', log_config=LOGGING_CONFIG)
And to close logging, simply assign access_log=False:
.. code:: python
if __name__ == "__main__":
app.run(access_log=False)
This would skip calling logging functions when handling requests. And
you could even do further in production to gain extra speed:
.. code:: python
if __name__ == "__main__":
# disable debug messages
app.run(debug=False, access_log=False)
Configuration
~~~~~~~~~~~~~
By default, ``log_config`` parameter is set to use
``sanic.log.LOGGING_CONFIG_DEFAULTS`` dictionary for configuration.
There are three ``loggers`` used in sanic, and **must be defined if you
want to create your own logging configuration**:
================ ==============================
Logger Name Usecase
================ ==============================
``sanic.root`` Used to log internal messages.
``sanic.error`` Used to log error logs.
``sanic.access`` Used to log access logs.
================ ==============================
Log format:
^^^^^^^^^^^
In addition to default parameters provided by python (``asctime``,
``levelname``, ``message``), Sanic provides additional parameters for
access logger with:
===================== ========================================== ========
Log Context Parameter Parameter Value Datatype
===================== ========================================== ========
``host`` ``request.ip`` str
``request`` ``request.method`` + " " + ``request.url`` str
``status`` ``response.status`` int
``byte`` ``len(response.body)`` int
===================== ========================================== ========
The default access log format is ``%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d``
.. _python3 logging API: https://docs.python.org/3/howto/logging.html

View File

@@ -36,31 +36,20 @@ this.
```
app = Sanic(__name__)
@app.middleware('request')
async def add_key(request):
# Add a key to request object like dict object
request['foo'] = 'bar'
@app.middleware('response')
async def custom_banner(request, response):
response.headers["Server"] = "Fake-Server"
@app.middleware('response')
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
app.run(host="0.0.0.0", port=8000)
```
The above code will apply the three middleware in order. The first middleware
**add_key** will add a new key `foo` into `request` object. This worked because
`request` object can be manipulated like `dict` object. Then, the second middleware
The above code will apply the two middleware in order. First, the middleware
**custom_banner** will change the HTTP response header *Server* to
*Fake-Server*, and the last middleware **prevent_xss** will add the HTTP
*Fake-Server*, and the second middleware **prevent_xss** will add the HTTP
header for preventing Cross-Site-Scripting (XSS) attacks. These two functions
are invoked *after* a user function returns a response.

View File

@@ -19,8 +19,6 @@ The following variables are accessible as properties on `Request` objects:
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
The request's `query_string` variable holds the unparsed string value.
Property is providing the default parsing strategy. If you would like to change it look to the section below
(`Changing the default parsing rules of the queryset`).
```python
from sanic.response import json
@@ -30,54 +28,9 @@ The following variables are accessible as properties on `Request` objects:
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
```
- `query_args` (list) - On many cases you would need to access the url arguments in
a less packed form. `query_args` is the list of `(key, value)` tuples.
Property is providing the default parsing strategy. If you would like to change it look to the section below
(`Changing the default parsing rules of the queryset`).
For the same previous URL queryset `?key1=value1&key2=value2`, the
`query_args` list would look like `[('key1', 'value1'), ('key2', 'value2')]`.
And in case of the multiple params with the same key like `?key1=value1&key2=value2&key1=value3`
the `query_args` list would look like `[('key1', 'value1'), ('key2', 'value2'), ('key1', 'value3')]`.
The difference between Request.args and Request.query_args
for the queryset `?key1=value1&key2=value2&key1=value3`
```python
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
@app.route("/test_request_args")
async def test_request_args(request):
return json({
"parsed": True,
"url": request.url,
"query_string": request.query_string,
"args": request.args,
"raw_args": request.raw_args,
"query_args": request.query_args,
})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
```
Output
```
{
"parsed":true,
"url":"http:\/\/0.0.0.0:8000\/test_request_args?key1=value1&key2=value2&key1=value3",
"query_string":"key1=value1&key2=value2&key1=value3",
"args":{"key1":["value1","value3"],"key2":["value2"]},
"raw_args":{"key1":"value1","key2":"value2"},
"query_args":[["key1","value1"],["key2","value2"],["key1","value3"]]
}
```
`raw_args` contains only the first entry of `key1`. Will be deprecated in the future versions.
- `raw_args` (dict) - On many cases you would need to access the url arguments in
a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the
`raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
@@ -153,51 +106,6 @@ The following variables are accessible as properties on `Request` objects:
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
## Changing the default parsing rules of the queryset
The default parameters that are using internally in `args` and `query_args` properties to parse queryset:
- `keep_blank_values` (bool): `False` - flag indicating whether blank values in
percent-encoded queries should be treated as blank strings.
A true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
- `strict_parsing` (bool): `False` - flag indicating what to do with parsing errors. If
false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
- `encoding` and `errors` (str): 'utf-8' and 'replace' - specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
If you would like to change that default parameters you could call `get_args` and `get_query_args` methods
with the new values.
For the queryset `/?test1=value1&test2=&test3=value3`:
```python
from sanic.response import json
@app.route("/query_string")
def query_string(request):
args_with_blank_values = request.get_args(keep_blank_values=True)
return json({
"parsed": True,
"url": request.url,
"args_with_blank_values": args_with_blank_values,
"query_string": request.query_string
})
```
The output will be:
```
{
"parsed": true,
"url": "http:\/\/0.0.0.0:8000\/query_string?test1=value1&test2=&test3=value3",
"args_with_blank_values": {"test1": ["value1""], "test2": "", "test3": ["value3"]},
"query_string": "test1=value1&test2=&test3=value3"
}
```
## Accessing values using `get` and `getlist`
The request properties which return a dictionary actually return a subclass of
@@ -218,40 +126,3 @@ args.get('titles') # => 'Post 1'
args.getlist('titles') # => ['Post 1', 'Post 2']
```
## Accessing the handler name with the request.endpoint attribute
The `request.endpoint` attribute holds the handler's name. For instance, the below
route will return "hello".
```python
from sanic.response import text
from sanic import Sanic
app = Sanic()
@app.get("/")
def hello(request):
return text(request.endpoint)
```
Or, with a blueprint it will be include both, separated by a period. For example,
the below route would return foo.bar:
```python
from sanic import Sanic
from sanic import Blueprint
from sanic.response import text
app = Sanic(__name__)
blueprint = Blueprint('foo')
@blueprint.get('/')
async def bar(request):
return text(request.endpoint)
app.blueprint(blueprint)
app.run(host="0.0.0.0", port=8000, debug=True)
```

View File

@@ -164,24 +164,24 @@ url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
# /posts/5?arg_one=one&arg_one=two
```
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not supported now and will be ignored). For example:
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not support now and will be ignored). For example:
```python
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
# /posts/5?arg_one=one#anchor
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
# //server/posts/5?arg_one=one
# _external requires you to pass an argument _server or set SERVER_NAME in app.config if not url will be same as no _external
# _external requires passed argument _server or SERVER_NAME in app.config or url will be same as no _external
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
# http://server/posts/5?arg_one=one
# when specifying _scheme, _external must be True
# you can pass all special arguments at once
# you can pass all special arguments one time
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
```
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be raised.
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown.
## WebSocket routes
@@ -209,7 +209,7 @@ async def feed(request, ws):
app.add_websocket_route(my_websocket_handler, '/feed')
```
Handlers to a WebSocket route are invoked with the request as first argument, and a
Handlers for a WebSocket route are passed the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has `send`
and `recv` methods to send and receive data respectively.
@@ -243,8 +243,7 @@ app.blueprint(bp)
## User defined route name
A custom route name can be used by passing a `name` argument while registering the route which will
override the default route name generated using the `handler.__name__` attribute.
You can pass `name` to change the route name to avoid using the default name (`handler.__name__`).
```python
@@ -306,8 +305,8 @@ def handler(request):
## Build URL for static files
Sanic supports using `url_for` method to build static file urls. In case if the static url
is pointing to a directory, `filename` parameter to the `url_for` can be ignored. q
You can use `url_for` for static file url building now.
If it's for file directly, `filename` can be ignored.
```python

View File

@@ -48,7 +48,7 @@ app.run(host="0.0.0.0", port=8000)
## Virtual Host
The `app.static()` method also support **virtual host**. You can serve your static files with specific **virtual host** with `host` argument. For example:
The `app.static()` method also support **virtual host**. You can serve your static files with spefic **virtual host** with `host` argument. For example:
```python
from sanic import Sanic

View File

@@ -2,7 +2,7 @@
## Request Streaming
Sanic allows you to get request data by stream, as below. When the request ends, `await request.stream.read()` returns `None`. Only post, put and patch decorator have stream argument.
Sanic allows you to get request data by stream, as below. When the request ends, `request.stream.get()` returns `None`. Only post, put and patch decorator have stream argument.
```python
from sanic import Sanic
@@ -22,7 +22,7 @@ class SimpleView(HTTPMethodView):
async def post(self, request):
result = ''
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
result += body.decode('utf-8')
@@ -33,7 +33,7 @@ class SimpleView(HTTPMethodView):
async def handler(request):
async def streaming(response):
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
@@ -42,33 +42,20 @@ async def handler(request):
@bp.put('/bp_stream', stream=True)
async def bp_put_handler(request):
async def bp_handler(request):
result = ''
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
return text(result)
# You can also use `bp.add_route()` with stream argument
async def bp_post_handler(request):
result = ''
while True:
body = await request.stream.read()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
return text(result)
bp.add_route(bp_post_handler, '/bp_stream', methods=['POST'], stream=True)
async def post_handler(request):
result = ''
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
result += body.decode('utf-8')

View File

@@ -59,23 +59,6 @@ the available arguments to aiohttp can be found
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
## Using a random port
If you need to test using a free unpriveleged port chosen by the kernel
instead of the default with `SanicTestClient`, you can do so by specifying
`port=None`. On most systems the port will be in the range 1024 to 65535.
```python
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
from sanic.testing import SanicTestClient
def test_index_returns_200():
request, response = SanicTestClient(app, port=None).get('/')
assert response.status == 200
```
## pytest-sanic
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously.

View File

@@ -1,7 +1,7 @@
WebSocket
=========
Sanic provides an easy to user abstraction on top of `websockets`. To setup a WebSocket:
Sanic supports websockets, to setup a WebSocket:
.. code:: python
@@ -35,7 +35,7 @@ decorator:
app.add_websocket_route(feed, '/feed')
Handlers for a WebSocket route is invoked with the request as first argument, and a
Handlers for a WebSocket route are passed the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has ``send``
and ``recv`` methods to send and receive data respectively.

View File

@@ -1,18 +1,21 @@
name: py36
name: py35
dependencies:
- pip=18.1=py36_0
- python=3.6=0
- setuptools=40.4.3=py36_0
- openssl=1.0.2g=0
- pip=8.1.1=py35_0
- python=3.5.1=0
- readline=6.2=2
- setuptools=20.3=py35_0
- sqlite=3.9.2=0
- tk=8.5.18=0
- wheel=0.29.0=py35_0
- xz=5.0.5=1
- zlib=1.2.8=0
- pip:
- httptools>=0.0.10
- uvloop>=0.5.3
- httptools>=0.0.10
- ujson>=1.35
- aiofiles>=0.3.0
- websockets>=6.0,<7.0
- multidict>=4.0,<5.0
- sphinx==1.8.3
- sphinx_rtd_theme==0.4.2
- recommonmark==0.5.0
- websockets>=6.0
- sphinxcontrib-asyncio>=0.2.0
- docutils==0.14
- pygments==2.3.1
- multidict>=4.0,<5.0
- https://github.com/channelcat/docutils-fork/zipball/master

View File

@@ -1,30 +0,0 @@
from sanic import Sanic
from sanic.response import text
from random import randint
app = Sanic()
@app.middleware('request')
def append_request(request):
# Add new key with random value
request['num'] = randint(0, 100)
@app.get('/pop')
def pop_handler(request):
# Pop key from request object
num = request.pop('num')
return text(num)
@app.get('/key_exist')
def key_exist_handler(request):
# Check the key is exist or not
if 'num' in request:
return text('num exist in request')
return text('num does not exist in reqeust')
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,61 +0,0 @@
import logging
import socket
from os import getenv
from platform import node
from uuid import getnode as get_mac
from logdna import LogDNAHandler
from sanic import Sanic
from sanic.response import json
from sanic.request import Request
log = logging.getLogger('logdna')
log.setLevel(logging.INFO)
def get_my_ip_address(remote_server="google.com"):
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect((remote_server, 80))
return s.getsockname()[0]
def get_mac_address():
h = iter(hex(get_mac())[2:].zfill(12))
return ":".join(i + next(h) for i in h)
logdna_options = {
"app": __name__,
"index_meta": True,
"hostname": node(),
"ip": get_my_ip_address(),
"mac": get_mac_address()
}
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
logdna = logging.getLogger(__name__)
logdna.setLevel(logging.INFO)
logdna.addHandler(logdna_handler)
app = Sanic(__name__)
@app.middleware
def log_request(request: Request):
logdna.info("I was Here with a new Request to URL: {}".format(request.url))
@app.route("/")
def default(request):
return json({
"response": "I was here"
})
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -1,37 +0,0 @@
from os import getenv
from raygun4py.raygunprovider import RaygunSender
from sanic import Sanic
from sanic.exceptions import SanicException
from sanic.handlers import ErrorHandler
class RaygunExceptionReporter(ErrorHandler):
def __init__(self, raygun_api_key=None):
super().__init__()
if raygun_api_key is None:
raygun_api_key = getenv("RAYGUN_API_KEY")
self.sender = RaygunSender(raygun_api_key)
def default(self, request, exception):
self.sender.send_exception(exception=exception)
return super().default(request, exception)
raygun_error_reporter = RaygunExceptionReporter()
app = Sanic(__name__, error_handler=raygun_error_reporter)
@app.route("/raise")
async def test(request):
raise SanicException('You Broke It!')
if __name__ == '__main__':
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -1,30 +0,0 @@
import rollbar
from sanic.handlers import ErrorHandler
from sanic import Sanic
from sanic.exceptions import SanicException
from os import getenv
rollbar.init(getenv("ROLLBAR_API_KEY"))
class RollbarExceptionHandler(ErrorHandler):
def default(self, request, exception):
rollbar.report_message(str(exception))
return super().default(request, exception)
app = Sanic(__name__, error_handler=RollbarExceptionHandler())
@app.route("/raise")
def create_error(request):
raise SanicException("I was here and I don't like where I am")
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -1,35 +0,0 @@
from os import getenv
from sentry_sdk import init as sentry_init
from sentry_sdk.integrations.sanic import SanicIntegration
from sanic import Sanic
from sanic.response import json
sentry_init(
dsn=getenv("SENTRY_DSN"),
integrations=[SanicIntegration()],
)
app = Sanic(__name__)
# noinspection PyUnusedLocal
@app.route("/working")
async def working_path(request):
return json({
"response": "Working API Response"
})
# noinspection PyUnusedLocal
@app.route("/raise-error")
async def raise_error(request):
raise Exception("Testing Sentry Integration")
if __name__ == '__main__':
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -1,304 +0,0 @@
#!/usr/bin/env python
from argparse import ArgumentParser, Namespace
from collections import OrderedDict
from configparser import RawConfigParser
from datetime import datetime
from json import dumps
from os import path
from subprocess import Popen, PIPE
from jinja2 import Environment, BaseLoader
from requests import patch
GIT_COMMANDS = {
"get_tag": ["git describe --tags --abbrev=0"],
"commit_version_change": [
"git add . && git commit -m 'Bumping up version from "
"{current_version} to {new_version}'"
],
"create_new_tag": [
"git tag -a {new_version} -m 'Bumping up version from "
"{current_version} to {new_version}'"
],
"push_tag": ["git push origin {new_version}"],
"get_change_log": [
'git log --no-merges --pretty=format:"%h::: %cn::: %s" '
"{current_version}.."
],
}
RELEASE_NOTE_TEMPLATE = """
# {{ release_name }} - {% now 'utc', '%Y-%m-%d' %}
To see the exhaustive list of pull requests included in this release see:
https://github.com/huge-success/sanic/milestone/{{milestone}}?closed=1
# Changelog
{% for row in changelogs %}
* {{ row -}}
{% endfor %}
# Credits
{% for author in authors %}
* {{ author -}}
{% endfor %}
"""
JINJA_RELEASE_NOTE_TEMPLATE = Environment(
loader=BaseLoader, extensions=["jinja2_time.TimeExtension"]
).from_string(RELEASE_NOTE_TEMPLATE)
RELEASE_NOTE_UPDATE_URL = (
"https://api.github.com/repos/huge-success/sanic/releases/tags/"
"{new_version}?access_token={token}"
)
def _run_shell_command(command: list):
try:
process = Popen(
command, stderr=PIPE, stdout=PIPE, stdin=PIPE, shell=True
)
output, error = process.communicate()
return_code = process.returncode
return output.decode("utf-8"), error, return_code
except:
return None, None, -1
def _fetch_default_calendar_release_version():
return datetime.now().strftime("%y.%m.0")
def _fetch_current_version(config_file: str) -> str:
if path.isfile(config_file):
config_parser = RawConfigParser()
with open(config_file) as cfg:
config_parser.read_file(cfg)
return (
config_parser.get("version", "current_version")
or _fetch_default_calendar_release_version()
)
else:
return _fetch_default_calendar_release_version()
def _change_micro_version(current_version: str):
version_string = current_version.split(".")
version_string[-1] = str((int(version_string[-1]) + 1))
return ".".join(version_string)
def _get_new_version(
config_file: str = "./setup.cfg",
current_version: str = None,
micro_release: bool = False,
):
if micro_release:
if current_version:
return _change_micro_version(current_version)
elif config_file:
return _change_micro_version(_fetch_current_version(config_file))
else:
return _fetch_default_calendar_release_version()
else:
return _fetch_default_calendar_release_version()
def _get_current_tag(git_command_name="get_tag"):
global GIT_COMMANDS
command = GIT_COMMANDS.get(git_command_name)
out, err, ret = _run_shell_command(command)
if len(str(out)):
return str(out).split("\n")[0]
else:
return None
def _update_release_version_for_sanic(
current_version, new_version, config_file
):
config_parser = RawConfigParser()
with open(config_file) as cfg:
config_parser.read_file(cfg)
config_parser.set("version", "current_version", new_version)
version_file = config_parser.get("version", "file")
current_version_line = config_parser.get(
"version", "current_version_pattern"
).format(current_version=current_version)
new_version_line = config_parser.get(
"version", "new_version_pattern"
).format(new_version=new_version)
with open(version_file) as init_file:
data = init_file.read()
new_data = data.replace(current_version_line, new_version_line)
with open(version_file, "w") as init_file:
init_file.write(new_data)
with open(config_file, "w") as config:
config_parser.write(config)
command = GIT_COMMANDS.get("commit_version_change")
command[0] = command[0].format(
new_version=new_version, current_version=current_version
)
_, err, ret = _run_shell_command(command)
if int(ret) != 0:
print(
"Failed to Commit Version upgrade changes to Sanic: {}".format(
err.decode("utf-8")
)
)
exit(1)
def _generate_change_log(current_version: str = None):
global GIT_COMMANDS
command = GIT_COMMANDS.get("get_change_log")
command[0] = command[0].format(current_version=current_version)
output, error, ret = _run_shell_command(command=command)
if not len(str(output)):
print("Unable to Fetch Change log details to update the Release Note")
exit(1)
commit_details = OrderedDict()
commit_details["authors"] = dict()
commit_details["commits"] = list()
for line in str(output).split("\n"):
commit, author, description = line.split(":::")
if "GitHub" not in author:
commit_details["authors"][author] = 1
commit_details["commits"].append(" - ".join([commit, description]))
return commit_details
def _generate_markdown_document(
milestone, release_name, current_version, release_version
):
global JINJA_RELEASE_NOTE_TEMPLATE
release_name = release_name or release_version
change_log = _generate_change_log(current_version=current_version)
return JINJA_RELEASE_NOTE_TEMPLATE.render(
release_name=release_name,
milestone=milestone,
changelogs=change_log["commits"],
authors=change_log["authors"].keys(),
)
def _tag_release(new_version, current_version, milestone, release_name, token):
global GIT_COMMANDS
global RELEASE_NOTE_UPDATE_URL
for command_name in ["create_new_tag", "push_tag"]:
command = GIT_COMMANDS.get(command_name)
command[0] = command[0].format(
new_version=new_version, current_version=current_version
)
out, error, ret = _run_shell_command(command=command)
if int(ret) != 0:
print("Failed to execute the command: {}".format(command[0]))
exit(1)
change_log = _generate_markdown_document(
milestone, release_name, current_version, new_version
)
body = {"name": release_name or new_version, "body": change_log}
headers = {"content-type": "application/json"}
response = patch(
RELEASE_NOTE_UPDATE_URL.format(new_version=new_version, token=token),
data=dumps(body),
headers=headers,
)
response.raise_for_status()
def release(args: Namespace):
current_tag = _get_current_tag()
current_version = _fetch_current_version(args.config)
if current_tag and current_version not in current_tag:
print(
"Tag mismatch between what's in git and what was provided by "
"--current-version. Existing: {}, Give: {}".format(
current_tag, current_version
)
)
exit(1)
new_version = args.release_version or _get_new_version(
args.config, current_version, args.micro_release
)
_update_release_version_for_sanic(
current_version=current_version,
new_version=new_version,
config_file=args.config,
)
_tag_release(
current_version=current_version,
new_version=new_version,
milestone=args.milestone,
release_name=args.release_name,
token=args.token,
)
if __name__ == "__main__":
cli = ArgumentParser(description="Sanic Release Manager")
cli.add_argument(
"--release-version",
"-r",
help="New Version to use for Release",
default=_fetch_default_calendar_release_version(),
required=False,
)
cli.add_argument(
"--current-version",
"-cv",
help="Current Version to default in case if you don't want to "
"use the version configuration files",
default=None,
required=False,
)
cli.add_argument(
"--config",
"-c",
help="Configuration file used for release",
default="./setup.cfg",
required=False,
)
cli.add_argument(
"--token",
"-t",
help="Git access token with necessary access to Huge Sanic Org",
required=True,
)
cli.add_argument(
"--milestone",
"-ms",
help="Git Release milestone information to include in relase note",
required=True,
)
cli.add_argument(
"--release-name",
"-n",
help="Release Name to use if any",
required=False,
)
cli.add_argument(
"--micro-release",
"-m",
help="Micro Release with patches only",
default=False,
action="store_true",
required=False,
)
args = cli.parse_args()
release(args)

13
requirements-dev.txt Normal file
View File

@@ -0,0 +1,13 @@
aiofiles
aiohttp>=2.3.0,<=3.2.1
chardet<=2.3.0
beautifulsoup4
coverage
httptools>=0.0.10
flake8
pytest==3.3.2
tox
ujson; sys_platform != "win32" and implementation_name == "cpython"
uvloop; sys_platform != "win32" and implementation_name == "cpython"
gunicorn
multidict>=4.0,<5.0

4
requirements-docs.txt Normal file
View File

@@ -0,0 +1,4 @@
sphinx
sphinx_rtd_theme
recommonmark
sphinxcontrib-asyncio

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
aiofiles
httptools>=0.0.10
ujson; sys_platform != "win32" and implementation_name == "cpython"
uvloop; sys_platform != "win32" and implementation_name == "cpython"
websockets>=6.0,<7.0
multidict>=4.0,<5.0

View File

@@ -4,19 +4,16 @@ import os
import re
import warnings
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
from asyncio import CancelledError, ensure_future, get_event_loop
from collections import defaultdict, deque
from functools import partial
from inspect import getmodulename, isawaitable, signature, stack
from socket import socket
from ssl import Purpose, SSLContext, create_default_context
from ssl import Purpose, create_default_context
from traceback import format_exc
from typing import Any, Optional, Type, Union
from urllib.parse import urlencode, urlunparse
from sanic import reloader_helpers
from sanic.blueprint_group import BlueprintGroup
from sanic.config import BASE_LOGO, Config
from sanic.config import Config
from sanic.constants import HTTP_METHODS
from sanic.exceptions import SanicException, ServerError, URLBuildError
from sanic.handlers import ErrorHandler
@@ -207,17 +204,6 @@ class Sanic:
def get(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **GET** *HTTP* method
:param uri: URL to be tagged to **GET** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"GET"}),
@@ -236,17 +222,6 @@ class Sanic:
version=None,
name=None,
):
"""
Add an API URL under the **POST** *HTTP* method
:param uri: URL to be tagged to **POST** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"POST"}),
@@ -266,17 +241,6 @@ class Sanic:
version=None,
name=None,
):
"""
Add an API URL under the **PUT** *HTTP* method
:param uri: URL to be tagged to **PUT** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"PUT"}),
@@ -302,17 +266,6 @@ class Sanic:
def options(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **OPTIONS** *HTTP* method
:param uri: URL to be tagged to **OPTIONS** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"OPTIONS"}),
@@ -331,17 +284,6 @@ class Sanic:
version=None,
name=None,
):
"""
Add an API URL under the **DELETE** *HTTP* method
:param uri: URL to be tagged to **PATCH** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"PATCH"}),
@@ -355,17 +297,6 @@ class Sanic:
def delete(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **DELETE** *HTTP* method
:param uri: URL to be tagged to **DELETE** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"DELETE"}),
@@ -457,13 +388,6 @@ class Sanic:
def response(handler):
async def websocket_handler(request, *args, **kwargs):
request.app = self
if not getattr(handler, "__blueprintname__", False):
request.endpoint = handler.__name__
else:
request.endpoint = (
getattr(handler, "__blueprintname__", "")
+ handler.__name__
)
try:
protocol = request.transport.get_protocol()
except AttributeError:
@@ -506,22 +430,7 @@ class Sanic:
subprotocols=None,
name=None,
):
"""
A helper method to register a function as a websocket route.
:param handler: a callable function or instance of a class
that can handle the websocket request
:param host: Host IP or FQDN details
:param uri: URL path that will be mapped to the websocket
handler
:param strict_slashes: If the API endpoint needs to terminate
with a "/" or not
:param subprotocols: Subprotocols to be used with websocket
handshake
:param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:return: Objected decorated by :func:`websocket`
"""
"""A helper method to register a function as a websocket route."""
if strict_slashes is None:
strict_slashes = self.strict_slashes
@@ -550,16 +459,6 @@ class Sanic:
self.websocket_enabled = enable
def remove_route(self, uri, clean_cache=True, host=None):
"""
This method provides the app user a mechanism by which an already
existing route can be removed from the :class:`Sanic` object
: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
"""
self.router.remove(uri, clean_cache, host)
# Decorator
@@ -582,39 +481,18 @@ class Sanic:
return response
def register_middleware(self, middleware, attach_to="request"):
"""
Register an application level middleware that will be attached
to all the API URLs registered under this application.
This method is internally invoked by the :func:`middleware`
decorator provided at the app level.
:param middleware: Callback method to be attached to the
middleware
:param attach_to: The state at which the middleware needs to be
invoked in the lifecycle of an *HTTP Request*.
**request** - Invoke before the request is processed
**response** - Invoke before the response is returned back
:return: decorated method
"""
if attach_to == "request":
if middleware not in self.request_middleware:
self.request_middleware.append(middleware)
if attach_to == "response":
if middleware not in self.response_middleware:
self.response_middleware.appendleft(middleware)
return middleware
# Decorator
def middleware(self, middleware_or_request):
"""Decorate and register middleware to be called before a request.
Can either be called as @app.middleware or @app.middleware('request')
"""
Decorate and register middleware to be called before a request.
Can either be called as *@app.middleware* or
*@app.middleware('request')*
:param: middleware_or_request: Optional parameter to use for
identifying which type of middleware is being registered.
"""
# Detect which way this was called, @middleware or @middleware('AT')
if callable(middleware_or_request):
return self.register_middleware(middleware_or_request)
@@ -638,30 +516,8 @@ class Sanic:
strict_slashes=None,
content_type=None,
):
"""
Register a root to serve files from. The input can either be a
file or a directory. This method will enable an easy and simple way
to setup the :class:`Route` necessary to serve the static files.
:param uri: URL path to be used for serving static content
:param file_or_directory: Path for the Static file/directory with
static files
:param pattern: Regex Pattern identifying the valid static files
:param use_modified_since: If true, send file modified time, and return
not modified if the browser's matches the server's
:param use_content_range: If true, process header for range requests
and sends the file part that is requested
:param stream_large_files: If true, use the
:func:`StreamingHTTPResponse.file_stream` handler rather
than the :func:`HTTPResponse.file` handler to send the file.
If this is an integer, this represents the threshold size to
switch to :func:`StreamingHTTPResponse.file_stream`
:param name: user defined name used for url_for
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`Sanic` to check if the request
URLs need to terminate with a */*
:param content_type: user defined content type for header
:return: None
"""Register a root to serve files from. The input can either be a
file or a directory. See
"""
static_register(
self,
@@ -684,7 +540,7 @@ class Sanic:
:param options: option dictionary with blueprint defaults
:return: Nothing
"""
if isinstance(blueprint, (list, tuple, BlueprintGroup)):
if isinstance(blueprint, (list, tuple)):
for item in blueprint:
self.blueprint(item, **options)
return
@@ -699,17 +555,7 @@ class Sanic:
blueprint.register(self, options)
def register_blueprint(self, *args, **kwargs):
"""
Proxy method provided for invoking the :func:`blueprint` method
.. note::
To be deprecated in 1.0. Use :func:`blueprint` instead.
:param args: Blueprint object or (list, tuple) thereof
:param kwargs: option dictionary with blueprint defaults
:return: None
"""
# TODO: deprecate 1.0
if self.debug:
warnings.simplefilter("default")
warnings.warn(
@@ -854,9 +700,6 @@ class Sanic:
# -------------------------------------------------------------------- #
def converted_response_type(self, response):
"""
No implementation provided.
"""
pass
async def handle_request(self, request, write_callback, stream_callback):
@@ -900,16 +743,6 @@ class Sanic:
"handler from the router"
)
)
else:
if not getattr(handler, "__blueprintname__", False):
request.endpoint = self._build_endpoint_name(
handler.__name__
)
else:
request.endpoint = self._build_endpoint_name(
getattr(handler, "__blueprintname__", ""),
handler.__name__,
)
# Run response handler
response = handler(request, *args, **kwargs)
@@ -989,49 +822,19 @@ class Sanic:
def run(
self,
host: Optional[str] = None,
port: Optional[int] = None,
debug: bool = False,
ssl: Union[dict, SSLContext, None] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Type[Protocol] = None,
backlog: int = 100,
stop_event: Any = None,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
**kwargs: Any
) -> None:
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl:SSLContext or dict
:param sock: Socket for the server to accept connections from
:type sock: socket
:param workers: Number of processes received before it is respected
:type workers: int
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param stop_event: event to be triggered
before stopping the app - deprecated
:type stop_event: None
:param register_sys_signals: Register SIG* events
:type register_sys_signals: bool
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:return: Nothing
"""
host=None,
port=None,
debug=False,
ssl=None,
sock=None,
workers=1,
protocol=None,
backlog=100,
stop_event=None,
register_sys_signals=True,
access_log=True,
**kwargs
):
if "loop" in kwargs:
raise TypeError(
"loop is not a valid argument. To use an existing loop, "
@@ -1040,6 +843,23 @@ class Sanic:
"#asynchronous-support"
)
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:param backlog:
:param stop_event:
:param register_sys_signals:
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
# Default auto_reload to false
auto_reload = False
# If debug is set, default it to true (unless on windows)
@@ -1062,10 +882,8 @@ class Sanic:
"stop_event will be removed from future versions.",
DeprecationWarning,
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
# compatibility old access_log params
self.config.ACCESS_LOG = access_log
server_settings = self._helper(
host=host,
port=port,
@@ -1115,58 +933,20 @@ class Sanic:
async def create_server(
self,
host: Optional[str] = None,
port: Optional[int] = None,
debug: bool = False,
ssl: Union[dict, SSLContext, None] = None,
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
stop_event: Any = None,
access_log: Optional[bool] = None,
return_asyncio_server=False,
asyncio_server_kwargs=None,
) -> None:
"""
Asynchronous version of :func:`run`.
host=None,
port=None,
debug=False,
ssl=None,
sock=None,
protocol=None,
backlog=100,
stop_event=None,
access_log=True,
):
"""Asynchronous version of `run`.
This method will take care of the operations necessary to invoke
the *before_start* events via :func:`trigger_events` method invocation
before starting the *sanic* app in Async mode.
.. note::
This does not support multiprocessing and is not the preferred
way to run a :class:`Sanic` application.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl:SSLContext or dict
:param sock: Socket for the server to accept connections from
:type sock: socket
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param stop_event: event to be triggered
before stopping the app - deprecated
:type stop_event: None
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param return_asyncio_server: flag that defines whether there's a need
to return asyncio.Server or
start it serving right away
:type return_asyncio_server: bool
:param asyncio_server_kwargs: key-value arguments for
asyncio/uvloop create_server method
:type asyncio_server_kwargs: dict
:return: Nothing
NOTE: This does not support multiprocessing and is not the preferred
way to run a Sanic application.
"""
if sock is None:
@@ -1183,10 +963,8 @@ class Sanic:
"stop_event will be removed from future versions.",
DeprecationWarning,
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
# compatibility old access_log params
self.config.ACCESS_LOG = access_log
server_settings = self._helper(
host=host,
port=port,
@@ -1196,7 +974,7 @@ class Sanic:
loop=get_event_loop(),
protocol=protocol,
backlog=backlog,
run_async=return_asyncio_server,
run_async=True,
)
# Trigger before_start events
@@ -1205,9 +983,7 @@ class Sanic:
server_settings.get("loop"),
)
return await serve(
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
)
return await serve(**server_settings)
async def trigger_events(self, events, loop):
"""Trigger events (functions or async)
@@ -1295,7 +1071,6 @@ class Sanic:
"response_timeout": self.config.RESPONSE_TIMEOUT,
"keep_alive_timeout": self.config.KEEP_ALIVE_TIMEOUT,
"request_max_size": self.config.REQUEST_MAX_SIZE,
"request_buffer_queue_size": self.config.REQUEST_BUFFER_QUEUE_SIZE,
"keep_alive": self.config.KEEP_ALIVE,
"loop": loop,
"register_sys_signals": register_sys_signals,
@@ -1329,14 +1104,10 @@ class Sanic:
logger.setLevel(logging.DEBUG)
if (
self.config.LOGO
self.config.LOGO is not None
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
):
logger.debug(
self.config.LOGO
if isinstance(self.config.LOGO, str)
else BASE_LOGO
)
logger.debug(self.config.LOGO)
if run_async:
server_settings["run_async"] = True
@@ -1349,7 +1120,3 @@ class Sanic:
logger.info("Goin' Fast @ {}://{}:{}".format(proto, host, port))
return server_settings
def _build_endpoint_name(self, *parts):
parts = [self.name, *parts]
return ".".join(parts)

View File

@@ -1,120 +0,0 @@
from collections import MutableSequence
class BlueprintGroup(MutableSequence):
"""
This class provides a mechanism to implement a Blueprint Group
using the `Blueprint.group` method. To avoid having to re-write
some of the existing implementation, this class provides a custom
iterator implementation that will let you use the object of this
class as a list/tuple inside the existing implementation.
"""
__slots__ = ("_blueprints", "_url_prefix")
def __init__(self, url_prefix=None):
"""
Create a new Blueprint Group
:param url_prefix: URL: to be prefixed before all the Blueprint Prefix
"""
self._blueprints = []
self._url_prefix = url_prefix
@property
def url_prefix(self):
"""
Retrieve the URL prefix being used for the Current Blueprint Group
:return: string with url prefix
"""
return self._url_prefix
@property
def blueprints(self):
"""
Retrieve a list of all the available blueprints under this group.
:return: List of Blueprint instance
"""
return self._blueprints
def __iter__(self):
"""Tun the class Blueprint Group into an Iterable item"""
return iter(self._blueprints)
def __getitem__(self, item):
"""
This method returns a blueprint inside the group specified by
an index value. This will enable indexing, splice and slicing
of the blueprint group like we can do with regular list/tuple.
This method is provided to ensure backward compatibility with
any of the pre-existing usage that might break.
:param item: Index of the Blueprint item in the group
:return: Blueprint object
"""
return self._blueprints[item]
def __setitem__(self, index: int, item: object) -> None:
"""
Abstract method implemented to turn the `BlueprintGroup` class
into a list like object to support all the existing behavior.
This method is used to perform the list's indexed setter operation.
:param index: Index to use for inserting a new Blueprint item
:param item: New `Blueprint` object.
:return: None
"""
self._blueprints[index] = item
def __delitem__(self, index: int) -> None:
"""
Abstract method implemented to turn the `BlueprintGroup` class
into a list like object to support all the existing behavior.
This method is used to delete an item from the list of blueprint
groups like it can be done on a regular list with index.
:param index: Index to use for removing a new Blueprint item
:return: None
"""
del self._blueprints[index]
def __len__(self) -> int:
"""
Get the Length of the blueprint group object.
:return: Length of Blueprint group object
"""
return len(self._blueprints)
def insert(self, index: int, item: object) -> None:
"""
The Abstract class `MutableSequence` leverages this insert method to
perform the `BlueprintGroup.append` operation.
:param index: Index to use for removing a new Blueprint item
:param item: New `Blueprint` object.
:return: None
"""
self._blueprints.insert(index, item)
def middleware(self, *args, **kwargs):
"""
A decorator that can be used to implement a Middleware plugin to
all of the Blueprints that belongs to this specific Blueprint Group.
In case of nested Blueprint Groups, the same middleware is applied
across each of the Blueprints recursively.
:param args: Optional positional Parameters to be use middleware
:param kwargs: Optional Keyword arg to use with Middleware
:return: Partial function to apply the middleware
"""
kwargs["bp_group"] = True
def register_middleware_for_blueprints(fn):
for blueprint in self.blueprints:
blueprint.middleware(fn, *args, **kwargs)
return register_middleware_for_blueprints

View File

@@ -1,6 +1,5 @@
from collections import defaultdict, namedtuple
from sanic.blueprint_group import BlueprintGroup
from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView
@@ -39,17 +38,11 @@ class Blueprint:
version=None,
strict_slashes=False,
):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
URLs that perform a specific set of tasks which can be identified by
a unique name.
"""Create a new blueprint
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a
training */*
:param strict_slashes: strict to trailing slash
"""
self.name = name
self.url_prefix = url_prefix
@@ -66,9 +59,8 @@ class Blueprint:
@staticmethod
def group(*blueprints, url_prefix=""):
"""
Create a list of blueprints, optionally grouping them under a
general URL prefix.
"""Create a list of blueprints, optionally
grouping them under a general URL prefix.
:param blueprints: blueprints to be registered as a group
:param url_prefix: URL route to be prepended to all sub-prefixes
@@ -79,12 +71,10 @@ class Blueprint:
for i in nested:
if isinstance(i, (list, tuple)):
yield from chain(i)
elif isinstance(i, BlueprintGroup):
yield from i.blueprints
else:
yield i
bps = BlueprintGroup(url_prefix=url_prefix)
bps = []
for bp in chain(blueprints):
if bp.url_prefix is None:
bp.url_prefix = ""
@@ -93,14 +83,7 @@ class Blueprint:
return bps
def register(self, app, options):
"""
Register the blueprint to the sanic app.
:param app: Instance of :class:`sanic.app.Sanic` class
:param options: Options to be used while registering the
blueprint into the app.
*url_prefix* - URL Prefix to override the blueprint prefix
"""
"""Register the blueprint to the sanic app."""
url_prefix = options.get("url_prefix", self.url_prefix)
@@ -177,15 +160,6 @@ class Blueprint:
:param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods.
:param host: IP Address of FQDN for the sanic server to use.
:param strict_slashes: Enforce the API urls are requested with a
training */*
:param stream: If the route should provide a streaming support
:param version: Blueprint Version
:param name: Unique name to identify the Route
:return a decorated method that when invoked will return an object
of type :class:`FutureRoute`
"""
if strict_slashes is None:
strict_slashes = self.strict_slashes
@@ -215,7 +189,6 @@ class Blueprint:
strict_slashes=None,
version=None,
name=None,
stream=False,
):
"""Create a blueprint route from a function.
@@ -223,12 +196,10 @@ class Blueprint:
or class instance with a view_class method.
:param uri: endpoint at which the route will be accessible.
:param methods: list of acceptable HTTP methods.
:param host: IP Address of FQDN for the sanic server to use.
:param strict_slashes: Enforce the API urls are requested with a
training */*
:param version: Blueprint Version
:param host:
:param strict_slashes:
:param version:
:param name: user defined route name for url_for
:param stream: boolean specifying if the handler is a stream handler
:return: function or class instance
"""
# Handle HTTPMethodView differently
@@ -251,7 +222,6 @@ class Blueprint:
methods=methods,
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)(handler)
@@ -263,11 +233,6 @@ class Blueprint:
"""Create a blueprint websocket route from a decorated function.
:param uri: endpoint at which the route will be accessible.
:param host: IP Address of FQDN for the sanic server to use.
:param strict_slashes: Enforce the API urls are requested with a
training */*
:param version: Blueprint Version
:param name: Unique name to identify the Websocket Route
"""
if strict_slashes is None:
strict_slashes = self.strict_slashes
@@ -289,9 +254,6 @@ class Blueprint:
:param handler: function for handling uri requests. Accepts function,
or class instance with a view_class method.
:param uri: endpoint at which the route will be accessible.
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param name: Unique name to identify the Websocket Route
:return: function or class instance
"""
self.websocket(uri=uri, host=host, version=version, name=name)(handler)
@@ -310,14 +272,7 @@ class Blueprint:
return decorator
def middleware(self, *args, **kwargs):
"""
Create a blueprint middleware from a decorated function.
:param args: Positional arguments to be used while invoking the
middleware
:param kwargs: optional keyword args that can be used with the
middleware.
"""
"""Create a blueprint middleware from a decorated function."""
def register_middleware(_middleware):
future_middleware = FutureMiddleware(_middleware, args, kwargs)
@@ -329,27 +284,11 @@ class Blueprint:
middleware = args[0]
args = []
return register_middleware(middleware)
else:
if kwargs.get("bp_group") and callable(args[0]):
middleware = args[0]
args = args[1:]
kwargs.pop("bp_group")
return register_middleware(middleware)
else:
return register_middleware
def exception(self, *args, **kwargs):
"""
This method enables the process of creating a global exception
handler for the current blueprint under question.
:param args: List of Python exceptions to be caught by the handler
:param kwargs: Additional optional arguments to be passed to the
exception handler
:return a decorated method to handle global exceptions for any
route registered under this blueprint.
"""
"""Create a blueprint exception from a decorated function."""
def decorator(handler):
exception = FutureException(handler, args, kwargs)
@@ -380,20 +319,9 @@ class Blueprint:
def get(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **GET** *HTTP* method
:param uri: URL to be tagged to **GET** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"GET"}),
methods=["GET"],
host=host,
strict_slashes=strict_slashes,
version=version,
@@ -409,20 +337,9 @@ class Blueprint:
version=None,
name=None,
):
"""
Add an API URL under the **POST** *HTTP* method
:param uri: URL to be tagged to **POST** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"POST"}),
methods=["POST"],
host=host,
strict_slashes=strict_slashes,
stream=stream,
@@ -439,20 +356,9 @@ class Blueprint:
version=None,
name=None,
):
"""
Add an API URL under the **PUT** *HTTP* method
:param uri: URL to be tagged to **PUT** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"PUT"}),
methods=["PUT"],
host=host,
strict_slashes=strict_slashes,
stream=stream,
@@ -463,20 +369,9 @@ class Blueprint:
def head(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **HEAD** *HTTP* method
:param uri: URL to be tagged to **HEAD** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"HEAD"}),
methods=["HEAD"],
host=host,
strict_slashes=strict_slashes,
version=version,
@@ -486,20 +381,9 @@ class Blueprint:
def options(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **OPTIONS** *HTTP* method
:param uri: URL to be tagged to **OPTIONS** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"OPTIONS"}),
methods=["OPTIONS"],
host=host,
strict_slashes=strict_slashes,
version=version,
@@ -515,20 +399,9 @@ class Blueprint:
version=None,
name=None,
):
"""
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
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"PATCH"}),
methods=["PATCH"],
host=host,
strict_slashes=strict_slashes,
stream=stream,
@@ -539,20 +412,9 @@ class Blueprint:
def delete(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""
Add an API URL under the **DELETE** *HTTP* method
:param uri: URL to be tagged to **DELETE** method of *HTTP*
:param host: Host IP or FQDN for the service to use
:param strict_slashes: Instruct :class:`sanic.app.Sanic` to check
if the request URLs need to terminate with a */*
:param version: API Version
:param name: Unique name that can be used to identify the Route
:return: Object decorated with :func:`route` method
"""
return self.route(
uri,
methods=frozenset({"DELETE"}),
methods=["DELETE"],
host=host,
strict_slashes=strict_slashes,
version=version,

View File

@@ -1,44 +1,47 @@
import os
import types
from distutils.util import strtobool
from sanic.exceptions import PyFileError
SANIC_PREFIX = "SANIC_"
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_BUFFER_QUEUE_SIZE": 100,
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"KEEP_ALIVE": True,
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabytes
"WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True,
}
class Config(dict):
def __init__(self, defaults=None, load_env=True, keep_alive=None):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
self.LOGO = BASE_LOGO
if keep_alive is not None:
def __init__(self, defaults=None, load_env=True, keep_alive=True):
super().__init__(defaults or {})
self.LOGO = """
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \\
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀
"""
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
self.REQUEST_TIMEOUT = 60 # 60 seconds
self.RESPONSE_TIMEOUT = 60 # 60 seconds
self.KEEP_ALIVE = keep_alive
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
self.WEBSOCKET_MAX_QUEUE = 32
self.WEBSOCKET_READ_LIMIT = 2 ** 16
self.WEBSOCKET_WRITE_LIMIT = 2 ** 16
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
self.ACCESS_LOG = True
if load_env:
prefix = SANIC_PREFIX if load_env is True else load_env
@@ -125,8 +128,5 @@ class Config(dict):
except ValueError:
try:
self[config_key] = float(v)
except ValueError:
try:
self[config_key] = bool(strtobool(v))
except ValueError:
self[config_key] = v

View File

@@ -1,10 +1,6 @@
import re
import string
from datetime import datetime
DEFAULT_MAX_AGE = 0
# ------------------------------------------------------------ #
# SimpleCookie
@@ -107,29 +103,9 @@ class Cookie(dict):
if key not in self._keys:
raise KeyError("Unknown cookie property")
if value is not False:
if key.lower() == "max-age":
if not str(value).isdigit():
value = DEFAULT_MAX_AGE
elif key.lower() == "expires":
if not isinstance(value, datetime):
raise TypeError(
"Cookie 'expires' property must be a datetime"
)
return super().__setitem__(key, value)
def encode(self, encoding):
"""
Encode the cookie content in a specific type of encoding instructed
by the developer. Leverages the :func:`str.encode` method provided
by python.
This method can be used to encode and embed ``utf-8`` content into
the cookies.
:param encoding: Encoding to be used with the cookie
:return: Cookie encoded in a codec of choosing.
:except: UnicodeEncodeError
"""
output = ["%s=%s" % (self.key, _quote(self.value))]
for key, value in self.items():
if key == "max-age":
@@ -138,10 +114,16 @@ class Cookie(dict):
except TypeError:
output.append("%s=%s" % (self._keys[key], value))
elif key == "expires":
try:
output.append(
"%s=%s"
% (self._keys[key], value.strftime("%a, %d-%b-%Y %T GMT"))
% (
self._keys[key],
value.strftime("%a, %d-%b-%Y %T GMT"),
)
)
except AttributeError:
output.append("%s=%s" % (self._keys[key], value))
elif key in self._flags and self[key]:
output.append(self._keys[key])
else:

View File

@@ -123,7 +123,7 @@ _sanic_exceptions = {}
def add_status_code(code):
"""
Decorator used for adding exceptions to :class:`SanicException`.
Decorator used for adding exceptions to _sanic_exceptions.
"""
def class_decorator(cls):

View File

@@ -19,18 +19,6 @@ from sanic.response import html, text
class ErrorHandler:
"""
Provide :class:`sanic.app.Sanic` application with a mechanism to handle
and process any and all uncaught exceptions in a way the application
developer will set fit.
This error handling framework is built into the core that can be extended
by the developers to perform a wide range of tasks from recording the error
stats to reporting them to an external service that can be used for
realtime alerting system.
"""
handlers = None
cached_handlers = None
_missing = object()
@@ -70,34 +58,9 @@ class ErrorHandler:
)
def add(self, exception, handler):
"""
Add a new exception handler to an already existing handler object.
:param exception: Type of exception that need to be handled
:param handler: Reference to the method that will handle the exception
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:type handler: ``function``
:return: None
"""
self.handlers.append((exception, handler))
def lookup(self, exception):
"""
Lookup the existing instance of :class:`ErrorHandler` and fetch the
registered handler for a specific type of exception.
This method leverages a dict lookup to speedup the retrieval process.
:param exception: Type of exception
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:return: Registered function if found ``None`` otherwise
"""
handler = self.cached_handlers.get(type(exception), self._missing)
if handler is self._missing:
for exception_class, handler in self.handlers:
@@ -112,15 +75,9 @@ class ErrorHandler:
"""Fetches and executes an exception handler and returns a response
object
:param request: Instance of :class:`sanic.request.Request`
:param request: Request
:param exception: Exception to handle
:type request: :class:`sanic.request.Request`
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:return: Wrap the return value obtained from :func:`default`
or registered handler for that type of exception.
:return: Response object
"""
handler = self.lookup(exception)
response = None
@@ -152,20 +109,6 @@ class ErrorHandler:
"""
def default(self, request, exception):
"""
Provide a default behavior for the objects of :class:`ErrorHandler`.
If a developer chooses to extent the :class:`ErrorHandler` they can
provide a custom implementation for this method to behave in a way
they see fit.
:param request: Incoming request
:param exception: Exception object
:type request: :class:`sanic.request.Request`
:type exception: :class:`sanic.exceptions.SanicException` or
:class:`Exception`
:return:
"""
self.log(format_exc())
try:
url = repr(request.url)
@@ -190,23 +133,7 @@ class ErrorHandler:
class ContentRangeHandler:
"""
A mechanism to parse and process the incoming request headers to
extract the content range information.
:param request: Incoming api request
:param stats: Stats related to the content
:type request: :class:`sanic.request.Request`
:type stats: :class:`posix.stat_result`
:ivar start: Content Range start
:ivar end: Content Range end
:ivar size: Length of the content
:ivar total: Total size identified by the :class:`posix.stat_result`
instance
:ivar ContentRangeHandler.headers: Content range header ``dict``
"""
"""Class responsible for parsing request header"""
__slots__ = ("start", "end", "size", "total", "headers")

View File

@@ -36,14 +36,6 @@ def _iter_module_files():
def _get_args_for_reloading():
"""Returns the executable."""
rv = [sys.executable]
main_module = sys.modules["__main__"]
mod_spec = getattr(main_module, "__spec__", None)
if mod_spec:
# Parent exe was launched as a module rather than a script
rv.extend(["-m", mod_spec.name])
if len(sys.argv) > 1:
rv.extend(sys.argv[1:])
else:
rv.extend(sys.argv)
return rv
@@ -52,7 +44,6 @@ def restart_with_reloader():
"""Create a new process and a subprocess in it with the same arguments as
this one.
"""
cwd = os.getcwd()
args = _get_args_for_reloading()
new_environ = os.environ.copy()
new_environ["SANIC_SERVER_RUNNING"] = "true"
@@ -60,7 +51,7 @@ def restart_with_reloader():
worker_process = Process(
target=subprocess.call,
args=(cmd,),
kwargs={"cwd": cwd, "shell": True, "env": new_environ},
kwargs=dict(shell=True, env=new_environ),
)
worker_process.start()
return worker_process

View File

@@ -1,13 +1,10 @@
import asyncio
import email.utils
import json
import sys
import warnings
from cgi import parse_header
from collections import defaultdict, namedtuple
from collections import namedtuple
from http.cookies import SimpleCookie
from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
from urllib.parse import parse_qs, urlunparse
from httptools import parse_url
@@ -50,49 +47,30 @@ class RequestParameters(dict):
return super().get(name, default)
class StreamBuffer:
def __init__(self, buffer_size=100):
self._queue = asyncio.Queue(buffer_size)
async def read(self):
""" Stop reading when gets None """
payload = await self._queue.get()
self._queue.task_done()
return payload
async def put(self, payload):
await self._queue.put(payload)
def is_full(self):
return self._queue.full()
class Request(dict):
"""Properties of an HTTP request such as URL, headers, etc."""
__slots__ = (
"__weakref__",
"app",
"headers",
"version",
"method",
"_cookies",
"transport",
"body",
"parsed_json",
"parsed_args",
"parsed_form",
"parsed_files",
"_ip",
"_parsed_url",
"_port",
"uri_template",
"stream",
"_remote_addr",
"_socket",
"app",
"body",
"endpoint",
"headers",
"method",
"parsed_args",
"parsed_not_grouped_args",
"parsed_files",
"parsed_form",
"parsed_json",
"_port",
"__weakref__",
"raw_url",
"stream",
"transport",
"uri_template",
"version",
)
def __init__(self, url_bytes, headers, version, method, transport):
@@ -111,14 +89,14 @@ class Request(dict):
self.parsed_json = None
self.parsed_form = None
self.parsed_files = None
self.parsed_args = defaultdict(RequestParameters)
self.parsed_not_grouped_args = defaultdict(list)
self.parsed_args = None
self.uri_template = None
self._cookies = None
self.stream = None
self.endpoint = None
def __repr__(self):
if self.method is None or not self.path:
return "<{0}>".format(self.__class__.__name__)
return "<{0}: {1} {2}>".format(
self.__class__.__name__, self.method, self.path
)
@@ -202,117 +180,21 @@ class Request(dict):
return self.parsed_files
def get_args(
self,
keep_blank_values: bool = False,
strict_parsing: bool = False,
encoding: str = "utf-8",
errors: str = "replace",
) -> RequestParameters:
"""
Method to parse `query_string` using `urllib.parse.parse_qs`.
This methods is used by `args` property.
Can be used directly if you need to change default parameters.
:param keep_blank_values: flag indicating whether blank values in
percent-encoded queries should be treated as blank strings.
A true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
:type keep_blank_values: bool
:param strict_parsing: flag indicating what to do with parsing errors.
If false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
:type strict_parsing: bool
:param encoding: specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type encoding: str
:param errors: specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type errors: str
:return: RequestParameters
"""
if not self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
]:
@property
def args(self):
if self.parsed_args is None:
if self.query_string:
self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
] = RequestParameters(
parse_qs(
qs=self.query_string,
keep_blank_values=keep_blank_values,
strict_parsing=strict_parsing,
encoding=encoding,
errors=errors,
self.parsed_args = RequestParameters(
parse_qs(self.query_string)
)
)
return self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
]
args = property(get_args)
else:
self.parsed_args = RequestParameters()
return self.parsed_args
@property
def raw_args(self) -> dict:
if self.app.debug: # pragma: no cover
warnings.simplefilter("default")
warnings.warn(
"Use of raw_args will be deprecated in "
"the future versions. Please use args or query_args "
"properties instead",
DeprecationWarning,
)
def raw_args(self):
return {k: v[0] for k, v in self.args.items()}
def get_query_args(
self,
keep_blank_values: bool = False,
strict_parsing: bool = False,
encoding: str = "utf-8",
errors: str = "replace",
) -> list:
"""
Method to parse `query_string` using `urllib.parse.parse_qsl`.
This methods is used by `query_args` property.
Can be used directly if you need to change default parameters.
:param keep_blank_values: flag indicating whether blank values in
percent-encoded queries should be treated as blank strings.
A true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
:type keep_blank_values: bool
:param strict_parsing: flag indicating what to do with parsing errors.
If false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
:type strict_parsing: bool
:param encoding: specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type encoding: str
:param errors: specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
:type errors: str
:return: list
"""
if not self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
]:
if self.query_string:
self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
] = parse_qsl(
qs=self.query_string,
keep_blank_values=keep_blank_values,
strict_parsing=strict_parsing,
encoding=encoding,
errors=errors,
)
return self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
]
query_args = property(get_query_args)
@property
def cookies(self):
if self._cookies is None:
@@ -456,28 +338,15 @@ def parse_multipart_form(body, boundary):
)
if form_header_field == "content-disposition":
field_name = form_parameters.get("name")
file_name = form_parameters.get("filename")
# non-ASCII filenames in RFC2231, "filename*" format
if file_name is None and form_parameters.get("filename*"):
encoding, _, value = email.utils.decode_rfc2231(
form_parameters["filename*"]
)
file_name = unquote(value, encoding=encoding)
field_name = form_parameters.get("name")
elif form_header_field == "content-type":
content_type = form_header_value
content_charset = form_parameters.get("charset", "utf-8")
if field_name:
post_data = form_part[line_index:-4]
if file_name is None:
value = post_data.decode(content_charset)
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
else:
if file_name:
form_file = File(
type=content_type, name=file_name, body=post_data
)
@@ -485,6 +354,12 @@ def parse_multipart_form(body, boundary):
files[field_name].append(form_file)
else:
files[field_name] = [form_file]
else:
value = post_data.decode(content_charset)
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
else:
logger.debug(
"Form-data field does not have a 'name' parameter "

View File

@@ -17,8 +17,8 @@ Parameter = namedtuple("Parameter", ["name", "cast"])
REGEX_TYPES = {
"string": (str, r"[^/]+"),
"int": (int, r"-?\d+"),
"number": (float, r"-?[0-9\\.]+"),
"int": (int, r"\d+"),
"number": (float, r"[0-9\\.]+"),
"alpha": (str, r"[A-Za-z]+"),
"path": (str, r"[^/].*?"),
"uuid": (
@@ -331,17 +331,6 @@ class Router:
@staticmethod
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
"""
Check if a URL pattern exists in a list of routes provided based on
the comparison of URL pattern and the parameters.
:param pattern: URL parameter pattern
:param routes_to_check: list of dynamic routes either hashable or
unhashable routes.
:param parameters: List of :class:`Parameter` items
:return: Tuple of index and route if matching route exists else
-1 for index and None for route
"""
for ndx, route in enumerate(routes_to_check):
if route.pattern == pattern and route.parameters == parameters:
return ndx, route

View File

@@ -22,7 +22,7 @@ from sanic.exceptions import (
ServiceUnavailable,
)
from sanic.log import access_logger, logger
from sanic.request import Request, StreamBuffer
from sanic.request import Request
from sanic.response import HTTPResponse
@@ -39,10 +39,6 @@ class Signal:
class HttpProtocol(asyncio.Protocol):
"""
This class provides a basic HTTP implementation of the sanic framework.
"""
__slots__ = (
# event loop, connection
"loop",
@@ -60,7 +56,6 @@ class HttpProtocol(asyncio.Protocol):
"response_timeout",
"keep_alive_timeout",
"request_max_size",
"request_buffer_queue_size",
"request_class",
"is_request_stream",
"router",
@@ -91,12 +86,11 @@ class HttpProtocol(asyncio.Protocol):
request_handler,
error_handler,
signal=Signal(),
connections=None,
connections=set(),
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
request_max_size=None,
request_buffer_queue_size=100,
request_class=None,
access_log=True,
keep_alive=True,
@@ -115,11 +109,10 @@ class HttpProtocol(asyncio.Protocol):
self.router = router
self.signal = signal
self.access_log = access_log
self.connections = connections or set()
self.connections = connections
self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout
self.request_buffer_queue_size = request_buffer_queue_size
self.response_timeout = response_timeout
self.keep_alive_timeout = keep_alive_timeout
self.request_max_size = request_max_size
@@ -145,13 +138,6 @@ class HttpProtocol(asyncio.Protocol):
@property
def keep_alive(self):
"""
Check if the connection needs to be kept alive based on the params
attached to the `_keep_alive` attribute, :attr:`Signal.stopped`
and :func:`HttpProtocol.parser.should_keep_alive`
:return: ``True`` if connection is to be kept alive ``False`` else
"""
return (
self._keep_alive
and not self.signal.stopped
@@ -224,13 +210,8 @@ class HttpProtocol(asyncio.Protocol):
self.write_error(ServiceUnavailable("Response Timeout"))
def keep_alive_timeout_callback(self):
"""
Check if elapsed time since last response exceeds our configured
maximum keep alive timeout value and if so, close the transport
pipe and let the response writer handle the error.
:return: None
"""
# Check if elapsed time since last response exceeds our configured
# maximum keep alive timeout value
time_elapsed = time() - self._last_response_time
if time_elapsed < self.keep_alive_timeout:
time_left = self.keep_alive_timeout - time_elapsed
@@ -314,27 +295,17 @@ class HttpProtocol(asyncio.Protocol):
self.request
)
if self._is_stream_handler:
self.request.stream = StreamBuffer(
self.request_buffer_queue_size
)
self.request.stream = asyncio.Queue()
self.execute_request_handler()
def on_body(self, body):
if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task(
self.body_append(body)
self.request.stream.put(body)
)
else:
return
self.request.body_push(body)
async def body_append(self, body):
if self.request.stream.is_full():
self.transport.pause_reading()
await self.request.stream.put(body)
self.transport.resume_reading()
else:
await self.request.stream.put(body)
def on_message_complete(self):
# Entire request (headers and whole body) is received.
# We can cancel and remove the request timeout handler now.
@@ -350,12 +321,6 @@ class HttpProtocol(asyncio.Protocol):
self.execute_request_handler()
def execute_request_handler(self):
"""
Invoke the request handler defined by the
:func:`sanic.app.Sanic.handle_request` method
:return: None
"""
self._response_timeout_handler = self.loop.call_later(
self.response_timeout, self.response_timeout_callback
)
@@ -370,17 +335,6 @@ class HttpProtocol(asyncio.Protocol):
# Responding
# -------------------------------------------- #
def log_response(self, response):
"""
Helper method provided to enable the logging of responses in case if
the :attr:`HttpProtocol.access_log` is enabled.
:param response: Response generated for the current request
:type response: :class:`sanic.response.HTTPResponse` or
:class:`sanic.response.StreamingHTTPResponse`
:return: None
"""
if self.access_log:
extra = {"status": getattr(response, "status", 0)}
@@ -535,20 +489,6 @@ class HttpProtocol(asyncio.Protocol):
logger.debug("Connection lost before server could close it.")
def bail_out(self, message, from_error=False):
"""
In case if the transport pipes are closed and the sanic app encounters
an error while writing data to the transport pipe, we log the error
with proper details.
:param message: Error message to display
:param from_error: If the bail out was invoked while handling an
exception scenario.
:type message: str
:type from_error: bool
:return: None
"""
if from_error or self.transport.is_closing():
logger.error(
"Transport closed @ %s and exception "
@@ -592,6 +532,18 @@ class HttpProtocol(asyncio.Protocol):
self.transport = None
def update_current_time(loop):
"""Cache the current time, since it is needed at the end of every
keep-alive request to update the request timeout time
:param loop:
:return:
"""
global current_time
current_time = time()
loop.call_later(1, partial(update_current_time, loop))
def trigger_events(events, loop):
"""Trigger event callbacks (functions or async)
@@ -620,7 +572,6 @@ def serve(
ssl=None,
sock=None,
request_max_size=None,
request_buffer_queue_size=100,
reuse_port=False,
loop=None,
protocol=HttpProtocol,
@@ -641,7 +592,6 @@ def serve(
websocket_write_limit=2 ** 16,
state=None,
graceful_shutdown_timeout=15.0,
asyncio_server_kwargs=None,
):
"""Start asynchronous HTTP Server on an individual process.
@@ -682,12 +632,9 @@ def serve(
outgoing bytes, the low-water limit is a
quarter of the high-water limit.
:param is_request_stream: disable/enable Request.stream
:param request_buffer_queue_size: streaming request buffer queue size
:param router: Router object
:param graceful_shutdown_timeout: How long take to Force close non-idle
connection
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
create_server method
:return: Nothing
"""
if not run_async:
@@ -722,9 +669,7 @@ def serve(
state=state,
debug=debug,
)
asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {}
)
server_coroutine = loop.create_server(
server,
host,
@@ -733,9 +678,12 @@ def serve(
reuse_port=reuse_port,
sock=sock,
backlog=backlog,
**asyncio_server_kwargs
)
# Instead of pulling time at the end of every request,
# pull it once per minute
loop.call_soon(partial(update_current_time, loop))
if run_async:
return server_coroutine

View File

@@ -1,5 +1,4 @@
from json import JSONDecodeError
from socket import socket
from sanic.exceptions import MethodNotSupported
from sanic.log import logger
@@ -12,13 +11,19 @@ PORT = 42101
class SanicTestClient:
def __init__(self, app, port=PORT):
"""Use port=None to bind to a random port"""
self.app = app
self.port = port
async def _local_request(self, method, url, cookies=None, *args, **kwargs):
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
import aiohttp
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
url = uri
else:
url = "http://{host}:{port}{uri}".format(
host=HOST, port=self.port, uri=uri
)
logger.info(url)
conn = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(
@@ -74,27 +79,11 @@ class SanicTestClient:
else:
return self.app.error_handler.default(request, exception)
if self.port:
server_kwargs = dict(host=HOST, port=self.port, **server_kwargs)
host, port = HOST, self.port
else:
sock = socket()
sock.bind((HOST, 0))
server_kwargs = dict(sock=sock, **server_kwargs)
host, port = sock.getsockname()
if uri.startswith(("http:", "https:", "ftp:", "ftps://", "//")):
url = uri
else:
url = "http://{host}:{port}{uri}".format(
host=host, port=port, uri=uri
)
@self.app.listener("after_server_start")
async def _collect_response(sanic, loop):
try:
response = await self._local_request(
method, url, *request_args, **request_kwargs
method, uri, *request_args, **request_kwargs
)
results[-1] = response
except Exception as e:
@@ -102,7 +91,7 @@ class SanicTestClient:
exceptions.append(e)
self.app.stop()
self.app.run(debug=debug, **server_kwargs)
self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
self.app.listeners["after_server_start"].pop()
if exceptions:

View File

@@ -57,7 +57,6 @@ class GunicornWorker(base.Worker):
if self.app.callable.websocket_enabled
else self.http_protocol
)
self._server_settings = self.app.callable._helper(
loop=self.loop,
debug=is_debug,

View File

@@ -1,8 +1,11 @@
[flake8]
# https://github.com/ambv/black#slices
# https://github.com/ambv/black#line-breaks--binary-operators
ignore = E203, W503
[isort]
atomic = true
atomic=true
default_section = THIRDPARTY
include_trailing_comma = true
known_first_party = sanic
@@ -12,9 +15,3 @@ lines_after_imports = 2
lines_between_types = 1
multi_line_output = 3
not_skip = __init__.py
[version]
current_version = 0.8.3
file = sanic/__init__.py
current_version_pattern = __version__ = "{current_version}"
new_version_pattern = __version__ = "{new_version}"

128
setup.py
View File

@@ -4,126 +4,74 @@ Sanic
import codecs
import os
import re
import sys
from distutils.errors import DistutilsPlatformError
from distutils.util import strtobool
from setuptools import setup
from setuptools.command.test import test as TestCommand
class PyTest(TestCommand):
"""
Provide a Test runner to be used from setup.py to run unit tests
"""
user_options = [("pytest-args=", "a", "Arguments to pass to pytest")]
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = ""
def run_tests(self):
import shlex
import pytest
errno = pytest.main(shlex.split(self.pytest_args))
sys.exit(errno)
def open_local(paths, mode="r", encoding="utf8"):
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), *paths)
def open_local(paths, mode='r', encoding='utf8'):
path = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
*paths
)
return codecs.open(path, mode, encoding)
with open_local(["sanic", "__init__.py"], encoding="latin1") as fp:
with open_local(['sanic', '__init__.py'], encoding='latin1') as fp:
try:
version = re.findall(
r"^__version__ = \"([^']+)\"\r?$", fp.read(), re.M
)[0]
version = re.findall(r"^__version__ = \"([^']+)\"\r?$",
fp.read(), re.M)[0]
except IndexError:
raise RuntimeError("Unable to determine version.")
raise RuntimeError('Unable to determine version.')
with open_local(["README.rst"]) as rm:
with open_local(['README.rst']) as rm:
long_description = rm.read()
setup_kwargs = {
"name": "sanic",
"version": version,
"url": "http://github.com/channelcat/sanic/",
"license": "MIT",
"author": "Channel Cat",
"author_email": "channelcat@gmail.com",
"description": (
"A microframework based on uvloop, httptools, and learnings of flask"
),
"long_description": long_description,
"packages": ["sanic"],
"platforms": "any",
"classifiers": [
"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",
'name': 'sanic',
'version': version,
'url': 'http://github.com/channelcat/sanic/',
'license': 'MIT',
'author': 'Channel Cat',
'author_email': 'channelcat@gmail.com',
'description': (
'A microframework based on uvloop, httptools, and learnings of flask'),
'long_description': long_description,
'packages': ['sanic'],
'platforms': 'any',
'classifiers': [
'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',
],
}
env_dependency = (
'; sys_platform != "win32" ' 'and implementation_name == "cpython"'
)
ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency
env_dependency = '; sys_platform != "win32" and implementation_name == "cpython"'
ujson = 'ujson>=1.35' + env_dependency
uvloop = 'uvloop>=0.5.3' + env_dependency
requirements = [
"httptools>=0.0.10",
'httptools>=0.0.10',
uvloop,
ujson,
"aiofiles>=0.3.0",
"websockets>=6.0,<7.0",
"multidict>=4.0,<5.0",
'aiofiles>=0.3.0',
'websockets>=6.0,<7.0',
'multidict>=4.0,<5.0',
]
tests_require = [
"pytest==4.1.0",
"multidict>=4.0,<5.0",
"gunicorn",
"pytest-cov",
"aiohttp>=2.3.0,<=3.2.1",
"beautifulsoup4",
uvloop,
ujson,
"pytest-sanic",
"pytest-sugar",
]
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
print("Installing without uJSON")
requirements.remove(ujson)
tests_require.remove(ujson)
# 'nt' means windows OS
if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
print("Installing without uvLoop")
requirements.remove(uvloop)
tests_require.remove(uvloop)
extras_require = {
"test": tests_require,
"dev": tests_require + ["aiofiles", "tox", "black", "flake8"],
"docs": [
"sphinx",
"sphinx_rtd_theme",
"recommonmark",
"sphinxcontrib-asyncio",
"docutils",
"pygments"
],
}
setup_kwargs["install_requires"] = requirements
setup_kwargs["tests_require"] = tests_require
setup_kwargs["extras_require"] = extras_require
setup_kwargs["cmdclass"] = {"test": PyTest}
setup_kwargs['install_requires'] = requirements
setup(**setup_kwargs)

View File

@@ -1,53 +0,0 @@
from random import choice, seed
from pytest import mark
import sanic.router
seed("Pack my box with five dozen liquor jugs.")
# Disable Caching for testing purpose
sanic.router.ROUTER_CACHE_SIZE = 0
class TestSanicRouteResolution:
@mark.asyncio
async def test_resolve_route_no_arg_string_path(
self, sanic_router, route_generator, benchmark
):
simple_routes = route_generator.generate_random_direct_route(
max_route_depth=4
)
router, simple_routes = sanic_router(route_details=simple_routes)
route_to_call = choice(simple_routes)
result = benchmark.pedantic(
router._get,
("/{}".format(route_to_call[-1]), route_to_call[0], "localhost"),
iterations=1000,
rounds=1000,
)
assert await result[0](None) == 1
@mark.asyncio
async def test_resolve_route_with_typed_args(
self, sanic_router, route_generator, benchmark
):
typed_routes = route_generator.add_typed_parameters(
route_generator.generate_random_direct_route(max_route_depth=4),
max_route_depth=8,
)
router, typed_routes = sanic_router(route_details=typed_routes)
route_to_call = choice(typed_routes)
url = route_generator.generate_url_for_template(
template=route_to_call[-1]
)
print("{} -> {}".format(route_to_call[-1], url))
result = benchmark.pedantic(
router._get,
("/{}".format(url), route_to_call[0], "localhost"),
iterations=1000,
rounds=1000,
)
assert await result[0](None) == 1

View File

@@ -1,130 +1,12 @@
import random
import re
import string
import sys
import uuid
import pytest
from sanic import Sanic
from sanic.router import RouteExists, Router
random.seed("Pack my box with five dozen liquor jugs.")
if sys.platform in ["win32", "cygwin"]:
if sys.platform in ['win32', 'cygwin']:
collect_ignore = ["test_worker.py"]
async def _handler(request):
"""
Dummy placeholder method used for route resolver when creating a new
route into the sanic router. This router is not actually called by the
sanic app. So do not worry about the arguments to this method.
If you change the return value of this method, make sure to propagate the
change to any test case that leverages RouteStringGenerator.
"""
return 1
TYPE_TO_GENERATOR_MAP = {
"string": lambda: "".join(
[random.choice(string.ascii_letters + string.digits) for _ in range(4)]
),
"int": lambda: random.choice(range(1000000)),
"number": lambda: random.random(),
"alpha": lambda: "".join(
[random.choice(string.ascii_letters) for _ in range(4)]
),
"uuid": lambda: str(uuid.uuid1()),
}
class RouteStringGenerator:
ROUTE_COUNT_PER_DEPTH = 100
HTTP_METHODS = ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTION"]
ROUTE_PARAM_TYPES = ["string", "int", "number", "alpha", "uuid"]
def generate_random_direct_route(self, max_route_depth=4):
routes = []
for depth in range(1, max_route_depth + 1):
for _ in range(self.ROUTE_COUNT_PER_DEPTH):
route = "/".join(
[
TYPE_TO_GENERATOR_MAP.get("string")()
for _ in range(depth)
]
)
route = route.replace(".", "", -1)
route_detail = (random.choice(self.HTTP_METHODS), route)
if route_detail not in routes:
routes.append(route_detail)
return routes
def add_typed_parameters(self, current_routes, max_route_depth=8):
routes = []
for method, route in current_routes:
current_length = len(route.split("/"))
new_route_part = "/".join(
[
"<{}:{}>".format(
TYPE_TO_GENERATOR_MAP.get("string")(),
random.choice(self.ROUTE_PARAM_TYPES),
)
for _ in range(max_route_depth - current_length)
]
)
route = "/".join([route, new_route_part])
route = route.replace(".", "", -1)
routes.append((method, route))
return routes
@staticmethod
def generate_url_for_template(template):
url = template
for pattern, param_type in re.findall(
re.compile(r"((?:<\w+:(string|int|number|alpha|uuid)>)+)"),
template,
):
value = TYPE_TO_GENERATOR_MAP.get(param_type)()
url = url.replace(pattern, str(value), -1)
return url
@pytest.fixture(scope="function")
def sanic_router():
# noinspection PyProtectedMember
def _setup(route_details: tuple) -> (Router, tuple):
router = Router()
added_router = []
for method, route in route_details:
try:
router._add(
uri="/{}".format(route),
methods=frozenset({method}),
host="localhost",
handler=_handler,
)
added_router.append((method, route))
except RouteExists:
pass
return router, added_router
return _setup
@pytest.fixture(scope="function")
def route_generator() -> RouteStringGenerator:
return RouteStringGenerator()
@pytest.fixture(scope="function")
def url_param_generator():
return TYPE_TO_GENERATOR_MAP
@pytest.fixture
def app(request):
return Sanic(request.node.name)

View File

@@ -9,15 +9,10 @@ import ujson as json
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)
async def handle(request):
return web.Response(
body=json.dumps({"test": True}).encode("utf-8"),
content_type="application/json",
)
return web.Response(body=json.dumps({"test":True}).encode('utf-8'), content_type='application/json')
app = web.Application(loop=loop)
app.router.add_route("GET", "/", handle)
app.router.add_route('GET', '/', handle)
web.run_app(app, port=sys.argv[1], access_log=None)

View File

@@ -4,9 +4,8 @@ from bottle import route, run
import ujson
@route("/")
@route('/')
def index():
return ujson.dumps({"test": True})
return ujson.dumps({'test': True})
app = bottle.default_app()

View File

@@ -3,11 +3,9 @@
import falcon
import ujson as json
class TestResource:
def on_get(self, req, resp):
resp.body = json.dumps({"test": True})
app = falcon.API()
app.add_route("/", TestResource())
app.add_route('/', TestResource())

View File

@@ -13,14 +13,8 @@ kyk = Kyoukai("example_app")
logger = logging.getLogger("Kyoukai")
logger.setLevel(logging.ERROR)
@kyk.route("/")
async def index(ctx: HTTPRequestContext):
return (
ujson.dumps({"test": True}),
200,
{"Content-Type": "application/json"},
)
return ujson.dumps({"test":True}), 200, {"Content-Type": "application/json"}
kyk.run()

View File

@@ -3,10 +3,8 @@ import sys
import os
import inspect
currentdir = os.path.dirname(
os.path.abspath(inspect.getfile(inspect.currentframe()))
)
sys.path.insert(0, currentdir + "/../../../")
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, currentdir + '/../../../')
import timeit
@@ -18,11 +16,7 @@ print("Running New 100,000 times")
times = 0
total_time = 0
for n in range(6):
time = timeit.timeit(
'json({ "test":True }).output()',
setup="from sanic.response import json",
number=100000,
)
time = timeit.timeit('json({ "test":True }).output()', setup='from sanic.response import json', number=100000)
print("Took {} seconds".format(time))
total_time += time
times += 1
@@ -32,11 +26,7 @@ print("Running Old 100,000 times")
times = 0
total_time = 0
for n in range(6):
time = timeit.timeit(
'json({ "test":True }).output_old()',
setup="from sanic.response import json",
number=100000,
)
time = timeit.timeit('json({ "test":True }).output_old()', setup='from sanic.response import json', number=100000)
print("Took {} seconds".format(time))
total_time += time
times += 1

View File

@@ -2,10 +2,8 @@ import sys
import os
import inspect
currentdir = os.path.dirname(
os.path.abspath(inspect.getfile(inspect.currentframe()))
)
sys.path.insert(0, currentdir + "/../../../")
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
@@ -17,6 +15,5 @@ app = Sanic("test")
async def test(request):
return json({"test": True})
if __name__ == "__main__":
if __name__ == '__main__':
app.run(host="0.0.0.0", port=sys.argv[1])

View File

@@ -2,10 +2,8 @@ import sys
import os
import inspect
currentdir = os.path.dirname(
os.path.abspath(inspect.getfile(inspect.currentframe()))
)
sys.path.insert(0, currentdir + "/../../../")
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
@@ -19,7 +17,7 @@ async def test(request):
return json({"test": True})
@app.route("/sync", methods=["GET", "POST"])
@app.route("/sync", methods=['GET', 'POST'])
def test(request):
return json({"test": True})
@@ -46,14 +44,7 @@ def post_json(request):
@app.route("/query_string")
def query_string(request):
return json(
{
"parsed": True,
"args": request.args,
"url": request.url,
"query_string": request.query_string,
}
)
return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string})
import sys
@@ -61,6 +52,7 @@ import sys
app.run(host="0.0.0.0", port=sys.argv[1])
# import asyncio_redis
# import asyncpg
# async def setup(sanic, loop):

View File

@@ -5,14 +5,14 @@ from tornado import ioloop, web
class MainHandler(web.RequestHandler):
def get(self):
self.write(ujson.dumps({"test": True}))
self.write(ujson.dumps({'test': True}))
app = web.Application(
[(r"/", MainHandler)],
debug=False,
app = web.Application([
(r'/', MainHandler)
], debug=False,
compress_response=False,
static_hash_cache=True,
static_hash_cache=True
)
app.listen(8000)

View File

@@ -12,17 +12,16 @@ from wheezy.web.middleware import path_routing_middleware_factory
import ujson
class WelcomeHandler(BaseHandler):
def get(self):
response = HTTPResponse(content_type="application/json; charset=UTF-8")
response.write(ujson.dumps({"test": True}))
response = HTTPResponse(content_type='application/json; charset=UTF-8')
response.write(ujson.dumps({"test":True}))
return response
all_urls = [
url("", WelcomeHandler, name="default"),
# url('', welcome, name='welcome')
url('', WelcomeHandler, name='default'),
# url('', welcome, name='welcome')
]
@@ -30,19 +29,18 @@ options = {}
main = WSGIApplication(
middleware=[
bootstrap_defaults(url_mapping=all_urls),
path_routing_middleware_factory,
path_routing_middleware_factory
],
options=options,
options=options
)
if __name__ == "__main__":
if __name__ == '__main__':
import sys
from wsgiref.simple_server import make_server
try:
print("Visit http://localhost:{}/".format(sys.argv[-1]))
make_server("", int(sys.argv[-1]), main).serve_forever()
print('Visit http://localhost:{}/'.format(sys.argv[-1]))
make_server('', int(sys.argv[-1]), main).serve_forever()
except KeyboardInterrupt:
pass
print("\nThanks!")
print('\nThanks!')

View File

@@ -1,173 +1,127 @@
import asyncio
import logging
import sys
from inspect import isawaitable
import pytest
from sanic.exceptions import SanicException
from sanic.response import text
def uvloop_installed():
try:
import uvloop
return True
except ImportError:
return False
def test_app_loop_running(app):
@app.get("/test")
@app.get('/test')
async def handler(request):
assert isinstance(app.loop, asyncio.AbstractEventLoop)
return text("pass")
return text('pass')
request, response = app.test_client.get("/test")
assert response.text == "pass"
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher"
)
def test_create_asyncio_server(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
assert isawaitable(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is True
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher"
)
def test_asyncio_server_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
request, response = app.test_client.get('/test')
assert response.text == 'pass'
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 "
"running. Not supported with `create_server` function"
'Loop can only be retrieved after the app has started '
'running. Not supported with `create_server` function'
)
def test_app_run_raise_type_error(app):
with pytest.raises(TypeError) as excinfo:
app.run(loop="loop")
app.run(loop='loop')
assert str(excinfo.value) == (
"loop is not a valid argument. To use an existing loop, "
"change to create_server().\nSee more: "
"https://sanic.readthedocs.io/en/latest/sanic/deploying.html"
"#asynchronous-support"
'loop is not a valid argument. To use an existing loop, '
'change to create_server().\nSee more: '
'https://sanic.readthedocs.io/en/latest/sanic/deploying.html'
'#asynchronous-support'
)
def test_app_route_raise_value_error(app):
with pytest.raises(ValueError) as excinfo:
@app.route("/test")
@app.route('/test')
async def handler():
return text("test")
return text('test')
assert (
str(excinfo.value)
== "Required parameter `request` missing in the handler() route?"
)
assert str(excinfo.value) == 'Required parameter `request` missing in the handler() route?'
def test_app_handle_request_handler_is_none(app, monkeypatch):
def mockreturn(*args, **kwargs):
return None, [], {}, ""
return None, [], {}, ''
# Not sure how to make app.router.get() return None, so use mock here.
monkeypatch.setattr(app.router, "get", mockreturn)
monkeypatch.setattr(app.router, 'get', mockreturn)
@app.get("/test")
@app.get('/test')
def handler(request):
return text("test")
return text('test')
request, response = app.test_client.get("/test")
request, response = app.test_client.get('/test')
assert (
response.text
== "Error: 'None' was returned while requesting a handler from the router"
)
assert response.text == 'Error: \'None\' was returned while requesting a handler from the router'
@pytest.mark.parametrize("websocket_enabled", [True, False])
@pytest.mark.parametrize("enable", [True, False])
@pytest.mark.parametrize('websocket_enabled', [True, False])
@pytest.mark.parametrize('enable', [True, False])
def test_app_enable_websocket(app, websocket_enabled, enable):
app.websocket_enabled = websocket_enabled
app.enable_websocket(enable=enable)
assert app.websocket_enabled == enable
@app.websocket("/ws")
@app.websocket('/ws')
async def handler(request, ws):
await ws.send("test")
await ws.send('test')
assert app.websocket_enabled == True
def test_handle_request_with_nested_exception(app, monkeypatch):
err_msg = "Mock Exception"
err_msg = 'Mock Exception'
# Not sure how to raise an exception in app.error_handler.response(), use mock here
def mock_error_handler_response(*args, **kwargs):
raise Exception(err_msg)
monkeypatch.setattr(
app.error_handler, "response", mock_error_handler_response
)
monkeypatch.setattr(app.error_handler, 'response', mock_error_handler_response)
@app.get("/")
@app.get('/')
def handler(request):
raise Exception
return text('OK')
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert response.status == 500
assert response.text == "An error occurred while handling an error"
assert response.text == 'An error occurred while handling an error'
def test_handle_request_with_nested_exception_debug(app, monkeypatch):
err_msg = "Mock Exception"
err_msg = 'Mock Exception'
# Not sure how to raise an exception in app.error_handler.response(), use mock here
def mock_error_handler_response(*args, **kwargs):
raise Exception(err_msg)
monkeypatch.setattr(
app.error_handler, "response", mock_error_handler_response
)
monkeypatch.setattr(app.error_handler, 'response', mock_error_handler_response)
@app.get("/")
@app.get('/')
def handler(request):
raise Exception
return text('OK')
request, response = app.test_client.get("/", debug=True)
request, response = app.test_client.get('/', debug=True)
assert response.status == 500
assert response.text.startswith(
"Error while handling error: {}\nStack: Traceback (most recent call last):\n".format(
err_msg
)
'Error while handling error: {}\nStack: Traceback (most recent call last):\n'.format(err_msg)
)
@@ -175,22 +129,22 @@ def test_handle_request_with_nested_sanic_exception(app, monkeypatch, caplog):
# Not sure how to raise an exception in app.error_handler.response(), use mock here
def mock_error_handler_response(*args, **kwargs):
raise SanicException("Mock SanicException")
raise SanicException('Mock SanicException')
monkeypatch.setattr(
app.error_handler, "response", mock_error_handler_response
)
monkeypatch.setattr(app.error_handler, 'response', mock_error_handler_response)
@app.get("/")
@app.get('/')
def handler(request):
raise Exception
return text('OK')
caplog.set_level(logging.ERROR, logger="sanic.root")
with caplog.at_level(logging.ERROR):
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert response.status == 500
assert response.text == "Error: Mock SanicException"
assert (
"sanic.root",
assert response.text == 'Error: Mock SanicException'
assert caplog.record_tuples[0] == (
'sanic.root',
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) in caplog.record_tuples
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'"
)

View File

@@ -4,18 +4,17 @@ import asyncio
def test_bad_request_response(app):
lines = []
@app.listener("after_server_start")
@app.listener('after_server_start')
async def _request(sanic, loop):
connect = asyncio.open_connection("127.0.0.1", 42101)
connect = asyncio.open_connection('127.0.0.1', 42101)
reader, writer = await connect
writer.write(b"not http")
writer.write(b'not http')
while True:
line = await reader.readline()
if not line:
break
lines.append(line)
app.stop()
app.run(host="127.0.0.1", port=42101, debug=False)
assert lines[0] == b"HTTP/1.1 400 Bad Request\r\n"
assert lines[-1] == b"Error: Bad Request"
app.run(host='127.0.0.1', port=42101, debug=False)
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
assert lines[-1] == b'Error: Bad Request'

View File

@@ -1,180 +0,0 @@
from pytest import raises
from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.request import Request
from sanic.response import text, HTTPResponse
MIDDLEWARE_INVOKE_COUNTER = {"request": 0, "response": 0}
AUTH = "dGVzdDp0ZXN0Cg=="
def test_bp_group_indexing(app: Sanic):
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
group = Blueprint.group(blueprint_1, blueprint_2)
assert group[0] == blueprint_1
with raises(expected_exception=IndexError) as e:
_ = group[3]
def test_bp_group_with_additional_route_params(app: Sanic):
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
@blueprint_1.route(
"/request_path", methods=frozenset({"PUT", "POST"}), version=2
)
def blueprint_1_v2_method_with_put_and_post(request: Request):
if request.method == "PUT":
return text("PUT_OK")
elif request.method == "POST":
return text("POST_OK")
@blueprint_2.route(
"/route/<param>", methods=frozenset({"DELETE", "PATCH"}), name="test"
)
def blueprint_2_named_method(request: Request, param):
if request.method == "DELETE":
return text("DELETE_{}".format(param))
elif request.method == "PATCH":
return text("PATCH_{}".format(param))
blueprint_group = Blueprint.group(
blueprint_1, blueprint_2, url_prefix="/api"
)
@blueprint_group.middleware("request")
def authenticate_request(request: Request):
global AUTH
auth = request.headers.get("authorization")
if auth:
# Dummy auth check. We can have anything here and it's fine.
if AUTH not in auth:
return text("Unauthorized", status=401)
else:
return text("Unauthorized", status=401)
@blueprint_group.middleware("response")
def enhance_response_middleware(request: Request, response: HTTPResponse):
response.headers.add("x-test-middleware", "value")
app.blueprint(blueprint_group)
header = {"authorization": " ".join(["Basic", AUTH])}
_, response = app.test_client.put(
"/v2/api/bp1/request_path", headers=header
)
assert response.text == "PUT_OK"
assert response.headers.get("x-test-middleware") == "value"
_, response = app.test_client.post(
"/v2/api/bp1/request_path", headers=header
)
assert response.text == "POST_OK"
_, response = app.test_client.delete("/api/bp2/route/bp2", headers=header)
assert response.text == "DELETE_bp2"
_, response = app.test_client.patch("/api/bp2/route/bp2", headers=header)
assert response.text == "PATCH_bp2"
_, response = app.test_client.get("/v2/api/bp1/request_path")
assert response.status == 401
def test_bp_group(app: Sanic):
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
@blueprint_1.route("/")
def blueprint_1_default_route(request):
return text("BP1_OK")
@blueprint_2.route("/")
def blueprint_2_default_route(request):
return text("BP2_OK")
blueprint_group_1 = Blueprint.group(
blueprint_1, blueprint_2, url_prefix="/bp"
)
blueprint_3 = Blueprint("blueprint_3", url_prefix="/bp3")
@blueprint_group_1.middleware("request")
def blueprint_group_1_middleware(request):
global MIDDLEWARE_INVOKE_COUNTER
MIDDLEWARE_INVOKE_COUNTER["request"] += 1
@blueprint_3.route("/")
def blueprint_3_default_route(request):
return text("BP3_OK")
blueprint_group_2 = Blueprint.group(
blueprint_group_1, blueprint_3, url_prefix="/api"
)
@blueprint_group_2.middleware("response")
def blueprint_group_2_middleware(request, response):
global MIDDLEWARE_INVOKE_COUNTER
MIDDLEWARE_INVOKE_COUNTER["response"] += 1
app.blueprint(blueprint_group_2)
@app.route("/")
def app_default_route(request):
return text("APP_OK")
_, response = app.test_client.get("/")
assert response.text == "APP_OK"
_, response = app.test_client.get("/api/bp/bp1")
assert response.text == "BP1_OK"
_, response = app.test_client.get("/api/bp/bp2")
assert response.text == "BP2_OK"
_, response = app.test_client.get("/api/bp3")
assert response.text == "BP3_OK"
assert MIDDLEWARE_INVOKE_COUNTER["response"] == 4
assert MIDDLEWARE_INVOKE_COUNTER["request"] == 4
def test_bp_group_list_operations(app: Sanic):
blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1")
blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2")
@blueprint_1.route("/")
def blueprint_1_default_route(request):
return text("BP1_OK")
@blueprint_2.route("/")
def blueprint_2_default_route(request):
return text("BP2_OK")
blueprint_group_1 = Blueprint.group(
blueprint_1, blueprint_2, url_prefix="/bp"
)
blueprint_3 = Blueprint("blueprint_2", url_prefix="/bp3")
@blueprint_3.route("/second")
def blueprint_3_second_route(request):
return text("BP3_OK")
assert len(blueprint_group_1) == 2
blueprint_group_1.append(blueprint_3)
assert len(blueprint_group_1) == 3
del blueprint_group_1[2]
assert len(blueprint_group_1) == 2
blueprint_group_1[1] = blueprint_3
assert len(blueprint_group_1) == 2
assert blueprint_group_1.url_prefix == "/bp"

View File

@@ -17,13 +17,12 @@ from sanic.views import CompositionView
# GET
# ------------------------------------------------------------ #
@pytest.fixture(scope="module")
@pytest.fixture(scope='module')
def static_file_directory():
"""The static directory to serve"""
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, "static")
static_directory = os.path.join(current_directory, 'static')
return static_directory
@@ -33,23 +32,21 @@ def get_file_path(static_file_directory, file_name):
def get_file_content(static_file_directory, file_name):
"""The content of the static file to check"""
with open(get_file_path(static_file_directory, file_name), "rb") as file:
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
return file.read()
@pytest.mark.parametrize("method", HTTP_METHODS)
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_routes_get(app, method):
bp = Blueprint("test_text")
bp = Blueprint('test_text')
method = method.lower()
func = getattr(bp, method)
if callable(func):
@func("/{}".format(method), version=1)
@func('/{}'.format(method), version=1)
def handler(request):
return text("OK")
return text('OK')
else:
print(func)
raise Exception("{} is not callable".format(func))
@@ -58,231 +55,245 @@ def test_versioned_routes_get(app, method):
client_method = getattr(app.test_client, method)
request, response = client_method("/v1/{}".format(method))
request, response = client_method('/v1/{}'.format(method))
assert response.status == 200
def test_bp(app):
bp = Blueprint("test_text")
bp = Blueprint('test_text')
@bp.route("/")
@bp.route('/')
def handler(request):
return text("Hello")
return text('Hello')
app.blueprint(bp)
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert app.is_request_stream is False
assert response.text == "Hello"
assert response.text == 'Hello'
def test_bp_strict_slash(app):
bp = Blueprint("test_text")
bp = Blueprint('test_text')
@bp.get("/get", strict_slashes=True)
@bp.get('/get', strict_slashes=True)
def get_handler(request):
return text("OK")
return text('OK')
@bp.post("/post/", strict_slashes=True)
@bp.post('/post/', strict_slashes=True)
def post_handler(request):
return text("OK")
return text('OK')
app.blueprint(bp)
request, response = app.test_client.get("/get")
assert response.text == "OK"
request, response = app.test_client.get('/get')
assert response.text == 'OK'
assert response.json is None
request, response = app.test_client.get("/get/")
request, response = app.test_client.get('/get/')
assert response.status == 404
request, response = app.test_client.post("/post/")
assert response.text == "OK"
request, response = app.test_client.post('/post/')
assert response.text == 'OK'
request, response = app.test_client.post("/post")
request, response = app.test_client.post('/post')
assert response.status == 404
def test_bp_strict_slash_default_value(app):
bp = Blueprint("test_text", strict_slashes=True)
bp = Blueprint('test_text', strict_slashes=True)
@bp.get("/get")
@bp.get('/get')
def get_handler(request):
return text("OK")
return text('OK')
@bp.post("/post/")
@bp.post('/post/')
def post_handler(request):
return text("OK")
return text('OK')
app.blueprint(bp)
request, response = app.test_client.get("/get/")
request, response = app.test_client.get('/get/')
assert response.status == 404
request, response = app.test_client.post("/post")
request, response = app.test_client.post('/post')
assert response.status == 404
def test_bp_strict_slash_without_passing_default_value(app):
bp = Blueprint("test_text")
bp = Blueprint('test_text')
@bp.get("/get")
@bp.get('/get')
def get_handler(request):
return text("OK")
return text('OK')
@bp.post("/post/")
@bp.post('/post/')
def post_handler(request):
return text("OK")
return text('OK')
app.blueprint(bp)
request, response = app.test_client.get("/get/")
assert response.text == "OK"
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
request, response = app.test_client.post("/post")
assert response.text == "OK"
request, response = app.test_client.post('/post')
assert response.text == 'OK'
def test_bp_strict_slash_default_value_can_be_overwritten(app):
bp = Blueprint("test_text", strict_slashes=True)
bp = Blueprint('test_text', strict_slashes=True)
@bp.get("/get", strict_slashes=False)
@bp.get('/get', strict_slashes=False)
def get_handler(request):
return text("OK")
return text('OK')
@bp.post("/post/", strict_slashes=False)
@bp.post('/post/', strict_slashes=False)
def post_handler(request):
return text("OK")
return text('OK')
app.blueprint(bp)
request, response = app.test_client.get("/get/")
assert response.text == "OK"
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
request, response = app.test_client.post("/post")
assert response.text == "OK"
request, response = app.test_client.post('/post')
assert response.text == 'OK'
def test_bp_with_url_prefix(app):
bp = Blueprint("test_text", url_prefix="/test1")
bp = Blueprint('test_text', url_prefix='/test1')
@bp.route("/")
@bp.route('/')
def handler(request):
return text("Hello")
return text('Hello')
app.blueprint(bp)
request, response = app.test_client.get("/test1/")
request, response = app.test_client.get('/test1/')
assert response.text == "Hello"
assert response.text == 'Hello'
def test_several_bp_with_url_prefix(app):
bp = Blueprint("test_text", url_prefix="/test1")
bp2 = Blueprint("test_text2", url_prefix="/test2")
bp = Blueprint('test_text', url_prefix='/test1')
bp2 = Blueprint('test_text2', url_prefix='/test2')
@bp.route("/")
@bp.route('/')
def handler(request):
return text("Hello")
return text('Hello')
@bp2.route("/")
@bp2.route('/')
def handler2(request):
return text("Hello2")
return text('Hello2')
app.blueprint(bp)
app.blueprint(bp2)
request, response = app.test_client.get("/test1/")
assert response.text == "Hello"
request, response = app.test_client.get('/test1/')
assert response.text == 'Hello'
request, response = app.test_client.get("/test2/")
assert response.text == "Hello2"
request, response = app.test_client.get('/test2/')
assert response.text == 'Hello2'
def test_bp_with_host(app):
bp = Blueprint("test_bp_host", url_prefix="/test1", host="example.com")
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
@bp.route("/")
@bp.route('/')
def handler1(request):
return text("Hello")
return text('Hello')
@bp.route("/", host="sub.example.com")
@bp.route('/', host="sub.example.com")
def handler2(request):
return text("Hello subdomain!")
return text('Hello subdomain!')
app.blueprint(bp)
headers = {"Host": "example.com"}
request, response = app.test_client.get("/test1/", headers=headers)
assert response.text == "Hello"
request, response = app.test_client.get(
'/test1/',
headers=headers)
assert response.text == 'Hello'
headers = {"Host": "sub.example.com"}
request, response = app.test_client.get("/test1/", headers=headers)
request, response = app.test_client.get(
'/test1/',
headers=headers)
assert response.text == "Hello subdomain!"
assert response.text == 'Hello subdomain!'
def test_several_bp_with_host(app):
bp = Blueprint("test_text", url_prefix="/test", host="example.com")
bp2 = Blueprint("test_text2", url_prefix="/test", host="sub.example.com")
bp = Blueprint('test_text',
url_prefix='/test',
host="example.com")
bp2 = Blueprint('test_text2',
url_prefix='/test',
host="sub.example.com")
@bp.route("/")
@bp.route('/')
def handler(request):
return text("Hello")
return text('Hello')
@bp2.route("/")
@bp2.route('/')
def handler1(request):
return text("Hello2")
return text('Hello2')
@bp2.route("/other/")
@bp2.route('/other/')
def handler2(request):
return text("Hello3")
return text('Hello3')
app.blueprint(bp)
app.blueprint(bp2)
assert bp.host == "example.com"
headers = {"Host": "example.com"}
request, response = app.test_client.get("/test/", headers=headers)
assert response.text == "Hello"
request, response = app.test_client.get(
'/test/',
headers=headers)
assert response.text == 'Hello'
assert bp2.host == "sub.example.com"
headers = {"Host": "sub.example.com"}
request, response = app.test_client.get("/test/", headers=headers)
request, response = app.test_client.get(
'/test/',
headers=headers)
assert response.text == "Hello2"
request, response = app.test_client.get("/test/other/", headers=headers)
assert response.text == "Hello3"
assert response.text == 'Hello2'
request, response = app.test_client.get(
'/test/other/',
headers=headers)
assert response.text == 'Hello3'
def test_bp_middleware(app):
blueprint = Blueprint("test_middleware")
blueprint = Blueprint('test_middleware')
@blueprint.middleware("response")
@blueprint.middleware('response')
async def process_response(request, response):
return text("OK")
return text('OK')
@app.route("/")
@app.route('/')
async def handler(request):
return text("FAIL")
return text('FAIL')
app.blueprint(blueprint)
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
def test_bp_exception_handler(app):
blueprint = Blueprint("test_middleware")
blueprint = Blueprint('test_middleware')
@blueprint.route("/1")
@blueprint.route('/1')
def handler_1(request):
raise InvalidUsage("OK")
@blueprint.route("/2")
@blueprint.route('/2')
def handler_2(request):
raise ServerError("OK")
@blueprint.route("/3")
@blueprint.route('/3')
def handler_3(request):
raise NotFound("OK")
@@ -292,131 +303,131 @@ def test_bp_exception_handler(app):
app.blueprint(blueprint)
request, response = app.test_client.get("/1")
request, response = app.test_client.get('/1')
assert response.status == 400
request, response = app.test_client.get("/2")
request, response = app.test_client.get('/2')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
request, response = app.test_client.get("/3")
request, response = app.test_client.get('/3')
assert response.status == 200
def test_bp_listeners(app):
blueprint = Blueprint("test_middleware")
blueprint = Blueprint('test_middleware')
order = []
@blueprint.listener("before_server_start")
@blueprint.listener('before_server_start')
def handler_1(sanic, loop):
order.append(1)
@blueprint.listener("after_server_start")
@blueprint.listener('after_server_start')
def handler_2(sanic, loop):
order.append(2)
@blueprint.listener("after_server_start")
@blueprint.listener('after_server_start')
def handler_3(sanic, loop):
order.append(3)
@blueprint.listener("before_server_stop")
@blueprint.listener('before_server_stop')
def handler_4(sanic, loop):
order.append(5)
@blueprint.listener("before_server_stop")
@blueprint.listener('before_server_stop')
def handler_5(sanic, loop):
order.append(4)
@blueprint.listener("after_server_stop")
@blueprint.listener('after_server_stop')
def handler_6(sanic, loop):
order.append(6)
app.blueprint(blueprint)
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert order == [1, 2, 3, 4, 5, 6]
def test_bp_static(app):
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, "rb") as file:
with open(current_file, 'rb') as file:
current_file_contents = file.read()
blueprint = Blueprint("test_static")
blueprint = Blueprint('test_static')
blueprint.static("/testing.file", current_file)
blueprint.static('/testing.file', current_file)
app.blueprint(blueprint)
request, response = app.test_client.get("/testing.file")
request, response = app.test_client.get('/testing.file')
assert response.status == 200
assert response.body == current_file_contents
@pytest.mark.parametrize("file_name", ["test.html"])
@pytest.mark.parametrize('file_name', ['test.html'])
def test_bp_static_content_type(app, file_name):
# This is done here, since no other test loads a file here
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, "static")
static_directory = os.path.join(current_directory, 'static')
blueprint = Blueprint("test_static")
blueprint = Blueprint('test_static')
blueprint.static(
"/testing.file",
'/testing.file',
get_file_path(static_directory, file_name),
content_type="text/html; charset=utf-8",
content_type='text/html; charset=utf-8'
)
app.blueprint(blueprint)
request, response = app.test_client.get("/testing.file")
request, response = app.test_client.get('/testing.file')
assert response.status == 200
assert response.body == get_file_content(static_directory, file_name)
assert response.headers["Content-Type"] == "text/html; charset=utf-8"
assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
def test_bp_shorthand(app):
blueprint = Blueprint("test_shorhand_routes")
blueprint = Blueprint('test_shorhand_routes')
ev = asyncio.Event()
@blueprint.get("/get")
@blueprint.get('/get')
def handler(request):
assert request.stream is None
return text("OK")
return text('OK')
@blueprint.put("/put")
@blueprint.put('/put')
def put_handler(request):
assert request.stream is None
return text("OK")
return text('OK')
@blueprint.post("/post")
@blueprint.post('/post')
def post_handler(request):
assert request.stream is None
return text("OK")
return text('OK')
@blueprint.head("/head")
@blueprint.head('/head')
def head_handler(request):
assert request.stream is None
return text("OK")
return text('OK')
@blueprint.options("/options")
@blueprint.options('/options')
def options_handler(request):
assert request.stream is None
return text("OK")
return text('OK')
@blueprint.patch("/patch")
@blueprint.patch('/patch')
def patch_handler(request):
assert request.stream is None
return text("OK")
return text('OK')
@blueprint.delete("/delete")
@blueprint.delete('/delete')
def delete_handler(request):
assert request.stream is None
return text("OK")
return text('OK')
@blueprint.websocket("/ws/", strict_slashes=True)
@blueprint.websocket('/ws/', strict_slashes=True)
async def websocket_handler(request, ws):
assert request.stream is None
ev.set()
@@ -425,146 +436,137 @@ def test_bp_shorthand(app):
assert app.is_request_stream is False
request, response = app.test_client.get("/get")
assert response.text == "OK"
request, response = app.test_client.get('/get')
assert response.text == 'OK'
request, response = app.test_client.post("/get")
request, response = app.test_client.post('/get')
assert response.status == 405
request, response = app.test_client.put("/put")
assert response.text == "OK"
request, response = app.test_client.put('/put')
assert response.text == 'OK'
request, response = app.test_client.get("/post")
request, response = app.test_client.get('/post')
assert response.status == 405
request, response = app.test_client.post("/post")
assert response.text == "OK"
request, response = app.test_client.post('/post')
assert response.text == 'OK'
request, response = app.test_client.get("/post")
request, response = app.test_client.get('/post')
assert response.status == 405
request, response = app.test_client.head("/head")
request, response = app.test_client.head('/head')
assert response.status == 200
request, response = app.test_client.get("/head")
request, response = app.test_client.get('/head')
assert response.status == 405
request, response = app.test_client.options("/options")
assert response.text == "OK"
request, response = app.test_client.options('/options')
assert response.text == 'OK'
request, response = app.test_client.get("/options")
request, response = app.test_client.get('/options')
assert response.status == 405
request, response = app.test_client.patch("/patch")
assert response.text == "OK"
request, response = app.test_client.patch('/patch')
assert response.text == 'OK'
request, response = app.test_client.get("/patch")
request, response = app.test_client.get('/patch')
assert response.status == 405
request, response = app.test_client.delete("/delete")
assert response.text == "OK"
request, response = app.test_client.delete('/delete')
assert response.text == 'OK'
request, response = app.test_client.get("/delete")
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",
},
)
request, response = app.test_client.get('/ws/', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'})
assert response.status == 101
assert ev.is_set()
def test_bp_group(app):
deep_0 = Blueprint("deep_0", url_prefix="/deep")
deep_1 = Blueprint("deep_1", url_prefix="/deep1")
deep_0 = Blueprint('deep_0', url_prefix='/deep')
deep_1 = Blueprint('deep_1', url_prefix='/deep1')
@deep_0.route("/")
@deep_0.route('/')
def handler(request):
return text("D0_OK")
return text('D0_OK')
@deep_1.route("/bottom")
@deep_1.route('/bottom')
def bottom_handler(request):
return text("D1B_OK")
return text('D1B_OK')
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix="/mid")
mid_1 = Blueprint("mid_tier", url_prefix="/mid1")
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
@mid_1.route("/")
@mid_1.route('/')
def handler1(request):
return text("M1_OK")
return text('M1_OK')
top = Blueprint.group(mid_0, mid_1)
app.blueprint(top)
@app.route("/")
@app.route('/')
def handler2(request):
return text("TOP_OK")
return text('TOP_OK')
request, response = app.test_client.get("/")
assert response.text == "TOP_OK"
request, response = app.test_client.get('/')
assert response.text == 'TOP_OK'
request, response = app.test_client.get("/mid1")
assert response.text == "M1_OK"
request, response = app.test_client.get('/mid1')
assert response.text == 'M1_OK'
request, response = app.test_client.get("/mid/deep")
assert response.text == "D0_OK"
request, response = app.test_client.get('/mid/deep')
assert response.text == 'D0_OK'
request, response = app.test_client.get("/mid/deep1/bottom")
assert response.text == "D1B_OK"
request, response = app.test_client.get('/mid/deep1/bottom')
assert response.text == 'D1B_OK'
def test_bp_group_with_default_url_prefix(app):
from sanic.response import json
bp_resources = Blueprint('bp_resources')
bp_resources = Blueprint("bp_resources")
@bp_resources.get("/")
@bp_resources.get('/')
def list_resources_handler(request):
resource = {}
return json([resource])
bp_resource = Blueprint("bp_resource", url_prefix="/<resource_id>")
bp_resource = Blueprint('bp_resource', url_prefix='/<resource_id>')
@bp_resource.get("/")
@bp_resource.get('/')
def get_resource_hander(request, resource_id):
resource = {"resource_id": resource_id}
resource = {'resource_id': resource_id}
return json(resource)
bp_resources_group = Blueprint.group(
bp_resources, bp_resource, url_prefix="/resources"
)
bp_api_v1 = Blueprint("bp_api_v1")
bp_resources_group = Blueprint.group(bp_resources, bp_resource,
url_prefix='/resources')
bp_api_v1 = Blueprint('bp_api_v1')
@bp_api_v1.get("/info")
@bp_api_v1.get('/info')
def api_v1_info(request):
return text("api_version: v1")
return text('api_version: v1')
bp_api_v1_group = Blueprint.group(
bp_api_v1, bp_resources_group, url_prefix="/v1"
)
bp_api_group = Blueprint.group(bp_api_v1_group, url_prefix="/api")
bp_api_v1_group = Blueprint.group(bp_api_v1, bp_resources_group,
url_prefix='/v1')
bp_api_group = Blueprint.group(bp_api_v1_group, url_prefix='/api')
app.blueprint(bp_api_group)
request, response = app.test_client.get("/api/v1/info")
assert response.text == "api_version: v1"
request, response = app.test_client.get('/api/v1/info')
assert response.text == 'api_version: v1'
request, response = app.test_client.get("/api/v1/resources")
request, response = app.test_client.get('/api/v1/resources')
assert response.json == [{}]
from uuid import uuid4
resource_id = str(uuid4())
request, response = app.test_client.get(
"/api/v1/resources/{0}".format(resource_id)
)
assert response.json == {"resource_id": resource_id}
'/api/v1/resources/{0}'.format(resource_id))
assert response.json == {'resource_id': resource_id}
def test_blueprint_middleware_with_args(app: Sanic):
@@ -586,22 +588,19 @@ def test_blueprint_middleware_with_args(app: Sanic):
app.blueprint(bp)
_, response = app.test_client.get(
"/wa", headers={"content-type": "application/json"}
)
_, response = app.test_client.get("/wa", headers={"content-type": "application/json"})
assert response.text == "value"
_, response = app.test_client.get(
"/wa", headers={"content-type": "plain/text"}
)
_, response = app.test_client.get("/wa", headers={"content-type": "plain/text"})
assert response.json.get("test") == "value"
d = {}
@pytest.mark.parametrize("file_name", ["test.file"])
@pytest.mark.parametrize('file_name',
['test.file'])
def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, "rb") as file:
with open(current_file, 'rb') as file:
current_file_contents = file.read()
bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False)
@@ -610,12 +609,11 @@ def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
"/test.file/",
get_file_path(static_file_directory, file_name),
name="static.testing",
strict_slashes=True,
)
strict_slashes=True)
app.blueprint(bp)
uri = app.url_for("static", name="static.testing")
uri = app.url_for('static', name='static.testing')
assert uri == "/static/test.file"
_, response = app.test_client.get("/static/test.file")
@@ -629,7 +627,9 @@ def test_route_handler_add(app: Sanic):
view = CompositionView()
async def get_handler(request):
return json({"response": "OK"})
return json({
"response": "OK"
})
view.add(["GET"], get_handler, stream=False)
@@ -637,7 +637,10 @@ def test_route_handler_add(app: Sanic):
return text("OK")
bp = Blueprint(name="handler", url_prefix="/handler")
bp.add_route(default_handler, uri="/default/", strict_slashes=True)
bp.add_route(
default_handler,
uri="/default/",
strict_slashes=True)
bp.add_route(view, uri="/view", name="test")
@@ -662,21 +665,18 @@ 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",
},
)
_, response = app.test_client.get("/ws/test", headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'
})
assert response.status == 101
assert event.is_set()
def test_duplicate_blueprint(app):
bp_name = "bp"
bp_name = 'bp'
bp = Blueprint(bp_name)
bp1 = Blueprint(bp_name)
@@ -687,13 +687,13 @@ def test_duplicate_blueprint(app):
assert str(excinfo.value) == (
'A blueprint with the name "{}" is already registered. '
"Blueprint names must be unique."
'Blueprint names must be unique.'
).format(bp_name)
@pytest.mark.parametrize("debug", [True, False, None])
@pytest.mark.parametrize('debug', [True, False, None])
def test_register_blueprint(app, debug):
bp = Blueprint("bp")
bp = Blueprint('bp')
app.debug = debug
with pytest.warns(DeprecationWarning) as record:

View File

@@ -6,7 +6,6 @@ from textwrap import dedent
import pytest
from sanic import Sanic
from sanic.config import Config, DEFAULT_CONFIG
from sanic.exceptions import PyFileError
@@ -14,18 +13,18 @@ from sanic.exceptions import PyFileError
def temp_path():
""" a simple cross platform replacement for NamedTemporaryFile """
with TemporaryDirectory() as td:
yield Path(td, "file")
yield Path(td, 'file')
def test_load_from_object(app):
class Config:
not_for_config = "should not be used"
CONFIG_VALUE = "should be used"
not_for_config = 'should not be used'
CONFIG_VALUE = 'should be used'
app.config.from_object(Config)
assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config
assert 'CONFIG_VALUE' in app.config
assert app.config.CONFIG_VALUE == 'should be used'
assert 'not_for_config' not in app.config
def test_auto_load_env():
@@ -35,23 +34,16 @@ def test_auto_load_env():
del environ["SANIC_TEST_ANSWER"]
def test_auto_load_bool_env():
environ["SANIC_TEST_ANSWER"] = "True"
app = Sanic()
assert app.config.TEST_ANSWER == True
del environ["SANIC_TEST_ANSWER"]
def test_dont_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(load_env=False)
assert getattr(app.config, "TEST_ANSWER", None) is None
assert getattr(app.config, 'TEST_ANSWER', None) is None
del environ["SANIC_TEST_ANSWER"]
def test_load_env_prefix():
environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(load_env="MYAPP_")
app = Sanic(load_env='MYAPP_')
assert app.config.TEST_ANSWER == 42
del environ["MYAPP_TEST_ANSWER"]
@@ -71,47 +63,43 @@ def test_load_env_prefix_string_value():
def test_load_from_file(app):
config = dedent(
"""
config = dedent("""
VALUE = 'some value'
condition = 1 == 1
if condition:
CONDITIONAL = 'should be set'
"""
)
""")
with temp_path() as config_path:
config_path.write_text(config)
app.config.from_pyfile(str(config_path))
assert "VALUE" in app.config
assert app.config.VALUE == "some value"
assert "CONDITIONAL" in app.config
assert app.config.CONDITIONAL == "should be set"
assert "condition" not in app.config
assert 'VALUE' in app.config
assert app.config.VALUE == 'some value'
assert 'CONDITIONAL' in app.config
assert app.config.CONDITIONAL == 'should be set'
assert 'condition' not in app.config
def test_load_from_missing_file(app):
with pytest.raises(IOError):
app.config.from_pyfile("non-existent file")
app.config.from_pyfile('non-existent file')
def test_load_from_envvar(app):
config = "VALUE = 'some value'"
with temp_path() as config_path:
config_path.write_text(config)
environ["APP_CONFIG"] = str(config_path)
app.config.from_envvar("APP_CONFIG")
assert "VALUE" in app.config
assert app.config.VALUE == "some value"
environ['APP_CONFIG'] = str(config_path)
app.config.from_envvar('APP_CONFIG')
assert 'VALUE' in app.config
assert app.config.VALUE == 'some value'
def test_load_from_missing_envvar(app):
with pytest.raises(RuntimeError) as e:
app.config.from_envvar("non-existent variable")
assert str(e.value) == (
"The environment variable 'non-existent "
app.config.from_envvar('non-existent variable')
assert str(e.value) == ("The environment variable 'non-existent "
"variable' is not set and thus configuration "
"could not be loaded."
)
"could not be loaded.")
def test_load_config_from_file_invalid_syntax(app):
@@ -144,115 +132,6 @@ def test_overwrite_exisiting_config_ignore_lowercase(app):
def test_missing_config(app):
with pytest.raises(
AttributeError, match="Config has no 'NON_EXISTENT'"
) as e:
_ = app.config.NON_EXISTENT
def test_config_defaults():
"""
load DEFAULT_CONFIG
"""
conf = Config()
for key, value in DEFAULT_CONFIG.items():
assert getattr(conf, key) == value
def test_config_custom_defaults():
"""
we should have all the variables from defaults rewriting them with custom defaults passed in
Config
"""
custom_defaults = {
"REQUEST_MAX_SIZE": 1,
"KEEP_ALIVE": False,
"ACCESS_LOG": False,
}
conf = Config(defaults=custom_defaults)
for key, value in DEFAULT_CONFIG.items():
if key in custom_defaults.keys():
value = custom_defaults[key]
assert getattr(conf, key) == value
def test_config_custom_defaults_with_env():
"""
test that environment variables has higher priority than DEFAULT_CONFIG and passed defaults dict
"""
custom_defaults = {
"REQUEST_MAX_SIZE123": 1,
"KEEP_ALIVE123": False,
"ACCESS_LOG123": False,
}
environ_defaults = {
"SANIC_REQUEST_MAX_SIZE123": "2",
"SANIC_KEEP_ALIVE123": "True",
"SANIC_ACCESS_LOG123": "False",
}
for key, value in environ_defaults.items():
environ[key] = value
conf = Config(defaults=custom_defaults)
for key, value in DEFAULT_CONFIG.items():
if "SANIC_" + key in environ_defaults.keys():
value = environ_defaults["SANIC_" + key]
try:
value = int(value)
except ValueError:
if value in ["True", "False"]:
value = value == "True"
assert getattr(conf, key) == value
for key, value in environ_defaults.items():
del environ[key]
def test_config_access_log_passing_in_run(app):
assert app.config.ACCESS_LOG == True
@app.listener("after_server_start")
async def _request(sanic, loop):
app.stop()
app.run(port=1340, access_log=False)
assert app.config.ACCESS_LOG == False
app.run(port=1340, access_log=True)
assert app.config.ACCESS_LOG == True
async def test_config_access_log_passing_in_create_server(app):
assert app.config.ACCESS_LOG == True
@app.listener("after_server_start")
async def _request(sanic, loop):
app.stop()
await app.create_server(
port=1341, access_log=False, return_asyncio_server=True
)
assert app.config.ACCESS_LOG == False
await app.create_server(
port=1342, access_log=True, return_asyncio_server=True
)
assert app.config.ACCESS_LOG == True
def test_config_rewrite_keep_alive():
config = Config()
assert config.KEEP_ALIVE == DEFAULT_CONFIG["KEEP_ALIVE"]
config = Config(keep_alive=True)
assert config.KEEP_ALIVE == True
config = Config(keep_alive=False)
assert config.KEEP_ALIVE == False
# use defaults
config = Config(defaults={"KEEP_ALIVE": False})
assert config.KEEP_ALIVE == False
config = Config(defaults={"KEEP_ALIVE": True})
assert config.KEEP_ALIVE == True
with pytest.raises(AttributeError) as e:
app.config.NON_EXISTENT
assert str(e.value) == ("Config has no 'NON_EXISTENT'")

View File

@@ -2,105 +2,115 @@ 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
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
def test_cookies(app):
@app.route("/")
@app.route('/')
def handler(request):
response = text("Cookies are: {}".format(request.cookies["test"]))
response.cookies["right_back"] = "at you"
response = text('Cookies are: {}'.format(request.cookies['test']))
response.cookies['right_back'] = 'at you'
return response
request, response = app.test_client.get("/", cookies={"test": "working!"})
request, response = app.test_client.get('/', cookies={"test": "working!"})
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get("Set-Cookie", {}))
response_cookies.load(response.headers.get('Set-Cookie', {}))
assert response.text == "Cookies are: working!"
assert response_cookies["right_back"].value == "at you"
assert response.text == 'Cookies are: working!'
assert response_cookies['right_back'].value == 'at you'
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
@pytest.mark.parametrize("httponly,expected", [
(False, False),
(True, True),
])
def test_false_cookies_encoded(app, httponly, expected):
@app.route("/")
@app.route('/')
def handler(request):
response = text("hello cookies")
response.cookies["hello"] = "world"
response.cookies["hello"]["httponly"] = httponly
return text(response.cookies["hello"].encode("utf8"))
response = text('hello cookies')
response.cookies['hello'] = 'world'
response.cookies['hello']['httponly'] = httponly
return text(response.cookies['hello'].encode('utf8'))
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert ("HttpOnly" in response.text) == expected
assert ('HttpOnly' in response.text) == expected
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
@pytest.mark.parametrize("httponly,expected", [
(False, False),
(True, True),
])
def test_false_cookies(app, httponly, expected):
@app.route("/")
@app.route('/')
def handler(request):
response = text("hello cookies")
response.cookies["right_back"] = "at you"
response.cookies["right_back"]["httponly"] = httponly
response = text('hello cookies')
response.cookies['right_back'] = 'at you'
response.cookies['right_back']['httponly'] = httponly
return response
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get("Set-Cookie", {}))
response_cookies.load(response.headers.get('Set-Cookie', {}))
assert ("HttpOnly" in response_cookies["right_back"].output()) == expected
assert ('HttpOnly' in response_cookies['right_back'].output()) == expected
def test_http2_cookies(app):
@app.route("/")
@app.route('/')
async def handler(request):
response = text("Cookies are: {}".format(request.cookies["test"]))
response = text('Cookies are: {}'.format(request.cookies['test']))
return response
headers = {"cookie": "test=working!"}
request, response = app.test_client.get("/", headers=headers)
headers = {'cookie': 'test=working!'}
request, response = app.test_client.get('/', headers=headers)
assert response.text == "Cookies are: working!"
assert response.text == 'Cookies are: working!'
def test_cookie_options(app):
@app.route("/")
@app.route('/')
def handler(request):
response = text("OK")
response.cookies["test"] = "at you"
response.cookies["test"]["httponly"] = True
response.cookies["test"]["expires"] = datetime.now() + timedelta(
seconds=10
)
response.cookies['test'] = 'at you'
response.cookies['test']['httponly'] = True
response.cookies['test']['expires'] = (datetime.now() +
timedelta(seconds=10))
return response
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get("Set-Cookie", {}))
response_cookies.load(response.headers.get('Set-Cookie', {}))
assert response_cookies["test"].value == "at you"
assert response_cookies["test"]["httponly"] is True
assert response_cookies['test'].value == 'at you'
assert response_cookies['test']['httponly'] is True
def test_cookie_deletion(app):
@app.route("/")
@app.route('/')
def handler(request):
response = text("OK")
del response.cookies["i_want_to_die"]
response.cookies["i_never_existed"] = "testing"
del response.cookies["i_never_existed"]
del response.cookies['i_want_to_die']
response.cookies['i_never_existed'] = 'testing'
del response.cookies['i_never_existed']
return response
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get("Set-Cookie", {}))
response_cookies.load(response.headers.get('Set-Cookie', {}))
assert int(response_cookies["i_want_to_die"]["max-age"]) == 0
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():
@@ -124,67 +134,58 @@ def test_cookie_set_unknown_property():
def test_cookie_set_same_key(app):
cookies = {"test": "wait"}
cookies = {'test': 'wait'}
@app.get("/")
@app.get('/')
def handler(request):
response = text("pass")
response.cookies["test"] = "modified"
response.cookies["test"] = "pass"
response = text('pass')
response.cookies['test'] = 'modified'
response.cookies['test'] = 'pass'
return response
request, response = app.test_client.get("/", cookies=cookies)
request, response = app.test_client.get('/', cookies=cookies)
assert response.status == 200
assert response.cookies["test"].value == "pass"
assert response.cookies['test'].value == 'pass'
@pytest.mark.parametrize("max_age", ["0", 30, 30.0, 30.1, "30", "test"])
@pytest.mark.parametrize('max_age', ['0', 30, '30'])
def test_cookie_max_age(app, max_age):
cookies = {"test": "wait"}
cookies = {'test': 'wait'}
@app.get("/")
@app.get('/')
def handler(request):
response = text("pass")
response.cookies["test"] = "pass"
response.cookies["test"]["max-age"] = max_age
response = text('pass')
response.cookies['test'] = 'pass'
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)
assert response.status == 200
assert response.cookies["test"].value == "pass"
if str(max_age).isdigit() and int(max_age) == float(max_age):
assert response.cookies["test"]["max-age"] == str(max_age)
else:
assert response.cookies["test"]["max-age"] == str(DEFAULT_MAX_AGE)
assert response.cookies['test'].value == 'pass'
assert response.cookies['test']['max-age'] == str(max_age)
@pytest.mark.parametrize("expires", [datetime.now() + timedelta(seconds=60)])
@pytest.mark.parametrize('expires', [
datetime.now() + timedelta(seconds=60),
'Fri, 21-Dec-2018 15:30:00 GMT'
])
def test_cookie_expires(app, expires):
cookies = {"test": "wait"}
cookies = {'test': 'wait'}
@app.get("/")
@app.get('/')
def handler(request):
response = text("pass")
response.cookies["test"] = "pass"
response.cookies["test"]["expires"] = expires
response = text('pass')
response.cookies['test'] = 'pass'
response.cookies['test']['expires'] = expires
return response
request, response = app.test_client.get("/", cookies=cookies)
request, response = app.test_client.get('/', cookies=cookies)
assert response.status == 200
assert response.cookies["test"].value == "pass"
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
@pytest.mark.parametrize("expires", ["Fri, 21-Dec-2018 15:30:00 GMT"])
def test_cookie_expires_illegal_instance_type(expires):
c = Cookie("test_cookie", "value")
with pytest.raises(expected_exception=TypeError) as e:
c["expires"] = expires
assert e.message == "Cookie 'expires' property must be a datetime"
assert response.cookies['test']['expires'] == expires

View File

@@ -13,26 +13,26 @@ def test_create_task(app):
app.add_task(coro)
@app.route("/early")
@app.route('/early')
def not_set(request):
return text(e.is_set())
@app.route("/late")
@app.route('/late')
async def set(request):
await asyncio.sleep(0.1)
return text(e.is_set())
request, response = app.test_client.get("/early")
assert response.body == b"False"
request, response = app.test_client.get('/early')
assert response.body == b'False'
request, response = app.test_client.get("/late")
assert response.body == b"True"
request, response = app.test_client.get('/late')
assert response.body == b'True'
def test_create_task_with_app_arg(app):
q = Queue()
@app.route("/")
@app.route('/')
def not_set(request):
return "hello"
@@ -41,5 +41,5 @@ def test_create_task_with_app_arg(app):
app.add_task(coro)
request, response = app.test_client.get("/")
assert q.get() == "test_create_task_with_app_arg"
request, response = app.test_client.get('/')
assert q.get() == 'test_create_task_with_app_arg'

View File

@@ -3,19 +3,26 @@ from sanic.response import text
class CustomHttpProtocol(HttpProtocol):
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(response.output(self.request.version))
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
def test_use_custom_protocol(app):
@app.route("/1")
async def handler_1(request):
return "OK"
server_kwargs = {"protocol": CustomHttpProtocol}
request, response = app.test_client.get("/1", server_kwargs=server_kwargs)
@app.route('/1')
async def handler_1(request):
return 'OK'
server_kwargs = {
'protocol': CustomHttpProtocol
}
request, response = app.test_client.get(
'/1', server_kwargs=server_kwargs)
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'

View File

@@ -3,41 +3,39 @@ from sanic.router import RouteExists
import pytest
@pytest.mark.parametrize(
"method,attr, expected",
[
@pytest.mark.parametrize("method,attr, expected", [
("get", "text", "OK1 test"),
("post", "text", "OK2 test"),
("put", "text", "OK2 test"),
("delete", "status", 405),
],
)
])
def test_overload_dynamic_routes(app, method, attr, expected):
@app.route("/overload/<param>", methods=["GET"])
@app.route('/overload/<param>', methods=['GET'])
async def handler1(request, param):
return text("OK1 " + param)
return text('OK1 ' + param)
@app.route("/overload/<param>", methods=["POST", "PUT"])
@app.route('/overload/<param>', methods=['POST', 'PUT'])
async def handler2(request, param):
return text("OK2 " + param)
return text('OK2 ' + param)
request, response = getattr(app.test_client, method)("/overload/test")
request, response = getattr(app.test_client, method)('/overload/test')
assert getattr(response, attr) == expected
def test_overload_dynamic_routes_exist(app):
@app.route("/overload/<param>", methods=["GET"])
async def handler1(request, param):
return text("OK1 " + param)
@app.route("/overload/<param>", methods=["POST", "PUT"])
@app.route('/overload/<param>', methods=['GET'])
async def handler1(request, param):
return text('OK1 ' + param)
@app.route('/overload/<param>', methods=['POST', 'PUT'])
async def handler2(request, param):
return text("OK2 " + param)
return text('OK2 ' + param)
# if this doesn't raise an error, than at least the below should happen:
# assert response.text == 'Duplicated'
with pytest.raises(RouteExists):
@app.route("/overload/<param>", methods=["PUT", "DELETE"])
async def handler3(request, param):
return text("Duplicated")
@app.route('/overload/<param>', methods=['PUT', 'DELETE'])
async def handler3(request):
return text('Duplicated')

View File

@@ -11,204 +11,201 @@ class SanicExceptionTestException(Exception):
pass
@pytest.fixture(scope="module")
@pytest.fixture(scope='module')
def exception_app():
app = Sanic("test_exceptions")
app = Sanic('test_exceptions')
@app.route("/")
@app.route('/')
def handler(request):
return text("OK")
return text('OK')
@app.route("/error")
@app.route('/error')
def handler_error(request):
raise ServerError("OK")
@app.route("/404")
@app.route('/404')
def handler_404(request):
raise NotFound("OK")
@app.route("/403")
@app.route('/403')
def handler_403(request):
raise Forbidden("Forbidden")
@app.route("/401")
@app.route('/401')
def handler_401(request):
raise Unauthorized("Unauthorized")
@app.route("/401/basic")
@app.route('/401/basic')
def handler_401_basic(request):
raise Unauthorized("Unauthorized", scheme="Basic", realm="Sanic")
@app.route("/401/digest")
@app.route('/401/digest')
def handler_401_digest(request):
raise Unauthorized(
"Unauthorized",
raise Unauthorized("Unauthorized",
scheme="Digest",
realm="Sanic",
qop="auth, auth-int",
algorithm="MD5",
nonce="abcdef",
opaque="zyxwvu",
)
opaque="zyxwvu")
@app.route("/401/bearer")
@app.route('/401/bearer')
def handler_401_bearer(request):
raise Unauthorized("Unauthorized", scheme="Bearer")
@app.route("/invalid")
@app.route('/invalid')
def handler_invalid(request):
raise InvalidUsage("OK")
@app.route("/abort/401")
@app.route('/abort/401')
def handler_401_error(request):
abort(401)
@app.route("/abort")
@app.route('/abort')
def handler_500_error(request):
abort(500)
return text("OK")
@app.route("/abort/message")
@app.route('/abort/message')
def handler_abort_message(request):
abort(500, message="Abort")
abort(500, message='Abort')
@app.route("/divide_by_zero")
@app.route('/divide_by_zero')
def handle_unhandled_exception(request):
_ = 1 / 0
1 / 0
@app.route("/error_in_error_handler_handler")
@app.route('/error_in_error_handler_handler')
def custom_error_handler(request):
raise SanicExceptionTestException("Dummy message!")
raise SanicExceptionTestException('Dummy message!')
@app.exception(SanicExceptionTestException)
def error_in_error_handler_handler(request, exception):
_ = 1 / 0
1 / 0
return app
def test_catch_exception_list(app):
@app.exception([SanicExceptionTestException, NotFound])
def exception_list(request, exception):
return text("ok")
@app.route("/")
@app.route('/')
def exception(request):
raise SanicExceptionTestException("You won't see me")
request, response = app.test_client.get("/random")
assert response.text == "ok"
request, response = app.test_client.get('/random')
assert response.text == 'ok'
request, response = app.test_client.get("/")
assert response.text == "ok"
request, response = app.test_client.get('/')
assert response.text == 'ok'
def test_no_exception(exception_app):
"""Test that a route works without an exception"""
request, response = exception_app.test_client.get("/")
request, response = exception_app.test_client.get('/')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
def test_server_error_exception(exception_app):
"""Test the built-in ServerError exception works"""
request, response = exception_app.test_client.get("/error")
request, response = exception_app.test_client.get('/error')
assert response.status == 500
def test_invalid_usage_exception(exception_app):
"""Test the built-in InvalidUsage exception works"""
request, response = exception_app.test_client.get("/invalid")
request, response = exception_app.test_client.get('/invalid')
assert response.status == 400
def test_not_found_exception(exception_app):
"""Test the built-in NotFound exception works"""
request, response = exception_app.test_client.get("/404")
request, response = exception_app.test_client.get('/404')
assert response.status == 404
def test_forbidden_exception(exception_app):
"""Test the built-in Forbidden exception"""
request, response = exception_app.test_client.get("/403")
request, response = exception_app.test_client.get('/403')
assert response.status == 403
def test_unauthorized_exception(exception_app):
"""Test the built-in Unauthorized exception"""
request, response = exception_app.test_client.get("/401")
request, response = exception_app.test_client.get('/401')
assert response.status == 401
request, response = exception_app.test_client.get("/401/basic")
request, response = exception_app.test_client.get('/401/basic')
assert response.status == 401
assert response.headers.get("WWW-Authenticate") is not None
assert response.headers.get("WWW-Authenticate") == 'Basic realm="Sanic"'
assert response.headers.get('WWW-Authenticate') is not None
assert response.headers.get('WWW-Authenticate') == 'Basic realm="Sanic"'
request, response = exception_app.test_client.get("/401/digest")
request, response = exception_app.test_client.get('/401/digest')
assert response.status == 401
auth_header = response.headers.get("WWW-Authenticate")
auth_header = response.headers.get('WWW-Authenticate')
assert auth_header is not None
assert auth_header.startswith("Digest")
assert auth_header.startswith('Digest')
assert 'qop="auth, auth-int"' in auth_header
assert 'algorithm="MD5"' in auth_header
assert 'nonce="abcdef"' in auth_header
assert 'opaque="zyxwvu"' in auth_header
request, response = exception_app.test_client.get("/401/bearer")
request, response = exception_app.test_client.get('/401/bearer')
assert response.status == 401
assert response.headers.get("WWW-Authenticate") == "Bearer"
assert response.headers.get('WWW-Authenticate') == "Bearer"
def test_handled_unhandled_exception(exception_app):
"""Test that an exception not built into sanic is handled"""
request, response = exception_app.test_client.get("/divide_by_zero")
request, response = exception_app.test_client.get('/divide_by_zero')
assert response.status == 500
soup = BeautifulSoup(response.body, "html.parser")
assert soup.h1.text == "Internal Server Error"
soup = BeautifulSoup(response.body, 'html.parser')
assert soup.h1.text == 'Internal Server Error'
message = " ".join(soup.p.text.split())
assert message == (
"The server encountered an internal error and "
"cannot complete your request."
)
"cannot complete your request.")
def test_exception_in_exception_handler(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = exception_app.test_client.get(
"/error_in_error_handler_handler"
)
'/error_in_error_handler_handler')
assert response.status == 500
assert response.body == b"An error occurred while handling an error"
assert response.body == b'An error occurred while handling an error'
def test_exception_in_exception_handler_debug_off(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = exception_app.test_client.get(
"/error_in_error_handler_handler", debug=False
)
'/error_in_error_handler_handler',
debug=False)
assert response.status == 500
assert response.body == b"An error occurred while handling an error"
assert response.body == b'An error occurred while handling an error'
def test_exception_in_exception_handler_debug_on(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = exception_app.test_client.get(
"/error_in_error_handler_handler", debug=True
)
'/error_in_error_handler_handler',
debug=True)
assert response.status == 500
assert response.body.startswith(b"Exception raised in exception ")
assert response.body.startswith(b'Exception raised in exception ')
def test_abort(exception_app):
"""Test the abort function"""
request, response = exception_app.test_client.get("/abort/401")
request, response = exception_app.test_client.get('/abort/401')
assert response.status == 401
request, response = exception_app.test_client.get("/abort")
request, response = exception_app.test_client.get('/abort')
assert response.status == 500
request, response = exception_app.test_client.get("/abort/message")
request, response = exception_app.test_client.get('/abort/message')
assert response.status == 500
assert response.text == "Error: Abort"
assert response.text == 'Error: Abort'

View File

@@ -4,39 +4,38 @@ from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.handlers import ErrorHandler
from bs4 import BeautifulSoup
exception_handler_app = Sanic("test_exception_handler")
exception_handler_app = Sanic('test_exception_handler')
@exception_handler_app.route("/1")
@exception_handler_app.route('/1')
def handler_1(request):
raise InvalidUsage("OK")
@exception_handler_app.route("/2")
@exception_handler_app.route('/2')
def handler_2(request):
raise ServerError("OK")
@exception_handler_app.route("/3")
@exception_handler_app.route('/3')
def handler_3(request):
raise NotFound("OK")
@exception_handler_app.route("/4")
@exception_handler_app.route('/4')
def handler_4(request):
foo = bar # noqa -- F821 undefined name 'bar' is done to throw exception
return text(foo)
@exception_handler_app.route("/5")
@exception_handler_app.route('/5')
def handler_5(request):
class CustomServerError(ServerError):
pass
raise CustomServerError("Custom server error")
raise CustomServerError('Custom server error')
@exception_handler_app.route("/6/<arg:int>")
@exception_handler_app.route('/6/<arg:int>')
def handler_6(request, arg):
try:
foo = 1 / arg
@@ -51,67 +50,67 @@ def handler_exception(request, exception):
def test_invalid_usage_exception_handler():
request, response = exception_handler_app.test_client.get("/1")
request, response = exception_handler_app.test_client.get('/1')
assert response.status == 400
def test_server_error_exception_handler():
request, response = exception_handler_app.test_client.get("/2")
request, response = exception_handler_app.test_client.get('/2')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
def test_not_found_exception_handler():
request, response = exception_handler_app.test_client.get("/3")
request, response = exception_handler_app.test_client.get('/3')
assert response.status == 200
def test_text_exception__handler():
request, response = exception_handler_app.test_client.get("/random")
request, response = exception_handler_app.test_client.get('/random')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
def test_html_traceback_output_in_debug_mode():
request, response = exception_handler_app.test_client.get("/4", debug=True)
request, response = exception_handler_app.test_client.get(
'/4', debug=True)
assert response.status == 500
soup = BeautifulSoup(response.body, "html.parser")
soup = BeautifulSoup(response.body, 'html.parser')
html = str(soup)
assert "response = handler(request, *args, **kwargs)" in html
assert "handler_4" in html
assert "foo = bar" in html
assert 'response = handler(request, *args, **kwargs)' in html
assert 'handler_4' in html
assert 'foo = bar' in html
summary_text = " ".join(soup.select(".summary")[0].text.split())
summary_text = " ".join(soup.select('.summary')[0].text.split())
assert (
"NameError: name 'bar' " "is not defined while handling path /4"
) == summary_text
"NameError: name 'bar' "
"is not defined while handling path /4") == summary_text
def test_inherited_exception_handler():
request, response = exception_handler_app.test_client.get("/5")
request, response = exception_handler_app.test_client.get('/5')
assert response.status == 200
def test_chained_exception_handler():
request, response = exception_handler_app.test_client.get(
"/6/0", debug=True
)
'/6/0', debug=True)
assert response.status == 500
soup = BeautifulSoup(response.body, "html.parser")
soup = BeautifulSoup(response.body, 'html.parser')
html = str(soup)
assert "response = handler(request, *args, **kwargs)" in html
assert "handler_6" in html
assert "foo = 1 / arg" in html
assert "ValueError" in html
assert "The above exception was the direct cause" in html
assert 'response = handler(request, *args, **kwargs)' in html
assert 'handler_6' in html
assert 'foo = 1 / arg' in html
assert 'ValueError' in html
assert 'The above exception was the direct cause' in html
summary_text = " ".join(soup.select(".summary")[0].text.split())
summary_text = " ".join(soup.select('.summary')[0].text.split())
assert (
"ZeroDivisionError: division by zero " "while handling path /6/0"
) == summary_text
"ZeroDivisionError: division by zero "
"while handling path /6/0") == summary_text
def test_exception_handler_lookup():
@@ -133,7 +132,6 @@ def test_exception_handler_lookup():
try:
ModuleNotFoundError
except Exception:
class ModuleNotFoundError(ImportError):
pass
@@ -145,12 +143,12 @@ def test_exception_handler_lookup():
assert handler.lookup(ImportError()) == import_error_handler
assert handler.lookup(ModuleNotFoundError()) == import_error_handler
assert handler.lookup(CustomError()) == custom_error_handler
assert handler.lookup(ServerError("Error")) == server_error_handler
assert handler.lookup(CustomServerError("Error")) == server_error_handler
assert handler.lookup(ServerError('Error')) == server_error_handler
assert handler.lookup(CustomServerError('Error')) == server_error_handler
# once again to ensure there is no caching bug
assert handler.lookup(ImportError()) == import_error_handler
assert handler.lookup(ModuleNotFoundError()) == import_error_handler
assert handler.lookup(CustomError()) == custom_error_handler
assert handler.lookup(ServerError("Error")) == server_error_handler
assert handler.lookup(CustomServerError("Error")) == server_error_handler
assert handler.lookup(ServerError('Error')) == server_error_handler
assert handler.lookup(CustomServerError('Error')) == server_error_handler

View File

@@ -38,19 +38,35 @@ def test_is_hop_by_hop_header():
def test_remove_entity_headers():
tests = (
({}, {}),
({"Allow": "GET, POST, HEAD"}, {}),
(
{},
{}
),
(
{
"Allow": "GET, POST, HEAD",
},
{}
),
(
{
"Content-Type": "application/json",
"Expires": "Wed, 21 Oct 2015 07:28:00 GMT",
"Foo": "Bar",
"Foo": "Bar"
},
{
"Expires": "Wed, 21 Oct 2015 07:28:00 GMT",
"Foo": "Bar"
},
{"Expires": "Wed, 21 Oct 2015 07:28:00 GMT", "Foo": "Bar"},
),
(
{"Allow": "GET, POST, HEAD", "Content-Location": "/test"},
{"Content-Location": "/test"},
{
"Allow": "GET, POST, HEAD",
"Content-Location": "/test"
},
{
"Content-Location": "/test"
},
),
)

View File

@@ -3,29 +3,25 @@ from sanic import Sanic
import asyncio
from asyncio import sleep as aio_sleep
from sanic.response import text
from sanic.config import Config
from sanic import server
import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
class ReuseableTCPConnector(TCPConnector):
def __init__(self, *args, **kwargs):
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
self.old_proto = None
async def connect(self, req, *args, **kwargs):
new_conn = await super(ReuseableTCPConnector, self).connect(
req, *args, **kwargs
)
new_conn = await super(ReuseableTCPConnector, self)\
.connect(req, *args, **kwargs)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
raise RuntimeError(
"We got a new connection, wanted the same one!"
)
"We got a new connection, wanted the same one!")
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
@@ -44,39 +40,31 @@ class ReuseableSanicTestClient(SanicTestClient):
# Copied from SanicTestClient, but with some changes to reuse the
# same loop for the same app.
def _sanic_endpoint_test(
self,
method="get",
uri="/",
gather_request=True,
debug=False,
server_kwargs={"return_asyncio_server": True},
*request_args,
**request_kwargs
):
self, method='get', uri='/', gather_request=True,
debug=False, server_kwargs={},
*request_args, **request_kwargs):
loop = self._loop
results = [None, None]
exceptions = []
do_kill_server = request_kwargs.pop("end_server", False)
do_kill_server = request_kwargs.pop('end_server', False)
if gather_request:
def _collect_request(request):
if results[0] is None:
results[0] = request
self.app.request_middleware.appendleft(_collect_request)
@self.app.listener("after_server_start")
@self.app.listener('after_server_start')
async def _collect_response(loop):
try:
if do_kill_server:
request_kwargs["end_session"] = True
request_kwargs['end_session'] = True
response = await self._local_request(
method, uri, *request_args, **request_kwargs
)
method, uri, *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()
@@ -84,25 +72,23 @@ class ReuseableSanicTestClient(SanicTestClient):
if self._server is not None:
_server = self._server
else:
_server_co = self.app.create_server(
host=HOST, debug=debug, port=PORT, **server_kwargs
)
_server_co = self.app.create_server(host=HOST, debug=debug,
port=PORT, **server_kwargs)
server.trigger_events(
self.app.listeners["before_server_start"], loop
)
self.app.listeners['before_server_start'], loop)
try:
loop._stopping = False
http_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
server.trigger_events(self.app.listeners["after_server_start"], loop)
self.app.listeners["after_server_start"].pop()
server.trigger_events(
self.app.listeners['after_server_start'], loop)
self.app.listeners['after_server_start'].pop()
if do_kill_server:
try:
@@ -112,11 +98,11 @@ class ReuseableSanicTestClient(SanicTestClient):
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))
raise ValueError(
"Exception during request: {}".format(exceptions))
if gather_request:
self.app.request_middleware.pop()
@@ -126,32 +112,28 @@ class ReuseableSanicTestClient(SanicTestClient):
except Exception:
raise ValueError(
"Request and response object expected, got ({})".format(
results
)
)
results))
else:
try:
return results[-1]
except Exception:
raise ValueError(
"Request object expected, got ({})".format(results)
)
"Request object expected, got ({})".format(results))
# 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):
request_keepalive = kwargs.pop(
"request_keepalive", CONFIG_FOR_TESTS["KEEP_ALIVE_TIMEOUT"]
)
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
async def _local_request(self, method, uri, cookies=None, *args,
**kwargs):
request_keepalive = kwargs.pop('request_keepalive',
Config.KEEP_ALIVE_TIMEOUT)
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
url = uri
else:
url = "http://{host}:{port}{uri}".format(
host=HOST, port=self.port, uri=uri
)
do_kill_session = kwargs.pop("end_session", False)
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=self.port, uri=uri)
do_kill_session = kwargs.pop('end_session', False)
if self._session:
session = self._session
else:
@@ -161,17 +143,16 @@ class ReuseableSanicTestClient(SanicTestClient):
conn = ReuseableTCPConnector(
ssl=False,
loop=self._loop,
keepalive_timeout=request_keepalive,
keepalive_timeout=request_keepalive
)
self._tcp_connector = conn
session = aiohttp.ClientSession(
cookies=cookies, connector=conn, loop=self._loop
)
session = aiohttp.ClientSession(cookies=cookies,
connector=conn,
loop=self._loop)
self._session = session
async with getattr(session, method.lower())(
url, *args, **kwargs
) as response:
url, *args, **kwargs) as response:
try:
response.text = await response.text()
except UnicodeDecodeError:
@@ -179,11 +160,9 @@ class ReuseableSanicTestClient(SanicTestClient):
try:
response.json = await response.json()
except (
JSONDecodeError,
except (JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError,
):
aiohttp.ClientResponseError):
response.json = None
response.body = await response.read()
@@ -193,28 +172,26 @@ class ReuseableSanicTestClient(SanicTestClient):
return response
keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse")
keep_alive_app_client_timeout = Sanic("test_ka_client_timeout")
keep_alive_app_server_timeout = Sanic("test_ka_server_timeout")
keep_alive_timeout_app_reuse.config.update(CONFIG_FOR_TESTS)
keep_alive_app_client_timeout.config.update(CONFIG_FOR_TESTS)
keep_alive_app_server_timeout.config.update(CONFIG_FOR_TESTS)
Config.KEEP_ALIVE_TIMEOUT = 2
Config.KEEP_ALIVE = True
keep_alive_timeout_app_reuse = Sanic('test_ka_timeout_reuse')
keep_alive_app_client_timeout = Sanic('test_ka_client_timeout')
keep_alive_app_server_timeout = Sanic('test_ka_server_timeout')
@keep_alive_timeout_app_reuse.route("/1")
@keep_alive_timeout_app_reuse.route('/1')
async def handler1(request):
return text("OK")
return text('OK')
@keep_alive_app_client_timeout.route("/1")
@keep_alive_app_client_timeout.route('/1')
async def handler2(request):
return text("OK")
return text('OK')
@keep_alive_app_server_timeout.route("/1")
@keep_alive_app_server_timeout.route('/1')
async def handler3(request):
return text("OK")
return text('OK')
def test_keep_alive_timeout_reuse():
@@ -224,14 +201,16 @@ def test_keep_alive_timeout_reuse():
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)
headers = {
'Connection': 'keep-alive'
}
request, response = client.get('/1', headers=headers)
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
loop.run_until_complete(aio_sleep(1))
request, response = client.get("/1", end_server=True)
request, response = client.get('/1', end_server=True)
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
def test_keep_alive_client_timeout():
@@ -239,17 +218,20 @@ def test_keep_alive_client_timeout():
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)
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"
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
)
request, response = client.get('/1', end_server=True,
request_keepalive=1)
except ValueError as e:
exception = e
assert exception is not None
@@ -264,22 +246,23 @@ def test_keep_alive_server_timeout():
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)
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"
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
)
request, response = client.get('/1', request_keepalive=60,
end_server=True)
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]
)
assert "Connection reset" in exception.args[0] or \
"got a new connection" in exception.args[0]

View File

@@ -14,9 +14,9 @@ from sanic import Sanic
from sanic.log import logger
logging_format = """module: %(module)s; \
logging_format = '''module: %(module)s; \
function: %(funcName)s(); \
message: %(message)s"""
message: %(message)s'''
def reset_logging():
@@ -29,17 +29,19 @@ def test_log(app):
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
logging.basicConfig(
format=logging_format, level=logging.DEBUG, stream=log_stream
format=logging_format,
level=logging.DEBUG,
stream=log_stream
)
log = logging.getLogger()
rand_string = str(uuid.uuid4())
@app.route("/")
@app.route('/')
def handler(request):
log.info(rand_string)
return text("hello")
return text('hello')
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
log_text = log_stream.getvalue()
assert rand_string in log_text
@@ -48,80 +50,59 @@ def test_logging_defaults():
# reset_logging()
app = Sanic("test_logging")
for fmt in [h.formatter for h in logging.getLogger("sanic.root").handlers]:
assert (
fmt._fmt
== LOGGING_CONFIG_DEFAULTS["formatters"]["generic"]["format"]
)
for fmt in [h.formatter for h in logging.getLogger('sanic.root').handlers]:
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format']
for fmt in [
h.formatter for h in logging.getLogger("sanic.error").handlers
]:
assert (
fmt._fmt
== LOGGING_CONFIG_DEFAULTS["formatters"]["generic"]["format"]
)
for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format']
for fmt in [
h.formatter for h in logging.getLogger("sanic.access").handlers
]:
assert (
fmt._fmt
== LOGGING_CONFIG_DEFAULTS["formatters"]["access"]["format"]
)
for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]:
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['access']['format']
def test_logging_pass_customer_logconfig():
# reset_logging()
modified_config = LOGGING_CONFIG_DEFAULTS
modified_config["formatters"]["generic"][
"format"
] = "%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s"
modified_config["formatters"]["access"][
"format"
] = "%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s"
modified_config['formatters']['generic']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s'
modified_config['formatters']['access']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s'
app = Sanic("test_logging", log_config=modified_config)
for fmt in [h.formatter for h in logging.getLogger("sanic.root").handlers]:
assert fmt._fmt == modified_config["formatters"]["generic"]["format"]
for fmt in [h.formatter for h in logging.getLogger('sanic.root').handlers]:
assert fmt._fmt == modified_config['formatters']['generic']['format']
for fmt in [
h.formatter for h in logging.getLogger("sanic.error").handlers
]:
assert fmt._fmt == modified_config["formatters"]["generic"]["format"]
for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
assert fmt._fmt == modified_config['formatters']['generic']['format']
for fmt in [
h.formatter for h in logging.getLogger("sanic.access").handlers
]:
assert fmt._fmt == modified_config["formatters"]["access"]["format"]
for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]:
assert fmt._fmt == modified_config['formatters']['access']['format']
@pytest.mark.parametrize("debug", (True, False))
@pytest.mark.parametrize('debug', (True, False, ))
def test_log_connection_lost(app, debug, monkeypatch):
""" Should not log Connection lost exception on non debug """
stream = StringIO()
root = logging.getLogger("sanic.root")
root = logging.getLogger('sanic.root')
root.addHandler(logging.StreamHandler(stream))
monkeypatch.setattr(sanic.server, "logger", root)
monkeypatch.setattr(sanic.server, 'logger', root)
@app.route("/conn_lost")
@app.route('/conn_lost')
async def conn_lost(request):
response = text("Ok")
response = text('Ok')
response.output = Mock(side_effect=RuntimeError)
return response
with pytest.raises(ValueError):
# catch ValueError: Exception during request
app.test_client.get("/conn_lost", debug=debug)
app.test_client.get('/conn_lost', debug=debug)
log = stream.getvalue()
if debug:
assert "Connection lost before response written @" in log
assert 'Connection lost before response written @' in log
else:
assert "Connection lost before response written @" not in log
assert 'Connection lost before response written @' not in log
def test_logger(caplog):
@@ -129,38 +110,26 @@ def test_logger(caplog):
app = Sanic()
@app.get("/")
@app.get('/')
def log_info(request):
logger.info(rand_string)
return text("hello")
return text('hello')
with caplog.at_level(logging.INFO):
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert caplog.record_tuples[0] == (
"sanic.root",
logging.INFO,
"Goin' Fast @ http://127.0.0.1:42101",
)
assert caplog.record_tuples[1] == (
"sanic.root",
logging.INFO,
"http://127.0.0.1:42101/",
)
assert caplog.record_tuples[2] == ("sanic.root", logging.INFO, rand_string)
assert caplog.record_tuples[-1] == (
"sanic.root",
logging.INFO,
"Server Stopped",
)
assert caplog.record_tuples[0] == ('sanic.root', logging.INFO, 'Goin\' Fast @ http://127.0.0.1:42101')
assert caplog.record_tuples[1] == ('sanic.root', logging.INFO, 'http://127.0.0.1:42101/')
assert caplog.record_tuples[2] == ('sanic.root', logging.INFO, rand_string)
assert caplog.record_tuples[-1] == ('sanic.root', logging.INFO, 'Server Stopped')
def test_logging_modified_root_logger_config():
# reset_logging()
modified_config = LOGGING_CONFIG_DEFAULTS
modified_config["loggers"]["sanic.root"]["level"] = "DEBUG"
modified_config['loggers']['sanic.root']['level'] = 'DEBUG'
app = Sanic("test_logging", log_config=modified_config)
assert logging.getLogger("sanic.root").getEffectiveLevel() == logging.DEBUG
assert logging.getLogger('sanic.root').getEffectiveLevel() == logging.DEBUG

View File

@@ -1,85 +0,0 @@
import logging
import asyncio
from sanic.config import BASE_LOGO
try:
import uvloop # noqa
ROW = 0
except BaseException:
ROW = 1
def test_logo_base(app, caplog):
server = app.create_server(debug=True, return_asyncio_server=True)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop._stopping = False
with caplog.at_level(logging.DEBUG):
_server = loop.run_until_complete(server)
_server.close()
loop.run_until_complete(_server.wait_closed())
app.stop()
assert caplog.record_tuples[ROW][1] == logging.DEBUG
assert caplog.record_tuples[ROW][2] == BASE_LOGO
def test_logo_false(app, caplog):
app.config.LOGO = False
server = app.create_server(debug=True, return_asyncio_server=True)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop._stopping = False
with caplog.at_level(logging.DEBUG):
_server = loop.run_until_complete(server)
_server.close()
loop.run_until_complete(_server.wait_closed())
app.stop()
assert caplog.record_tuples[ROW][1] == logging.INFO
assert caplog.record_tuples[ROW][2] == "Goin' Fast @ http://127.0.0.1:8000"
def test_logo_true(app, caplog):
app.config.LOGO = True
server = app.create_server(debug=True, return_asyncio_server=True)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop._stopping = False
with caplog.at_level(logging.DEBUG):
_server = loop.run_until_complete(server)
_server.close()
loop.run_until_complete(_server.wait_closed())
app.stop()
assert caplog.record_tuples[ROW][1] == logging.DEBUG
assert caplog.record_tuples[ROW][2] == BASE_LOGO
def test_logo_custom(app, caplog):
app.config.LOGO = "My Custom Logo"
server = app.create_server(debug=True, return_asyncio_server=True)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop._stopping = False
with caplog.at_level(logging.DEBUG):
_server = loop.run_until_complete(server)
_server.close()
loop.run_until_complete(_server.wait_closed())
app.stop()
assert caplog.record_tuples[ROW][1] == logging.DEBUG
assert caplog.record_tuples[ROW][2] == "My Custom Logo"

View File

@@ -9,7 +9,6 @@ from sanic.response import HTTPResponse, text
# GET
# ------------------------------------------------------------ #
def test_middleware_request(app):
results = []
@@ -17,166 +16,170 @@ def test_middleware_request(app):
async def handler1(request):
results.append(request)
@app.route("/")
@app.route('/')
async def handler2(request):
return text("OK")
return text('OK')
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert response.text == "OK"
assert response.text == 'OK'
assert type(results[0]) is Request
def test_middleware_response(app):
results = []
@app.middleware("request")
@app.middleware('request')
async def process_request(request):
results.append(request)
@app.middleware("response")
@app.middleware('response')
async def process_response(request, response):
results.append(request)
results.append(response)
@app.route("/")
@app.route('/')
async def handler(request):
return text("OK")
return text('OK')
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert response.text == "OK"
assert response.text == 'OK'
assert type(results[0]) is Request
assert type(results[1]) is Request
assert isinstance(results[2], HTTPResponse)
def test_middleware_response_exception(app):
result = {"status_code": None}
result = {'status_code': None}
@app.middleware("response")
@app.middleware('response')
async def process_response(request, response):
result["status_code"] = response.status
result['status_code'] = response.status
return response
@app.exception(NotFound)
async def error_handler(request, exception):
return text("OK", exception.status_code)
return text('OK', exception.status_code)
@app.route("/")
@app.route('/')
async def handler(request):
return text("FAIL")
return text('FAIL')
request, response = app.test_client.get("/page_not_found")
assert response.text == "OK"
assert result["status_code"] == 404
request, response = app.test_client.get('/page_not_found')
assert response.text == 'OK'
assert result['status_code'] == 404
def test_middleware_response_raise_cancelled_error(app, caplog):
app.config.RESPONSE_TIMEOUT = 1
@app.middleware("response")
@app.middleware('response')
async def process_response(request, response):
raise CancelledError("CancelledError at response middleware")
raise CancelledError('CancelledError at response middleware')
@app.get("/")
@app.get('/')
def handler(request):
return text("OK")
return text('OK')
caplog.set_level(logging.ERROR, logger="sanic.root")
with caplog.at_level(logging.ERROR):
reqrequest, response = app.test_client.get("/")
reqrequest, response = app.test_client.get('/')
assert response.status == 503
assert (
"sanic.root",
assert caplog.record_tuples[0] == (
'sanic.root',
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) in caplog.record_tuples
'Exception occurred while handling uri: \'http://127.0.0.1:42101/\''
)
def test_middleware_response_raise_exception(app, caplog):
@app.middleware("response")
async def process_response(request, response):
raise Exception("Exception at response middleware")
@app.middleware('response')
async def process_response(request, response):
raise Exception('Exception at response middleware')
caplog.set_level(logging.ERROR, logger="sanic.root")
with caplog.at_level(logging.ERROR):
reqrequest, response = app.test_client.get("/")
reqrequest, response = app.test_client.get('/')
assert response.status == 404
assert (
"sanic.root",
assert caplog.record_tuples[0] == (
'sanic.root',
logging.ERROR,
"Exception occurred while handling uri: 'http://127.0.0.1:42101/'",
) in caplog.record_tuples
assert (
"sanic.error",
'Exception occurred while handling uri: \'http://127.0.0.1:42101/\''
)
assert caplog.record_tuples[1] == (
'sanic.error',
logging.ERROR,
"Exception occurred in one of response middleware handlers",
) in caplog.record_tuples
'Exception occurred in one of response middleware handlers'
)
def test_middleware_override_request(app):
@app.middleware
async def halt_request(request):
return text("OK")
return text('OK')
@app.route("/")
@app.route('/')
async def handler(request):
return text("FAIL")
return text('FAIL')
response = app.test_client.get("/", gather_request=False)
response = app.test_client.get('/', gather_request=False)
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
def test_middleware_override_response(app):
@app.middleware("response")
@app.middleware('response')
async def process_response(request, response):
return text("OK")
return text('OK')
@app.route("/")
@app.route('/')
async def handler(request):
return text("FAIL")
return text('FAIL')
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
def test_middleware_order(app):
order = []
@app.middleware("request")
@app.middleware('request')
async def request1(request):
order.append(1)
@app.middleware("request")
@app.middleware('request')
async def request2(request):
order.append(2)
@app.middleware("request")
@app.middleware('request')
async def request3(request):
order.append(3)
@app.middleware("response")
@app.middleware('response')
async def response1(request, response):
order.append(6)
@app.middleware("response")
@app.middleware('response')
async def response2(request, response):
order.append(5)
@app.middleware("response")
@app.middleware('response')
async def response3(request, response):
order.append(4)
@app.route("/")
@app.route('/')
async def handler(request):
return text("OK")
return text('OK')
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert response.status == 200
assert order == [1, 2, 3, 4, 5, 6]

View File

@@ -9,9 +9,9 @@ from sanic.response import text
@pytest.mark.skipif(
not hasattr(signal, "SIGALRM"),
reason="SIGALRM is not implemented for this platform, we have to come "
"up with another timeout strategy to test these",
not hasattr(signal, 'SIGALRM'),
reason='SIGALRM is not implemented for this platform, we have to come '
'up with another timeout strategy to test these'
)
def test_multiprocessing(app):
"""Tests that the number of children we produce is correct"""
@@ -32,12 +32,11 @@ def test_multiprocessing(app):
@pytest.mark.skipif(
not hasattr(signal, "SIGALRM"),
reason="SIGALRM is not implemented for this platform",
not hasattr(signal, 'SIGALRM'),
reason='SIGALRM is not implemented for this platform',
)
def test_multiprocessing_with_blueprint(app):
from sanic import Blueprint
# Selects a number at random so we can spot check
num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1))
process_list = set()
@@ -50,7 +49,7 @@ def test_multiprocessing_with_blueprint(app):
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(3)
bp = Blueprint("test_text")
bp = Blueprint('test_text')
app.blueprint(bp)
app.run(HOST, PORT, workers=num_workers)
@@ -60,30 +59,28 @@ def test_multiprocessing_with_blueprint(app):
# this function must be outside a test function so that it can be
# able to be pickled (local functions cannot be pickled).
def handler(request):
return text("Hello")
return text('Hello')
# Muliprocessing on Windows requires app to be able to be pickled
@pytest.mark.parametrize("protocol", [3, 4])
@pytest.mark.parametrize('protocol', [3, 4])
def test_pickle_app(app, protocol):
app.route("/")(handler)
app.route('/')(handler)
p_app = pickle.dumps(app, protocol=protocol)
up_p_app = pickle.loads(p_app)
assert up_p_app
request, response = app.test_client.get("/")
assert response.text == "Hello"
request, response = app.test_client.get('/')
assert response.text == 'Hello'
@pytest.mark.parametrize("protocol", [3, 4])
@pytest.mark.parametrize('protocol', [3, 4])
def test_pickle_app_with_bp(app, protocol):
from sanic import Blueprint
bp = Blueprint("test_text")
bp.route("/")(handler)
bp = Blueprint('test_text')
bp.route('/')(handler)
app.blueprint(bp)
p_app = pickle.dumps(app, protocol=protocol)
up_p_app = pickle.loads(p_app)
assert up_p_app
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert app.is_request_stream is False
assert response.text == "Hello"
assert response.text == 'Hello'

View File

@@ -14,32 +14,28 @@ from sanic.constants import HTTP_METHODS
# UTF-8
# ------------------------------------------------------------ #
@pytest.mark.parametrize("method", HTTP_METHODS)
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_named_routes_get(app, method):
bp = Blueprint("test_bp", url_prefix="/bp")
bp = Blueprint('test_bp', url_prefix='/bp')
method = method.lower()
route_name = "route_{}".format(method)
route_name2 = "route2_{}".format(method)
route_name = 'route_{}'.format(method)
route_name2 = 'route2_{}'.format(method)
func = getattr(app, method)
if callable(func):
@func("/{}".format(method), version=1, name=route_name)
@func('/{}'.format(method), version=1, name=route_name)
def handler(request):
return text("OK")
return text('OK')
else:
print(func)
raise
func = getattr(bp, method)
if callable(func):
@func("/{}".format(method), version=1, name=route_name2)
@func('/{}'.format(method), version=1, name=route_name2)
def handler2(request):
return text("OK")
return text('OK')
else:
print(func)
@@ -47,250 +43,261 @@ def test_versioned_named_routes_get(app, method):
app.blueprint(bp)
assert app.router.routes_all["/v1/{}".format(method)].name == route_name
assert app.router.routes_all['/v1/{}'.format(method)].name == route_name
route = app.router.routes_all["/v1/bp/{}".format(method)]
assert route.name == "test_bp.{}".format(route_name2)
route = app.router.routes_all['/v1/bp/{}'.format(method)]
assert route.name == 'test_bp.{}'.format(route_name2)
assert app.url_for(route_name) == "/v1/{}".format(method)
url = app.url_for("test_bp.{}".format(route_name2))
assert url == "/v1/bp/{}".format(method)
assert app.url_for(route_name) == '/v1/{}'.format(method)
url = app.url_for('test_bp.{}'.format(route_name2))
assert url == '/v1/bp/{}'.format(method)
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_shorthand_default_routes_get(app):
@app.get("/get")
def handler(request):
return text("OK")
assert app.router.routes_all["/get"].name == "handler"
assert app.url_for("handler") == "/get"
@app.get('/get')
def handler(request):
return text('OK')
assert app.router.routes_all['/get'].name == 'handler'
assert app.url_for('handler') == '/get'
def test_shorthand_named_routes_get(app):
bp = Blueprint("test_bp", url_prefix="/bp")
bp = Blueprint('test_bp', url_prefix='/bp')
@app.get("/get", name="route_get")
@app.get('/get', name='route_get')
def handler(request):
return text("OK")
return text('OK')
@bp.get("/get", name="route_bp")
@bp.get('/get', name='route_bp')
def handler2(request):
return text("Blueprint")
return text('Blueprint')
app.blueprint(bp)
assert app.router.routes_all["/get"].name == "route_get"
assert app.url_for("route_get") == "/get"
assert app.router.routes_all['/get'].name == 'route_get'
assert app.url_for('route_get') == '/get'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
assert app.router.routes_all["/bp/get"].name == "test_bp.route_bp"
assert app.url_for("test_bp.route_bp") == "/bp/get"
assert app.router.routes_all['/bp/get'].name == 'test_bp.route_bp'
assert app.url_for('test_bp.route_bp') == '/bp/get'
with pytest.raises(URLBuildError):
app.url_for("test_bp.handler2")
app.url_for('test_bp.handler2')
def test_shorthand_named_routes_post(app):
@app.post("/post", name="route_name")
def handler(request):
return text("OK")
assert app.router.routes_all["/post"].name == "route_name"
assert app.url_for("route_name") == "/post"
@app.post('/post', name='route_name')
def handler(request):
return text('OK')
assert app.router.routes_all['/post'].name == 'route_name'
assert app.url_for('route_name') == '/post'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_shorthand_named_routes_put(app):
@app.put("/put", name="route_put")
@app.put('/put', name='route_put')
def handler(request):
assert request.stream is None
return text("OK")
return text('OK')
assert app.is_request_stream is False
assert app.router.routes_all["/put"].name == "route_put"
assert app.url_for("route_put") == "/put"
assert app.router.routes_all['/put'].name == 'route_put'
assert app.url_for('route_put') == '/put'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_shorthand_named_routes_delete(app):
@app.delete("/delete", name="route_delete")
@app.delete('/delete', name='route_delete')
def handler(request):
assert request.stream is None
return text("OK")
return text('OK')
assert app.is_request_stream is False
assert app.router.routes_all["/delete"].name == "route_delete"
assert app.url_for("route_delete") == "/delete"
assert app.router.routes_all['/delete'].name == 'route_delete'
assert app.url_for('route_delete') == '/delete'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_shorthand_named_routes_patch(app):
@app.patch("/patch", name="route_patch")
@app.patch('/patch', name='route_patch')
def handler(request):
assert request.stream is None
return text("OK")
return text('OK')
assert app.is_request_stream is False
assert app.router.routes_all["/patch"].name == "route_patch"
assert app.url_for("route_patch") == "/patch"
assert app.router.routes_all['/patch'].name == 'route_patch'
assert app.url_for('route_patch') == '/patch'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_shorthand_named_routes_head(app):
@app.head("/head", name="route_head")
@app.head('/head', name='route_head')
def handler(request):
assert request.stream is None
return text("OK")
return text('OK')
assert app.is_request_stream is False
assert app.router.routes_all["/head"].name == "route_head"
assert app.url_for("route_head") == "/head"
assert app.router.routes_all['/head'].name == 'route_head'
assert app.url_for('route_head') == '/head'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_shorthand_named_routes_options(app):
@app.options("/options", name="route_options")
@app.options('/options', name='route_options')
def handler(request):
assert request.stream is None
return text("OK")
return text('OK')
assert app.is_request_stream is False
assert app.router.routes_all["/options"].name == "route_options"
assert app.url_for("route_options") == "/options"
assert app.router.routes_all['/options'].name == 'route_options'
assert app.url_for('route_options') == '/options'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_named_static_routes(app):
@app.route("/test", name="route_test")
@app.route('/test', name='route_test')
async def handler1(request):
return text("OK1")
return text('OK1')
@app.route("/pizazz", name="route_pizazz")
@app.route('/pizazz', name='route_pizazz')
async def handler2(request):
return text("OK2")
return text('OK2')
assert app.router.routes_all["/test"].name == "route_test"
assert app.router.routes_static["/test"].name == "route_test"
assert app.url_for("route_test") == "/test"
assert app.router.routes_all['/test'].name == 'route_test'
assert app.router.routes_static['/test'].name == 'route_test'
assert app.url_for('route_test') == '/test'
with pytest.raises(URLBuildError):
app.url_for("handler1")
app.url_for('handler1')
assert app.router.routes_all["/pizazz"].name == "route_pizazz"
assert app.router.routes_static["/pizazz"].name == "route_pizazz"
assert app.url_for("route_pizazz") == "/pizazz"
assert app.router.routes_all['/pizazz'].name == 'route_pizazz'
assert app.router.routes_static['/pizazz'].name == 'route_pizazz'
assert app.url_for('route_pizazz') == '/pizazz'
with pytest.raises(URLBuildError):
app.url_for("handler2")
app.url_for('handler2')
def test_named_dynamic_route(app):
results = []
@app.route("/folder/<name>", name="route_dynamic")
@app.route('/folder/<name>', name='route_dynamic')
async def handler(request, name):
results.append(name)
return text("OK")
return text('OK')
assert app.router.routes_all["/folder/<name>"].name == "route_dynamic"
assert app.url_for("route_dynamic", name="test") == "/folder/test"
assert app.router.routes_all['/folder/<name>'].name == 'route_dynamic'
assert app.url_for('route_dynamic', name='test') == '/folder/test'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_dynamic_named_route_regex(app):
@app.route("/folder/<folder_id:[A-Za-z0-9]{0,4}>", name="route_re")
async def handler(request, folder_id):
return text("OK")
route = app.router.routes_all["/folder/<folder_id:[A-Za-z0-9]{0,4}>"]
assert route.name == "route_re"
assert app.url_for("route_re", folder_id="test") == "/folder/test"
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>', name='route_re')
async def handler(request, folder_id):
return text('OK')
route = app.router.routes_all['/folder/<folder_id:[A-Za-z0-9]{0,4}>']
assert route.name == 'route_re'
assert app.url_for('route_re', folder_id='test') == '/folder/test'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_dynamic_named_route_path(app):
@app.route("/<path:path>/info", name="route_dynamic_path")
async def handler(request, path):
return text("OK")
route = app.router.routes_all["/<path:path>/info"]
assert route.name == "route_dynamic_path"
assert app.url_for("route_dynamic_path", path="path/1") == "/path/1/info"
@app.route('/<path:path>/info', name='route_dynamic_path')
async def handler(request, path):
return text('OK')
route = app.router.routes_all['/<path:path>/info']
assert route.name == 'route_dynamic_path'
assert app.url_for('route_dynamic_path', path='path/1') == '/path/1/info'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_dynamic_named_route_unhashable(app):
@app.route(
"/folder/<unhashable:[A-Za-z0-9/]+>/end/", name="route_unhashable"
)
async def handler(request, unhashable):
return text("OK")
route = app.router.routes_all["/folder/<unhashable:[A-Za-z0-9/]+>/end/"]
assert route.name == "route_unhashable"
url = app.url_for("route_unhashable", unhashable="test/asdf")
assert url == "/folder/test/asdf/end"
@app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/',
name='route_unhashable')
async def handler(request, unhashable):
return text('OK')
route = app.router.routes_all['/folder/<unhashable:[A-Za-z0-9/]+>/end/']
assert route.name == 'route_unhashable'
url = app.url_for('route_unhashable', unhashable='test/asdf')
assert url == '/folder/test/asdf/end'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_websocket_named_route(app):
ev = asyncio.Event()
@app.websocket("/ws", name="route_ws")
@app.websocket('/ws', name='route_ws')
async def handler(request, ws):
assert ws.subprotocol is None
ev.set()
assert app.router.routes_all["/ws"].name == "route_ws"
assert app.url_for("route_ws") == "/ws"
assert app.router.routes_all['/ws'].name == 'route_ws'
assert app.url_for('route_ws') == '/ws'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_websocket_named_route_with_subprotocols(app):
results = []
@app.websocket("/ws", subprotocols=["foo", "bar"], name="route_ws")
@app.websocket('/ws', subprotocols=['foo', 'bar'], name='route_ws')
async def handler(request, ws):
results.append(ws.subprotocol)
assert app.router.routes_all["/ws"].name == "route_ws"
assert app.url_for("route_ws") == "/ws"
assert app.router.routes_all['/ws'].name == 'route_ws'
assert app.url_for('route_ws') == '/ws'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_static_add_named_route(app):
async def handler1(request):
return text("OK1")
return text('OK1')
async def handler2(request):
return text("OK2")
return text('OK2')
app.add_route(handler1, "/test", name="route_test")
app.add_route(handler2, "/test2", name="route_test2")
app.add_route(handler1, '/test', name='route_test')
app.add_route(handler2, '/test2', name='route_test2')
assert app.router.routes_all["/test"].name == "route_test"
assert app.router.routes_static["/test"].name == "route_test"
assert app.url_for("route_test") == "/test"
assert app.router.routes_all['/test'].name == 'route_test'
assert app.router.routes_static['/test'].name == 'route_test'
assert app.url_for('route_test') == '/test'
with pytest.raises(URLBuildError):
app.url_for("handler1")
app.url_for('handler1')
assert app.router.routes_all["/test2"].name == "route_test2"
assert app.router.routes_static["/test2"].name == "route_test2"
assert app.url_for("route_test2") == "/test2"
assert app.router.routes_all['/test2'].name == 'route_test2'
assert app.router.routes_static['/test2'].name == 'route_test2'
assert app.url_for('route_test2') == '/test2'
with pytest.raises(URLBuildError):
app.url_for("handler2")
app.url_for('handler2')
def test_dynamic_add_named_route(app):
@@ -298,62 +305,61 @@ def test_dynamic_add_named_route(app):
async def handler(request, name):
results.append(name)
return text("OK")
return text('OK')
app.add_route(handler, "/folder/<name>", name="route_dynamic")
assert app.router.routes_all["/folder/<name>"].name == "route_dynamic"
assert app.url_for("route_dynamic", name="test") == "/folder/test"
app.add_route(handler, '/folder/<name>', name='route_dynamic')
assert app.router.routes_all['/folder/<name>'].name == 'route_dynamic'
assert app.url_for('route_dynamic', name='test') == '/folder/test'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_dynamic_add_named_route_unhashable(app):
async def handler(request, unhashable):
return text("OK")
app.add_route(
handler,
"/folder/<unhashable:[A-Za-z0-9/]+>/end/",
name="route_unhashable",
)
route = app.router.routes_all["/folder/<unhashable:[A-Za-z0-9/]+>/end/"]
assert route.name == "route_unhashable"
url = app.url_for("route_unhashable", unhashable="folder1")
assert url == "/folder/folder1/end"
async def handler(request, unhashable):
return text('OK')
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/',
name='route_unhashable')
route = app.router.routes_all['/folder/<unhashable:[A-Za-z0-9/]+>/end/']
assert route.name == 'route_unhashable'
url = app.url_for('route_unhashable', unhashable='folder1')
assert url == '/folder/folder1/end'
with pytest.raises(URLBuildError):
app.url_for("handler")
app.url_for('handler')
def test_overload_routes(app):
@app.route("/overload", methods=["GET"], name="route_first")
@app.route('/overload', methods=['GET'], name='route_first')
async def handler1(request):
return text("OK1")
return text('OK1')
@app.route("/overload", methods=["POST", "PUT"], name="route_second")
@app.route('/overload', methods=['POST', 'PUT'], name='route_second')
async def handler2(request):
return text("OK2")
return text('OK2')
request, response = app.test_client.get(app.url_for("route_first"))
assert response.text == "OK1"
request, response = app.test_client.get(app.url_for('route_first'))
assert response.text == 'OK1'
request, response = app.test_client.post(app.url_for("route_first"))
assert response.text == "OK2"
request, response = app.test_client.post(app.url_for('route_first'))
assert response.text == 'OK2'
request, response = app.test_client.put(app.url_for("route_first"))
assert response.text == "OK2"
request, response = app.test_client.put(app.url_for('route_first'))
assert response.text == 'OK2'
request, response = app.test_client.get(app.url_for("route_second"))
assert response.text == "OK1"
request, response = app.test_client.get(app.url_for('route_second'))
assert response.text == 'OK1'
request, response = app.test_client.post(app.url_for("route_second"))
assert response.text == "OK2"
request, response = app.test_client.post(app.url_for('route_second'))
assert response.text == 'OK2'
request, response = app.test_client.put(app.url_for("route_second"))
assert response.text == "OK2"
request, response = app.test_client.put(app.url_for('route_second'))
assert response.text == 'OK2'
assert app.router.routes_all["/overload"].name == "route_first"
assert app.router.routes_all['/overload'].name == 'route_first'
with pytest.raises(URLBuildError):
app.url_for("handler1")
app.url_for('handler1')
assert app.url_for("route_first") == "/overload"
assert app.url_for("route_second") == app.url_for("route_first")
assert app.url_for('route_first') == '/overload'
assert app.url_for('route_second') == app.url_for('route_first')

View File

@@ -5,39 +5,41 @@ from sanic.response import text
def test_payload_too_large_from_error_handler(app):
app.config.REQUEST_MAX_SIZE = 1
@app.route("/1")
@app.route('/1')
async def handler1(request):
return text("OK")
return text('OK')
@app.exception(PayloadTooLarge)
def handler_exception(request, exception):
return text("Payload Too Large from error_handler.", 413)
return text('Payload Too Large from error_handler.', 413)
response = app.test_client.get("/1", gather_request=False)
response = app.test_client.get('/1', gather_request=False)
assert response.status == 413
assert response.text == "Payload Too Large from error_handler."
assert response.text == 'Payload Too Large from error_handler.'
def test_payload_too_large_at_data_received_default(app):
app.config.REQUEST_MAX_SIZE = 1
@app.route("/1")
@app.route('/1')
async def handler2(request):
return text("OK")
return text('OK')
response = app.test_client.get("/1", gather_request=False)
response = app.test_client.get(
'/1', gather_request=False)
assert response.status == 413
assert response.text == "Error: Payload Too Large"
assert response.text == 'Error: Payload Too Large'
def test_payload_too_large_at_on_header_default(app):
app.config.REQUEST_MAX_SIZE = 500
@app.post("/1")
@app.post('/1')
async def handler3(request):
return text("OK")
return text('OK')
data = "a" * 1000
response = app.test_client.post("/1", gather_request=False, data=data)
data = 'a' * 1000
response = app.test_client.post(
'/1', gather_request=False, data=data)
assert response.status == 413
assert response.text == "Error: Payload Too Large"
assert response.text == 'Error: Payload Too Large'

View File

@@ -6,31 +6,32 @@ from sanic.response import text, redirect
@pytest.fixture
def redirect_app(app):
@app.route("/redirect_init")
@app.route('/redirect_init')
async def redirect_init(request):
return redirect("/redirect_target")
@app.route("/redirect_init_with_301")
@app.route('/redirect_init_with_301')
async def redirect_init_with_301(request):
return redirect("/redirect_target", status=301)
@app.route("/redirect_target")
@app.route('/redirect_target')
async def redirect_target(request):
return text("OK")
return text('OK')
@app.route("/1")
@app.route('/1')
def handler1(request):
return redirect("/2")
return redirect('/2')
@app.route("/2")
@app.route('/2')
def handler2(request):
return redirect("/3")
return redirect('/3')
@app.route("/3")
@app.route('/3')
def handler3(request):
return text("OK")
return text('OK')
@app.route("/redirect_with_header_injection")
@app.route('/redirect_with_header_injection')
async def redirect_with_header_injection(request):
return redirect("/unsafe\ntest-header: test-value\n\ntest-body")
@@ -42,18 +43,19 @@ def test_redirect_default_302(redirect_app):
We expect a 302 default status code and the headers to be set.
"""
request, response = redirect_app.test_client.get(
"/redirect_init", allow_redirects=False
)
'/redirect_init',
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
assert response.headers["Content-Type"] == "text/html; charset=utf-8"
assert response.headers["Content-Type"] == 'text/html; charset=utf-8'
def test_redirect_headers_none(redirect_app):
request, response = redirect_app.test_client.get(
uri="/redirect_init", headers=None, allow_redirects=False
)
uri="/redirect_init",
headers=None,
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
@@ -64,8 +66,8 @@ def test_redirect_with_301(redirect_app):
Test redirection with a different status code.
"""
request, response = redirect_app.test_client.get(
"/redirect_init_with_301", allow_redirects=False
)
"/redirect_init_with_301",
allow_redirects=False)
assert response.status == 301
assert response.headers["Location"] == "/redirect_target"
@@ -76,23 +78,23 @@ def test_get_then_redirect_follow_redirect(redirect_app):
With `allow_redirects` we expect a 200.
"""
request, response = redirect_app.test_client.get(
"/redirect_init", allow_redirects=True
)
"/redirect_init",
allow_redirects=True)
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
def test_chained_redirect(redirect_app):
"""Test test_client is working for redirection"""
request, response = redirect_app.test_client.get("/1")
assert request.url.endswith("/1")
request, response = redirect_app.test_client.get('/1')
assert request.url.endswith('/1')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
try:
assert response.url.endswith("/3")
assert response.url.endswith('/3')
except AttributeError:
assert response.url.path.endswith("/3")
assert response.url.path.endswith('/3')
def test_redirect_with_header_injection(redirect_app):
@@ -100,16 +102,17 @@ def test_redirect_with_header_injection(redirect_app):
Test redirection to a URL with header and body injections.
"""
request, response = redirect_app.test_client.get(
"/redirect_with_header_injection", allow_redirects=False
)
"/redirect_with_header_injection",
allow_redirects=False)
assert response.status == 302
assert "test-header" not in response.headers
assert not response.text.startswith("test-body")
assert not response.text.startswith('test-body')
@pytest.mark.parametrize("test_str", ["sanic-test", "sanictest", "sanic test"])
async def test_redirect_with_params(app, test_client, test_str):
@app.route("/api/v1/test/<test>/")
async def init_handler(request, test):
assert test == test_str

View File

@@ -7,17 +7,17 @@ from sanic.response import text, stream
async def test_request_cancel_when_connection_lost(loop, app, test_client):
app.still_serving_cancelled_request = False
@app.get("/")
@app.get('/')
async def handler(request):
await asyncio.sleep(1.0)
# at this point client is already disconnected
app.still_serving_cancelled_request = True
return text("OK")
return text('OK')
test_cli = await test_client(app)
# schedule client call
task = loop.create_task(test_cli.get("/"))
task = loop.create_task(test_cli.get('/'))
loop.call_later(0.01, task)
await asyncio.sleep(0.5)
@@ -36,7 +36,7 @@ async def test_request_cancel_when_connection_lost(loop, app, test_client):
async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
app.still_serving_cancelled_request = False
@app.post("/post/<id>", stream=True)
@app.post('/post/<id>', stream=True)
async def post(request, id):
assert isinstance(request.stream, asyncio.Queue)
@@ -45,7 +45,7 @@ async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
body = await request.stream.get()
if body is None:
break
await response.write(body.decode("utf-8"))
await response.write(body.decode('utf-8'))
await asyncio.sleep(1.0)
# at this point client is already disconnected
@@ -56,7 +56,7 @@ async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
test_cli = await test_client(app)
# schedule client call
task = loop.create_task(test_cli.post("/post/1"))
task = loop.create_task(test_cli.post('/post/1'))
loop.call_later(0.01, task)
await asyncio.sleep(0.5)

View File

@@ -9,37 +9,39 @@ except ImportError:
def test_storage(app):
@app.middleware("request")
@app.middleware('request')
def store(request):
request["user"] = "sanic"
request["sidekick"] = "tails"
del request["sidekick"]
request['user'] = 'sanic'
request['sidekick'] = 'tails'
del request['sidekick']
@app.route("/")
@app.route('/')
def handler(request):
return json(
{"user": request.get("user"), "sidekick": request.get("sidekick")}
)
return json({
'user': request.get('user'),
'sidekick': request.get('sidekick')
})
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
response_json = loads(response.text)
assert response_json["user"] == "sanic"
assert response_json.get("sidekick") is None
assert response_json['user'] == 'sanic'
assert response_json.get('sidekick') is None
def test_app_injection(app):
expected = random.choice(range(0, 100))
@app.listener("after_server_start")
@app.listener('after_server_start')
async def inject_data(app, loop):
app.injected = expected
@app.get("/")
@app.get('/')
async def handler(request):
return json({"injected": request.app.injected})
return json({'injected': request.app.injected})
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
response_json = loads(response.text)
assert response_json["injected"] == expected
assert response_json['injected'] == expected

View File

@@ -4,477 +4,448 @@ from sanic.views import CompositionView
from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator
from sanic.response import stream, text
from sanic.request import StreamBuffer
data = "abc" * 10000000
data = "abc" * 100000
def test_request_stream_method_view(app):
"""for self.is_request_stream = True"""
'''for self.is_request_stream = True'''
class SimpleView(HTTPMethodView):
def get(self, request):
assert request.stream is None
return text("OK")
return text('OK')
@stream_decorator
async def post(self, request):
assert isinstance(request.stream, StreamBuffer)
result = ""
assert isinstance(request.stream, asyncio.Queue)
result = ''
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
result += body.decode("utf-8")
result += body.decode('utf-8')
return text(result)
app.add_route(SimpleView.as_view(), "/method_view")
app.add_route(SimpleView.as_view(), '/method_view')
assert app.is_request_stream is True
request, response = app.test_client.get("/method_view")
request, response = app.test_client.get('/method_view')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
request, response = app.test_client.post("/method_view", data=data)
request, response = app.test_client.post('/method_view', data=data)
assert response.status == 200
assert response.text == data
def test_request_stream_app(app):
"""for self.is_request_stream = True and decorators"""
'''for self.is_request_stream = True and decorators'''
@app.get("/get")
@app.get('/get')
async def get(request):
assert request.stream is None
return text("GET")
return text('GET')
@app.head("/head")
@app.head('/head')
async def head(request):
assert request.stream is None
return text("HEAD")
return text('HEAD')
@app.delete("/delete")
@app.delete('/delete')
async def delete(request):
assert request.stream is None
return text("DELETE")
return text('DELETE')
@app.options("/options")
@app.options('/options')
async def options(request):
assert request.stream is None
return text("OPTIONS")
return text('OPTIONS')
@app.post("/_post/<id>")
@app.post('/_post/<id>')
async def _post(request, id):
assert request.stream is None
return text("_POST")
return text('_POST')
@app.post("/post/<id>", stream=True)
@app.post('/post/<id>', stream=True)
async def post(request, id):
assert isinstance(request.stream, StreamBuffer)
assert isinstance(request.stream, asyncio.Queue)
async def streaming(response):
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
await response.write(body.decode("utf-8"))
await response.write(body.decode('utf-8'))
return stream(streaming)
@app.put("/_put")
@app.put('/_put')
async def _put(request):
assert request.stream is None
return text("_PUT")
return text('_PUT')
@app.put("/put", stream=True)
@app.put('/put', stream=True)
async def put(request):
assert isinstance(request.stream, StreamBuffer)
assert isinstance(request.stream, asyncio.Queue)
async def streaming(response):
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
await response.write(body.decode("utf-8"))
await response.write(body.decode('utf-8'))
return stream(streaming)
@app.patch("/_patch")
@app.patch('/_patch')
async def _patch(request):
assert request.stream is None
return text("_PATCH")
return text('_PATCH')
@app.patch("/patch", stream=True)
@app.patch('/patch', stream=True)
async def patch(request):
assert isinstance(request.stream, StreamBuffer)
assert isinstance(request.stream, asyncio.Queue)
async def streaming(response):
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
await response.write(body.decode("utf-8"))
await response.write(body.decode('utf-8'))
return stream(streaming)
assert app.is_request_stream is True
request, response = app.test_client.get("/get")
request, response = app.test_client.get('/get')
assert response.status == 200
assert response.text == "GET"
assert response.text == 'GET'
request, response = app.test_client.head("/head")
request, response = app.test_client.head('/head')
assert response.status == 200
assert response.text == ""
assert response.text == ''
request, response = app.test_client.delete("/delete")
request, response = app.test_client.delete('/delete')
assert response.status == 200
assert response.text == "DELETE"
assert response.text == 'DELETE'
request, response = app.test_client.options("/options")
request, response = app.test_client.options('/options')
assert response.status == 200
assert response.text == "OPTIONS"
assert response.text == 'OPTIONS'
request, response = app.test_client.post("/_post/1", data=data)
request, response = app.test_client.post('/_post/1', data=data)
assert response.status == 200
assert response.text == "_POST"
assert response.text == '_POST'
request, response = app.test_client.post("/post/1", data=data)
request, response = app.test_client.post('/post/1', data=data)
assert response.status == 200
assert response.text == data
request, response = app.test_client.put("/_put", data=data)
request, response = app.test_client.put('/_put', data=data)
assert response.status == 200
assert response.text == "_PUT"
assert response.text == '_PUT'
request, response = app.test_client.put("/put", data=data)
request, response = app.test_client.put('/put', data=data)
assert response.status == 200
assert response.text == data
request, response = app.test_client.patch("/_patch", data=data)
request, response = app.test_client.patch('/_patch', data=data)
assert response.status == 200
assert response.text == "_PATCH"
assert response.text == '_PATCH'
request, response = app.test_client.patch("/patch", data=data)
request, response = app.test_client.patch('/patch', data=data)
assert response.status == 200
assert response.text == data
def test_request_stream_handle_exception(app):
"""for handling exceptions properly"""
'''for handling exceptions properly'''
@app.post("/post/<id>", stream=True)
@app.post('/post/<id>', stream=True)
async def post(request, id):
assert isinstance(request.stream, StreamBuffer)
assert isinstance(request.stream, asyncio.Queue)
async def streaming(response):
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
await response.write(body.decode("utf-8"))
await response.write(body.decode('utf-8'))
return stream(streaming)
# 404
request, response = app.test_client.post("/in_valid_post", data=data)
request, response = app.test_client.post('/in_valid_post', data=data)
assert response.status == 404
assert response.text == "Error: Requested URL /in_valid_post not found"
assert response.text == 'Error: Requested URL /in_valid_post not found'
# 405
request, response = app.test_client.get("/post/random_id", data=data)
request, response = app.test_client.get('/post/random_id', data=data)
assert response.status == 405
assert (
response.text == "Error: Method GET not allowed for URL"
" /post/random_id"
)
assert response.text == 'Error: Method GET not allowed for URL' \
' /post/random_id'
def test_request_stream_blueprint(app):
"""for self.is_request_stream = True"""
bp = Blueprint("test_blueprint_request_stream_blueprint")
'''for self.is_request_stream = True'''
bp = Blueprint('test_blueprint_request_stream_blueprint')
@app.get("/get")
@app.get('/get')
async def get(request):
assert request.stream is None
return text("GET")
return text('GET')
@bp.head("/head")
@bp.head('/head')
async def head(request):
assert request.stream is None
return text("HEAD")
return text('HEAD')
@bp.delete("/delete")
@bp.delete('/delete')
async def delete(request):
assert request.stream is None
return text("DELETE")
return text('DELETE')
@bp.options("/options")
@bp.options('/options')
async def options(request):
assert request.stream is None
return text("OPTIONS")
return text('OPTIONS')
@bp.post("/_post/<id>")
@bp.post('/_post/<id>')
async def _post(request, id):
assert request.stream is None
return text("_POST")
return text('_POST')
@bp.post("/post/<id>", stream=True)
@bp.post('/post/<id>', stream=True)
async def post(request, id):
assert isinstance(request.stream, StreamBuffer)
assert isinstance(request.stream, asyncio.Queue)
async def streaming(response):
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
await response.write(body.decode("utf-8"))
await response.write(body.decode('utf-8'))
return stream(streaming)
@bp.put("/_put")
@bp.put('/_put')
async def _put(request):
assert request.stream is None
return text("_PUT")
return text('_PUT')
@bp.put("/put", stream=True)
@bp.put('/put', stream=True)
async def put(request):
assert isinstance(request.stream, StreamBuffer)
assert isinstance(request.stream, asyncio.Queue)
async def streaming(response):
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
await response.write(body.decode("utf-8"))
await response.write(body.decode('utf-8'))
return stream(streaming)
@bp.patch("/_patch")
@bp.patch('/_patch')
async def _patch(request):
assert request.stream is None
return text("_PATCH")
return text('_PATCH')
@bp.patch("/patch", stream=True)
@bp.patch('/patch', stream=True)
async def patch(request):
assert isinstance(request.stream, StreamBuffer)
assert isinstance(request.stream, asyncio.Queue)
async def streaming(response):
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
await response.write(body.decode("utf-8"))
await response.write(body.decode('utf-8'))
return stream(streaming)
async def post_add_route(request):
assert isinstance(request.stream, StreamBuffer)
async def streaming(response):
while True:
body = await request.stream.read()
if body is None:
break
await response.write(body.decode("utf-8"))
return stream(streaming)
bp.add_route(
post_add_route, "/post/add_route", methods=["POST"], stream=True
)
app.blueprint(bp)
assert app.is_request_stream is True
request, response = app.test_client.get("/get")
request, response = app.test_client.get('/get')
assert response.status == 200
assert response.text == "GET"
assert response.text == 'GET'
request, response = app.test_client.head("/head")
request, response = app.test_client.head('/head')
assert response.status == 200
assert response.text == ""
assert response.text == ''
request, response = app.test_client.delete("/delete")
request, response = app.test_client.delete('/delete')
assert response.status == 200
assert response.text == "DELETE"
assert response.text == 'DELETE'
request, response = app.test_client.options("/options")
request, response = app.test_client.options('/options')
assert response.status == 200
assert response.text == "OPTIONS"
assert response.text == 'OPTIONS'
request, response = app.test_client.post("/_post/1", data=data)
request, response = app.test_client.post('/_post/1', data=data)
assert response.status == 200
assert response.text == "_POST"
assert response.text == '_POST'
request, response = app.test_client.post("/post/1", data=data)
request, response = app.test_client.post('/post/1', data=data)
assert response.status == 200
assert response.text == data
request, response = app.test_client.put("/_put", data=data)
request, response = app.test_client.put('/_put', data=data)
assert response.status == 200
assert response.text == "_PUT"
assert response.text == '_PUT'
request, response = app.test_client.put("/put", data=data)
request, response = app.test_client.put('/put', data=data)
assert response.status == 200
assert response.text == data
request, response = app.test_client.patch("/_patch", data=data)
request, response = app.test_client.patch('/_patch', data=data)
assert response.status == 200
assert response.text == "_PATCH"
assert response.text == '_PATCH'
request, response = app.test_client.patch("/patch", data=data)
assert response.status == 200
assert response.text == data
request, response = app.test_client.post("/post/add_route", data=data)
request, response = app.test_client.patch('/patch', data=data)
assert response.status == 200
assert response.text == data
def test_request_stream_composition_view(app):
"""for self.is_request_stream = True"""
'''for self.is_request_stream = True'''
def get_handler(request):
assert request.stream is None
return text("OK")
return text('OK')
async def post_handler(request):
assert isinstance(request.stream, StreamBuffer)
result = ""
assert isinstance(request.stream, asyncio.Queue)
result = ''
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
result += body.decode("utf-8")
result += body.decode('utf-8')
return text(result)
view = CompositionView()
view.add(["GET"], get_handler)
view.add(["POST"], post_handler, stream=True)
app.add_route(view, "/composition_view")
view.add(['GET'], get_handler)
view.add(['POST'], post_handler, stream=True)
app.add_route(view, '/composition_view')
assert app.is_request_stream is True
request, response = app.test_client.get("/composition_view")
request, response = app.test_client.get('/composition_view')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
request, response = app.test_client.post("/composition_view", data=data)
request, response = app.test_client.post('/composition_view', data=data)
assert response.status == 200
assert response.text == data
def test_request_stream(app):
"""test for complex application"""
bp = Blueprint("test_blueprint_request_stream")
'''test for complex application'''
bp = Blueprint('test_blueprint_request_stream')
class SimpleView(HTTPMethodView):
def get(self, request):
assert request.stream is None
return text("OK")
return text('OK')
@stream_decorator
async def post(self, request):
assert isinstance(request.stream, StreamBuffer)
result = ""
assert isinstance(request.stream, asyncio.Queue)
result = ''
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
result += body.decode("utf-8")
result += body.decode('utf-8')
return text(result)
@app.post("/stream", stream=True)
@app.post('/stream', stream=True)
async def handler(request):
assert isinstance(request.stream, StreamBuffer)
assert isinstance(request.stream, asyncio.Queue)
async def streaming(response):
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
await response.write(body.decode("utf-8"))
await response.write(body.decode('utf-8'))
return stream(streaming)
@app.get("/get")
@app.get('/get')
async def get(request):
assert request.stream is None
return text("OK")
return text('OK')
@bp.post("/bp_stream", stream=True)
@bp.post('/bp_stream', stream=True)
async def bp_stream(request):
assert isinstance(request.stream, StreamBuffer)
result = ""
assert isinstance(request.stream, asyncio.Queue)
result = ''
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
result += body.decode("utf-8")
result += body.decode('utf-8')
return text(result)
@bp.get("/bp_get")
@bp.get('/bp_get')
async def bp_get(request):
assert request.stream is None
return text("OK")
return text('OK')
def get_handler(request):
assert request.stream is None
return text("OK")
return text('OK')
async def post_handler(request):
assert isinstance(request.stream, StreamBuffer)
result = ""
assert isinstance(request.stream, asyncio.Queue)
result = ''
while True:
body = await request.stream.read()
body = await request.stream.get()
if body is None:
break
result += body.decode("utf-8")
result += body.decode('utf-8')
return text(result)
app.add_route(SimpleView.as_view(), "/method_view")
app.add_route(SimpleView.as_view(), '/method_view')
view = CompositionView()
view.add(["GET"], get_handler)
view.add(["POST"], post_handler, stream=True)
view.add(['GET'], get_handler)
view.add(['POST'], post_handler, stream=True)
app.blueprint(bp)
app.add_route(view, "/composition_view")
app.add_route(view, '/composition_view')
assert app.is_request_stream is True
request, response = app.test_client.get("/method_view")
request, response = app.test_client.get('/method_view')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
request, response = app.test_client.post("/method_view", data=data)
request, response = app.test_client.post('/method_view', data=data)
assert response.status == 200
assert response.text == data
request, response = app.test_client.get("/composition_view")
request, response = app.test_client.get('/composition_view')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
request, response = app.test_client.post("/composition_view", data=data)
request, response = app.test_client.post('/composition_view', data=data)
assert response.status == 200
assert response.text == data
request, response = app.test_client.get("/get")
request, response = app.test_client.get('/get')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
request, response = app.test_client.post("/stream", data=data)
request, response = app.test_client.post('/stream', data=data)
assert response.status == 200
assert response.text == data
request, response = app.test_client.get("/bp_get")
request, response = app.test_client.get('/bp_get')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
request, response = app.test_client.post("/bp_stream", data=data)
request, response = app.test_client.post('/bp_stream', data=data)
assert response.status == 200
assert response.text == data

View File

@@ -3,6 +3,7 @@ from json import JSONDecodeError
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.config import Config
import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST
@@ -11,7 +12,6 @@ try:
try:
# direct use
import packaging
version = packaging.version
except (ImportError, AttributeError):
# setuptools v39.0 and above.
@@ -29,15 +29,15 @@ aiohttp_version = version.parse(aiohttp.__version__)
class DelayableTCPConnector(TCPConnector):
class RequestContextManager(object):
def __new__(cls, req, delay):
cls = super(
DelayableTCPConnector.RequestContextManager, cls
).__new__(cls)
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_send = getattr(req, 'send')
cls.orig_start = None
cls.delay = delay
cls._acting_as = req
@@ -54,7 +54,7 @@ class DelayableTCPConnector(TCPConnector):
self.send_task = None
self.resp = resp
self._acting_as = self.resp
self.orig_start = getattr(resp, "start")
self.orig_start = getattr(resp, 'start')
try:
if aiohttp_version >= version.parse("3.3.0"):
@@ -92,10 +92,10 @@ class DelayableTCPConnector(TCPConnector):
request_info=None,
traces=[],
loop=req.loop,
session=None,
session=None
)
if aiohttp_version < version.parse("3.3.0"):
kw["auto_decompress"] = None
kw['auto_decompress'] = None
return aiohttp.ClientResponse(req.method, req.url, **kw)
def _send(self, *args, **kwargs):
@@ -109,26 +109,24 @@ class DelayableTCPConnector(TCPConnector):
# 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)
_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
)
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)
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)
@@ -141,27 +139,24 @@ class DelayableSanicTestClient(SanicTestClient):
self._request_delay = request_delay
self._loop = None
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
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://" "//")):
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:
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:
url, *args, **kwargs) as response:
try:
response.text = await response.text()
except UnicodeDecodeError:
@@ -169,63 +164,60 @@ class DelayableSanicTestClient(SanicTestClient):
try:
response.json = await response.json()
except (
JSONDecodeError,
except (JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError,
):
aiohttp.ClientResponseError):
response.json = None
response.body = await response.read()
return response
request_timeout_default_app = Sanic("test_request_timeout_default")
request_no_timeout_app = Sanic("test_request_no_timeout")
request_timeout_default_app.config.REQUEST_TIMEOUT = 0.6
request_no_timeout_app.config.REQUEST_TIMEOUT = 0.6
Config.REQUEST_TIMEOUT = 0.6
request_timeout_default_app = Sanic('test_request_timeout_default')
request_no_timeout_app = Sanic('test_request_no_timeout')
@request_timeout_default_app.route("/1")
@request_timeout_default_app.route('/1')
async def handler1(request):
return text("OK")
return text('OK')
@request_no_timeout_app.route("/1")
@request_no_timeout_app.route('/1')
async def handler2(request):
return text("OK")
return text('OK')
@request_timeout_default_app.websocket("/ws1")
@request_timeout_default_app.websocket('/ws1')
async def ws_handler1(request, ws):
await ws.send("OK")
await ws.send('OK')
def test_default_server_error_request_timeout():
client = DelayableSanicTestClient(request_timeout_default_app, None, 2)
request, response = client.get("/1")
request, response = client.get('/1')
assert response.status == 408
assert response.text == "Error: Request Timeout"
assert response.text == 'Error: Request Timeout'
def test_default_server_error_request_dont_timeout():
client = DelayableSanicTestClient(request_no_timeout_app, None, 0.2)
request, response = client.get("/1")
request, response = client.get('/1')
assert response.status == 200
assert response.text == "OK"
assert response.text == 'OK'
def test_default_server_error_websocket_request_timeout():
headers = {
"Upgrade": "websocket",
"Connection": "upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'
}
client = DelayableSanicTestClient(request_timeout_default_app, None, 2)
request, response = client.get("/ws1", headers=headers)
request, response = client.get('/ws1', headers=headers)
assert response.status == 408
assert response.text == "Error: Request Timeout"
assert response.text == 'Error: Request Timeout'

File diff suppressed because it is too large Load Diff

View File

@@ -10,128 +10,127 @@ from urllib.parse import unquote
import pytest
from aiofiles import os as async_os
from sanic.response import (
HTTPResponse,
StreamingHTTPResponse,
file,
file_stream,
json,
raw,
stream,
text,
)
from sanic.response import (HTTPResponse, StreamingHTTPResponse, file,
file_stream, json, raw, stream, text)
from sanic.server import HttpProtocol
from sanic.testing import HOST, PORT
JSON_DATA = {"ok": True}
JSON_DATA = {'ok': True}
def test_response_body_not_a_string(app):
"""Test when a response body sent from the application is not a string"""
random_num = choice(range(1000))
@app.route("/hello")
@app.route('/hello')
async def hello_route(request):
return HTTPResponse(body=random_num)
request, response = app.test_client.get("/hello")
request, response = app.test_client.get('/hello')
assert response.text == str(random_num)
async def sample_streaming_fn(response):
await response.write("foo,")
await asyncio.sleep(0.001)
await response.write("bar")
await response.write('foo,')
await asyncio.sleep(.001)
await response.write('bar')
def test_method_not_allowed(app):
@app.get("/")
@app.get('/')
async def test_get(request):
return response.json({"hello": "world"})
return response.json({'hello': 'world'})
request, response = app.test_client.head("/")
assert response.headers["Allow"] == "GET"
request, response = app.test_client.head('/')
assert response.headers['Allow'] == 'GET'
request, response = app.test_client.post("/")
assert response.headers["Allow"] == "GET"
request, response = app.test_client.post('/')
assert response.headers['Allow'] == 'GET'
@app.post("/")
@app.post('/')
async def test_post(request):
return response.json({"hello": "world"})
return response.json({'hello': 'world'})
request, response = app.test_client.head("/")
request, response = app.test_client.head('/')
assert response.status == 405
assert set(response.headers["Allow"].split(", ")) == {"GET", "POST"}
assert response.headers["Content-Length"] == "0"
assert set(response.headers['Allow'].split(', ')) == {'GET', 'POST'}
assert response.headers['Content-Length'] == '0'
request, response = app.test_client.patch("/")
request, response = app.test_client.patch('/')
assert response.status == 405
assert set(response.headers["Allow"].split(", ")) == {"GET", "POST"}
assert response.headers["Content-Length"] == "0"
assert set(response.headers['Allow'].split(', ')) == {'GET', 'POST'}
assert response.headers['Content-Length'] == '0'
def test_response_header(app):
@app.get("/")
async def test(request):
return json({"ok": True}, headers={"CONTENT-TYPE": "application/json"})
request, response = app.test_client.get("/")
@app.get('/')
async def test(request):
return json({
"ok": True
}, headers={
'CONTENT-TYPE': 'application/json'
})
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',
}
def test_response_content_length(app):
@app.get("/response_with_space")
async def response_with_space(request):
return json(
{"message": "Data", "details": "Some Details"},
headers={"CONTENT-TYPE": "application/json"},
)
return json({
"message": "Data",
"details": "Some Details"
}, headers={
'CONTENT-TYPE': 'application/json'
})
@app.get("/response_without_space")
async def response_without_space(request):
return json(
{"message": "Data", "details": "Some Details"},
headers={"CONTENT-TYPE": "application/json"},
)
return json({
"message":"Data",
"details":"Some Details"
}, headers={
'CONTENT-TYPE': 'application/json'
})
_, response = app.test_client.get("/response_with_space")
content_length_for_response_with_space = response.headers.get(
"Content-Length"
)
content_length_for_response_with_space = response.headers.get("Content-Length")
_, response = app.test_client.get("/response_without_space")
content_length_for_response_without_space = response.headers.get(
"Content-Length"
)
content_length_for_response_without_space = response.headers.get("Content-Length")
assert (
content_length_for_response_with_space
== content_length_for_response_without_space
)
assert content_length_for_response_with_space == content_length_for_response_without_space
assert content_length_for_response_with_space == "43"
assert content_length_for_response_with_space == '43'
def test_response_content_length_with_different_data_types(app):
@app.get("/")
async def get_data_with_different_types(request):
# Indentation issues in the Response is intentional. Please do not fix
return json(
{"bool": True, "none": None, "string": "string", "number": -1},
headers={"CONTENT-TYPE": "application/json"},
)
return json({
'bool': True,
'none': None,
'string':'string',
'number': -1},
headers={
'CONTENT-TYPE': 'application/json'
})
_, response = app.test_client.get("/")
assert response.headers.get("Content-Length") == "55"
assert response.headers.get("Content-Length") == '55'
@pytest.fixture
def json_app(app):
@app.route("/")
async def test(request):
return json(JSON_DATA)
@@ -157,72 +156,70 @@ def json_app(app):
def test_json_response(json_app):
from sanic.response import json_dumps
request, response = json_app.test_client.get("/")
request, response = json_app.test_client.get('/')
assert response.status == 200
assert response.text == json_dumps(JSON_DATA)
assert response.json == JSON_DATA
def test_no_content(json_app):
request, response = json_app.test_client.get("/no-content")
request, response = json_app.test_client.get('/no-content')
assert response.status == 204
assert response.text == ""
assert "Content-Length" not in response.headers
assert response.text == ''
assert 'Content-Length' not in response.headers
request, response = json_app.test_client.get("/no-content/unmodified")
request, response = json_app.test_client.get('/no-content/unmodified')
assert response.status == 304
assert response.text == ""
assert "Content-Length" not in response.headers
assert "Content-Type" not in response.headers
assert response.text == ''
assert 'Content-Length' not in response.headers
assert 'Content-Type' not in response.headers
request, response = json_app.test_client.get("/unmodified")
request, response = json_app.test_client.get('/unmodified')
assert response.status == 304
assert response.text == ""
assert "Content-Length" not in response.headers
assert "Content-Type" not in response.headers
assert response.text == ''
assert 'Content-Length' not in response.headers
assert 'Content-Type' not in response.headers
request, response = json_app.test_client.delete("/")
request, response = json_app.test_client.delete('/')
assert response.status == 204
assert response.text == ""
assert "Content-Length" not in response.headers
assert response.text == ''
assert 'Content-Length' not in response.headers
@pytest.fixture
def streaming_app(app):
@app.route("/")
async def test(request):
return stream(sample_streaming_fn, content_type="text/csv")
return stream(sample_streaming_fn, content_type='text/csv')
return app
def test_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"
request, response = streaming_app.test_client.get('/')
assert response.headers['Transfer-Encoding'] == 'chunked'
assert response.headers['Content-Type'] == 'text/csv'
def test_streaming_returns_correct_content(streaming_app):
request, response = streaming_app.test_client.get("/")
assert response.text == "foo,bar"
request, response = streaming_app.test_client.get('/')
assert response.text == 'foo,bar'
@pytest.mark.parametrize("status", [200, 201, 400, 401])
@pytest.mark.parametrize('status', [200, 201, 400, 401])
def test_stream_response_status_returns_correct_headers(status):
response = StreamingHTTPResponse(sample_streaming_fn, status=status)
headers = response.get_headers()
assert b"HTTP/1.1 %s" % str(status).encode() in headers
@pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30])
@pytest.mark.parametrize('keep_alive_timeout', [10, 20, 30])
def test_stream_response_keep_alive_returns_correct_headers(
keep_alive_timeout
):
keep_alive_timeout):
response = StreamingHTTPResponse(sample_streaming_fn)
headers = response.get_headers(
keep_alive=True, keep_alive_timeout=keep_alive_timeout
)
keep_alive=True, keep_alive_timeout=keep_alive_timeout)
assert b"Keep-Alive: %s\r\n" % str(keep_alive_timeout).encode() in headers
@@ -247,19 +244,19 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app):
response.protocol.push_data = mock_push_data
response.protocol.drain = mock_drain
@streaming_app.listener("after_server_start")
@streaming_app.listener('after_server_start')
async def run_stream(app, loop):
await response.stream()
assert response.protocol.transport.write.call_args_list[1][0][0] == (
b"4\r\nfoo,\r\n"
b'4\r\nfoo,\r\n'
)
assert response.protocol.transport.write.call_args_list[2][0][0] == (
b"3\r\nbar\r\n"
b'3\r\nbar\r\n'
)
assert response.protocol.transport.write.call_args_list[3][0][0] == (
b"0\r\n\r\n"
b'0\r\n\r\n'
)
app.stop()
@@ -268,23 +265,25 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app):
def test_stream_response_with_cookies(app):
@app.route("/")
async def test(request):
response = stream(sample_streaming_fn, content_type="text/csv")
response.cookies["test"] = "modified"
response.cookies["test"] = "pass"
response = stream(sample_streaming_fn, content_type='text/csv')
response.cookies['test'] = 'modified'
response.cookies['test'] = 'pass'
return response
request, response = app.test_client.get("/")
assert response.cookies["test"].value == "pass"
request, response = app.test_client.get('/')
assert response.cookies['test'].value == 'pass'
def test_stream_response_without_cookies(app):
@app.route("/")
async def test(request):
return stream(sample_streaming_fn, content_type="text/csv")
return stream(sample_streaming_fn, content_type='text/csv')
request, response = app.test_client.get("/")
request, response = app.test_client.get('/')
assert response.cookies == {}
@@ -293,214 +292,195 @@ def static_file_directory():
"""The static directory to serve"""
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, "static")
static_directory = os.path.join(current_directory, 'static')
return static_directory
def get_file_content(static_file_directory, file_name):
"""The content of the static file to check"""
with open(os.path.join(static_file_directory, file_name), "rb") as file:
with open(os.path.join(static_file_directory, file_name), 'rb') as file:
return file.read()
@pytest.mark.parametrize(
"file_name", ["test.file", "decode me.txt", "python.png"]
)
@pytest.mark.parametrize("status", [200, 401])
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png'])
@pytest.mark.parametrize('status', [200, 401])
def test_file_response(app, file_name, static_file_directory, status):
@app.route("/files/<filename>", methods=["GET"])
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file(
file_path,
status=status,
mime_type=guess_type(file_path)[0] or "text/plain",
)
return file(file_path, status=status,
mime_type=guess_type(file_path)[0] or 'text/plain')
request, response = app.test_client.get("/files/{}".format(file_name))
request, response = app.test_client.get('/files/{}'.format(file_name))
assert response.status == status
assert response.body == get_file_content(static_file_directory, file_name)
assert "Content-Disposition" not in response.headers
assert 'Content-Disposition' not in response.headers
@pytest.mark.parametrize(
"source,dest",
'source,dest',
[
("test.file", "my_file.txt"),
("decode me.txt", "readme.md"),
("python.png", "logo.png"),
],
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'),
('python.png', 'logo.png')
]
)
def test_file_response_custom_filename(
app, source, dest, static_file_directory
):
@app.route("/files/<filename>", methods=["GET"])
def test_file_response_custom_filename(app, source, dest,
static_file_directory):
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file(file_path, filename=dest)
request, response = app.test_client.get("/files/{}".format(source))
request, response = app.test_client.get('/files/{}'.format(source))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, source)
assert response.headers[
"Content-Disposition"
] == 'attachment; filename="{}"'.format(dest)
assert response.headers['Content-Disposition'] == \
'attachment; filename="{}"'.format(dest)
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_file_head_response(app, file_name, static_file_directory):
@app.route("/files/<filename>", methods=["GET", "HEAD"])
@app.route('/files/<filename>', methods=['GET', 'HEAD'])
async def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
stats = await async_os.stat(file_path)
headers = dict()
headers["Accept-Ranges"] = "bytes"
headers["Content-Length"] = str(stats.st_size)
headers['Accept-Ranges'] = 'bytes'
headers['Content-Length'] = str(stats.st_size)
if request.method == "HEAD":
return HTTPResponse(
headers=headers,
content_type=guess_type(file_path)[0] or "text/plain",
)
content_type=guess_type(file_path)[0] or 'text/plain')
else:
return file(
file_path,
headers=headers,
mime_type=guess_type(file_path)[0] or "text/plain",
)
return file(file_path, headers=headers,
mime_type=guess_type(file_path)[0] or 'text/plain')
request, response = app.test_client.head("/files/{}".format(file_name))
request, response = app.test_client.head('/files/{}'.format(file_name))
assert response.status == 200
assert "Accept-Ranges" in response.headers
assert "Content-Length" in response.headers
assert int(response.headers["Content-Length"]) == len(
get_file_content(static_file_directory, file_name)
)
assert 'Accept-Ranges' in response.headers
assert 'Content-Length' in response.headers
assert int(response.headers[
'Content-Length']) == len(
get_file_content(static_file_directory, file_name))
@pytest.mark.parametrize(
"file_name", ["test.file", "decode me.txt", "python.png"]
)
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png'])
def test_file_stream_response(app, file_name, static_file_directory):
@app.route("/files/<filename>", methods=["GET"])
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file_stream(
file_path,
chunk_size=32,
mime_type=guess_type(file_path)[0] or "text/plain",
)
return file_stream(file_path, chunk_size=32,
mime_type=guess_type(file_path)[0] or 'text/plain')
request, response = app.test_client.get("/files/{}".format(file_name))
request, response = app.test_client.get('/files/{}'.format(file_name))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
assert "Content-Disposition" not in response.headers
assert 'Content-Disposition' not in response.headers
@pytest.mark.parametrize(
"source,dest",
'source,dest',
[
("test.file", "my_file.txt"),
("decode me.txt", "readme.md"),
("python.png", "logo.png"),
],
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'),
('python.png', 'logo.png')
]
)
def test_file_stream_response_custom_filename(
app, source, dest, static_file_directory
):
@app.route("/files/<filename>", methods=["GET"])
def test_file_stream_response_custom_filename(app, source, dest,
static_file_directory):
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file_stream(file_path, chunk_size=32, filename=dest)
request, response = app.test_client.get("/files/{}".format(source))
request, response = app.test_client.get('/files/{}'.format(source))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, source)
assert response.headers[
"Content-Disposition"
] == 'attachment; filename="{}"'.format(dest)
assert response.headers['Content-Disposition'] == \
'attachment; filename="{}"'.format(dest)
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_file_stream_head_response(app, file_name, static_file_directory):
@app.route("/files/<filename>", methods=["GET", "HEAD"])
@app.route('/files/<filename>', methods=['GET', 'HEAD'])
async def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
headers = dict()
headers["Accept-Ranges"] = "bytes"
headers['Accept-Ranges'] = 'bytes'
if request.method == "HEAD":
# Return a normal HTTPResponse, not a
# StreamingHTTPResponse for a HEAD request
stats = await async_os.stat(file_path)
headers["Content-Length"] = str(stats.st_size)
headers['Content-Length'] = str(stats.st_size)
return HTTPResponse(
headers=headers,
content_type=guess_type(file_path)[0] or "text/plain",
)
content_type=guess_type(file_path)[0] or 'text/plain')
else:
return file_stream(
file_path,
chunk_size=32,
headers=headers,
mime_type=guess_type(file_path)[0] or "text/plain",
file_path, chunk_size=32, headers=headers,
mime_type=guess_type(file_path)[0] or 'text/plain'
)
request, response = app.test_client.head("/files/{}".format(file_name))
request, response = app.test_client.head('/files/{}'.format(file_name))
assert response.status == 200
# A HEAD request should never be streamed/chunked.
if "Transfer-Encoding" in response.headers:
assert response.headers["Transfer-Encoding"] != "chunked"
assert "Accept-Ranges" in response.headers
if 'Transfer-Encoding' in response.headers:
assert response.headers['Transfer-Encoding'] != "chunked"
assert 'Accept-Ranges' in response.headers
# A HEAD request should get the Content-Length too
assert "Content-Length" in response.headers
assert int(response.headers["Content-Length"]) == len(
get_file_content(static_file_directory, file_name)
)
assert 'Content-Length' in response.headers
assert int(response.headers[
'Content-Length']) == len(
get_file_content(static_file_directory, file_name))
@pytest.mark.parametrize(
"file_name", ["test.file", "decode me.txt", "python.png"]
)
@pytest.mark.parametrize(
"size,start,end", [(1024, 0, 1024), (4096, 1024, 8192)]
)
def test_file_stream_response_range(
app, file_name, static_file_directory, size, start, end
):
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png'])
@pytest.mark.parametrize('size,start,end', [
(1024, 0, 1024),
(4096, 1024, 8192),
])
def test_file_stream_response_range(app, file_name, static_file_directory, size, start, end):
Range = namedtuple("Range", ["size", "start", "end", "total"])
Range = namedtuple('Range', ['size', 'start', 'end', 'total'])
total = len(get_file_content(static_file_directory, file_name))
range = Range(size=size, start=start, end=end, total=total)
@app.route("/files/<filename>", methods=["GET"])
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file_stream(
file_path,
chunk_size=32,
mime_type=guess_type(file_path)[0] or "text/plain",
_range=range,
)
mime_type=guess_type(file_path)[0] or 'text/plain',
_range=range)
request, response = app.test_client.get("/files/{}".format(file_name))
request, response = app.test_client.get('/files/{}'.format(file_name))
assert response.status == 206
assert "Content-Range" in response.headers
assert response.headers["Content-Range"] == "bytes {}-{}/{}".format(
range.start, range.end, range.total
)
assert 'Content-Range' in response.headers
assert response.headers['Content-Range'] == 'bytes {}-{}/{}'.format(range.start, range.end, range.total)
def test_raw_response(app):
@app.get("/test")
def handler(request):
return raw(b"raw_response")
request, response = app.test_client.get("/test")
assert response.content_type == "application/octet-stream"
assert response.body == b"raw_response"
@app.get('/test')
def handler(request):
return raw(b'raw_response')
request, response = app.test_client.get('/test')
assert response.content_type == 'application/octet-stream'
assert response.body == b'raw_response'

View File

@@ -2,43 +2,41 @@ from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.exceptions import ServiceUnavailable
from sanic.config import Config
response_timeout_app = Sanic("test_response_timeout")
response_timeout_default_app = Sanic("test_response_timeout_default")
response_handler_cancelled_app = Sanic("test_response_handler_cancelled")
response_timeout_app.config.RESPONSE_TIMEOUT = 1
response_timeout_default_app.config.RESPONSE_TIMEOUT = 1
response_handler_cancelled_app.config.RESPONSE_TIMEOUT = 1
Config.RESPONSE_TIMEOUT = 1
response_timeout_app = Sanic('test_response_timeout')
response_timeout_default_app = Sanic('test_response_timeout_default')
response_handler_cancelled_app = Sanic('test_response_handler_cancelled')
@response_timeout_app.route("/1")
@response_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(2)
return text("OK")
return text('OK')
@response_timeout_app.exception(ServiceUnavailable)
def handler_exception(request, exception):
return text("Response Timeout from error_handler.", 503)
return text('Response Timeout from error_handler.', 503)
def test_server_error_response_timeout():
request, response = response_timeout_app.test_client.get("/1")
request, response = response_timeout_app.test_client.get('/1')
assert response.status == 503
assert response.text == "Response Timeout from error_handler."
assert response.text == 'Response Timeout from error_handler.'
@response_timeout_default_app.route("/1")
@response_timeout_default_app.route('/1')
async def handler_2(request):
await asyncio.sleep(2)
return text("OK")
return text('OK')
def test_default_server_error_response_timeout():
request, response = response_timeout_default_app.test_client.get("/1")
request, response = response_timeout_default_app.test_client.get('/1')
assert response.status == 503
assert response.text == "Error: Response Timeout"
assert response.text == 'Error: Response Timeout'
response_handler_cancelled_app.flag = False
@@ -54,14 +52,14 @@ def handler_cancelled(request, exception):
# is already closed when we get a CancelledError.
@response_handler_cancelled_app.route("/1")
@response_handler_cancelled_app.route('/1')
async def handler_3(request):
await asyncio.sleep(2)
return text("OK")
return text('OK')
def test_response_handler_cancelled():
request, response = response_handler_cancelled_app.test_client.get("/1")
request, response = response_handler_cancelled_app.test_client.get('/1')
assert response.status == 503
assert response.text == "Error: Response Timeout"
assert response.text == 'Error: Response Timeout'
assert response_handler_cancelled_app.flag is False

File diff suppressed because it is too large Load Diff

View File

@@ -5,30 +5,30 @@ import pytest
from sanic.testing import HOST, PORT
AVAILABLE_LISTENERS = [
"before_server_start",
"after_server_start",
"before_server_stop",
"after_server_stop",
'before_server_start',
'after_server_start',
'before_server_stop',
'after_server_stop'
]
skipif_no_alarm = pytest.mark.skipif(
not hasattr(signal, "SIGALRM"),
reason="SIGALRM is not implemented for this platform, we have to come "
"up with another timeout strategy to test these",
not hasattr(signal, 'SIGALRM'),
reason='SIGALRM is not implemented for this platform, we have to come '
'up with another timeout strategy to test these'
)
def create_listener(listener_name, in_list):
async def _listener(app, loop):
print("DEBUG MESSAGE FOR PYTEST for {}".format(listener_name))
print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name))
in_list.insert(0, app.name + listener_name)
return _listener
def start_stop_app(random_name_app, **run_kwargs):
def stop_on_alarm(signum, frame):
raise KeyboardInterrupt("SIGINT for sanic to stop gracefully")
raise KeyboardInterrupt('SIGINT for sanic to stop gracefully')
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1)
@@ -39,18 +39,19 @@ def start_stop_app(random_name_app, **run_kwargs):
@skipif_no_alarm
@pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS)
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
def test_single_listener(app, listener_name):
"""Test that listeners on their own work"""
output = []
# Register listener
app.listener(listener_name)(create_listener(listener_name, output))
app.listener(listener_name)(
create_listener(listener_name, output))
start_stop_app(app)
assert app.name + listener_name == output.pop()
@skipif_no_alarm
@pytest.mark.parametrize("listener_name", AVAILABLE_LISTENERS)
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
def test_register_listener(app, listener_name):
"""
Test that listeners on their own work with
@@ -76,14 +77,15 @@ def test_all_listeners(app):
async def test_trigger_before_events_create_server(app):
class MySanicDb:
pass
@app.listener("before_server_start")
@app.listener('before_server_start')
async def init_db(app, loop):
app.db = MySanicDb()
await app.create_server(debug=True, return_asyncio_server=True)
await app.create_server()
assert hasattr(app, "db")
assert isinstance(app.db, MySanicDb)

View File

@@ -9,7 +9,6 @@ async def stop(app, loop):
await asyncio.sleep(0.1)
app.stop()
calledq = Queue()
@@ -24,13 +23,13 @@ def after(app, loop):
def test_register_system_signals(app):
"""Test if sanic register system signals"""
@app.route("/hello")
@app.route('/hello')
async def hello_route(request):
return HTTPResponse()
app.listener("after_server_start")(stop)
app.listener("before_server_start")(set_loop)
app.listener("after_server_stop")(after)
app.listener('after_server_start')(stop)
app.listener('before_server_start')(set_loop)
app.listener('after_server_stop')(after)
app.run(HOST, PORT)
assert calledq.get() is True
@@ -39,13 +38,13 @@ def test_register_system_signals(app):
def test_dont_register_system_signals(app):
"""Test if sanic don't register system signals"""
@app.route("/hello")
@app.route('/hello')
async def hello_route(request):
return HTTPResponse()
app.listener("after_server_start")(stop)
app.listener("before_server_start")(set_loop)
app.listener("after_server_stop")(after)
app.listener('after_server_start')(stop)
app.listener('before_server_start')(set_loop)
app.listener('after_server_stop')(after)
app.run(HOST, PORT, register_sys_signals=False)
assert calledq.get() is False

View File

@@ -5,12 +5,12 @@ from time import gmtime, strftime
import pytest
@pytest.fixture(scope="module")
@pytest.fixture(scope='module')
def static_file_directory():
"""The static directory to serve"""
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, "static")
static_directory = os.path.join(current_directory, 'static')
return static_directory
@@ -20,376 +20,336 @@ def get_file_path(static_file_directory, file_name):
def get_file_content(static_file_directory, file_name):
"""The content of the static file to check"""
with open(get_file_path(static_file_directory, file_name), "rb") as file:
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
return file.read()
@pytest.fixture(scope="module")
@pytest.fixture(scope='module')
def large_file(static_file_directory):
large_file_path = os.path.join(static_file_directory, "large.file")
large_file_path = os.path.join(static_file_directory, 'large.file')
size = 2 * 1024 * 1024
with open(large_file_path, "w") as f:
f.write("a" * size)
with open(large_file_path, 'w') as f:
f.write('a' * size)
yield large_file_path
os.remove(large_file_path)
@pytest.fixture(autouse=True, scope="module")
@pytest.fixture(autouse=True, scope='module')
def symlink(static_file_directory):
src = os.path.abspath(
os.path.join(os.path.dirname(static_file_directory), "conftest.py")
)
symlink = "symlink"
src = os.path.abspath(os.path.join(os.path.dirname(static_file_directory), 'conftest.py'))
symlink = 'symlink'
dist = os.path.join(static_file_directory, symlink)
os.symlink(src, dist)
yield symlink
os.remove(dist)
@pytest.fixture(autouse=True, scope="module")
@pytest.fixture(autouse=True, scope='module')
def hard_link(static_file_directory):
src = os.path.abspath(
os.path.join(os.path.dirname(static_file_directory), "conftest.py")
)
hard_link = "hard_link"
src = os.path.abspath(os.path.join(os.path.dirname(static_file_directory), 'conftest.py'))
hard_link = 'hard_link'
dist = os.path.join(static_file_directory, hard_link)
os.link(src, dist)
yield hard_link
os.remove(dist)
@pytest.mark.parametrize(
"file_name",
["test.file", "decode me.txt", "python.png", "symlink", "hard_link"],
)
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png', 'symlink', 'hard_link'])
def test_static_file(app, static_file_directory, file_name):
app.static(
"/testing.file", get_file_path(static_file_directory, file_name)
)
'/testing.file', get_file_path(static_file_directory, file_name))
request, response = app.test_client.get("/testing.file")
request, response = app.test_client.get('/testing.file')
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
@pytest.mark.parametrize("file_name", ["test.html"])
@pytest.mark.parametrize('file_name', ['test.html'])
def test_static_file_content_type(app, static_file_directory, file_name):
app.static(
"/testing.file",
'/testing.file',
get_file_path(static_file_directory, file_name),
content_type="text/html; charset=utf-8",
content_type='text/html; charset=utf-8'
)
request, response = app.test_client.get("/testing.file")
request, response = app.test_client.get('/testing.file')
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
assert response.headers["Content-Type"] == "text/html; charset=utf-8"
assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
@pytest.mark.parametrize(
"file_name", ["test.file", "decode me.txt", "symlink", "hard_link"]
)
@pytest.mark.parametrize("base_uri", ["/static", "", "/dir"])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'symlink', 'hard_link'])
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
def test_static_directory(app, file_name, base_uri, static_file_directory):
app.static(base_uri, static_file_directory)
request, response = app.test_client.get(
uri="{}/{}".format(base_uri, file_name)
)
uri='{}/{}'.format(base_uri, file_name))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_head_request(app, file_name, static_file_directory):
app.static(
"/testing.file",
get_file_path(static_file_directory, file_name),
use_content_range=True,
)
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
request, response = app.test_client.head("/testing.file")
request, response = app.test_client.head('/testing.file')
assert response.status == 200
assert "Accept-Ranges" in response.headers
assert "Content-Length" in response.headers
assert int(response.headers["Content-Length"]) == len(
get_file_content(static_file_directory, file_name)
)
assert 'Accept-Ranges' in response.headers
assert 'Content-Length' in response.headers
assert int(response.headers[
'Content-Length']) == len(
get_file_content(static_file_directory, file_name))
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_correct(app, file_name, static_file_directory):
app.static(
"/testing.file",
get_file_path(static_file_directory, file_name),
use_content_range=True,
)
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
headers = {"Range": "bytes=12-19"}
request, response = app.test_client.get("/testing.file", headers=headers)
headers = {
'Range': 'bytes=12-19'
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 206
assert "Content-Length" in response.headers
assert "Content-Range" in response.headers
static_content = bytes(get_file_content(static_file_directory, file_name))[
12:20
]
assert int(response.headers["Content-Length"]) == len(static_content)
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[12:20]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_front(app, file_name, static_file_directory):
app.static(
"/testing.file",
get_file_path(static_file_directory, file_name),
use_content_range=True,
)
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
headers = {"Range": "bytes=12-"}
request, response = app.test_client.get("/testing.file", headers=headers)
headers = {
'Range': 'bytes=12-'
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 206
assert "Content-Length" in response.headers
assert "Content-Range" in response.headers
static_content = bytes(get_file_content(static_file_directory, file_name))[
12:
]
assert int(response.headers["Content-Length"]) == len(static_content)
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[12:]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_back(app, file_name, static_file_directory):
app.static(
"/testing.file",
get_file_path(static_file_directory, file_name),
use_content_range=True,
)
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
headers = {"Range": "bytes=-12"}
request, response = app.test_client.get("/testing.file", headers=headers)
headers = {
'Range': 'bytes=-12'
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 206
assert "Content-Length" in response.headers
assert "Content-Range" in response.headers
static_content = bytes(get_file_content(static_file_directory, file_name))[
-12:
]
assert int(response.headers["Content-Length"]) == len(static_content)
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[-12:]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
@pytest.mark.parametrize("use_modified_since", [True, False])
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
def test_static_content_range_empty(
app, file_name, static_file_directory, use_modified_since
):
@pytest.mark.parametrize('use_modified_since', [True, False])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_empty(app, file_name, static_file_directory, use_modified_since):
app.static(
"/testing.file",
'/testing.file',
get_file_path(static_file_directory, file_name),
use_content_range=True,
use_modified_since=use_modified_since,
use_modified_since=use_modified_since
)
request, response = app.test_client.get("/testing.file")
request, response = app.test_client.get('/testing.file')
assert response.status == 200
assert "Content-Length" in response.headers
assert "Content-Range" not in response.headers
assert int(response.headers["Content-Length"]) == len(
get_file_content(static_file_directory, file_name)
)
assert 'Content-Length' in response.headers
assert 'Content-Range' not in response.headers
assert int(response.headers['Content-Length']) == \
len(get_file_content(static_file_directory, file_name))
assert response.body == bytes(
get_file_content(static_file_directory, file_name)
)
get_file_content(static_file_directory, file_name))
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_error(app, file_name, static_file_directory):
app.static(
"/testing.file",
get_file_path(static_file_directory, file_name),
use_content_range=True,
)
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
headers = {"Range": "bytes=1-0"}
request, response = app.test_client.get("/testing.file", headers=headers)
headers = {
'Range': 'bytes=1-0'
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 416
assert "Content-Length" in response.headers
assert "Content-Range" in response.headers
assert response.headers["Content-Range"] == "bytes */%s" % (
len(get_file_content(static_file_directory, file_name)),
)
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
assert response.headers['Content-Range'] == "bytes */%s" % (
len(get_file_content(static_file_directory, file_name)),)
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
def test_static_content_range_invalid_unit(
app, file_name, static_file_directory
):
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_invalid_unit(app, file_name, static_file_directory):
app.static(
"/testing.file",
get_file_path(static_file_directory, file_name),
use_content_range=True,
)
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
unit = "bit"
headers = {"Range": "{}=1-0".format(unit)}
request, response = app.test_client.get("/testing.file", headers=headers)
unit = 'bit'
headers = {
'Range': '{}=1-0'.format(unit)
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 416
assert response.text == "Error: {} is not a valid Range Type".format(unit)
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
def test_static_content_range_invalid_start(
app, file_name, static_file_directory
):
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_invalid_start(app, file_name, static_file_directory):
app.static(
"/testing.file",
get_file_path(static_file_directory, file_name),
use_content_range=True,
)
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
start = "start"
headers = {"Range": "bytes={}-0".format(start)}
request, response = app.test_client.get("/testing.file", headers=headers)
start = 'start'
headers = {
'Range': 'bytes={}-0'.format(start)
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 416
assert response.text == "Error: '{}' is invalid for Content Range".format(
start
)
assert response.text == "Error: '{}' is invalid for Content Range".format(start)
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
def test_static_content_range_invalid_end(
app, file_name, static_file_directory
):
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_invalid_end(app, file_name, static_file_directory):
app.static(
"/testing.file",
get_file_path(static_file_directory, file_name),
use_content_range=True,
)
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
end = "end"
headers = {"Range": "bytes=1-{}".format(end)}
request, response = app.test_client.get("/testing.file", headers=headers)
end = 'end'
headers = {
'Range': 'bytes=1-{}'.format(end)
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 416
assert response.text == "Error: '{}' is invalid for Content Range".format(
end
)
assert response.text == "Error: '{}' is invalid for Content Range".format(end)
@pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
def test_static_content_range_invalid_parameters(
app, file_name, static_file_directory
):
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_invalid_parameters(app, file_name, static_file_directory):
app.static(
"/testing.file",
get_file_path(static_file_directory, file_name),
use_content_range=True,
)
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
headers = {"Range": "bytes=-"}
request, response = app.test_client.get("/testing.file", headers=headers)
headers = {
'Range': 'bytes=-'
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 416
assert response.text == "Error: Invalid for Content Range parameters"
@pytest.mark.parametrize(
"file_name", ["test.file", "decode me.txt", "python.png"]
)
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png'])
def test_static_file_specified_host(app, static_file_directory, file_name):
app.static(
"/testing.file",
'/testing.file',
get_file_path(static_file_directory, file_name),
host="www.example.com",
host="www.example.com"
)
headers = {"Host": "www.example.com"}
request, response = app.test_client.get("/testing.file", headers=headers)
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
request, response = app.test_client.get("/testing.file")
request, response = app.test_client.get('/testing.file')
assert response.status == 404
@pytest.mark.parametrize("use_modified_since", [True, False])
@pytest.mark.parametrize("stream_large_files", [True, 1024])
@pytest.mark.parametrize("file_name", ["test.file", "large.file"])
def test_static_stream_large_file(
app,
static_file_directory,
file_name,
use_modified_since,
stream_large_files,
large_file,
):
@pytest.mark.parametrize('use_modified_since', [True, False])
@pytest.mark.parametrize('stream_large_files', [True, 1024])
@pytest.mark.parametrize('file_name', ['test.file', 'large.file'])
def test_static_stream_large_file(app, static_file_directory, file_name, use_modified_since, stream_large_files, large_file):
app.static(
"/testing.file",
'/testing.file',
get_file_path(static_file_directory, file_name),
use_modified_since=use_modified_since,
stream_large_files=stream_large_files,
stream_large_files=stream_large_files
)
request, response = app.test_client.get("/testing.file")
request, response = app.test_client.get('/testing.file')
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
@pytest.mark.parametrize(
"file_name", ["test.file", "decode me.txt", "python.png"]
)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_use_modified_since(app, static_file_directory, file_name):
file_stat = os.stat(get_file_path(static_file_directory, file_name))
modified_since = strftime(
"%a, %d %b %Y %H:%M:%S GMT", gmtime(file_stat.st_mtime)
)
modified_since = strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime(file_stat.st_mtime))
app.static(
"/testing.file",
'/testing.file',
get_file_path(static_file_directory, file_name),
use_modified_since=True,
use_modified_since=True
)
request, response = app.test_client.get(
"/testing.file", headers={"If-Modified-Since": modified_since}
)
'/testing.file', headers={'If-Modified-Since': modified_since})
assert response.status == 304
def test_file_not_found(app, static_file_directory):
app.static("/static", static_file_directory)
app.static('/static', static_file_directory)
request, response = app.test_client.get("/static/not_found")
request, response = app.test_client.get('/static/not_found')
assert response.status == 404
assert response.text == "Error: File not found"
assert response.text == 'Error: File not found'
@pytest.mark.parametrize("static_name", ["_static_name", "static"])
@pytest.mark.parametrize("file_name", ["test.html"])
@pytest.mark.parametrize('static_name', ['_static_name', 'static'])
@pytest.mark.parametrize('file_name', ['test.html'])
def test_static_name(app, static_file_directory, static_name, file_name):
app.static("/static", static_file_directory, name=static_name)
app.static('/static', static_file_directory, name=static_name)
request, response = app.test_client.get("/static/{}".format(file_name))
request, response = app.test_client.get('/static/{}'.format(file_name))
assert response.status == 200
@pytest.mark.parametrize("file_name", ["test.file"])
@pytest.mark.parametrize('file_name',
['test.file'])
def test_static_remove_route(app, static_file_directory, file_name):
app.static(
"/testing.file", get_file_path(static_file_directory, file_name)
'/testing.file',
get_file_path(static_file_directory, file_name)
)
request, response = app.test_client.get("/testing.file")
request, response = app.test_client.get('/testing.file')
assert response.status == 200
app.remove_route("/testing.file")
request, response = app.test_client.get("/testing.file")
app.remove_route('/testing.file')
request, response = app.test_client.get('/testing.file')
assert response.status == 404

View File

@@ -1,34 +0,0 @@
import socket
from sanic.testing import PORT, SanicTestClient
from sanic.response import json, text
# ------------------------------------------------------------ #
# UTF-8
# ------------------------------------------------------------ #
def test_test_client_port_none(app):
@app.get('/get')
def handler(request):
return text('OK')
test_client = SanicTestClient(app, port=None)
request, response = test_client.get('/get')
assert response.text == 'OK'
request, response = test_client.post('/get')
assert response.status == 405
def test_test_client_port_default(app):
@app.get('/get')
def handler(request):
return json(request.transport.get_extra_info('sockname')[1])
test_client = SanicTestClient(app)
assert test_client.port == PORT
request, response = test_client.get('/get')
assert response.json == PORT

View File

@@ -9,39 +9,27 @@ from sanic.exceptions import URLBuildError
import string
URL_FOR_ARGS1 = dict(arg1=["v1", "v2"])
URL_FOR_VALUE1 = "/myurl?arg1=v1&arg1=v2"
URL_FOR_ARGS2 = dict(arg1=["v1", "v2"], _anchor="anchor")
URL_FOR_VALUE2 = "/myurl?arg1=v1&arg1=v2#anchor"
URL_FOR_ARGS1 = dict(arg1=['v1', 'v2'])
URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2'
URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor')
URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
URL_FOR_ARGS3 = dict(
arg1="v1",
_anchor="anchor",
_scheme="http",
_server="{}:{}".format(test_host, test_port),
_external=True,
)
URL_FOR_VALUE3 = "http://{}:{}/myurl?arg1=v1#anchor".format(
test_host, test_port
)
URL_FOR_ARGS4 = dict(
arg1="v1",
_anchor="anchor",
_external=True,
_server="http://{}:{}".format(test_host, test_port),
)
URL_FOR_VALUE4 = "http://{}:{}/myurl?arg1=v1#anchor".format(
test_host, test_port
arg1='v1', _anchor='anchor', _scheme='http',
_server='{}:{}'.format(test_host, test_port), _external=True
)
URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host,
test_port)
URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True,
_server='http://{}:{}'.format(test_host, test_port))
URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host,
test_port)
def _generate_handlers_from_names(app, l):
for name in l:
# this is the easiest way to generate functions with dynamic names
exec(
'@app.route(name)\ndef {}(request):\n\treturn text("{}")'.format(
name, name
)
)
exec('@app.route(name)\ndef {}(request):\n\treturn text("{}")'.format(
name, name))
@pytest.fixture
@@ -57,69 +45,69 @@ def test_simple_url_for_getting(simple_app):
for letter in string.ascii_letters:
url = simple_app.url_for(letter)
assert url == "/{}".format(letter)
assert url == '/{}'.format(letter)
request, response = simple_app.test_client.get(url)
assert response.status == 200
assert response.text == letter
@pytest.mark.parametrize(
"args,url",
[
(URL_FOR_ARGS1, URL_FOR_VALUE1),
@pytest.mark.parametrize('args,url',
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
(URL_FOR_ARGS2, URL_FOR_VALUE2),
(URL_FOR_ARGS3, URL_FOR_VALUE3),
(URL_FOR_ARGS4, URL_FOR_VALUE4),
],
)
(URL_FOR_ARGS4, URL_FOR_VALUE4)])
def test_simple_url_for_getting_with_more_params(app, args, url):
@app.route("/myurl")
def passes(request):
return text("this should pass")
assert url == app.url_for("passes", **args)
@app.route('/myurl')
def passes(request):
return text('this should pass')
assert url == app.url_for('passes', **args)
request, response = app.test_client.get(url)
assert response.status == 200
assert response.text == "this should pass"
assert response.text == 'this should pass'
def test_url_for_with_server_name(app):
server_name = "{}:{}".format(test_host, test_port)
app.config.update({"SERVER_NAME": server_name})
path = "/myurl"
server_name = '{}:{}'.format(test_host, test_port)
app.config.update({
'SERVER_NAME': server_name
})
path = '/myurl'
@app.route(path)
def passes(request):
return text("this should pass")
return text('this should pass')
url = "http://{}{}".format(server_name, path)
assert url == app.url_for("passes", _server=None, _external=True)
url = 'http://{}{}'.format(server_name, path)
assert url == app.url_for('passes', _server=None, _external=True)
request, response = app.test_client.get(url)
assert response.status == 200
assert response.text == "this should pass"
assert response.text == 'this should pass'
def test_fails_if_endpoint_not_found(app):
@app.route("/fail")
@app.route('/fail')
def fail(request):
return text("this should fail")
return text('this should fail')
with pytest.raises(URLBuildError) as e:
app.url_for("passes")
app.url_for('passes')
assert str(e.value) == "Endpoint with name `passes` was not found"
assert str(e.value) == 'Endpoint with name `passes` was not found'
def test_fails_url_build_if_param_not_passed(app):
url = "/"
url = '/'
for letter in string.ascii_letters:
url += "<{}>/".format(letter)
url += '<{}>/'.format(letter)
@app.route(url)
def fail(request):
return text("this should fail")
return text('this should fail')
fail_args = list(string.ascii_letters)
fail_args.pop()
@@ -127,160 +115,130 @@ def test_fails_url_build_if_param_not_passed(app):
fail_kwargs = {l: l for l in fail_args}
with pytest.raises(URLBuildError) as e:
app.url_for("fail", **fail_kwargs)
app.url_for('fail', **fail_kwargs)
assert "Required parameter `Z` was not passed to url_for" in str(e.value)
assert 'Required parameter `Z` was not passed to url_for' in str(e.value)
def test_fails_url_build_if_params_not_passed(app):
@app.route("/fail")
@app.route('/fail')
def fail(request):
return text("this should fail")
return text('this should fail')
with pytest.raises(ValueError) as e:
app.url_for("fail", _scheme="http")
app.url_for('fail', _scheme='http')
assert str(e.value) == "When specifying _scheme, _external must be True"
assert str(e.value) == 'When specifying _scheme, _external must be True'
COMPLEX_PARAM_URL = (
"/<foo:int>/<four_letter_string:[A-z]{4}>/"
"<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>"
)
'/<foo:int>/<four_letter_string:[A-z]{4}>/'
'<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>')
PASSING_KWARGS = {
"foo": 4,
"four_letter_string": "woof",
"two_letter_string": "ba",
"normal_string": "normal",
"some_number": "1.001",
}
EXPECTED_BUILT_URL = "/4/woof/ba/normal/1.001"
'foo': 4, 'four_letter_string': 'woof',
'two_letter_string': 'ba', 'normal_string': 'normal',
'some_number': '1.001'}
EXPECTED_BUILT_URL = '/4/woof/ba/normal/1.001'
def test_fails_with_int_message(app):
@app.route(COMPLEX_PARAM_URL)
def fail(request):
return text("this should fail")
return text('this should fail')
failing_kwargs = dict(PASSING_KWARGS)
failing_kwargs["foo"] = "not_int"
failing_kwargs['foo'] = 'not_int'
with pytest.raises(URLBuildError) as e:
app.url_for("fail", **failing_kwargs)
app.url_for('fail', **failing_kwargs)
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
def test_passes_with_negative_int_message(app):
@app.route("path/<possibly_neg:int>/another-word")
def good(request, possibly_neg):
assert isinstance(possibly_neg, int)
return text("this should pass with `{}`".format(possibly_neg))
u_plus_3 = app.url_for("good", possibly_neg=3)
assert u_plus_3 == "/path/3/another-word", u_plus_3
request, response = app.test_client.get(u_plus_3)
assert response.text == "this should pass with `3`"
u_neg_3 = app.url_for("good", possibly_neg=-3)
assert u_neg_3 == "/path/-3/another-word", u_neg_3
request, response = app.test_client.get(u_neg_3)
assert response.text == "this should pass with `-3`"
def test_fails_with_two_letter_string_message(app):
@app.route(COMPLEX_PARAM_URL)
def fail(request):
return text("this should fail")
return text('this should fail')
failing_kwargs = dict(PASSING_KWARGS)
failing_kwargs["two_letter_string"] = "foobar"
failing_kwargs['two_letter_string'] = 'foobar'
with pytest.raises(URLBuildError) as e:
app.url_for("fail", **failing_kwargs)
app.url_for('fail', **failing_kwargs)
expected_error = (
'Value "foobar" for parameter `two_letter_string` '
"does not satisfy pattern [A-z]{2}"
)
'does not satisfy pattern [A-z]{2}')
assert str(e.value) == expected_error
def test_fails_with_number_message(app):
@app.route(COMPLEX_PARAM_URL)
def fail(request):
return text("this should fail")
return text('this should fail')
failing_kwargs = dict(PASSING_KWARGS)
failing_kwargs["some_number"] = "foo"
failing_kwargs['some_number'] = 'foo'
with pytest.raises(URLBuildError) as e:
app.url_for("fail", **failing_kwargs)
app.url_for('fail', **failing_kwargs)
expected_error = (
'Value "foo" for parameter `some_number` '
"does not match pattern for type `float`: -?[0-9\\\\.]+"
)
'does not match pattern for type `float`: [0-9\\\\.]+')
assert str(e.value) == expected_error
@pytest.mark.parametrize("number", [3, -3, 13.123, -13.123])
def test_passes_with_negative_number_message(app, number):
@app.route("path/<possibly_neg:number>/another-word")
def good(request, possibly_neg):
assert isinstance(possibly_neg, (int, float))
return text("this should pass with `{}`".format(possibly_neg))
u = app.url_for("good", possibly_neg=number)
assert u == "/path/{}/another-word".format(number), u
request, response = app.test_client.get(u)
# For ``number``, it has been cast to a float - so a ``3`` becomes a ``3.0``
assert response.text == "this should pass with `{}`".format(float(number))
def test_adds_other_supplied_values_as_query_string(app):
@app.route(COMPLEX_PARAM_URL)
def passes(request):
return text("this should pass")
return text('this should pass')
new_kwargs = dict(PASSING_KWARGS)
new_kwargs["added_value_one"] = "one"
new_kwargs["added_value_two"] = "two"
new_kwargs['added_value_one'] = 'one'
new_kwargs['added_value_two'] = 'two'
url = app.url_for("passes", **new_kwargs)
url = app.url_for('passes', **new_kwargs)
query = dict(parse_qsl(urlsplit(url).query))
assert query["added_value_one"] == "one"
assert query["added_value_two"] == "two"
assert query['added_value_one'] == 'one'
assert query['added_value_two'] == 'two'
@pytest.fixture
def blueprint_app(app):
first_print = Blueprint("first", url_prefix="/first")
second_print = Blueprint("second", url_prefix="/second")
first_print = Blueprint('first', url_prefix='/first')
second_print = Blueprint('second', url_prefix='/second')
@first_print.route("/foo")
@first_print.route('/foo')
def foo(request):
return text("foo from first")
return text('foo from first')
@first_print.route("/foo/<param>")
@first_print.route('/foo/<param>')
def foo_with_param(request, param):
return text("foo from first : {}".format(param))
return text(
'foo from first : {}'.format(param))
@second_print.route("/foo") # noqa
@second_print.route('/foo') # noqa
def foo(request):
return text("foo from second")
return text('foo from second')
@second_print.route("/foo/<param>") # noqa
@second_print.route('/foo/<param>') # noqa
def foo_with_param(request, param):
return text("foo from second : {}".format(param))
return text(
'foo from second : {}'.format(param))
app.blueprint(first_print)
app.blueprint(second_print)
@@ -289,65 +247,66 @@ def blueprint_app(app):
def test_blueprints_are_named_correctly(blueprint_app):
first_url = blueprint_app.url_for("first.foo")
assert first_url == "/first/foo"
first_url = blueprint_app.url_for('first.foo')
assert first_url == '/first/foo'
second_url = blueprint_app.url_for("second.foo")
assert second_url == "/second/foo"
second_url = blueprint_app.url_for('second.foo')
assert second_url == '/second/foo'
def test_blueprints_work_with_params(blueprint_app):
first_url = blueprint_app.url_for("first.foo_with_param", param="bar")
assert first_url == "/first/foo/bar"
first_url = blueprint_app.url_for('first.foo_with_param', param='bar')
assert first_url == '/first/foo/bar'
second_url = blueprint_app.url_for("second.foo_with_param", param="bar")
assert second_url == "/second/foo/bar"
second_url = blueprint_app.url_for('second.foo_with_param', param='bar')
assert second_url == '/second/foo/bar'
@pytest.fixture
def methodview_app(app):
class ViewOne(HTTPMethodView):
def get(self, request):
return text("I am get method")
return text('I am get method')
def post(self, request):
return text("I am post method")
return text('I am post method')
def put(self, request):
return text("I am put method")
return text('I am put method')
def patch(self, request):
return text("I am patch method")
return text('I am patch method')
def delete(self, request):
return text("I am delete method")
return text('I am delete method')
app.add_route(ViewOne.as_view("view_one"), "/view_one")
app.add_route(ViewOne.as_view('view_one'), '/view_one')
class ViewTwo(HTTPMethodView):
def get(self, request):
return text("I am get method")
return text('I am get method')
def post(self, request):
return text("I am post method")
return text('I am post method')
def put(self, request):
return text("I am put method")
return text('I am put method')
def patch(self, request):
return text("I am patch method")
return text('I am patch method')
def delete(self, request):
return text("I am delete method")
return text('I am delete method')
app.add_route(ViewTwo.as_view(), "/view_two")
app.add_route(ViewTwo.as_view(), '/view_two')
return app
def test_methodview_naming(methodview_app):
viewone_url = methodview_app.url_for("ViewOne")
viewtwo_url = methodview_app.url_for("ViewTwo")
viewone_url = methodview_app.url_for('ViewOne')
viewtwo_url = methodview_app.url_for('ViewTwo')
assert viewone_url == "/view_one"
assert viewtwo_url == "/view_two"
assert viewone_url == '/view_one'
assert viewtwo_url == '/view_two'

Some files were not shown because too many files have changed in this diff Show More