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]
branch = True
source = sanic, tests
omit = site-packages
source = sanic
omit = site-packages, sanic/utils.py
[html]
directory = coverage
directory = coverage

3
.gitignore vendored
View File

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

View File

@@ -78,10 +78,7 @@ Documentation
TODO
----
* Streamed file processing
* File output
* Examples of integrations with 3rd-party modules
* RESTful router
* http2
Limitations
-----------
* 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 import Blueprint
blueprint_v1 = Blueprint('v1')
blueprint_v2 = Blueprint('v2')
blueprint_v1 = Blueprint('v1', url_prefix='/v1')
blueprint_v2 = Blueprint('v2', url_prefix='/v2')
@blueprint_v1.route('/')
async def api_v1_root(request):
@@ -153,8 +153,8 @@ from sanic import Sanic
from blueprints import blueprint_v1, blueprint_v2
app = Sanic(__name__)
app.blueprint(blueprint_v1)
app.blueprint(blueprint_v2)
app.blueprint(blueprint_v1, url_prefix='/v1')
app.blueprint(blueprint_v2, url_prefix='/v2')
app.run(host='0.0.0.0', port=8000, debug=True)
```
@@ -167,7 +167,7 @@ takes the format `<blueprint_name>.<handler_name>`. For example:
```
@blueprint_v1.route('/')
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)

View File

@@ -7,15 +7,6 @@ keyword arguments:
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
- `port` *(default `8000`)*: Port to host the server on.
- `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).
- `sock` *(default `None`)*: Socket for the server to accept connections from.
- `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.
Allows using redis, memcache or an in memory store.
- [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.
- [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.
- `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`
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("/")
async def test(request):
return json({ "hello": "world" })
```
```
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
@@ -64,9 +64,9 @@ async def folder_handler(request, folder_id):
## 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`,
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
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:
```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')
# /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.

View File

@@ -1,51 +1,73 @@
# 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/)
library. The `sanic_endpoint_test` function runs a local server, issues a
configurable request to an endpoint, and returns the result. It takes the
following arguments:
library.
- `app` An instance of a Sanic app.
- `method` *(default `'get'`)* A string representing the HTTP method to use.
- `uri` *(default `'/'`)* A string representing the endpoint to test.
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
for you to run against your application. A simple example (using pytest) is like follows:
```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
original request will be returned by the function. If set to `True`, the
return value is a tuple of `(request, response)`, if `False` only the
response is returned.
- `loop` *(default `None`)* The event loop to use.
- `debug` *(default `False`)* A boolean which determines whether to run the
server in debug mode.
- `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run.
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
The function further takes the `*request_args` and `**request_kwargs`, which
are passed directly to the aiohttp ClientSession request. For example, to
supply data with a GET request, `method` would be `get` and the keyword
argument `params={'value', 'key'}` would be supplied. More information about
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the aiohttp ClientSession request.
For example, to supply data to a GET request, you would do the following:
```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
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
Below is a complete example of an endpoint test,
using [pytest](http://doc.pytest.org/en/latest/). The test checks that the
`/challenge` endpoint responds to a GET request with a supplied challenge
string.
```python
import pytest
import aiohttp
### Deprecated: `sanic_endpoint_test`
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
# Import the Sanic app, usually created with Sanic(__name__)
from external_server import app
def test_endpoint_challenge():
# Create the challenge data
request_data = {'challenge': 'dummy_challenge'}
# Send the request to the endpoint, using the default `get` method
request, response = sanic_endpoint_test(app,
uri='/challenge',
params=request_data)
# Assert that the server responds with the challenge string
assert response.text == request_data['challenge']
def test_index_returns_200():
request, response = sanic_endpoint_test(app)
assert response.status == 200
```

View File

@@ -1,6 +1,6 @@
"""
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
the rest are instant because the value is retrieved from the Redis.
@@ -20,9 +20,14 @@ from aiocache.serializers import JsonSerializer
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())
@@ -38,4 +43,4 @@ async def test(request):
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
@app.listener('before_server_start')
def init(sanic, loop):
global sem
CONCURRENCY_PER_WORKER = 4
@@ -33,4 +34,4 @@ async def test(request):
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.listener('before_server_start')
async def prepare_db(app, loop):
"""
Let's create some table and add some data
@@ -61,5 +62,4 @@ async def handle(request):
if __name__ == '__main__':
app.run(host='0.0.0.0',
port=8000,
debug=True,
before_start=prepare_db)
debug=True)

View File

@@ -32,7 +32,7 @@ polls = sa.Table('sanic_polls', metadata,
app = Sanic(name=__name__)
@app.listener('before_server_start')
async def prepare_db(app, loop):
""" Let's add some data
@@ -58,9 +58,10 @@ async def handle(request):
async with engine.acquire() as conn:
result = []
async for row in conn.execute(polls.select()):
result.append({"question": row.question, "pub_date": row.pub_date})
result.append({"question": row.question,
"pub_date": row.pub_date})
return json({"polls": result})
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.listener('before_server_start')
async def create_db(app, loop):
"""
Create some table and add some data
@@ -55,4 +56,4 @@ async def handler(request):
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
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:
# 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
@@ -48,6 +40,15 @@ objects.database.allow_sync = False # this will raise AssertionError on ANY sync
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>')
async def post(request, key, value):
"""
@@ -75,4 +76,4 @@ async def get(request):
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.log import log
from sanic.response import json, text
from sanic.response import json, text, file
from sanic.exceptions import ServerError
app = Sanic(__name__)
@@ -31,6 +33,10 @@ async def test_await(request):
await asyncio.sleep(5)
return text("I'm feeling sleepy")
@app.route("/file")
async def test_file(request):
return await file(os.path.abspath("setup.py"))
# ----------------------------------------------- #
# Exceptions
@@ -64,12 +70,14 @@ def query_string(request):
# Run Server
# ----------------------------------------------- #
@app.listener('after_server_start')
def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH")
@app.listener('before_server_stop')
def before_stop(app, loop):
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 .blueprints import Blueprint
from sanic.app import Sanic
from sanic.blueprints import Blueprint
__version__ = '0.3.1'
__version__ = '0.4.1'
__all__ = ['Sanic', 'Blueprint']

View File

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

View File

@@ -1,27 +1,29 @@
import logging
import re
import warnings
from asyncio import get_event_loop
from collections import deque
from collections import deque, defaultdict
from functools import partial
from inspect import isawaitable, stack, getmodulename
import re
from traceback import format_exc
from urllib.parse import urlencode, urlunparse
import warnings
from .config import Config
from .constants import HTTP_METHODS
from .exceptions import Handler
from .exceptions import ServerError, URLBuildError
from .log import log
from .response import HTTPResponse
from .router import Router
from .server import serve, serve_multiple, HttpProtocol
from .static import register as static_register
from sanic.config import Config
from sanic.constants import HTTP_METHODS
from sanic.exceptions import ServerError, URLBuildError, SanicException
from sanic.handlers import ErrorHandler
from sanic.log import log
from sanic.response import HTTPResponse
from sanic.router import Router
from sanic.server import serve, serve_multiple, HttpProtocol
from sanic.static import register as static_register
from sanic.testing import TestClient
from sanic.views import CompositionView
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
# end-user application didn't set anything up.
if not logging.root.handlers and log.level == logging.NOTSET:
@@ -31,12 +33,15 @@ class Sanic:
handler.setFormatter(formatter)
log.addHandler(handler)
log.setLevel(logging.INFO)
# Get name from previous stack frame
if name is None:
frame_records = stack()[1]
name = getmodulename(frame_records[1])
self.name = name
self.router = router or Router()
self.error_handler = error_handler or Handler()
self.error_handler = error_handler or ErrorHandler()
self.config = Config()
self.request_middleware = deque()
self.response_middleware = deque()
@@ -44,22 +49,61 @@ class Sanic:
self._blueprint_order = []
self.debug = None
self.sock = None
self.processes = None
self.listeners = defaultdict(list)
self.is_running = False
# Register alternative method names
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
# -------------------------------------------------------------------- #
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
def route(self, uri, methods=frozenset({'GET'}), host=None):
"""
Decorates a function to be registered as a route
"""Decorate a function to be registered as a route
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:param host:
:return: decorated function
"""
@@ -77,29 +121,28 @@ class Sanic:
# Shorthand method decorators
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):
return self.route(uri, methods=["POST"], host=host)
return self.route(uri, methods=frozenset({"POST"}), host=host)
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):
return self.route(uri, methods=["HEAD"], host=host)
return self.route(uri, methods=frozenset({"HEAD"}), host=host)
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):
return self.route(uri, methods=["PATCH"], host=host)
return self.route(uri, methods=frozenset({"PATCH"}), host=host)
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):
"""
A helper method to register class instance or
"""A helper method to register class instance or
functions as a handler to the application url
routes.
@@ -107,11 +150,21 @@ class Sanic:
:param uri: path of the URL
:param methods: list or tuple of methods allowed, these are overridden
if using a HTTPMethodView
:param host:
:return: function or class instance
"""
# Handle HTTPMethodView differently
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)
return handler
@@ -120,10 +173,9 @@ class Sanic:
# Decorator
def exception(self, *exceptions):
"""
Decorates a function to be registered as a handler for exceptions
"""Decorate a function to be registered as a handler for exceptions
:param \*exceptions: exceptions
:param exceptions: exceptions
:return: decorated function
"""
@@ -135,14 +187,11 @@ class Sanic:
return response
# 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
can either be called as @app.middleware or @app.middleware('request')
"""
attach_to = 'request'
def register_middleware(middleware):
def register_middleware(middleware, attach_to='request'):
if attach_to == 'request':
self.request_middleware.append(middleware)
if attach_to == 'response':
@@ -150,25 +199,24 @@ class Sanic:
return middleware
# Detect which way this was called, @middleware or @middleware('AT')
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
return register_middleware(args[0])
if callable(middleware_or_request):
return register_middleware(middleware_or_request)
else:
attach_to = args[0]
return register_middleware
return partial(register_middleware,
attach_to=middleware_or_request)
# Static Files
def static(self, uri, file_or_directory, pattern='.+',
use_modified_since=True):
"""
Registers a root to serve files from. The input can either be a file
or a directory. See
use_modified_since=True, use_content_range=False):
"""Register a root to serve files from. The input can either be a
file or a directory. See
"""
static_register(self, uri, file_or_directory, pattern,
use_modified_since)
use_modified_since, use_content_range)
def blueprint(self, blueprint, **options):
"""
Registers a blueprint on the application.
"""Register a blueprint on the application.
:param blueprint: Blueprint object
:param options: option dictionary with blueprint defaults
@@ -195,7 +243,7 @@ class Sanic:
return self.blueprint(*args, **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
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
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
parameters and query string arguments.
@@ -222,12 +270,28 @@ class Sanic:
'Endpoint with name `{}` was not found'.format(
view_name))
if uri != '/' and uri.endswith('/'):
uri = uri[:-1]
out = uri
# find all the parameters we will need to build in the URL
matched_params = re.findall(
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:
name, _type, pattern = self.router.parse_parameter_string(
match)
@@ -268,12 +332,9 @@ class Sanic:
replacement_regex, supplied_param, out)
# parse the remainder of the keyword arguments into a querystring
if kwargs:
query_string = urlencode(kwargs)
out = urlunparse((
'', '', out,
'', query_string, ''
))
query_string = urlencode(kwargs, doseq=True) if kwargs else ''
# scheme://netloc/path;parameters?query#fragment
out = urlunparse((scheme, netloc, out, '', query_string, anchor))
return out
@@ -285,9 +346,8 @@ class Sanic:
pass
async def handle_request(self, request, response_callback):
"""
Takes a request from the HTTP Server and returns a response object to
be sent back The HTTP Server only expects a response object, so
"""Take a request from the HTTP Server and return a response object
to be sent back The HTTP Server only expects a response object, so
exception handling must be done here
:param request: HTTP Request object
@@ -300,6 +360,8 @@ class Sanic:
# Request Middleware
# -------------------------------------------- #
request.app = self
response = False
# The if improves speed. I don't know why
if self.request_middleware:
@@ -361,6 +423,14 @@ class Sanic:
response_callback(response)
# -------------------------------------------------------------------- #
# Testing
# -------------------------------------------------------------------- #
@property
def test_client(self):
return TestClient(self)
# -------------------------------------------------------------------- #
# Execution
# -------------------------------------------------------------------- #
@@ -369,9 +439,8 @@ class Sanic:
after_start=None, before_stop=None, after_stop=None, ssl=None,
sock=None, workers=1, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None, register_sys_signals=True):
"""
Runs the HTTP Server and listens until keyboard interrupt or term
signal. On termination, drains connections before closing.
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:param port: Port to host on
@@ -388,6 +457,10 @@ class Sanic:
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:param loop:
:param backlog:
:param stop_event:
:param register_sys_signals:
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
@@ -397,16 +470,18 @@ class Sanic:
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
loop=loop, protocol=protocol, backlog=backlog,
stop_event=stop_event, register_sys_signals=register_sys_signals)
try:
self.is_running = True
if workers == 1:
serve(**server_settings)
else:
serve_multiple(server_settings, workers, stop_event)
except Exception as e:
except:
log.exception(
'Experienced exception while trying to serve')
finally:
self.is_running = False
log.info("Server Stopped")
def stop(self):
@@ -418,22 +493,19 @@ class Sanic:
before_stop=None, after_stop=None, ssl=None,
sock=None, loop=None, protocol=HttpProtocol,
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(
host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop,
after_stop=after_stop, ssl=ssl, sock=sock, loop=loop,
protocol=protocol, backlog=backlog, stop_event=stop_event,
after_stop=after_stop, ssl=ssl, sock=sock,
loop=loop or get_event_loop(), protocol=protocol,
backlog=backlog, stop_event=stop_event,
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)
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,
protocol=HttpProtocol, backlog=100, stop_event=None,
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 debug:
@@ -453,9 +523,18 @@ class Sanic:
"pull/335 has more information.",
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.debug = debug
self.loop = loop = get_event_loop()
server_settings = {
'protocol': protocol,
@@ -477,19 +556,18 @@ class Sanic:
# Register start/stop events
# -------------------------------------------- #
for event_name, settings_name, args, reverse in (
("before_server_start", "before_start", before_start, False),
("after_server_start", "after_start", after_start, False),
("before_server_stop", "before_stop", before_stop, True),
("after_server_stop", "after_stop", after_stop, True),
):
listeners = []
for blueprint in self.blueprints.values():
listeners += blueprint.listeners[event_name]
for event_name, settings_name, reverse, args in (
("before_server_start", "before_start", False, before_start),
("after_server_start", "after_start", False, after_start),
("before_server_stop", "before_stop", True, before_stop),
("after_server_stop", "after_stop", True, after_stop),
):
listeners = self.listeners[event_name].copy()
if args:
if callable(args):
args = [args]
listeners += args
listeners.append(args)
else:
listeners.extend(args)
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered

View File

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

View File

@@ -39,8 +39,9 @@ class Config(dict):
self[attr] = value
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.
:param variable_name: name of the environment variable
:return: bool. ``True`` if able to load config, ``False`` otherwise.
"""
@@ -52,8 +53,9 @@ class Config(dict):
return self.from_pyfile(config_file)
def from_pyfile(self, filename):
"""Updates the values in the config from a Python file. Only the uppercase
variables in that module are stored in the config.
"""Update the values in the config from a Python file.
Only the uppercase variables in that module are stored in the config.
:param filename: an absolute path to the config file
"""
module = types.ModuleType('config')
@@ -69,7 +71,7 @@ class Config(dict):
return True
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.
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 string
@@ -39,8 +38,7 @@ _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
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
MultiHeader class to provide a unique key that encodes to Set-Cookie.
"""
@@ -75,9 +73,7 @@ class CookieJar(dict):
class Cookie(dict):
"""
This is a stripped down version of Morsel from SimpleCookie #gottagofast
"""
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
_keys = {
"expires": "expires",
"path": "Path",
@@ -107,13 +103,19 @@ class Cookie(dict):
def encode(self, encoding):
output = ['%s=%s' % (self.key, _quote(self.value))]
for key, value in self.items():
if key == 'max-age' and isinstance(value, int):
output.append('%s=%d' % (self._keys[key], value))
elif key == 'expires' and isinstance(value, datetime):
output.append('%s=%s' % (
self._keys[key],
value.strftime("%a, %d-%b-%Y %T GMT")
))
if key == 'max-age':
try:
output.append('%s=%d' % (self._keys[key], value))
except TypeError:
output.append('%s=%s' % (self._keys[key], value))
elif key == 'expires':
try:
output.append('%s=%s' % (
self._keys[key],
value.strftime("%a, %d-%b-%Y %T GMT")
))
except AttributeError:
output.append('%s=%s' % (self._keys[key], value))
elif key in self._flags:
if self[key]:
output.append(self._keys[key])
@@ -128,9 +130,8 @@ class Cookie(dict):
class MultiHeader:
"""
Allows us to set a header within response that has a unique key,
but may contain duplicate header names
"""String-holding object which allow us to set a header within response
that has a unique key, but may contain duplicate header names
"""
def __init__(self, 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 = '''
<style>
body {
@@ -102,8 +97,10 @@ INTERNAL_SERVER_ERROR_HTML = '''
class SanicException(Exception):
def __init__(self, message, status_code=None):
super().__init__(message)
if status_code is not None:
self.status_code = status_code
@@ -141,85 +138,20 @@ class PayloadTooLarge(SanicException):
status_code = 413
class Handler:
handlers = None
cached_handlers = None
_missing = object()
class HeaderNotFound(SanicException):
status_code = 400
def __init__(self):
self.handlers = []
self.cached_handlers = {}
self.debug = False
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
frames = extract_tb(tb)
class ContentRangeError(SanicException):
status_code = 416
frame_html = []
for frame in frames:
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
def __init__(self, message, content_range):
super().__init__(message)
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):
self.handlers.append((exception, handler))
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)
class InvalidRangeType(ContentRangeError):
pass

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 httptools import parse_url
from urllib.parse import parse_qs
from ujson import loads as json_loads
from sanic.exceptions import InvalidUsage
from .log import log
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"
@@ -16,8 +20,7 @@ DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
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
"""
@@ -31,11 +34,9 @@ class RequestParameters(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__ = (
'url', 'headers', 'version', 'method', '_cookies', 'transport',
'app', 'url', 'headers', 'version', 'method', '_cookies', 'transport',
'query_string', 'body',
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip',
@@ -44,6 +45,7 @@ class Request(dict):
def __init__(self, url_bytes, headers, version, method, transport):
# TODO: Content-Encoding detection
url_parsed = parse_url(url_bytes)
self.app = None
self.url = url_parsed.path.decode('utf-8')
self.headers = headers
self.version = version
@@ -73,8 +75,8 @@ class Request(dict):
@property
def token(self):
"""
Attempts to return the auth header token.
"""Attempt to return the auth header token.
:return: token related to request
"""
auth_header = self.headers.get('Authorization')
@@ -118,8 +120,7 @@ class Request(dict):
self.parsed_args = RequestParameters(
parse_qs(self.query_string))
else:
self.parsed_args = {}
self.parsed_args = RequestParameters()
return self.parsed_args
@property
@@ -146,11 +147,10 @@ File = namedtuple('File', ['type', 'body', 'name'])
def parse_multipart_form(body, boundary):
"""
Parses a request body and returns fields and files
"""Parse a request body and returns fields and files
:param body: Bytes request body
:param boundary: Bytes multipart boundary
:param body: bytes request body
:param boundary: bytes multipart boundary
:return: fields (RequestParameters), files (RequestParameters)
"""
files = RequestParameters()

View File

@@ -1,10 +1,10 @@
from aiofiles import open as open_async
from mimetypes import guess_type
from os import path
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 = {
200: b'OK',
@@ -98,19 +98,22 @@ class HTTPResponse:
# This is all returned in a kind-of funky way
# We tried to make this as fast as possible in pure python
timeout_header = b''
if keep_alive and keep_alive_timeout:
timeout_header = b'Keep-Alive: timeout=%d\r\n' % keep_alive_timeout
if keep_alive and keep_alive_timeout is not None:
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''
if self.headers:
for name, value in self.headers.items():
try:
headers += (
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
except AttributeError:
headers += (
b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8')))
for name, value in self.headers.items():
try:
headers += (
b'%b: %b\r\n' % (
name.encode(), value.encode('utf-8')))
except AttributeError:
headers += (
b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8')))
# Try to pull from the common codes first
# Speeds up response rate 6% over pulling from all
@@ -119,16 +122,13 @@ class HTTPResponse:
status = ALL_STATUS_CODES.get(self.status)
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'%b%b\r\n'
b'%b'
b'%b\r\n'
b'%b') % (
version.encode(),
self.status,
status,
self.content_type.encode(),
len(self.body),
b'keep-alive' if keep_alive else b'close',
timeout_header,
headers,
@@ -148,21 +148,38 @@ def json(body, status=200, headers=None, **kwargs):
:param body: Response data to be serialized.
:param status: Response code.
: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,
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.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
:param content_type:
the content type (string) of the response
"""
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):
@@ -176,17 +193,24 @@ def html(body, status=200, headers=None):
content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None):
"""
Returns response object with file data.
async def file(location, mime_type=None, headers=None, _range=None):
"""Return a response object with file data.
:param location: Location of file on system.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param _range:
"""
filename = path.split(location)[-1]
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'
@@ -198,14 +222,12 @@ async def file(location, mime_type=None, headers=None):
def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"):
"""
Aborts execution and causes a 302 redirect (by default).
"""Abort execution and cause a 302 redirect (by default).
:param to: path or fully qualified URL to redirect to
: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 content_type:
the content type (string) of the response
:param content_type: the content type (string) of the response
:returns: the redirecting Response
"""
headers = headers or {}

View File

@@ -1,8 +1,10 @@
import re
from collections import defaultdict, namedtuple
from collections.abc import Iterable
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',
@@ -32,8 +34,7 @@ class RouteDoesNotExist(Exception):
class Router:
"""
Router supports basic routing with parameters and method checks
"""Router supports basic routing with parameters and method checks
Usage:
@@ -68,13 +69,14 @@ class Router:
self.routes_static = {}
self.routes_dynamic = defaultdict(list)
self.routes_always_check = []
self.hosts = None
self.hosts = set()
def parse_parameter_string(self, parameter_string):
"""
Parse a parameter string into its constituent name, type, and pattern
"""Parse a parameter string into its constituent name, type, and
pattern
For example:
`parse_parameter_string('<param_one:[A-z]')` ->
`parse_parameter_string('<param_one:[A-z]>')` ->
('param_one', str, '[A-z]')
:param parameter_string: String to parse
@@ -94,33 +96,50 @@ class Router:
return name, _type, pattern
def add(self, uri, methods, handler, host=None):
"""
Adds a handler to the route list
# add regular version
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
:param methods: Array 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.
def _add(self, uri, methods, handler, host=None):
"""Add a handler to the route list
:param uri: path to match
: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
"""
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):
uri = host + uri
self.hosts.add(host)
else:
for h in host:
self.add(uri, methods, handler, h)
if not isinstance(host, Iterable):
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
else:
# default host
self.hosts.add('*')
# Dict for faster lookups of if method allowed
if methods:
@@ -239,8 +258,7 @@ class Router:
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
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
:return: tuple containing (uri, Route)
@@ -255,26 +273,30 @@ class Router:
return (None, None)
def get(self, request):
"""
Gets a request handler based on the URL of the request, or raises an
"""Get a request handler based on the URL of the request, or raises an
error
:param request: Request object
: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, '')
else:
# virtual hosts specified; try to match route to the host header
try:
return self._get(request.url, request.method,
request.headers.get("Host", ''))
# try default hosts
except NotFound:
return self._get(request.url, request.method, '')
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, url, method, host):
"""
Gets a request handler based on the URL of the request, or raises an
"""Get a request handler based on the URL of the request, or raises an
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
"""
url = host + url

View File

@@ -1,6 +1,7 @@
import asyncio
import os
import traceback
import warnings
from functools import partial
from inspect import isawaitable
from multiprocessing import Process, Event
@@ -9,21 +10,19 @@ from signal import SIGTERM, SIGINT
from signal import signal as signal_func
from socket import socket, SOL_SOCKET, SO_REUSEADDR
from time import time
import warnings
from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
from .exceptions import ServerError
try:
import uvloop as async_loop
except ImportError:
async_loop = asyncio
from .log import log
from .request import Request
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
from sanic.log import log
from sanic.request import Request
from sanic.exceptions import (
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError)
current_time = None
@@ -33,8 +32,7 @@ class Signal:
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()
in favor of speed, since headers are case insensitive
"""
@@ -169,19 +167,26 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- #
def write_response(self, response):
keep_alive = (
self.parser.should_keep_alive() and not self.signal.stopped)
try:
keep_alive = (
self.parser.should_keep_alive() and not self.signal.stopped)
self.transport.write(
response.output(
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:
log.error(
'Connection lost before response written @ {}'.format(
self.request.ip))
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(e))
"Writing response failed, connection closed {}".format(
repr(e)))
finally:
if not keep_alive:
self.transport.close()
@@ -198,10 +203,10 @@ class HttpProtocol(asyncio.Protocol):
except RuntimeError:
log.error(
'Connection lost before error written @ {}'.format(
self.request.ip))
self.request.ip if self.request else 'Unknown'))
except Exception as e:
self.bail_out(
"Writing error failed, connection closed {}".format(e),
"Writing error failed, connection closed {}".format(repr(e)),
from_error=True)
finally:
self.transport.close()
@@ -228,8 +233,8 @@ class HttpProtocol(asyncio.Protocol):
self._total_request_size = 0
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
"""
if not self.parser:
@@ -239,9 +244,8 @@ class HttpProtocol(asyncio.Protocol):
def update_current_time(loop):
"""
Caches the current time, since it is needed
at the end of every keep-alive request to update the request timeout time
"""Cache the current time, since it is needed at the end of every
keep-alive request to update the request timeout time
:param loop:
:return:
@@ -252,17 +256,15 @@ def update_current_time(loop):
def trigger_events(events, loop):
"""
"""Trigger event callbacks (functions or async)
:param events: one or more sync or async functions to execute
:param loop: event loop
"""
if events:
if not isinstance(events, list):
events = [events]
for event in events:
result = event(loop)
if isawaitable(result):
loop.run_until_complete(result)
for event in events:
result = event(loop)
if isawaitable(result):
loop.run_until_complete(result)
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,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False):
"""
Starts asynchronous HTTP Server on an individual process.
"""Start asynchronous HTTP Server on an individual process.
:param host: Address to host on
:param port: Port to host on
:param request_handler: Sanic request handler with middleware
:param error_handler: Sanic error handler with middleware
:param before_start: Function to be executed before the server starts
:param before_start: function to be executed before the server starts
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`
: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
`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
`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 ssl: SSLContext
:param sock: Socket for the server to accept connections from
:param request_max_size: size in bytes, `None` for no limit
:param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop
:param protocol: Subclass of asyncio protocol class
:param protocol: subclass of asyncio protocol class
:return: Nothing
"""
if not run_async:
@@ -346,8 +347,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
# Register signals for graceful termination
if register_sys_signals:
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()
try:
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):
"""
Starts multiple server processes simultaneously. Stops on interrupt
and terminate signals, and drains connections when complete.
"""Start multiple server processes simultaneously. Stop on interrupt
and terminate signals, and drain connections when complete.
:param server_settings: kw arguments to be passed to the serve function
: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 re import sub
from time import strftime, gmtime
from urllib.parse import unquote
from .exceptions import FileNotFound, InvalidUsage
from .response import file, HTTPResponse
from aiofiles.os import stat
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):
# TODO: Though sanic is not a file server, I feel like we should atleast
def register(app, uri, file_or_directory, pattern,
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
# 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.
: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
not modified if the browser's matches the
server's
:param use_content_range: If true, process header for range requests
and sends the file part that is requested
"""
# If we're not trying to match a file directly,
# serve from the folder
if not path.isfile(file_or_directory):
@@ -50,18 +60,41 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
headers = {}
# Check if the client has been sent this file before
# and it has not been modified since
stats = None
if use_modified_since:
stats = await stat(file_path)
modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT',
gmtime(stats.st_mtime))
modified_since = strftime(
'%a, %d %b %Y %H:%M:%S GMT', gmtime(stats.st_mtime))
if request.headers.get('If-Modified-Since') == modified_since:
return HTTPResponse(status=304)
headers['Last-Modified'] = modified_since
return await file(file_path, headers=headers)
except:
_range = None
if use_content_range:
_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',
path=file_or_directory,
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
from sanic.log import log
import warnings
HOST = '127.0.0.1'
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
from sanic.testing import TestClient
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
debug=False, server_kwargs={},
*request_args, **request_kwargs):
results = [None, None]
exceptions = []
warnings.warn(
"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:
def _collect_request(request):
if results[0] is None:
results[0] = request
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))
test_client = TestClient(app)
return test_client._sanic_endpoint_test(
method, uri, gather_request, debug, server_kwargs,
*request_args, **request_kwargs)

View File

@@ -1,8 +1,9 @@
from .exceptions import InvalidUsage
from sanic.exceptions import InvalidUsage
from sanic.constants import HTTP_METHODS
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
to every HTTP method you want to support.
@@ -40,17 +41,12 @@ class HTTPMethodView:
def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
if handler:
return handler(request, *args, **kwargs)
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
return handler(request, *args, **kwargs)
@classmethod
def as_view(cls, *class_args, **class_kwargs):
""" Converts the class into an actual view function that can be used
with the routing system.
"""Return view function for use with the routing system, that
dispatches request to appropriate handler method.
"""
def view(*args, **kwargs):
self = view.view_class(*class_args, **class_kwargs)
@@ -69,7 +65,7 @@ class HTTPMethodView:
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)
for every HTTP method you want to support.
@@ -89,15 +85,15 @@ class CompositionView:
def add(self, methods, handler):
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:
raise KeyError(
'Method {} already is registered.'.format(method))
raise InvalidUsage(
'Method {} is already registered.'.format(method))
self.handlers[method] = handler
def __call__(self, request, *args, **kwargs):
handler = self.handlers.get(request.method.upper(), None)
if handler is None:
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
handler = self.handlers[request.method.upper()]
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:
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(
name='sanic',
version=version,
@@ -22,15 +31,11 @@ setup(
license='MIT',
author='Channel Cat',
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'],
platforms='any',
install_requires=[
'uvloop>=0.5.3',
'httptools>=0.0.9',
'ujson>=1.35',
'aiofiles>=0.3.0',
],
install_requires=install_requires,
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'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():
app = Sanic('test_bad_request_response')
lines = []
@app.listener('after_server_start')
async def _request(sanic, loop):
connect = asyncio.open_connection('127.0.0.1', 42101)
reader, writer = await connect
@@ -15,6 +16,6 @@ def test_bad_request_response():
break
lines.append(line)
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[-1] == b'Error: Bad Request'

View File

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

View File

@@ -2,7 +2,6 @@ from datetime import datetime, timedelta
from http.cookies import SimpleCookie
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
import pytest
@@ -19,7 +18,7 @@ def test_cookies():
response.cookies['right_back'] = 'at you'
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.load(response.headers.get('Set-Cookie', {}))
@@ -40,7 +39,7 @@ def test_false_cookies(httponly, expected):
response.cookies['right_back']['httponly'] = httponly
return response
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
@@ -55,7 +54,7 @@ def test_http2_cookies():
return response
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!'
@@ -70,7 +69,7 @@ def test_cookie_options():
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
return response
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
@@ -88,7 +87,7 @@ def test_cookie_deletion():
del response.cookies['i_never_existed']
return response
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
response_cookies = SimpleCookie()
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.server import HttpProtocol
from sanic.response import text
from sanic.utils import sanic_endpoint_test
app = Sanic('test_custom_porotocol')
@@ -26,7 +25,7 @@ def test_use_custom_protocol():
server_kwargs = {
'protocol': CustomHttpProtocol
}
request, response = sanic_endpoint_test(app, uri='/1',
server_kwargs=server_kwargs)
request, response = app.test_client.get(
'/1', server_kwargs=server_kwargs)
assert response.status == 200
assert response.text == 'OK'

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import uuid
from sanic.response import text
from sanic import Sanic
from io import StringIO
from sanic.utils import sanic_endpoint_test
import logging
logging_format = '''module: %(module)s; \
@@ -29,7 +28,7 @@ def test_log():
log.info(rand_string)
return text('hello')
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
log_text = log_stream.getvalue()
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.request import Request
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):
return text('OK')
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.text == 'OK'
assert type(results[0]) is Request
@@ -46,7 +45,7 @@ def test_middleware_response():
async def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.text == 'OK'
assert type(results[0]) is Request
@@ -65,7 +64,7 @@ def test_middleware_override_request():
async def handler(request):
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.text == 'OK'
@@ -82,7 +81,7 @@ def test_middleware_override_response():
async def handler(request):
return text('FAIL')
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.status == 200
assert response.text == 'OK'
@@ -122,7 +121,7 @@ def test_middleware_order():
async def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.status == 200
assert order == [1,2,3,4,5,6]

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import random
from sanic import Sanic
from sanic.response import json
from sanic.utils import sanic_endpoint_test
from ujson import loads
@@ -15,10 +16,28 @@ def test_storage():
@app.route('/')
def handler(request):
return json({ 'user': request.get('user'), 'sidekick': request.get('sidekick') })
return json({'user': request.get('user'), 'sidekick': request.get('sidekick')})
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
response_json = loads(response.text)
assert response_json['user'] == 'sanic'
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
from sanic.response import text
from sanic.exceptions import RequestTimeout
from sanic.utils import sanic_endpoint_test
from sanic.config import Config
Config.REQUEST_TIMEOUT = 1
@@ -22,7 +21,7 @@ def handler_exception(request, exception):
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.text == 'Request Timeout from error_handler.'
@@ -34,7 +33,6 @@ async def handler_2(request):
def test_default_server_error_request_timeout():
request, response = sanic_endpoint_test(
request_timeout_default_app, uri='/1')
request, response = request_timeout_default_app.test_client.get('/1')
assert response.status == 408
assert response.text == 'Error: Request Timeout'

View File

@@ -1,11 +1,12 @@
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
from sanic import Sanic
from sanic.exceptions import ServerError
from sanic.response import json, text, redirect
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
@@ -17,7 +18,7 @@ def test_sync():
def handler(request):
return text('Hello')
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.text == 'Hello'
@@ -29,7 +30,7 @@ def test_text():
async def handler(request):
return text('Hello')
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.text == 'Hello'
@@ -42,7 +43,7 @@ def test_headers():
headers = {"spam": "great"}
return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.headers.get('spam') == 'great'
@@ -55,7 +56,7 @@ def test_non_str_headers():
headers = {"answer": 42}
return text('Hello', headers=headers)
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.headers.get('answer') == '42'
@@ -70,7 +71,7 @@ def test_invalid_response():
async def handler(request):
return 'This should fail'
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.status == 500
assert response.text == "Internal Server Error."
@@ -82,7 +83,7 @@ def test_json():
async def handler(request):
return json({"test": True})
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
try:
results = json_loads(response.text)
@@ -100,7 +101,7 @@ def test_invalid_json():
return json(request.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
@@ -112,7 +113,8 @@ def test_query_string():
async def handler(request):
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('test2') == 'false'
@@ -132,7 +134,7 @@ def test_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
@@ -143,14 +145,15 @@ def test_token():
def test_post_json():
app = Sanic('test_post_json')
@app.route('/')
@app.route('/', methods=['POST'])
async def handler(request):
return text('OK')
payload = {'test': 'OK'}
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 response.text == 'OK'
@@ -159,14 +162,14 @@ def test_post_json():
def test_post_form_urlencoded():
app = Sanic('test_post_form_urlencoded')
@app.route('/')
@app.route('/', methods=['POST'])
async def handler(request):
return text('OK')
payload = 'test=OK'
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'
@@ -174,7 +177,7 @@ def test_post_form_urlencoded():
def test_post_form_multipart_form_data():
app = Sanic('test_post_form_multipart_form_data')
@app.route('/')
@app.route('/', methods=['POST'])
async def handler(request):
return text('OK')
@@ -186,6 +189,6 @@ def test_post_form_multipart_form_data():
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'

View File

@@ -2,7 +2,6 @@ from random import choice
from sanic import Sanic
from sanic.response import HTTPResponse
from sanic.utils import sanic_endpoint_test
def test_response_body_not_a_string():
@@ -14,5 +13,5 @@ def test_response_body_not_a_string():
async def hello_route(request):
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)

View File

@@ -3,7 +3,6 @@ import pytest
from sanic import Sanic
from sanic.response import text
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):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/get', method='get')
request, response = app.test_client.get('/get')
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
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():
app = Sanic('test_shorhand_routes_post')
@@ -30,10 +42,10 @@ def test_shorthand_routes_post():
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/post', method='post')
request, response = app.test_client.post('/post')
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
def test_shorthand_routes_put():
@@ -43,10 +55,10 @@ def test_shorthand_routes_put():
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/put', method='put')
request, response = app.test_client.put('/put')
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
def test_shorthand_routes_patch():
@@ -56,10 +68,10 @@ def test_shorthand_routes_patch():
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/patch', method='patch')
request, response = app.test_client.patch('/patch')
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
def test_shorthand_routes_head():
@@ -69,10 +81,10 @@ def test_shorthand_routes_head():
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/head', method='head')
request, response = app.test_client.head('/head')
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
def test_shorthand_routes_options():
@@ -82,10 +94,10 @@ def test_shorthand_routes_options():
def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/options', method='options')
request, response = app.test_client.options('/options')
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
def test_static_routes():
@@ -99,10 +111,10 @@ def test_static_routes():
async def handler2(request):
return text('OK2')
request, response = sanic_endpoint_test(app, uri='/test')
request, response = app.test_client.get('/test')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, uri='/pizazz')
request, response = app.test_client.get('/pizazz')
assert response.text == 'OK2'
@@ -116,7 +128,7 @@ def test_dynamic_route():
results.append(name)
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 results[0] == 'test123'
@@ -132,12 +144,12 @@ def test_dynamic_route_string():
results.append(name)
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 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 results[1] == 'favicon.ico'
@@ -153,11 +165,11 @@ def test_dynamic_route_int():
results.append(folder_id)
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 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
@@ -171,14 +183,14 @@ def test_dynamic_route_number():
results.append(weight)
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 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
request, response = sanic_endpoint_test(app, uri='/weight/1234-56')
request, response = app.test_client.get('/weight/1234-56')
assert response.status == 404
@@ -189,16 +201,16 @@ def test_dynamic_route_regex():
async def handler(request, folder_id):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/folder/test')
request, response = app.test_client.get('/folder/test')
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
request, response = sanic_endpoint_test(app, uri='/folder/test-123')
request, response = app.test_client.get('/folder/test-123')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/')
request, response = app.test_client.get('/folder/')
assert response.status == 200
@@ -209,16 +221,16 @@ def test_dynamic_route_unhashable():
async def handler(request, unhashable):
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
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
request, response = app.test_client.get('/folder/test///////end/')
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
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
request, response = app.test_client.get('/folder/test/nope/')
assert response.status == 404
@@ -251,10 +263,10 @@ def test_method_not_allowed():
async def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/test')
request, response = app.test_client.get('/test')
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
@@ -270,10 +282,10 @@ def test_static_add_route():
app.add_route(handler1, '/test')
app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test')
request, response = app.test_client.get('/test')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, uri='/test2')
request, response = app.test_client.get('/test2')
assert response.text == 'OK2'
@@ -287,7 +299,7 @@ def test_dynamic_add_route():
return text('OK')
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 results[0] == 'test123'
@@ -303,12 +315,12 @@ def test_dynamic_add_route_string():
return text('OK')
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 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 results[1] == 'favicon.ico'
@@ -325,11 +337,11 @@ def test_dynamic_add_route_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 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
@@ -344,14 +356,14 @@ def test_dynamic_add_route_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 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
request, response = sanic_endpoint_test(app, uri='/weight/1234-56')
request, response = app.test_client.get('/weight/1234-56')
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}>')
request, response = sanic_endpoint_test(app, uri='/folder/test')
request, response = app.test_client.get('/folder/test')
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
request, response = sanic_endpoint_test(app, uri='/folder/test-123')
request, response = app.test_client.get('/folder/test-123')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/')
request, response = app.test_client.get('/folder/')
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/')
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
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
request, response = app.test_client.get('/folder/test///////end/')
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
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
request, response = app.test_client.get('/folder/test/nope/')
assert response.status == 404
@@ -429,10 +441,10 @@ def test_add_route_method_not_allowed():
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
request, response = sanic_endpoint_test(app, method='post', uri='/test')
request, response = app.test_client.post('/test')
assert response.status == 405
@@ -448,19 +460,19 @@ def test_remove_static_route():
app.add_route(handler1, '/test')
app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test')
request, response = app.test_client.get('/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/test2')
request, response = app.test_client.get('/test2')
assert response.status == 200
app.remove_route('/test')
app.remove_route('/test2')
request, response = sanic_endpoint_test(app, uri='/test')
request, response = app.test_client.get('/test')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/test2')
request, response = app.test_client.get('/test2')
assert response.status == 404
@@ -472,11 +484,11 @@ def test_remove_dynamic_route():
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
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
@@ -486,6 +498,19 @@ def test_remove_inexistent_route():
with pytest.raises(RouteDoesNotExist):
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():
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/')
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
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
request, response = app.test_client.get('/folder/test///////end/')
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
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
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
request, response = app.test_client.get('/folder/test///////end/')
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
@@ -524,22 +549,22 @@ def test_remove_route_without_clean_cache():
app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test')
request, response = app.test_client.get('/test')
assert response.status == 200
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
app.add_route(handler, '/test')
request, response = sanic_endpoint_test(app, uri='/test')
request, response = app.test_client.get('/test')
assert response.status == 200
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
@@ -554,16 +579,16 @@ def test_overload_routes():
async def handler2(request):
return text('OK2')
request, response = sanic_endpoint_test(app, 'get', uri='/overload')
request, response = app.test_client.get('/overload')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, 'post', uri='/overload')
request, response = app.test_client.post('/overload')
assert response.text == 'OK2'
request, response = sanic_endpoint_test(app, 'put', uri='/overload')
request, response = app.test_client.put('/overload')
assert response.text == 'OK2'
request, response = sanic_endpoint_test(app, 'delete', uri='/overload')
request, response = app.test_client.delete('/overload')
assert response.status == 405
with pytest.raises(RouteExists):
@@ -584,10 +609,10 @@ def test_unmergeable_overload_routes():
async def handler2(request):
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'
request, response = sanic_endpoint_test(app, 'post', uri='/overload_whole')
request, response = app.test_client.post('/overload_whole')
assert response.text == 'OK1'
@@ -600,8 +625,8 @@ def test_unmergeable_overload_routes():
async def handler2(request):
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'
request, response = sanic_endpoint_test(app, 'post', uri='/overload_part')
request, response = app.test_client.post('/overload_part')
assert response.status == 405

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import os
import pytest
from sanic import Sanic
from sanic.utils import sanic_endpoint_test
@pytest.fixture(scope='module')
@@ -32,7 +31,7 @@ def test_static_file(static_file_directory, file_name):
app.static(
'/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.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.static(base_uri, static_file_directory)
request, response = sanic_endpoint_test(
app, uri='{}/{}'.format(base_uri, file_name))
request, response = app.test_client.get(
uri='{}/{}'.format(base_uri, file_name))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
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.views import HTTPMethodView
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
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):
for name in l:
@@ -33,12 +41,28 @@ def test_simple_url_for_getting(simple_app):
url = simple_app.url_for(letter)
assert url == '/{}'.format(letter)
request, response = sanic_endpoint_test(
simple_app, uri=url)
request, response = simple_app.test_client.get(url)
assert response.status == 200
assert response.text == letter
@pytest.mark.parametrize('args,url',
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
(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():
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)
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 = (
'/<foo:int>/<four_letter_string:[A-z]{4}>/'
'<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:number>')
@@ -179,11 +216,11 @@ def blueprint_app():
return text(
'foo from first : {}'.format(param))
@second_print.route('/foo') # noqa
@second_print.route('/foo') # noqa
def foo():
return text('foo from second')
@second_print.route('/foo/<param>') # noqa
@second_print.route('/foo/<param>') # noqa
def foo_with_param(request, param):
return text(
'foo from second : {}'.format(param))

View File

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

View File

@@ -1,6 +1,5 @@
from sanic import Sanic
from sanic.response import json, text
from sanic.utils import sanic_endpoint_test
def test_vhosts():
@@ -15,11 +14,11 @@ def test_vhosts():
return text("You're at subdomain.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!"
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!"
@@ -31,9 +30,27 @@ def test_vhosts_with_list():
return text("Hello, world!")
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!"
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!"
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
from sanic import Sanic
from sanic.exceptions import InvalidUsage
from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView
from sanic.views import HTTPMethodView, CompositionView
from sanic.blueprints import Blueprint
from sanic.request import Request
from sanic.utils import sanic_endpoint_test
from sanic.constants import HTTP_METHODS
@@ -38,7 +38,7 @@ def test_methods(method):
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
@@ -51,9 +51,9 @@ def test_unexisting_methods():
return text('I am get method')
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'
request, response = sanic_endpoint_test(app, method="post")
request, response = app.test_client.post('/')
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>')
request, response = sanic_endpoint_test(app, uri='/test123')
request, response = app.test_client.get('/test123')
assert response.text == 'I am get method with test123'
@@ -84,7 +84,7 @@ def test_with_bp():
bp.add_route(DummyView.as_view(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.text == 'I am get method'
@@ -101,7 +101,7 @@ def test_with_bp_with_url_prefix():
bp.add_route(DummyView.as_view(), '/')
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'
@@ -122,7 +122,7 @@ def test_with_middleware():
async def handler(request):
results.append(request)
request, response = sanic_endpoint_test(app)
request, response = app.test_client.get('/')
assert response.text == 'I am get method'
assert type(results[0]) is Request
@@ -149,7 +149,7 @@ def test_with_middleware_response():
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 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))
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'
@@ -193,6 +193,68 @@ def test_with_decorator():
return text('I am get method')
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 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
pytest
beautifulsoup4
coverage
commands =
pytest tests {posargs}
coverage erase
coverage run -m sanic.app
coverage report
[testenv:flake8]
deps =