Compare commits

..

146 Commits
0.3.1 ... 0.4.1

Author SHA1 Message Date
Eli Uriegas
5e5f513088 Merge pull request #497 from seemethere/increment_041
Increment version to 0.4.1
2017-02-28 08:55:34 -06:00
Eli Uriegas
9fcf725061 Increment version to 0.4.1 2017-02-28 08:55:04 -06:00
Eli Uriegas
601e015f00 Merge pull request #495 from r0fls/494
fix routing issue with slashes
2017-02-28 08:54:15 -06:00
Raphael Deem
21fb1dff7e fix routing issue with slashes 2017-02-27 20:01:11 -08:00
Eli Uriegas
da924a359c Merge pull request #493 from seemethere/fix_install
Attempt to remedy install problems from 0.4.0
2017-02-27 10:19:13 -06:00
Eli Uriegas
a5066f15dc Attempt to remedy install problems from 0.4.0
Relates to 1436fb3ef4
2017-02-27 10:14:47 -06:00
Eli Uriegas
bfbcdf8b86 Merge pull request #488 from seemethere/increment_version_040
Increment version to 0.4.0
2017-02-25 13:37:09 -06:00
Eli Uriegas
42e1d18470 Increment version to 0.4.0 2017-02-25 13:36:47 -06:00
Raphael Deem
435dc72aa1 Merge pull request #484 from r0fls/483
use getattr for request url in error handler
2017-02-24 10:12:04 -08:00
Raphael Deem
66c380548b use getattr for request url in error handler 2017-02-23 23:17:31 -08:00
Raphael Deem
13f81e9a6f Merge pull request #480 from subyraman/patch-2
Add documentation about `request.app`
2017-02-23 18:52:11 -08:00
Eli Uriegas
68c8796adb Merge pull request #481 from lixxu/master
url_for has something wrong with //
2017-02-23 18:27:13 -06:00
Suby Raman
4232f5342e correct indendation 2017-02-23 11:47:39 -05:00
lixxu
28c31359bf bug: url / will be empty 2017-02-24 00:45:50 +08:00
Suby Raman
a36e815227 Add documentation about request.app 2017-02-23 11:42:59 -05:00
lixxu
ff3d33d5e0 bug: url_for in blueprint may have // at the beginning 2017-02-24 00:17:43 +08:00
Eli Uriegas
d015d6b103 Merge pull request #476 from seemethere/inject_app_into_request
Inject app into request object
2017-02-23 09:12:41 -06:00
Eli Uriegas
0d160fbeaa Merge pull request #478 from messense/fix-requests-post-test-cases
Fix exception collection typo
2017-02-22 20:43:53 -06:00
messense
fedd7920a8 Fix exception collection typo 2017-02-23 10:39:14 +08:00
Eli Uriegas
6afac06596 Merge pull request #477 from messense/fix-requests-post-test-cases
Fix test cases for requests post
2017-02-22 20:39:03 -06:00
messense
91b2b40b9a Fix test cases for requests post 2017-02-23 10:36:08 +08:00
Eli Uriegas
56ecb6a3ea Inject app into request object
Allows for injection of the app object into the request object through
the request handler.

This allows for users to setup things like database connections etc. in
listeners and then utilize them throughout the their various route
handlers.

Usage is fairly simple like so:
```python
@app.get('/')
async def handler(request):
    request.app.anything
```

Name is up in the air but I'll leave this up for a few days and I'll
change it if we get a consensus
2017-02-22 11:34:14 -06:00
Eli Uriegas
64f73f624f Merge pull request #471 from r0fls/vhost-default
allow default vhost
2017-02-22 11:08:28 -06:00
Eli Uriegas
20b78b68a6 Merge pull request #470 from r0fls/url-slash
route logic for trailing slash
2017-02-22 11:07:23 -06:00
Raphael Deem
d1cc14201b Merge pull request #474 from r0fls/remove-ws-extension
remove websocket extension
2017-02-21 15:31:50 -08:00
Raphael Deem
a57ec9e669 remove websocket extension 2017-02-21 15:30:42 -08:00
Raphael Deem
9c72b557ec allow default vhost 2017-02-20 16:52:36 -08:00
Raphael Deem
207ec1e032 route logic for trailing slash 2017-02-20 16:11:28 -08:00
Eli Uriegas
ff5d4276bc Merge pull request #468 from hhstore/master
fix import path.
2017-02-20 07:29:11 -06:00
hhstore
26c9618e63 fix import path. 2017-02-20 17:57:15 +08:00
Eli Uriegas
9c306899ba Merge pull request #464 from tommyip/patch-1
Fix typo in routing.md
2017-02-19 11:40:48 -06:00
Tommy Ip
4b7e7aab33 Fix typo in routing.md 2017-02-19 12:39:39 +00:00
Eli Uriegas
2921e9f868 Merge pull request #463 from subyraman/patch-1
add sanic_compress extension
2017-02-18 18:56:17 -06:00
Suby Raman
70b7606cb8 add sanic_compress extension 2017-02-18 19:53:32 -05:00
Eli Uriegas
0cae91d525 Merge pull request #459 from seemethere/add_app_loop_safety
Add app loop safety
2017-02-18 16:53:49 -06:00
Eli Uriegas
2c2a28e46b Add better error message around app.loop 2017-02-18 12:29:25 -06:00
Eli Uriegas
c75d484f23 Merge branch 'master' of github.com:channelcat/sanic into add_app_loop_safety 2017-02-17 07:10:59 -06:00
Eli Uriegas
072f7fec30 Merge pull request #460 from seemethere/fix_travis_build
Fix coverage build
2017-02-17 07:10:27 -06:00
Eli Uriegas
a73379495f Fix coverage build
Moved `sanic.py` to `app.py` this change reflects that
2017-02-17 07:06:57 -06:00
Eli Uriegas
0b914866eb Merge pull request #401 from youknowone/error-logging
Enhance error logging
2017-02-17 07:01:40 -06:00
Eli Uriegas
f12186d024 Merge pull request #438 from r0fls/coverage
add coverage to tox
2017-02-17 07:01:14 -06:00
Eli Uriegas
4a2835dc84 Merge pull request #458 from lixxu/master
add user_agent to request
2017-02-17 07:00:52 -06:00
Eli Uriegas
55dc45de33 Merge pull request #455 from seemethere/add_better_response_error_detection
Adds some safety around response types being wrong
2017-02-17 06:59:52 -06:00
Eli Uriegas
39aa64ff68 Remove un-needed loop assignment 2017-02-17 06:57:41 -06:00
Eli Uriegas
94bb91f2fa Adds some safety around app loop property 2017-02-17 06:54:26 -06:00
Lix Xu
f553ca95a5 add user_agent to request 2017-02-17 15:12:07 +08:00
Jeong YunWon
b44e8167d7 Enhance error logging
- Prevent to fall into error logging recursion when it doesn't have
request info (Print 'Unknown' when the request is still `None`
- Print repr(e) rather than str(e)
2017-02-17 14:05:46 +09:00
Eli Uriegas
36c1122d20 Merge pull request #457 from seemethere/remove_double_logging_message
Removes the extra logging message for run
2017-02-16 21:53:25 -06:00
Eli Uriegas
ad13529eaa Removes the extra logging message for run
Solves for #456
2017-02-16 21:51:19 -06:00
Eli Uriegas
04c8f4a5ed Adds some safety around response types being wrong
Before we didn't check if the response object was actually a response
object. This PR catches the exception should it happen and reports it to
the user.
2017-02-16 17:49:14 -06:00
Eli Uriegas
850446195f Merge pull request #441 from robintiwari/master
added sanic motor (async driver for mongodb) example
2017-02-16 17:19:42 -06:00
Eli Uriegas
865506546f Merge pull request #440 from r0fls/isinstance
isinstance -> try/except
2017-02-16 17:19:04 -06:00
Eli Uriegas
fe72fadedd Merge pull request #432 from agoose77/cleanups
Bugfix & simplfication for host based routing
2017-02-16 17:18:24 -06:00
Eli Uriegas
9cf9d3ad5c Merge pull request #453 from seemethere/add_file_example
Adds file response object example
2017-02-16 17:17:29 -06:00
Eli Uriegas
426d0e6af1 Merge pull request #454 from seemethere/fix_empty_request_args
Default request.args to RequestParameters
2017-02-16 17:16:55 -06:00
Eli Uriegas
483b442b7d Default request.args to RequestParameters 2017-02-16 15:54:20 -06:00
Eli Uriegas
9420b0c947 Adds file response object example 2017-02-16 15:53:31 -06:00
Eli Uriegas
dcfb7345f0 Merge pull request #451 from abuckenheimer/master
#449 use stdlib json module if ujson is unavailible
2017-02-16 14:14:45 -06:00
Alec Buckenheimer
f9ab24a077 #449 use stdlib json module if ujson is unavailible 2017-02-16 15:03:52 -05:00
Raphael Deem
f932d16ba7 Merge pull request #448 from Typhon66/patch-1
Adding sanic_crud to extensions
2017-02-16 09:57:58 -08:00
Typhon
7d9acc3c36 Adding sanic_crud to extensions 2017-02-16 09:49:48 -08:00
Eli Uriegas
72f735124f Merge pull request #446 from subyraman/document-error
Fix blueprint documentation re: prefixes
2017-02-16 11:21:23 -06:00
Suby Raman
6ea5a4719a add url_for doc in blueprint 2017-02-16 11:48:31 -05:00
Suby Raman
f8c50b7f1e fix blueprints documentation 2017-02-16 11:46:19 -05:00
Eli Uriegas
feb1f1d71a Merge pull request #444 from lixxu/master
simple motor wrapper
2017-02-16 09:46:09 -06:00
Lix Xu
550afc27dc simple motor wrapper 2017-02-16 22:36:16 +08:00
@robintiwari
d13af4bdc4 updated examples 2017-02-15 22:11:16 -06:00
Robin
53a365dd2b added sanic motor (async driver for mongodb) example 2017-02-15 21:42:25 -06:00
Raphael Deem
6726affa7e isinstance -> try/except 2017-02-15 19:34:45 -08:00
Eli Uriegas
6ecf2a6eb2 Merge pull request #439 from seemethere/move_sanic_to_app
Moves sanic/sanic.py to sanic/app.py
2017-02-15 21:03:41 -06:00
Eli Uriegas
a359e11f97 Merge pull request #422 from r0fls/420
move logging to method in ErrorHandler
2017-02-15 21:03:15 -06:00
Eli Uriegas
54b2d74068 Get rid of relative imports 2017-02-15 20:54:00 -06:00
Eli Uriegas
c99aa7279b Moves sanic/sanic.py to sanic/app.py
Functionality wise this won't change much for most users, unless you
were directly importing from `sanic.sanic` in which case I am sorry if
you're affected by this.

Main motivation was because jedi autocompletion didn't work with this
and we were using relative imports to compensate for the fact that
having a module inside of your module with the same name creates major
namespace problems.
2017-02-15 20:47:34 -06:00
Raphael Deem
4fa568ce8a add coverage to tox 2017-02-15 15:20:43 -08:00
Eli Uriegas
874698b93f Merge pull request #431 from subyraman/test-client-v2
Add Flask-like `test_client` to replace `sanic_endpoint_test`
2017-02-15 15:44:30 -06:00
Eli Uriegas
b286fc1e4a Merge pull request #436 from lixxu/master
simple pagination support
2017-02-15 13:31:57 -06:00
Raphael Deem
1cf1024332 Merge branch 'master' into 420 2017-02-15 09:29:52 -08:00
Lix Xu
6b391b701b simple pagination support 2017-02-15 13:27:58 +08:00
Raphael Deem
efc90f8f5a Merge pull request #434 from agoose77/fix_warn_error
Fix mistake in warning
2017-02-14 16:46:37 -08:00
Angus Hollands
6535ba7c24 Fix mistake in warning 2017-02-14 20:53:55 +00:00
Angus Hollands
5c29c3d160 Merge branch 'master' of https://github.com/channelcat/sanic into cleanups 2017-02-14 20:47:28 +00:00
Angus Hollands
742d4bff78 Change to iterable as Python3.5 doesn't support Collection.
We don't really need the getitem and len attributes anyway
2017-02-14 20:46:14 +00:00
Suby Raman
7726ffa3f7 remove documentation about passing in the loop 2017-02-14 15:44:43 -05:00
Angus Hollands
b442d78ebb Bugfix & simplfication for host based routing (if list of hosts passed after a previous vhost route was added, previously attempted to add set to set)
Add comment documenting substandard behaviour
2017-02-14 20:32:04 +00:00
Eli Uriegas
d44edb5930 Merge pull request #430 from aquacash5/master
Added raw response for bag o' bytes responses
2017-02-14 14:19:01 -06:00
Suby Raman
d5633b3705 fix deprecation message 2017-02-14 15:16:58 -05:00
Suby Raman
3b68dc72e7 rework testing 2017-02-14 14:51:20 -05:00
Angus Hollands
51611c3934 Pep8 cleanups (#429)
* PEP8 cleanups

* PEP8 cleanups (server.py)

* PEP8 cleanups (blueprints.py)

* PEP8 cleanups (config.py)

* PEP8 cleanups (cookies.py)

* PEP8 cleanups (handlers.py)

* PEP8 cleanups (request.py)

* PEP8 cleanups (response.py)

* PEP8 cleanups (router.py)

* PEP8 cleanups (sanic.py) #2

* PEP8 cleanups (server.py) #2

* PEP8 cleanups (static.py)

* PEP8 cleanups (utils.py)

* PEP8 cleanups (views.py)
Updated docstring
2017-02-14 13:10:19 -06:00
Kyle Blöm
747b7567d7 Changed docstring for raw response 2017-02-14 09:40:33 -08:00
Kyle Blöm
797891d6cf Added raw response for bag o' bytes responses 2017-02-14 09:27:39 -08:00
Eli Uriegas
286dc3c32b Merge pull request #399 from lixxu/master
improve url_for to support multi values and special options
2017-02-14 10:27:54 -06:00
Eli Uriegas
a66ba21c3d Merge pull request #421 from Superman132/master
Fixed readme
2017-02-14 10:26:41 -06:00
Eli Uriegas
b139810b6a Merge pull request #424 from growingdever/blueprint-support-view
support view instance for blueprint add_route method
2017-02-14 10:26:05 -06:00
Eli Uriegas
dddc18d77c Merge pull request #427 from agoose77/trigger_cleanup
Simplify trigger events (now guaranteeed to receive list of events)
2017-02-14 09:34:34 -06:00
Angus Hollands
56f56d008a Simplify trigger events (now guaranteeed to receive list of events)
Don't bother checking if list empty - this function is not called often
2017-02-14 15:15:15 +00:00
growingdever
81a8a99b6e wrap over width comment 2017-02-14 17:20:39 +09:00
growingdever
07aa0ee7ad - copy codes from Sonic.add_route
- modify comment by r0fls
2017-02-14 17:15:38 +09:00
growingdever
b66a6bddbc fix typo 2017-02-14 14:30:07 +09:00
growingdever
d57d90fe6b - make blueprint add_route method support view instance
- update documentation that doesn't specify url_prefix parameter
2017-02-14 14:23:22 +09:00
Raphael Deem
de6c646ee8 move logging to method in ErrorHandler 2017-02-13 19:44:54 -08:00
lixxu
4839ede64f update test for url_for and update routing.md doc 2017-02-14 10:26:30 +08:00
Superman132
84f5faf653 Update README.rst 2017-02-13 20:48:37 -05:00
Superman132
281077bc26 Update README.rst 2017-02-13 19:35:03 -05:00
Superman132
ed5fe9ae9f Merge pull request #1 from channelcat/master
Merge commits with master
2017-02-13 19:31:33 -05:00
Eli Uriegas
1866e4ef44 Merge pull request #418 from argaen/fix_cache_example
Use decorator for cache example
2017-02-13 17:23:29 -06:00
Eli Uriegas
a6a07c3b3a Merge pull request #411 from r0fls/ensure-future
Ensure future
2017-02-13 17:13:26 -06:00
argaen
b2af8e640c Use decorator 2017-02-14 00:12:39 +01:00
Raphael Deem
7a3f5d508b fix merge conflicts 2017-02-13 14:19:44 -08:00
Eli Uriegas
7e1fd03104 Merge pull request #417 from argaen/fix_cache_example
Fixed aiocache example according to new loop policy
2017-02-13 16:11:26 -06:00
argaen
758415d326 Fixed aiocache example according to new loop policy 2017-02-13 23:08:42 +01:00
Eli Uriegas
1660041470 Merge pull request #408 from agoose77/master
Use app decorator instead of run arguments for before_start
2017-02-13 12:54:46 -06:00
Eli Uriegas
1783df883e Merge pull request #416 from subyraman/more-view-tests
Add CompositionView tests, simplify checks for invalid/duplicate methods
2017-02-13 11:09:56 -06:00
Suby Raman
b2be821637 reverse router changes 2017-02-13 11:55:00 -05:00
Suby Raman
051ff2b325 remove repr stuff 2017-02-13 11:50:09 -05:00
Suby Raman
4d6f9ffd7c rebase 2017-02-13 11:45:55 -05:00
Suby Raman
d614823013 rebase 2017-02-13 11:38:28 -05:00
Eli Uriegas
48aa51b739 Merge pull request #413 from r0fls/loop-signal-handlers
use try/except when adding loop sig handlers
2017-02-13 10:13:56 -06:00
Raphael Deem
41c6125e1b use try/except when adding loop sig handlers 2017-02-12 14:43:00 -08:00
Eli Uriegas
bb3d48f98b Merge pull request #412 from seemethere/improving_performance
Header performance gains
2017-02-12 15:48:45 -06:00
Raphael Deem
b5e46e83e2 ensure_future -> add_task 2017-02-12 12:29:12 -08:00
Angus Hollands
2340910b46 Update deprecation message
Fix bug with single callbacks
2017-02-12 18:15:14 +00:00
Eli Uriegas
d8c4c1525d Merge pull request #406 from agoose77/master_pre_patches_1
Cleanup middleware decorator
2017-02-12 12:14:50 -06:00
Eli Uriegas
6713ef7726 Remove unused import 2017-02-12 12:09:06 -06:00
Eli Uriegas
ae7555b065 Performance was down so this brings it back up
Changes from #378 introduced about a 10k request/sec slowdown. This
tries to rememdy it while keeping the same functionality but it's still
not as fast as 0.3.1
2017-02-12 12:05:14 -06:00
Angus Hollands
ee6ff0cc60 Add deprecation and old API 2017-02-12 12:28:02 +00:00
Raphael Deem
94b2352c2c add ensure_future method 2017-02-11 17:40:17 -08:00
Eli Uriegas
cf3f943feb Merge pull request #409 from r0fls/loop-attribute
add loop property
2017-02-11 18:31:50 -06:00
Eli Uriegas
55c4d583b9 Merge pull request #410 from r0fls/extensions
add extension to docs
2017-02-11 18:31:16 -06:00
Raphael Deem
0e1bb6ab04 add loop property 2017-02-11 16:28:35 -08:00
Raphael Deem
7944cff7a5 add extension to docs 2017-02-11 15:31:58 -08:00
Angus Hollands
8b08a370c5 Remove todo 2017-02-11 14:39:32 +00:00
Angus Hollands
2d5fd2fe1c fix test 2017-02-11 14:35:44 +00:00
Angus Hollands
b5e50ecb75 Use app decorator instead of run arguments for before_start
Mirror listener of blueprints
2017-02-11 14:30:17 +00:00
Angus Hollands
e00c9d0ee0 Fix line length 2017-02-11 12:39:04 +00:00
Angus Hollands
be9c9f045a Cleanup middleware decorator 2017-02-11 12:27:25 +00:00
Raphael Deem
75fca1b9c7 Merge pull request #402 from agoose77/patch-1
Don't ask for uvloop on windows
2017-02-10 12:11:12 -08:00
Angus Hollands
1436fb3ef4 Don't ask for uvloop on windows
This is a tricky issue, but essentially uvloop is unavailable on windows. This means for windows users, we have to install Sanic with no requirements, and then manually specify all requirements apart from uvloop.

However, Sanic will work with standard asyncio event loop. So, I propose we remove the uvloop requirement on windows. This patch doesn't touch any demo imports.
2017-02-10 13:14:36 +00:00
Raphael Deem
2bfd127218 Merge pull request #400 from aquacash5/master
added build folder to .gitignore
2017-02-09 23:13:49 -08:00
Kyle Blöm
de5da63d5c added build folder to .gitignore 2017-02-09 18:49:11 -08:00
lixxu
fb419eaa36 fix bug: netloc always in url if SERVER_NAME defined in config even _external not true 2017-02-09 18:26:17 +08:00
lixxu
cf2a363e5e improve url_for to support multi values for one arg, add _anchor/_external/_scheme options 2017-02-09 16:44:23 +08:00
Eli Uriegas
7401facc21 Merge pull request #398 from seemethere/fix_errors_from_content_range
Fixes errors related to #378
2017-02-08 20:06:02 -06:00
Eli Uriegas
579afe012b Fixes errors related to #378 2017-02-08 19:59:34 -06:00
Eli Uriegas
4f856e8783 Merge pull request #378 from aquacash5/master
Added the tests, code formatting changes, and the Range Request feature.
2017-02-08 19:39:29 -06:00
Eli Uriegas
eb059183f7 Merge branch 'master' into master 2017-02-08 19:37:32 -06:00
Kyle Blöm
d193a1eb70 Added the tests, code formatting changes, and the Range Request feature. 2017-01-30 17:04:51 -08:00
61 changed files with 1507 additions and 795 deletions

View File

@@ -1,7 +1,7 @@
[run] [run]
branch = True branch = True
source = sanic, tests source = sanic
omit = site-packages omit = site-packages, sanic/utils.py
[html] [html]
directory = coverage directory = coverage

3
.gitignore vendored
View File

@@ -12,4 +12,5 @@ settings.py
.cache/* .cache/*
.python-version .python-version
docs/_build/ docs/_build/
docs/_api/ docs/_api/
build/*

View File

@@ -78,10 +78,7 @@ Documentation
TODO TODO
---- ----
* Streamed file processing * Streamed file processing
* File output * http2
* Examples of integrations with 3rd-party modules
* RESTful router
Limitations Limitations
----------- -----------
* No wheels for uvloop and httptools on Windows :( * No wheels for uvloop and httptools on Windows :(

View File

@@ -131,8 +131,8 @@ can be used to implement our API versioning scheme.
from sanic.response import text from sanic.response import text
from sanic import Blueprint from sanic import Blueprint
blueprint_v1 = Blueprint('v1') blueprint_v1 = Blueprint('v1', url_prefix='/v1')
blueprint_v2 = Blueprint('v2') blueprint_v2 = Blueprint('v2', url_prefix='/v2')
@blueprint_v1.route('/') @blueprint_v1.route('/')
async def api_v1_root(request): async def api_v1_root(request):
@@ -153,8 +153,8 @@ from sanic import Sanic
from blueprints import blueprint_v1, blueprint_v2 from blueprints import blueprint_v1, blueprint_v2
app = Sanic(__name__) app = Sanic(__name__)
app.blueprint(blueprint_v1) app.blueprint(blueprint_v1, url_prefix='/v1')
app.blueprint(blueprint_v2) app.blueprint(blueprint_v2, url_prefix='/v2')
app.run(host='0.0.0.0', port=8000, debug=True) app.run(host='0.0.0.0', port=8000, debug=True)
``` ```
@@ -167,7 +167,7 @@ takes the format `<blueprint_name>.<handler_name>`. For example:
``` ```
@blueprint_v1.route('/') @blueprint_v1.route('/')
async def root(request): async def root(request):
url = app.url_for('v1.post_handler', post_id=5) url = app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
return redirect(url) return redirect(url)

View File

@@ -7,15 +7,6 @@ keyword arguments:
- `host` *(default `"127.0.0.1"`)*: Address to host the server on. - `host` *(default `"127.0.0.1"`)*: Address to host the server on.
- `port` *(default `8000`)*: Port to host the server on. - `port` *(default `8000`)*: Port to host the server on.
- `debug` *(default `False`)*: Enables debug output (slows server). - `debug` *(default `False`)*: Enables debug output (slows server).
- `before_start` *(default `None`)*: Function or list of functions to be executed
before the server starts accepting connections.
- `after_start` *(default `None`)*: Function or list of functions to be executed
after the server starts accepting connections.
- `before_stop` *(default `None`)*: Function or list of functions to be
executed when a stop signal is received before it is
respected.
- `after_stop` *(default `None`)*: Function or list of functions to be executed
when all requests are complete.
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s). - `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
- `sock` *(default `None`)*: Socket for the server to accept connections from. - `sock` *(default `None`)*: Socket for the server to accept connections from.
- `workers` *(default `1`)*: Number of worker processes to spawn. - `workers` *(default `1`)*: Number of worker processes to spawn.

View File

@@ -5,5 +5,10 @@ A list of Sanic extensions created by the community.
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. - [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
Allows using redis, memcache or an in memory store. Allows using redis, memcache or an in memory store.
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. - [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
- [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. - [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI. - [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
- [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.
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request

View File

@@ -69,6 +69,23 @@ The following variables are accessible as properties on `Request` objects:
- `ip` (str) - IP address of the requester. - `ip` (str) - IP address of the requester.
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
```python
from sanic.response import json
from sanic import Blueprint
bp = Blueprint('my_blueprint')
@bp.route('/')
async def bp_root(request):
if request.app.config['DEBUG']:
return json({'status': 'debug'})
else:
return json({'status': 'production'})
```
## Accessing values using `get` and `getlist` ## Accessing values using `get` and `getlist`
The request properties which return a dictionary actually return a subclass of The request properties which return a dictionary actually return a subclass of

View File

@@ -11,7 +11,7 @@ from sanic.response import json
@app.route("/") @app.route("/")
async def test(request): async def test(request):
return json({ "hello": "world" }) return json({ "hello": "world" })
``` ```
When the url `http://server.url/` is accessed (the base url of the server), the When the url `http://server.url/` is accessed (the base url of the server), the
final `/` is matched by the router to the handler function, `test`, which then final `/` is matched by the router to the handler function, `test`, which then
@@ -64,9 +64,9 @@ async def folder_handler(request, folder_id):
## HTTP request types ## HTTP request types
By default, a route defined on a URL will be avaialble for only GET requests to that URL. By default, a route defined on a URL will be available for only GET requests to that URL.
However, the `@app.route` decorator accepts an optional parameter, `methods`, However, the `@app.route` decorator accepts an optional parameter, `methods`,
whicl allows the handler function to work with any of the HTTP methods in the list. which allows the handler function to work with any of the HTTP methods in the list.
```python ```python
from sanic.response import text from sanic.response import text
@@ -81,6 +81,19 @@ async def get_handler(request):
``` ```
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
```python
@app.route('/get', methods=['GET'], host='example.com')
async def get_handler(request):
return text('GET request - {}'.format(request.args))
# if the host header doesn't match example.com, this route will be used
@app.route('/get', methods=['GET'])
async def get_handler(request):
return text('GET request in default - {}'.format(request.args))
```
There are also shorthand method decorators: There are also shorthand method decorators:
```python ```python
@@ -145,9 +158,26 @@ Other things to keep in mind when using `url_for`:
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', arg_two='two')
# /posts/5?arg_one=one&arg_two=two # /posts/5?arg_one=one&arg_two=two
``` ```
- Multivalue argument can be passed to `url_for`. For example:
```python
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 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 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 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 thrown. - 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.

View File

@@ -1,51 +1,73 @@
# Testing # Testing
Sanic endpoints can be tested locally using the `sanic.utils` module, which Sanic endpoints can be tested locally using the `test_client` object, which
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/) depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
library. The `sanic_endpoint_test` function runs a local server, issues a library.
configurable request to an endpoint, and returns the result. It takes the
following arguments:
- `app` An instance of a Sanic app. The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
- `method` *(default `'get'`)* A string representing the HTTP method to use. for you to run against your application. A simple example (using pytest) is like follows:
- `uri` *(default `'/'`)* A string representing the endpoint to test.
```python
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
def test_index_returns_200():
request, response = app.test_client.get('/')
assert response.status == 200
def test_index_put_not_allowed():
request, response = app.test_client.put('/')
assert response.status == 405
```
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.01:42101` and
your test request is executed against your application, using `aiohttp`.
The `test_client` methods accept the following arguments and keyword arguments:
- `uri` *(default `'/'`)* A string representing the URI to test.
- `gather_request` *(default `True`)* A boolean which determines whether the - `gather_request` *(default `True`)* A boolean which determines whether the
original request will be returned by the function. If set to `True`, 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 return value is a tuple of `(request, response)`, if `False` only the
response is returned. response is returned.
- `loop` *(default `None`)* The event loop to use. - `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run.
- `debug` *(default `False`)* A boolean which determines whether to run the - `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
server in debug mode.
The function further takes the `*request_args` and `**request_kwargs`, which The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the aiohttp ClientSession request.
are passed directly to the aiohttp ClientSession request. For example, to
supply data with a GET request, `method` would be `get` and the keyword For example, to supply data to a GET request, you would do the following:
argument `params={'value', 'key'}` would be supplied. More information about
```python
def test_get_request_includes_data():
params = {'key1': 'value1', 'key2': 'value2'}
request, response = app.test_client.get('/', params=params)
assert request.args.get('key1') == 'value1'
```
And to supply data to a JSON POST request:
```python
def test_post_json_request_includes_data():
data = {'key1': 'value1', 'key2': 'value2'}
request, response = app.test_client.post('/', data=json.dumps(data))
assert request.json.get('key1') == 'value1'
```
More information about
the available arguments to aiohttp can be found the available arguments to aiohttp can be found
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session). [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 ### Deprecated: `sanic_endpoint_test`
import pytest
import aiohttp Prior to version 0.3.2, testing was provided through the `sanic_endpoint_test` method. This method will be deprecated in the next major version after 0.4.0; please use the `test_client` instead.
```
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
# Import the Sanic app, usually created with Sanic(__name__) def test_index_returns_200():
from external_server import app request, response = sanic_endpoint_test(app)
assert response.status == 200
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']
``` ```

View File

@@ -1,6 +1,6 @@
""" """
Example of caching using aiocache package. To run it you will need a Redis Example of caching using aiocache package. To run it you will need a Redis
instance running in localhost:6379. instance running in localhost:6379. You can also try with SimpleMemoryCache.
Running this example you will see that the first call lasts 3 seconds and Running this example you will see that the first call lasts 3 seconds and
the rest are instant because the value is retrieved from the Redis. the rest are instant because the value is retrieved from the Redis.
@@ -20,9 +20,14 @@ from aiocache.serializers import JsonSerializer
app = Sanic(__name__) app = Sanic(__name__)
aiocache.settings.set_defaults(
class_="aiocache.RedisCache" @app.listener('before_server_start')
) def init_cache(sanic, loop):
aiocache.settings.set_defaults(
class_="aiocache.RedisCache",
# class_="aiocache.SimpleMemoryCache",
loop=loop
)
@cached(key="my_custom_key", serializer=JsonSerializer()) @cached(key="my_custom_key", serializer=JsonSerializer())
@@ -38,4 +43,4 @@ async def test(request):
return json(await expensive_call()) return json(await expensive_call())
app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop()) app.run(host="0.0.0.0", port=8000)

View File

@@ -8,6 +8,7 @@ app = Sanic(__name__)
sem = None sem = None
@app.listener('before_server_start')
def init(sanic, loop): def init(sanic, loop):
global sem global sem
CONCURRENCY_PER_WORKER = 4 CONCURRENCY_PER_WORKER = 4
@@ -33,4 +34,4 @@ async def test(request):
return json(response) return json(response)
app.run(host="0.0.0.0", port=8000, workers=2, before_start=init) app.run(host="0.0.0.0", port=8000, workers=2)

View File

@@ -26,6 +26,7 @@ async def get_pool():
app = Sanic(name=__name__) app = Sanic(name=__name__)
@app.listener('before_server_start')
async def prepare_db(app, loop): async def prepare_db(app, loop):
""" """
Let's create some table and add some data Let's create some table and add some data
@@ -61,5 +62,4 @@ async def handle(request):
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', app.run(host='0.0.0.0',
port=8000, port=8000,
debug=True, debug=True)
before_start=prepare_db)

View File

@@ -32,7 +32,7 @@ polls = sa.Table('sanic_polls', metadata,
app = Sanic(name=__name__) app = Sanic(name=__name__)
@app.listener('before_server_start')
async def prepare_db(app, loop): async def prepare_db(app, loop):
""" Let's add some data """ Let's add some data
@@ -58,9 +58,10 @@ async def handle(request):
async with engine.acquire() as conn: async with engine.acquire() as conn:
result = [] result = []
async for row in conn.execute(polls.select()): async for row in conn.execute(polls.select()):
result.append({"question": row.question, "pub_date": row.pub_date}) result.append({"question": row.question,
"pub_date": row.pub_date})
return json({"polls": result}) return json({"polls": result})
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, before_start=prepare_db) app.run(host='0.0.0.0', port=8000)

View File

@@ -27,6 +27,7 @@ def jsonify(records):
app = Sanic(__name__) app = Sanic(__name__)
@app.listener('before_server_start')
async def create_db(app, loop): async def create_db(app, loop):
""" """
Create some table and add some data Create some table and add some data
@@ -55,4 +56,4 @@ async def handler(request):
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, before_start=create_db) app.run(host='0.0.0.0', port=8000)

41
examples/sanic_motor.py Normal file
View File

@@ -0,0 +1,41 @@
""" sanic motor (async driver for mongodb) example
Required packages:
pymongo==3.4.0
motor==1.1
sanic==0.2.0
"""
from sanic import Sanic
from sanic.response import json
app = Sanic('motor_mongodb')
def get_db():
from motor.motor_asyncio import AsyncIOMotorClient
mongo_uri = "mongodb://127.0.0.1:27017/test"
client = AsyncIOMotorClient(mongo_uri)
return client['test']
@app.route('/objects', methods=['GET'])
async def get(request):
db = get_db()
docs = await db.test_col.find().to_list(length=100)
for doc in docs:
doc['id'] = str(doc['_id'])
del doc['_id']
return json(docs)
@app.route('/post', methods=['POST'])
async def new(request):
doc = request.json
print(doc)
db = get_db()
object_id = await db.test_col.save(doc)
return json({'object_id': str(object_id)})
if __name__ == "__main__":
app.run(host='127.0.0.1', port=8000)

View File

@@ -14,14 +14,6 @@ from peewee_async import Manager, PostgresqlDatabase
# we instantiate a custom loop so we can pass it to our db manager # we instantiate a custom loop so we can pass it to our db manager
def setup(app, loop):
database = PostgresqlDatabase(database='test',
host='127.0.0.1',
user='postgres',
password='mysecretpassword')
objects = Manager(database, loop=loop)
## from peewee_async docs: ## from peewee_async docs:
# Also theres no need to connect and re-connect before executing async queries # Also theres no need to connect and re-connect before executing async queries
# with manager! Its all automatic. But you can run Manager.connect() or # with manager! Its all automatic. But you can run Manager.connect() or
@@ -48,6 +40,15 @@ objects.database.allow_sync = False # this will raise AssertionError on ANY sync
app = Sanic('peewee_example') app = Sanic('peewee_example')
@app.listener('before_server_start')
def setup(app, loop):
database = PostgresqlDatabase(database='test',
host='127.0.0.1',
user='postgres',
password='mysecretpassword')
objects = Manager(database, loop=loop)
@app.route('/post/<key>/<value>') @app.route('/post/<key>/<value>')
async def post(request, key, value): async def post(request, key, value):
""" """
@@ -75,4 +76,4 @@ async def get(request):
if __name__ == "__main__": if __name__ == "__main__":
app.run(host='0.0.0.0', port=8000, before_start=setup) app.run(host='0.0.0.0', port=8000)

View File

@@ -1,6 +1,8 @@
import os
from sanic import Sanic from sanic import Sanic
from sanic.log import log from sanic.log import log
from sanic.response import json, text from sanic.response import json, text, file
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
app = Sanic(__name__) app = Sanic(__name__)
@@ -31,6 +33,10 @@ async def test_await(request):
await asyncio.sleep(5) await asyncio.sleep(5)
return text("I'm feeling sleepy") return text("I'm feeling sleepy")
@app.route("/file")
async def test_file(request):
return await file(os.path.abspath("setup.py"))
# ----------------------------------------------- # # ----------------------------------------------- #
# Exceptions # Exceptions
@@ -64,12 +70,14 @@ def query_string(request):
# Run Server # Run Server
# ----------------------------------------------- # # ----------------------------------------------- #
@app.listener('after_server_start')
def after_start(app, loop): def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH") log.info("OH OH OH OH OHHHHHHHH")
@app.listener('before_server_stop')
def before_stop(app, loop): def before_stop(app, loop):
log.info("TRIED EVERYTHING") log.info("TRIED EVERYTHING")
app.run(host="0.0.0.0", port=8000, debug=True, after_start=after_start, before_stop=before_stop) app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,6 +1,6 @@
from .sanic import Sanic from sanic.app import Sanic
from .blueprints import Blueprint from sanic.blueprints import Blueprint
__version__ = '0.3.1' __version__ = '0.4.1'
__all__ = ['Sanic', 'Blueprint'] __all__ = ['Sanic', 'Blueprint']

View File

@@ -1,8 +1,8 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from importlib import import_module from importlib import import_module
from .log import log from sanic.log import log
from .sanic import Sanic from sanic.app import Sanic
if __name__ == "__main__": if __name__ == "__main__":
parser = ArgumentParser(prog='sanic') parser = ArgumentParser(prog='sanic')

View File

@@ -1,27 +1,29 @@
import logging import logging
import re
import warnings
from asyncio import get_event_loop from asyncio import get_event_loop
from collections import deque from collections import deque, defaultdict
from functools import partial from functools import partial
from inspect import isawaitable, stack, getmodulename from inspect import isawaitable, stack, getmodulename
import re
from traceback import format_exc from traceback import format_exc
from urllib.parse import urlencode, urlunparse from urllib.parse import urlencode, urlunparse
import warnings
from .config import Config from sanic.config import Config
from .constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from .exceptions import Handler from sanic.exceptions import ServerError, URLBuildError, SanicException
from .exceptions import ServerError, URLBuildError from sanic.handlers import ErrorHandler
from .log import log from sanic.log import log
from .response import HTTPResponse from sanic.response import HTTPResponse
from .router import Router from sanic.router import Router
from .server import serve, serve_multiple, HttpProtocol from sanic.server import serve, serve_multiple, HttpProtocol
from .static import register as static_register from sanic.static import register as static_register
from sanic.testing import TestClient
from sanic.views import CompositionView
class Sanic: class Sanic:
def __init__(self, name=None, router=None,
error_handler=None): def __init__(self, name=None, router=None, error_handler=None):
# Only set up a default log handler if the # Only set up a default log handler if the
# end-user application didn't set anything up. # end-user application didn't set anything up.
if not logging.root.handlers and log.level == logging.NOTSET: if not logging.root.handlers and log.level == logging.NOTSET:
@@ -31,12 +33,15 @@ class Sanic:
handler.setFormatter(formatter) handler.setFormatter(formatter)
log.addHandler(handler) log.addHandler(handler)
log.setLevel(logging.INFO) log.setLevel(logging.INFO)
# Get name from previous stack frame
if name is None: if name is None:
frame_records = stack()[1] frame_records = stack()[1]
name = getmodulename(frame_records[1]) name = getmodulename(frame_records[1])
self.name = name self.name = name
self.router = router or Router() self.router = router or Router()
self.error_handler = error_handler or Handler() self.error_handler = error_handler or ErrorHandler()
self.config = Config() self.config = Config()
self.request_middleware = deque() self.request_middleware = deque()
self.response_middleware = deque() self.response_middleware = deque()
@@ -44,22 +49,61 @@ class Sanic:
self._blueprint_order = [] self._blueprint_order = []
self.debug = None self.debug = None
self.sock = None self.sock = None
self.processes = None self.listeners = defaultdict(list)
self.is_running = False
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
@property
def loop(self):
"""Synonymous with asyncio.get_event_loop().
Only supported when using the `app.run` method.
"""
if not self.is_running:
raise SanicException(
'Loop can only be retrieved after the app has started '
'running. Not supported with `create_server` function')
return get_event_loop()
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Registration # Registration
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def add_task(self, task):
"""Schedule a task to run later, after the loop has started.
Different from asyncio.ensure_future in that it does not
also return a future, and the actual ensure_future call
is delayed until before server start.
:param task: future, couroutine or awaitable
"""
@self.listener('before_server_start')
def run(app, loop):
if callable(task):
loop.create_task(task())
else:
loop.create_task(task)
# Decorator
def listener(self, event):
"""Create a listener from a decorated function.
:param event: event to listen to
"""
def decorator(listener):
self.listeners[event].append(listener)
return listener
return decorator
# Decorator # Decorator
def route(self, uri, methods=frozenset({'GET'}), host=None): def route(self, uri, methods=frozenset({'GET'}), host=None):
""" """Decorate a function to be registered as a route
Decorates a function to be registered as a route
:param uri: path of the URL :param uri: path of the URL
:param methods: list or tuple of methods allowed :param methods: list or tuple of methods allowed
:param host:
:return: decorated function :return: decorated function
""" """
@@ -77,29 +121,28 @@ class Sanic:
# Shorthand method decorators # Shorthand method decorators
def get(self, uri, host=None): def get(self, uri, host=None):
return self.route(uri, methods=["GET"], host=host) return self.route(uri, methods=frozenset({"GET"}), host=host)
def post(self, uri, host=None): def post(self, uri, host=None):
return self.route(uri, methods=["POST"], host=host) return self.route(uri, methods=frozenset({"POST"}), host=host)
def put(self, uri, host=None): def put(self, uri, host=None):
return self.route(uri, methods=["PUT"], host=host) return self.route(uri, methods=frozenset({"PUT"}), host=host)
def head(self, uri, host=None): def head(self, uri, host=None):
return self.route(uri, methods=["HEAD"], host=host) return self.route(uri, methods=frozenset({"HEAD"}), host=host)
def options(self, uri, host=None): def options(self, uri, host=None):
return self.route(uri, methods=["OPTIONS"], host=host) return self.route(uri, methods=frozenset({"OPTIONS"}), host=host)
def patch(self, uri, host=None): def patch(self, uri, host=None):
return self.route(uri, methods=["PATCH"], host=host) return self.route(uri, methods=frozenset({"PATCH"}), host=host)
def delete(self, uri, host=None): def delete(self, uri, host=None):
return self.route(uri, methods=["DELETE"], host=host) return self.route(uri, methods=frozenset({"DELETE"}), host=host)
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None): def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
""" """A helper method to register class instance or
A helper method to register class instance or
functions as a handler to the application url functions as a handler to the application url
routes. routes.
@@ -107,11 +150,21 @@ class Sanic:
:param uri: path of the URL :param uri: path of the URL
:param methods: list or tuple of methods allowed, these are overridden :param methods: list or tuple of methods allowed, these are overridden
if using a HTTPMethodView if using a HTTPMethodView
:param host:
:return: function or class instance :return: function or class instance
""" """
# Handle HTTPMethodView differently # Handle HTTPMethodView differently
if hasattr(handler, 'view_class'): if hasattr(handler, 'view_class'):
methods = frozenset(HTTP_METHODS) methods = set()
for method in HTTP_METHODS:
if getattr(handler.view_class, method.lower(), None):
methods.add(method)
# handle composition view differently
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host)(handler) self.route(uri=uri, methods=methods, host=host)(handler)
return handler return handler
@@ -120,10 +173,9 @@ class Sanic:
# Decorator # Decorator
def exception(self, *exceptions): def exception(self, *exceptions):
""" """Decorate a function to be registered as a handler for exceptions
Decorates a function to be registered as a handler for exceptions
:param \*exceptions: exceptions :param exceptions: exceptions
:return: decorated function :return: decorated function
""" """
@@ -135,14 +187,11 @@ class Sanic:
return response return response
# Decorator # Decorator
def middleware(self, *args, **kwargs): 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')
""" """
Decorates and registers middleware to be called before a request def register_middleware(middleware, attach_to='request'):
can either be called as @app.middleware or @app.middleware('request')
"""
attach_to = 'request'
def register_middleware(middleware):
if attach_to == 'request': if attach_to == 'request':
self.request_middleware.append(middleware) self.request_middleware.append(middleware)
if attach_to == 'response': if attach_to == 'response':
@@ -150,25 +199,24 @@ class Sanic:
return middleware return middleware
# Detect which way this was called, @middleware or @middleware('AT') # Detect which way this was called, @middleware or @middleware('AT')
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): if callable(middleware_or_request):
return register_middleware(args[0]) return register_middleware(middleware_or_request)
else: else:
attach_to = args[0] return partial(register_middleware,
return register_middleware attach_to=middleware_or_request)
# Static Files # Static Files
def static(self, uri, file_or_directory, pattern='.+', def static(self, uri, file_or_directory, pattern='.+',
use_modified_since=True): use_modified_since=True, use_content_range=False):
""" """Register a root to serve files from. The input can either be a
Registers a root to serve files from. The input can either be a file file or a directory. See
or a directory. See
""" """
static_register(self, uri, file_or_directory, pattern, static_register(self, uri, file_or_directory, pattern,
use_modified_since) use_modified_since, use_content_range)
def blueprint(self, blueprint, **options): def blueprint(self, blueprint, **options):
""" """Register a blueprint on the application.
Registers a blueprint on the application.
:param blueprint: Blueprint object :param blueprint: Blueprint object
:param options: option dictionary with blueprint defaults :param options: option dictionary with blueprint defaults
@@ -195,7 +243,7 @@ class Sanic:
return self.blueprint(*args, **kwargs) return self.blueprint(*args, **kwargs)
def url_for(self, view_name: str, **kwargs): def url_for(self, view_name: str, **kwargs):
"""Builds a URL based on a view name and the values provided. """Build a URL based on a view name and the values provided.
In order to build a URL, all request parameters must be supplied as In order to build a URL, all request parameters must be supplied as
keyword arguments, and each parameter must pass the test for the keyword arguments, and each parameter must pass the test for the
@@ -205,7 +253,7 @@ class Sanic:
Keyword arguments that are not request parameters will be included in Keyword arguments that are not request parameters will be included in
the output URL's query string. the output URL's query string.
:param view_name: A string referencing the view name :param view_name: string referencing the view name
:param **kwargs: keys and values that are used to build request :param **kwargs: keys and values that are used to build request
parameters and query string arguments. parameters and query string arguments.
@@ -222,12 +270,28 @@ class Sanic:
'Endpoint with name `{}` was not found'.format( 'Endpoint with name `{}` was not found'.format(
view_name)) view_name))
if uri != '/' and uri.endswith('/'):
uri = uri[:-1]
out = uri out = uri
# find all the parameters we will need to build in the URL # find all the parameters we will need to build in the URL
matched_params = re.findall( matched_params = re.findall(
self.router.parameter_pattern, uri) self.router.parameter_pattern, uri)
# _method is only a placeholder now, don't know how to support it
kwargs.pop('_method', None)
anchor = kwargs.pop('_anchor', '')
# _external need SERVER_NAME in config or pass _server arg
external = kwargs.pop('_external', False)
scheme = kwargs.pop('_scheme', '')
if scheme and not external:
raise ValueError('When specifying _scheme, _external must be True')
netloc = kwargs.pop('_server', None)
if netloc is None and external:
netloc = self.config.get('SERVER_NAME', '')
for match in matched_params: for match in matched_params:
name, _type, pattern = self.router.parse_parameter_string( name, _type, pattern = self.router.parse_parameter_string(
match) match)
@@ -268,12 +332,9 @@ class Sanic:
replacement_regex, supplied_param, out) replacement_regex, supplied_param, out)
# parse the remainder of the keyword arguments into a querystring # parse the remainder of the keyword arguments into a querystring
if kwargs: query_string = urlencode(kwargs, doseq=True) if kwargs else ''
query_string = urlencode(kwargs) # scheme://netloc/path;parameters?query#fragment
out = urlunparse(( out = urlunparse((scheme, netloc, out, '', query_string, anchor))
'', '', out,
'', query_string, ''
))
return out return out
@@ -285,9 +346,8 @@ class Sanic:
pass pass
async def handle_request(self, request, response_callback): async def handle_request(self, request, response_callback):
""" """Take a request from the HTTP Server and return a response object
Takes a request from the HTTP Server and returns a response object to to be sent back The HTTP Server only expects a response object, so
be sent back The HTTP Server only expects a response object, so
exception handling must be done here exception handling must be done here
:param request: HTTP Request object :param request: HTTP Request object
@@ -300,6 +360,8 @@ class Sanic:
# Request Middleware # Request Middleware
# -------------------------------------------- # # -------------------------------------------- #
request.app = self
response = False response = False
# The if improves speed. I don't know why # The if improves speed. I don't know why
if self.request_middleware: if self.request_middleware:
@@ -361,6 +423,14 @@ class Sanic:
response_callback(response) response_callback(response)
# -------------------------------------------------------------------- #
# Testing
# -------------------------------------------------------------------- #
@property
def test_client(self):
return TestClient(self)
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Execution # Execution
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
@@ -369,9 +439,8 @@ class Sanic:
after_start=None, before_stop=None, after_stop=None, ssl=None, after_start=None, before_stop=None, after_stop=None, ssl=None,
sock=None, workers=1, loop=None, protocol=HttpProtocol, sock=None, workers=1, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None, register_sys_signals=True): backlog=100, stop_event=None, register_sys_signals=True):
""" """Run the HTTP Server and listen until keyboard interrupt or term
Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drain connections before closing.
signal. On termination, drains connections before closing.
:param host: Address to host on :param host: Address to host on
:param port: Port to host on :param port: Port to host on
@@ -388,6 +457,10 @@ class Sanic:
:param sock: Socket for the server to accept connections from :param sock: Socket for the server to accept connections from
:param workers: Number of processes :param workers: Number of processes
received before it is respected received before it is respected
:param loop:
:param backlog:
:param stop_event:
:param register_sys_signals:
:param protocol: Subclass of asyncio protocol class :param protocol: Subclass of asyncio protocol class
:return: Nothing :return: Nothing
""" """
@@ -397,16 +470,18 @@ class Sanic:
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
loop=loop, protocol=protocol, backlog=backlog, loop=loop, protocol=protocol, backlog=backlog,
stop_event=stop_event, register_sys_signals=register_sys_signals) stop_event=stop_event, register_sys_signals=register_sys_signals)
try: try:
self.is_running = True
if workers == 1: if workers == 1:
serve(**server_settings) serve(**server_settings)
else: else:
serve_multiple(server_settings, workers, stop_event) serve_multiple(server_settings, workers, stop_event)
except:
except Exception as e:
log.exception( log.exception(
'Experienced exception while trying to serve') 'Experienced exception while trying to serve')
finally:
self.is_running = False
log.info("Server Stopped") log.info("Server Stopped")
def stop(self): def stop(self):
@@ -418,22 +493,19 @@ class Sanic:
before_stop=None, after_stop=None, ssl=None, before_stop=None, after_stop=None, ssl=None,
sock=None, loop=None, protocol=HttpProtocol, sock=None, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None): backlog=100, stop_event=None):
""" """Asynchronous version of `run`.
Asynchronous version of `run`.
NOTE: This does not support multiprocessing and is not the preferred
way to run a Sanic application.
""" """
server_settings = self._helper( server_settings = self._helper(
host=host, port=port, debug=debug, before_start=before_start, host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop, after_start=after_start, before_stop=before_stop,
after_stop=after_stop, ssl=ssl, sock=sock, loop=loop, after_stop=after_stop, ssl=ssl, sock=sock,
protocol=protocol, backlog=backlog, stop_event=stop_event, loop=loop or get_event_loop(), protocol=protocol,
backlog=backlog, stop_event=stop_event,
run_async=True) run_async=True)
# Serve
proto = "http"
if ssl is not None:
proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
return await serve(**server_settings) return await serve(**server_settings)
def _helper(self, host="127.0.0.1", port=8000, debug=False, def _helper(self, host="127.0.0.1", port=8000, debug=False,
@@ -441,9 +513,7 @@ class Sanic:
after_stop=None, ssl=None, sock=None, workers=1, loop=None, after_stop=None, ssl=None, sock=None, workers=1, loop=None,
protocol=HttpProtocol, backlog=100, stop_event=None, protocol=HttpProtocol, backlog=100, stop_event=None,
register_sys_signals=True, run_async=False): register_sys_signals=True, run_async=False):
""" """Helper function used by `run` and `create_server`."""
Helper function used by `run` and `create_server`.
"""
if loop is not None: if loop is not None:
if debug: if debug:
@@ -453,9 +523,18 @@ class Sanic:
"pull/335 has more information.", "pull/335 has more information.",
DeprecationWarning) DeprecationWarning)
# Deprecate this
if any(arg is not None for arg in (after_stop, after_start,
before_start, before_stop)):
if debug:
warnings.simplefilter('default')
warnings.warn("Passing a before_start, before_stop, after_start or"
"after_stop callback will be deprecated in next "
"major version after 0.4.0",
DeprecationWarning)
self.error_handler.debug = debug self.error_handler.debug = debug
self.debug = debug self.debug = debug
self.loop = loop = get_event_loop()
server_settings = { server_settings = {
'protocol': protocol, 'protocol': protocol,
@@ -477,19 +556,18 @@ class Sanic:
# Register start/stop events # Register start/stop events
# -------------------------------------------- # # -------------------------------------------- #
for event_name, settings_name, args, reverse in ( for event_name, settings_name, reverse, args in (
("before_server_start", "before_start", before_start, False), ("before_server_start", "before_start", False, before_start),
("after_server_start", "after_start", after_start, False), ("after_server_start", "after_start", False, after_start),
("before_server_stop", "before_stop", before_stop, True), ("before_server_stop", "before_stop", True, before_stop),
("after_server_stop", "after_stop", after_stop, True), ("after_server_stop", "after_stop", True, after_stop),
): ):
listeners = [] listeners = self.listeners[event_name].copy()
for blueprint in self.blueprints.values():
listeners += blueprint.listeners[event_name]
if args: if args:
if callable(args): if callable(args):
args = [args] listeners.append(args)
listeners += args else:
listeners.extend(args)
if reverse: if reverse:
listeners.reverse() listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered # Prepend sanic to the arguments when listeners are triggered

View File

@@ -1,5 +1,7 @@
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host']) FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
@@ -11,9 +13,9 @@ FutureStatic = namedtuple('Route',
class Blueprint: class Blueprint:
def __init__(self, name, url_prefix=None, host=None): def __init__(self, name, url_prefix=None, host=None):
""" """Create a new blueprint
Creates a new blueprint
:param name: Unique name of the blueprint :param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs :param url_prefix: URL to be prefixed before all route URLs
""" """
self.name = name self.name = name
@@ -27,9 +29,7 @@ class Blueprint:
self.statics = [] self.statics = []
def register(self, app, options): def register(self, app, options):
""" """Register the blueprint to the sanic app."""
Registers the blueprint to the sanic app.
"""
url_prefix = options.get('url_prefix', self.url_prefix) url_prefix = options.get('url_prefix', self.url_prefix)
@@ -41,7 +41,7 @@ class Blueprint:
# Prepend the blueprint URI prefix if available # Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri uri = url_prefix + future.uri if url_prefix else future.uri
app.route( app.route(
uri=uri, uri=uri[1:] if uri.startswith('//') else uri,
methods=future.methods, methods=future.methods,
host=future.host or self.host host=future.host or self.host
)(future.handler) )(future.handler)
@@ -65,11 +65,16 @@ class Blueprint:
app.static(uri, future.file_or_directory, app.static(uri, future.file_or_directory,
*future.args, **future.kwargs) *future.args, **future.kwargs)
# Event listeners
for event, listeners in self.listeners.items():
for listener in listeners:
app.listener(event)(listener)
def route(self, uri, methods=frozenset({'GET'}), host=None): def route(self, uri, methods=frozenset({'GET'}), host=None):
""" """Create a blueprint route from a decorated function.
Creates a blueprint route from a decorated function.
:param uri: Endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
:param methods: List of acceptable HTTP methods. :param methods: list of acceptable HTTP methods.
""" """
def decorator(handler): def decorator(handler):
route = FutureRoute(handler, uri, methods, host) route = FutureRoute(handler, uri, methods, host)
@@ -77,20 +82,33 @@ class Blueprint:
return handler return handler
return decorator return decorator
def add_route(self, handler, uri, methods=None, host=None): def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
"""Create a blueprint route from a function.
: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 methods: list of acceptable HTTP methods.
:return: function or class instance
""" """
Creates a blueprint route from a function. # Handle HTTPMethodView differently
:param handler: Function to handle uri request. if hasattr(handler, 'view_class'):
:param uri: Endpoint at which the route will be accessible. methods = set()
:param methods: List of acceptable HTTP methods.
""" for method in HTTP_METHODS:
route = FutureRoute(handler, uri, methods, host) if getattr(handler.view_class, method.lower(), None):
self.routes.append(route) methods.add(method)
# handle composition view differently
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host)(handler)
return handler return handler
def listener(self, event): def listener(self, event):
""" """Create a listener from a decorated function.
Create a listener from a decorated function.
:param event: Event to listen to. :param event: Event to listen to.
""" """
def decorator(listener): def decorator(listener):
@@ -99,9 +117,7 @@ class Blueprint:
return decorator return decorator
def middleware(self, *args, **kwargs): def middleware(self, *args, **kwargs):
""" """Create a blueprint middleware from a decorated function."""
Creates a blueprint middleware from a decorated function.
"""
def register_middleware(_middleware): def register_middleware(_middleware):
future_middleware = FutureMiddleware(_middleware, args, kwargs) future_middleware = FutureMiddleware(_middleware, args, kwargs)
self.middlewares.append(future_middleware) self.middlewares.append(future_middleware)
@@ -116,9 +132,7 @@ class Blueprint:
return register_middleware return register_middleware
def exception(self, *args, **kwargs): def exception(self, *args, **kwargs):
""" """Create a blueprint exception from a decorated function."""
Creates a blueprint exception from a decorated function.
"""
def decorator(handler): def decorator(handler):
exception = FutureException(handler, args, kwargs) exception = FutureException(handler, args, kwargs)
self.exceptions.append(exception) self.exceptions.append(exception)
@@ -126,9 +140,9 @@ class Blueprint:
return decorator return decorator
def static(self, uri, file_or_directory, *args, **kwargs): def static(self, uri, file_or_directory, *args, **kwargs):
""" """Create a blueprint static route from a decorated function.
Creates a blueprint static route from a decorated function.
:param uri: Endpoint at which the route will be accessible. :param uri: endpoint at which the route will be accessible.
:param file_or_directory: Static asset. :param file_or_directory: Static asset.
""" """
static = FutureStatic(uri, file_or_directory, args, kwargs) static = FutureStatic(uri, file_or_directory, args, kwargs)

View File

@@ -39,8 +39,9 @@ class Config(dict):
self[attr] = value self[attr] = value
def from_envvar(self, variable_name): def from_envvar(self, variable_name):
"""Loads a configuration from an environment variable pointing to """Load a configuration from an environment variable pointing to
a configuration file. a configuration file.
:param variable_name: name of the environment variable :param variable_name: name of the environment variable
:return: bool. ``True`` if able to load config, ``False`` otherwise. :return: bool. ``True`` if able to load config, ``False`` otherwise.
""" """
@@ -52,8 +53,9 @@ class Config(dict):
return self.from_pyfile(config_file) return self.from_pyfile(config_file)
def from_pyfile(self, filename): def from_pyfile(self, filename):
"""Updates the values in the config from a Python file. Only the uppercase """Update the values in the config from a Python file.
variables in that module are stored in the config. Only the uppercase variables in that module are stored in the config.
:param filename: an absolute path to the config file :param filename: an absolute path to the config file
""" """
module = types.ModuleType('config') module = types.ModuleType('config')
@@ -69,7 +71,7 @@ class Config(dict):
return True return True
def from_object(self, obj): def from_object(self, obj):
"""Updates the values from the given object. """Update the values from the given object.
Objects are usually either modules or classes. Objects are usually either modules or classes.
Just the uppercase variables in that object are stored in the config. Just the uppercase variables in that object are stored in the config.

View File

@@ -1,4 +1,3 @@
from datetime import datetime
import re import re
import string import string
@@ -39,8 +38,7 @@ _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
class CookieJar(dict): class CookieJar(dict):
""" """CookieJar dynamically writes headers as cookies are added and removed
CookieJar dynamically writes headers as cookies are added and removed
It gets around the limitation of one header per name by using the It gets around the limitation of one header per name by using the
MultiHeader class to provide a unique key that encodes to Set-Cookie. MultiHeader class to provide a unique key that encodes to Set-Cookie.
""" """
@@ -75,9 +73,7 @@ class CookieJar(dict):
class Cookie(dict): class Cookie(dict):
""" """A stripped down version of Morsel from SimpleCookie #gottagofast"""
This is a stripped down version of Morsel from SimpleCookie #gottagofast
"""
_keys = { _keys = {
"expires": "expires", "expires": "expires",
"path": "Path", "path": "Path",
@@ -107,13 +103,19 @@ class Cookie(dict):
def encode(self, encoding): def encode(self, encoding):
output = ['%s=%s' % (self.key, _quote(self.value))] output = ['%s=%s' % (self.key, _quote(self.value))]
for key, value in self.items(): for key, value in self.items():
if key == 'max-age' and isinstance(value, int): if key == 'max-age':
output.append('%s=%d' % (self._keys[key], value)) try:
elif key == 'expires' and isinstance(value, datetime): output.append('%s=%d' % (self._keys[key], value))
output.append('%s=%s' % ( except TypeError:
self._keys[key], output.append('%s=%s' % (self._keys[key], value))
value.strftime("%a, %d-%b-%Y %T GMT") elif key == 'expires':
)) try:
output.append('%s=%s' % (
self._keys[key],
value.strftime("%a, %d-%b-%Y %T GMT")
))
except AttributeError:
output.append('%s=%s' % (self._keys[key], value))
elif key in self._flags: elif key in self._flags:
if self[key]: if self[key]:
output.append(self._keys[key]) output.append(self._keys[key])
@@ -128,9 +130,8 @@ class Cookie(dict):
class MultiHeader: class MultiHeader:
""" """String-holding object which allow us to set a header within response
Allows us to set a header within response that has a unique key, that has a unique key, but may contain duplicate header names
but may contain duplicate header names
""" """
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name

View File

@@ -1,8 +1,3 @@
from .response import text, html
from .log import log
from traceback import format_exc, extract_tb
import sys
TRACEBACK_STYLE = ''' TRACEBACK_STYLE = '''
<style> <style>
body { body {
@@ -102,8 +97,10 @@ INTERNAL_SERVER_ERROR_HTML = '''
class SanicException(Exception): class SanicException(Exception):
def __init__(self, message, status_code=None): def __init__(self, message, status_code=None):
super().__init__(message) super().__init__(message)
if status_code is not None: if status_code is not None:
self.status_code = status_code self.status_code = status_code
@@ -141,85 +138,20 @@ class PayloadTooLarge(SanicException):
status_code = 413 status_code = 413
class Handler: class HeaderNotFound(SanicException):
handlers = None status_code = 400
cached_handlers = None
_missing = object()
def __init__(self):
self.handlers = []
self.cached_handlers = {}
self.debug = False
def _render_traceback_html(self, exception, request): class ContentRangeError(SanicException):
exc_type, exc_value, tb = sys.exc_info() status_code = 416
frames = extract_tb(tb)
frame_html = [] def __init__(self, message, content_range):
for frame in frames: super().__init__(message)
frame_html.append(TRACEBACK_LINE_HTML.format(frame)) self.headers = {
'Content-Type': 'text/plain',
"Content-Range": "bytes */%s" % (content_range.total,)
}
return TRACEBACK_WRAPPER_HTML.format(
style=TRACEBACK_STYLE,
exc_name=exc_type.__name__,
exc_value=exc_value,
frame_html=''.join(frame_html),
uri=request.url)
def add(self, exception, handler): class InvalidRangeType(ContentRangeError):
self.handlers.append((exception, handler)) pass
def lookup(self, exception):
handler = self.cached_handlers.get(exception, self._missing)
if handler is self._missing:
for exception_class, handler in self.handlers:
if isinstance(exception, exception_class):
self.cached_handlers[type(exception)] = handler
return handler
self.cached_handlers[type(exception)] = None
handler = None
return handler
def response(self, request, exception):
"""
Fetches and executes an exception handler and returns a response object
:param request: Request
:param exception: Exception to handle
:return: Response object
"""
handler = self.lookup(exception)
try:
response = handler and handler(
request=request, exception=exception)
if response is None:
response = self.default(request=request, exception=exception)
except:
log.error(format_exc())
if self.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):
log.error(format_exc())
if isinstance(exception, SanicException):
return text(
'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500))
elif self.debug:
html_output = self._render_traceback_html(exception, request)
response_message = (
'Exception occurred while handling uri: "{}"\n{}'.format(
request.url, format_exc()))
log.error(response_message)
return html(html_output, status=500)
else:
return html(INTERNAL_SERVER_ERROR_HTML, status=500)

139
sanic/handlers.py Normal file
View File

@@ -0,0 +1,139 @@
import sys
from traceback import format_exc, extract_tb
from sanic.exceptions import (
ContentRangeError,
HeaderNotFound,
INTERNAL_SERVER_ERROR_HTML,
InvalidRangeType,
SanicException,
TRACEBACK_LINE_HTML,
TRACEBACK_STYLE,
TRACEBACK_WRAPPER_HTML)
from sanic.log import log
from sanic.response import text, html
class ErrorHandler:
handlers = None
def __init__(self):
self.handlers = {}
self.debug = False
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
frames = extract_tb(tb)
frame_html = []
for frame in frames:
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
return TRACEBACK_WRAPPER_HTML.format(
style=TRACEBACK_STYLE,
exc_name=exc_type.__name__,
exc_value=exc_value,
frame_html=''.join(frame_html),
uri=request.url)
def add(self, exception, handler):
self.handlers[exception] = handler
def response(self, request, exception):
"""Fetches and executes an exception handler and returns a response
object
:param request: Request
:param exception: Exception to handle
:return: Response object
"""
handler = self.handlers.get(type(exception), self.default)
try:
response = handler(request=request, exception=exception)
except Exception:
self.log(format_exc())
if self.debug:
url = getattr(request, 'url', 'unknown')
response_message = (
'Exception raised in exception handler "{}" '
'for uri: "{}"\n{}').format(
handler.__name__, 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 log(self, message, level='error'):
"""
Override this method in an ErrorHandler subclass to prevent
logging exceptions.
"""
getattr(log, level)(message)
def default(self, request, exception):
self.log(format_exc())
if issubclass(type(exception), SanicException):
return text(
'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500),
headers=getattr(exception, 'headers', dict())
)
elif self.debug:
html_output = self._render_traceback_html(exception, request)
response_message = (
'Exception occurred while handling uri: "{}"\n{}'.format(
request.url, format_exc()))
log.error(response_message)
return html(html_output, status=500)
else:
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
class ContentRangeHandler:
"""Class responsible for parsing request header"""
__slots__ = ('start', 'end', 'size', 'total', 'headers')
def __init__(self, request, stats):
self.total = stats.st_size
_range = request.headers.get('Range')
if _range is None:
raise HeaderNotFound('Range Header Not Found')
unit, _, value = tuple(map(str.strip, _range.partition('=')))
if unit != 'bytes':
raise InvalidRangeType(
'%s is not a valid Range Type' % (unit,), self)
start_b, _, end_b = tuple(map(str.strip, value.partition('-')))
try:
self.start = int(start_b) if start_b else None
except ValueError:
raise ContentRangeError(
'\'%s\' is invalid for Content Range' % (start_b,), self)
try:
self.end = int(end_b) if end_b else None
except ValueError:
raise ContentRangeError(
'\'%s\' is invalid for Content Range' % (end_b,), self)
if self.end is None:
if self.start is None:
raise ContentRangeError(
'Invalid for Content Range parameters', self)
else:
# this case represents `Content-Range: bytes 5-`
self.end = self.total
else:
if self.start is None:
# this case represents `Content-Range: bytes -5`
self.start = self.total - self.end
self.end = self.total
if self.start >= self.end:
raise ContentRangeError(
'Invalid for Content Range parameters', self)
self.size = self.end - self.start
self.headers = {
'Content-Range': "bytes %s-%s/%s" % (
self.start, self.end, self.total)}
def __bool__(self):
return self.size > 0

View File

@@ -3,10 +3,14 @@ from collections import namedtuple
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from httptools import parse_url from httptools import parse_url
from urllib.parse import parse_qs from urllib.parse import parse_qs
from ujson import loads as json_loads
from sanic.exceptions import InvalidUsage
from .log import log try:
from ujson import loads as json_loads
except ImportError:
from json import loads as json_loads
from sanic.exceptions import InvalidUsage
from sanic.log import log
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
@@ -16,8 +20,7 @@ DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
class RequestParameters(dict): class RequestParameters(dict):
""" """Hosts a dict with lists as values where get returns the first
Hosts a dict with lists as values where get returns the first
value of the list and getlist returns the whole shebang value of the list and getlist returns the whole shebang
""" """
@@ -31,11 +34,9 @@ class RequestParameters(dict):
class Request(dict): class Request(dict):
""" """Properties of an HTTP request such as URL, headers, etc."""
Properties of an HTTP request such as URL, headers, etc.
"""
__slots__ = ( __slots__ = (
'url', 'headers', 'version', 'method', '_cookies', 'transport', 'app', 'url', 'headers', 'version', 'method', '_cookies', 'transport',
'query_string', 'body', 'query_string', 'body',
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip', '_ip',
@@ -44,6 +45,7 @@ class Request(dict):
def __init__(self, url_bytes, headers, version, method, transport): def __init__(self, url_bytes, headers, version, method, transport):
# TODO: Content-Encoding detection # TODO: Content-Encoding detection
url_parsed = parse_url(url_bytes) url_parsed = parse_url(url_bytes)
self.app = None
self.url = url_parsed.path.decode('utf-8') self.url = url_parsed.path.decode('utf-8')
self.headers = headers self.headers = headers
self.version = version self.version = version
@@ -73,8 +75,8 @@ class Request(dict):
@property @property
def token(self): def token(self):
""" """Attempt to return the auth header token.
Attempts to return the auth header token.
:return: token related to request :return: token related to request
""" """
auth_header = self.headers.get('Authorization') auth_header = self.headers.get('Authorization')
@@ -118,8 +120,7 @@ class Request(dict):
self.parsed_args = RequestParameters( self.parsed_args = RequestParameters(
parse_qs(self.query_string)) parse_qs(self.query_string))
else: else:
self.parsed_args = {} self.parsed_args = RequestParameters()
return self.parsed_args return self.parsed_args
@property @property
@@ -146,11 +147,10 @@ File = namedtuple('File', ['type', 'body', 'name'])
def parse_multipart_form(body, boundary): def parse_multipart_form(body, boundary):
""" """Parse a request body and returns fields and files
Parses a request body and returns fields and files
:param body: Bytes request body :param body: bytes request body
:param boundary: Bytes multipart boundary :param boundary: bytes multipart boundary
:return: fields (RequestParameters), files (RequestParameters) :return: fields (RequestParameters), files (RequestParameters)
""" """
files = RequestParameters() files = RequestParameters()

View File

@@ -1,10 +1,10 @@
from aiofiles import open as open_async
from mimetypes import guess_type from mimetypes import guess_type
from os import path from os import path
from ujson import dumps as json_dumps from ujson import dumps as json_dumps
from .cookies import CookieJar from aiofiles import open as open_async
from sanic.cookies import CookieJar
COMMON_STATUS_CODES = { COMMON_STATUS_CODES = {
200: b'OK', 200: b'OK',
@@ -98,19 +98,22 @@ class HTTPResponse:
# This is all returned in a kind-of funky way # This is all returned in a kind-of funky way
# We tried to make this as fast as possible in pure python # We tried to make this as fast as possible in pure python
timeout_header = b'' timeout_header = b''
if keep_alive and keep_alive_timeout: if keep_alive and keep_alive_timeout is not None:
timeout_header = b'Keep-Alive: timeout=%d\r\n' % keep_alive_timeout timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
self.headers['Content-Length'] = self.headers.get(
'Content-Length', len(self.body))
self.headers['Content-Type'] = self.headers.get(
'Content-Type', self.content_type)
headers = b'' headers = b''
if self.headers: for name, value in self.headers.items():
for name, value in self.headers.items(): try:
try: headers += (
headers += ( b'%b: %b\r\n' % (
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))) name.encode(), value.encode('utf-8')))
except AttributeError: except AttributeError:
headers += ( headers += (
b'%b: %b\r\n' % ( b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8'))) str(name).encode(), str(value).encode('utf-8')))
# Try to pull from the common codes first # Try to pull from the common codes first
# Speeds up response rate 6% over pulling from all # Speeds up response rate 6% over pulling from all
@@ -119,16 +122,13 @@ class HTTPResponse:
status = ALL_STATUS_CODES.get(self.status) status = ALL_STATUS_CODES.get(self.status)
return (b'HTTP/%b %d %b\r\n' return (b'HTTP/%b %d %b\r\n'
b'Content-Type: %b\r\n'
b'Content-Length: %d\r\n'
b'Connection: %b\r\n' b'Connection: %b\r\n'
b'%b%b\r\n' b'%b'
b'%b\r\n'
b'%b') % ( b'%b') % (
version.encode(), version.encode(),
self.status, self.status,
status, status,
self.content_type.encode(),
len(self.body),
b'keep-alive' if keep_alive else b'close', b'keep-alive' if keep_alive else b'close',
timeout_header, timeout_header,
headers, headers,
@@ -148,21 +148,38 @@ def json(body, status=200, headers=None, **kwargs):
:param body: Response data to be serialized. :param body: Response data to be serialized.
:param status: Response code. :param status: Response code.
:param headers: Custom Headers. :param headers: Custom Headers.
:param \**kwargs: Remaining arguments that are passed to the json encoder. :param kwargs: Remaining arguments that are passed to the json encoder.
""" """
return HTTPResponse(json_dumps(body, **kwargs), headers=headers, return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
status=status, content_type="application/json") status=status, content_type="application/json")
def text(body, status=200, headers=None): def text(body, status=200, headers=None,
content_type="text/plain; charset=utf-8"):
""" """
Returns response object with body in text format. Returns response object with body in text format.
:param body: Response data to be encoded. :param body: Response data to be encoded.
:param status: Response code. :param status: Response code.
:param headers: Custom Headers. :param headers: Custom Headers.
:param content_type:
the content type (string) of the response
""" """
return HTTPResponse(body, status=status, headers=headers, return HTTPResponse(body, status=status, headers=headers,
content_type="text/plain; charset=utf-8") content_type=content_type)
def raw(body, status=200, headers=None,
content_type="application/octet-stream"):
"""
Returns response object without encoding the body.
:param body: Response data.
:param status: Response code.
:param headers: Custom Headers.
:param content_type:
the content type (string) of the response
"""
return HTTPResponse(body_bytes=body, status=status, headers=headers,
content_type=content_type)
def html(body, status=200, headers=None): def html(body, status=200, headers=None):
@@ -176,17 +193,24 @@ def html(body, status=200, headers=None):
content_type="text/html; charset=utf-8") content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None): async def file(location, mime_type=None, headers=None, _range=None):
""" """Return a response object with file data.
Returns response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
:param mime_type: Specific mime_type. :param mime_type: Specific mime_type.
:param headers: Custom Headers. :param headers: Custom Headers.
:param _range:
""" """
filename = path.split(location)[-1] filename = path.split(location)[-1]
async with open_async(location, mode='rb') as _file: async with open_async(location, mode='rb') as _file:
out_stream = await _file.read() if _range:
await _file.seek(_range.start)
out_stream = await _file.read(_range.size)
headers['Content-Range'] = 'bytes %s-%s/%s' % (
_range.start, _range.end, _range.total)
else:
out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or 'text/plain' mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
@@ -198,14 +222,12 @@ async def file(location, mime_type=None, headers=None):
def redirect(to, headers=None, status=302, def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"): content_type="text/html; charset=utf-8"):
""" """Abort execution and cause a 302 redirect (by default).
Aborts execution and causes a 302 redirect (by default).
:param to: path or fully qualified URL to redirect to :param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request :param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302 :param status: status code (int) of the new request, defaults to 302
:param content_type: :param content_type: the content type (string) of the response
the content type (string) of the response
:returns: the redirecting Response :returns: the redirecting Response
""" """
headers = headers or {} headers = headers or {}

View File

@@ -1,8 +1,10 @@
import re import re
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from collections.abc import Iterable
from functools import lru_cache from functools import lru_cache
from .exceptions import NotFound, InvalidUsage
from .views import CompositionView from sanic.exceptions import NotFound, InvalidUsage
from sanic.views import CompositionView
Route = namedtuple( Route = namedtuple(
'Route', 'Route',
@@ -32,8 +34,7 @@ class RouteDoesNotExist(Exception):
class Router: class Router:
""" """Router supports basic routing with parameters and method checks
Router supports basic routing with parameters and method checks
Usage: Usage:
@@ -68,13 +69,14 @@ class Router:
self.routes_static = {} self.routes_static = {}
self.routes_dynamic = defaultdict(list) self.routes_dynamic = defaultdict(list)
self.routes_always_check = [] self.routes_always_check = []
self.hosts = None self.hosts = set()
def parse_parameter_string(self, parameter_string): def parse_parameter_string(self, parameter_string):
""" """Parse a parameter string into its constituent name, type, and
Parse a parameter string into its constituent name, type, and pattern pattern
For example: For example:
`parse_parameter_string('<param_one:[A-z]')` -> `parse_parameter_string('<param_one:[A-z]>')` ->
('param_one', str, '[A-z]') ('param_one', str, '[A-z]')
:param parameter_string: String to parse :param parameter_string: String to parse
@@ -94,33 +96,50 @@ class Router:
return name, _type, pattern return name, _type, pattern
def add(self, uri, methods, handler, host=None): def add(self, uri, methods, handler, host=None):
""" # add regular version
Adds a handler to the route list self._add(uri, methods, handler, host)
slash_is_missing = (
not uri[-1] == '/'
and not self.routes_all.get(uri + '/', False)
)
without_slash_is_missing = (
uri[-1] == '/'
and not self.routes_all.get(uri[:-1], False)
and not uri == '/'
)
# add version with trailing slash
if slash_is_missing:
self._add(uri + '/', methods, handler, host)
# add version without trailing slash
elif without_slash_is_missing:
self._add(uri[:-1], methods, handler, host)
:param uri: Path to match def _add(self, uri, methods, handler, host=None):
:param methods: Array of accepted method names. """Add a handler to the route list
If none are provided, any method is allowed
:param handler: Request handler function. :param uri: path to match
When executed, it should provide a response object. :param methods: sequence of accepted method names. If none are
provided, any method is allowed
:param handler: request handler function.
When executed, it should provide a response object.
:return: Nothing :return: Nothing
""" """
if host is not None: 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:
if isinstance(host, list):
host = set(host)
self.hosts.add(host)
if isinstance(host, str): if isinstance(host, str):
uri = host + uri uri = host + uri
self.hosts.add(host)
else: else:
for h in host: if not isinstance(host, Iterable):
self.add(uri, methods, handler, h) raise ValueError("Expected either string or Iterable of "
"host strings, not {!r}".format(host))
for host_ in host:
self.add(uri, methods, handler, host_)
return return
else:
# default host
self.hosts.add('*')
# Dict for faster lookups of if method allowed # Dict for faster lookups of if method allowed
if methods: if methods:
@@ -239,8 +258,7 @@ class Router:
@lru_cache(maxsize=ROUTER_CACHE_SIZE) @lru_cache(maxsize=ROUTER_CACHE_SIZE)
def find_route_by_view_name(self, view_name): def find_route_by_view_name(self, view_name):
""" """Find a route in the router based on the specified view name.
Find a route in the router based on the specified view name.
:param view_name: string of view name to search by :param view_name: string of view name to search by
:return: tuple containing (uri, Route) :return: tuple containing (uri, Route)
@@ -255,26 +273,30 @@ class Router:
return (None, None) return (None, None)
def get(self, request): def get(self, request):
""" """Get a request handler based on the URL of the request, or raises an
Gets a request handler based on the URL of the request, or raises an
error error
:param request: Request object :param request: Request object
:return: handler, arguments, keyword arguments :return: handler, arguments, keyword arguments
""" """
if self.hosts is None: # No virtual hosts specified; default behavior
if not self.hosts:
return self._get(request.url, request.method, '') return self._get(request.url, request.method, '')
else: # virtual hosts specified; try to match route to the host header
try:
return self._get(request.url, request.method, return self._get(request.url, request.method,
request.headers.get("Host", '')) request.headers.get("Host", ''))
# try default hosts
except NotFound:
return self._get(request.url, request.method, '')
@lru_cache(maxsize=ROUTER_CACHE_SIZE) @lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, url, method, host): def _get(self, url, method, host):
""" """Get a request handler based on the URL of the request, or raises an
Gets a request handler based on the URL of the request, or raises an
error. Internal method for caching. error. Internal method for caching.
:param url: Request URL
:param method: Request method :param url: request URL
:param method: request method
:return: handler, arguments, keyword arguments :return: handler, arguments, keyword arguments
""" """
url = host + url url = host + url

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import os import os
import traceback import traceback
import warnings
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable
from multiprocessing import Process, Event from multiprocessing import Process, Event
@@ -9,21 +10,19 @@ from signal import SIGTERM, SIGINT
from signal import signal as signal_func from signal import signal as signal_func
from socket import socket, SOL_SOCKET, SO_REUSEADDR from socket import socket, SOL_SOCKET, SO_REUSEADDR
from time import time from time import time
import warnings
from httptools import HttpRequestParser from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError from httptools.parser.errors import HttpParserError
from .exceptions import ServerError
try: try:
import uvloop as async_loop import uvloop as async_loop
except ImportError: except ImportError:
async_loop = asyncio async_loop = asyncio
from .log import log from sanic.log import log
from .request import Request from sanic.request import Request
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage from sanic.exceptions import (
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError)
current_time = None current_time = None
@@ -33,8 +32,7 @@ class Signal:
class CIDict(dict): class CIDict(dict):
""" """Case Insensitive dict where all keys are converted to lowercase
Case Insensitive dict where all keys are converted to lowercase
This does not maintain the inputted case when calling items() or keys() This does not maintain the inputted case when calling items() or keys()
in favor of speed, since headers are case insensitive in favor of speed, since headers are case insensitive
""" """
@@ -169,19 +167,26 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- # # -------------------------------------------- #
def write_response(self, response): def write_response(self, response):
keep_alive = (
self.parser.should_keep_alive() and not self.signal.stopped)
try: try:
keep_alive = (
self.parser.should_keep_alive() and not self.signal.stopped)
self.transport.write( self.transport.write(
response.output( response.output(
self.request.version, keep_alive, self.request_timeout)) self.request.version, keep_alive, self.request_timeout))
except AttributeError:
log.error(
('Invalid response object for url {}, '
'Expected Type: HTTPResponse, Actual Type: {}').format(
self.url, type(response)))
self.write_error(ServerError('Invalid response type'))
except RuntimeError: except RuntimeError:
log.error( log.error(
'Connection lost before response written @ {}'.format( 'Connection lost before response written @ {}'.format(
self.request.ip)) self.request.ip))
except Exception as e: except Exception as e:
self.bail_out( self.bail_out(
"Writing response failed, connection closed {}".format(e)) "Writing response failed, connection closed {}".format(
repr(e)))
finally: finally:
if not keep_alive: if not keep_alive:
self.transport.close() self.transport.close()
@@ -198,10 +203,10 @@ class HttpProtocol(asyncio.Protocol):
except RuntimeError: except RuntimeError:
log.error( log.error(
'Connection lost before error written @ {}'.format( 'Connection lost before error written @ {}'.format(
self.request.ip)) self.request.ip if self.request else 'Unknown'))
except Exception as e: except Exception as e:
self.bail_out( self.bail_out(
"Writing error failed, connection closed {}".format(e), "Writing error failed, connection closed {}".format(repr(e)),
from_error=True) from_error=True)
finally: finally:
self.transport.close() self.transport.close()
@@ -228,8 +233,8 @@ class HttpProtocol(asyncio.Protocol):
self._total_request_size = 0 self._total_request_size = 0
def close_if_idle(self): def close_if_idle(self):
""" """Close the connection if a request is not being sent or received
Close the connection if a request is not being sent or received
:return: boolean - True if closed, false if staying open :return: boolean - True if closed, false if staying open
""" """
if not self.parser: if not self.parser:
@@ -239,9 +244,8 @@ class HttpProtocol(asyncio.Protocol):
def update_current_time(loop): def update_current_time(loop):
""" """Cache the current time, since it is needed at the end of every
Caches the current time, since it is needed keep-alive request to update the request timeout time
at the end of every keep-alive request to update the request timeout time
:param loop: :param loop:
:return: :return:
@@ -252,17 +256,15 @@ def update_current_time(loop):
def trigger_events(events, loop): def trigger_events(events, loop):
""" """Trigger event callbacks (functions or async)
:param events: one or more sync or async functions to execute :param events: one or more sync or async functions to execute
:param loop: event loop :param loop: event loop
""" """
if events: for event in events:
if not isinstance(events, list): result = event(loop)
events = [events] if isawaitable(result):
for event in events: loop.run_until_complete(result)
result = event(loop)
if isawaitable(result):
loop.run_until_complete(result)
def serve(host, port, request_handler, error_handler, before_start=None, def serve(host, port, request_handler, error_handler, before_start=None,
@@ -270,31 +272,30 @@ def serve(host, port, request_handler, error_handler, before_start=None,
request_timeout=60, ssl=None, sock=None, request_max_size=None, request_timeout=60, ssl=None, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False): register_sys_signals=True, run_async=False):
""" """Start asynchronous HTTP Server on an individual process.
Starts asynchronous HTTP Server on an individual process.
:param host: Address to host on :param host: Address to host on
:param port: Port to host on :param port: Port to host on
:param request_handler: Sanic request handler with middleware :param request_handler: Sanic request handler with middleware
:param error_handler: Sanic error handler with middleware :param error_handler: Sanic error handler with middleware
:param before_start: Function to be executed before the server starts :param before_start: function to be executed before the server starts
listening. Takes arguments `app` instance and `loop` listening. Takes arguments `app` instance and `loop`
:param after_start: Function to be executed after the server starts :param after_start: function to be executed after the server starts
listening. Takes arguments `app` instance and `loop` listening. Takes arguments `app` instance and `loop`
:param before_stop: Function to be executed when a stop signal is :param before_stop: function to be executed when a stop signal is
received before it is respected. Takes arguments received before it is respected. Takes arguments
`app` instance and `loop` `app` instance and `loop`
:param after_stop: Function to be executed when a stop signal is :param after_stop: function to be executed when a stop signal is
received after it is respected. Takes arguments received after it is respected. Takes arguments
`app` instance and `loop` `app` instance and `loop`
:param debug: Enables debug output (slows server) :param debug: enables debug output (slows server)
:param request_timeout: time in seconds :param request_timeout: time in seconds
:param ssl: SSLContext :param ssl: SSLContext
:param sock: Socket for the server to accept connections from :param sock: Socket for the server to accept connections from
:param request_max_size: size in bytes, `None` for no limit :param request_max_size: size in bytes, `None` for no limit
:param reuse_port: `True` for multiple workers :param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop :param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class :param protocol: subclass of asyncio protocol class
:return: Nothing :return: Nothing
""" """
if not run_async: if not run_async:
@@ -346,8 +347,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
# Register signals for graceful termination # Register signals for graceful termination
if register_sys_signals: if register_sys_signals:
for _signal in (SIGINT, SIGTERM): for _signal in (SIGINT, SIGTERM):
loop.add_signal_handler(_signal, loop.stop) try:
loop.add_signal_handler(_signal, loop.stop)
except NotImplementedError:
log.warn('Sanic tried to use loop.add_signal_handler but it is'
' not implemented on this platform.')
pid = os.getpid() pid = os.getpid()
try: try:
log.info('Starting worker [{}]'.format(pid)) log.info('Starting worker [{}]'.format(pid))
@@ -376,9 +380,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
def serve_multiple(server_settings, workers, stop_event=None): def serve_multiple(server_settings, workers, stop_event=None):
""" """Start multiple server processes simultaneously. Stop on interrupt
Starts multiple server processes simultaneously. Stops on interrupt and terminate signals, and drain connections when complete.
and terminate signals, and drains connections when complete.
:param server_settings: kw arguments to be passed to the serve function :param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch :param workers: number of workers to launch

View File

@@ -1,19 +1,28 @@
from aiofiles.os import stat from mimetypes import guess_type
from os import path from os import path
from re import sub from re import sub
from time import strftime, gmtime from time import strftime, gmtime
from urllib.parse import unquote from urllib.parse import unquote
from .exceptions import FileNotFound, InvalidUsage from aiofiles.os import stat
from .response import file, HTTPResponse
from sanic.exceptions import (
ContentRangeError,
FileNotFound,
HeaderNotFound,
InvalidUsage,
)
from sanic.handlers import ContentRangeHandler
from sanic.response import file, HTTPResponse
def register(app, uri, file_or_directory, pattern, use_modified_since): def register(app, uri, file_or_directory, pattern,
# TODO: Though sanic is not a file server, I feel like we should atleast use_modified_since, use_content_range):
# TODO: Though sanic is not a file server, I feel like we should at least
# make a good effort here. Modified-since is nice, but we could # make a good effort here. Modified-since is nice, but we could
# also look into etags, expires, and caching # also look into etags, expires, and caching
""" """
Registers a static directory handler with Sanic by adding a route to the Register a static directory handler with Sanic by adding a route to the
router and registering a handler. router and registering a handler.
:param app: Sanic :param app: Sanic
@@ -23,8 +32,9 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
:param use_modified_since: If true, send file modified time, and return :param use_modified_since: If true, send file modified time, and return
not modified if the browser's matches the not modified if the browser's matches the
server's server's
:param use_content_range: If true, process header for range requests
and sends the file part that is requested
""" """
# If we're not trying to match a file directly, # If we're not trying to match a file directly,
# serve from the folder # serve from the folder
if not path.isfile(file_or_directory): if not path.isfile(file_or_directory):
@@ -50,18 +60,41 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
headers = {} headers = {}
# Check if the client has been sent this file before # Check if the client has been sent this file before
# and it has not been modified since # and it has not been modified since
stats = None
if use_modified_since: if use_modified_since:
stats = await stat(file_path) stats = await stat(file_path)
modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT', modified_since = strftime(
gmtime(stats.st_mtime)) '%a, %d %b %Y %H:%M:%S GMT', gmtime(stats.st_mtime))
if request.headers.get('If-Modified-Since') == modified_since: if request.headers.get('If-Modified-Since') == modified_since:
return HTTPResponse(status=304) return HTTPResponse(status=304)
headers['Last-Modified'] = modified_since headers['Last-Modified'] = modified_since
_range = None
return await file(file_path, headers=headers) if use_content_range:
except: _range = None
if not stats:
stats = await stat(file_path)
headers['Accept-Ranges'] = 'bytes'
headers['Content-Length'] = str(stats.st_size)
if request.method != 'HEAD':
try:
_range = ContentRangeHandler(request, stats)
except HeaderNotFound:
pass
else:
del headers['Content-Length']
for key, value in _range.headers.items():
headers[key] = value
if request.method == 'HEAD':
return HTTPResponse(
headers=headers,
content_type=guess_type(file_path)[0] or 'text/plain')
else:
return await file(file_path, headers=headers, _range=_range)
except ContentRangeError:
raise
except Exception:
raise FileNotFound('File not found', raise FileNotFound('File not found',
path=file_or_directory, path=file_or_directory,
relative_url=file_uri) relative_url=file_uri)
app.route(uri, methods=['GET'])(_handler) app.route(uri, methods=['GET', 'HEAD'])(_handler)

91
sanic/testing.py Normal file
View File

@@ -0,0 +1,91 @@
from sanic.log import log
HOST = '127.0.0.1'
PORT = 42101
class TestClient:
def __init__(self, app):
self.app = app
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=PORT, uri=uri)
log.info(url)
async with aiohttp.ClientSession(cookies=cookies) as session:
async with getattr(
session, method.lower())(url, *args, **kwargs) as response:
response.text = await response.text()
response.body = await response.read()
return response
def _sanic_endpoint_test(
self, method='get', uri='/', gather_request=True,
debug=False, server_kwargs={},
*request_args, **request_kwargs):
results = [None, None]
exceptions = []
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')
async def _collect_response(sanic, loop):
try:
response = await self._local_request(
method, uri, *request_args,
**request_kwargs)
results[-1] = response
except Exception as e:
exceptions.append(e)
self.app.stop()
self.app.run(host=HOST, debug=debug, port=PORT, **server_kwargs)
self.app.listeners['after_server_start'].pop()
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))
if gather_request:
try:
request, response = results
return request, response
except:
raise ValueError(
"Request and response object expected, got ({})".format(
results))
else:
try:
return results[-1]
except:
raise ValueError(
"Request object expected, got ({})".format(results))
def get(self, *args, **kwargs):
return self._sanic_endpoint_test('get', *args, **kwargs)
def post(self, *args, **kwargs):
return self._sanic_endpoint_test('post', *args, **kwargs)
def put(self, *args, **kwargs):
return self._sanic_endpoint_test('put', *args, **kwargs)
def delete(self, *args, **kwargs):
return self._sanic_endpoint_test('delete', *args, **kwargs)
def patch(self, *args, **kwargs):
return self._sanic_endpoint_test('patch', *args, **kwargs)
def options(self, *args, **kwargs):
return self._sanic_endpoint_test('options', *args, **kwargs)
def head(self, *args, **kwargs):
return self._sanic_endpoint_test('head', *args, **kwargs)

View File

@@ -1,59 +1,17 @@
import aiohttp import warnings
from sanic.log import log
HOST = '127.0.0.1' from sanic.testing import TestClient
PORT = 42101
async def local_request(method, uri, cookies=None, *args, **kwargs):
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
log.info(url)
async with aiohttp.ClientSession(cookies=cookies) as session:
async with getattr(
session, method.lower())(url, *args, **kwargs) as response:
response.text = await response.text()
response.body = await response.read()
return response
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
debug=False, server_kwargs={}, debug=False, server_kwargs={},
*request_args, **request_kwargs): *request_args, **request_kwargs):
results = [None, None] warnings.warn(
exceptions = [] "Use of sanic_endpoint_test will be deprecated in"
"the next major version after 0.4.0. Please use the `test_client` "
"available on the app object.", DeprecationWarning)
if gather_request: test_client = TestClient(app)
def _collect_request(request): return test_client._sanic_endpoint_test(
if results[0] is None: method, uri, gather_request, debug, server_kwargs,
results[0] = request *request_args, **request_kwargs)
app.request_middleware.appendleft(_collect_request)
async def _collect_response(sanic, loop):
try:
response = await local_request(method, uri, *request_args,
**request_kwargs)
results[-1] = response
except Exception as e:
exceptions.append(e)
app.stop()
app.run(host=HOST, debug=debug, port=PORT,
after_start=_collect_response, **server_kwargs)
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))
if gather_request:
try:
request, response = results
return request, response
except:
raise ValueError(
"Request and response object expected, got ({})".format(
results))
else:
try:
return results[-1]
except:
raise ValueError(
"Request object expected, got ({})".format(results))

View File

@@ -1,8 +1,9 @@
from .exceptions import InvalidUsage from sanic.exceptions import InvalidUsage
from sanic.constants import HTTP_METHODS
class HTTPMethodView: class HTTPMethodView:
""" Simple class based implementation of view for the sanic. """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. to every HTTP method you want to support.
@@ -40,17 +41,12 @@ class HTTPMethodView:
def dispatch_request(self, request, *args, **kwargs): def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None) handler = getattr(self, request.method.lower(), None)
if handler: return handler(request, *args, **kwargs)
return handler(request, *args, **kwargs)
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
@classmethod @classmethod
def as_view(cls, *class_args, **class_kwargs): def as_view(cls, *class_args, **class_kwargs):
""" Converts the class into an actual view function that can be used """Return view function for use with the routing system, that
with the routing system. dispatches request to appropriate handler method.
""" """
def view(*args, **kwargs): def view(*args, **kwargs):
self = view.view_class(*class_args, **class_kwargs) self = view.view_class(*class_args, **class_kwargs)
@@ -69,7 +65,7 @@ class HTTPMethodView:
class CompositionView: class CompositionView:
""" Simple method-function mapped view for the sanic. """Simple method-function mapped view for the sanic.
You can add handler functions to methods (get, post, put, patch, delete) You can add handler functions to methods (get, post, put, patch, delete)
for every HTTP method you want to support. for every HTTP method you want to support.
@@ -89,15 +85,15 @@ class CompositionView:
def add(self, methods, handler): def add(self, methods, handler):
for method in methods: for method in methods:
if method not in HTTP_METHODS:
raise InvalidUsage(
'{} is not a valid HTTP method.'.format(method))
if method in self.handlers: if method in self.handlers:
raise KeyError( raise InvalidUsage(
'Method {} already is registered.'.format(method)) 'Method {} is already registered.'.format(method))
self.handlers[method] = handler self.handlers[method] = handler
def __call__(self, request, *args, **kwargs): def __call__(self, request, *args, **kwargs):
handler = self.handlers.get(request.method.upper(), None) handler = self.handlers[request.method.upper()]
if handler is None:
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
return handler(request, *args, **kwargs) return handler(request, *args, **kwargs)

View File

@@ -15,6 +15,15 @@ with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
except IndexError: except IndexError:
raise RuntimeError('Unable to determine version.') raise RuntimeError('Unable to determine version.')
install_requires = [
'httptools>=0.0.9',
'ujson>=1.35',
'aiofiles>=0.3.0',
]
if os.name != 'nt':
install_requires.append('uvloop>=0.5.3')
setup( setup(
name='sanic', name='sanic',
version=version, version=version,
@@ -22,15 +31,11 @@ setup(
license='MIT', license='MIT',
author='Channel Cat', author='Channel Cat',
author_email='channelcat@gmail.com', author_email='channelcat@gmail.com',
description='A microframework based on uvloop, httptools, and learnings of flask', description=(
'A microframework based on uvloop, httptools, and learnings of flask'),
packages=['sanic'], packages=['sanic'],
platforms='any', platforms='any',
install_requires=[ install_requires=install_requires,
'uvloop>=0.5.3',
'httptools>=0.0.9',
'ujson>=1.35',
'aiofiles>=0.3.0',
],
classifiers=[ classifiers=[
'Development Status :: 2 - Pre-Alpha', 'Development Status :: 2 - Pre-Alpha',
'Environment :: Web Environment', 'Environment :: Web Environment',

View File

@@ -1 +1 @@
I need to be decoded as a uri I am just a regular static file that needs to have its uri decoded

View File

@@ -5,6 +5,7 @@ from sanic import Sanic
def test_bad_request_response(): def test_bad_request_response():
app = Sanic('test_bad_request_response') app = Sanic('test_bad_request_response')
lines = [] lines = []
@app.listener('after_server_start')
async def _request(sanic, loop): 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 reader, writer = await connect
@@ -15,6 +16,6 @@ def test_bad_request_response():
break break
lines.append(line) lines.append(line)
app.stop() app.stop()
app.run(host='127.0.0.1', port=42101, debug=False, after_start=_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[0] == b'HTTP/1.1 400 Bad Request\r\n'
assert lines[-1] == b'Error: Bad Request' assert lines[-1] == b'Error: Bad Request'

View File

@@ -3,7 +3,6 @@ import inspect
from sanic import Sanic from sanic import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.response import json, text from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
from sanic.exceptions import NotFound, ServerError, InvalidUsage from sanic.exceptions import NotFound, ServerError, InvalidUsage
@@ -20,7 +19,7 @@ def test_bp():
return text('Hello') return text('Hello')
app.blueprint(bp) app.blueprint(bp)
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.text == 'Hello' assert response.text == 'Hello'
@@ -33,7 +32,7 @@ def test_bp_with_url_prefix():
return text('Hello') return text('Hello')
app.blueprint(bp) app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/') request, response = app.test_client.get('/test1/')
assert response.text == 'Hello' assert response.text == 'Hello'
@@ -53,10 +52,10 @@ def test_several_bp_with_url_prefix():
app.blueprint(bp) app.blueprint(bp)
app.blueprint(bp2) app.blueprint(bp2)
request, response = sanic_endpoint_test(app, uri='/test1/') request, response = app.test_client.get('/test1/')
assert response.text == 'Hello' assert response.text == 'Hello'
request, response = sanic_endpoint_test(app, uri='/test2/') request, response = app.test_client.get('/test2/')
assert response.text == 'Hello2' assert response.text == 'Hello2'
def test_bp_with_host(): def test_bp_with_host():
@@ -73,13 +72,15 @@ def test_bp_with_host():
app.blueprint(bp) app.blueprint(bp)
headers = {"Host": "example.com"} headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, uri='/test1/', request, response = app.test_client.get(
headers=headers) '/test1/',
headers=headers)
assert response.text == 'Hello' assert response.text == 'Hello'
headers = {"Host": "sub.example.com"} headers = {"Host": "sub.example.com"}
request, response = sanic_endpoint_test(app, uri='/test1/', request, response = app.test_client.get(
headers=headers) '/test1/',
headers=headers)
assert response.text == 'Hello subdomain!' assert response.text == 'Hello subdomain!'
@@ -111,18 +112,21 @@ def test_several_bp_with_host():
assert bp.host == "example.com" assert bp.host == "example.com"
headers = {"Host": "example.com"} headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, uri='/test/', request, response = app.test_client.get(
headers=headers) '/test/',
headers=headers)
assert response.text == 'Hello' assert response.text == 'Hello'
assert bp2.host == "sub.example.com" assert bp2.host == "sub.example.com"
headers = {"Host": "sub.example.com"} headers = {"Host": "sub.example.com"}
request, response = sanic_endpoint_test(app, uri='/test/', request, response = app.test_client.get(
headers=headers) '/test/',
headers=headers)
assert response.text == 'Hello2' assert response.text == 'Hello2'
request, response = sanic_endpoint_test(app, uri='/test/other/', request, response = app.test_client.get(
headers=headers) '/test/other/',
headers=headers)
assert response.text == 'Hello3' assert response.text == 'Hello3'
def test_bp_middleware(): def test_bp_middleware():
@@ -139,7 +143,7 @@ def test_bp_middleware():
app.blueprint(blueprint) app.blueprint(blueprint)
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
@@ -166,15 +170,15 @@ def test_bp_exception_handler():
app.blueprint(blueprint) app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/1') request, response = app.test_client.get('/1')
assert response.status == 400 assert response.status == 400
request, response = sanic_endpoint_test(app, uri='/2') request, response = app.test_client.get('/2')
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/3') request, response = app.test_client.get('/3')
assert response.status == 200 assert response.status == 200
def test_bp_listeners(): def test_bp_listeners():
@@ -209,7 +213,7 @@ def test_bp_listeners():
app.blueprint(blueprint) app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/') request, response = app.test_client.get('/')
assert order == [1,2,3,4,5,6] assert order == [1,2,3,4,5,6]
@@ -225,7 +229,7 @@ def test_bp_static():
app.blueprint(blueprint) app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/testing.file') request, response = app.test_client.get('/testing.file')
assert response.status == 200 assert response.status == 200
assert response.body == current_file_contents assert response.body == current_file_contents
@@ -263,44 +267,44 @@ def test_bp_shorthand():
app.blueprint(blueprint) app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/get', method='get') request, response = app.test_client.get('/get')
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/get', method='post') request, response = app.test_client.post('/get')
assert response.status == 405 assert response.status == 405
request, response = sanic_endpoint_test(app, uri='/put', method='put') request, response = app.test_client.put('/put')
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/put', method='get') request, response = app.test_client.get('/post')
assert response.status == 405 assert response.status == 405
request, response = sanic_endpoint_test(app, uri='/post', method='post') request, response = app.test_client.post('/post')
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/post', method='get') request, response = app.test_client.get('/post')
assert response.status == 405 assert response.status == 405
request, response = sanic_endpoint_test(app, uri='/head', method='head') request, response = app.test_client.head('/head')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/head', method='get') request, response = app.test_client.get('/head')
assert response.status == 405 assert response.status == 405
request, response = sanic_endpoint_test(app, uri='/options', method='options') request, response = app.test_client.options('/options')
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/options', method='get') request, response = app.test_client.get('/options')
assert response.status == 405 assert response.status == 405
request, response = sanic_endpoint_test(app, uri='/patch', method='patch') request, response = app.test_client.patch('/patch')
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/patch', method='get') request, response = app.test_client.get('/patch')
assert response.status == 405 assert response.status == 405
request, response = sanic_endpoint_test(app, uri='/delete', method='delete') request, response = app.test_client.delete('/delete')
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/delete', method='get') request, response = app.test_client.get('/delete')
assert response.status == 405 assert response.status == 405

View File

@@ -2,7 +2,6 @@ from datetime import datetime, timedelta
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from sanic import Sanic from sanic import Sanic
from sanic.response import json, text from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
import pytest import pytest
@@ -19,7 +18,7 @@ def test_cookies():
response.cookies['right_back'] = 'at you' response.cookies['right_back'] = 'at you'
return response return response
request, response = sanic_endpoint_test(app, cookies={"test": "working!"}) request, response = app.test_client.get('/', cookies={"test": "working!"})
response_cookies = SimpleCookie() response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {})) response_cookies.load(response.headers.get('Set-Cookie', {}))
@@ -40,7 +39,7 @@ def test_false_cookies(httponly, expected):
response.cookies['right_back']['httponly'] = httponly response.cookies['right_back']['httponly'] = httponly
return response return response
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
response_cookies = SimpleCookie() response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {})) response_cookies.load(response.headers.get('Set-Cookie', {}))
@@ -55,7 +54,7 @@ def test_http2_cookies():
return response return response
headers = {'cookie': 'test=working!'} headers = {'cookie': 'test=working!'}
request, response = sanic_endpoint_test(app, headers=headers) request, response = app.test_client.get('/', headers=headers)
assert response.text == 'Cookies are: working!' assert response.text == 'Cookies are: working!'
@@ -70,7 +69,7 @@ def test_cookie_options():
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10) response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
return response return response
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
response_cookies = SimpleCookie() response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {})) response_cookies.load(response.headers.get('Set-Cookie', {}))
@@ -88,7 +87,7 @@ def test_cookie_deletion():
del response.cookies['i_never_existed'] del response.cookies['i_never_existed']
return response return response
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
response_cookies = SimpleCookie() response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {})) response_cookies.load(response.headers.get('Set-Cookie', {}))

30
tests/test_create_task.py Normal file
View File

@@ -0,0 +1,30 @@
import sanic
from sanic.utils import sanic_endpoint_test
from sanic.response import text
from threading import Event
import asyncio
def test_create_task():
e = Event()
async def coro():
await asyncio.sleep(0.05)
e.set()
app = sanic.Sanic()
app.add_task(coro)
@app.route('/early')
def not_set(request):
return text(e.is_set())
@app.route('/late')
async def set(request):
await asyncio.sleep(0.1)
return text(e.is_set())
request, response = sanic_endpoint_test(app, uri='/early')
assert response.body == b'False'
request, response = sanic_endpoint_test(app, uri='/late')
assert response.body == b'True'

View File

@@ -1,7 +1,6 @@
from sanic import Sanic from sanic import Sanic
from sanic.server import HttpProtocol from sanic.server import HttpProtocol
from sanic.response import text from sanic.response import text
from sanic.utils import sanic_endpoint_test
app = Sanic('test_custom_porotocol') app = Sanic('test_custom_porotocol')
@@ -26,7 +25,7 @@ def test_use_custom_protocol():
server_kwargs = { server_kwargs = {
'protocol': CustomHttpProtocol 'protocol': CustomHttpProtocol
} }
request, response = sanic_endpoint_test(app, uri='/1', request, response = app.test_client.get(
server_kwargs=server_kwargs) '/1', server_kwargs=server_kwargs)
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'

View File

@@ -1,6 +1,5 @@
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.utils import sanic_endpoint_test
from sanic.router import RouteExists from sanic.router import RouteExists
import pytest import pytest
@@ -22,8 +21,7 @@ def test_overload_dynamic_routes(method, attr, expected):
async def handler2(request, param): async def handler2(request, param):
return text('OK2 ' + param) return text('OK2 ' + param)
request, response = sanic_endpoint_test( request, response = getattr(app.test_client, method)('/overload/test')
app, method, uri='/overload/test')
assert getattr(response, attr) == expected assert getattr(response, attr) == expected

View File

@@ -4,7 +4,6 @@ from bs4 import BeautifulSoup
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.utils import sanic_endpoint_test
class SanicExceptionTestException(Exception): class SanicExceptionTestException(Exception):
@@ -48,33 +47,32 @@ def exception_app():
def test_no_exception(exception_app): def test_no_exception(exception_app):
"""Test that a route works without an exception""" """Test that a route works without an exception"""
request, response = sanic_endpoint_test(exception_app) request, response = exception_app.test_client.get('/')
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
def test_server_error_exception(exception_app): def test_server_error_exception(exception_app):
"""Test the built-in ServerError exception works""" """Test the built-in ServerError exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/error') request, response = exception_app.test_client.get('/error')
assert response.status == 500 assert response.status == 500
def test_invalid_usage_exception(exception_app): def test_invalid_usage_exception(exception_app):
"""Test the built-in InvalidUsage exception works""" """Test the built-in InvalidUsage exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/invalid') request, response = exception_app.test_client.get('/invalid')
assert response.status == 400 assert response.status == 400
def test_not_found_exception(exception_app): def test_not_found_exception(exception_app):
"""Test the built-in NotFound exception works""" """Test the built-in NotFound exception works"""
request, response = sanic_endpoint_test(exception_app, uri='/404') request, response = exception_app.test_client.get('/404')
assert response.status == 404 assert response.status == 404
def test_handled_unhandled_exception(exception_app): def test_handled_unhandled_exception(exception_app):
"""Test that an exception not built into sanic is handled""" """Test that an exception not built into sanic is handled"""
request, response = sanic_endpoint_test( request, response = exception_app.test_client.get('/divide_by_zero')
exception_app, uri='/divide_by_zero')
assert response.status == 500 assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser') soup = BeautifulSoup(response.body, 'html.parser')
assert soup.h1.text == 'Internal Server Error' assert soup.h1.text == 'Internal Server Error'
@@ -86,17 +84,16 @@ def test_handled_unhandled_exception(exception_app):
def test_exception_in_exception_handler(exception_app): def test_exception_in_exception_handler(exception_app):
"""Test that an exception thrown in an error handler is handled""" """Test that an exception thrown in an error handler is handled"""
request, response = sanic_endpoint_test( request, response = exception_app.test_client.get(
exception_app, uri='/error_in_error_handler_handler') '/error_in_error_handler_handler')
assert response.status == 500 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): def test_exception_in_exception_handler_debug_off(exception_app):
"""Test that an exception thrown in an error handler is handled""" """Test that an exception thrown in an error handler is handled"""
request, response = sanic_endpoint_test( request, response = exception_app.test_client.get(
exception_app, '/error_in_error_handler_handler',
uri='/error_in_error_handler_handler',
debug=False) debug=False)
assert response.status == 500 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'
@@ -104,9 +101,8 @@ def test_exception_in_exception_handler_debug_off(exception_app):
def test_exception_in_exception_handler_debug_off(exception_app): def test_exception_in_exception_handler_debug_off(exception_app):
"""Test that an exception thrown in an error handler is handled""" """Test that an exception thrown in an error handler is handled"""
request, response = sanic_endpoint_test( request, response = exception_app.test_client.get(
exception_app, '/error_in_error_handler_handler',
uri='/error_in_error_handler_handler',
debug=True) debug=True)
assert response.status == 500 assert response.status == 500
assert response.body.startswith(b'Exception raised in exception ') assert response.body.startswith(b'Exception raised in exception ')

View File

@@ -1,7 +1,6 @@
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.exceptions import InvalidUsage, ServerError, NotFound
from sanic.utils import sanic_endpoint_test
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
exception_handler_app = Sanic('test_exception_handler') exception_handler_app = Sanic('test_exception_handler')
@@ -31,7 +30,7 @@ def handler_4(request):
@exception_handler_app.route('/5') @exception_handler_app.route('/5')
def handler_5(request): def handler_5(request):
class CustomServerError(ServerError): class CustomServerError(ServerError):
pass status_code=200
raise CustomServerError('Custom server error') raise CustomServerError('Custom server error')
@@ -41,31 +40,30 @@ def handler_exception(request, exception):
def test_invalid_usage_exception_handler(): def test_invalid_usage_exception_handler():
request, response = sanic_endpoint_test(exception_handler_app, uri='/1') request, response = exception_handler_app.test_client.get('/1')
assert response.status == 400 assert response.status == 400
def test_server_error_exception_handler(): def test_server_error_exception_handler():
request, response = sanic_endpoint_test(exception_handler_app, uri='/2') request, response = exception_handler_app.test_client.get('/2')
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
def test_not_found_exception_handler(): def test_not_found_exception_handler():
request, response = sanic_endpoint_test(exception_handler_app, uri='/3') request, response = exception_handler_app.test_client.get('/3')
assert response.status == 200 assert response.status == 200
def test_text_exception__handler(): def test_text_exception__handler():
request, response = sanic_endpoint_test( request, response = exception_handler_app.test_client.get('/random')
exception_handler_app, uri='/random')
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
def test_html_traceback_output_in_debug_mode(): def test_html_traceback_output_in_debug_mode():
request, response = sanic_endpoint_test( request, response = exception_handler_app.test_client.get(
exception_handler_app, uri='/4', debug=True) '/4', debug=True)
assert response.status == 500 assert response.status == 500
soup = BeautifulSoup(response.body, 'html.parser') soup = BeautifulSoup(response.body, 'html.parser')
html = str(soup) html = str(soup)
@@ -81,5 +79,5 @@ def test_html_traceback_output_in_debug_mode():
def test_inherited_exception_handler(): def test_inherited_exception_handler():
request, response = sanic_endpoint_test(exception_handler_app, uri='/5') request, response = exception_handler_app.test_client.get('/5')
assert response.status == 200 assert response.status == 200

View File

@@ -3,7 +3,6 @@ import uuid
from sanic.response import text from sanic.response import text
from sanic import Sanic from sanic import Sanic
from io import StringIO from io import StringIO
from sanic.utils import sanic_endpoint_test
import logging import logging
logging_format = '''module: %(module)s; \ logging_format = '''module: %(module)s; \
@@ -29,7 +28,7 @@ def test_log():
log.info(rand_string) log.info(rand_string)
return text('hello') return text('hello')
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
log_text = log_stream.getvalue() log_text = log_stream.getvalue()
assert rand_string in log_text assert rand_string in log_text

View File

@@ -2,7 +2,6 @@ from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic from sanic import Sanic
from sanic.request import Request from sanic.request import Request
from sanic.response import json, text, HTTPResponse from sanic.response import json, text, HTTPResponse
from sanic.utils import sanic_endpoint_test
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@@ -22,7 +21,7 @@ def test_middleware_request():
async def handler(request): async def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.text == 'OK' assert response.text == 'OK'
assert type(results[0]) is Request assert type(results[0]) is Request
@@ -46,7 +45,7 @@ def test_middleware_response():
async def handler(request): async def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.text == 'OK' assert response.text == 'OK'
assert type(results[0]) is Request assert type(results[0]) is Request
@@ -65,7 +64,7 @@ def test_middleware_override_request():
async def handler(request): async def handler(request):
return text('FAIL') return text('FAIL')
response = sanic_endpoint_test(app, gather_request=False) response = app.test_client.get('/', gather_request=False)
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
@@ -82,7 +81,7 @@ def test_middleware_override_response():
async def handler(request): async def handler(request):
return text('FAIL') return text('FAIL')
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'
@@ -122,7 +121,7 @@ def test_middleware_order():
async def handler(request): async def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.status == 200 assert response.status == 200
assert order == [1,2,3,4,5,6] assert order == [1,2,3,4,5,6]

View File

@@ -3,7 +3,7 @@ import random
import signal import signal
from sanic import Sanic from sanic import Sanic
from sanic.utils import HOST, PORT from sanic.testing import HOST, PORT
def test_multiprocessing(): def test_multiprocessing():

View File

@@ -1,7 +1,6 @@
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.exceptions import PayloadTooLarge from sanic.exceptions import PayloadTooLarge
from sanic.utils import sanic_endpoint_test
data_received_app = Sanic('data_received') data_received_app = Sanic('data_received')
data_received_app.config.REQUEST_MAX_SIZE = 1 data_received_app.config.REQUEST_MAX_SIZE = 1
@@ -22,8 +21,7 @@ def handler_exception(request, exception):
def test_payload_too_large_from_error_handler(): def test_payload_too_large_from_error_handler():
response = sanic_endpoint_test( response = data_received_app.test_client.get('/1', gather_request=False)
data_received_app, uri='/1', gather_request=False)
assert response.status == 413 assert response.status == 413
assert response.text == 'Payload Too Large from error_handler.' assert response.text == 'Payload Too Large from error_handler.'
@@ -34,8 +32,8 @@ async def handler2(request):
def test_payload_too_large_at_data_received_default(): def test_payload_too_large_at_data_received_default():
response = sanic_endpoint_test( response = data_received_default_app.test_client.get(
data_received_default_app, uri='/1', gather_request=False) '/1', gather_request=False)
assert response.status == 413 assert response.status == 413
assert response.text == 'Error: Payload Too Large' assert response.text == 'Error: Payload Too Large'
@@ -47,8 +45,7 @@ async def handler3(request):
def test_payload_too_large_at_on_header_default(): def test_payload_too_large_at_on_header_default():
data = 'a' * 1000 data = 'a' * 1000
response = sanic_endpoint_test( response = on_header_default_app.test_client.post(
on_header_default_app, method='post', uri='/1', '/1', gather_request=False, data=data)
gather_request=False, data=data)
assert response.status == 413 assert response.status == 413
assert response.text == 'Error: Payload Too Large' assert response.text == 'Error: Payload Too Large'

View File

@@ -2,7 +2,6 @@ import pytest
from sanic import Sanic from sanic import Sanic
from sanic.response import text, redirect from sanic.response import text, redirect
from sanic.utils import sanic_endpoint_test
@pytest.fixture @pytest.fixture
@@ -40,9 +39,8 @@ def test_redirect_default_302(redirect_app):
""" """
We expect a 302 default status code and the headers to be set. We expect a 302 default status code and the headers to be set.
""" """
request, response = sanic_endpoint_test( request, response = redirect_app.test_client.get(
redirect_app, method="get", '/redirect_init',
uri="/redirect_init",
allow_redirects=False) allow_redirects=False)
assert response.status == 302 assert response.status == 302
@@ -51,8 +49,7 @@ def test_redirect_default_302(redirect_app):
def test_redirect_headers_none(redirect_app): def test_redirect_headers_none(redirect_app):
request, response = sanic_endpoint_test( request, response = redirect_app.test_client.get(
redirect_app, method="get",
uri="/redirect_init", uri="/redirect_init",
headers=None, headers=None,
allow_redirects=False) allow_redirects=False)
@@ -65,9 +62,8 @@ def test_redirect_with_301(redirect_app):
""" """
Test redirection with a different status code. Test redirection with a different status code.
""" """
request, response = sanic_endpoint_test( request, response = redirect_app.test_client.get(
redirect_app, method="get", "/redirect_init_with_301",
uri="/redirect_init_with_301",
allow_redirects=False) allow_redirects=False)
assert response.status == 301 assert response.status == 301
@@ -78,9 +74,8 @@ def test_get_then_redirect_follow_redirect(redirect_app):
""" """
With `allow_redirects` we expect a 200. With `allow_redirects` we expect a 200.
""" """
response = sanic_endpoint_test( request, response = redirect_app.test_client.get(
redirect_app, method="get", "/redirect_init",
uri="/redirect_init", gather_request=False,
allow_redirects=True) allow_redirects=True)
assert response.status == 200 assert response.status == 200
@@ -88,8 +83,8 @@ def test_get_then_redirect_follow_redirect(redirect_app):
def test_chained_redirect(redirect_app): def test_chained_redirect(redirect_app):
"""Test sanic_endpoint_test is working for redirection""" """Test test_client is working for redirection"""
request, response = sanic_endpoint_test(redirect_app, uri='/1') request, response = redirect_app.test_client.get('/1')
assert request.url.endswith('/1') assert request.url.endswith('/1')
assert response.status == 200 assert response.status == 200
assert response.text == 'OK' assert response.text == 'OK'

View File

@@ -1,6 +1,7 @@
import random
from sanic import Sanic from sanic import Sanic
from sanic.response import json from sanic.response import json
from sanic.utils import sanic_endpoint_test
from ujson import loads from ujson import loads
@@ -15,10 +16,28 @@ def test_storage():
@app.route('/') @app.route('/')
def handler(request): 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 = sanic_endpoint_test(app) request, response = app.test_client.get('/')
response_json = loads(response.text) response_json = loads(response.text)
assert response_json['user'] == 'sanic' assert response_json['user'] == 'sanic'
assert response_json.get('sidekick') is None assert response_json.get('sidekick') is None
def test_app_injection():
app = Sanic('test_app_injection')
expected = random.choice(range(0, 100))
@app.listener('after_server_start')
async def inject_data(app, loop):
app.injected = expected
@app.get('/')
async def handler(request):
return json({'injected': request.app.injected})
request, response = app.test_client.get('/')
response_json = loads(response.text)
assert response_json['injected'] == expected

View File

@@ -2,7 +2,6 @@ from sanic import Sanic
import asyncio import asyncio
from sanic.response import text from sanic.response import text
from sanic.exceptions import RequestTimeout from sanic.exceptions import RequestTimeout
from sanic.utils import sanic_endpoint_test
from sanic.config import Config from sanic.config import Config
Config.REQUEST_TIMEOUT = 1 Config.REQUEST_TIMEOUT = 1
@@ -22,7 +21,7 @@ def handler_exception(request, exception):
def test_server_error_request_timeout(): def test_server_error_request_timeout():
request, response = sanic_endpoint_test(request_timeout_app, uri='/1') request, response = request_timeout_app.test_client.get('/1')
assert response.status == 408 assert response.status == 408
assert response.text == 'Request Timeout from error_handler.' assert response.text == 'Request Timeout from error_handler.'
@@ -34,7 +33,6 @@ async def handler_2(request):
def test_default_server_error_request_timeout(): def test_default_server_error_request_timeout():
request, response = sanic_endpoint_test( request, response = request_timeout_default_app.test_client.get('/1')
request_timeout_default_app, uri='/1')
assert response.status == 408 assert response.status == 408
assert response.text == 'Error: Request Timeout' assert response.text == 'Error: Request Timeout'

View File

@@ -1,11 +1,12 @@
from json import loads as json_loads, dumps as json_dumps from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic
from sanic.response import json, text, redirect
from sanic.utils import sanic_endpoint_test
from sanic.exceptions import ServerError
import pytest import pytest
from sanic import Sanic
from sanic.exceptions import ServerError
from sanic.response import json, text, redirect
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
# GET # GET
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@@ -17,7 +18,7 @@ def test_sync():
def handler(request): def handler(request):
return text('Hello') return text('Hello')
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.text == 'Hello' assert response.text == 'Hello'
@@ -29,7 +30,7 @@ def test_text():
async def handler(request): async def handler(request):
return text('Hello') return text('Hello')
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.text == 'Hello' assert response.text == 'Hello'
@@ -42,7 +43,7 @@ def test_headers():
headers = {"spam": "great"} headers = {"spam": "great"}
return text('Hello', headers=headers) return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.headers.get('spam') == 'great' assert response.headers.get('spam') == 'great'
@@ -55,7 +56,7 @@ def test_non_str_headers():
headers = {"answer": 42} headers = {"answer": 42}
return text('Hello', headers=headers) return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.headers.get('answer') == '42' assert response.headers.get('answer') == '42'
@@ -70,7 +71,7 @@ def test_invalid_response():
async def handler(request): async def handler(request):
return 'This should fail' return 'This should fail'
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.status == 500 assert response.status == 500
assert response.text == "Internal Server Error." assert response.text == "Internal Server Error."
@@ -82,7 +83,7 @@ def test_json():
async def handler(request): async def handler(request):
return json({"test": True}) return json({"test": True})
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
try: try:
results = json_loads(response.text) results = json_loads(response.text)
@@ -100,7 +101,7 @@ def test_invalid_json():
return json(request.json()) return json(request.json())
data = "I am not json" data = "I am not json"
request, response = sanic_endpoint_test(app, data=data) request, response = app.test_client.get('/', data=data)
assert response.status == 400 assert response.status == 400
@@ -112,7 +113,8 @@ def test_query_string():
async def handler(request): async def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")]) request, response = app.test_client.get(
'/', params=[("test1", "1"), ("test2", "false"), ("test2", "true")])
assert request.args.get('test1') == '1' assert request.args.get('test1') == '1'
assert request.args.get('test2') == 'false' assert request.args.get('test2') == 'false'
@@ -132,7 +134,7 @@ def test_token():
'Authorization': 'Token {}'.format(token) 'Authorization': 'Token {}'.format(token)
} }
request, response = sanic_endpoint_test(app, headers=headers) request, response = app.test_client.get('/', headers=headers)
assert request.token == token assert request.token == token
@@ -143,14 +145,15 @@ def test_token():
def test_post_json(): def test_post_json():
app = Sanic('test_post_json') app = Sanic('test_post_json')
@app.route('/') @app.route('/', methods=['POST'])
async def handler(request): async def handler(request):
return text('OK') return text('OK')
payload = {'test': 'OK'} payload = {'test': 'OK'}
headers = {'content-type': 'application/json'} headers = {'content-type': 'application/json'}
request, response = sanic_endpoint_test(app, data=json_dumps(payload), headers=headers) request, response = app.test_client.post(
'/', data=json_dumps(payload), headers=headers)
assert request.json.get('test') == 'OK' assert request.json.get('test') == 'OK'
assert response.text == 'OK' assert response.text == 'OK'
@@ -159,14 +162,14 @@ def test_post_json():
def test_post_form_urlencoded(): def test_post_form_urlencoded():
app = Sanic('test_post_form_urlencoded') app = Sanic('test_post_form_urlencoded')
@app.route('/') @app.route('/', methods=['POST'])
async def handler(request): async def handler(request):
return text('OK') return text('OK')
payload = 'test=OK' payload = 'test=OK'
headers = {'content-type': 'application/x-www-form-urlencoded'} headers = {'content-type': 'application/x-www-form-urlencoded'}
request, response = sanic_endpoint_test(app, data=payload, headers=headers) request, response = app.test_client.post('/', data=payload, headers=headers)
assert request.form.get('test') == 'OK' assert request.form.get('test') == 'OK'
@@ -174,7 +177,7 @@ def test_post_form_urlencoded():
def test_post_form_multipart_form_data(): def test_post_form_multipart_form_data():
app = Sanic('test_post_form_multipart_form_data') app = Sanic('test_post_form_multipart_form_data')
@app.route('/') @app.route('/', methods=['POST'])
async def handler(request): async def handler(request):
return text('OK') return text('OK')
@@ -186,6 +189,6 @@ def test_post_form_multipart_form_data():
headers = {'content-type': 'multipart/form-data; boundary=----sanic'} headers = {'content-type': 'multipart/form-data; boundary=----sanic'}
request, response = sanic_endpoint_test(app, data=payload, headers=headers) request, response = app.test_client.post(data=payload, headers=headers)
assert request.form.get('test') == 'OK' assert request.form.get('test') == 'OK'

View File

@@ -2,7 +2,6 @@ from random import choice
from sanic import Sanic from sanic import Sanic
from sanic.response import HTTPResponse from sanic.response import HTTPResponse
from sanic.utils import sanic_endpoint_test
def test_response_body_not_a_string(): def test_response_body_not_a_string():
@@ -14,5 +13,5 @@ def test_response_body_not_a_string():
async def hello_route(request): async def hello_route(request):
return HTTPResponse(body=random_num) return HTTPResponse(body=random_num)
request, response = sanic_endpoint_test(app, uri='/hello') request, response = app.test_client.get('/hello')
assert response.text == str(random_num) assert response.text == str(random_num)

View File

@@ -3,7 +3,6 @@ import pytest
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.router import RouteExists, RouteDoesNotExist from sanic.router import RouteExists, RouteDoesNotExist
from sanic.utils import sanic_endpoint_test
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@@ -17,12 +16,25 @@ def test_shorthand_routes_get():
def handler(request): def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/get', method='get') request, response = app.test_client.get('/get')
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/get', method='post') request, response = app.test_client.post('/get')
assert response.status == 405 assert response.status == 405
def test_route_optional_slash():
app = Sanic('test_route_optional_slash')
@app.get('/get')
def handler(request):
return text('OK')
request, response = app.test_client.get('/get')
assert response.text == 'OK'
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
def test_shorthand_routes_post(): def test_shorthand_routes_post():
app = Sanic('test_shorhand_routes_post') app = Sanic('test_shorhand_routes_post')
@@ -30,10 +42,10 @@ def test_shorthand_routes_post():
def handler(request): def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/post', method='post') request, response = app.test_client.post('/post')
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/post', method='get') request, response = app.test_client.get('/post')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_put(): def test_shorthand_routes_put():
@@ -43,10 +55,10 @@ def test_shorthand_routes_put():
def handler(request): def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/put', method='put') request, response = app.test_client.put('/put')
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/put', method='get') request, response = app.test_client.get('/put')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_patch(): def test_shorthand_routes_patch():
@@ -56,10 +68,10 @@ def test_shorthand_routes_patch():
def handler(request): def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/patch', method='patch') request, response = app.test_client.patch('/patch')
assert response.text == 'OK' assert response.text == 'OK'
request, response = sanic_endpoint_test(app, uri='/patch', method='get') request, response = app.test_client.get('/patch')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_head(): def test_shorthand_routes_head():
@@ -69,10 +81,10 @@ def test_shorthand_routes_head():
def handler(request): def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/head', method='head') request, response = app.test_client.head('/head')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/head', method='get') request, response = app.test_client.get('/head')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_options(): def test_shorthand_routes_options():
@@ -82,10 +94,10 @@ def test_shorthand_routes_options():
def handler(request): def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/options', method='options') request, response = app.test_client.options('/options')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/options', method='get') request, response = app.test_client.get('/options')
assert response.status == 405 assert response.status == 405
def test_static_routes(): def test_static_routes():
@@ -99,10 +111,10 @@ def test_static_routes():
async def handler2(request): async def handler2(request):
return text('OK2') return text('OK2')
request, response = sanic_endpoint_test(app, uri='/test') request, response = app.test_client.get('/test')
assert response.text == 'OK1' assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, uri='/pizazz') request, response = app.test_client.get('/pizazz')
assert response.text == 'OK2' assert response.text == 'OK2'
@@ -116,7 +128,7 @@ def test_dynamic_route():
results.append(name) results.append(name)
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/folder/test123') request, response = app.test_client.get('/folder/test123')
assert response.text == 'OK' assert response.text == 'OK'
assert results[0] == 'test123' assert results[0] == 'test123'
@@ -132,12 +144,12 @@ def test_dynamic_route_string():
results.append(name) results.append(name)
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/folder/test123') request, response = app.test_client.get('/folder/test123')
assert response.text == 'OK' assert response.text == 'OK'
assert results[0] == 'test123' assert results[0] == 'test123'
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico') request, response = app.test_client.get('/folder/favicon.ico')
assert response.text == 'OK' assert response.text == 'OK'
assert results[1] == 'favicon.ico' assert results[1] == 'favicon.ico'
@@ -153,11 +165,11 @@ def test_dynamic_route_int():
results.append(folder_id) results.append(folder_id)
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/folder/12345') request, response = app.test_client.get('/folder/12345')
assert response.text == 'OK' assert response.text == 'OK'
assert type(results[0]) is int assert type(results[0]) is int
request, response = sanic_endpoint_test(app, uri='/folder/asdf') request, response = app.test_client.get('/folder/asdf')
assert response.status == 404 assert response.status == 404
@@ -171,14 +183,14 @@ def test_dynamic_route_number():
results.append(weight) results.append(weight)
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/weight/12345') request, response = app.test_client.get('/weight/12345')
assert response.text == 'OK' assert response.text == 'OK'
assert type(results[0]) is float assert type(results[0]) is float
request, response = sanic_endpoint_test(app, uri='/weight/1234.56') request, response = app.test_client.get('/weight/1234.56')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/weight/1234-56') request, response = app.test_client.get('/weight/1234-56')
assert response.status == 404 assert response.status == 404
@@ -189,16 +201,16 @@ def test_dynamic_route_regex():
async def handler(request, folder_id): async def handler(request, folder_id):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/folder/test') request, response = app.test_client.get('/folder/test')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test1') request, response = app.test_client.get('/folder/test1')
assert response.status == 404 assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test-123') request, response = app.test_client.get('/folder/test-123')
assert response.status == 404 assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/') request, response = app.test_client.get('/folder/')
assert response.status == 200 assert response.status == 200
@@ -209,16 +221,16 @@ def test_dynamic_route_unhashable():
async def handler(request, unhashable): async def handler(request, unhashable):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') request, response = app.test_client.get('/folder/test/asdf/end/')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') request, response = app.test_client.get('/folder/test///////end/')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/end/') request, response = app.test_client.get('/folder/test/end/')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/') request, response = app.test_client.get('/folder/test/nope/')
assert response.status == 404 assert response.status == 404
@@ -251,10 +263,10 @@ def test_method_not_allowed():
async def handler(request): async def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, uri='/test') request, response = app.test_client.get('/test')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, method='post', uri='/test') request, response = app.test_client.post('/test')
assert response.status == 405 assert response.status == 405
@@ -270,10 +282,10 @@ def test_static_add_route():
app.add_route(handler1, '/test') app.add_route(handler1, '/test')
app.add_route(handler2, '/test2') app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test') request, response = app.test_client.get('/test')
assert response.text == 'OK1' assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, uri='/test2') request, response = app.test_client.get('/test2')
assert response.text == 'OK2' assert response.text == 'OK2'
@@ -287,7 +299,7 @@ def test_dynamic_add_route():
return text('OK') return text('OK')
app.add_route(handler, '/folder/<name>') app.add_route(handler, '/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123') request, response = app.test_client.get('/folder/test123')
assert response.text == 'OK' assert response.text == 'OK'
assert results[0] == 'test123' assert results[0] == 'test123'
@@ -303,12 +315,12 @@ def test_dynamic_add_route_string():
return text('OK') return text('OK')
app.add_route(handler, '/folder/<name:string>') app.add_route(handler, '/folder/<name:string>')
request, response = sanic_endpoint_test(app, uri='/folder/test123') request, response = app.test_client.get('/folder/test123')
assert response.text == 'OK' assert response.text == 'OK'
assert results[0] == 'test123' assert results[0] == 'test123'
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico') request, response = app.test_client.get('/folder/favicon.ico')
assert response.text == 'OK' assert response.text == 'OK'
assert results[1] == 'favicon.ico' assert results[1] == 'favicon.ico'
@@ -325,11 +337,11 @@ def test_dynamic_add_route_int():
app.add_route(handler, '/folder/<folder_id:int>') app.add_route(handler, '/folder/<folder_id:int>')
request, response = sanic_endpoint_test(app, uri='/folder/12345') request, response = app.test_client.get('/folder/12345')
assert response.text == 'OK' assert response.text == 'OK'
assert type(results[0]) is int assert type(results[0]) is int
request, response = sanic_endpoint_test(app, uri='/folder/asdf') request, response = app.test_client.get('/folder/asdf')
assert response.status == 404 assert response.status == 404
@@ -344,14 +356,14 @@ def test_dynamic_add_route_number():
app.add_route(handler, '/weight/<weight:number>') app.add_route(handler, '/weight/<weight:number>')
request, response = sanic_endpoint_test(app, uri='/weight/12345') request, response = app.test_client.get('/weight/12345')
assert response.text == 'OK' assert response.text == 'OK'
assert type(results[0]) is float assert type(results[0]) is float
request, response = sanic_endpoint_test(app, uri='/weight/1234.56') request, response = app.test_client.get('/weight/1234.56')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/weight/1234-56') request, response = app.test_client.get('/weight/1234-56')
assert response.status == 404 assert response.status == 404
@@ -363,16 +375,16 @@ def test_dynamic_add_route_regex():
app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>') app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>')
request, response = sanic_endpoint_test(app, uri='/folder/test') request, response = app.test_client.get('/folder/test')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test1') request, response = app.test_client.get('/folder/test1')
assert response.status == 404 assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test-123') request, response = app.test_client.get('/folder/test-123')
assert response.status == 404 assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/') request, response = app.test_client.get('/folder/')
assert response.status == 200 assert response.status == 200
@@ -384,16 +396,16 @@ def test_dynamic_add_route_unhashable():
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/') app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') request, response = app.test_client.get('/folder/test/asdf/end/')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') request, response = app.test_client.get('/folder/test///////end/')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/end/') request, response = app.test_client.get('/folder/test/end/')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/') request, response = app.test_client.get('/folder/test/nope/')
assert response.status == 404 assert response.status == 404
@@ -429,10 +441,10 @@ def test_add_route_method_not_allowed():
app.add_route(handler, '/test', methods=['GET']) app.add_route(handler, '/test', methods=['GET'])
request, response = sanic_endpoint_test(app, uri='/test') request, response = app.test_client.get('/test')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, method='post', uri='/test') request, response = app.test_client.post('/test')
assert response.status == 405 assert response.status == 405
@@ -448,19 +460,19 @@ def test_remove_static_route():
app.add_route(handler1, '/test') app.add_route(handler1, '/test')
app.add_route(handler2, '/test2') app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test') request, response = app.test_client.get('/test')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/test2') request, response = app.test_client.get('/test2')
assert response.status == 200 assert response.status == 200
app.remove_route('/test') app.remove_route('/test')
app.remove_route('/test2') app.remove_route('/test2')
request, response = sanic_endpoint_test(app, uri='/test') request, response = app.test_client.get('/test')
assert response.status == 404 assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/test2') request, response = app.test_client.get('/test2')
assert response.status == 404 assert response.status == 404
@@ -472,11 +484,11 @@ def test_remove_dynamic_route():
app.add_route(handler, '/folder/<name>') app.add_route(handler, '/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123') request, response = app.test_client.get('/folder/test123')
assert response.status == 200 assert response.status == 200
app.remove_route('/folder/<name>') app.remove_route('/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123') request, response = app.test_client.get('/folder/test123')
assert response.status == 404 assert response.status == 404
@@ -486,6 +498,19 @@ def test_remove_inexistent_route():
with pytest.raises(RouteDoesNotExist): with pytest.raises(RouteDoesNotExist):
app.remove_route('/test') app.remove_route('/test')
def test_removing_slash():
app = Sanic(__name__)
@app.get('/rest/<resource>')
def get(_):
pass
@app.post('/rest/<resource>')
def post(_):
pass
assert len(app.router.routes_all.keys()) == 2
def test_remove_unhashable_route(): def test_remove_unhashable_route():
app = Sanic('test_remove_unhashable_route') app = Sanic('test_remove_unhashable_route')
@@ -495,24 +520,24 @@ def test_remove_unhashable_route():
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/') app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') request, response = app.test_client.get('/folder/test/asdf/end/')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') request, response = app.test_client.get('/folder/test///////end/')
assert response.status == 200 assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/end/') request, response = app.test_client.get('/folder/test/end/')
assert response.status == 200 assert response.status == 200
app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/') app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') request, response = app.test_client.get('/folder/test/asdf/end/')
assert response.status == 404 assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') request, response = app.test_client.get('/folder/test///////end/')
assert response.status == 404 assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test/end/') request, response = app.test_client.get('/folder/test/end/')
assert response.status == 404 assert response.status == 404
@@ -524,22 +549,22 @@ def test_remove_route_without_clean_cache():
app.add_route(handler, '/test') app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test') request, response = app.test_client.get('/test')
assert response.status == 200 assert response.status == 200
app.remove_route('/test', clean_cache=True) app.remove_route('/test', clean_cache=True)
request, response = sanic_endpoint_test(app, uri='/test') request, response = app.test_client.get('/test')
assert response.status == 404 assert response.status == 404
app.add_route(handler, '/test') app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test') request, response = app.test_client.get('/test')
assert response.status == 200 assert response.status == 200
app.remove_route('/test', clean_cache=False) app.remove_route('/test', clean_cache=False)
request, response = sanic_endpoint_test(app, uri='/test') request, response = app.test_client.get('/test')
assert response.status == 200 assert response.status == 200
@@ -554,16 +579,16 @@ def test_overload_routes():
async def handler2(request): async def handler2(request):
return text('OK2') return text('OK2')
request, response = sanic_endpoint_test(app, 'get', uri='/overload') request, response = app.test_client.get('/overload')
assert response.text == 'OK1' assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, 'post', uri='/overload') request, response = app.test_client.post('/overload')
assert response.text == 'OK2' assert response.text == 'OK2'
request, response = sanic_endpoint_test(app, 'put', uri='/overload') request, response = app.test_client.put('/overload')
assert response.text == 'OK2' assert response.text == 'OK2'
request, response = sanic_endpoint_test(app, 'delete', uri='/overload') request, response = app.test_client.delete('/overload')
assert response.status == 405 assert response.status == 405
with pytest.raises(RouteExists): with pytest.raises(RouteExists):
@@ -584,10 +609,10 @@ def test_unmergeable_overload_routes():
async def handler2(request): async def handler2(request):
return text('Duplicated') return text('Duplicated')
request, response = sanic_endpoint_test(app, 'get', uri='/overload_whole') request, response = app.test_client.get('/overload_whole')
assert response.text == 'OK1' assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, 'post', uri='/overload_whole') request, response = app.test_client.post('/overload_whole')
assert response.text == 'OK1' assert response.text == 'OK1'
@@ -600,8 +625,8 @@ def test_unmergeable_overload_routes():
async def handler2(request): async def handler2(request):
return text('Duplicated') return text('Duplicated')
request, response = sanic_endpoint_test(app, 'get', uri='/overload_part') request, response = app.test_client.get('/overload_part')
assert response.text == 'OK1' assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, 'post', uri='/overload_part') request, response = app.test_client.post('/overload_part')
assert response.status == 405 assert response.status == 405

View File

@@ -6,12 +6,13 @@ import signal
import pytest import pytest
from sanic import Sanic from sanic import Sanic
from sanic.testing import HOST, PORT
AVAILABLE_LISTENERS = [ AVAILABLE_LISTENERS = [
'before_start', 'before_server_start',
'after_start', 'after_server_start',
'before_stop', 'before_server_stop',
'after_stop' 'after_server_stop'
] ]
@@ -30,7 +31,7 @@ def start_stop_app(random_name_app, **run_kwargs):
signal.signal(signal.SIGALRM, stop_on_alarm) signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1) signal.alarm(1)
try: try:
random_name_app.run(**run_kwargs) random_name_app.run(HOST, PORT, **run_kwargs)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
@@ -41,9 +42,10 @@ def test_single_listener(listener_name):
random_name_app = Sanic(''.join( random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))])) [choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list() output = list()
start_stop_app( # Register listener
random_name_app, random_name_app.listener(listener_name)(
**{listener_name: create_listener(listener_name, output)}) create_listener(listener_name, output))
start_stop_app(random_name_app)
assert random_name_app.name + listener_name == output.pop() assert random_name_app.name + listener_name == output.pop()
@@ -51,9 +53,9 @@ def test_all_listeners():
random_name_app = Sanic(''.join( random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))])) [choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list() output = list()
start_stop_app( for listener_name in AVAILABLE_LISTENERS:
random_name_app, listener = create_listener(listener_name, output)
**{listener_name: create_listener(listener_name, output) random_name_app.listener(listener_name)(listener)
for listener_name in AVAILABLE_LISTENERS}) start_stop_app(random_name_app)
for listener_name in AVAILABLE_LISTENERS: for listener_name in AVAILABLE_LISTENERS:
assert random_name_app.name + listener_name == output.pop() assert random_name_app.name + listener_name == output.pop()

View File

@@ -1,6 +1,6 @@
from sanic import Sanic from sanic import Sanic
from sanic.response import HTTPResponse from sanic.response import HTTPResponse
from sanic.utils import HOST, PORT from sanic.testing import HOST, PORT
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
import asyncio import asyncio
@@ -27,10 +27,11 @@ def test_register_system_signals():
async def hello_route(request): async def hello_route(request):
return HTTPResponse() return HTTPResponse()
app.run(HOST, PORT, app.listener('after_server_start')(stop)
before_start=set_loop, app.listener('before_server_start')(set_loop)
after_start=stop, app.listener('after_server_stop')(after)
after_stop=after)
app.run(HOST, PORT)
assert calledq.get() == True assert calledq.get() == True
@@ -42,9 +43,9 @@ def test_dont_register_system_signals():
async def hello_route(request): async def hello_route(request):
return HTTPResponse() return HTTPResponse()
app.run(HOST, PORT, app.listener('after_server_start')(stop)
before_start=set_loop, app.listener('before_server_start')(set_loop)
after_start=stop, app.listener('after_server_stop')(after)
after_stop=after,
register_sys_signals=False) app.run(HOST, PORT, register_sys_signals=False)
assert calledq.get() == False assert calledq.get() == False

View File

@@ -4,7 +4,6 @@ import os
import pytest import pytest
from sanic import Sanic from sanic import Sanic
from sanic.utils import sanic_endpoint_test
@pytest.fixture(scope='module') @pytest.fixture(scope='module')
@@ -32,7 +31,7 @@ def test_static_file(static_file_directory, file_name):
app.static( app.static(
'/testing.file', get_file_path(static_file_directory, file_name)) '/testing.file', get_file_path(static_file_directory, file_name))
request, response = sanic_endpoint_test(app, uri='/testing.file') request, response = app.test_client.get('/testing.file')
assert response.status == 200 assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name) assert response.body == get_file_content(static_file_directory, file_name)
@@ -44,7 +43,121 @@ def test_static_directory(file_name, base_uri, static_file_directory):
app = Sanic('test_static') app = Sanic('test_static')
app.static(base_uri, static_file_directory) app.static(base_uri, static_file_directory)
request, response = sanic_endpoint_test( request, response = app.test_client.get(
app, uri='{}/{}'.format(base_uri, file_name)) uri='{}/{}'.format(base_uri, file_name))
assert response.status == 200 assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name) assert response.body == get_file_content(static_file_directory, file_name)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_head_request(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
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))
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_correct(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/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)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[12:19]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_front(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/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)
assert response.status == 200
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'])
def test_static_content_range_back(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/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)
assert response.status == 200
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'])
def test_static_content_range_empty(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
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 response.body == bytes(
get_file_content(static_file_directory, file_name))
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_error(file_name, static_file_directory):
app = Sanic('test_static')
app.static(
'/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)
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)),)

View File

@@ -5,11 +5,19 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.utils import sanic_endpoint_test from sanic.testing import PORT as test_port
from sanic.exceptions import URLBuildError from sanic.exceptions import URLBuildError
import string 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_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
_server='localhost:{}'.format(test_port), _external=True)
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port)
def _generate_handlers_from_names(app, l): def _generate_handlers_from_names(app, l):
for name in l: for name in l:
@@ -33,12 +41,28 @@ def test_simple_url_for_getting(simple_app):
url = simple_app.url_for(letter) url = simple_app.url_for(letter)
assert url == '/{}'.format(letter) assert url == '/{}'.format(letter)
request, response = sanic_endpoint_test( request, response = simple_app.test_client.get(url)
simple_app, uri=url)
assert response.status == 200 assert response.status == 200
assert response.text == letter assert response.text == letter
@pytest.mark.parametrize('args,url',
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
(URL_FOR_ARGS2, URL_FOR_VALUE2),
(URL_FOR_ARGS3, URL_FOR_VALUE3)])
def test_simple_url_for_getting_with_more_params(args, url):
app = Sanic('more_url_build')
@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'
def test_fails_if_endpoint_not_found(): def test_fails_if_endpoint_not_found():
app = Sanic('fail_url_build') app = Sanic('fail_url_build')
@@ -75,6 +99,19 @@ def test_fails_url_build_if_param_not_passed():
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 = Sanic('fail_url_build')
@app.route('/fail')
def fail():
return text('this should fail')
with pytest.raises(ValueError) as e:
app.url_for('fail', _scheme='http')
assert str(e.value) == 'When specifying _scheme, _external must be True'
COMPLEX_PARAM_URL = ( COMPLEX_PARAM_URL = (
'/<foo:int>/<four_letter_string:[A-z]{4}>/' '/<foo:int>/<four_letter_string:[A-z]{4}>/'
'<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>') '<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>')
@@ -179,11 +216,11 @@ def blueprint_app():
return text( return text(
'foo from first : {}'.format(param)) 'foo from first : {}'.format(param))
@second_print.route('/foo') # noqa @second_print.route('/foo') # noqa
def foo(): def foo():
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): def foo_with_param(request, param):
return text( return text(
'foo from second : {}'.format(param)) 'foo from second : {}'.format(param))

View File

@@ -1,7 +1,6 @@
from json import loads as json_loads, dumps as json_dumps from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic from sanic import Sanic
from sanic.response import json, text from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@@ -15,7 +14,7 @@ def test_utf8_query_string():
async def handler(request): async def handler(request):
return text('OK') return text('OK')
request, response = sanic_endpoint_test(app, params=[("utf8", '')]) request, response = app.test_client.get('/', params=[("utf8", '')])
assert request.args.get('utf8') == '' assert request.args.get('utf8') == ''
@@ -26,7 +25,7 @@ def test_utf8_response():
async def handler(request): async def handler(request):
return text('') return text('')
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.text == '' assert response.text == ''
@@ -38,7 +37,7 @@ def skip_test_utf8_route():
return text('OK') return text('OK')
# UTF-8 Paths are not supported # UTF-8 Paths are not supported
request, response = sanic_endpoint_test(app, route='/✓', uri='/✓') request, response = app.test_client.get('/✓')
assert response.text == 'OK' assert response.text == 'OK'
@@ -52,7 +51,9 @@ def test_utf8_post_json():
payload = {'test': ''} payload = {'test': ''}
headers = {'content-type': 'application/json'} headers = {'content-type': 'application/json'}
request, response = sanic_endpoint_test(app, data=json_dumps(payload), headers=headers) request, response = app.test_client.get(
'/',
data=json_dumps(payload), headers=headers)
assert request.json.get('test') == '' assert request.json.get('test') == ''
assert response.text == 'OK' assert response.text == 'OK'

View File

@@ -1,6 +1,5 @@
from sanic import Sanic from sanic import Sanic
from sanic.response import json, text from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
def test_vhosts(): def test_vhosts():
@@ -15,11 +14,11 @@ def test_vhosts():
return text("You're at subdomain.example.com!") return text("You're at subdomain.example.com!")
headers = {"Host": "example.com"} headers = {"Host": "example.com"}
request, response = sanic_endpoint_test(app, headers=headers) request, response = app.test_client.get('/', headers=headers)
assert response.text == "You're at example.com!" assert response.text == "You're at example.com!"
headers = {"Host": "subdomain.example.com"} headers = {"Host": "subdomain.example.com"}
request, response = sanic_endpoint_test(app, headers=headers) request, response = app.test_client.get('/', headers=headers)
assert response.text == "You're at subdomain.example.com!" assert response.text == "You're at subdomain.example.com!"
@@ -31,9 +30,27 @@ def test_vhosts_with_list():
return text("Hello, world!") return text("Hello, world!")
headers = {"Host": "hello.com"} headers = {"Host": "hello.com"}
request, response = sanic_endpoint_test(app, headers=headers) request, response = app.test_client.get('/', headers=headers)
assert response.text == "Hello, world!" assert response.text == "Hello, world!"
headers = {"Host": "world.com"} headers = {"Host": "world.com"}
request, response = sanic_endpoint_test(app, headers=headers) request, response = app.test_client.get('/', headers=headers)
assert response.text == "Hello, world!" assert response.text == "Hello, world!"
def test_vhosts_with_defaults():
app = Sanic('test_vhosts')
@app.route('/', host="hello.com")
async def handler(request):
return text("Hello, world!")
@app.route('/')
async def handler(request):
return text("default")
headers = {"Host": "hello.com"}
request, response = app.test_client.get('/', headers=headers)
assert response.text == "Hello, world!"
request, response = app.test_client.get('/')
assert response.text == "default"

View File

@@ -1,11 +1,11 @@
import pytest as pytest import pytest as pytest
from sanic import Sanic from sanic import Sanic
from sanic.exceptions import InvalidUsage
from sanic.response import text, HTTPResponse from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView, CompositionView
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.request import Request from sanic.request import Request
from sanic.utils import sanic_endpoint_test
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
@@ -38,7 +38,7 @@ def test_methods(method):
app.add_route(DummyView.as_view(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method=method) request, response = getattr(app.test_client, method.lower())('/')
assert response.headers['method'] == method assert response.headers['method'] == method
@@ -51,9 +51,9 @@ def test_unexisting_methods():
return text('I am get method') return text('I am get method')
app.add_route(DummyView.as_view(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get") request, response = app.test_client.get('/')
assert response.text == 'I am get method' assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post") request, response = app.test_client.post('/')
assert response.text == 'Error: Method POST not allowed for URL /' assert response.text == 'Error: Method POST not allowed for URL /'
@@ -67,7 +67,7 @@ def test_argument_methods():
app.add_route(DummyView.as_view(), '/<my_param_here>') app.add_route(DummyView.as_view(), '/<my_param_here>')
request, response = sanic_endpoint_test(app, uri='/test123') request, response = app.test_client.get('/test123')
assert response.text == 'I am get method with test123' assert response.text == 'I am get method with test123'
@@ -84,7 +84,7 @@ def test_with_bp():
bp.add_route(DummyView.as_view(), '/') bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp) app.blueprint(bp)
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.text == 'I am get method' assert response.text == 'I am get method'
@@ -101,7 +101,7 @@ def test_with_bp_with_url_prefix():
bp.add_route(DummyView.as_view(), '/') bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp) app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/') request, response = app.test_client.get('/test1/')
assert response.text == 'I am get method' assert response.text == 'I am get method'
@@ -122,7 +122,7 @@ def test_with_middleware():
async def handler(request): async def handler(request):
results.append(request) results.append(request)
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.text == 'I am get method' assert response.text == 'I am get method'
assert type(results[0]) is Request assert type(results[0]) is Request
@@ -149,7 +149,7 @@ def test_with_middleware_response():
app.add_route(DummyView.as_view(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app) request, response = app.test_client.get('/')
assert response.text == 'I am get method' assert response.text == 'I am get method'
assert type(results[0]) is Request assert type(results[0]) is Request
@@ -171,7 +171,7 @@ def test_with_custom_class_methods():
return text('I am get method and global var is {}'.format(self.global_var)) return text('I am get method and global var is {}'.format(self.global_var))
app.add_route(DummyView.as_view(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get") request, response = app.test_client.get('/')
assert response.text == 'I am get method and global var is 10' assert response.text == 'I am get method and global var is 10'
@@ -193,6 +193,68 @@ def test_with_decorator():
return text('I am get method') return text('I am get method')
app.add_route(DummyView.as_view(), '/') app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get") request, response = app.test_client.get('/')
assert response.text == 'I am get method' assert response.text == 'I am get method'
assert results[0] == 1 assert results[0] == 1
def test_composition_view_rejects_incorrect_methods():
def foo(request):
return text('Foo')
view = CompositionView()
with pytest.raises(InvalidUsage) as e:
view.add(['GET', 'FOO'], foo)
assert str(e.value) == 'FOO is not a valid HTTP method.'
def test_composition_view_rejects_duplicate_methods():
def foo(request):
return text('Foo')
view = CompositionView()
with pytest.raises(InvalidUsage) as e:
view.add(['GET', 'POST', 'GET'], foo)
assert str(e.value) == 'Method GET is already registered.'
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_composition_view_runs_methods_as_expected(method):
app = Sanic('test_composition_view')
view = CompositionView()
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
view.add(['DELETE', 'PATCH'], lambda x: text('second method'))
app.add_route(view, '/')
if method in ['GET', 'POST', 'PUT']:
request, response = getattr(app.test_client, method.lower())('/')
assert response.text == 'first method'
if method in ['DELETE', 'PATCH']:
request, response = getattr(app.test_client, method.lower())('/')
assert response.text == 'second method'
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_composition_view_rejects_invalid_methods(method):
app = Sanic('test_composition_view')
view = CompositionView()
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
app.add_route(view, '/')
if method in ['GET', 'POST', 'PUT']:
request, response = getattr(app.test_client, method.lower())('/')
assert response.status == 200
assert response.text == 'first method'
if method in ['DELETE', 'PATCH']:
request, response = getattr(app.test_client, method.lower())('/')
assert response.status == 405

View File

@@ -14,10 +14,13 @@ deps =
aiohttp aiohttp
pytest pytest
beautifulsoup4 beautifulsoup4
coverage
commands = commands =
pytest tests {posargs} pytest tests {posargs}
coverage erase
coverage run -m sanic.app
coverage report
[testenv:flake8] [testenv:flake8]
deps = deps =