Compare commits

...

147 Commits
0.1.8 ... 0.2.0

Author SHA1 Message Date
Eli Uriegas
f6c2d0bcaf Merge pull request #297 from seemethere/increment_020
Increment version to 0.2.0
2017-01-14 11:25:10 -06:00
Eli Uriegas
e2a16f96a8 Increment version to 0.2.0 2017-01-14 11:24:31 -06:00
Eli Uriegas
cf60ebd988 Merge pull request #289 from seemethere/update_request_form_getitem
Update request.form to work with __getitem__
2017-01-11 17:02:45 -06:00
Eli Uriegas
9dd954bccd Update request.form to work with __getitem__ 2017-01-11 16:55:34 -06:00
Eli Uriegas
a02eb8e7cb Merge pull request #284 from r0fls/remove-defualt-type
remove default from host in _get method
2017-01-11 10:01:11 -06:00
Eli Uriegas
a93ca9b8f1 Merge pull request #285 from r0fls/blueprint-domains
add vhosts to blueprints
2017-01-11 10:00:35 -06:00
Raphael Deem
15e4ec7ffb add ability to override default host in blueprint 2017-01-10 22:08:15 -08:00
Raphael Deem
62df50e22b add vhosts to blueprints 2017-01-10 21:35:07 -08:00
Raphael Deem
055430d4b8 remove default from host in _get method 2017-01-10 16:01:21 -08:00
Eli Uriegas
57f27c41e0 Merge pull request #278 from r0fls/vhosts
add support for virtual hosts
2017-01-10 15:18:36 -06:00
Raphael Deem
4f832ac9af add support for virtual hosts 2017-01-08 18:46:29 -08:00
Eli Uriegas
b0bf989056 Merge pull request #261 from yoloseem/parsed_json
Cache request.json even when it's empty.
2017-01-08 11:57:33 -06:00
Eli Uriegas
fd0e8624c4 Merge pull request #276 from r0fls/workers
Fix multiple worker problem
2017-01-08 11:57:10 -06:00
Eli Uriegas
5566668a5f Change the skips to actual pytest skips
By using the builtin pytest skips we can identify that the tests are still there but are being currently skipped.

Will update later to remove the skips once we figure out why they freeze with pytest (I experienced this same issue with multiprocessing when testing start/stop events).
2017-01-08 11:55:08 -06:00
Raphael Deem
f8e6becb9e skip multiprocessing tests 2017-01-07 18:58:02 -08:00
Raphael Deem
dd28d70680 fix stop event 2017-01-07 18:46:38 -08:00
Raphael Deem
ed8e3f237c this branch is broken 2017-01-07 15:28:21 -08:00
Raphael Deem
77c04c4cf9 fix multiple worker problem 2017-01-07 12:57:14 -08:00
Eli Uriegas
ebc8d7168a Merge pull request #274 from AntonDnepr/cbw_fixes
Class based views fixes
2017-01-07 10:07:14 -06:00
Anton Zhyrney
434fa74e67 removed debug from test 2017-01-07 07:14:27 +02:00
Anton Zhyrney
47a4f34cdf tests&small update 2017-01-07 07:13:49 +02:00
Anton Zhyrney
1317b1799c add docstrings&updated docs 2017-01-07 06:57:07 +02:00
Anton Zhyrney
fcae4a9f0a added as_view 2017-01-07 06:30:23 +02:00
Eli Uriegas
d733c5bb7c Merge pull request #271 from easydaniel/master
Asyncpg example
2017-01-06 09:11:42 -06:00
Eli Uriegas
baf8254907 Change Ellipsis to None for consistency 2017-01-05 15:29:57 -06:00
Eli Uriegas
802e7d4654 Merge pull request #269 from r0fls/263
add configurable backlog
2017-01-04 20:37:19 -06:00
Raphael Deem
616e20d467 move backlog to run() 2017-01-04 09:31:06 -08:00
easydaniel
5c7c2cf85e Update sanic_asyncpg_example.py
Remove unused library
2017-01-04 23:35:06 +08:00
DanielChien
1942644434 modify config to varbles 2017-01-04 23:30:29 +08:00
DanielChien
b67482de9b add example for asyncpg 2017-01-04 23:29:09 +08:00
Eli Uriegas
35bb71f952 Merge pull request #270 from seemethere/add_server_event_tests
Add server event tests
2017-01-04 00:32:13 -06:00
Eli Uriegas
9c91b09ab1 Fix this to actually reflect current behavior 2017-01-04 00:23:59 -06:00
Eli Uriegas
06911a8d2e Add tests for server start/stop event functions 2017-01-04 00:23:35 -06:00
Raphael Deem
e7922c1b54 add configurable backlog #263 2017-01-03 18:36:31 -08:00
Eli Uriegas
16959caa34 Merge pull request #259 from yoloseem/route
Correct Router documentation
2017-01-03 18:47:54 -06:00
Eli Uriegas
0675f388b9 Merge pull request #255 from seemethere/add_more_verbose_debug_error_handling
Add more verbose debug error handling
2017-01-03 15:43:26 -06:00
Eli Uriegas
b4ad9459da Merge pull request #260 from yoloseem/gitign
Update .gitignore
2017-01-03 14:42:00 -06:00
Eli Uriegas
8a9b1fee14 Merge pull request #265 from youknowone/test-middleware-schedule
Prevent flooding of meaningless traceback in `sanic_endpoint_test`
2017-01-03 14:41:23 -06:00
Jeong YunWon
e6eb697bb2 Use constant PORT rather than literal in test code (#266) 2017-01-03 14:40:13 -06:00
Eli Uriegas
4ccc782e29 Merge pull request #209 from 38elements/protocol
Customizable protocol
2017-01-03 11:52:54 -06:00
Hyunjun Kim
cfdd9f66d1 Correct sanic.router.Router documentation 2017-01-02 14:32:22 +09:00
Hyunjun Kim
035cbf84ae Cache request.json even when it's null or empty
In case of request body is set to `{}`, `[]` or `null`, even it's
already processed, parsed_json won't be used due to its boolean
evaluation.
2017-01-02 14:20:20 +09:00
Hyunjun Kim
31e92a8b4f Update .gitignore
* .python-version is generated by `pyenv local` command
* .eggs/ directory contains *.egg files
2017-01-02 13:33:24 +09:00
Jeong YunWon
87c24e5a7c Prevent flooding of meaningless traceback in sanic_endpoint_test
When Sanic has an exception in a request middleware, it fails to
save request object in `results`. In `sanic_endpoint_test`, because
it always requires `results` to have both `request` and `response` objects,
it prints traceback like attached example. It is not a user code and
it doesn't give any information to users, it is better to suppress
to print this kind of error.

To fix it, this patch insert collect hook as first request middleware
to guarantee to successfully run it always.

```
app = <sanic.sanic.Sanic object at 0x1102b5358>, method = 'get', uri = '/ping/', gather_request = True, loop = None
debug = True, request_args = (), request_kwargs = {}
_collect_request = <function sanic_endpoint_test.<locals>._collect_request at 0x11286c158>
_collect_response = <function sanic_endpoint_test.<locals>._collect_response at 0x11286c378>

    def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
                            loop=None, debug=False, *request_args,
                            **request_kwargs):
        results = []
        exceptions = []

        if gather_request:
            @app.middleware
            def _collect_request(request):
                results.append(request)

        async def _collect_response(sanic, loop):
            try:
                response = await local_request(method, uri, *request_args,
                                               **request_kwargs)
                results.append(response)
            except Exception as e:
                exceptions.append(e)
            app.stop()

        app.run(host=HOST, debug=debug, port=42101,
                after_start=_collect_response, loop=loop)

        if exceptions:
            raise ValueError("Exception during request: {}".format(exceptions))

        if gather_request:
            try:
>               request, response = results
E               ValueError: not enough values to unpack (expected 2, got 1)

../sanic/sanic/utils.py:46: ValueError
```
2017-01-02 13:18:22 +09:00
Eli Uriegas
552ff9d736 Merge pull request #235 from yoloseem/patch-1
Allow Sanic-inherited application
2016-12-31 13:21:44 -06:00
Eli Uriegas
738396c2e2 Merge pull request #236 from seanpar203/token_property
Add token property to request
2016-12-31 13:21:12 -06:00
Eli Uriegas
9f18b5a096 Merge pull request #248 from youknowone/debuggable-typecheck
Handle hooks parameters in more debuggable way
2016-12-31 13:17:06 -06:00
Eli Uriegas
15c965c08c Make exception tests test unhandled exceptions
* Adds tests for unhandled exceptions
* Adds tests for unhandled exceptions in exception handlers
* Rewrites tests to utilize pytest fixtures (No need to create the app
on import)
2016-12-30 13:50:12 -06:00
Eli Uriegas
7a8fd6b0df Add more verbose error handling
* Adds logging to error messages in debug mode as pointed out in PR #249,
while also improving the debug message.
2016-12-30 13:48:17 -06:00
Eli Uriegas
3ada6f358c Merge pull request #197 from r0fls/178
convert header values to strings
2016-12-30 12:17:29 -06:00
Eli Uriegas
f1c2854358 Merge branch 'master' into 178 2016-12-30 12:15:08 -06:00
Eli Uriegas
87559a34f8 Include more explicit loop for headers conversion
Also merges master changes into this PR for this branch
2016-12-30 12:13:16 -06:00
Eli Uriegas
9586351f37 Merge pull request #243 from dutradda/add_remove_route_method
created methods to remove a route from api/router
2016-12-30 09:39:57 -06:00
Diogo
0f6ed642da created methods to remove a route from api/router 2016-12-30 07:36:57 -02:00
Eli Uriegas
73a57e1105 Merge pull request #252 from AntonDnepr/misprints
Small docs improvements
2016-12-29 11:49:55 -06:00
Anton Zhyrney
e7314d1775 fix misprints&renaming 2016-12-29 19:22:11 +02:00
38elements
ee8f8c2930 Merge branch 'master' into protocol 2016-12-29 16:44:15 +09:00
38elements
64e0e2d19f Improve custom_protocol.md 2016-12-29 16:41:04 +09:00
38elements
6bb4dae5e0 Fix format in custom_protocol.md 2016-12-29 13:25:04 +09:00
38elements
83e9d08853 Add document for custom protocol 2016-12-29 13:11:27 +09:00
Eli Uriegas
7dfa62516c Merge pull request #238 from r0fls/73
stop multiple worker server without sleep loop; issue #73
2016-12-28 18:05:06 -06:00
Jeong YunWon
15e7d8ab2e Handle hooks parameters in more debuggable way
1. not list() -> callable()
The args of hooking parameters of Sanic have to be callables.
For wrong parameters, errors will be generated from:
```
    listeners += args
```

By checking just list type, the raised error will be associated
with `[args]` instead of `args`, which is not given by users.
With this patch, the raised error will be associated with `args`.
Then users can notice their argument was neither callable nor list
in the easier way.

2. Function -> Functions in document
Regarding the parameter as a list is harmless to the user code.
But unawareness of its type can be list can limit the potent of
the user code.
2016-12-28 18:14:57 +09:00
Raphael Deem
a4f77984b7 stop multiple worker server without sleep loop; issue #73 2016-12-26 14:51:16 -08:00
38elements
39b279f0f2 Improve examples/custom_protocol.py 2016-12-26 23:54:59 +09:00
38elements
ac44900fc4 Add test and example for custom protocol 2016-12-26 23:41:10 +09:00
Sean Parsons
548458c3e0 Added test for new token property on request object. 2016-12-26 06:48:53 -05:00
Sean Parsons
986b0aa106 Added token property to request object. 2016-12-26 06:41:41 -05:00
Hyunjun Kim
01b42fb399 Allow Sanic-inherited application 2016-12-26 20:37:16 +09:00
Eli Uriegas
f74d44152a Merge pull request #234 from seemethere/update_tests_to_use_tox
Change travis job to use tox
2016-12-25 19:10:36 -08:00
Eli Uriegas
1557854755 Update to make flake8 actually work 2016-12-25 19:05:11 -08:00
Eli Uriegas
56d6c2a929 Change travis job to use tox 2016-12-25 18:55:25 -08:00
Eli Uriegas
5402e6d3a6 Merge pull request #212 from r0fls/176
allow overriding logging basicConfig settings
2016-12-25 09:13:51 -08:00
Raphael Deem
be9eca2d63 use try/except 2016-12-24 23:05:03 -08:00
Raphael Deem
7d7cbaacf1 header format function 2016-12-24 20:56:13 -08:00
Raphael Deem
00b5a496dd type -> isinstance 2016-12-24 20:56:13 -08:00
Raphael Deem
7e6c92dc52 convert header values to strings 2016-12-24 20:56:13 -08:00
Eli Uriegas
6cf3754051 Merge pull request #226 from seemethere/increment_019
Increment version to 0.1.9
2016-12-24 18:54:07 -08:00
Eli Uriegas
cf7616ebe5 Increment version to 0.1.9 2016-12-24 18:51:16 -08:00
Eli Uriegas
5b7964f8b6 Merge pull request #225 from seemethere/add_response_body_not_a_string_test
Add test for PR: #215
2016-12-24 18:50:11 -08:00
Eli Uriegas
f1f38c24da Add test for PR: #215 2016-12-24 18:47:15 -08:00
Eli Uriegas
67a50becb0 Merge pull request #215 from cr0hn/patch-1
Improvement: avoid to encoding in each HTTP Response
2016-12-24 18:41:01 -08:00
Eli Uriegas
d7e94473f3 Use a try/except, it's a bit faster
Also reorder some imports and add some comments
2016-12-24 18:37:55 -08:00
Eli Uriegas
184c896f41 Merge pull request #224 from seemethere/rewrite_static_files_tests
Rewrite static files tests
2016-12-24 18:23:06 -08:00
Eli Uriegas
8be849cc40 Rewrite static files tests
Relates to PR #188

Changes include:
- Rewriting to work with pytest fixtures and an actual static directory
- Addition of a test that covers file paths that must be
  unquoted as a uri
2016-12-24 18:18:56 -08:00
Eli Uriegas
275851a755 Merge pull request #188 from webtic/master
Find URL encoded filenames on the fs by decoding them first
2016-12-24 18:14:45 -08:00
Eli Uriegas
16182472fa Remove trailing whitespace 2016-12-24 18:11:46 -08:00
Eli Uriegas
29f3c22fed Rework conditionals to not be inline 2016-12-24 18:11:12 -08:00
Eli Uriegas
a116666d55 Merge pull request #223 from r0fls/115
Raise error if response is malformed.
2016-12-24 17:12:17 -08:00
Raphael Deem
c2622511ce Raise error if response is malformed. Issue #115 2016-12-24 17:09:41 -08:00
Eli Uriegas
50243037eb Merge pull request #222 from seemethere/add_py36_testing
Adds python36 to tox.ini and .travis.yml
2016-12-24 14:11:55 -08:00
Eli Uriegas
74f305cfb7 Adds python36 to tox.ini and .travis.yml 2016-12-24 14:06:53 -08:00
Eli Uriegas
75990fbaf4 Merge pull request #220 from kgantsov/master
Make golang performance test return JSON instead of string
2016-12-24 13:40:24 -08:00
cr0hn
cc982c5a61 Update response.py
Type check by isinstance
2016-12-24 15:24:25 +01:00
38elements
2d05243c4a Refactor arguments of run function 2016-12-24 22:49:48 +09:00
Konstantin Hantsov
2f0a582aa7 Make golang performance test return JSON instead of string 2016-12-24 10:28:34 +01:00
Eli Uriegas
665881471d Merge pull request #217 from cr0hn/patch-3
Upgraded Middlewares doc: Explain how to chain two (or more) middlewares
2016-12-23 22:31:03 -08:00
38elements
39211f8fbd Refactor arguments of serve function 2016-12-24 11:40:07 +09:00
Raphael Deem
32ea45d403 allow overriding logging.basicConfig 2016-12-23 16:17:34 -08:00
Eli Uriegas
cd17a42234 Fix some verbage 2016-12-23 09:59:28 -08:00
Eli Uriegas
8e19b5938c Merge pull request #216 from cr0hn/patch-2
Apply response Middleware always
2016-12-23 09:57:38 -08:00
Eli Uriegas
9e208ab744 Merge pull request #210 from kdelwat/testing-documentation
Create documentation for testing server endpoints.
2016-12-23 09:57:22 -08:00
Eli Uriegas
94bd9702e5 Merge pull request #211 from rmno/master
Fixed import error in docs
2016-12-23 09:57:08 -08:00
cr0hn
3add40625d Explain how to chain two (or more) middlewares
A funny and useful examples about how to chain middlewares.
2016-12-23 16:07:59 +01:00
cr0hn
5afae986a0 Apply response Middleware always
Response middleware are useful to apply some post-process information, just before send to the user. For example: Add some HTTP headers (security headers, for example), remove "Server" banner (for security reasons) or cookie management. 

The change is very very simple: although an "request" middleware has produced any response, we'll even apply the response middlewares.
2016-12-23 15:59:04 +01:00
cr0hn
f091d82bad Improvement
improvement: support fo binary data as a input. This do that the response process has more performance because not encoding needed.
2016-12-23 13:12:59 +01:00
Romano Bodha
5c1ef2c1cf Fixed import error 2016-12-23 01:42:05 +01:00
Cadel Watson
8411255700 Create documentation for testing server endpoints.
Currently the sanic.utils functionality is undocumented. This provides
information on the interface as well as a complete example of testing
a server endpoint.
2016-12-23 11:08:04 +11:00
38elements
c657c531b4 Customizable protocol 2016-12-23 00:13:38 +09:00
Eli Uriegas
ef9d8710f5 Merge pull request #200 from 38elements/invalid-usage
Change HttpParserError process
2016-12-17 22:50:00 -06:00
38elements
75fc9f91b9 Change HttpParserError process 2016-12-18 09:25:39 +09:00
Eli Uriegas
545d9eb59b Merge pull request #198 from sagnew/patch-2
Fix quotes in sample code for consistency
2016-12-14 11:39:22 -06:00
Sam Agnew
a9b67c3028 Fix quotes in sample code for consistency 2016-12-14 12:36:33 -05:00
Eli Uriegas
35e79f3985 Merge pull request #171 from jpiasetz/convert_dict_to_set
Convert connections dict to set
2016-12-14 11:30:45 -06:00
Eli Uriegas
435d5585e9 Fix leftover blank line
flake8 build failed here: https://travis-ci.org/channelcat/sanic/builds/183991976
2016-12-14 11:29:09 -06:00
Eli Uriegas
ddfb7f2861 Merge branch 'master' into convert_dict_to_set 2016-12-14 11:26:31 -06:00
Eli Uriegas
6c806549ae Merge pull request #194 from sagnew/patch-1
Fix PEP8 in Hello World example
2016-12-13 11:44:53 -06:00
Sam Agnew
8957e4ec25 Fix PEP8 in Hello World example 2016-12-13 12:35:46 -05:00
Paul Jongsma
2003eceba1 remove trailing space 2016-12-13 10:41:39 +01:00
Eli Uriegas
8fc1462d11 Merge pull request #193 from r0fls/jinja-example
add jinja example
2016-12-13 00:30:07 -06:00
Raphael Deem
93b45e9598 add jinja example 2016-12-12 22:25:24 -08:00
Eli Uriegas
a3a14cdab2 Merge pull request #170 from jpiasetz/convert_lambda_to_partial
Convert server lambda to partial
2016-12-12 20:40:29 -06:00
Paul Jongsma
9ba2f99ea2 added a comment on why to decode the file_path 2016-12-13 01:10:24 +01:00
Eli Uriegas
f9db796a6e Merge pull request #189 from AntonDnepr/aiopg_examples
Aiopg examples
2016-12-12 13:22:32 -06:00
Eli Uriegas
94c7aaf7f8 Merge pull request #190 from kamyarg/master
url params docs typo fix
2016-12-12 13:21:46 -06:00
kamyar
6ef6d9a905 url params docs typo fix
add missing '>' in url params docs example
2016-12-11 16:34:22 +02:00
Anton Zhyrney
b44e9baaec aiopg with sqlalchemy example 2016-12-11 14:21:02 +02:00
Anton Zhyrney
f9176bfdea pep8&improvements 2016-12-11 14:14:03 +02:00
Anton Zhyrney
721044b378 improvements for aiopg example 2016-12-11 14:04:24 +02:00
Anton Zhyrney
154f8570f0 add sanic aiopg example with raw sql 2016-12-11 13:43:31 +02:00
Paul Jongsma
0464d31a9c Find URL encoded filenames on the fs by decoding them first 2016-12-10 12:16:37 +01:00
Eli Uriegas
e3453553e1 Merge pull request #183 from 38elements/payload-too-large
Change Payload Too Large process
2016-12-08 10:27:55 -06:00
Eli Uriegas
6abaa78f9e Merge pull request #186 from r0fls/master
return 400 on invalid json post data
2016-12-08 10:27:29 -06:00
Raphael Deem
457507d8dc return 400 on invalid json post data 2016-12-07 20:40:31 -08:00
Eli Uriegas
3ea1a80496 Merge pull request #185 from 1a23456789/master
Fix test_request_timeout.py
2016-12-06 16:04:15 -06:00
1a23456789
fac4bca4f4 Fix test_request_timeout.py
This increases sleep time, Because sometimes timeout error does not occur.
2016-12-06 10:44:08 +09:00
38elements
662e0c9965 Change Payload Too Large process
When Payload Too Large occurs, it uses error handler.
2016-12-04 10:50:32 +09:00
Eli Uriegas
80af9e6d76 Merge pull request #173 from jackfischer/master
fix for cookie header capitalization bug
2016-12-03 17:28:38 -06:00
Jack Fischer
9b466db5c9 test for http2 lowercase header cookies 2016-12-03 15:19:24 -05:00
Jack Fischer
c34427690a Merge branch 'master' of git://github.com/channelcat/sanic 2016-12-03 15:08:07 -05:00
Eli Uriegas
d8a974bb4f Merge pull request #175 from Derrreks/master
Improving comments
2016-12-02 20:07:28 -06:00
Derek Schuster
70c56b7db3 fixing line length 2016-11-28 14:22:07 -05:00
Derek Schuster
209b763302 fix typo 2016-11-28 14:05:47 -05:00
Derek Schuster
190b7a6076 improving comments and examples 2016-11-28 14:00:39 -05:00
Jack Fischer
0c215685f2 refactoring cookies 2016-11-27 08:30:46 -05:00
Jack Fischer
d86ac5e3e0 fix for cookie header capitalization bug 2016-11-26 11:20:29 -05:00
John Piasetzki
47927608b2 Convert connections dict to set
Connections don't need to be a dict since the value is never used
2016-11-25 15:14:19 -05:00
John Piasetzki
13808bf282 Convert server lambda to partial
Partials are faster then lambdas for repeated calls.
2016-11-25 15:13:58 -05:00
48 changed files with 1469 additions and 219 deletions

4
.gitignore vendored
View File

@@ -1,11 +1,13 @@
*~
*.egg-info
*.egg
*.eggs
*.pyc
.coverage
.coverage.*
coverage
.tox
settings.py
*.pyc
.idea/*
.cache/*
.python-version

View File

@@ -1,14 +1,10 @@
sudo: false
language: python
python:
- '3.5'
install:
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
- python setup.py install
- pip install flake8
- pip install pytest
before_script: flake8 sanic
script: py.test -v tests
- '3.6'
install: pip install tox-travis
script: tox
deploy:
provider: pypi
user: channelcat

View File

@@ -33,13 +33,17 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E
from sanic import Sanic
from sanic.response import json
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
app.run(host="0.0.0.0", port=8000)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
```
## Installation
@@ -55,6 +59,8 @@ app.run(host="0.0.0.0", port=8000)
* [Class Based Views](docs/class_based_views.md)
* [Cookies](docs/cookies.md)
* [Static Files](docs/static_files.md)
* [Custom Protocol](docs/custom_protocol.md)
* [Testing](docs/testing.md)
* [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md)
* [License](LICENSE)

View File

@@ -6,6 +6,7 @@ Sanic has simple class based implementation. You should implement methods(get, p
```python
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
@@ -27,7 +28,7 @@ class SimpleView(HTTPMethodView):
def delete(self, request):
return text('I am delete method')
app.add_route(SimpleView(), '/')
app.add_route(SimpleView.as_view(), '/')
```
@@ -39,6 +40,19 @@ class NameView(HTTPMethodView):
def get(self, request, name):
return text('Hello {}'.format(name))
app.add_route(NameView(), '/<name')
app.add_route(NameView.as_view(), '/<name>')
```
If you want to add decorator for class, you could set decorators variable
```
class ViewWithDecorator(HTTPMethodView):
decorators = [some_decorator_here]
def get(self, request, name):
return text('Hello I have a decorator')
app.add_route(ViewWithDecorator.as_view(), '/url')
```

70
docs/custom_protocol.md Normal file
View File

@@ -0,0 +1,70 @@
# Custom Protocol
You can change the behavior of protocol by using custom protocol.
If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic.
* loop
`loop` is an asyncio compatible event loop.
* connections
`connections` is a `set object` to store protocol objects.
When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections.
* signal
`signal` is a `sanic.server.Signal object` with `stopped attribute`.
When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`.
* request_handler
`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments.
* error_handler
`error_handler` is a `sanic.exceptions.Handler` object.
* request_timeout
`request_timeout` is seconds for timeout.
* request_max_size
`request_max_size` is bytes of max request size.
## Example
By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`.
In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`.
```python
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
app = Sanic(__name__)
class CustomHttpProtocol(HttpProtocol):
def __init__(self, *, loop, request_handler, error_handler,
signal, connections, request_timeout, request_max_size):
super().__init__(
loop=loop, request_handler=request_handler,
error_handler=error_handler, signal=signal,
connections=connections, request_timeout=request_timeout,
request_max_size=request_max_size)
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/')
async def string(request):
return 'string'
@app.route('/1')
async def response(request):
return text('response')
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
```

View File

@@ -27,3 +27,23 @@ async def handler(request):
app.run(host="0.0.0.0", port=8000)
```
## Middleware chain
If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware:
```python
app = Sanic(__name__)
@app.middleware('response')
async def custom_banner(request, response):
response.headers["Server"] = "Fake-Server"
@app.middleware('response')
async def prevent_xss(request, response):
response.headers["x-xss-protection"] = "1; mode=block"
app.run(host="0.0.0.0", port=8000)
```
The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks.

View File

@@ -33,12 +33,12 @@ async def handler1(request):
return text('OK')
app.add_route(handler1, '/test')
async def handler(request, name):
async def handler2(request, name):
return text('Folder - {}'.format(name))
app.add_route(handler, '/folder/<name>')
app.add_route(handler2, '/folder/<name>')
async def person_handler(request, name):
async def person_handler2(request, name):
return text('Person - {}'.format(name))
app.add_route(handler, '/person/<name:[A-z]>')
app.add_route(person_handler2, '/person/<name:[A-z]>')
```

51
docs/testing.md Normal file
View File

@@ -0,0 +1,51 @@
# Testing
Sanic endpoints can be tested locally using the `sanic.utils` module, which
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
library. The `sanic_endpoint_test` function runs a local server, issues a
configurable request to an endpoint, and returns the result. It takes the
following arguments:
- `app` An instance of a Sanic app.
- `method` *(default `'get'`)* A string representing the HTTP method to use.
- `uri` *(default `'/'`)* A string representing the endpoint to test.
- `gather_request` *(default `True`)* A boolean which determines whether the
original request will be returned by the function. If set to `True`, the
return value is a tuple of `(request, response)`, if `False` only the
response is returned.
- `loop` *(default `None`)* The event loop to use.
- `debug` *(default `False`)* A boolean which determines whether to run the
server in debug mode.
The function further takes the `*request_args` and `**request_kwargs`, which
are passed directly to the aiohttp ClientSession request. For example, to
supply data with a GET request, `method` would be `get` and the keyword
argument `params={'value', 'key'}` would be supplied. More information about
the available arguments to aiohttp can be found
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
Below is a complete example of an endpoint test,
using [pytest](http://doc.pytest.org/en/latest/). The test checks that the
`/challenge` endpoint responds to a GET request with a supplied challenge
string.
```python
import pytest
import aiohttp
from sanic.utils import sanic_endpoint_test
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
def test_endpoint_challenge():
# Create the challenge data
request_data = {'challenge': 'dummy_challenge'}
# Send the request to the endpoint, using the default `get` method
request, response = sanic_endpoint_test(app,
uri='/challenge',
params=request_data)
# Assert that the server responds with the challenge string
assert response.text == request_data['challenge']
```

18
examples/jinja_example.py Normal file
View File

@@ -0,0 +1,18 @@
## To use this example:
# curl -d '{"name": "John Doe"}' localhost:8000
from sanic import Sanic
from sanic.response import html
from jinja2 import Template
template = Template('Hello {{ name }}!')
app = Sanic(__name__)
@app.route('/')
async def test(request):
data = request.json
return html(template.render(**data))
app.run(host="0.0.0.0", port=8000)

View File

@@ -0,0 +1,23 @@
from sanic import Sanic
from sanic.response import text
import json
import logging
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
logging_format += "%(message)s"
logging.basicConfig(
format=logging_format,
level=logging.DEBUG
)
log = logging.getLogger()
# Set logger to override default basicConfig
sanic = Sanic(logger=True)
@sanic.route("/")
def test(request):
log.info("received request; responding with 'hey'")
return text("hey")
sanic.run(host="0.0.0.0", port=8000)

View File

@@ -0,0 +1,65 @@
""" To run this example you need additional aiopg package
"""
import os
import asyncio
import uvloop
import aiopg
from sanic import Sanic
from sanic.response import json
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
database_name = os.environ['DATABASE_NAME']
database_host = os.environ['DATABASE_HOST']
database_user = os.environ['DATABASE_USER']
database_password = os.environ['DATABASE_PASSWORD']
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
database_password,
database_host,
database_name)
loop = asyncio.get_event_loop()
async def get_pool():
return await aiopg.create_pool(connection)
app = Sanic(name=__name__)
pool = loop.run_until_complete(get_pool())
async def prepare_db():
""" Let's create some table and add some data
"""
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute('DROP TABLE IF EXISTS sanic_polls')
await cur.execute("""CREATE TABLE sanic_polls (
id serial primary key,
question varchar(50),
pub_date timestamp
);""")
for i in range(0, 100):
await cur.execute("""INSERT INTO sanic_polls
(id, question, pub_date) VALUES ({}, {}, now())
""".format(i, i))
@app.route("/")
async def handle(request):
async with pool.acquire() as conn:
async with conn.cursor() as cur:
result = []
await cur.execute("SELECT question, pub_date FROM sanic_polls")
async for row in cur:
result.append({"question": row[0], "pub_date": row[1]})
return json({"polls": result})
if __name__ == '__main__':
loop.run_until_complete(prepare_db())
app.run(host='0.0.0.0', port=8000, loop=loop)

View File

@@ -0,0 +1,73 @@
""" To run this example you need additional aiopg package
"""
import os
import asyncio
import datetime
import uvloop
from aiopg.sa import create_engine
import sqlalchemy as sa
from sanic import Sanic
from sanic.response import json
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
database_name = os.environ['DATABASE_NAME']
database_host = os.environ['DATABASE_HOST']
database_user = os.environ['DATABASE_USER']
database_password = os.environ['DATABASE_PASSWORD']
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
database_password,
database_host,
database_name)
loop = asyncio.get_event_loop()
metadata = sa.MetaData()
polls = sa.Table('sanic_polls', metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('question', sa.String(50)),
sa.Column("pub_date", sa.DateTime))
async def get_engine():
return await create_engine(connection)
app = Sanic(name=__name__)
engine = loop.run_until_complete(get_engine())
async def prepare_db():
""" Let's add some data
"""
async with engine.acquire() as conn:
await conn.execute('DROP TABLE IF EXISTS sanic_polls')
await conn.execute("""CREATE TABLE sanic_polls (
id serial primary key,
question varchar(50),
pub_date timestamp
);""")
for i in range(0, 100):
await conn.execute(
polls.insert().values(question=i,
pub_date=datetime.datetime.now())
)
@app.route("/")
async def handle(request):
async with engine.acquire() as conn:
result = []
async for row in conn.execute(polls.select()):
result.append({"question": row.question, "pub_date": row.pub_date})
return json({"polls": result})
if __name__ == '__main__':
loop.run_until_complete(prepare_db())
app.run(host='0.0.0.0', port=8000, loop=loop)

View File

@@ -0,0 +1,65 @@
""" To run this example you need additional asyncpg package
"""
import os
import asyncio
import uvloop
from asyncpg import create_pool
from sanic import Sanic
from sanic.response import json
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
DB_CONFIG = {
'host': '<host>',
'user': '<username>',
'password': '<password>',
'port': '<port>',
'database': '<database>'
}
def jsonify(records):
""" Parse asyncpg record response into JSON format
"""
return [{key: value for key, value in
zip(r.keys(), r.values())} for r in records]
loop = asyncio.get_event_loop()
async def make_pool():
return await create_pool(**DB_CONFIG)
app = Sanic(__name__)
pool = loop.run_until_complete(make_pool())
async def create_db():
""" Create some table and add some data
"""
async with pool.acquire() as connection:
async with connection.transaction():
await connection.execute('DROP TABLE IF EXISTS sanic_post')
await connection.execute("""CREATE TABLE sanic_post (
id serial primary key,
content varchar(50),
post_date timestamp
);""")
for i in range(0, 100):
await connection.execute(f"""INSERT INTO sanic_post
(id, content, post_date) VALUES ({i}, {i}, now())""")
@app.route("/")
async def handler(request):
async with pool.acquire() as connection:
async with connection.transaction():
results = await connection.fetch('SELECT * FROM sanic_post')
return json({'posts': jsonify(results)})
if __name__ == '__main__':
loop.run_until_complete(create_db())
app.run(host='0.0.0.0', port=8000, loop=loop)

View File

@@ -64,11 +64,11 @@ def query_string(request):
# Run Server
# ----------------------------------------------- #
def after_start(loop):
def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH")
def before_stop(loop):
def before_stop(app, loop):
log.info("TRIED EVERYTHING")

32
examples/vhosts.py Normal file
View File

@@ -0,0 +1,32 @@
from sanic.response import text
from sanic import Sanic
from sanic.blueprints import Blueprint
# Usage
# curl -H "Host: example.com" localhost:8000
# curl -H "Host: sub.example.com" localhost:8000
# curl -H "Host: bp.example.com" localhost:8000/question
# curl -H "Host: bp.example.com" localhost:8000/answer
app = Sanic()
bp = Blueprint("bp", host="bp.example.com")
@app.route('/', host="example.com")
async def hello(request):
return text("Answer")
@app.route('/', host="sub.example.com")
async def hello(request):
return text("42")
@bp.route("/question")
async def hello(request):
return text("What is the meaning of life?")
@bp.route("/answer")
async def hello(request):
return text("42")
app.register_blueprint(bp)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,6 +1,6 @@
from .sanic import Sanic
from .blueprints import Blueprint
__version__ = '0.1.8'
__version__ = '0.2.0'
__all__ = ['Sanic', 'Blueprint']

View File

@@ -20,7 +20,7 @@ if __name__ == "__main__":
module = import_module(module_name)
app = getattr(module, app_name, None)
if type(app) is not Sanic:
if not isinstance(app, Sanic):
raise ValueError("Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?"
.format(type(app).__name__, args.module))

View File

@@ -18,14 +18,17 @@ class BlueprintSetup:
#: blueprint.
self.url_prefix = url_prefix
def add_route(self, handler, uri, methods):
def add_route(self, handler, uri, methods, host=None):
"""
A helper method to register a handler to the application url routes.
"""
if self.url_prefix:
uri = self.url_prefix + uri
self.app.route(uri=uri, methods=methods)(handler)
if host is None:
host = self.blueprint.host
self.app.route(uri=uri, methods=methods, host=host)(handler)
def add_exception(self, handler, *args, **kwargs):
"""
@@ -53,7 +56,7 @@ class BlueprintSetup:
class Blueprint:
def __init__(self, name, url_prefix=None):
def __init__(self, name, url_prefix=None, host=None):
"""
Creates a new blueprint
:param name: Unique name of the blueprint
@@ -63,6 +66,7 @@ class Blueprint:
self.url_prefix = url_prefix
self.deferred_functions = []
self.listeners = defaultdict(list)
self.host = host
def record(self, func):
"""
@@ -83,18 +87,18 @@ class Blueprint:
for deferred in self.deferred_functions:
deferred(state)
def route(self, uri, methods=None):
def route(self, uri, methods=None, host=None):
"""
"""
def decorator(handler):
self.record(lambda s: s.add_route(handler, uri, methods))
self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler
return decorator
def add_route(self, handler, uri, methods=None):
def add_route(self, handler, uri, methods=None, host=None):
"""
"""
self.record(lambda s: s.add_route(handler, uri, methods))
self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler
def listener(self, event):

View File

@@ -1,4 +1,5 @@
from .response import text
from .log import log
from traceback import format_exc
@@ -34,6 +35,10 @@ class RequestTimeout(SanicException):
status_code = 408
class PayloadTooLarge(SanicException):
status_code = 413
class Handler:
handlers = None
@@ -52,18 +57,31 @@ class Handler:
:return: Response object
"""
handler = self.handlers.get(type(exception), self.default)
response = handler(request=request, exception=exception)
try:
response = handler(request=request, exception=exception)
except:
if self.sanic.debug:
response_message = (
'Exception raised in exception handler "{}" '
'for uri: "{}"\n{}').format(
handler.__name__, request.url, format_exc())
log.error(response_message)
return text(response_message, 500)
else:
return text('An error occurred while handling an error', 500)
return response
def default(self, request, exception):
if issubclass(type(exception), SanicException):
return text(
"Error: {}".format(exception),
'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500))
elif self.sanic.debug:
return text(
"Error: {}\nException: {}".format(
exception, format_exc()), status=500)
response_message = (
'Exception occurred while handling uri: "{}"\n{}'.format(
request.url, format_exc()))
log.error(response_message)
return text(response_message, status=500)
else:
return text(
"An error occurred while generating the request", status=500)
'An error occurred while generating the response', status=500)

View File

@@ -1,5 +1,3 @@
import logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
log = logging.getLogger(__name__)

View File

@@ -4,6 +4,7 @@ from http.cookies import SimpleCookie
from httptools import parse_url
from urllib.parse import parse_qs
from ujson import loads as json_loads
from sanic.exceptions import InvalidUsage
from .log import log
@@ -24,6 +25,9 @@ class RequestParameters(dict):
self.super = super()
self.super.__init__(*args, **kwargs)
def __getitem__(self, name):
return self.get(name)
def get(self, name, default=None):
values = self.super.get(name)
return values[0] if values else default
@@ -63,14 +67,25 @@ class Request(dict):
@property
def json(self):
if not self.parsed_json:
if self.parsed_json is None:
try:
self.parsed_json = json_loads(self.body)
except Exception:
log.exception("failed when parsing body as json")
raise InvalidUsage("Failed when parsing body as json")
return self.parsed_json
@property
def token(self):
"""
Attempts to return the auth header token.
:return: token related to request
"""
auth_header = self.headers.get('Authorization')
if auth_header is not None:
return auth_header.split()[1]
return auth_header
@property
def form(self):
if self.parsed_form is None:
@@ -89,7 +104,7 @@ class Request(dict):
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
except Exception:
log.exception("failed when parsing form")
log.exception("Failed when parsing form")
return self.parsed_form
@@ -114,9 +129,10 @@ class Request(dict):
@property
def cookies(self):
if self._cookies is None:
if 'Cookie' in self.headers:
cookie = self.headers.get('Cookie') or self.headers.get('cookie')
if cookie is not None:
cookies = SimpleCookie()
cookies.load(self.headers['Cookie'])
cookies.load(cookie)
self._cookies = {name: cookie.value
for name, cookie in cookies.items()}
else:

View File

@@ -1,9 +1,11 @@
from aiofiles import open as open_async
from .cookies import CookieJar
from mimetypes import guess_type
from os import path
from ujson import dumps as json_dumps
from .cookies import CookieJar
COMMON_STATUS_CODES = {
200: b'OK',
400: b'Bad Request',
@@ -79,7 +81,12 @@ class HTTPResponse:
self.content_type = content_type
if body is not None:
self.body = body.encode('utf-8')
try:
# Try to encode it regularly
self.body = body.encode('utf-8')
except AttributeError:
# Convert it to a str if you can't
self.body = str(body).encode('utf-8')
else:
self.body = body_bytes
@@ -96,10 +103,14 @@ class HTTPResponse:
headers = b''
if self.headers:
headers = b''.join(
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
for name, value in self.headers.items()
)
for name, value in self.headers.items():
try:
headers += (
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
except AttributeError:
headers += (
b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8')))
# Try to pull from the common codes first
# Speeds up response rate 6% over pulling from all

View File

@@ -23,18 +23,28 @@ class RouteExists(Exception):
pass
class RouteDoesNotExist(Exception):
pass
class Router:
"""
Router supports basic routing with parameters and method checks
Usage:
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
def my_route(request, my_parameter):
@app.route('/my_url/<my_param>', methods=['GET', 'POST', ...])
def my_route(request, my_param):
do stuff...
or
@app.route('/my_url/<my_param:my_type>', methods=['GET', 'POST', ...])
def my_route_with_type(request, my_param: my_type):
do stuff...
Parameters will be passed as keyword arguments to the request handling
function provided Parameters can also have a type by appending :type to
the <parameter>. If no type is provided, a string is expected. A regular
expression can also be passed in as the type
function. Provided parameters can also have a type by appending :type to
the <parameter>. Given parameter must be able to be type-casted to this.
If no type is provided, a string is expected. A regular expression can
also be passed in as the type. The argument given to the function will
always be a string, independent of the type.
"""
routes_static = None
routes_dynamic = None
@@ -45,8 +55,9 @@ class Router:
self.routes_static = {}
self.routes_dynamic = defaultdict(list)
self.routes_always_check = []
self.hosts = None
def add(self, uri, methods, handler):
def add(self, uri, methods, handler, host=None):
"""
Adds a handler to the route list
:param uri: Path to match
@@ -56,6 +67,17 @@ class Router:
When executed, it should provide a response object.
:return: Nothing
"""
if host is not None:
# we want to track if there are any
# vhosts on the Router instance so that we can
# default to the behavior without vhosts
if self.hosts is None:
self.hosts = set(host)
else:
self.hosts.add(host)
uri = host + uri
if uri in self.routes_all:
raise RouteExists("Route already registered: {}".format(uri))
@@ -103,6 +125,25 @@ class Router:
else:
self.routes_static[uri] = route
def remove(self, uri, clean_cache=True, host=None):
if host is not None:
uri = host + uri
try:
route = self.routes_all.pop(uri)
except KeyError:
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
if route in self.routes_always_check:
self.routes_always_check.remove(route)
elif url_hash(uri) in self.routes_dynamic \
and route in self.routes_dynamic[url_hash(uri)]:
self.routes_dynamic[url_hash(uri)].remove(route)
else:
self.routes_static.pop(uri)
if clean_cache:
self._get.cache_clear()
def get(self, request):
"""
Gets a request handler based on the URL of the request, or raises an
@@ -110,10 +151,14 @@ class Router:
:param request: Request object
:return: handler, arguments, keyword arguments
"""
return self._get(request.url, request.method)
if self.hosts is None:
return self._get(request.url, request.method, '')
else:
return self._get(request.url, request.method,
request.headers.get("Host", ''))
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
def _get(self, url, method):
def _get(self, url, method, host):
"""
Gets a request handler based on the URL of the request, or raises an
error. Internal method for caching.
@@ -121,6 +166,7 @@ class Router:
:param method: Request method
:return: handler, arguments, keyword arguments
"""
url = host + url
# Check against known static routes
route = self.routes_static.get(url)
if route:

View File

@@ -4,21 +4,29 @@ from functools import partial
from inspect import isawaitable, stack, getmodulename
from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT
from time import sleep
from traceback import format_exc
import logging
from .config import Config
from .exceptions import Handler
from .log import log, logging
from .log import log
from .response import HTTPResponse
from .router import Router
from .server import serve
from .server import serve, HttpProtocol
from .static import register as static_register
from .exceptions import ServerError
from socket import socket, SOL_SOCKET, SO_REUSEADDR
from os import set_inheritable
class Sanic:
def __init__(self, name=None, router=None, error_handler=None):
def __init__(self, name=None, router=None,
error_handler=None, logger=None):
if logger is None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s: %(levelname)s: %(message)s"
)
if name is None:
frame_records = stack()[1]
name = getmodulename(frame_records[1])
@@ -32,6 +40,8 @@ class Sanic:
self._blueprint_order = []
self.loop = None
self.debug = None
self.sock = None
self.processes = None
# Register alternative method names
self.go_fast = self.run
@@ -41,7 +51,7 @@ class Sanic:
# -------------------------------------------------------------------- #
# Decorator
def route(self, uri, methods=None):
def route(self, uri, methods=None, host=None):
"""
Decorates a function to be registered as a route
:param uri: path of the URL
@@ -55,12 +65,13 @@ class Sanic:
uri = '/' + uri
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler)
self.router.add(uri=uri, methods=methods, handler=handler,
host=host)
return handler
return response
def add_route(self, handler, uri, methods=None):
def add_route(self, handler, uri, methods=None, host=None):
"""
A helper method to register class instance or
functions as a handler to the application url
@@ -70,9 +81,12 @@ class Sanic:
:param methods: list or tuple of methods allowed
:return: function or class instance
"""
self.route(uri=uri, methods=methods)(handler)
self.route(uri=uri, methods=methods, host=host)(handler)
return handler
def remove_route(self, uri, clean_cache=True, host=None):
self.router.remove(uri, clean_cache, host)
# Decorator
def exception(self, *exceptions):
"""
@@ -193,18 +207,18 @@ class Sanic:
if isawaitable(response):
response = await response
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
if self.response_middleware:
for middleware in self.response_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
if _response:
response = _response
break
if self.response_middleware:
for middleware in self.response_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
if _response:
response = _response
break
except Exception as e:
# -------------------------------------------- #
@@ -232,25 +246,27 @@ class Sanic:
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
after_start=None, before_stop=None, after_stop=None, sock=None,
workers=1, loop=None):
workers=1, loop=None, protocol=HttpProtocol, backlog=100,
stop_event=None):
"""
Runs the HTTP Server and listens until keyboard interrupt or term
signal. On termination, drains connections before closing.
:param host: Address to host on
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param before_start: Function to be executed before the server starts
:param before_start: Functions to be executed before the server starts
accepting connections
:param after_start: Function to be executed after the server starts
:param after_start: Functions to be executed after the server starts
accepting connections
:param before_stop: Function to be executed when a stop signal is
:param before_stop: Functions to be executed when a stop signal is
received before it is respected
:param after_stop: Function to be executed when all requests are
:param after_stop: Functions to be executed when all requests are
complete
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
self.error_handler.debug = True
@@ -258,6 +274,7 @@ class Sanic:
self.loop = loop
server_settings = {
'protocol': protocol,
'host': host,
'port': port,
'sock': sock,
@@ -266,7 +283,8 @@ class Sanic:
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop
'loop': loop,
'backlog': backlog
}
# -------------------------------------------- #
@@ -283,7 +301,7 @@ class Sanic:
for blueprint in self.blueprints.values():
listeners += blueprint.listeners[event_name]
if args:
if type(args) is not list:
if callable(args):
args = [args]
listeners += args
if reverse:
@@ -305,7 +323,7 @@ class Sanic:
else:
log.info('Spinning up {} workers...'.format(workers))
self.serve_multiple(server_settings, workers)
self.serve_multiple(server_settings, workers, stop_event)
except Exception as e:
log.exception(
@@ -317,10 +335,13 @@ class Sanic:
"""
This kills the Sanic
"""
if self.processes is not None:
for process in self.processes:
process.terminate()
self.sock.close()
get_event_loop().stop()
@staticmethod
def serve_multiple(server_settings, workers, stop_event=None):
def serve_multiple(self, server_settings, workers, stop_event=None):
"""
Starts multiple server processes simultaneously. Stops on interrupt
and terminate signals, and drains connections when complete.
@@ -332,26 +353,28 @@ class Sanic:
server_settings['reuse_port'] = True
# Create a stop event to be triggered by a signal
if not stop_event:
if stop_event is None:
stop_event = Event()
signal(SIGINT, lambda s, f: stop_event.set())
signal(SIGTERM, lambda s, f: stop_event.set())
processes = []
self.sock = socket()
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.sock.bind((server_settings['host'], server_settings['port']))
set_inheritable(self.sock.fileno(), True)
server_settings['sock'] = self.sock
server_settings['host'] = None
server_settings['port'] = None
self.processes = []
for _ in range(workers):
process = Process(target=serve, kwargs=server_settings)
process.daemon = True
process.start()
processes.append(process)
self.processes.append(process)
# Infinitely wait for the stop event
try:
while not stop_event.is_set():
sleep(0.3)
except:
pass
log.info('Spinning down workers...')
for process in processes:
process.terminate()
for process in processes:
for process in self.processes:
process.join()
# the above processes will block this until they're stopped
self.stop()

View File

@@ -6,6 +6,7 @@ from signal import SIGINT, SIGTERM
from time import time
from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
from .exceptions import ServerError
try:
import uvloop as async_loop
@@ -14,7 +15,7 @@ except ImportError:
from .log import log
from .request import Request
from .exceptions import RequestTimeout
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
class Signal:
@@ -60,14 +61,14 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- #
def connection_made(self, transport):
self.connections[self] = True
self.connections.add(self)
self._timeout_handler = self.loop.call_later(
self.request_timeout, self.connection_timeout)
self.transport = transport
self._last_request_time = current_time
def connection_lost(self, exc):
del self.connections[self]
self.connections.discard(self)
self._timeout_handler.cancel()
self.cleanup()
@@ -81,9 +82,8 @@ class HttpProtocol(asyncio.Protocol):
else:
if self._request_handler_task:
self._request_handler_task.cancel()
response = self.error_handler.response(
self.request, RequestTimeout('Request Timeout'))
self.write_response(response)
exception = RequestTimeout('Request Timeout')
self.write_error(exception)
# -------------------------------------------- #
# Parsing
@@ -94,9 +94,8 @@ class HttpProtocol(asyncio.Protocol):
# memory limits
self._total_request_size += len(data)
if self._total_request_size > self.request_max_size:
return self.bail_out(
"Request too large ({}), connection closed".format(
self._total_request_size))
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
# Create parser if this is the first time we're receiving data
if self.parser is None:
@@ -107,17 +106,17 @@ class HttpProtocol(asyncio.Protocol):
# Parse request chunk or close connection
try:
self.parser.feed_data(data)
except HttpParserError as e:
self.bail_out(
"Invalid request data, connection closed ({})".format(e))
except HttpParserError:
exception = InvalidUsage('Bad Request')
self.write_error(exception)
def on_url(self, url):
self.url = url
def on_header(self, name, value):
if name == b'Content-Length' and int(value) > self.request_max_size:
return self.bail_out(
"Request body too large ({}), connection closed".format(value))
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
self.headers.append((name.decode(), value.decode('utf-8')))
@@ -164,9 +163,20 @@ class HttpProtocol(asyncio.Protocol):
self.bail_out(
"Writing response failed, connection closed {}".format(e))
def write_error(self, exception):
try:
response = self.error_handler.response(self.request, exception)
version = self.request.version if self.request else '1.1'
self.transport.write(response.output(version))
self.transport.close()
except Exception as e:
self.bail_out(
"Writing error failed, connection closed {}".format(e))
def bail_out(self, message):
log.debug(message)
self.transport.close()
exception = ServerError(message)
self.write_error(exception)
log.error(message)
def cleanup(self):
self.parser = None
@@ -214,24 +224,30 @@ def trigger_events(events, loop):
def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None,
debug=False, request_timeout=60, sock=None,
request_max_size=None, reuse_port=False, loop=None):
after_start=None, before_stop=None, after_stop=None, debug=False,
request_timeout=60, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100):
"""
Starts asynchronous HTTP Server on an individual process.
:param host: Address to host on
:param port: Port to host on
:param request_handler: Sanic request handler with middleware
:param error_handler: Sanic error handler with middleware
:param before_start: Function to be executed before the server starts
listening. Takes single argument `loop`
:param after_start: Function to be executed after the server starts
listening. Takes single argument `loop`
:param before_stop: Function to be executed when a stop signal is
received before it is respected. Takes single argumenet `loop`
:param after_stop: Function to be executed when a stop signal is
received after it is respected. Takes single argumenet `loop`
:param debug: Enables debug output (slows server)
:param request_timeout: time in seconds
:param sock: Socket for the server to accept connections from
:param request_max_size: size in bytes, `None` for no limit
:param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
loop = loop or async_loop.new_event_loop()
@@ -242,9 +258,10 @@ def serve(host, port, request_handler, error_handler, before_start=None,
trigger_events(before_start, loop)
connections = {}
connections = set()
signal = Signal()
server_coroutine = loop.create_server(lambda: HttpProtocol(
server = partial(
protocol,
loop=loop,
connections=connections,
signal=signal,
@@ -252,7 +269,16 @@ def serve(host, port, request_handler, error_handler, before_start=None,
error_handler=error_handler,
request_timeout=request_timeout,
request_max_size=request_max_size,
), host, port, reuse_port=reuse_port, sock=sock)
)
server_coroutine = loop.create_server(
server,
host,
port,
reuse_port=reuse_port,
sock=sock,
backlog=backlog
)
# Instead of pulling time at the end of every request,
# pull it once per minute
@@ -284,7 +310,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
# Complete all tasks on the loop
signal.stopped = True
for connection in connections.keys():
for connection in connections:
connection.close_if_idle()
while connections:

View File

@@ -2,6 +2,7 @@ from aiofiles.os import stat
from os import path
from re import sub
from time import strftime, gmtime
from urllib.parse import unquote
from .exceptions import FileNotFound, InvalidUsage
from .response import file, HTTPResponse
@@ -32,12 +33,17 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
# served. os.path.realpath seems to be very slow
if file_uri and '../' in file_uri:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path
file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \
if file_uri else file_or_directory
file_path = file_or_directory
if file_uri:
file_path = path.join(
file_or_directory, sub('^[/]*', '', file_uri))
# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = unquote(file_path)
try:
headers = {}
# Check if the client has been sent this file before

View File

@@ -16,15 +16,15 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
loop=None, debug=False, *request_args,
**request_kwargs):
loop=None, debug=False, server_kwargs={},
*request_args, **request_kwargs):
results = []
exceptions = []
if gather_request:
@app.middleware
def _collect_request(request):
results.append(request)
app.request_middleware.appendleft(_collect_request)
async def _collect_response(sanic, loop):
try:
@@ -35,8 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
exceptions.append(e)
app.stop()
app.run(host=HOST, debug=debug, port=42101,
after_start=_collect_response, loop=loop)
app.run(host=HOST, debug=debug, port=PORT,
after_start=_collect_response, loop=loop, **server_kwargs)
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))
@@ -47,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
return request, response
except:
raise ValueError(
"request and response object expected, got ({})".format(
"Request and response object expected, got ({})".format(
results))
else:
try:
return results[0]
except:
raise ValueError(
"request object expected, got ({})".format(results))
"Request object expected, got ({})".format(results))

View File

@@ -3,10 +3,11 @@ from .exceptions import InvalidUsage
class HTTPMethodView:
""" Simple class based implementation of view for the sanic.
You should implement methods(get, post, put, patch, delete) for the class
You should implement methods (get, post, put, patch, delete) for the class
to every HTTP method you want to support.
For example:
class DummyView(View):
class DummyView(HTTPMethodView):
def get(self, request, *args, **kwargs):
return text('I am get method')
@@ -14,23 +15,49 @@ class HTTPMethodView:
def put(self, request, *args, **kwargs):
return text('I am put method')
etc.
If someone try use not implemented method, there will be 405 response
If you need any url params just mention them in method definition like:
class DummyView(View):
If someone tries to use a non-implemented method, there will be a
405 response.
If you need any url params just mention them in method definition:
class DummyView(HTTPMethodView):
def get(self, request, my_param_here, *args, **kwargs):
return text('I am get method with %s' % my_param_here)
To add the view into the routing you could use
1) app.add_route(DummyView(), '/')
2) app.route('/')(DummyView())
1) app.add_route(DummyView.as_view(), '/')
2) app.route('/')(DummyView.as_view())
To add any decorator you could set it into decorators variable
"""
def __call__(self, request, *args, **kwargs):
decorators = []
def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
if handler:
return handler(request, *args, **kwargs)
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
@classmethod
def as_view(cls, *class_args, **class_kwargs):
""" Converts the class into an actual view function that can be used
with the routing system.
"""
def view(*args, **kwargs):
self = view.view_class(*class_args, **class_kwargs)
return self.dispatch_request(*args, **kwargs)
if cls.decorators:
view.__module__ = cls.__module__
for decorator in cls.decorators:
view = decorator(view)
view.view_class = cls
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
return view

View File

@@ -1,16 +1,30 @@
package main
import (
"fmt"
"os"
"net/http"
"encoding/json"
"net/http"
"os"
)
type TestJSONResponse struct {
Test bool
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
response := TestJSONResponse{true}
js, err := json.Marshal(response)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":" + os.Args[1], nil)
http.HandleFunc("/", handler)
http.ListenAndServe(":"+os.Args[1], nil)
}

View File

@@ -0,0 +1 @@
I need to be decoded as a uri

1
tests/static/test.file Normal file
View File

@@ -0,0 +1 @@
I am just a regular static file

20
tests/test_bad_request.py Normal file
View File

@@ -0,0 +1,20 @@
import asyncio
from sanic import Sanic
def test_bad_request_response():
app = Sanic('test_bad_request_response')
lines = []
async def _request(sanic, loop):
connect = asyncio.open_connection('127.0.0.1', 42101)
reader, writer = await connect
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, after_start=_request)
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
assert lines[-1] == b'Error: Bad Request'

View File

@@ -59,6 +59,71 @@ def test_several_bp_with_url_prefix():
request, response = sanic_endpoint_test(app, uri='/test2/')
assert response.text == 'Hello2'
def test_bp_with_host():
app = Sanic('test_bp_host')
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
@bp.route('/')
def handler(request):
return text('Hello')
@bp.route('/', host="sub.example.com")
def handler(request):
return text('Hello subdomain!')
app.blueprint(bp)
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, uri='/test1/',
headers=headers)
assert response.text == 'Hello'
headers = {"Host": "sub.example.com"}
request, response = sanic_endpoint_test(app, uri='/test1/',
headers=headers)
assert response.text == 'Hello subdomain!'
def test_several_bp_with_host():
app = Sanic('test_text')
bp = Blueprint('test_text',
url_prefix='/test',
host="example.com")
bp2 = Blueprint('test_text2',
url_prefix='/test',
host="sub.example.com")
@bp.route('/')
def handler(request):
return text('Hello')
@bp2.route('/')
def handler2(request):
return text('Hello2')
@bp2.route('/other/')
def handler2(request):
return text('Hello3')
app.blueprint(bp)
app.blueprint(bp2)
assert bp.host == "example.com"
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, uri='/test/',
headers=headers)
assert response.text == 'Hello'
assert bp2.host == "sub.example.com"
headers = {"Host": "sub.example.com"}
request, response = sanic_endpoint_test(app, uri='/test/',
headers=headers)
assert response.text == 'Hello2'
request, response = sanic_endpoint_test(app, uri='/test/other/',
headers=headers)
assert response.text == 'Hello3'
def test_bp_middleware():
app = Sanic('test_middleware')
@@ -162,4 +227,4 @@ def test_bp_static():
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == current_file_contents
assert response.body == current_file_contents

View File

@@ -25,6 +25,19 @@ def test_cookies():
assert response.text == 'Cookies are: working!'
assert response_cookies['right_back'].value == 'at you'
def test_http2_cookies():
app = Sanic('test_http2_cookies')
@app.route('/')
async def handler(request):
response = text('Cookies are: {}'.format(request.cookies['test']))
return response
headers = {'cookie': 'test=working!'}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == 'Cookies are: working!'
def test_cookie_options():
app = Sanic('test_text')

View File

@@ -0,0 +1,32 @@
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
from sanic.utils import sanic_endpoint_test
app = Sanic('test_custom_porotocol')
class CustomHttpProtocol(HttpProtocol):
def write_response(self, response):
if isinstance(response, str):
response = text(response)
self.transport.write(
response.output(self.request.version)
)
self.transport.close()
@app.route('/1')
async def handler_1(request):
return 'OK'
def test_use_custom_protocol():
server_kwargs = {
'protocol': CustomHttpProtocol
}
request, response = sanic_endpoint_test(app, uri='/1',
server_kwargs=server_kwargs)
assert response.status == 200
assert response.text == 'OK'

View File

@@ -1,51 +1,86 @@
import pytest
from sanic import Sanic
from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.utils import sanic_endpoint_test
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
exception_app = Sanic('test_exceptions')
class SanicExceptionTestException(Exception):
pass
@exception_app.route('/')
def handler(request):
return text('OK')
@pytest.fixture(scope='module')
def exception_app():
app = Sanic('test_exceptions')
@app.route('/')
def handler(request):
return text('OK')
@app.route('/error')
def handler_error(request):
raise ServerError("OK")
@app.route('/404')
def handler_404(request):
raise NotFound("OK")
@app.route('/invalid')
def handler_invalid(request):
raise InvalidUsage("OK")
@app.route('/divide_by_zero')
def handle_unhandled_exception(request):
1 / 0
@app.route('/error_in_error_handler_handler')
def custom_error_handler(request):
raise SanicExceptionTestException('Dummy message!')
@app.exception(SanicExceptionTestException)
def error_in_error_handler_handler(request, exception):
1 / 0
return app
@exception_app.route('/error')
def handler_error(request):
raise ServerError("OK")
@exception_app.route('/404')
def handler_404(request):
raise NotFound("OK")
@exception_app.route('/invalid')
def handler_invalid(request):
raise InvalidUsage("OK")
def test_no_exception():
def test_no_exception(exception_app):
"""Test that a route works without an exception"""
request, response = sanic_endpoint_test(exception_app)
assert response.status == 200
assert response.text == 'OK'
def test_server_error_exception():
def test_server_error_exception(exception_app):
"""Test the built-in ServerError exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/error')
assert response.status == 500
def test_invalid_usage_exception():
def test_invalid_usage_exception(exception_app):
"""Test the built-in InvalidUsage exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/invalid')
assert response.status == 400
def test_not_found_exception():
def test_not_found_exception(exception_app):
"""Test the built-in NotFound exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/404')
assert response.status == 404
def test_handled_unhandled_exception(exception_app):
"""Test that an exception not built into sanic is handled"""
request, response = sanic_endpoint_test(
exception_app, uri='/divide_by_zero')
assert response.status == 500
assert response.body == b'An error occurred while generating the response'
def test_exception_in_exception_handler(exception_app):
"""Test that an exception thrown in an error handler is handled"""
request, response = sanic_endpoint_test(
exception_app, uri='/error_in_error_handler_handler')
assert response.status == 500
assert response.body == b'An error occurred while handling an error'

33
tests/test_logging.py Normal file
View File

@@ -0,0 +1,33 @@
import asyncio
from sanic.response import text
from sanic import Sanic
from io import StringIO
from sanic.utils import sanic_endpoint_test
import logging
logging_format = '''module: %(module)s; \
function: %(funcName)s(); \
message: %(message)s'''
def test_log():
log_stream = StringIO()
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
logging.basicConfig(
format=logging_format,
level=logging.DEBUG,
stream=log_stream
)
log = logging.getLogger()
app = Sanic('test_logging', logger=True)
@app.route('/')
def handler(request):
log.info('hello world')
return text('hello')
request, response = sanic_endpoint_test(app)
log_text = log_stream.getvalue().strip().split('\n')[-3]
assert log_text == "module: test_logging; function: handler(); message: hello world"
if __name__ =="__main__":
test_log()

View File

@@ -1,7 +1,9 @@
from multiprocessing import Array, Event, Process
from time import sleep
from time import sleep, time
from ujson import loads as json_loads
import pytest
from sanic import Sanic
from sanic.response import json
from sanic.utils import local_request, HOST, PORT
@@ -13,8 +15,9 @@ from sanic.utils import local_request, HOST, PORT
# TODO: Figure out why this freezes on pytest but not when
# executed via interpreter
def skip_test_multiprocessing():
@pytest.mark.skip(
reason="Freezes with pytest not on interpreter")
def test_multiprocessing():
app = Sanic('test_json')
response = Array('c', 50)
@@ -51,3 +54,28 @@ def skip_test_multiprocessing():
raise ValueError("Expected JSON response but got '{}'".format(response))
assert results.get('test') == True
@pytest.mark.skip(
reason="Freezes with pytest not on interpreter")
def test_drain_connections():
app = Sanic('test_json')
@app.route('/')
async def handler(request):
return json({"test": True})
stop_event = Event()
async def after_start(*args, **kwargs):
http_response = await local_request('get', '/')
stop_event.set()
start = time()
app.serve_multiple({
'host': HOST,
'port': PORT,
'after_start': after_start,
'request_handler': app.handle_request,
}, workers=2, stop_event=stop_event)
end = time()
assert end - start < 0.05

View File

@@ -0,0 +1,54 @@
from sanic import Sanic
from sanic.response import text
from sanic.exceptions import PayloadTooLarge
from sanic.utils import sanic_endpoint_test
data_received_app = Sanic('data_received')
data_received_app.config.REQUEST_MAX_SIZE = 1
data_received_default_app = Sanic('data_received_default')
data_received_default_app.config.REQUEST_MAX_SIZE = 1
on_header_default_app = Sanic('on_header')
on_header_default_app.config.REQUEST_MAX_SIZE = 500
@data_received_app.route('/1')
async def handler1(request):
return text('OK')
@data_received_app.exception(PayloadTooLarge)
def handler_exception(request, exception):
return text('Payload Too Large from error_handler.', 413)
def test_payload_too_large_from_error_handler():
response = sanic_endpoint_test(
data_received_app, uri='/1', gather_request=False)
assert response.status == 413
assert response.text == 'Payload Too Large from error_handler.'
@data_received_default_app.route('/1')
async def handler2(request):
return text('OK')
def test_payload_too_large_at_data_received_default():
response = sanic_endpoint_test(
data_received_default_app, uri='/1', gather_request=False)
assert response.status == 413
assert response.text == 'Error: Payload Too Large'
@on_header_default_app.route('/1')
async def handler3(request):
return text('OK')
def test_payload_too_large_at_on_header_default():
data = 'a' * 1000
response = sanic_endpoint_test(
on_header_default_app, method='post', uri='/1',
gather_request=False, data=data)
assert response.status == 413
assert response.text == 'Error: Payload Too Large'

View File

@@ -12,7 +12,7 @@ request_timeout_default_app = Sanic('test_request_timeout_default')
@request_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(1)
await asyncio.sleep(2)
return text('OK')
@@ -29,7 +29,7 @@ def test_server_error_request_timeout():
@request_timeout_default_app.route('/1')
async def handler_2(request):
await asyncio.sleep(1)
await asyncio.sleep(2)
return text('OK')

View File

@@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
from sanic.exceptions import ServerError
# ------------------------------------------------------------ #
@@ -32,6 +33,47 @@ def test_text():
assert response.text == 'Hello'
def test_headers():
app = Sanic('test_text')
@app.route('/')
async def handler(request):
headers = {"spam": "great"}
return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app)
assert response.headers.get('spam') == 'great'
def test_non_str_headers():
app = Sanic('test_text')
@app.route('/')
async def handler(request):
headers = {"answer": 42}
return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app)
assert response.headers.get('answer') == '42'
def test_invalid_response():
app = Sanic('test_invalid_response')
@app.exception(ServerError)
def handler_exception(request, exception):
return text('Internal Server Error.', 500)
@app.route('/')
async def handler(request):
return 'This should fail'
request, response = sanic_endpoint_test(app)
assert response.status == 500
assert response.text == "Internal Server Error."
def test_json():
app = Sanic('test_json')
@@ -49,6 +91,19 @@ def test_json():
assert results.get('test') == True
def test_invalid_json():
app = Sanic('test_json')
@app.route('/')
async def handler(request):
return json(request.json())
data = "I am not json"
request, response = sanic_endpoint_test(app, data=data)
assert response.status == 400
def test_query_string():
app = Sanic('test_query_string')
@@ -62,6 +117,24 @@ def test_query_string():
assert request.args.get('test2') == 'false'
def test_token():
app = Sanic('test_post_token')
@app.route('/')
async def handler(request):
return text('OK')
# uuid4 generated token.
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
headers = {
'content-type': 'application/json',
'Authorization': 'Token {}'.format(token)
}
request, response = sanic_endpoint_test(app, headers=headers)
assert request.token == token
# ------------------------------------------------------------ #
# POST
# ------------------------------------------------------------ #

18
tests/test_response.py Normal file
View File

@@ -0,0 +1,18 @@
from random import choice
from sanic import Sanic
from sanic.response import HTTPResponse
from sanic.utils import sanic_endpoint_test
def test_response_body_not_a_string():
"""Test when a response body sent from the application is not a string"""
app = Sanic('response_body_not_a_string')
random_num = choice(range(1000))
@app.route('/hello')
async def hello_route(request):
return HTTPResponse(body=random_num)
request, response = sanic_endpoint_test(app, uri='/hello')
assert response.text == str(random_num)

View File

@@ -2,7 +2,7 @@ import pytest
from sanic import Sanic
from sanic.response import text
from sanic.router import RouteExists
from sanic.router import RouteExists, RouteDoesNotExist
from sanic.utils import sanic_endpoint_test
@@ -356,3 +356,110 @@ def test_add_route_method_not_allowed():
request, response = sanic_endpoint_test(app, method='post', uri='/test')
assert response.status == 405
def test_remove_static_route():
app = Sanic('test_remove_static_route')
async def handler1(request):
return text('OK1')
async def handler2(request):
return text('OK2')
app.add_route(handler1, '/test')
app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.status == 200
app.remove_route('/test')
app.remove_route('/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.status == 404
def test_remove_dynamic_route():
app = Sanic('test_remove_dynamic_route')
async def handler(request, name):
return text('OK')
app.add_route(handler, '/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.status == 200
app.remove_route('/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.status == 404
def test_remove_inexistent_route():
app = Sanic('test_remove_inexistent_route')
with pytest.raises(RouteDoesNotExist):
app.remove_route('/test')
def test_remove_unhashable_route():
app = Sanic('test_remove_unhashable_route')
async def handler(request, unhashable):
return text('OK')
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
assert response.status == 200
app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
assert response.status == 404
def test_remove_route_without_clean_cache():
app = Sanic('test_remove_static_route')
async def handler(request):
return text('OK')
app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
app.remove_route('/test', clean_cache=True)
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 404
app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
app.remove_route('/test', clean_cache=False)
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200

View File

@@ -0,0 +1,59 @@
from io import StringIO
from random import choice
from string import ascii_letters
import signal
import pytest
from sanic import Sanic
AVAILABLE_LISTENERS = [
'before_start',
'after_start',
'before_stop',
'after_stop'
]
def create_listener(listener_name, in_list):
async def _listener(app, loop):
print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name))
in_list.insert(0, app.name + listener_name)
return _listener
def start_stop_app(random_name_app, **run_kwargs):
def stop_on_alarm(signum, frame):
raise KeyboardInterrupt('SIGINT for sanic to stop gracefully')
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1)
try:
random_name_app.run(**run_kwargs)
except KeyboardInterrupt:
pass
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
def test_single_listener(listener_name):
"""Test that listeners on their own work"""
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
start_stop_app(
random_name_app,
**{listener_name: create_listener(listener_name, output)})
assert random_name_app.name + listener_name == output.pop()
def test_all_listeners():
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
start_stop_app(
random_name_app,
**{listener_name: create_listener(listener_name, output)
for listener_name in AVAILABLE_LISTENERS})
for listener_name in AVAILABLE_LISTENERS:
assert random_name_app.name + listener_name == output.pop()

View File

@@ -1,30 +1,62 @@
import inspect
import os
import pytest
from sanic import Sanic
from sanic.utils import sanic_endpoint_test
def test_static_file():
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
@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')
return static_directory
@pytest.fixture(scope='module')
def static_file_path(static_file_directory):
"""The path to the static file that we want to serve"""
return os.path.join(static_file_directory, 'test.file')
@pytest.fixture(scope='module')
def static_file_content(static_file_path):
"""The content of the static file to check"""
with open(static_file_path, 'rb') as file:
return file.read()
def test_static_file(static_file_path, static_file_content):
app = Sanic('test_static')
app.static('/testing.file', current_file)
app.static('/testing.file', static_file_path)
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == current_file_contents
assert response.body == static_file_content
def test_static_directory():
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
with open(current_file, 'rb') as file:
current_file_contents = file.read()
def test_static_directory(
static_file_directory, static_file_path, static_file_content):
app = Sanic('test_static')
app.static('/dir', current_directory)
app.static('/dir', static_file_directory)
request, response = sanic_endpoint_test(app, uri='/dir/test_static.py')
request, response = sanic_endpoint_test(app, uri='/dir/test.file')
assert response.status == 200
assert response.body == current_file_contents
assert response.body == static_file_content
def test_static_url_decode_file(static_file_directory):
decode_me_path = os.path.join(static_file_directory, 'decode me.txt')
with open(decode_me_path, 'rb') as file:
decode_me_contents = file.read()
app = Sanic('test_static')
app.static('/dir', static_file_directory)
request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt')
assert response.status == 200
assert response.body == decode_me_contents

23
tests/test_vhosts.py Normal file
View File

@@ -0,0 +1,23 @@
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
def test_vhosts():
app = Sanic('test_text')
@app.route('/', host="example.com")
async def handler(request):
return text("You're at example.com!")
@app.route('/', host="subdomain.example.com")
async def handler(request):
return text("You're at subdomain.example.com!")
headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "You're at example.com!"
headers = {"Host": "subdomain.example.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "You're at subdomain.example.com!"

View File

@@ -26,7 +26,7 @@ def test_methods():
def delete(self, request):
return text('I am delete method')
app.add_route(DummyView(), '/')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
@@ -48,7 +48,7 @@ def test_unexisting_methods():
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post")
@@ -63,7 +63,7 @@ def test_argument_methods():
def get(self, request, my_param_here):
return text('I am get method with %s' % my_param_here)
app.add_route(DummyView(), '/<my_param_here>')
app.add_route(DummyView.as_view(), '/<my_param_here>')
request, response = sanic_endpoint_test(app, uri='/test123')
@@ -79,7 +79,7 @@ def test_with_bp():
def get(self, request):
return text('I am get method')
bp.add_route(DummyView(), '/')
bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app)
@@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix():
def get(self, request):
return text('I am get method')
bp.add_route(DummyView(), '/')
bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/')
@@ -112,7 +112,7 @@ def test_with_middleware():
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
app.add_route(DummyView.as_view(), '/')
results = []
@@ -145,7 +145,7 @@ def test_with_middleware_response():
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app)
@@ -153,3 +153,44 @@ def test_with_middleware_response():
assert type(results[0]) is Request
assert type(results[1]) is Request
assert issubclass(type(results[2]), HTTPResponse)
def test_with_custom_class_methods():
app = Sanic('test_with_custom_class_methods')
class DummyView(HTTPMethodView):
global_var = 0
def _iternal_method(self):
self.global_var += 10
def get(self, request):
self._iternal_method()
return text('I am get method and global var is {}'.format(self.global_var))
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method and global var is 10'
def test_with_decorator():
app = Sanic('test_with_decorator')
results = []
def stupid_decorator(view):
def decorator(*args, **kwargs):
results.append(1)
return view(*args, **kwargs)
return decorator
class DummyView(HTTPMethodView):
decorators = [stupid_decorator]
def get(self, request):
return text('I am get method')
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
assert results[0] == 1

33
tox.ini
View File

@@ -1,34 +1,25 @@
[tox]
envlist = py35, report
envlist = py35, py36, flake8
[travis]
python =
3.5: py35, flake8
3.6: py36, flake8
[testenv]
deps =
aiohttp
pytest
# pytest-cov
coverage
commands =
coverage run -m pytest tests {posargs}
mv .coverage .coverage.{envname}
pytest tests {posargs}
basepython:
py35: python3.5
whitelist_externals =
coverage
mv
echo
[testenv:report]
[testenv:flake8]
deps =
flake8
commands =
coverage combine
coverage report
coverage html
echo "Open file://{toxinidir}/coverage/index.html"
basepython =
python3.5
flake8 sanic