Compare commits

..

223 Commits
0.3.0 ... 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
Eli Uriegas
419156f7cc Merge pull request #397 from seemethere/increment_031
Increment to v0.3.1
2017-02-08 19:23:20 -06:00
Eli Uriegas
b7f7883fb7 Increment to v0.3.1 2017-02-08 19:22:51 -06:00
Eli Uriegas
a5a7490bca Merge pull request #379 from youknowone/exception
Let exception handler handle inherited exceptions
2017-02-08 19:20:42 -06:00
Eli Uriegas
6724d8131c Merge pull request #387 from subyraman/url-for-v3
Add `url_for` method for simple routes, blueprints, HTTPMethodView
2017-02-08 19:20:09 -06:00
Raphael Deem
a19bb15070 Merge pull request #389 from chenfengyuan/fix_run_async_demo
fix run_async demo
2017-02-07 14:58:48 -08:00
Eli Uriegas
2ee0147848 Merge pull request #386 from youknowone/sanic_endpoint_test
Fix sanic_endpoint_test working with redirects
2017-02-07 10:57:05 -06:00
Eli Uriegas
252f1add7e Merge pull request #394 from brian-bates/tox-cleanup
Add missing dependency to tox.ini
2017-02-07 10:55:52 -06:00
Jeong YunWon
413c92c631 Let exception handler handle inherited exceptions
Original sanic exception handler only could handle exact matching
exceptions. New `lookup` method will provide ability to looking up
their ancestors without additional cost of performance.
2017-02-07 21:08:31 +09:00
Suby Raman
36d519026f reject unnamed handlers 2017-02-06 11:11:00 -05:00
Fengyuan Chen
aa54785918 fix always warning loop is passed issue 2017-02-06 11:11:00 -05:00
Raphael Deem
d6b386083f Merge pull request #390 from chenfengyuan/pass_loop_warning
fix always warning loop is passed issue
2017-02-05 14:01:41 -08:00
Brian Bates
82f383b64f Add missing dependency
Added missing `aiofiles` to tox.ini and cleaned up requirements files.
2017-02-05 11:44:01 -08:00
Jeong YunWon
a15ee3ad06 Fix sanic_endpoint_test working with redirects
Before fix, it raises error like:

```
tests/test_utils.py F

================================= FAILURES =================================
______________________________ test_redirect _______________________________

app = <sanic.sanic.Sanic object at 0x1045fda20>, method = 'get', uri = '/1', gather_request = True, debug = False
server_kwargs = {}, request_args = (), request_kwargs = {}
_collect_request = <function sanic_endpoint_test.<locals>._collect_request at 0x1045ec950>
_collect_response = <function sanic_endpoint_test.<locals>._collect_response at 0x1045ec7b8>

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

        if gather_request:
            def _collect_request(request):
                results.append(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.append(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
E               ValueError: too many values to unpack (expected 2)

sanic/utils.py:47: ValueError

During handling of the above exception, another exception occurred:

utils_app = <sanic.sanic.Sanic object at 0x1045fda20>

    def test_redirect(utils_app):
        """Test sanic_endpoint_test is working for redirection"""
>       request, response = sanic_endpoint_test(utils_app, uri='/1')

tests/test_utils.py:33:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

app = <sanic.sanic.Sanic object at 0x1045fda20>, method = 'get', uri = '/1', gather_request = True, debug = False
server_kwargs = {}, request_args = (), request_kwargs = {}
_collect_request = <function sanic_endpoint_test.<locals>._collect_request at 0x1045ec950>
_collect_response = <function sanic_endpoint_test.<locals>._collect_response at 0x1045ec7b8>

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

        if gather_request:
            def _collect_request(request):
                results.append(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.append(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))
E               ValueError: Request and response object expected, got ([{}, {}, {}, <ClientResponse(http://127.0.0.1:42101/3) [200 OK]>
E               <CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '2', 'Connection': 'keep-alive', 'Keep-Alive': 'timeout=1')>
E               ])

sanic/utils.py:52: ValueError
```
2017-02-05 13:57:04 +09:00
Fengyuan Chen
29680cb515 fix always warning loop is passed issue 2017-02-04 16:09:09 +08:00
Fengyuan Chen
884749f345 fix run_async demo 2017-02-04 15:27:46 +08:00
Suby Raman
5c63ce666c punctuation 2017-02-02 14:21:59 -05:00
Suby Raman
5632d073be update docs 2017-02-02 13:00:15 -05:00
Suby Raman
f9056099f9 all works 2017-02-02 12:52:48 -05:00
Suby Raman
7c09ec29f7 rebase 2017-02-02 12:21:14 -05:00
Eli Uriegas
6f0b09509e Merge pull request #213 from sfstpala/master
Make it possible to disable the logo by subclassing Config
2017-02-01 23:07:52 -06:00
Eli Uriegas
31c53d67e2 Merge pull request #384 from seemethere/update_static_tests
Updates static tests to test for issue #374
2017-02-01 15:20:34 -06:00
Eli Uriegas
6a322ba3f8 Updates static tests to test for issue #374
Adds a test to test for serving a static directory at the root uri '/'
to address concerns found in #374. Also rewrites the tests so that they
are parametrized and do more with less.
2017-02-01 09:00:57 -06:00
Raphael Deem
dece636d54 Merge pull request #383 from r0fls/derp
typo: async_run -> run_async
2017-02-01 00:29:12 -08:00
Raphael Deem
b29f648148 typo: async_run -> run_async 2017-01-31 12:46:02 -08:00
Eli Uriegas
c91d264ff1 Merge pull request #380 from channelcat/openapi-extension
Added sanic-openapi to extensions
2017-01-31 07:39:38 -06:00
Eli Uriegas
487e3352e4 Revert "fix async run, add tests"
This reverts commit 41da793b5a.
2017-01-31 07:30:17 -06:00
Channel Cat
34966fb182 Added sanic-openapi to extensions 2017-01-31 01:24:41 -08:00
Eli Uriegas
17a92a58b2 Merge pull request #369 from r0fls/fix-async-run
fix async run, add tests
2017-01-30 22:21:25 -06:00
Raphael Deem
60d3e5b9e0 Merge pull request #377 from r0fls/373
update route method docs
2017-01-30 17:07:26 -08:00
Kyle Blöm
d193a1eb70 Added the tests, code formatting changes, and the Range Request feature. 2017-01-30 17:04:51 -08:00
Raphael Deem
1501c56bbc update route method docs 2017-01-30 16:42:43 -08:00
Eli Uriegas
6d18fb6bae Merge pull request #363 from r0fls/run-helper
Run helper
2017-01-30 05:51:50 -06:00
Eli Uriegas
eac26a4514 Merge pull request #372 from JordanP/fix_docs_config
Fix docs/config.md: the MYAPP_SETTINGS is not exported
2017-01-30 05:43:29 -06:00
Channel Cat
1649f30808 Updated password 2017-01-30 02:22:12 -08:00
Jordan Pittier
82680bf43f Fix docs/config.md: the MYAPP_SETTINGS is not exported
If we don"t `export` the variable, it's not available in subcommand:

MYAPP_SETTINGS=/path/to/config_file; python3 -c "import os; os.environ['MYAPP_SETTINGS']"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/lib/python3.5/os.py", line 725, in __getitem__
    raise KeyError(key) from None
KeyError: 'MYAPP_SETTINGS'

The ';' is the culprit here.
2017-01-30 10:39:02 +01:00
Raphael Deem
41da793b5a fix async run, add tests 2017-01-29 23:47:47 -08:00
Raphael Deem
cfb5734b85 Merge pull request #371 from channelcat/blueprint-shorthand
Blueprint route shorthand
2017-01-29 23:23:47 -08:00
Channel Cat
b72d841619 . 2017-01-29 23:21:00 -08:00
Channel Cat
0ef39f35ae Added route shorthands to blueprints 2017-01-29 23:20:38 -08:00
Channel Cat
38d1ed76d2 Merge pull request #368 from channelcat/blueprint-clarity
Restructured blueprint class for clarity
2017-01-29 18:47:27 -08:00
Channel Cat
4c80cd185f Fix flake8 2017-01-29 17:44:46 -08:00
Channel Cat
629524af04 Restructured blueprint class
Blueprints currently queue functions to be called, which are simple, yet
hard to inspect.  These changes allow tools to be built that analyze
blueprints more easily.
2017-01-29 17:39:55 -08:00
Channel Cat
a245e54bd3 Merge pull request #367 from channelcat/fix-rtd-build
Fix readthedocs includes
2017-01-29 16:46:48 -08:00
Channel Cat
52e485cce9 Fix readthedocs includes 2017-01-29 16:46:16 -08:00
Channel Cat
02d374b65b Merge pull request #365 from channelcat/fix-rtd-build
Adding readthedocs file
2017-01-29 16:41:41 -08:00
Channel Cat
0de6bb0063 Adding readthedocs file 2017-01-29 16:40:36 -08:00
Channel Cat
9d6b379999 Merge pull request #364 from channelcat/fix-rtd-build
Fix readthedocs build
2017-01-29 16:38:29 -08:00
Channel Cat
c132c4e673 fix conflict part 2 2017-01-29 16:32:57 -08:00
Channel Cat
6962dcd66c fix conflict 2017-01-29 16:31:37 -08:00
Channel Cat
2a9496fcda Fix readthedocs build 2017-01-29 16:25:22 -08:00
Raphael Deem
82d1d30a41 review updates 2017-01-29 14:01:00 -08:00
Eli Uriegas
85639d0806 Revert "testing"
This reverts commit 3fd6ecaedb.
2017-01-29 15:55:47 -06:00
Channel Cat
3fd6ecaedb testing 2017-01-29 13:52:17 -08:00
Channel Cat
884d3a0316 Fix RTD build 2017-01-29 13:45:44 -08:00
Raphael Deem
10dbb9186d combine logic from create_server() and run() 2017-01-29 13:36:13 -08:00
Eli Uriegas
a547798b08 Merge pull request #360 from seemethere/fix_route_overloading_for_dynamic_routes
Fixes route overloading for dynamic routes
2017-01-29 15:35:13 -06:00
Eli Uriegas
894b434875 Merge pull request #362 from channelcat/read-the-docs
Added basic readthedocs support
2017-01-29 15:34:58 -06:00
Eli Uriegas
8db0ece459 Merge branch 'master' into read-the-docs 2017-01-29 15:31:22 -06:00
Eli Uriegas
f56c5e3a45 Merge pull request #199 from Tim-Erwin/improved_config
added methods to load config from a file
2017-01-29 15:27:34 -06:00
Eli Uriegas
0a5fa72099 Add logic to make dynamic route merging work
This is by no means the final solution but it's a start in the right
direction. Eventually what needs to happen is we need to reduce the
complexity of the routing. CompsitionView can probably be removed later
on in favor of better Route objects. Also in the next version of sanic
we need to move merge_route and add_parameter out of the add_route logic
and just have them as standalone methods.

The tests should cover everything that we need so that if any changes
are made we can identify regression.
2017-01-29 15:16:07 -06:00
Channel Cat
0eaccea38f updated project name in docs build 2017-01-29 12:49:59 -08:00
Channel Cat
de32c389d0 Added basic readthedocs support 2017-01-29 12:47:00 -08:00
Raphael Deem
753d2da6db fix async run 2017-01-28 15:47:29 -08:00
Eli Uriegas
d3344da9c5 Add a pesky newline 2017-01-27 22:15:34 -06:00
Eli Uriegas
ae0876876e Switch them to verifying headers instead 2017-01-27 22:13:16 -06:00
Eli Uriegas
dea8e16f49 Force method to lower 2017-01-27 22:07:31 -06:00
Eli Uriegas
13803bdb30 Update for HTTPMethodView compatibility 2017-01-27 22:05:46 -06:00
Eli Uriegas
7257e5794f Merge pull request #359 from r0fls/fix-warnings
fix deprecation warnings
2017-01-27 21:05:51 -06:00
Eli Uriegas
41c52487ee Fixes route overloading for dynamic routes
Addresses #353, now dynamic routes work alongside our newly minted
overloaded routes! Also fixed an unintended side effect where methods
were still being passed in as None for `Sanic.add_route`.
2017-01-27 21:00:33 -06:00
Raphael Deem
0eb779185d fix deprecation warnings 2017-01-27 18:09:19 -08:00
Tim Mundt
5bba3388a0 Merge branch 'master' into improved_config 2017-01-25 09:36:21 +01:00
Tim Mundt
0b9094d348 Merge branch 'master' into improved_config 2017-01-13 12:34:56 +01:00
Eli Uriegas
2d4512cd1c Merge branch 'master' into improved_config 2016-12-25 15:26:33 -08:00
Stefano Palazzo
a73a7d1e7b Make it possible to disable the logo by subclassing Config 2016-12-23 11:42:00 +01:00
Tim Mundt
ef9edfd160 added documentation for configuration 2016-12-17 20:20:07 +01:00
Tim Mundt
234a7925c6 restored accidentally degraded doc string 2016-12-17 19:24:41 +01:00
Tim Mundt
a550b5c112 added tests and small fixes for config 2016-12-16 18:46:07 +01:00
Tim Mundt
04798cbf5b added methods to load config from a file 2016-12-16 17:05:09 +01:00
88 changed files with 3194 additions and 1513 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

@@ -1,14 +1,14 @@
sudo: false
language: python
python:
- '3.5'
- '3.6'
- '3.5'
- '3.6'
install: pip install tox-travis
script: tox
deploy:
provider: pypi
user: channelcat
password:
secure: jH4+Di2/qcBwWVhI5/3NYd/JuDDgf5/cF85h+oQnAjgwP6me3th9RS0PHL2gjKJrmyRgwrW7a3eSAityo5sQSlBloQCNrtCE30rkDiwtgoIxDW72NR/nE8nUkS9Utgy87eS+3B4NrO7ag4GTqO5ET8SQ4/MCiQwyUQATLXj2s2eTpQvqJeZG6YgoeFAOYvlR580yznXoOwldWlkiymJiWSdR/01lthtWCi40sYC/QoU7psODJ/tPcsqgQtQKyUVsci7mKvp3Y8ImkoO/POM01jYNsS9qLh5pKTNCEYxtyzC77whenCNHn7WReVidd56g1ADosbNo4yY/1D3VAvwjUnkQ0SzdBQfT7IIzccEuC0j1NXKPN97OX0a6XzyUMYJ1XiU3juTJOPxdYBPbsDM3imQiwrOh1faIf0HCgNTN+Lxe5l8obCH7kffNcVUhs2zI0+2t4MS5tjb/OVuYD/TFn+bM33DqzLctTOK/pGn6xefzZcdzb191LPo99Lof+4fo6jNUpb0UmcBu5ZJzxh0lGe8FPIK3UAG/hrYDDgjx8s8RtUJjcEUQz0659XffYx7DLlgHO7cWyfjrHD3yrLzDbYr5mAS4FR+4D917V7UL+on4SsKHN00UuMGPguqSYo/xYyPLnJU5XK0du4MIpsNMB8TtrJOIewOOfD32+AisPQ8=
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k=
on:
tags: true

View File

@@ -45,10 +45,8 @@ Hello World Example
from sanic import Sanic
from sanic.response import json
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
@@ -56,21 +54,6 @@ Hello World Example
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
SSL Example
-----------
Optionally pass in an SSLContext:
.. code:: python
import ssl
certificate = "/path/to/certificate"
keyfile = "/path/to/keyfile"
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certificate, keyfile=keyfile)
app.run(host="0.0.0.0", port=8443, ssl=context)
Installation
------------
@@ -79,12 +62,14 @@ Installation
Documentation
-------------
Documentation can be found in the ``docs`` directory.
`Documentation on Readthedocs <http://sanic.readthedocs.io/>`_.
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master
:target: https://travis-ci.org/channelcat/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
@@ -93,10 +78,7 @@ Documentation can be found in the ``docs`` directory.
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 :(

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = Sanic
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -22,9 +22,7 @@ import sanic
# -- General configuration ------------------------------------------------
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages']
extensions = []
templates_path = ['_templates']
@@ -75,7 +73,7 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'alabaster'
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
@@ -153,3 +151,7 @@ epub_copyright = copyright
epub_exclude_files = ['search.html']
# -- Custom Settings -------------------------------------------------------
suppress_warnings = ['image.nonlocal_uri']

78
docs/config.md Normal file
View File

@@ -0,0 +1,78 @@
# Configuration
Any reasonably complex application will need configuration that is not baked into the acutal code. Settings might be different for different environments or installations.
## Basics
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
```
app = Sanic('myapp')
app.config.DB_NAME = 'appdb'
app.config.DB_USER = 'appuser'
```
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
```
db_settings = {
'DB_HOST': 'localhost',
'DB_NAME': 'appdb',
'DB_USER': 'appuser'
}
app.config.update(db_settings)
```
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
## Loading Configuration
There are several ways how to load configuration.
### From an Object
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
```
import myapp.default_settings
app = Sanic('myapp')
app.config.from_object(myapp.default_settings)
```
You could use a class or any other object as well.
### From a File
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_file(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
```
app = Sanic('myapp')
app.config.from_envvar('MYAPP_SETTINGS')
```
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
```
$ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py
INFO: Goin' Fast @ http://0.0.0.0:8000
```
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase varibales are added to the configuration. Most commonly the configuration consists of simple key value pairs:
```
# config_file
DB_HOST = 'localhost'
DB_NAME = 'appdb'
DB_USER = 'appuser'
```
## Builtin Configuration Values
Out of the box there are just a few predefined values which can be overwritten when creating the application.
| Variable | Default | Description |
| ----------------- | --------- | --------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |

View File

@@ -1,12 +0,0 @@
# Sanic Extensions
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.
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
**Previous:** [Testing](testing.md)
**Next:** [Contributing](contributing.md)

View File

@@ -1,4 +1,4 @@
.. include:: ../README.rst
.. include:: sanic/index.rst
Guides
======
@@ -6,20 +6,22 @@ Guides
.. toctree::
:maxdepth: 2
getting_started
routing
request_data
deploying
static_files
middleware
exceptions
blueprints
class_based_views
cookies
custom_protocol
testing
extensions
contributing
sanic/getting_started
sanic/routing
sanic/request_data
sanic/static_files
sanic/exceptions
sanic/middleware
sanic/blueprints
sanic/config
sanic/cookies
sanic/class_based_views
sanic/custom_protocol
sanic/ssl
sanic/testing
sanic/deploying
sanic/extensions
sanic/contributing
Module Documentation
@@ -27,7 +29,5 @@ Module Documentation
.. toctree::
Module Reference <_api/sanic>
* :ref:`genindex`
* :ref:`search`

36
docs/make.bat Normal file
View File

@@ -0,0 +1,36 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=Sanic
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

View File

@@ -1,111 +0,0 @@
# Routing
Routing allows the user to specify handler functions for different URL endpoints.
A basic route looks like the following, where `app` is an instance of the
`Sanic` class:
```python
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
returns a JSON object.
Sanic handler functions must be defined using the `async def` syntax, as they
are asynchronous functions.
## Request parameters
Sanic comes with a basic router that supports request parameters.
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
Request parameters will be passed to the route handler functions as keyword
arguments.
```python
from sanic.response import text
@app.route('/tag/<tag>')
async def tag_handler(request, tag):
return text('Tag - {}'.format(tag))
```
To specify a type for the parameter, add a `:type` after the parameter name,
inside the quotes. If the parameter does not match the specified type, Sanic
will throw a `NotFound` exception, resulting in a `404: Page not found` error
on the URL.
```python
from sanic.response import text
@app.route('/number/<integer_arg:int>')
async def integer_handler(request, integer_arg):
return text('Integer - {}'.format(integer_arg))
@app.route('/number/<number_arg:number>')
async def number_handler(request, number_arg):
return text('Number - {}'.format(number_arg))
@app.route('/person/<name:[A-z]>')
async def person_handler(request, name):
return text('Person - {}'.format(name))
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
async def folder_handler(request, folder_id):
return text('Folder - {}'.format(folder_id))
```
## HTTP request types
By default, a route defined on a URL will be used for all requests to that URL.
However, the `@app.route` decorator accepts an optional parameter, `methods`,
which restricts the handler function to the HTTP methods in the given list.
```python
from sanic.response import text
@app.route('/post')
async def post_handler(request, methods=['POST']):
return text('POST request - {}'.format(request.json))
@app.route('/get')
async def GET_handler(request, methods=['GET']):
return text('GET request - {}'.format(request.args))
```
## The `add_route` method
As we have seen, routes are often specified using the `@app.route` decorator.
However, this decorator is really just a wrapper for the `app.add_route`
method, which is used as follows:
```python
from sanic.response import text
# Define the handler functions
async def handler1(request):
return text('OK')
async def handler2(request, name):
return text('Folder - {}'.format(name))
async def person_handler2(request, name):
return text('Person - {}'.format(name))
# Add each handler function as a route
app.add_route(handler1, '/test')
app.add_route(handler2, '/folder/<name>')
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
```
**Previous:** [Getting Started](getting_started.md)
**Next:** [Request Data](request_data.md)

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,12 +153,27 @@ 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)
```
**Previous:** [Exceptions](exceptions.md)
## URL Building with `url_for`
If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name
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) # --> '/v1/post/5'
return redirect(url)
@blueprint_v1.route('/post/<post_id>')
async def post_handler(request, post_id):
return text('Post {} in Blueprint V1'.format(post_id))
```
**Next:** [Class-based views](class_based_views.md)

View File

@@ -67,7 +67,7 @@ app.add_route(NameView.as_view(), '/<name>')
If you want to add any decorators to the class, you can set the `decorators`
class variable. These will be applied to the class when `as_view` is called.
```
```python
class ViewWithDecorator(HTTPMethodView):
decorators = [some_decorator_here]
@@ -77,6 +77,27 @@ class ViewWithDecorator(HTTPMethodView):
app.add_route(ViewWithDecorator.as_view(), '/url')
```
#### URL Building
If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint
that you will pass into `url_for`. For example:
```python
@app.route('/')
def index(request):
url = app.url_for('SpecialClassView')
return redirect(url)
class SpecialClassView(HTTPMethodView):
def get(self, request):
return text('Hello from the Special Class View!')
app.add_route(SpecialClassView.as_view(), '/special_class_view')
```
## Using CompositionView
As an alternative to the `HTTPMethodView`, you can use `CompositionView` to
@@ -107,6 +128,4 @@ view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
app.add_route(view, '/')
```
**Previous:** [Blueprints](blueprints.md)
**Next:** [Cookies](cookies.md)
Note: currently you cannot build a URL for a CompositionView using `url_for`.

View File

@@ -31,5 +31,3 @@ One of the main goals of Sanic is speed. Code that lowers the performance of
Sanic without significant gains in usability, security, or features may not be
merged. Please don't let this intimidate you! If you have any concerns about an
idea, open an issue for discussion and help.
**Previous:** [Sanic extensions](extensions.md)

View File

@@ -73,7 +73,3 @@ parameters available:
HTTPS.
- `httponly` (boolean): Specifies whether the cookie cannot be read by
Javascript.
**Previous:** [Class-based views](class_based_views.md)
**Next:** [Custom protocols](custom_protocol.md)

View File

@@ -70,7 +70,3 @@ async def response(request):
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
```
**Previous:** [Cookies](cookies.md)
**Next:** [Testing](testing.md)

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.
@@ -53,7 +44,3 @@ directly run by the interpreter.
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337, workers=4)
```
**Previous:** [Request Data](request_data.md)
**Next:** [Static Files](static_files.md)

View File

@@ -43,7 +43,3 @@ Some of the most useful exceptions are presented below:
usually occurs if there is an exception raised in user code.
See the `sanic.exceptions` module for the full list of exceptions to throw.
**Previous:** [Middleware](middleware.md)
**Next:** [Blueprints](blueprints.md)

14
docs/sanic/extensions.md Normal file
View File

@@ -0,0 +1,14 @@
# Extensions
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.
- [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

@@ -25,5 +25,3 @@ syntax, so earlier versions of python won't work.
the message *Hello world!*.
You now have a working Sanic server!
**Next:** [Routing](routing.md)

25
docs/sanic/index.rst Normal file
View File

@@ -0,0 +1,25 @@
Sanic
=================================
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
Sanic aspires to be simple:
-------------------
.. code:: python
from sanic import Sanic
from sanic.response import json
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@@ -64,7 +64,3 @@ async def halt_request(request):
async def halt_response(request, response):
return text('I halted the response')
```
**Previous:** [Static Files](static_files.md)
**Next:** [Exceptions](exceptions.md)

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
@@ -89,7 +106,3 @@ args.get('titles') # => 'Post 1'
args.getlist('titles') # => ['Post 1', 'Post 2']
```
**Previous:** [Routing](routing.md)
**Next:** [Deploying](deploying.md)

183
docs/sanic/routing.md Normal file
View File

@@ -0,0 +1,183 @@
# Routing
Routing allows the user to specify handler functions for different URL endpoints.
A basic route looks like the following, where `app` is an instance of the
`Sanic` class:
```python
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
returns a JSON object.
Sanic handler functions must be defined using the `async def` syntax, as they
are asynchronous functions.
## Request parameters
Sanic comes with a basic router that supports request parameters.
To specify a parameter, surround it with angle quotes like so: `<PARAM>`.
Request parameters will be passed to the route handler functions as keyword
arguments.
```python
from sanic.response import text
@app.route('/tag/<tag>')
async def tag_handler(request, tag):
return text('Tag - {}'.format(tag))
```
To specify a type for the parameter, add a `:type` after the parameter name,
inside the quotes. If the parameter does not match the specified type, Sanic
will throw a `NotFound` exception, resulting in a `404: Page not found` error
on the URL.
```python
from sanic.response import text
@app.route('/number/<integer_arg:int>')
async def integer_handler(request, integer_arg):
return text('Integer - {}'.format(integer_arg))
@app.route('/number/<number_arg:number>')
async def number_handler(request, number_arg):
return text('Number - {}'.format(number_arg))
@app.route('/person/<name:[A-z]>')
async def person_handler(request, name):
return text('Person - {}'.format(name))
@app.route('/folder/<folder_id:[A-z0-9]{0,4}>')
async def folder_handler(request, folder_id):
return text('Folder - {}'.format(folder_id))
```
## HTTP request types
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`,
which allows the handler function to work with any of the HTTP methods in the list.
```python
from sanic.response import text
@app.route('/post', methods=['POST'])
async def post_handler(request):
return text('POST request - {}'.format(request.json))
@app.route('/get', methods=['GET'])
async def get_handler(request):
return text('GET request - {}'.format(request.args))
```
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
from sanic.response import text
@app.post('/post')
async def post_handler(request):
return text('POST request - {}'.format(request.json))
@app.get('/get')
async def get_handler(request):
return text('GET request - {}'.format(request.args))
```
## The `add_route` method
As we have seen, routes are often specified using the `@app.route` decorator.
However, this decorator is really just a wrapper for the `app.add_route`
method, which is used as follows:
```python
from sanic.response import text
# Define the handler functions
async def handler1(request):
return text('OK')
async def handler2(request, name):
return text('Folder - {}'.format(name))
async def person_handler2(request, name):
return text('Person - {}'.format(name))
# Add each handler function as a route
app.add_route(handler1, '/test')
app.add_route(handler2, '/folder/<name>')
app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
```
## URL building with `url_for`
Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example:
```python
@app.route('/')
async def index(request):
# generate a URL for the endpoint `post_handler`
url = app.url_for('post_handler', post_id=5)
# the URL is `/posts/5`, redirect to it
return redirect(url)
@app.route('/posts/<post_id>')
async def post_handler(request, post_id):
return text('Post - {}'.format(post_id))
```
Other things to keep in mind when using `url_for`:
- Keyword arguments passed to `url_for` that are not request parameters will be included in the URL's query string. For example:
```python
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.

12
docs/sanic/ssl.rst Normal file
View File

@@ -0,0 +1,12 @@
SSL Example
-----------
Optionally pass in an SSLContext:
.. code:: python
import ssl
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain("/path/to/cert", keyfile="/path/to/keyfile")
app.run(host="0.0.0.0", port=8443, ssl=context)

View File

@@ -18,6 +18,4 @@ app.static('/the_best.png', '/home/ubuntu/test.png')
app.run(host="0.0.0.0", port=8000)
```
**Previous:** [Deploying](deploying.md)
**Next:** [Middleware](middleware.md)
Note: currently you cannot build a URL for a static file using `url_for`.

73
docs/sanic/testing.md Normal file
View File

@@ -0,0 +1,73 @@
# Testing
Sanic endpoints can be tested locally using the `test_client` object, which
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
library.
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.
- `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 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).
### 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
def test_index_returns_200():
request, response = sanic_endpoint_test(app)
assert response.status == 200
```

View File

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

18
environment.yml Normal file
View File

@@ -0,0 +1,18 @@
name: py35
dependencies:
- openssl=1.0.2g=0
- pip=8.1.1=py35_0
- python=3.5.1=0
- readline=6.2=2
- setuptools=20.3=py35_0
- sqlite=3.9.2=0
- tk=8.5.18=0
- wheel=0.29.0=py35_0
- xz=5.0.5=1
- zlib=1.2.8=0
- pip:
- uvloop>=0.5.3
- httptools>=0.0.9
- ujson>=1.35
- aiofiles>=0.3.0
- https://github.com/channelcat/docutils-fork/zipball/master

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

@@ -3,6 +3,7 @@ from sanic.response import json
from multiprocessing import Event
from signal import signal, SIGINT
import asyncio
import uvloop
app = Sanic(__name__)
@@ -10,10 +11,11 @@ app = Sanic(__name__)
async def test(request):
return json({"answer": "42"})
asyncio.set_event_loop(uvloop.new_event_loop())
server = app.create_server(host="0.0.0.0", port=8001)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
signal(SIGINT, lambda s, f: loop.close())
signal(SIGINT, lambda s, f: loop.stop())
try:
loop.run_forever()
except:

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)

2
readthedocs.yml Normal file
View File

@@ -0,0 +1,2 @@
conda:
file: environment.yml

View File

@@ -1,17 +1,18 @@
aiocache
aiofiles
aiohttp
beautifulsoup4
bottle
coverage
falcon
gunicorn
httptools
kyoukai
pytest
recommonmark
sphinx
sphinx_rtd_theme
tornado
tox
ujson
uvloop
aiohttp
aiocache
pytest
coverage
tox
gunicorn
bottle
kyoukai
falcon
tornado
aiofiles
sphinx
recommonmark
beautifulsoup4

View File

@@ -1,4 +1,4 @@
aiofiles
httptools
ujson
uvloop
aiofiles

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.0'
__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')

591
sanic/app.py Normal file
View File

@@ -0,0 +1,591 @@
import logging
import re
import warnings
from asyncio import get_event_loop
from collections import deque, defaultdict
from functools import partial
from inspect import isawaitable, stack, getmodulename
from traceback import format_exc
from urllib.parse import urlencode, urlunparse
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):
# 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:
formatter = logging.Formatter(
"%(asctime)s: %(levelname)s: %(message)s")
handler = logging.StreamHandler()
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 ErrorHandler()
self.config = Config()
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
self._blueprint_order = []
self.debug = None
self.sock = 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):
"""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
"""
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
if not uri.startswith('/'):
uri = '/' + uri
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler,
host=host)
return handler
return response
# Shorthand method decorators
def get(self, uri, host=None):
return self.route(uri, methods=frozenset({"GET"}), host=host)
def post(self, uri, host=None):
return self.route(uri, methods=frozenset({"POST"}), host=host)
def put(self, uri, host=None):
return self.route(uri, methods=frozenset({"PUT"}), host=host)
def head(self, uri, host=None):
return self.route(uri, methods=frozenset({"HEAD"}), host=host)
def options(self, uri, host=None):
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host)
def patch(self, uri, host=None):
return self.route(uri, methods=frozenset({"PATCH"}), host=host)
def delete(self, uri, host=None):
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
functions as a handler to the application url
routes.
:param handler: function or class instance
: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 = 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 remove_route(self, uri, clean_cache=True, host=None):
self.router.remove(uri, clean_cache, host)
# Decorator
def exception(self, *exceptions):
"""Decorate a function to be registered as a handler for exceptions
:param exceptions: exceptions
:return: decorated function
"""
def response(handler):
for exception in exceptions:
self.error_handler.add(exception, handler)
return handler
return response
# Decorator
def middleware(self, middleware_or_request):
"""Decorate and register middleware to be called before a request.
Can either be called as @app.middleware or @app.middleware('request')
"""
def register_middleware(middleware, attach_to='request'):
if attach_to == 'request':
self.request_middleware.append(middleware)
if attach_to == 'response':
self.response_middleware.appendleft(middleware)
return middleware
# Detect which way this was called, @middleware or @middleware('AT')
if callable(middleware_or_request):
return register_middleware(middleware_or_request)
else:
return partial(register_middleware,
attach_to=middleware_or_request)
# Static Files
def static(self, uri, file_or_directory, pattern='.+',
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_content_range)
def blueprint(self, blueprint, **options):
"""Register a blueprint on the application.
:param blueprint: Blueprint object
:param options: option dictionary with blueprint defaults
:return: Nothing
"""
if blueprint.name in self.blueprints:
assert self.blueprints[blueprint.name] is blueprint, \
'A blueprint with the name "%s" is already registered. ' \
'Blueprint names must be unique.' % \
(blueprint.name,)
else:
self.blueprints[blueprint.name] = blueprint
self._blueprint_order.append(blueprint)
blueprint.register(self, options)
def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0
if self.debug:
warnings.simplefilter('default')
warnings.warn("Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method"
" instead",
DeprecationWarning)
return self.blueprint(*args, **kwargs)
def url_for(self, view_name: str, **kwargs):
"""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
specified parameter type. If these conditions are not met, a
`URLBuildError` will be thrown.
Keyword arguments that are not request parameters will be included in
the output URL's query string.
:param view_name: string referencing the view name
:param **kwargs: keys and values that are used to build request
parameters and query string arguments.
:return: the built URL
Raises:
URLBuildError
"""
# find the route by the supplied view name
uri, route = self.router.find_route_by_view_name(view_name)
if not uri or not route:
raise URLBuildError(
'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)
# we only want to match against each individual parameter
specific_pattern = '^{}$'.format(pattern)
supplied_param = None
if kwargs.get(name):
supplied_param = kwargs.get(name)
del kwargs[name]
else:
raise URLBuildError(
'Required parameter `{}` was not passed to url_for'.format(
name))
supplied_param = str(supplied_param)
# determine if the parameter supplied by the caller passes the test
# in the URL
passes_pattern = re.match(specific_pattern, supplied_param)
if not passes_pattern:
if _type != str:
msg = (
'Value "{}" for parameter `{}` does not '
'match pattern for type `{}`: {}'.format(
supplied_param, name, _type.__name__, pattern))
else:
msg = (
'Value "{}" for parameter `{}` '
'does not satisfy pattern {}'.format(
supplied_param, name, pattern))
raise URLBuildError(msg)
# replace the parameter in the URL with the supplied value
replacement_regex = '(<{}.*?>)'.format(name)
out = re.sub(
replacement_regex, supplied_param, out)
# parse the remainder of the keyword arguments into a querystring
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
# -------------------------------------------------------------------- #
# Request Handling
# -------------------------------------------------------------------- #
def converted_response_type(self, response):
pass
async def handle_request(self, request, response_callback):
"""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
:param response_callback: Response function to be called with the
response as the only argument
:return: Nothing
"""
try:
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
request.app = self
response = False
# The if improves speed. I don't know why
if self.request_middleware:
for middleware in self.request_middleware:
response = middleware(request)
if isawaitable(response):
response = await response
if response:
break
# No middleware results
if not response:
# -------------------------------------------- #
# Execute Handler
# -------------------------------------------- #
# Fetch handler from router
handler, args, kwargs = self.router.get(request)
if handler is None:
raise ServerError(
("'None' was returned while requesting a "
"handler from the router"))
# Run response handler
response = handler(request, *args, **kwargs)
if isawaitable(response):
response = await response
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
if self.response_middleware:
for middleware in self.response_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
if _response:
response = _response
break
except Exception as e:
# -------------------------------------------- #
# Response Generation Failed
# -------------------------------------------- #
try:
response = self.error_handler.response(request, e)
if isawaitable(response):
response = await response
except Exception as e:
if self.debug:
response = HTTPResponse(
"Error while handling error: {}\nStack: {}".format(
e, format_exc()))
else:
response = HTTPResponse(
"An error occurred while handling an error")
response_callback(response)
# -------------------------------------------------------------------- #
# Testing
# -------------------------------------------------------------------- #
@property
def test_client(self):
return TestClient(self)
# -------------------------------------------------------------------- #
# Execution
# -------------------------------------------------------------------- #
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
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):
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param before_start: Functions to be executed before the server starts
accepting connections
:param after_start: Functions to be executed after the server starts
accepting connections
:param before_stop: Functions to be executed when a stop signal is
received before it is respected
:param after_stop: Functions to be executed when all requests are
complete
:param ssl: SSLContext for SSL encryption of worker(s)
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:param loop:
:param backlog:
:param stop_event:
:param register_sys_signals:
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
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, 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:
log.exception(
'Experienced exception while trying to serve')
finally:
self.is_running = False
log.info("Server Stopped")
def stop(self):
"""This kills the Sanic"""
get_event_loop().stop()
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
before_start=None, after_start=None,
before_stop=None, after_stop=None, ssl=None,
sock=None, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None):
"""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 or get_event_loop(), protocol=protocol,
backlog=backlog, stop_event=stop_event,
run_async=True)
return await serve(**server_settings)
def _helper(self, host="127.0.0.1", port=8000, debug=False,
before_start=None, 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, run_async=False):
"""Helper function used by `run` and `create_server`."""
if loop is not None:
if debug:
warnings.simplefilter('default')
warnings.warn("Passing a loop will be deprecated in version"
" 0.4.0 https://github.com/channelcat/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
server_settings = {
'protocol': protocol,
'host': host,
'port': port,
'sock': sock,
'ssl': ssl,
'debug': debug,
'request_handler': self.handle_request,
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop,
'register_sys_signals': register_sys_signals,
'backlog': backlog
}
# -------------------------------------------- #
# Register start/stop events
# -------------------------------------------- #
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):
listeners.append(args)
else:
listeners.extend(args)
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners
if debug:
log.setLevel(logging.DEBUG)
if self.config.LOGO is not None:
log.debug(self.config.LOGO)
if run_async:
server_settings['run_async'] = True
# Serve
proto = "http"
if ssl is not None:
proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
return server_settings

View File

@@ -1,119 +1,114 @@
from collections import defaultdict
from collections import defaultdict, namedtuple
from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView
class BlueprintSetup:
"""
Creates a blueprint state like object.
"""
def __init__(self, blueprint, app, options):
self.app = app
self.blueprint = blueprint
self.options = options
url_prefix = self.options.get('url_prefix')
if url_prefix is None:
url_prefix = self.blueprint.url_prefix
#: The prefix that should be used for all URLs defined on the
#: blueprint.
self.url_prefix = url_prefix
def add_route(self, handler, uri, methods, host=None):
"""
A helper method to register a handler to the application url routes.
"""
if self.url_prefix:
uri = self.url_prefix + uri
if host is None:
host = self.blueprint.host
self.app.route(uri=uri, methods=methods, host=host)(handler)
def add_exception(self, handler, *args, **kwargs):
"""
Registers exceptions to sanic.
"""
self.app.exception(*args, **kwargs)(handler)
def add_static(self, uri, file_or_directory, *args, **kwargs):
"""
Registers static files to sanic.
"""
if self.url_prefix:
uri = self.url_prefix + uri
self.app.static(uri, file_or_directory, *args, **kwargs)
def add_middleware(self, middleware, *args, **kwargs):
"""
Registers middleware to sanic.
"""
if args or kwargs:
self.app.middleware(*args, **kwargs)(middleware)
else:
self.app.middleware(middleware)
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
FutureStatic = namedtuple('Route',
['uri', 'file_or_directory', 'args', 'kwargs'])
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
self.url_prefix = url_prefix
self.deferred_functions = []
self.listeners = defaultdict(list)
self.host = host
def record(self, func):
"""
Registers a callback function that is invoked when the blueprint is
registered on the application.
"""
self.deferred_functions.append(func)
def make_setup_state(self, app, options):
"""
Returns a new BlueprintSetup object
"""
return BlueprintSetup(self, app, options)
self.routes = []
self.exceptions = []
self.listeners = defaultdict(list)
self.middlewares = []
self.statics = []
def register(self, app, options):
"""
Registers the blueprint to the sanic app.
"""
state = self.make_setup_state(app, options)
for deferred in self.deferred_functions:
deferred(state)
"""Register the blueprint to the sanic app."""
url_prefix = options.get('url_prefix', self.url_prefix)
# Routes
for future in self.routes:
# attach the blueprint name to the handler so that it can be
# prefixed properly in the router
future.handler.__blueprintname__ = self.name
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
app.route(
uri=uri[1:] if uri.startswith('//') else uri,
methods=future.methods,
host=future.host or self.host
)(future.handler)
# Middleware
for future in self.middlewares:
if future.args or future.kwargs:
app.middleware(*future.args,
**future.kwargs)(future.middleware)
else:
app.middleware(future.middleware)
# Exceptions
for future in self.exceptions:
app.exception(*future.args, **future.kwargs)(future.handler)
# Static Files
for future in self.statics:
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
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):
self.record(lambda s: s.add_route(handler, uri, methods, host))
route = FutureRoute(handler, uri, methods, host)
self.routes.append(route)
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.
"""
self.record(lambda s: s.add_route(handler, uri, methods, host))
# 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):
@@ -122,13 +117,11 @@ class Blueprint:
return decorator
def middleware(self, *args, **kwargs):
"""
Creates a blueprint middleware from a decorated function.
"""
def register_middleware(middleware):
self.record(
lambda s: s.add_middleware(middleware, *args, **kwargs))
return middleware
"""Create a blueprint middleware from a decorated function."""
def register_middleware(_middleware):
future_middleware = FutureMiddleware(_middleware, args, kwargs)
self.middlewares.append(future_middleware)
return _middleware
# Detect which way this was called, @middleware or @middleware('AT')
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
@@ -139,19 +132,40 @@ 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):
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
exception = FutureException(handler, args, kwargs)
self.exceptions.append(exception)
return handler
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.
"""
self.record(
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))
static = FutureStatic(uri, file_or_directory, args, kwargs)
self.statics.append(static)
# Shorthand method decorators
def get(self, uri, host=None):
return self.route(uri, methods=["GET"], host=host)
def post(self, uri, host=None):
return self.route(uri, methods=["POST"], host=host)
def put(self, uri, host=None):
return self.route(uri, methods=["PUT"], host=host)
def head(self, uri, host=None):
return self.route(uri, methods=["HEAD"], host=host)
def options(self, uri, host=None):
return self.route(uri, methods=["OPTIONS"], host=host)
def patch(self, uri, host=None):
return self.route(uri, methods=["PATCH"], host=host)
def delete(self, uri, host=None):
return self.route(uri, methods=["DELETE"], host=host)

View File

@@ -1,5 +1,11 @@
class Config:
LOGO = """
import os
import types
class Config(dict):
def __init__(self, defaults=None):
super().__init__(defaults or {})
self.LOGO = """
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \\
@@ -20,6 +26,67 @@ class Config:
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀
"""
REQUEST_MAX_SIZE = 100000000 # 100 megababies
REQUEST_TIMEOUT = 60 # 60 seconds
ROUTER_CACHE_SIZE = 1024
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
self.REQUEST_TIMEOUT = 60 # 60 seconds
def __getattr__(self, attr):
try:
return self[attr]
except KeyError as ke:
raise AttributeError("Config has no '{}'".format(ke.args[0]))
def __setattr__(self, attr, value):
self[attr] = value
def from_envvar(self, variable_name):
"""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.
"""
config_file = os.environ.get(variable_name)
if not config_file:
raise RuntimeError('The environment variable %r is not set and '
'thus configuration could not be loaded.' %
variable_name)
return self.from_pyfile(config_file)
def from_pyfile(self, filename):
"""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')
module.__file__ = filename
try:
with open(filename) as config_file:
exec(compile(config_file.read(), filename, 'exec'),
module.__dict__)
except IOError as e:
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
self.from_object(module)
return True
def from_object(self, obj):
"""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.
Example usage::
from yourapplication import default_config
app.config.from_object(default_config)
You should not use this function to load the actual configuration but
rather configuration defaults. The actual config should be loaded
with :meth:`from_pyfile` and ideally from a location not within the
package because the package might be installed system wide.
:param obj: an object holding the configuration
"""
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

1
sanic/constants.py Normal file
View File

@@ -0,0 +1 @@
HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'PATCH', 'DELETE')

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
@@ -120,6 +117,10 @@ class ServerError(SanicException):
status_code = 500
class URLBuildError(SanicException):
status_code = 500
class FileNotFound(NotFound):
status_code = 404
@@ -137,68 +138,20 @@ class PayloadTooLarge(SanicException):
status_code = 413
class Handler:
handlers = None
class HeaderNotFound(SanicException):
status_code = 400
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)
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[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:
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,11 +1,14 @@
import re
from collections import defaultdict, namedtuple
from collections.abc import Iterable
from functools import lru_cache
from .config import Config
from .exceptions import NotFound, InvalidUsage
from .views import CompositionView
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
from sanic.exceptions import NotFound, InvalidUsage
from sanic.views import CompositionView
Route = namedtuple(
'Route',
['handler', 'methods', 'pattern', 'parameters', 'name'])
Parameter = namedtuple('Parameter', ['name', 'cast'])
REGEX_TYPES = {
@@ -15,6 +18,8 @@ REGEX_TYPES = {
'alpha': (str, r'[A-Za-z]+'),
}
ROUTER_CACHE_SIZE = 1024
def url_hash(url):
return url.count('/')
@@ -29,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:
@@ -58,42 +62,84 @@ class Router:
routes_static = None
routes_dynamic = None
routes_always_check = None
parameter_pattern = re.compile(r'<(.+?)>')
def __init__(self):
self.routes_all = {}
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
For example:
`parse_parameter_string('<param_one:[A-z]>')` ->
('param_one', str, '[A-z]')
:param parameter_string: String to parse
:return: tuple containing
(parameter_name, parameter_type, parameter_pattern)
"""
# We could receive NAME or NAME:PATTERN
name = parameter_string
pattern = 'string'
if ':' in parameter_string:
name, pattern = parameter_string.split(':', 1)
default = (str, pattern)
# Pull from pre-configured types
_type, pattern = REGEX_TYPES.get(pattern, default)
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:
@@ -103,16 +149,11 @@ class Router:
properties = {"unhashable": None}
def add_parameter(match):
# We could receive NAME or NAME:PATTERN
name = match.group(1)
pattern = 'string'
if ':' in name:
name, pattern = name.split(':', 1)
name, _type, pattern = self.parse_parameter_string(name)
default = (str, pattern)
# Pull from pre-configured types
_type, pattern = REGEX_TYPES.get(pattern, default)
parameter = Parameter(name=name, cast=_type)
parameter = Parameter(
name=name, cast=_type)
parameters.append(parameter)
# Mark the whole route as unhashable if it has the hash key in it
@@ -124,7 +165,7 @@ class Router:
return '({})'.format(pattern)
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri)
pattern = re.compile(r'^{}$'.format(pattern_string))
def merge_route(route, methods, handler):
@@ -149,13 +190,36 @@ class Router:
handler=view, methods=methods.union(route.methods))
return route
route = self.routes_all.get(uri)
if parameters:
# TODO: This is too complex, we need to reduce the complexity
if properties['unhashable']:
routes_to_check = self.routes_always_check
ndx, route = self.check_dynamic_route_exists(
pattern, routes_to_check)
else:
routes_to_check = self.routes_dynamic[url_hash(uri)]
ndx, route = self.check_dynamic_route_exists(
pattern, routes_to_check)
if ndx != -1:
# Pop the ndx of the route, no dups of the same route
routes_to_check.pop(ndx)
else:
route = self.routes_all.get(uri)
if route:
route = merge_route(route, methods, handler)
else:
# prefix the handler name with the blueprint name
# if available
if hasattr(handler, '__blueprintname__'):
handler_name = '{}.{}'.format(
handler.__blueprintname__, handler.__name__)
else:
handler_name = getattr(handler, '__name__', None)
route = Route(
handler=handler, methods=methods, pattern=pattern,
parameters=parameters)
parameters=parameters, name=handler_name)
self.routes_all[uri] = route
if properties['unhashable']:
@@ -165,6 +229,14 @@ class Router:
else:
self.routes_static[uri] = route
@staticmethod
def check_dynamic_route_exists(pattern, routes_to_check):
for ndx, route in enumerate(routes_to_check):
if route.pattern == pattern:
return ndx, route
else:
return -1, None
def remove(self, uri, clean_cache=True, host=None):
if host is not None:
uri = host + uri
@@ -184,55 +256,86 @@ class Router:
if clean_cache:
self._get.cache_clear()
def get(self, request):
@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.
:param view_name: string of view name to search by
:return: tuple containing (uri, Route)
"""
Gets a request handler based on the URL of the request, or raises an
if not view_name:
return (None, None)
for uri, route in self.routes_all.items():
if route.name == view_name:
return uri, route
return (None, None)
def get(self, request):
"""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=Config.ROUTER_CACHE_SIZE)
@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
# Check against known static routes
route = self.routes_static.get(url)
method_not_supported = InvalidUsage(
'Method {} not allowed for URL {}'.format(
method, url), status_code=405)
if route:
if route.methods and method not in route.methods:
raise method_not_supported
match = route.pattern.match(url)
else:
route_found = False
# Move on to testing all regex routes
for route in self.routes_dynamic[url_hash(url)]:
match = route.pattern.match(url)
if match:
route_found |= match is not None
# Do early method checking
if match and method in route.methods:
break
else:
# Lastly, check against all regex routes that cannot be hashed
for route in self.routes_always_check:
match = route.pattern.match(url)
if match:
route_found |= match is not None
# Do early method checking
if match and method in route.methods:
break
else:
# Route was found but the methods didn't match
if route_found:
raise method_not_supported
raise NotFound('Requested URL {} not found'.format(url))
if route.methods and method not in route.methods:
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
method, url), status_code=405)
kwargs = {p.name: p.cast(value)
for value, p
in zip(match.groups(1), route.parameters)}
return route.handler, [], kwargs
route_handler = route.handler
if hasattr(route_handler, 'handlers'):
route_handler = route_handler.handlers[method]
return route_handler, [], kwargs

View File

@@ -1,427 +0,0 @@
import logging
from asyncio import get_event_loop
from collections import deque
from functools import partial
from inspect import isawaitable, stack, getmodulename
from traceback import format_exc
from .config import Config
from .exceptions import Handler
from .exceptions import ServerError
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
class Sanic:
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:
formatter = logging.Formatter(
"%(asctime)s: %(levelname)s: %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(formatter)
log.addHandler(handler)
log.setLevel(logging.INFO)
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.config = Config()
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
self._blueprint_order = []
self.debug = None
self.sock = None
self.processes = None
# Register alternative method names
self.go_fast = self.run
# -------------------------------------------------------------------- #
# Registration
# -------------------------------------------------------------------- #
# Decorator
def route(self, uri, methods=frozenset({'GET'}), host=None):
"""
Decorates a function to be registered as a route
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: decorated function
"""
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
if not uri.startswith('/'):
uri = '/' + uri
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler,
host=host)
return handler
return response
# Shorthand method decorators
def get(self, uri, host=None):
return self.route(uri, methods=["GET"], host=host)
def post(self, uri, host=None):
return self.route(uri, methods=["POST"], host=host)
def put(self, uri, host=None):
return self.route(uri, methods=["PUT"], host=host)
def head(self, uri, host=None):
return self.route(uri, methods=["HEAD"], host=host)
def options(self, uri, host=None):
return self.route(uri, methods=["OPTIONS"], host=host)
def patch(self, uri, host=None):
return self.route(uri, methods=["PATCH"], host=host)
def add_route(self, handler, uri, methods=None, host=None):
"""
A helper method to register class instance or
functions as a handler to the application url
routes.
:param handler: function or class instance
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: function or class instance
"""
self.route(uri=uri, methods=methods, host=host)(handler)
return handler
def remove_route(self, uri, clean_cache=True, host=None):
self.router.remove(uri, clean_cache, host)
# Decorator
def exception(self, *exceptions):
"""
Decorates a function to be registered as a handler for exceptions
:param \*exceptions: exceptions
:return: decorated function
"""
def response(handler):
for exception in exceptions:
self.error_handler.add(exception, handler)
return handler
return response
# Decorator
def middleware(self, *args, **kwargs):
"""
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):
if attach_to == 'request':
self.request_middleware.append(middleware)
if attach_to == 'response':
self.response_middleware.appendleft(middleware)
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])
else:
attach_to = args[0]
return register_middleware
# 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
"""
static_register(self, uri, file_or_directory, pattern,
use_modified_since)
def blueprint(self, blueprint, **options):
"""
Registers a blueprint on the application.
:param blueprint: Blueprint object
:param options: option dictionary with blueprint defaults
:return: Nothing
"""
if blueprint.name in self.blueprints:
assert self.blueprints[blueprint.name] is blueprint, \
'A blueprint with the name "%s" is already registered. ' \
'Blueprint names must be unique.' % \
(blueprint.name,)
else:
self.blueprints[blueprint.name] = blueprint
self._blueprint_order.append(blueprint)
blueprint.register(self, options)
def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0
log.warning("Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method instead",
DeprecationWarning)
return self.blueprint(*args, **kwargs)
# -------------------------------------------------------------------- #
# Request Handling
# -------------------------------------------------------------------- #
def converted_response_type(self, response):
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
exception handling must be done here
:param request: HTTP Request object
:param response_callback: Response function to be called with the
response as the only argument
:return: Nothing
"""
try:
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
response = False
# The if improves speed. I don't know why
if self.request_middleware:
for middleware in self.request_middleware:
response = middleware(request)
if isawaitable(response):
response = await response
if response:
break
# No middleware results
if not response:
# -------------------------------------------- #
# Execute Handler
# -------------------------------------------- #
# Fetch handler from router
handler, args, kwargs = self.router.get(request)
if handler is None:
raise ServerError(
("'None' was returned while requesting a "
"handler from the router"))
# Run response handler
response = handler(request, *args, **kwargs)
if isawaitable(response):
response = await response
# -------------------------------------------- #
# Response Middleware
# -------------------------------------------- #
if self.response_middleware:
for middleware in self.response_middleware:
_response = middleware(request, response)
if isawaitable(_response):
_response = await _response
if _response:
response = _response
break
except Exception as e:
# -------------------------------------------- #
# Response Generation Failed
# -------------------------------------------- #
try:
response = self.error_handler.response(request, e)
if isawaitable(response):
response = await response
except Exception as e:
if self.debug:
response = HTTPResponse(
"Error while handling error: {}\nStack: {}".format(
e, format_exc()))
else:
response = HTTPResponse(
"An error occurred while handling an error")
response_callback(response)
# -------------------------------------------------------------------- #
# Execution
# -------------------------------------------------------------------- #
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
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.
:param host: Address to host on
:param port: Port to host on
:param debug: Enables debug output (slows server)
:param before_start: Functions to be executed before the server starts
accepting connections
:param after_start: Functions to be executed after the server starts
accepting connections
:param before_stop: Functions to be executed when a stop signal is
received before it is respected
:param after_stop: Functions to be executed when all requests are
complete
:param ssl: SSLContext for SSL encryption of worker(s)
:param sock: Socket for the server to accept connections from
:param workers: Number of processes
received before it is respected
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
self.error_handler.debug = debug
self.debug = debug
if loop is not None:
log.warning("Passing a loop will be deprecated in version 0.4.0"
" https://github.com/channelcat/sanic/pull/335"
" has more information.", DeprecationWarning)
self.loop = loop
server_settings = {
'protocol': protocol,
'host': host,
'port': port,
'sock': sock,
'ssl': ssl,
'debug': debug,
'request_handler': self.handle_request,
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'register_sys_signals': register_sys_signals,
'backlog': backlog
}
# -------------------------------------------- #
# 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]
if args:
if callable(args):
args = [args]
listeners += args
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners
if debug:
log.setLevel(logging.DEBUG)
log.debug(self.config.LOGO)
# Serve
proto = "http"
if ssl is not None:
proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
try:
if workers == 1:
serve(**server_settings)
else:
serve_multiple(server_settings, workers, stop_event)
except Exception as e:
log.exception(
'Experienced exception while trying to serve')
log.info("Server Stopped")
def stop(self):
"""This kills the Sanic"""
get_event_loop().stop()
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
before_start=None, after_start=None,
before_stop=None, after_stop=None, ssl=None,
sock=None, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None):
"""
Asynchronous version of `run`.
"""
if loop is not None:
log.warning("Passing a loop will be deprecated in version 0.4.0"
" https://github.com/channelcat/sanic/pull/335"
" has more information.", DeprecationWarning)
loop = get_event_loop()
server_settings = {
'protocol': protocol,
'host': host,
'port': port,
'sock': sock,
'ssl': ssl,
'debug': debug,
'request_handler': self.handle_request,
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop,
'backlog': backlog
}
# -------------------------------------------- #
# 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]
if args:
if callable(args):
args = [args]
listeners += args
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners
server_settings['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)

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
@@ -13,16 +14,15 @@ from time import time
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
@@ -32,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
"""
@@ -168,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()
@@ -197,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()
@@ -227,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:
@@ -238,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:
@@ -251,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,
@@ -269,35 +272,35 @@ 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
"""
loop = async_loop.new_event_loop()
asyncio.set_event_loop(loop)
if not run_async:
loop = async_loop.new_event_loop()
asyncio.set_event_loop(loop)
if debug:
loop.set_debug(debug)
@@ -344,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))
@@ -374,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
@@ -384,9 +389,11 @@ def serve_multiple(server_settings, workers, stop_event=None):
:return:
"""
if server_settings.get('loop', None) is not None:
log.warning("Passing a loop will be deprecated in version 0.4.0"
" https://github.com/channelcat/sanic/pull/335"
" has more information.", DeprecationWarning)
if server_settings.get('debug', False):
warnings.simplefilter('default')
warnings.warn("Passing a loop will be deprecated in version 0.4.0"
" https://github.com/channelcat/sanic/pull/335"
" has more information.", DeprecationWarning)
server_settings['reuse_port'] = True
sock = socket()

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,57 +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)(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 = []
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):
results.append(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.append(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[0]
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)
@@ -64,11 +60,12 @@ class HTTPMethodView:
view.view_class = cls
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
view.__name__ = cls.__name__
return view
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.
@@ -88,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,22 +15,27 @@ 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',
name='sanic',
version=version,
url='http://github.com/channelcat/sanic/',
license='MIT',
author='Channel Cat',
author_email='channelcat@gmail.com',
description='A microframework based on uvloop, httptools, and learnings of flask',
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,6 +229,82 @@ 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
def test_bp_shorthand():
app = Sanic('test_shorhand_routes')
blueprint = Blueprint('test_shorhand_routes')
@blueprint.get('/get')
def handler(request):
return text('OK')
@blueprint.put('/put')
def handler(request):
return text('OK')
@blueprint.post('/post')
def handler(request):
return text('OK')
@blueprint.head('/head')
def handler(request):
return text('OK')
@blueprint.options('/options')
def handler(request):
return text('OK')
@blueprint.patch('/patch')
def handler(request):
return text('OK')
@blueprint.delete('/delete')
def handler(request):
return text('OK')
app.blueprint(blueprint)
request, response = app.test_client.get('/get')
assert response.text == 'OK'
request, response = app.test_client.post('/get')
assert response.status == 405
request, response = app.test_client.put('/put')
assert response.text == 'OK'
request, response = app.test_client.get('/post')
assert response.status == 405
request, response = app.test_client.post('/post')
assert response.text == 'OK'
request, response = app.test_client.get('/post')
assert response.status == 405
request, response = app.test_client.head('/head')
assert response.status == 200
request, response = app.test_client.get('/head')
assert response.status == 405
request, response = app.test_client.options('/options')
assert response.text == 'OK'
request, response = app.test_client.get('/options')
assert response.status == 405
request, response = app.test_client.patch('/patch')
assert response.text == 'OK'
request, response = app.test_client.get('/patch')
assert response.status == 405
request, response = app.test_client.delete('/delete')
assert response.text == 'OK'
request, response = app.test_client.get('/delete')
assert response.status == 405

76
tests/test_config.py Normal file
View File

@@ -0,0 +1,76 @@
from os import environ
import pytest
from tempfile import NamedTemporaryFile
from sanic import Sanic
def test_load_from_object():
app = Sanic('test_load_from_object')
class Config:
not_for_config = 'should not be used'
CONFIG_VALUE = 'should be used'
app.config.from_object(Config)
assert 'CONFIG_VALUE' in app.config
assert app.config.CONFIG_VALUE == 'should be used'
assert 'not_for_config' not in app.config
def test_load_from_file():
app = Sanic('test_load_from_file')
config = b"""
VALUE = 'some value'
condition = 1 == 1
if condition:
CONDITIONAL = 'should be set'
"""
with NamedTemporaryFile() as config_file:
config_file.write(config)
config_file.seek(0)
app.config.from_pyfile(config_file.name)
assert 'VALUE' in app.config
assert app.config.VALUE == 'some value'
assert 'CONDITIONAL' in app.config
assert app.config.CONDITIONAL == 'should be set'
assert 'condition' not in app.config
def test_load_from_missing_file():
app = Sanic('test_load_from_missing_file')
with pytest.raises(IOError):
app.config.from_pyfile('non-existent file')
def test_load_from_envvar():
app = Sanic('test_load_from_envvar')
config = b"VALUE = 'some value'"
with NamedTemporaryFile() as config_file:
config_file.write(config)
config_file.seek(0)
environ['APP_CONFIG'] = config_file.name
app.config.from_envvar('APP_CONFIG')
assert 'VALUE' in app.config
assert app.config.VALUE == 'some value'
def test_load_from_missing_envvar():
app = Sanic('test_load_from_missing_envvar')
with pytest.raises(RuntimeError):
app.config.from_envvar('non-existent variable')
def test_overwrite_exisiting_config():
app = Sanic('test_overwrite_exisiting_config')
app.config.DEFAULT = 1
class Config:
DEFAULT = 2
app.config.from_object(Config)
assert app.config.DEFAULT == 2
def test_missing_config():
app = Sanic('test_missing_config')
with pytest.raises(AttributeError):
app.config.NON_EXISTENT

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

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

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')
@@ -28,37 +27,43 @@ def handler_4(request):
return text(foo)
@exception_handler_app.route('/5')
def handler_5(request):
class CustomServerError(ServerError):
status_code=200
raise CustomServerError('Custom server error')
@exception_handler_app.exception(NotFound, ServerError)
def handler_exception(request, exception):
return text("OK")
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)
@@ -71,3 +76,8 @@ def test_html_traceback_output_in_debug_mode():
assert (
"NameError: name 'bar' "
"is not defined while handling uri /4") == summary_text
def test_inherited_exception_handler():
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'

91
tests/test_redirect.py Normal file
View File

@@ -0,0 +1,91 @@
import pytest
from sanic import Sanic
from sanic.response import text, redirect
@pytest.fixture
def redirect_app():
app = Sanic('test_redirection')
@app.route('/redirect_init')
async def redirect_init(request):
return redirect("/redirect_target")
@app.route('/redirect_init_with_301')
async def redirect_init_with_301(request):
return redirect("/redirect_target", status=301)
@app.route('/redirect_target')
async def redirect_target(request):
return text('OK')
@app.route('/1')
def handler(request):
return redirect('/2')
@app.route('/2')
def handler(request):
return redirect('/3')
@app.route('/3')
def handler(request):
return text('OK')
return app
def test_redirect_default_302(redirect_app):
"""
We expect a 302 default status code and the headers to be set.
"""
request, response = redirect_app.test_client.get(
'/redirect_init',
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
assert response.headers["Content-Type"] == 'text/html; charset=utf-8'
def test_redirect_headers_none(redirect_app):
request, response = redirect_app.test_client.get(
uri="/redirect_init",
headers=None,
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
def test_redirect_with_301(redirect_app):
"""
Test redirection with a different status code.
"""
request, response = redirect_app.test_client.get(
"/redirect_init_with_301",
allow_redirects=False)
assert response.status == 301
assert response.headers["Location"] == "/redirect_target"
def test_get_then_redirect_follow_redirect(redirect_app):
"""
With `allow_redirects` we expect a 200.
"""
request, response = redirect_app.test_client.get(
"/redirect_init",
allow_redirects=True)
assert response.status == 200
assert response.text == 'OK'
def test_chained_redirect(redirect_app):
"""Test test_client is working for redirection"""
request, response = redirect_app.test_client.get('/1')
assert request.url.endswith('/1')
assert response.status == 200
assert response.text == 'OK'
assert response.url.endswith('/3')

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,10 +56,10 @@ 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'
def test_invalid_response():
app = Sanic('test_invalid_response')
@@ -70,11 +71,11 @@ 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."
def test_json():
app = Sanic('test_json')
@@ -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,76 +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'
@pytest.fixture
def redirect_app():
app = Sanic('test_redirection')
@app.route('/redirect_init')
async def redirect_init(request):
return redirect("/redirect_target")
@app.route('/redirect_init_with_301')
async def redirect_init_with_301(request):
return redirect("/redirect_target", status=301)
@app.route('/redirect_target')
async def redirect_target(request):
return text('OK')
return app
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",
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
assert response.headers["Content-Type"] == 'text/html; charset=utf-8'
def test_redirect_headers_none(redirect_app):
request, response = sanic_endpoint_test(
redirect_app, method="get",
uri="/redirect_init",
headers=None,
allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/redirect_target"
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",
allow_redirects=False)
assert response.status == 301
assert response.headers["Location"] == "/redirect_target"
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,
allow_redirects=True)
assert response.status == 200
assert response.text == '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')
@@ -16,47 +15,149 @@ def static_file_directory():
return static_directory
@pytest.fixture(scope='module')
def static_file_path(static_file_directory):
"""The path to the static file that we want to serve"""
return os.path.join(static_file_directory, 'test.file')
def get_file_path(static_file_directory, file_name):
return os.path.join(static_file_directory, file_name)
@pytest.fixture(scope='module')
def static_file_content(static_file_path):
def get_file_content(static_file_directory, file_name):
"""The content of the static file to check"""
with open(static_file_path, 'rb') as file:
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
return file.read()
def test_static_file(static_file_path, static_file_content):
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_file(static_file_directory, file_name):
app = Sanic('test_static')
app.static('/testing.file', static_file_path)
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 == static_file_content
assert response.body == get_file_content(static_file_directory, file_name)
def test_static_directory(
static_file_directory, static_file_path, static_file_content):
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
def test_static_directory(file_name, base_uri, static_file_directory):
app = Sanic('test_static')
app.static('/dir', static_file_directory)
app.static(base_uri, static_file_directory)
request, response = sanic_endpoint_test(app, uri='/dir/test.file')
request, response = app.test_client.get(
uri='{}/{}'.format(base_uri, file_name))
assert response.status == 200
assert response.body == static_file_content
assert response.body == get_file_content(static_file_directory, file_name)
def test_static_url_decode_file(static_file_directory):
decode_me_path = os.path.join(static_file_directory, 'decode me.txt')
with open(decode_me_path, 'rb') as file:
decode_me_contents = file.read()
@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('/dir', static_file_directory)
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt')
request, response = app.test_client.head('/testing.file')
assert response.status == 200
assert response.body == decode_me_contents
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)),)

298
tests/test_url_building.py Normal file
View File

@@ -0,0 +1,298 @@
import pytest as pytest
from urllib.parse import urlsplit, parse_qsl
from sanic import Sanic
from sanic.response import text
from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint
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:
# this is the easiest way to generate functions with dynamic names
exec('@app.route(name)\ndef {}(request):\n\treturn text("{}")'.format(
name, name))
@pytest.fixture
def simple_app():
app = Sanic('simple_app')
handler_names = list(string.ascii_letters)
_generate_handlers_from_names(app, handler_names)
return app
def test_simple_url_for_getting(simple_app):
for letter in string.ascii_letters:
url = simple_app.url_for(letter)
assert url == '/{}'.format(letter)
request, response = simple_app.test_client.get(url)
assert response.status == 200
assert response.text == letter
@pytest.mark.parametrize('args,url',
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
(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')
@app.route('/fail')
def fail():
return text('this should fail')
with pytest.raises(URLBuildError) as e:
app.url_for('passes')
assert str(e.value) == 'Endpoint with name `passes` was not found'
def test_fails_url_build_if_param_not_passed():
url = '/'
for letter in string.ascii_letters:
url += '<{}>/'.format(letter)
app = Sanic('fail_url_build')
@app.route(url)
def fail():
return text('this should fail')
fail_args = list(string.ascii_letters)
fail_args.pop()
fail_kwargs = {l: l for l in fail_args}
with pytest.raises(URLBuildError) as e:
app.url_for('fail', **fail_kwargs)
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>')
PASSING_KWARGS = {
'foo': 4, 'four_letter_string': 'woof',
'two_letter_string': 'ba', 'normal_string': 'normal',
'some_number': '1.001'}
EXPECTED_BUILT_URL = '/4/woof/ba/normal/1.001'
def test_fails_with_int_message():
app = Sanic('fail_url_build')
@app.route(COMPLEX_PARAM_URL)
def fail():
return text('this should fail')
failing_kwargs = dict(PASSING_KWARGS)
failing_kwargs['foo'] = 'not_int'
with pytest.raises(URLBuildError) as e:
app.url_for('fail', **failing_kwargs)
expected_error = (
'Value "not_int" for parameter `foo` '
'does not match pattern for type `int`: \d+')
assert str(e.value) == expected_error
def test_fails_with_two_letter_string_message():
app = Sanic('fail_url_build')
@app.route(COMPLEX_PARAM_URL)
def fail():
return text('this should fail')
failing_kwargs = dict(PASSING_KWARGS)
failing_kwargs['two_letter_string'] = 'foobar'
with pytest.raises(URLBuildError) as e:
app.url_for('fail', **failing_kwargs)
expected_error = (
'Value "foobar" for parameter `two_letter_string` '
'does not satisfy pattern [A-z]{2}')
assert str(e.value) == expected_error
def test_fails_with_number_message():
app = Sanic('fail_url_build')
@app.route(COMPLEX_PARAM_URL)
def fail():
return text('this should fail')
failing_kwargs = dict(PASSING_KWARGS)
failing_kwargs['some_number'] = 'foo'
with pytest.raises(URLBuildError) as e:
app.url_for('fail', **failing_kwargs)
expected_error = (
'Value "foo" for parameter `some_number` '
'does not match pattern for type `float`: [0-9\\\\.]+')
assert str(e.value) == expected_error
def test_adds_other_supplied_values_as_query_string():
app = Sanic('passes')
@app.route(COMPLEX_PARAM_URL)
def passes():
return text('this should pass')
new_kwargs = dict(PASSING_KWARGS)
new_kwargs['added_value_one'] = 'one'
new_kwargs['added_value_two'] = 'two'
url = app.url_for('passes', **new_kwargs)
query = dict(parse_qsl(urlsplit(url).query))
assert query['added_value_one'] == 'one'
assert query['added_value_two'] == 'two'
@pytest.fixture
def blueprint_app():
app = Sanic('blueprints')
first_print = Blueprint('first', url_prefix='/first')
second_print = Blueprint('second', url_prefix='/second')
@first_print.route('/foo')
def foo():
return text('foo from first')
@first_print.route('/foo/<param>')
def foo_with_param(request, param):
return text(
'foo from first : {}'.format(param))
@second_print.route('/foo') # noqa
def foo():
return text('foo from second')
@second_print.route('/foo/<param>') # noqa
def foo_with_param(request, param):
return text(
'foo from second : {}'.format(param))
app.blueprint(first_print)
app.blueprint(second_print)
return app
def test_blueprints_are_named_correctly(blueprint_app):
first_url = blueprint_app.url_for('first.foo')
assert first_url == '/first/foo'
second_url = blueprint_app.url_for('second.foo')
assert second_url == '/second/foo'
def test_blueprints_work_with_params(blueprint_app):
first_url = blueprint_app.url_for('first.foo_with_param', param='bar')
assert first_url == '/first/foo/bar'
second_url = blueprint_app.url_for('second.foo_with_param', param='bar')
assert second_url == '/second/foo/bar'
@pytest.fixture
def methodview_app():
app = Sanic('methodview')
class ViewOne(HTTPMethodView):
def get(self, request):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(ViewOne.as_view('view_one'), '/view_one')
class ViewTwo(HTTPMethodView):
def get(self, request):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(ViewTwo.as_view(), '/view_two')
return app
def test_methodview_naming(methodview_app):
viewone_url = methodview_app.url_for('ViewOne')
viewtwo_url = methodview_app.url_for('ViewTwo')
assert viewone_url == '/view_one'
assert viewtwo_url == '/view_two'

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,43 +1,45 @@
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
def test_methods():
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_methods(method):
app = Sanic('test_methods')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
return text('', headers={'method': 'GET'})
def post(self, request):
return text('I am post method')
return text('', headers={'method': 'POST'})
def put(self, request):
return text('I am put method')
return text('', headers={'method': 'PUT'})
def head(self, request):
return text('', headers={'method': 'HEAD'})
def options(self, request):
return text('', headers={'method': 'OPTIONS'})
def patch(self, request):
return text('I am patch method')
return text('', headers={'method': 'PATCH'})
def delete(self, request):
return text('I am delete method')
return text('', headers={'method': 'DELETE'})
app.add_route(DummyView.as_view(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post")
assert response.text == 'I am post method'
request, response = sanic_endpoint_test(app, method="put")
assert response.text == 'I am put method'
request, response = sanic_endpoint_test(app, method="patch")
assert response.text == 'I am patch method'
request, response = sanic_endpoint_test(app, method="delete")
assert response.text == 'I am delete method'
request, response = getattr(app.test_client, method.lower())('/')
assert response.headers['method'] == method
def test_unexisting_methods():
@@ -49,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 /'
@@ -65,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'
@@ -82,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'
@@ -99,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'
@@ -120,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
@@ -147,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
@@ -169,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'
@@ -191,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

10
tox.ini
View File

@@ -1,22 +1,26 @@
[tox]
envlist = py35, py36, flake8
[travis]
[travis]
python =
3.5: py35, flake8
3.6: py36, flake8
[testenv]
[testenv]
deps =
aiofiles
aiohttp
pytest
beautifulsoup4
coverage
commands =
pytest tests {posargs}
coverage erase
coverage run -m sanic.app
coverage report
[testenv:flake8]
deps =