Compare commits

...

361 Commits

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

* GIT1505: fix pytest version to address UT failures

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

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2019-03-06 08:36:03 -06:00
7
aa7f2759a6 Merge pull request #1438 from yunstanford/master
18.12 Release
2018-12-27 11:15:07 -08:00
Yun Xu
9b9dd67797 adopt CalVer: MM.YY.MICRO, 18.12.0 release 2018-12-27 11:00:38 -08:00
7
3f73bc075a Merge pull request #1437 from FlouieInCl/master
Fix typo in exceptions.md
2018-12-27 09:59:00 -08:00
Yun Xu
56989a017b 18.12 release 2018-12-27 08:55:17 -08:00
JeongKyungSeo
ada5918bc8 Fix typo in exceptions.md 2018-12-27 16:11:37 +09:00
Jacob
4efd450b32 Add tests (#1433)
* Add tests for remove_route()

* Add tests for sanic/router.py

* Add tests for sanic/cookies.py

* Disable reset logging in test_logging.py

* Add tests for sanic/request.py

* Add tests for ContentRangeHandler

* Add tests for exception at response middleware

* Fix cached_handlers for ErrorHandler.lookup()

* Add test for websocket request timeout

* Add tests for getting cookies of StreamResponse, Remove some unused variables in tests/test_cookies.py

* Add tests for nested error handle
2018-12-22 09:21:45 -06:00
Omar Ryhan
d2670664ba Update exceptions.md (#1431)
Documented error handling from ``app.error_handler.add``
Documented custom error handling by subclassing.
2018-12-22 09:21:03 -06:00
7
fa7405fe9c Merge pull request #1422 from ashleysommer/server_slots
Add in some server.py __slots__ attribute names that are missing.
2018-12-15 13:58:22 -08:00
Jacob
33297f48a5 Add tests (#1430) 2018-12-13 11:50:50 -06:00
Ashley Sommer
06297a1918 Add in some server.py __slots__ property names that are missing. 2018-12-03 11:22:17 +10:00
Harsha Narayana
aa0874b6d8 100% Coverage for Sanic Blueprint (#1419)
* add unit tests to completely cover blueprints

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>

* fix typo in the unit test code

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-25 13:56:34 -06:00
7
822ced6294 Merge pull request #1416 from chenjr0719/add_tests_for_static
Add tests for static and update document
2018-11-21 23:01:37 +08:00
jacob
1a59614f79 Add stream_large_files and host examples in static_file document 2018-11-20 14:28:08 +08:00
jacob
f2d528e52a Add tests for static 2018-11-20 12:28:00 +08:00
Hasan Ramezani
f7adc5f84c Fix remove_entity_headers helper function (#1415)
* Fix `remove_entity_headers` helper function

* Add test for `remove_entity_headers` helper function
2018-11-19 09:30:53 -06:00
7
e955e833c4 Merge pull request #33 from huge-success/master
Merge upstream master branch
2018-11-16 13:02:16 +08:00
Tim&Anna
096c44b910 Update extensions.md (#1263)
* Update extensions.md

add an extension: sanic-script

* Update extensions.md
2018-11-14 07:16:43 -06:00
Nir Galon
efb9a42045 Change deprecated verify_ssl to ssl (#1155) 2018-11-14 07:16:14 -06:00
7
296cda7801 Merge pull request #1411 from devArtoria/patch-2
ADD: Sanic-JWT-Extended extension to extension docs
2018-11-13 13:49:35 +08:00
Lewis
90b9d73244 ADD: Sanic-JWT-Extended extension 2018-11-13 14:39:29 +09:00
Richard K
c8b0e7f2a7 Created methods to append and finish body content on Request (#1379)
* created methods to append and finish body content on request.py so the underlying body instance can have certain flexibility; modified server.py to reflect these changes

* - made some adjustments (including the Request.body_init method) as requested by @ahopkins;
- created a new test with a custom Request class implementation of the flexibility provided by the new methods;
2018-11-12 09:11:41 -06:00
7
6ce88ab5a4 Merge pull request #1400 from chenjr0719/add_tests_for_log
Add test for sanic.root logger and update the docs of logging
2018-11-12 20:45:05 +08:00
7
e13ab805df Merge pull request #1409 from yunstanford/windows-ci
CI Support for Windows
2018-11-12 20:05:21 +08:00
Yun Xu
e58ea8c7b4 fix unit test for windows ci
fix unit tests for windows ci

add appveyor build status badge

add readthedoc build status badge
2018-11-12 01:04:53 -08:00
jacob
dd5bac61cb Update document for logging 2018-11-12 16:09:12 +08:00
Jacob
6270b27a97 Merge branch 'master' into add_tests_for_log 2018-11-12 09:53:44 +08:00
Hasan Ramezani
f89ba1d39f Add tests for is_entity_header and is_hop_by_hop_header helper functions (#1410) 2018-11-11 10:57:57 -06:00
Yun Xu
8b5d137d8f fix .appveyor.yml 2018-11-10 06:11:01 -08:00
Yun Xu
2629fab649 add .appveyor.yml for windows ci support 2018-11-10 05:50:22 -08:00
7
92cd10c6a8 Merge pull request #32 from huge-success/master
merge upstream master branch
2018-11-10 21:26:37 +08:00
7
cc3edb90dc Merge pull request #1408 from harshanarayana/feature/Unit_Test_Enhancements
Additional Unit Tests
2018-11-10 20:46:51 +08:00
Harsha Narayana
c60ba81984 cleanup stale test for cookie object
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-10 16:54:24 +05:30
Harsha Narayana
ece3cdaa2e add unit tests for App Config, Cokkies and Request handler
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-10 16:50:30 +05:30
7
4cb40f2042 Merge pull request #1403 from harshanarayana/fix/GIT-1398-Http_Response_Content_Length_Mismatch
Fix Content-Length Mismatch while using json and ujson
2018-11-10 00:14:03 +08:00
7
0e9f350982 Merge pull request #1405 from hramezani/test_has_message_body
Add test for has_message_body helper function.
2018-11-08 22:20:07 +08:00
Hasan Ramezani
cf439f01f8 Add test for has_message_body helper function. 2018-11-07 21:29:12 +01:00
Harsha Narayana
f1f1b8a630 add additional test cases to validate Content-Length header
Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-07 22:07:28 +05:30
Harsha Narayana
d4d1df03c9 fix content length mismatch in windows and other platform
The current implementation of `sanic` attempts to make use of `ujson` if
it's available in the system and if not, it will default to the inbuilt
`json` module provided by python.

The current implementation of `ujson` does not provide a mechanism to
provide a custom `seperators` parameter as part of the `dumps` method
invocation and the default behavior of the module is to strip all the
spaces around seperators such as `:` and `,`. This leads to an
inconsistency in the response length when the response is generated
using the `ujson` and in built `json` module provided by python.

To maintain the consistency, this commit overrides the default behavior
of the `dumps` method provided by the `json` module to add a `seperators`
argument that will strip the white spaces around these character like
the default behavior of `ujson`

This addresses the issue referenced in #1398

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-07 21:38:32 +05:30
Harsha Narayana
92b73a6f4f fix Range header handling for static files (#1402)
This commit fixes the issue in the `Range` header handling that was done
while serving the file contents.

As per the HTTP response standards, a status code of 206 will be used in
case if the Range is returning a partial value and default of 200 in
other cases.

Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
2018-11-07 07:36:56 -06:00
Meng Wang
b63c06c75a fix the logger and make it work (#1397)
* fix the logger and make it work

* modify test_logging parameters and add a new unit test
2018-11-06 08:39:38 -06:00
jacob
3e3bce422e Add test for sanic.root logger and update the docs of logging 2018-11-06 21:27:01 +08:00
Stephen Sadowski
e3a27c2cc4 Merge pull request #1391 from AndresSan6/loop_exception
Handle "loop" exception in app.py
2018-11-05 08:19:01 -06:00
Stephen Sadowski
f13f451084 Merge pull request #1385 from lixxu/master
update doc for latest blueprint code
2018-11-05 07:40:12 -06:00
Stephen Sadowski
df0e3de911 Merge pull request #1393 from ashleysommer/pickleable-app-blueprint
Fix pickling blueprints Fixes #1392
2018-11-05 07:24:15 -06:00
Ashley Sommer
8466be8728 Fix type pikcle->pickle in multiprocessing test 2018-11-04 15:27:25 +10:00
Ashley Sommer
5cf2144b3f Fix pickling blueprints
Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name.
This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows.
Added a test for pickling and unpickling blueprints
Added a test for pickling and unpickling sanic itself
Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
2018-11-04 15:04:12 +10:00
Andres Sanchez
7c182f63c8 Indentation fix 2018-11-01 10:59:45 -06:00
Andres Sanchez
056180782c Removed unnecessary changes to request and router files, changes to fix lint test 2018-11-01 10:53:53 -06:00
Andres Sanchez
ff0d5870e9 Merge branch 'lintfix' into loop_exception
Made changes unnecesarry changes in request and router files, went back to previous commit and made correct changes to fix lint
2018-11-01 10:40:47 -06:00
Andres Sanchez
b70176f8c7 Fixed character limit per line in requested changes for app.py 2018-11-01 10:36:34 -06:00
Andres Sanchez
e3655b525d Modifications to ruequest and router files to fix linting issues. 2018-11-01 10:04:40 -06:00
Andres Sanchez
e63d0091af Assert was chnaged for an if and updated error messages 2018-10-31 15:23:29 -06:00
Andres Sanchez
7b0af2d80d Handle loop exception in app.py 2018-10-31 13:35:03 -06:00
Stephen Sadowski
7d79a86d4d Merge pull request #1387 from huge-success/docbuild
Resolve build of latex documentation relating to markdown lists
2018-10-30 16:13:29 -05:00
Adam Hopkins
ba46aff069 Resolve build of latex documentation relating to markdown lists 2018-10-30 22:39:17 +02:00
lixxu
7a65471ba5 update doc for latest blueprint code 2018-10-29 16:54:34 +08:00
Stephen Sadowski
c7c46da975 Merge pull request #1383 from huge-success/docbuild
Fix documentation build errors
2018-10-26 08:19:10 -05:00
Adam Hopkins
c708e8425f Fix documentation build errors 2018-10-26 11:57:28 +03:00
Eli Uriegas
905c51bef0 Merge pull request #1371 from yunstanford/integrate-isort
codestyle: Integrate isort
2018-10-23 16:05:36 -07:00
Eli Uriegas
bd87098b7e Merge pull request #1368 from yunstanford/fix-redirect
Add '%' to quote_plus's `safe` parameter in response.redirect
2018-10-23 15:12:02 -07:00
Eli Uriegas
5f486cc25f Merge pull request #1378 from hramezani/fix_some_lint_error
Fix some test files lint errors.
2018-10-23 15:10:15 -07:00
Eli Uriegas
f79fb72a33 Merge pull request #1377 from yunstanford/fix-readthedoc-build
Fix readthedoc build
2018-10-23 15:07:25 -07:00
Yun Xu
0505aa2dda refactor import 2018-10-23 14:53:39 -07:00
Hasan Ramezani
485ff32e42 Fix all test files lint errors. 2018-10-23 11:04:17 +02:00
Stephen Sadowski
5ead67972f Merge pull request #1375 from sjsadowski/master
Added documentation for AF_INET6 and AF_UNIX socket usage
2018-10-21 15:28:40 -05:00
Yun Xu
9c860dbff3 fix readthedoc build 2018-10-21 01:56:48 -07:00
Stephen Sadowski
a20ad99638 Added documentation for AF_INET6 and AF_UNIX socket usage 2018-10-19 13:33:01 -05:00
Yun Xu
8ef7bf8e7b integrate with isort 2018-10-17 21:20:16 -07:00
7
0d5be1969a Merge pull request #31 from huge-success/master
Merge Upstream master branch
2018-10-17 21:02:44 -07:00
Adam Hopkins
d06ea9bfc3 Merge pull request #1370 from huge-success/ahopkins-patch-1
Update issue templates
2018-10-17 09:47:22 +03:00
Adam Hopkins
57e79882e1 Update issue templates 2018-10-16 15:42:52 +03:00
Yun Xu
20d1ab60c7 remove unused json import 2018-10-15 22:13:42 -07:00
Yun Xu
277c2ce2d2 fix redirect with quoted param 2018-10-15 21:53:11 -07:00
7
34e51f01d1 Merge pull request #30 from huge-success/master
Merge Upstream Master Branch
2018-10-15 20:04:57 -07:00
7
f4b4e3a58c Merge pull request #1366 from hramezani/lint_test_blueprints
Fix some lint errors and warnings in `tests/test_blueprints.py`
2018-10-14 21:02:48 -07:00
7
def2e033c8 Merge pull request #1365 from yunstanford/codestyle-black
Codestyle black
2018-10-14 10:07:09 -07:00
Hasan Ramezani
dfec18278b Fix some lint errors and warnings in tests/test_blueprints.py. 2018-10-14 16:09:47 +02:00
Yun Xu
cd5bdecda3 add codestyle badge in README 2018-10-13 18:33:02 -07:00
Yun Xu
9b6217ba41 fix travisci 2018-10-13 18:19:08 -07:00
Yun Xu
272f6e195d added black for lint check 2018-10-13 18:10:43 -07:00
Yun Xu
aa9bf04dfe run black against sanic module 2018-10-13 17:55:33 -07:00
7
9ae6dfb6d2 Merge pull request #29 from huge-success/master
merge upstream master branch
2018-10-13 17:28:32 -07:00
7
619bb79a2f Merge pull request #1336 from untitaker/logging-refactor
Try not to stringify exception in logging messages
2018-10-13 16:54:57 -07:00
7
0cad831eca Merge pull request #1364 from yunstanford/raise-exception-when-param-conflicts
Raise exception when param conflicts
2018-10-13 16:28:59 -07:00
Yun Xu
f15a7fb588 fix flake8 2018-10-12 23:06:43 -07:00
Yun Xu
1bdf9ca057 add py37 in setup.py 2018-10-12 22:58:49 -07:00
Yun Xu
c8c370b784 raise exception when param conflicts in route 2018-10-12 22:57:56 -07:00
7
63182f55f7 Merge pull request #28 from huge-success/master
Merge upstream master branch
2018-10-12 22:38:37 -07:00
Stephen Sadowski
41759248e2 Merge pull request #1361 from yunstanford/cancel-request-when-connection-lost
Cancel request when connection lost
2018-10-12 07:25:10 -05:00
Yun Xu
3149d5a66d add unit test for request_stream 2018-10-11 23:12:33 -07:00
Yun Xu
8b13597099 add unit tests for verifying 2018-10-11 23:02:21 -07:00
Yun Xu
36032cc26e cancel task when connection_lost 2018-10-11 22:38:26 -07:00
7
4cb107aedc Merge pull request #27 from huge-success/master
Merge upstream master branch
2018-10-11 22:34:09 -07:00
7
176f8d1981 Merge pull request #1358 from hramezani/fix_config_tests
Change the config test to remove `NamedTemporaryFile`
2018-10-11 21:39:48 -07:00
Hasan Ramezani
9a26030bd5 Change the config test to remove NamedTemporaryFile 2018-10-11 17:34:46 +02:00
Stephen Sadowski
6778f4d9e0 Merge pull request #1342 from hramezani/load_config_file_syntax_error
Handle syntax error in load config file.
2018-10-11 08:56:48 -05:00
Stephen Sadowski
fd61b9e3e2 Merge pull request #1327 from hatarist/fix-1323
Rename the `http` module to `helpers`
2018-10-11 07:56:51 -05:00
Stephen Sadowski
298d5cdf24 Merge pull request #1334 from chenjr0719/master
Fix TypeError when use Blueprint.group() to group blueprint with defa…
2018-10-11 07:28:10 -05:00
7
1bf1c9d006 Merge pull request #26 from huge-success/master
Merge upstream master branch
2018-10-10 20:33:57 -07:00
7
7dc62be5cf Merge pull request #1335 from abuckenheimer/fix_windows_unittests
unittests passing on windows again
2018-10-10 20:15:35 -07:00
jacob
be580a6a5b Clean up files created by pytest-html 2018-10-11 10:06:05 +08:00
7
8ce519668b Merge pull request #1353 from abn/fix-unhandled-exception
Simplify request ip and port retrieval logic
2018-10-09 23:33:51 -07:00
jacob
801258c46a Merge branch 'master' of github.com:chenjr0719/sanic 2018-10-10 14:04:45 +08:00
jacob
32a1db3622 Remove normpath 2018-10-10 14:04:21 +08:00
7
ed1f3daacc Merge pull request #1352 from devArtoria/patch-1
Fix missing quotes in decorator example
2018-10-08 21:57:34 -07:00
Alec Buckenheimer
b7d74c82ba simplified aiohttp version diffs, reverted worker import policy 2018-10-08 22:48:21 -04:00
Arun Babu Neelicattu
c3b31a6fb0 Simplify request ip and port retrieval logic
This change also ensures that cases where transport stream is
already closed is handled gracefully.
2018-10-08 21:25:47 +02:00
Hasan Ramezani
f4c55bbc07 Handle config error in load config file. 2018-10-08 19:17:06 +02:00
Lewis
a16842f7bc Fix missing quotes in decorator example 2018-10-08 18:59:15 +09:00
7
439a38664f Merge pull request #25 from huge-success/master
Merge upstream master branch
2018-10-07 20:32:52 -07:00
7
5cc12fd945 Merge pull request #1348 from hramezani/add_config_test
Add test for `config.from_object`.
2018-10-07 19:53:58 -07:00
7
fe116fff5a Merge pull request #1350 from hramezani/config_documentation
Add missed documentation for config section.
2018-10-07 13:58:06 -07:00
Stephen Sadowski
06aaaf4727 Merge pull request #1351 from yunstanford/integrate-with-codecov
Integrate with codecov
2018-10-07 10:13:31 -05:00
Yun Xu
6deb9b49b2 correct Codecov badge url 2018-10-06 21:39:04 -07:00
Yun Xu
d59e92d3e5 integrate with codecov 2018-10-06 21:31:04 -07:00
7
cc83c1f0cf Merge pull request #24 from huge-success/master
merge upstream master branch
2018-10-06 21:22:54 -07:00
Hasan Ramezani
1fe7306af8 Add missed documentation for config section. 2018-10-07 01:32:36 +02:00
Hasan Ramezani
c796d73fc3 Add test for config.from_object. 2018-10-07 00:14:37 +02:00
Markus Unterwaditzer
eb93f884f3 fix: Missing import 2018-10-05 16:47:12 +02:00
Markus Unterwaditzer
3673feb256 fix: typo 2018-10-05 16:33:46 +02:00
Markus Unterwaditzer
7c9c783e9d deprecate Handler.log 2018-10-05 16:31:01 +02:00
Stephen Sadowski
74a4b9efaa Merge pull request #1345 from huge-success/ahopkins-patch-1
Update README.rst
2018-10-04 18:45:47 -05:00
Stephen Sadowski
4466e8cce1 Merge pull request #1304 from ignatenkobrain/fedora
Switch to websockets 6.0
2018-10-04 18:45:22 -05:00
Adam Hopkins
b689037984 Update README.rst 2018-10-04 12:31:57 +03:00
Stephen Sadowski
db1ba21d88 Merge pull request #1343 from vltr/httptools_pinned
pinned httptools requirement to version 0.0.10+
2018-10-03 19:27:25 -05:00
Eli Uriegas
50d270ef7c Merge pull request #1316 from sjsadowski/master
Updated changelog.md for 0.8.x
2018-10-03 15:19:21 -07:00
Richard Kuesters
d1a578b555 pinned httptools requirement to version 0.0.10+ 2018-10-03 12:22:29 -03:00
Stephen Sadowski
76e9859cf8 Merge branch 'master' into master 2018-10-03 09:56:29 -05:00
Stephen Sadowski
add9d363c5 Merge branch 'master' into logging-refactor 2018-10-03 09:55:01 -05:00
Stephen Sadowski
1498baab0f Merge pull request #1338 from hramezani/improve_config_test
Check error message and fix some lint error in test config.
2018-10-03 09:18:46 -05:00
Stephen Sadowski
df7f63d45d Merge branch 'master' into improve_config_test 2018-10-03 06:30:44 -05:00
Stephen Sadowski
f7425126a1 Merge pull request #1341 from ashleysommer/unnecessary_code
Fixes #1340
2018-10-03 06:30:22 -05:00
Ashley Sommer
790047e450 Fixes #1340 2018-10-03 10:59:24 +10:00
Stephen Sadowski
9198b5b0be Merge branch 'master' into improve_config_test 2018-10-02 13:21:23 -05:00
Stephen Sadowski
d534acb79d Merge branch 'master' into logging-refactor 2018-10-01 15:41:07 -05:00
Hasan Ramezani
d100f54551 Check error message and fix some lint error in test config. 2018-10-01 20:36:21 +02:00
Stephen Sadowski
7a9e100b0f Merge branch 'master' into fix_windows_unittests 2018-10-01 10:10:48 -05:00
Stephen Sadowski
fafe23d7c2 Merge pull request #1337 from cmcaine/fix-error-msg
Fix whitespace in error message
2018-10-01 09:31:45 -05:00
Alec Buckenheimer
9a08bdae4a fix flake8 linelength errors 2018-10-01 09:46:18 -04:00
Colin Caine
bcc11fa7fe Fix whitespace in error message 2018-09-30 09:36:55 +01:00
Markus Unterwaditzer
7d0c0fdf7c fix: Namespacing of sanic logger 2018-09-29 22:40:05 +02:00
Markus Unterwaditzer
0e33d46ead Try not to stringify exception in logging messages
This just fixes the worst offenders that trip up error reporting tools
like Sentry.io
2018-09-29 22:32:51 +02:00
Alec Buckenheimer
efbacc17cf unittests passing on windows again 2018-09-29 13:54:47 -04:00
jacob
bd6dbd9090 Fix TypeError when use Blueprint.group() to group blueprint with default url_prefix, Use os.path.normpath to avoid invalid url_prefix like api//v1 2018-09-29 18:23:16 +08:00
Eli Uriegas
076cf51fb2 Merge pull request #1305 from Stranger6667/app-fixture
Reuse app fixture in tests
2018-09-26 18:30:46 -07:00
Igor Hatarist
f8a6af1e28 Rename the http module to helpers to prevent conflicts with the built-in Python http library (fixes #1323) 2018-09-25 20:46:40 +03:00
Stephen Sadowski
96912f436d Corrected Raphael Deem's name in changelog - sorry @r0fls! 2018-09-24 09:05:58 -05:00
Raphael Deem
f0e162442f Merge branch 'master' into app-fixture 2018-09-21 15:16:00 -07:00
Eli Uriegas
04b8dd989f Merge pull request #1315 from seemethere/multidocs
Add multidict to readthedocs environment.yml
2018-09-15 19:03:56 +02:00
Stephen Sadowski
5851c8bd91 revised formatting for CHANGELOG.md 2018-09-14 13:30:57 -05:00
Stephen Sadowski
78efcf93f8 Updated changelog for all accepted PRs from 0.7.0 to Current 2018-09-14 10:56:32 -05:00
Eli Uriegas
bb35bc3898 Add multidict to readthedocs environment.yml
Signed-off-by: Eli Uriegas <seemethere101@gmail.com>
2018-09-14 16:00:29 +02:00
Stephen Sadowski
f38783bdef Merge pull request #1 from huge-success/master
Merge from head
2018-09-14 08:20:37 -05:00
Channel Cat
d8f9986089 Re-releasing with updated credentials 2018-09-13 02:24:31 -07:00
Channel Cat
3e616b599a update encrypted creds for new org 2018-09-13 02:17:27 -07:00
Channel Cat
d38fc17191 Update version to test pypi 2018-09-13 01:50:32 -07:00
Channel Cat
7ae0eb0dc3 Transfer ownership 2018-09-13 01:39:24 -07:00
Channel Cat
9082eb56a7 Update version to circumvent pypi upload errors 2018-09-06 13:51:31 -07:00
Igor Gnatenko
c578974246 Switch to websockets 6.0
Signed-off-by: Igor Gnatenko <i.gnatenko.brain@gmail.com>
2018-09-02 09:23:30 +02:00
dmitry.dygalo
fec81ffe73 Reuse app fixture in tests 2018-08-26 16:43:14 +02:00
Ashley Sommer
30e6a310f1 Pausable response streams (#1179)
* This commit adds handlers for the asyncio/uvloop protocol callbacks for pause_writing and resume_writing.
These are needed for the correct functioning of built-in tcp flow-control provided by uvloop and asyncio.
This is somewhat of a breaking change, because the `write` function in user streaming callbacks now must be `await`ed.
This is necessary because it is possible now that the http protocol may be paused, and any calls to write may need to wait on an async event to be called to become unpaused.

Updated examples and tests to reflect this change.

This change does not apply to websocket connections. A change to websocket connections may be required to match this change.

* Fix a couple of PEP8 errors caused by previous rebase.

* update docs

add await syntax to response.write in response-streaming docs.

* remove commented out code from a test file
2018-08-18 18:12:13 -07:00
Eli Uriegas
a87934d434 Merge pull request #1292 from seemethere/increment_080
Increment to 0.8.0
2018-08-17 11:52:47 -07:00
Eli Uriegas
b398c1fe72 Increment to 0.8.0
Signed-off-by: Eli Uriegas <seemethere101@gmail.com>
2018-08-17 11:43:15 -07:00
Eli Uriegas
6f813f940e Merge pull request #1278 from ashleysommer/graceful_cancel
Gracefully handle when the request_handler_task is cancelled.
2018-08-17 11:41:39 -07:00
Eli Uriegas
d52498b787 Merge pull request #1284 from ashleysommer/aiohttp_update
Fix broken tests when aiohttp >= 3.3.0
2018-08-17 11:40:49 -07:00
Ashley Sommer
79e35bbdf6 Fix auto_reload in Linux (#1286)
* Fix two problems with the auto_reloader in Linux.
1) Change 'posix' to 'linux' in sys.plaform check, because 'posix' is an invalid value and 'linux' is the correct value to use here.
2) In kill_process_children, don't just kill the 2nd level procs, also kill the 1st level procs.
   Also in kill_process_children, catch and ignore errors in the case that the child proc is already killed.

* Fix flake8 formatting on PR
2018-08-16 23:30:03 -07:00
Innokenty Lebedev
1814ff05f4 Add sse extension (#1288) 2018-08-16 11:59:58 -07:00
Ashley Sommer
ec226e33cb Pin aiohttp <= 3.2.1 in requirements-dev.txt (fixes errors for new contributors checking out the code and setting up a dev environment)
Future-proof the some test cases so they work with aiohttp >= 3.3.0, in case we bump the aiohttp version in the future.
2018-08-16 15:00:23 +10:00
hqy
6abdf9f9c1 fixed #1143 (#1276)
* fixed #1143

* fixed build failed with create_serve call _helper failed
2018-08-15 10:23:04 -07:00
abuckenheimer
212da1029e disabled auto_reload by default in windows (#1280) 2018-08-07 11:48:18 -07:00
Ashley Sommer
afea15e4a7 Add a test for the graceful CancelledError handling. The user app should _never_ see a CancelledError bubble up, nor should they be able to catch it, because the response is already sent at that point. 2018-08-06 15:02:12 +10:00
Ashley Sommer
39ff02b6e4 Modifications the handle_request function to detect and gracefully handle the case that the request_handler Task is canceled by the sanic server while it is handling the request. One common occurrence of this is when the server issues a ResponseTimeout error, it also cancels the response_handler Task.
The Canceled exception handler purposely sets `response` to `None` to drop references to the handler coroutine, in an attempt to preemptively release resources.
This commit also fixes a possible reference-before-assignment of the `response` variable in the `handle_request` function.
Finally, another byproduct of this change is that ResponseMiddleware will no longer run if the `response` is `None`.
2018-08-06 14:12:30 +10:00
Cosmo Borsky
b238be54a4 Add content_type flag to Sanic.static (#1267)
* Add content_type flag to Sanic.static

Fixes #1266

* Fix flake8 error in travis

Add line to document `content_type` arg

* Fix content_type for file streams

Update tests

herp derp

* Remove content_type as an arg to HTTPResponse

`response.HTTPResponse` will default to `headers['Content-Type']` instead of `content_type`
https://github.com/channelcat/sanic/pull/1267#discussion_r204190913
2018-07-20 22:31:15 -07:00
Cosmo Borsky
377c9890a3 Support status code for file reponse (#1269)
Fixes #1268
2018-07-20 13:39:10 -07:00
ciscorn
599834b0e1 Add subprotocols param to add_websocket_route (#1261) 2018-07-16 12:20:26 -07:00
John Doe
a39a7ca9d5 Add url_bytes to Request (#1258)
We need to have access to the raw unparsed URL.
2018-07-16 12:13:27 -07:00
Ave
cd22745e6b Sanitize the URL before redirecting (#1260)
* URL Quote the URL before redirecting

* Use safe url instead of unsafe one

* Fix query params

* fix build

* Whitelist all reserved characters from rfc3986

* Add tests for redirect url sanitizing

* Remove check for resulting URL on header injection test

The thing the tests are testing for can be implemented in other
ways that don't redirect to 100% the same address, but they'll all have
to match the remaining parts of the test to succeed.
2018-07-12 21:31:33 -07:00
7
334649dfd4 Fix response ci header (#1244)
* add unit tests, which should fail

* fix CIDict

* moving CIDict to avoid circular imports

* fix unit tests

* use multidict for headers

* fix cookie

* add version constraint for multidict

* omit test coverage for __main__.py

* make flake8 happy

* consolidate check in for loop

* travisci retry build
2018-07-11 01:44:21 -07:00
fanjindong
becbc5f9ef fix one example and add one example (#1257) 2018-07-11 01:42:34 -07:00
7
a7dd73c657 Merge pull request #23 from channelcat/master
py37 (#1256)
2018-07-03 22:12:02 -07:00
7
f9b29fd7e7 py37 (#1256)
* add py37 to travisci

* use dist:xenial for py37

* sudo: true in .travici

* bump websockets version for py37 support and fix unit tests
2018-07-03 22:07:08 -07:00
7
f770e16f6d Merge pull request #22 from channelcat/master
merge upstream master branch
2018-06-26 23:33:35 -07:00
Arnulfo Solís
9092ee9f0e HTTP Entity Headers (#1127)
* introduced basic entity and hopbyhop header identification

* removed entity headers

* coding style fixes

* remove unneeded header check

* moved from bytes to unicode in headers

* changed list to tuple in empty response statuses
2018-06-26 22:25:25 -07:00
GaryO
01257f65a6 Make auto reloader work on Mac (#1249) 2018-06-18 15:16:10 -07:00
7
c1222175b3 Merge pull request #21 from channelcat/master
remote tracking
2018-06-10 20:17:27 -07:00
Volodymyr Maksymiv
5ff481952d add UUID support (#1241) 2018-06-09 01:16:17 -07:00
7
baa689ad43 Fix failed build and add websockets version specifier (#1239)
* add websockets version constraint

* fix failed build
2018-06-07 10:07:26 -07:00
Philip Xu
2f30f4f69f Fixed #1231 - release resource no matter what (#1232) 2018-06-06 14:43:57 -07:00
Raphael Deem
202a4c6525 make request truthy if has transport (#1222) 2018-05-16 14:12:12 -07:00
7
7928b9b3a2 Merge pull request #20 from channelcat/master
merge upstream master branch
2018-04-29 21:50:07 -07:00
Adam Hopkins
e1c9020268 Update extensions.md (#1205)
Changing the description of [Sanic JWT](https://github.com/ahopkins/sanic-jwt) to include permission scoping
2018-04-29 18:41:17 -07:00
Philip Xu
04a12b436e Added Sanic-Auth, Sanic-CookieSession and Sanic-WTF to Extensions doc (#1210) 2018-04-29 18:40:18 -07:00
Fantix King
818a8c2196 Added GINO to Extensions doc (#1200) 2018-04-21 21:02:49 -07:00
Arnulfo Solís
b6715464fd added init docs (#1167) 2018-04-01 20:53:08 -07:00
Raphael Deem
8f2d543d9f default to auto_reload in debug mode (#1159)
* default to auto_reload in debug mode

* disable auto-reload in testing client
2018-04-01 20:52:56 -07:00
Raphael Deem
6cf320bedb Merge pull request #1181 from kot83/patch-1
rename function in examples to post_json
2018-03-29 20:13:48 -07:00
kot83
a850ce5086 rename function to something else
function already defined
2018-03-29 15:57:10 -07:00
Raphael Deem
ef3bdf5408 Merge pull request #1180 from ashleysommer/fix_aiohttp_breakages
Fix failing tests when aiohttp>=3.1.0
2018-03-29 01:05:50 -07:00
Ashley Sommer
94b9bc7950 Some of the tests in Sanic (test_request_timout, test_response_timeout, test_keep_alive_timeout) use a custom SanicClient with modified methods. This relies on overriding internal aiohttp Client classes.
In aiohttp 3.1.0 there were some breaking changes that caused the custom methods to be no longer compatible with latest upstream aiohttp Client class.
See: 903073283f
and: b42e0ced46

This commit adds aiohttp version checks to adapt to these changes.
2018-03-29 11:54:59 +10:00
Raphael Deem
8a07463a67 Merge pull request #1175 from PyManiacGR/patch-1
Fix try_everything example.
2018-03-28 00:41:07 -07:00
PyManiac
2995b23929 Update try_everything.py 2018-03-24 15:55:15 +02:00
TheRubyDoggy
eb4276373b Fix try_everything example. 2018-03-24 15:34:41 +02:00
Raphael Deem
79df52e519 Merge pull request #1169 from charlax/patch-1
Clarify arguments to request/response middleware
2018-03-21 10:46:09 -07:00
Charles-Axel Dein
3dfb31b1b9 Clarify arguments to request/response middleware 2018-03-21 12:07:26 +01:00
Raphael Deem
c4c4ed70d9 Merge pull request #1163 from vopankov/master
Add __weakref__ to Request slots
2018-03-17 14:52:25 -07:00
Raphael Deem
45422df1b7 Merge pull request #1162 from yunstanford/fix-hang-build
Fix hang build and failed builds
2018-03-16 11:14:44 -07:00
Yun Xu
e0b7624414 fix hang build 2018-03-15 22:06:58 -07:00
Yun Xu
b0ecb3170f fix hang build 2018-03-15 22:03:36 -07:00
Yun Xu
fc8b5f378a migrate to trusty 2018-03-15 21:39:21 -07:00
Yun Xu
d42cb7ddb3 fix hang build 2018-03-15 21:28:52 -07:00
Панков Василий
6454ac0944 Add __weakref__ to Request slots 2018-03-14 13:37:15 +03:00
7
31cf83f10b Merge pull request #19 from channelcat/master
merge upstream master branch
2018-03-13 22:11:40 -07:00
Raphael Deem
cc84005593 Merge pull request #1157 from kinware/feature/add-route-streams
Allow streaming handlers in app.add_route()
2018-03-13 00:08:25 -07:00
Kinware
915d2732a1 Allow streaming handlers in add_route 2018-03-12 20:21:59 +01:00
Raphael Deem
44bc47361e Merge pull request #1149 from channelcat/travis-retry
use travis_retry on tox
2018-03-06 15:54:19 -08:00
Raphael Deem
3619b07843 Merge pull request #1146 from yunstanford/upgrade-test-client
Upgrade test client
2018-03-01 23:18:20 -08:00
Raphael Deem
ad3f588c79 use travis_retry on tox 2018-03-01 23:16:49 -08:00
Yun Xu
a2fc37121b migrating all to async syntax 2018-03-01 22:35:58 -08:00
Raphael Deem
7f36d20123 Merge pull request #1145 from yingshaoxo/patch-1
add an necessary import for better understanding
2018-02-28 01:19:29 -08:00
Yun Xu
d1a8e8b042 fixed unit tests 2018-02-27 22:25:38 -08:00
Yun Xu
c39ddd00d3 workaround fix for an issue in aiohttp.Client 2018-02-27 21:42:41 -08:00
Yun Xu
d55e453bd5 cleaning up 2018-02-27 20:26:49 -08:00
Raphael Deem
bffed27bdb Merge pull request #1142 from clarksun/patch-1
exception.md code sample miss 'async' prefix
2018-02-27 01:03:47 -08:00
7
fffcb158f1 Merge pull request #18 from channelcat/master
Merge upstream master branch
2018-02-26 22:19:30 -08:00
Yun Xu
eca98a54eb fixed all unit tests 2018-02-26 22:18:21 -08:00
Yun Xu
46ed2c5270 upgrade aiohttp for test_client 2018-02-26 22:08:05 -08:00
Sun Wei
23ea0b7ec9 exception.md code sample miss 'async' prefix 2018-02-26 16:09:26 +08:00
yingshaoxo
ef26cb283b add an necessary import for better understanding
add `from sanic.response import redirect`
2018-02-26 11:24:54 +08:00
Eli Uriegas
b8bb77eff6 Merge pull request #1137 from Julien00859/1136
sanic.handlers.ErrorHandler.response handler call was too restrictive
2018-02-21 09:55:52 -06:00
Julien00859
9c75ad3de1 close #1136 2018-02-21 00:50:27 +01:00
Eli Uriegas
0b38dea613 Merge pull request #1117 from abuckenheimer/env_dependent_ujson_uvloop
only install ujson and uvloop with cpython on non windows machines
2018-02-20 13:13:21 -06:00
Raphael Deem
7e4a9e3bc2 Merge pull request #1047 from Yaser-Amiri/master
Add auto reloading.
2018-02-16 11:11:49 -08:00
Raphael Deem
36f12c822f Merge pull request #1122 from knowsuchagency/master
add app.register_listener method
2018-02-15 16:58:27 -08:00
Raphael Deem
0cbea0f5d3 Merge pull request #1129 from cloudship/patch-1
raw requires a bytes-like object
2018-02-14 20:44:40 -08:00
panxb
e735fe54c3 raw requires a bytes-like object
raw requires a bytes-like object, or an object that implements __bytes__, not 'str'
2018-02-15 00:11:37 +08:00
Stephan Fitzpatrick
e911e2e1df updated doc 2018-02-13 23:58:03 -08:00
Stephan Fitzpatrick
1d75f6c2be changed docstring spacing 2018-02-13 10:15:16 -08:00
Raphael Deem
ad8a168469 Merge pull request #1121 from tandalf/issue-1120
Fixed bug when passing a list into route decorator's host argument #1120
2018-02-12 12:48:13 -08:00
Raphael Deem
74fc502089 Merge pull request #1124 from yunstanford/add-doc
Expose WebSocket Param and Add Doc
2018-02-11 02:02:13 -08:00
Yun Xu
dfc2166d8b add websocket.rst to index.rst 2018-02-10 12:21:23 -08:00
Yun Xu
2b70346db4 fix doc 2018-02-09 21:32:09 -08:00
Yun Xu
090df6f224 add websocket section in doc 2018-02-09 21:26:39 -08:00
Yun Xu
745a1d6e94 document websocket args 2018-02-09 21:03:21 -08:00
Yun Xu
0fe0796870 expose websocket protocol arguments 2018-02-09 20:44:02 -08:00
7
224b56bd3a Merge pull request #17 from channelcat/master
merge from upstream sanic
2018-02-09 20:12:29 -08:00
Stephan Fitzpatrick
571b5b544d added app.register_listener method w/test 2018-02-09 14:01:17 -08:00
Timothy Ebiuwhe
220b40f7f4 Added regression tests for issue #1120 2018-02-09 22:33:34 +01:00
Timothy Ebiuwhe
60774c5a49 Fixed bug that occurs on calling @app.route or any of it's variants
causes a route to be added twice. One without the slash, the other with the
Setting strict_slashes to false when a route does not end with slashes
slash. This is ok if the Router._add method runs linearly, but problematic
when it runs recursively. Unfortunately recursion is triggered when
the host param to the Router._add function is a list of hosts.
2018-02-09 22:27:20 +01:00
Raphael Deem
6d37ef7256 Merge pull request #1109 from DirkGuijt/master
fixed bug in multipart/form-data parser
2018-02-08 00:11:20 -08:00
Dirk Guijt
e083224df1 changed bewline formatting 2018-02-07 09:29:44 +01:00
Alec Buckenheimer
5ef567405f fixed platform from windows to win32 2018-02-06 19:56:25 -05:00
Raphael Deem
ea2521f430 Merge pull request #1112 from boboldehampsink/extend_websocketprotocol_arguments
Extend WebSocketProtocol arguments
2018-02-06 15:05:58 -08:00
Alec Buckenheimer
82cb182fe7 added pip requirement to only install ujson and uvloop with cpython on non windows machines 2018-02-06 09:57:16 -05:00
Raphael Deem
3fe31ff551 Merge pull request #1104 from arnulfojr/minor/keep-alive-timeout-log-level
KeepAlive Timeout log level change to debug
2018-02-02 18:54:24 -08:00
Dirk Guijt
48d45f1ca4 sorry, style issue again 2018-02-03 03:14:04 +01:00
Dirk Guijt
ddf2a604d1 changed 'file' variable to 'form_file' to prevent overwriting the reserved word 2018-02-03 03:07:07 +01:00
Raphael Deem
8b920d9d56 Merge pull request #1113 from arnulfojr/bugfix/content-length-header-on-X04
Content Length header on 204 and 304 responses
2018-02-02 13:18:08 -08:00
Arnulfo Solis
f2c0489452 replaced comparison for in operator 2018-02-02 20:19:15 +01:00
Arnulfo Solis
f5a2d19199 touch commit 2018-02-02 14:13:14 +01:00
Arnulfo Solis
86fed12d91 less flake8 warnings in response test 2018-02-02 14:05:57 +01:00
Arnulfo Solis
7ca3ad5d4c no body and content length to 0 when 304 response is returned 2018-02-02 13:24:51 +01:00
Dirk Guijt
1eecffce97 fixed minor flake8 style problem 2018-02-02 09:57:06 +01:00
Dirk Guijt
5c341a2b00 made field name mandatory in multipart/form-data headers
A field name in the Content-Disposition header is required by the multipart/form-data spec. If one field/part does not have it, it will be omitted from the request. When this happens, we log it to DEBUG.
2018-02-02 09:43:42 +01:00
Arnulfo Solis
0ab64e9803 simplified logic when handling the body 2018-02-02 09:29:54 +01:00
Dirk Guijt
27108334f1 Merge branch 'master' of https://github.com/DirkGuijt/sanic 2018-02-02 00:55:58 +01:00
Dirk Guijt
788253cbe8 changes based on discussion on PR #1109 2018-02-02 00:55:51 +01:00
Arnulfo Solis
4b6e89a526 added one more test 2018-02-01 20:00:32 +01:00
Arnulfo Solis
68fd1b66b5 Response model now handles the 204 no content 2018-02-01 17:51:51 +01:00
Bob Olde Hampsink
5806666949 Extend WebSocketProtocol arguments to accept all arguments of websockets.protocol.WebSocketCommonProtocol 2018-02-01 16:23:10 +01:00
DirkGuijt
a76d8108fe small code style change
changed double quotes to single quotes to match the coding style
2018-02-01 11:55:30 +01:00
Arnulfo Solis
2135294e2e changed None to return empty string instead of null string 2018-02-01 11:52:55 +01:00
Dirk Guijt
ed1c563d1f fixed bug in multipart/form-data parser
Sanic automatically assumes that a form field is a file if it has a content-type header, even though the header is text/plain or application/json. This is a fix for it, I took into account the RFC7578 specification regarding the defaults.
2018-02-01 11:30:24 +01:00
Raphael Deem
74efa3a108 Merge pull request #1105 from manisenkov/upgrade-status-to-beta
Upgrade development status to beta
2018-01-31 14:36:06 -08:00
manisenkov
49c29e6862 Upgrade development status to beta 2018-01-31 23:25:50 +01:00
Raphael Deem
17d7f24825 Merge pull request #1108 from manisenkov/pin-pytest-version
Pin pytest version to 3.3.2
2018-01-31 14:24:04 -08:00
manisenkov
f23c3da4ff Pin pytest version to 3.3.2 2018-01-31 22:58:48 +01:00
Arnulfo Solis
cabcf50fbe KeepAlive Timeout log level change to debug 2018-01-30 11:26:15 +01:00
Raphael Deem
8b23b5d389 Merge pull request #1101 from SirEdvin/master
Provide information about sanic-oauth extension
2018-01-29 15:00:11 -08:00
SirEdvin
37eb2c1db6 Provide information about sanic-oauth extension 2018-01-27 10:28:53 +02:00
Eli Uriegas
9751a37343 Merge pull request #1098 from shahinism/refactor/docker
Install Python 3.5 and 3.6 on docker container
2018-01-26 14:01:21 -08:00
Eli Uriegas
ed3bdd3443 Merge pull request #1100 from NyanKiyoshi/master
No longer raising a missing parameter when value is null
2018-01-26 13:56:52 -08:00
NyanKiyoshi
285ad9bdc1 No longer raising a missing parameter when value is null
When passing a null value as parameter (ex.: 0, None or False), Sanic said "Error: Required parameter `param` was not passed to url_for"

Example:

```
@app.route("/<idx>")
def route(rq, idx):
    pass
```

```
url_for("route", idx=0)
```

No longer raises: `Error: Required parameter `idx` was not passed to url_for`
2018-01-26 21:13:43 +01:00
Shahin
16f5914c90 Install Python 3.5 and 3.6 on docker container
To cover all supported versions of Python using Tox
2018-01-24 16:46:45 +03:30
Raphael Deem
72d56a89a2 Merge pull request #1094 from caitinggui/master
update class_based_views
2018-01-23 17:25:59 -08:00
Raphael Deem
1135c8c1b1 Merge pull request #1097 from howie6879/master
Add parameter check
2018-01-23 17:25:25 -08:00
caitinggui
ec4339bd47 update description 2018-01-24 09:02:07 +08:00
howie6879
6c0fbef843 Add parameter check 2018-01-24 08:17:55 +08:00
howie6879
040c85a43b Add parameter check 2018-01-24 08:11:47 +08:00
Raphael Deem
420554c737 Merge pull request #1096 from kyb3r/patch-1
Typo in readme?
2018-01-22 23:56:16 -08:00
howie6879
f20b854dd2 Add parameter check 2018-01-22 14:52:30 +08:00
howie6879
3844cec7a4 Add parameter check 2018-01-22 14:12:41 +08:00
howie.hu
0db49f7520 Merge pull request #4 from channelcat/master
Update
2018-01-22 13:36:48 +08:00
Kyber
f8dedcaa1e Typo in readme? 2018-01-22 15:55:29 +11:00
Yaser Amiri
f8b1122467 Revert "Change parsing cookies mechanism. (like Django instade of http.cookies.SimpleCookie)"
This reverts commit ba1dbacd35.
2018-01-21 09:10:15 +03:30
Raphael Deem
f3bf5e9a5c Merge pull request #1090 from yunstanford/patch-signal-handling
Patch signal handling
2018-01-20 14:03:23 -08:00
Yaser Amiri
ba1dbacd35 Change parsing cookies mechanism. (like Django instade of http.cookies.SimpleCookie) 2018-01-20 12:49:16 +03:30
caitinggui
4036f1c121 update class_based_views 2018-01-19 16:20:07 +08:00
Raphael Deem
22ad697d1f Merge pull request #1078 from eltrhn/master
Add support for blueprint groups and nesting
2018-01-18 17:26:52 -08:00
Eli
a10d7469cd Add blueprint groups + nesting 2018-01-18 17:20:51 -08:00
Raphael Deem
06c3153d22 Merge pull request #1092 from mattfox/patch-1
Add request.method to documentation
2018-01-17 16:10:42 -08:00
Matt Fox
9677158b75 Add request.method to documentation 2018-01-17 07:31:39 -08:00
Raphael Deem
226a73141b Merge pull request #1091 from yunstanford/patch-router-fix
Patch router fix
2018-01-17 00:15:33 -08:00
Raphael Deem
da3201bf35 Merge pull request #1088 from cosven/master
use single quote in readme.rst
2018-01-16 13:34:39 -08:00
Yun Xu
7daebc6aea fix Router.check_dynamic_route_exists 2018-01-15 17:53:37 -08:00
Yun Xu
d9002769cf fix a typo 2018-01-15 17:49:11 -08:00
Yun Xu
6d0b30953a add unit test which should fail on original code 2018-01-15 17:40:44 -08:00
Yun Xu
09d6452475 fixed unit test 2018-01-15 15:15:08 -08:00
Yun Xu
6a61fce84e worker process should ignore SIGINT when run_multiple 2018-01-15 11:53:15 -08:00
Yun Xu
11017902be signal handling 2018-01-15 11:23:49 -08:00
7
bd7333723e Merge pull request #16 from channelcat/master
Merge upstream master branch
2018-01-15 10:48:34 -08:00
cosven
6648250fb9 Merge pull request #1 from cosven/cosven-patch-1
use single quote in readme.rst
2018-01-15 14:56:52 +08:00
cosven
a94a2d46d0 use single quote in readme.rst
As we use single quote in sanic package, we may be supposed to use single quote in readme also?
2018-01-15 14:55:36 +08:00
Raphael Deem
ab97018c78 Merge pull request #1082 from channelcat/1042
fix exception handling
2018-01-13 17:06:46 -08:00
Raphael Deem
be702b0924 Merge pull request #1087 from Stranger6667/fix-get_socket
Fix typo
2018-01-13 17:06:03 -08:00
Dmitry Dygalo
c5c10cfb50 Fix typo 2018-01-13 17:56:29 +01:00
howie.hu
5682d642a6 Merge pull request #3 from channelcat/master
Update
2018-01-10 09:23:43 +08:00
Raphael Deem
62c6d7274c Merge pull request #1083 from bow/fix/log_response_host
Fix log_response to correctly output request ip and port
2018-01-09 13:55:14 -08:00
bow
4f8633375d Fix log_response to correctly output request ip and port 2018-01-09 13:47:01 +01:00
Raphael Deem
9f559818e5 Merge pull request #1081 from howie6879/master
Fix: the Chinese URI
2018-01-08 15:54:46 -08:00
howie6879
5f329f72ee Update test_routes.py 2018-01-08 08:38:54 +08:00
howie6879
7303a06f83 Fix: the Chinese URI 2018-01-07 12:07:18 +08:00
howie6879
e34de96b24 Fix: the Chinese URI 2018-01-07 12:06:21 +08:00
howie6879
42cd424274 Fix: the Chinese URI 2018-01-07 10:59:12 +08:00
howie.hu
9426e94314 Merge pull request #2 from channelcat/master
Update
2018-01-07 09:57:51 +08:00
r0fls
7a1dab3319 fix exception handling 2018-01-05 14:12:22 -08:00
Raphael Deem
d63ec84745 Merge pull request #1037 from youknowone/xdist
Boost test speed by pytest-xdist
2018-01-05 12:10:27 -08:00
Raphael Deem
0e7e2f4e5b Merge pull request #1080 from channelcat/1079
fix timeout bug when self.transport is None
2018-01-04 15:01:40 -08:00
r0fls
46521240a9 fix timeout bug when self.transport is None 2018-01-03 23:33:22 -08:00
Jeong YunWon
a8827a5d95 Boost test speed by pytest-xdist 2018-01-03 18:52:25 +09:00
Raphael Deem
ca0bc1cb7d Merge pull request #1076 from channelcat/1074
fix strict_slashes bug when route has slash
2018-01-01 12:47:48 -08:00
r0fls
8c28ce7d79 fix strict_slashes bug when route has slash 2018-01-01 02:24:48 -08:00
r0fls
5b051f0891 add test 2018-01-01 02:14:55 -08:00
Raphael Deem
c93de9450a Merge pull request #1073 from Kurlov/master
remove uvloop for windows setup
2017-12-29 14:56:38 -08:00
Aleksandr Kurlov
96976fa892 remove uvloop for windows setup 2017-12-29 23:04:22 +05:00
Raphael Deem
c14e99cef0 Merge pull request #1071 from r0fls/1062-docs
udpate docs with add_task app injection
2017-12-28 02:01:23 -08:00
Raphael Deem
c91a806774 udpate docs with add_task app injection 2017-12-28 01:59:16 -08:00
Raphael Deem
2f0076f429 Merge pull request #1063 from r0fls/1062
try to inject the app in add_task method
2017-12-27 11:40:05 -08:00
Raphael Deem
a1ffc6d55b try to inject the app in add_task method 2017-12-27 01:06:43 -08:00
Yaser Amiri
9bdf7a9980 Revert files those fixed for flake problems. 2017-12-26 23:35:54 +03:30
Yaser Amiri
81494453b0 Remove dependency on requests library.
Change auto reloader enviroment varible name to SANIC_SERVER_RUNNING
Fix some typo mistakes, flake uncompatibilities and such problems.
Raise NotImplementedError for operating systems except posix systems for auto reloading.
2017-12-26 19:17:13 +03:30
Raphael Deem
008cbe5ce7 Merge pull request #1069 from r0fls/1050
add samesite cookie to cookie keys
2017-12-24 02:50:37 -08:00
Raphael Deem
5ee35e7eeb add samesite cookie to cookie keys 2017-12-24 02:33:52 -08:00
Raphael Deem
1a98e70281 Merge pull request #1066 from r0fls/1065
allow add_task after server starts
2017-12-21 23:44:18 -08:00
Raphael Deem
8e3f3977bd allow add_task after server starts 2017-12-21 23:37:42 -08:00
Raphael Deem
04b04f094c Merge pull request #1064 from r0fls/1061
double quotes in unauthorized exception per rfc7230
2017-12-21 18:59:41 -08:00
Raphael Deem
9c02cdbad9 double quotes in unauthorized exception per rfc7230 2017-12-21 18:05:05 -08:00
Raphael Deem
c30e805623 Merge pull request #1060 from seemethere/fix_readthedocs_builds
Add sphinxcontrib-asyncio to environment.yml
2017-12-20 23:40:04 -08:00
Eli Uriegas
cd81538ce3 Add sphinxcontrib-asyncio to environment.yml
readthedocs builds were broken because they were missing this dependency

Signed-off-by: Eli Uriegas <seemethere101@gmail.com>
2017-12-19 22:55:50 -06:00
howie.hu
68cb280513 Merge pull request #1 from channelcat/master
Update
2017-12-20 09:08:52 +08:00
Raphael Deem
19466a15b4 Merge pull request #1055 from youknowone/cancel-timeout
Cancel request tasks when response timeout is triggered
2017-12-17 16:46:29 -08:00
Jeong YunWon
d54b406cba Cancel request tasks when response timeout is triggered
Before: Even after raising ResponseTimeout, server still processes
remaining tasks until it is done
After: Before raising ResponseTimeout, server stops working task.
2017-12-14 18:43:52 +09:00
Raphael Deem
72254a7af9 Merge pull request #1054 from r0fls/rfc7231
fix issues with method not allowed response
2017-12-13 23:42:37 -08:00
Raphael Deem
52feff266e fix edge case with methods as None 2017-12-13 23:23:04 -08:00
Raphael Deem
2c3f50e34a fix stream handling 2017-12-13 23:06:18 -08:00
Raphael Deem
2b0258c13a fix issues with method not allowed response 2017-12-11 20:12:26 -08:00
Raphael Deem
2585900692 Merge pull request #1053 from danpalmer/master
Fix ip and socket data format on V6
2017-12-11 20:24:52 -06:00
Dan Palmer
48c2dcb110 Fix ip and socket data format on V6 2017-12-11 22:16:03 +00:00
Dan Palmer
10a378bd46 Typo 2017-12-11 14:33:43 +00:00
Yaser Amiri
3fe3c2c79f Add test for auto reloading. 2017-12-07 20:19:40 +03:30
Yaser Amiri
52c2a8484e Add auto reloader. 2017-12-07 16:30:54 +03:30
104 changed files with 5683 additions and 2004 deletions

32
.appveyor.yml Normal file
View File

@@ -0,0 +1,32 @@
version: "{branch}.{build}"
environment:
matrix:
- TOXENV: py35-no-ext
PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5.x"
PYTHON_ARCH: "64"
- TOXENV: py36-no-ext
PYTHON: "C:\\Python36-x64"
PYTHON_VERSION: "3.6.x"
PYTHON_ARCH: "64"
- TOXENV: py37-no-ext
PYTHON: "C:\\Python37-x64"
PYTHON_VERSION: "3.7.x"
PYTHON_ARCH: "64"
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
install:
- pip install tox
build: off
test_script: tox
notifications:
- provider: Email
on_build_success: false
on_build_status_changed: false

View File

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

25
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,25 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks.
**Code snippet**
Relevant source code, make sure to remove what is not necessary.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 0.8.3]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,16 @@
---
name: Feature request
about: Suggest an idea for Sanic
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or sample code about the feature request here.

13
.github/ISSUE_TEMPLATE/help-wanted.md vendored Normal file
View File

@@ -0,0 +1,13 @@
---
name: Help wanted
about: Do you need help? Try community.sanicframework.org
---
*DELETE ALL BEFORE POSTING*
*Post your HELP WANTED questions on [the community forum](https://community.sanicframework.org/)*.
Checkout the community forum before posting any question here.
We prefer if you put these kinds of questions here:
https://community.sanicframework.org/c/questions-and-help

View File

@@ -1,5 +1,4 @@
sudo: false
dist: precise
language: python
cache:
directories:
@@ -14,17 +13,29 @@ matrix:
python: 3.6
- env: TOX_ENV=py36-no-ext
python: 3.6
- env: TOX_ENV=flake8
- env: TOX_ENV=py37
python: 3.7
dist: xenial
sudo: true
- env: TOX_ENV=py37-no-ext
python: 3.7
dist: xenial
sudo: true
- env: TOX_ENV=lint
python: 3.6
- env: TOX_ENV=check
python: 3.6
install: pip install -U tox
script: tox -e $TOX_ENV
install:
- pip install -U tox
- pip install codecov
script: travis_retry tox -e $TOX_ENV
after_success:
- codecov
deploy:
provider: pypi
user: channelcat
password:
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k=
secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc=
on:
tags: true
distributions: "sdist bdist_wheel"

View File

@@ -1,3 +1,80 @@
Version 0.8
-----------
0.8.3
- Changes:
- Ownership changed to org 'huge-success'
0.8.0
- Changes:
- Add Server-Sent Events extension (Innokenty Lebedev)
- Graceful handling of request_handler_task cancellation (Ashley Sommer)
- Sanitize URL before redirection (aveao)
- Add url_bytes to request (johndoe46)
- py37 support for travisci (yunstanford)
- Auto reloader support for OSX (garyo)
- Add UUID route support (Volodymyr Maksymiv)
- Add pausable response streams (Ashley Sommer)
- Add weakref to request slots (vopankov)
- remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
- Allow streaming handlers in add_route (kinware)
- use travis_retry for tox (Raphael Deem)
- update aiohttp version for test client (yunstanford)
- add redirect import for clarity (yingshaoxo)
- Update HTTP Entity headers (Arnulfo Solís)
- Add register_listener method (Stephan Fitzpatrick)
- Remove uvloop/ujson dependencies for Windows (abuckenheimer)
- Content-length header on 204/304 responses (Arnulfo Solís)
- Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
- Update development status from pre-alpha to beta (Maksim Anisenkov)
- KeepAlive Timout log level changed to debug (Arnulfo Solís)
- Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
- Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
- Add support for blueprint groups and nesting (Elias Tarhini)
- Remove uvloop for windows setup (Aleksandr Kurlov)
- Auto Reload (Yaser Amari)
- Documentation updates/fixups (multiple contributors)
- Fixes:
- Fix: auto_reload in Linux (Ashley Sommer)
- Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
- Fix: disable auto_reload by default on windows (abuckenheimer)
- Fix (1143): Turn off access log with gunicorn (hqy)
- Fix (1268): Support status code for file response (Cosmo Borsky)
- Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
- Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
- Fix (1242): Responses for CI header (yunstanford)
- Fix (1237): add version constraint for websockets (yunstanford)
- Fix (1231): memory leak - always release resource (Phillip Xu)
- Fix (1221): make request truthy if transport exists (Raphael Deem)
- Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
- Fix try_everything examples (PyManiacGR, kot83)
- Fix (1158): default to auto_reload in debug mode (Raphael Deem)
- Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
- Fix: raw requires bytes-like object (cloudship)
- Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
- Fix: Bug in multipart/form-data parser (DirkGuijt)
- Fix: Exception for missing parameter when value is null (NyanKiyoshi)
- Fix: Parameter check (Howie Hu)
- Fix (1089): Routing issue with named parameters and different methods (yunstanford)
- Fix (1085): Signal handling in multi-worker mode (yunstanford)
- Fix: single quote in readme.rst (Cosven)
- Fix: method typos (Dmitry Dygalo)
- Fix: log_response correct output for ip and port (Wibowo Arindrarto)
- Fix (1042): Exception Handling (Raphael Deem)
- Fix: Chinese URIs (Howie Hu)
- Fix (1079): timeout bug when self.transport is None (Raphael Deem)
- Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
- Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
- Fix (1065): allow add_task after server starts (Raphael Deem)
- Fix (1061): double quotes in unauthorized exception (Raphael Deem)
- Fix (1062): inject the app in add_task method (Raphael Deem)
- Fix: update environment.yml for readthedocs (Eli Uriegas)
- Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
- Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
- Fix: IPv6 Address and Socket Data Format (Dan Palmer)
Note: Changelog was unmaintained between 0.1 and 0.7
Version 0.1
-----------
- 0.1.7
@@ -5,18 +82,18 @@ Version 0.1
- 0.1.6
- Static files
- Lazy Cookie Loading
- 0.1.5
- 0.1.5
- Cookies
- Blueprint listeners and ordering
- Faster Router
- Fix: Incomplete file reads on medium+ sized post requests
- Breaking: after_start and before_stop now pass sanic as their first argument
- 0.1.4
- 0.1.4
- Multiprocessing
- 0.1.3
- Blueprint support
- Faster Response processing
- 0.1.1 - 0.1.2
- 0.1.1 - 0.1.2
- Struggling to update pypi via CI
- 0.1.0
- Released to public
- 0.1.0
- Released to public

View File

@@ -1,6 +0,0 @@
FROM python:3.6
ADD . /app
WORKDIR /app
RUN pip install tox

View File

@@ -1,4 +1,4 @@
test:
find . -name "*.pyc" -delete
docker build -t sanic/test-image .
docker build -t sanic/test-image -f docker/Dockerfile .
docker run -t sanic/test-image tox

View File

@@ -1,15 +1,15 @@
Sanic
=====
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version|
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |AppVeyor Build Status| |Documentation| |Codecov| |PyPI| |PyPI version| |Code style black|
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 is developed `on GitHub <https://github.com/huge-success/sanic/>`_. We also have `a community discussion board <https://community.sanicframework.org/>`_. Contributions are welcome!
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/channelcat/sanic/issues/396>`_ that we use to track those projects!
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/huge-success/sanic/issues/396>`_ that we use to track those projects!
Hello World Example
-------------------
@@ -21,19 +21,19 @@ Hello World Example
app = Sanic()
@app.route("/")
@app.route('/')
async def test(request):
return json({"hello": "world"})
return json({'hello': 'world'})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
Installation
------------
- ``pip install sanic``
To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables
To install sanic without uvloop or ujson using bash, you can provide either or both of these environmental variables
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features
installation.
@@ -47,33 +47,36 @@ Documentation
.. |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
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/huge-success/sanic
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
:target: https://travis-ci.org/huge-success/sanic
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
:target: https://ci.appveyor.com/project/huge-success/sanic
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
:target: https://pypi.python.org/pypi/sanic/
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
Questions and Discussion
------------------------
`Ask a question or join the conversation <https://community.sanicframework.org/>`_.
Examples
--------
`Non-Core examples <https://github.com/channelcat/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
`Non-Core examples <https://github.com/huge-success/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
`Extensions <https://github.com/channelcat/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
`Extensions <https://github.com/huge-success/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
`Projects <https://github.com/channelcat/sanic/wiki/Projects/>`_. Sanic in production use.
`Projects <https://github.com/huge-success/sanic/wiki/Projects/>`_. Sanic in production use.
TODO
----
* http2
Limitations
-----------
* No wheels for uvloop and httptools on Windows :(
Final Thoughts
--------------

28
docker/Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
FROM alpine:3.7
RUN apk add --no-cache --update \
curl \
bash \
build-base \
ca-certificates \
git \
bzip2-dev \
linux-headers \
ncurses-dev \
openssl \
openssl-dev \
readline-dev \
sqlite-dev
RUN update-ca-certificates
RUN rm -rf /var/cache/apk/*
ENV PYENV_ROOT="/root/.pyenv"
ENV PATH="$PYENV_ROOT/bin:$PATH"
ADD . /app
WORKDIR /app
RUN /app/docker/bin/install_python.sh 3.5.4 3.6.4
ENTRYPOINT ["./docker/bin/entrypoint.sh"]

11
docker/bin/entrypoint.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -e
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
source /root/.pyenv/completions/pyenv.bash
pip install tox
exec $@

17
docker/bin/install_python.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
set -e
export CFLAGS='-O2'
export EXTRA_CFLAGS="-DTHREAD_STACK_SIZE=0x100000"
curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
eval "$(pyenv init -)"
for ver in $@
do
pyenv install $ver
done
pyenv global $@
pip install --upgrade pip
pyenv rehash

0
docs/_static/.gitkeep vendored Normal file
View File

View File

@@ -14,14 +14,18 @@ Guides
sanic/exceptions
sanic/middleware
sanic/blueprints
sanic/websocket
sanic/config
sanic/cookies
sanic/decorators
sanic/streaming
sanic/class_based_views
sanic/custom_protocol
sanic/sockets
sanic/ssl
sanic/logging
sanic/versioning
sanic/debug_mode
sanic/testing
sanic/deploying
sanic/extensions

View File

@@ -48,7 +48,74 @@ by that blueprint. In this example, the registered routes in the `app.router`
will look like:
```python
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')]
```
## Blueprint groups and nesting
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example:
```
api/
├──content/
│ ├──authors.py
│ ├──static.py
│ └──__init__.py
├──info.py
└──__init__.py
app.py
```
Initialization of this app's blueprint hierarchy could go as follows:
```python
# api/content/authors.py
from sanic import Blueprint
authors = Blueprint('content_authors', url_prefix='/authors')
```
```python
# api/content/static.py
from sanic import Blueprint
static = Blueprint('content_static', url_prefix='/static')
```
```python
# api/content/__init__.py
from sanic import Blueprint
from .static import static
from .authors import authors
content = Blueprint.group(static, authors, url_prefix='/content')
```
```python
# api/info.py
from sanic import Blueprint
info = Blueprint('info', url_prefix='/info')
```
```python
# api/__init__.py
from sanic import Blueprint
from .content import content
from .info import info
api = Blueprint.group(content, info, url_prefix='/api')
```
And registering these blueprints in `app.py` can now be done like so:
```python
# app.py
from sanic import Sanic
from .api import api
app = Sanic(__name__)
app.blueprint(api)
```
## Using blueprints
@@ -187,5 +254,3 @@ async def root(request):
async def post_handler(request, post_id):
return text('Post {} in Blueprint V1'.format(post_id))
```

View File

@@ -92,10 +92,27 @@ class ViewWithDecorator(HTTPMethodView):
def get(self, request, name):
return text('Hello I have a decorator')
def post(self, request, name):
return text("Hello I also have a decorator")
app.add_route(ViewWithDecorator.as_view(), '/url')
```
#### URL Building
But if you just want to decorate some functions and not all functions, you can do as follows:
```python
class ViewWithSomeDecorator(HTTPMethodView):
@staticmethod
@some_decorator_here
def get(request, name):
return text("Hello I have a decorator")
def post(self, request, name):
return text("Hello I don't have any decorators")
```
## 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:

View File

@@ -85,13 +85,15 @@ DB_USER = 'appuser'
Out of the box there are just a few predefined values which can be overwritten when creating the application.
| Variable | Default | Description |
| ------------------ | --------- | --------------------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False |
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
| Variable | Default | Description |
| ------------------------- | --------- | ------------------------------------------------------ |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False |
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long take to force close non-idle connection (sec) |
| ACCESS_LOG | True | Disable or enable access log |
### The different Timeout variables:

View File

@@ -29,8 +29,8 @@ See it's that simple!
## Pull requests!
So the pull request approval rules are pretty simple:
1. All pull requests must pass unit tests
* All pull requests must be reviewed and approved by at least
* All pull requests must pass unit tests
* All pull requests must be reviewed and approved by at least
one current collaborator on the project
* All pull requests must pass flake8 checks
* If you decide to remove/change anything from any common interface

53
docs/sanic/debug_mode.rst Normal file
View File

@@ -0,0 +1,53 @@
Debug Mode
=============
When enabling Sanic's debug mode, Sanic will provide a more verbose logging output
and by default will enable the Auto Reload feature.
.. warning::
Sanic's debug more will slow down the server's performance
and is therefore advised to enable it only in development environments.
Setting the debug mode
----------------------
By setting the ``debug`` mode a more verbose output from Sanic will be outputed
and the Automatic Reloader will be activated.
.. code-block:: python
from sanic import Sanic
from sanic.response import json
app = Sanic()
@app.route('/')
async def hello_world(request):
return json({"hello": "world"})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)
Manually setting auto reload
----------------------------
Sanic offers a way to enable or disable the Automatic Reloader manually,
the ``auto_reload`` argument will activate or deactivate the Automatic Reloader.
.. code-block:: python
from sanic import Sanic
from sanic.response import json
app = Sanic()
@app.route('/')
async def hello_world(request):
return json({"hello": "world"})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, auto_reload=True)

View File

@@ -34,6 +34,6 @@ def authorized():
@app.route("/")
@authorized()
async def test(request):
return json({status: 'authorized'})
return json({'status': 'authorized'})
```

View File

@@ -13,7 +13,7 @@ To throw an exception, simply `raise` the relevant exception from the
from sanic.exceptions import ServerError
@app.route('/killme')
def i_am_ready_to_die(request):
async def i_am_ready_to_die(request):
raise ServerError("Something bad happened", status_code=500)
```
@@ -24,7 +24,7 @@ from sanic.exceptions import abort
from sanic.response import text
@app.route('/youshallnotpass')
def no_no(request):
async def no_no(request):
abort(401)
# this won't happen
text("OK")
@@ -43,10 +43,40 @@ from sanic.response import text
from sanic.exceptions import NotFound
@app.exception(NotFound)
def ignore_404s(request, exception):
async def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
```
You can also add an exception handler as such:
```python
from sanic import Sanic
async def server_error_handler(request, exception):
return text("Oops, server error", status=500)
app = Sanic()
app.error_handler.add(Exception, server_error_handler)
```
In some cases, you might want want to add some more error handling
functionality to what is provided by default. In that case, you
can subclass Sanic's default error handler as such:
```python
from sanic import Sanic
from sanic.handlers import ErrorHandler
class CustomErrorHandler(ErrorHandler):
def default(self, request, exception):
''' handles errors that have no error handlers assigned '''
# You custom error handling logic...
return super().default(request, exception)
app = Sanic()
app.error_handler = CustomErrorHandler()
```
## Useful exceptions
Some of the most useful exceptions are presented below:

View File

@@ -7,7 +7,8 @@ A list of Sanic extensions created by the community.
- [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.
- [JWT](https://github.com/ahopkins/sanic-jwt): Authentication extension for JSON Web Tokens (JWT).
- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic.
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for Sanic
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
@@ -19,6 +20,7 @@ A list of Sanic extensions created by the community.
`Babel` library
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
- [sanic-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support.
- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
@@ -26,3 +28,9 @@ A list of Sanic extensions created by the community.
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/))
- [GINO](https://github.com/fantix/gino): An asyncio ORM on top of SQLAlchemy core, delivered with a Sanic extension. ([Documentation](https://python-gino.readthedocs.io/))
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
- [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier.
- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application.
- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.

View File

@@ -4,8 +4,13 @@ Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
syntax, so earlier versions of python won't work.
1. Install Sanic: `python3 -m pip install sanic`
2. Create a file called `main.py` with the following code:
## 1. Install Sanic
```
python3 -m pip install sanic
```
## 2. Create a file called `main.py`
```python
from sanic import Sanic
@@ -20,9 +25,16 @@ syntax, so earlier versions of python won't work.
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
```
3. Run the server: `python3 main.py`
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
the message *Hello world!*.
## 3. Run the server
```
python3 main.py
```
## 4. Check your browser
Open the address `http://0.0.0.0:8000` in your web browser. You should see
the message *Hello world!*.
You now have a working Sanic server!

View File

@@ -9,17 +9,32 @@ A simple example using default settings would be like this:
```python
from sanic import Sanic
from sanic.log import logger
from sanic.response import text
app = Sanic('test')
@app.route('/')
async def test(request):
return response.text('Hello World!')
logger.info('Here is your log')
return text('Hello World!')
if __name__ == "__main__":
app.run(debug=True, access_log=True)
```
After the server is running, you can see some messages looks like:
```
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
```
You can send a request to server and it will print the log messages:
```
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
```
To use your own logging config, simply use `logging.config.dictConfig`, or
pass `log_config` when you initialize `Sanic` app:
@@ -49,7 +64,7 @@ By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS
There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
- root:<br>
- sanic.root:<br>
Used to log internal messages.
- sanic.error:<br>

View File

@@ -10,13 +10,14 @@ Additionally, Sanic provides listeners which allow you to run code at various po
There are two types of middleware: request and response. Both are declared
using the `@app.middleware` decorator, with the decorator's parameter being a
string representing its type: `'request'` or `'response'`. Response middleware
receives both the request and the response as arguments.
string representing its type: `'request'` or `'response'`.
* Request middleware receives only the `request` as argument.
* Response middleware receives both the `request` and `response`.
The simplest middleware doesn't modify the request or response at all:
```python
```
@app.middleware('request')
async def print_on_request(request):
print("I print when a request is received by the server")
@@ -32,7 +33,7 @@ Middleware can modify the request or response parameter it is given, *as long
as it does not return it*. The following example shows a practical use-case for
this.
```python
```
app = Sanic(__name__)
@app.middleware('response')
@@ -59,7 +60,7 @@ and the response will be returned. If this occurs to a request before the
relevant user route handler is reached, the handler will never be called.
Returning a response will also prevent any further middleware from running.
```python
```
@app.middleware('request')
async def halt_request(request):
return text('I halted the request')
@@ -78,11 +79,11 @@ If you want to execute startup/teardown code as your server starts or closes, yo
- `before_server_stop`
- `after_server_stop`
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
For example:
```python
```
@app.listener('before_server_start')
async def setup_db(app, loop):
app.db = await db_setup()
@@ -100,13 +101,47 @@ async def close_db(app, loop):
await app.db.close()
```
It's also possible to register a listener using the `register_listener` method.
This may be useful if you define your listeners in another module besides
the one you instantiate your app in.
```
app = Sanic()
async def setup_db(app, loop):
app.db = await db_setup()
app.register_listener(setup_db, 'before_server_start')
```
If you want to schedule a background task to run after the loop has started,
Sanic provides the `add_task` method to easily do so.
```python
```
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print('Server successfully started!')
app.add_task(notify_server_started_after_five_seconds())
```
Sanic will attempt to automatically inject the app, passing it as an argument to the task:
```
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)
app.add_task(notify_server_started_after_five_seconds)
```
Or you can pass the app explicitly for the same effect:
```
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)
app.add_task(notify_server_started_after_five_seconds(app))
`

View File

@@ -73,6 +73,8 @@ The following variables are accessible as properties on `Request` objects:
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
- `method` (str) - HTTP method of the request (ie `GET`, `POST`).
- `ip` (str) - IP address of the requester.
- `port` (str) - Port address of the requester.

View File

@@ -91,7 +91,7 @@ from sanic import response
@app.route('/raw')
def handle_request(request):
return response.raw('raw data')
return response.raw(b'raw data')
```
## Modify headers or status

View File

@@ -138,13 +138,14 @@ app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
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
from sanic.response import redirect
@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)
return redirect(url)
@app.route('/posts/<post_id>')
async def post_handler(request, post_id):

66
docs/sanic/sockets.rst Normal file
View File

@@ -0,0 +1,66 @@
Sockets
=======
Sanic can use the python
`socket module <https://docs.python.org/3/library/socket.html>`_ to accommodate
non IPv4 sockets.
IPv6 example:
.. code:: python
from sanic import Sanic
from sanic.response import json
import socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.bind(('::', 7777))
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
if __name__ == "__main__":
app.run(sock=sock)
to test IPv6 ``curl -g -6 "http://[::1]:7777/"``
UNIX socket example:
.. code:: python
import signal
import sys
import socket
import os
from sanic import Sanic
from sanic.response import json
server_socket = '/tmp/sanic.sock'
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_socket)
app = Sanic()
@app.route("/")
async def test(request):
return json({"hello": "world"})
def signal_handler(sig, frame):
print('Exiting')
os.unlink(server_socket)
sys.exit(0)
if __name__ == "__main__":
app.run(sock=sock)
to test UNIX: ``curl -v --unix-socket /tmp/sanic.sock http://localhost/hello``

View File

@@ -1,7 +1,7 @@
# Static Files
Static files and directories, such as an image file, are served by Sanic when
registered with the `app.static` method. The method takes an endpoint URL and a
registered with the `app.static()` method. The method takes an endpoint URL and a
filename. The file specified will then be accessible via the given endpoint.
```python
@@ -43,3 +43,41 @@ app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
app.run(host="0.0.0.0", port=8000)
```
> **Note:** Sanic does not provide directory index when you serve a static directory.
## Virtual Host
The `app.static()` method also support **virtual host**. You can serve your static files with spefic **virtual host** with `host` argument. For example:
```python
from sanic import Sanic
app = Sanic(__name__)
app.static('/static', './static')
app.static('/example_static', './example_static', host='www.example.com')
```
## Streaming Large File
In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly.
Here is an example:
```python
from sanic import Sanic
app = Sanic(__name__)
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True)
```
When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example:
```python
from sanic import Sanic
app = Sanic(__name__)
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
```

View File

@@ -37,7 +37,7 @@ async def handler(request):
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
response.write(body)
await response.write(body)
return stream(streaming)
@@ -85,8 +85,8 @@ app = Sanic(__name__)
@app.route("/")
async def test(request):
async def sample_streaming_fn(response):
response.write('foo,')
response.write('bar')
await response.write('foo,')
await response.write('bar')
return stream(sample_streaming_fn, content_type='text/csv')
```
@@ -100,7 +100,7 @@ async def index(request):
conn = await asyncpg.connect(database='test')
async with conn.transaction():
async for record in conn.cursor('SELECT generate_series(0, 10)'):
response.write(record[0])
await response.write(record[0])
return stream(stream_from_db)
```

View File

@@ -20,7 +20,7 @@ def test_index_put_not_allowed():
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
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
your test request is executed against your application, using `aiohttp`.
The `test_client` methods accept the following arguments and keyword arguments:

52
docs/sanic/websocket.rst Normal file
View File

@@ -0,0 +1,52 @@
WebSocket
=========
Sanic supports websockets, to setup a WebSocket:
.. code:: python
from sanic import Sanic
from sanic.response import json
from sanic.websocket import WebSocketProtocol
app = Sanic()
@app.websocket('/feed')
async def feed(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, protocol=WebSocketProtocol)
Alternatively, the ``app.add_websocket_route`` method can be used instead of the
decorator:
.. code:: python
async def feed(request, ws):
pass
app.add_websocket_route(feed, '/feed')
Handlers for a WebSocket route are passed the request as first argument, and a
WebSocket protocol object as second argument. The protocol object has ``send``
and ``recv`` methods to send and receive data respectively.
You could setup your own WebSocket configuration through ``app.config``, like
.. code:: python
app.config.WEBSOCKET_MAX_SIZE = 2 ** 20
app.config.WEBSOCKET_MAX_QUEUE = 32
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
app.config.WEBSOCKET_WRITE_LIMIT = 2 ** 16
Find more in ``Configuration`` section.

View File

@@ -12,8 +12,10 @@ dependencies:
- zlib=1.2.8=0
- pip:
- uvloop>=0.5.3
- httptools>=0.0.9
- httptools>=0.0.10
- ujson>=1.35
- aiofiles>=0.3.0
- websockets>=3.2
- https://github.com/channelcat/docutils-fork/zipball/master
- websockets>=6.0
- sphinxcontrib-asyncio>=0.2.0
- multidict>=4.0,<5.0
- https://github.com/channelcat/docutils-fork/zipball/master

View File

@@ -1,7 +1,5 @@
from sanic import Sanic
from sanic import Blueprint
from sanic.response import json
from sanic import Blueprint, Sanic
from sanic.response import file, json
app = Sanic(__name__)
blueprint = Blueprint('name', url_prefix='/my_blueprint')
@@ -19,7 +17,12 @@ async def foo2(request):
return json({'msg': 'hi from blueprint2'})
@blueprint3.websocket('/foo')
@blueprint3.route('/foo')
async def index(request):
return await file('websocket.html')
@app.websocket('/feed')
async def foo3(request, ws):
while True:
data = 'hello!'

View File

@@ -30,7 +30,7 @@ async def handler(request):
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
response.write(body)
await response.write(body)
return stream(streaming)

View File

@@ -0,0 +1,42 @@
from sanic import Sanic
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic('some_name')
class SimpleView(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')
class SimpleAsyncView(HTTPMethodView):
async def get(self, request):
return text('I am async get method')
async def post(self, request):
return text('I am async post method')
async def put(self, request):
return text('I am async put method')
app.add_route(SimpleView.as_view(), '/')
app.add_route(SimpleAsyncView.as_view(), '/async')
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,7 +1,7 @@
import os
from sanic import Sanic
from sanic.log import log
from sanic.log import logger as log
from sanic import response
from sanic.exceptions import ServerError
@@ -66,7 +66,7 @@ def post_json(request):
@app.route("/form")
def post_json(request):
def post_form_json(request):
return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})

2
pyproject.toml Normal file
View File

@@ -0,0 +1,2 @@
[tool.black]
line-length = 79

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
from sanic.app import Sanic
from sanic.blueprints import Blueprint
__version__ = '0.7.0'
__all__ = ['Sanic', 'Blueprint']
__version__ = "18.12.0"
__all__ = ["Sanic", "Blueprint"]

View File

@@ -1,20 +1,23 @@
from argparse import ArgumentParser
from importlib import import_module
from sanic.log import logger
from sanic.app import Sanic
from sanic.log import logger
if __name__ == "__main__":
parser = ArgumentParser(prog='sanic')
parser.add_argument('--host', dest='host', type=str, default='127.0.0.1')
parser.add_argument('--port', dest='port', type=int, default=8000)
parser.add_argument('--cert', dest='cert', type=str,
help='location of certificate for SSL')
parser.add_argument('--key', dest='key', type=str,
help='location of keyfile for SSL.')
parser.add_argument('--workers', dest='workers', type=int, default=1, )
parser.add_argument('--debug', dest='debug', action="store_true")
parser.add_argument('module')
parser = ArgumentParser(prog="sanic")
parser.add_argument("--host", dest="host", type=str, default="127.0.0.1")
parser.add_argument("--port", dest="port", type=int, default=8000)
parser.add_argument(
"--cert", dest="cert", type=str, help="location of certificate for SSL"
)
parser.add_argument(
"--key", dest="key", type=str, help="location of keyfile for SSL."
)
parser.add_argument("--workers", dest="workers", type=int, default=1)
parser.add_argument("--debug", dest="debug", action="store_true")
parser.add_argument("module")
args = parser.parse_args()
try:
@@ -25,20 +28,29 @@ if __name__ == "__main__":
module = import_module(module_name)
app = getattr(module, app_name, None)
if not isinstance(app, Sanic):
raise ValueError("Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?"
.format(type(app).__name__, args.module))
raise ValueError(
"Module is not a Sanic app, it is a {}. "
"Perhaps you meant {}.app?".format(
type(app).__name__, args.module
)
)
if args.cert is not None or args.key is not None:
ssl = {'cert': args.cert, 'key': args.key}
ssl = {"cert": args.cert, "key": args.key}
else:
ssl = None
app.run(host=args.host, port=args.port,
workers=args.workers, debug=args.debug, ssl=ssl)
app.run(
host=args.host,
port=args.port,
workers=args.workers,
debug=args.debug,
ssl=ssl,
)
except ImportError as e:
logger.error("No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
.format(e.name))
except ValueError as e:
logger.error("{}".format(e))
logger.error(
"No module named {} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app".format(e.name)
)
except ValueError:
logger.exception("Failed to run app")

File diff suppressed because it is too large Load Diff

View File

@@ -3,22 +3,41 @@ from collections import defaultdict, namedtuple
from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView
FutureRoute = namedtuple('Route',
['handler', 'uri', 'methods', 'host',
'strict_slashes', 'stream', 'version', 'name'])
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'])
FutureRoute = namedtuple(
"FutureRoute",
[
"handler",
"uri",
"methods",
"host",
"strict_slashes",
"stream",
"version",
"name",
],
)
FutureListener = namedtuple(
"FutureListener", ["handler", "uri", "methods", "host"]
)
FutureMiddleware = namedtuple(
"FutureMiddleware", ["middleware", "args", "kwargs"]
)
FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"])
FutureStatic = namedtuple(
"FutureStatic", ["uri", "file_or_directory", "args", "kwargs"]
)
class Blueprint:
def __init__(self, name,
url_prefix=None,
host=None, version=None,
strict_slashes=False):
def __init__(
self,
name,
url_prefix=None,
host=None,
version=None,
strict_slashes=False,
):
"""Create a new blueprint
:param name: unique name of the blueprint
@@ -38,10 +57,35 @@ class Blueprint:
self.version = version
self.strict_slashes = strict_slashes
@staticmethod
def group(*blueprints, url_prefix=""):
"""Create a list of blueprints, optionally
grouping them under a general URL prefix.
:param blueprints: blueprints to be registered as a group
:param url_prefix: URL route to be prepended to all sub-prefixes
"""
def chain(nested):
"""itertools.chain() but leaves strings untouched"""
for i in nested:
if isinstance(i, (list, tuple)):
yield from chain(i)
else:
yield i
bps = []
for bp in chain(blueprints):
if bp.url_prefix is None:
bp.url_prefix = ""
bp.url_prefix = url_prefix + bp.url_prefix
bps.append(bp)
return bps
def register(self, app, options):
"""Register the blueprint to the sanic app."""
url_prefix = options.get('url_prefix', self.url_prefix)
url_prefix = options.get("url_prefix", self.url_prefix)
# Routes
for future in self.routes:
@@ -53,14 +97,15 @@ class Blueprint:
version = future.version or self.version
app.route(uri=uri[1:] if uri.startswith('//') else uri,
methods=future.methods,
host=future.host or self.host,
strict_slashes=future.strict_slashes,
stream=future.stream,
version=version,
name=future.name,
)(future.handler)
app.route(
uri=uri[1:] if uri.startswith("//") else uri,
methods=future.methods,
host=future.host or self.host,
strict_slashes=future.strict_slashes,
stream=future.stream,
version=version,
name=future.name,
)(future.handler)
for future in self.websocket_routes:
# attach the blueprint name to the handler so that it can be
@@ -68,18 +113,19 @@ class Blueprint:
future.handler.__blueprintname__ = self.name
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
app.websocket(uri=uri,
host=future.host or self.host,
strict_slashes=future.strict_slashes,
name=future.name,
)(future.handler)
app.websocket(
uri=uri,
host=future.host or self.host,
strict_slashes=future.strict_slashes,
name=future.name,
)(future.handler)
# Middleware
for future in self.middlewares:
if future.args or future.kwargs:
app.register_middleware(future.middleware,
*future.args,
**future.kwargs)
app.register_middleware(
future.middleware, *future.args, **future.kwargs
)
else:
app.register_middleware(future.middleware)
@@ -91,16 +137,25 @@ class Blueprint:
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)
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,
strict_slashes=None, stream=False, version=None, name=None):
def route(
self,
uri,
methods=frozenset({"GET"}),
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
"""Create a blueprint route from a decorated function.
:param uri: endpoint at which the route will be accessible.
@@ -111,14 +166,30 @@ class Blueprint:
def decorator(handler):
route = FutureRoute(
handler, uri, methods, host, strict_slashes, stream, version,
name)
handler,
uri,
methods,
host,
strict_slashes,
stream,
version,
name,
)
self.routes.append(route)
return handler
return decorator
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=None, version=None, name=None):
def add_route(
self,
handler,
uri,
methods=frozenset({"GET"}),
host=None,
strict_slashes=None,
version=None,
name=None,
):
"""Create a blueprint route from a function.
:param handler: function for handling uri requests. Accepts function,
@@ -132,7 +203,7 @@ class Blueprint:
:return: function or class instance
"""
# Handle HTTPMethodView differently
if hasattr(handler, 'view_class'):
if hasattr(handler, "view_class"):
methods = set()
for method in HTTP_METHODS:
@@ -146,13 +217,19 @@ class Blueprint:
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes, version=version,
name=name)(handler)
self.route(
uri=uri,
methods=methods,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)(handler)
return handler
def websocket(self, uri, host=None, strict_slashes=None, version=None,
name=None):
def websocket(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
"""Create a blueprint websocket route from a decorated function.
:param uri: endpoint at which the route will be accessible.
@@ -161,14 +238,17 @@ class Blueprint:
strict_slashes = self.strict_slashes
def decorator(handler):
route = FutureRoute(handler, uri, [], host, strict_slashes,
False, version, name)
route = FutureRoute(
handler, uri, [], host, strict_slashes, False, version, name
)
self.websocket_routes.append(route)
return handler
return decorator
def add_websocket_route(self, handler, uri, host=None, version=None,
name=None):
def add_websocket_route(
self, handler, uri, host=None, version=None, name=None
):
"""Create a blueprint websocket route from a function.
:param handler: function for handling uri requests. Accepts function,
@@ -184,13 +264,16 @@ class Blueprint:
:param event: Event to listen to.
"""
def decorator(listener):
self.listeners[event].append(listener)
return listener
return decorator
def middleware(self, *args, **kwargs):
"""Create a blueprint middleware from a decorated function."""
def register_middleware(_middleware):
future_middleware = FutureMiddleware(_middleware, args, kwargs)
self.middlewares.append(future_middleware)
@@ -206,10 +289,12 @@ class Blueprint:
def exception(self, *args, **kwargs):
"""Create a blueprint exception from a decorated function."""
def decorator(handler):
exception = FutureException(handler, args, kwargs)
self.exceptions.append(exception)
return handler
return decorator
def static(self, uri, file_or_directory, *args, **kwargs):
@@ -218,12 +303,12 @@ class Blueprint:
:param uri: endpoint at which the route will be accessible.
:param file_or_directory: Static asset.
"""
name = kwargs.pop('name', 'static')
if not name.startswith(self.name + '.'):
name = '{}.{}'.format(self.name, name)
name = kwargs.pop("name", "static")
if not name.startswith(self.name + "."):
name = "{}.{}".format(self.name, name)
kwargs.update(name=name)
strict_slashes = kwargs.get('strict_slashes')
strict_slashes = kwargs.get("strict_slashes")
if strict_slashes is None and self.strict_slashes is not None:
kwargs.update(strict_slashes=self.strict_slashes)
@@ -231,44 +316,107 @@ class Blueprint:
self.statics.append(static)
# Shorthand method decorators
def get(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["GET"], host=host,
strict_slashes=strict_slashes, version=version,
name=name)
def get(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
return self.route(
uri,
methods=["GET"],
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
def post(self, uri, host=None, strict_slashes=None, stream=False,
version=None, name=None):
return self.route(uri, methods=["POST"], host=host,
strict_slashes=strict_slashes, stream=stream,
version=version, name=name)
def post(
self,
uri,
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
return self.route(
uri,
methods=["POST"],
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)
def put(self, uri, host=None, strict_slashes=None, stream=False,
version=None, name=None):
return self.route(uri, methods=["PUT"], host=host,
strict_slashes=strict_slashes, stream=stream,
version=version, name=name)
def put(
self,
uri,
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
return self.route(
uri,
methods=["PUT"],
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)
def head(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["HEAD"], host=host,
strict_slashes=strict_slashes, version=version,
name=name)
def head(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
return self.route(
uri,
methods=["HEAD"],
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
def options(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["OPTIONS"], host=host,
strict_slashes=strict_slashes, version=version,
name=name)
def options(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
return self.route(
uri,
methods=["OPTIONS"],
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
def patch(self, uri, host=None, strict_slashes=None, stream=False,
version=None, name=None):
return self.route(uri, methods=["PATCH"], host=host,
strict_slashes=strict_slashes, stream=stream,
version=version, name=name)
def patch(
self,
uri,
host=None,
strict_slashes=None,
stream=False,
version=None,
name=None,
):
return self.route(
uri,
methods=["PATCH"],
host=host,
strict_slashes=strict_slashes,
stream=stream,
version=version,
name=name,
)
def delete(self, uri, host=None, strict_slashes=None, version=None,
name=None):
return self.route(uri, methods=["DELETE"], host=host,
strict_slashes=strict_slashes, version=version,
name=name)
def delete(
self, uri, host=None, strict_slashes=None, version=None, name=None
):
return self.route(
uri,
methods=["DELETE"],
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)

View File

@@ -1,8 +1,10 @@
import os
import types
from sanic.exceptions import PyFileError
SANIC_PREFIX = 'SANIC_'
SANIC_PREFIX = "SANIC_"
class Config(dict):
@@ -36,7 +38,10 @@ class Config(dict):
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
self.WEBSOCKET_MAX_QUEUE = 32
self.WEBSOCKET_READ_LIMIT = 2 ** 16
self.WEBSOCKET_WRITE_LIMIT = 2 ** 16
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
self.ACCESS_LOG = True
if load_env:
prefix = SANIC_PREFIX if load_env is True else load_env
@@ -60,9 +65,10 @@ class Config(dict):
"""
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)
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):
@@ -71,15 +77,20 @@ class Config(dict):
:param filename: an absolute path to the config file
"""
module = types.ModuleType('config')
module = types.ModuleType("config")
module.__file__ = filename
try:
with open(filename) as config_file:
exec(compile(config_file.read(), filename, 'exec'),
module.__dict__)
exec(
compile(config_file.read(), filename, "exec"),
module.__dict__,
)
except IOError as e:
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
e.strerror = "Unable to load configuration file (%s)" % e.strerror
raise
except Exception as e:
raise PyFileError(filename) from e
self.from_object(module)
return True

View File

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

View File

@@ -1,6 +1,7 @@
import re
import string
# ------------------------------------------------------------ #
# SimpleCookie
# ------------------------------------------------------------ #
@@ -8,18 +9,16 @@ import string
# Straight up copied this section of dark magic from SimpleCookie
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
_UnescapedChars = _LegalChars + " ()/<=>?@[]{}"
_Translator = {n: '\\%03o' % n
for n in set(range(256)) - set(map(ord, _UnescapedChars))}
_Translator.update({
ord('"'): '\\"',
ord('\\'): '\\\\',
})
_Translator = {
n: "\\%03o" % n for n in set(range(256)) - set(map(ord, _UnescapedChars))
}
_Translator.update({ord('"'): '\\"', ord("\\"): "\\\\"})
def _quote(str):
"""Quote a string for use in a cookie header.
r"""Quote a string for use in a cookie header.
If the string does not need to be double-quoted, then just return the
string. Otherwise, surround the string in doublequotes and quote
(with a \) special characters.
@@ -30,7 +29,7 @@ def _quote(str):
return '"' + str.translate(_Translator) + '"'
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
_is_legal_key = re.compile("[%s]+" % re.escape(_LegalChars)).fullmatch
# ------------------------------------------------------------ #
# Custom SimpleCookie
@@ -47,33 +46,37 @@ class CookieJar(dict):
super().__init__()
self.headers = headers
self.cookie_headers = {}
self.header_key = "Set-Cookie"
def __setitem__(self, key, value):
# If this cookie doesn't exist, add it to the header keys
cookie_header = self.cookie_headers.get(key)
if not cookie_header:
if not self.cookie_headers.get(key):
cookie = Cookie(key, value)
cookie['path'] = '/'
cookie_header = MultiHeader("Set-Cookie")
self.cookie_headers[key] = cookie_header
self.headers[cookie_header] = cookie
cookie["path"] = "/"
self.cookie_headers[key] = self.header_key
self.headers.add(self.header_key, cookie)
return super().__setitem__(key, cookie)
else:
self[key].value = value
def __delitem__(self, key):
if key not in self.cookie_headers:
self[key] = ''
self[key]['max-age'] = 0
self[key] = ""
self[key]["max-age"] = 0
else:
cookie_header = self.cookie_headers[key]
del self.headers[cookie_header]
# remove it from header
cookies = self.headers.popall(cookie_header)
for cookie in cookies:
if cookie.key != key:
self.headers.add(cookie_header, cookie)
del self.cookie_headers[key]
return super().__delitem__(key)
class Cookie(dict):
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
_keys = {
"expires": "expires",
"path": "Path",
@@ -83,8 +86,9 @@ class Cookie(dict):
"secure": "Secure",
"httponly": "HttpOnly",
"version": "Version",
"samesite": "SameSite",
}
_flags = {'secure', 'httponly'}
_flags = {"secure", "httponly"}
def __init__(self, key, value):
if key in self._keys:
@@ -102,39 +106,27 @@ class Cookie(dict):
return super().__setitem__(key, value)
def encode(self, encoding):
output = ['%s=%s' % (self.key, _quote(self.value))]
output = ["%s=%s" % (self.key, _quote(self.value))]
for key, value in self.items():
if key == 'max-age':
if key == "max-age":
try:
output.append('%s=%d' % (self._keys[key], value))
output.append("%s=%d" % (self._keys[key], value))
except TypeError:
output.append('%s=%s' % (self._keys[key], value))
elif key == 'expires':
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")
))
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))
output.append("%s=%s" % (self._keys[key], value))
elif key in self._flags and self[key]:
output.append(self._keys[key])
else:
output.append('%s=%s' % (self._keys[key], value))
output.append("%s=%s" % (self._keys[key], value))
return "; ".join(output).encode(encoding)
# ------------------------------------------------------------ #
# Header Trickery
# ------------------------------------------------------------ #
class MultiHeader:
"""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
def encode(self):
return self.name.encode()

View File

@@ -1,6 +1,7 @@
from sanic.response import STATUS_CODES
from sanic.helpers import STATUS_CODES
TRACEBACK_STYLE = '''
TRACEBACK_STYLE = """
<style>
body {
padding: 20px;
@@ -61,9 +62,9 @@ TRACEBACK_STYLE = '''
font-size: 14px;
}
</style>
'''
"""
TRACEBACK_WRAPPER_HTML = '''
TRACEBACK_WRAPPER_HTML = """
<html>
<head>
{style}
@@ -78,27 +79,27 @@ TRACEBACK_WRAPPER_HTML = '''
</div>
</body>
</html>
'''
"""
TRACEBACK_WRAPPER_INNER_HTML = '''
TRACEBACK_WRAPPER_INNER_HTML = """
<h1>{exc_name}</h1>
<h3><code>{exc_value}</code></h3>
<div class="tb-wrapper">
<p class="tb-header">Traceback (most recent call last):</p>
{frame_html}
</div>
'''
"""
TRACEBACK_BORDER = '''
TRACEBACK_BORDER = """
<div class="tb-border">
<b><i>
The above exception was the direct cause of the
following exception:
</i></b>
</div>
'''
"""
TRACEBACK_LINE_HTML = '''
TRACEBACK_LINE_HTML = """
<div class="frame-line">
<p class="frame-descriptor">
File {0.filename}, line <i>{0.lineno}</i>,
@@ -106,15 +107,15 @@ TRACEBACK_LINE_HTML = '''
</p>
<p class="frame-code"><code>{0.line}</code></p>
</div>
'''
"""
INTERNAL_SERVER_ERROR_HTML = '''
INTERNAL_SERVER_ERROR_HTML = """
<h1>Internal Server Error</h1>
<p>
The server encountered an internal error and cannot complete
your request.
</p>
'''
"""
_sanic_exceptions = {}
@@ -124,15 +125,16 @@ def add_status_code(code):
"""
Decorator used for adding exceptions to _sanic_exceptions.
"""
def class_decorator(cls):
cls.status_code = code
_sanic_exceptions[code] = cls
return cls
return class_decorator
class SanicException(Exception):
def __init__(self, message, status_code=None):
super().__init__(message)
@@ -150,6 +152,16 @@ class InvalidUsage(SanicException):
pass
@add_status_code(405)
class MethodNotSupported(SanicException):
def __init__(self, message, method, allowed_methods):
super().__init__(message)
self.headers = dict()
self.headers["Allow"] = ", ".join(allowed_methods)
if method in ["HEAD", "PATCH", "PUT", "DELETE"]:
self.headers["Content-Length"] = 0
@add_status_code(500)
class ServerError(SanicException):
pass
@@ -159,6 +171,7 @@ class ServerError(SanicException):
class ServiceUnavailable(SanicException):
"""The server is currently unavailable (because it is overloaded or
down for maintenance). Generally, this is a temporary state."""
pass
@@ -167,8 +180,6 @@ class URLBuildError(ServerError):
class FileNotFound(NotFound):
pass
def __init__(self, message, path, relative_url):
super().__init__(message)
self.path = path
@@ -184,6 +195,7 @@ class RequestTimeout(SanicException):
the connection. The socket connection has actually been lost - the Web
server has 'timed out' on that particular socket connection.
"""
pass
@@ -198,13 +210,11 @@ class HeaderNotFound(InvalidUsage):
@add_status_code(416)
class ContentRangeError(SanicException):
pass
def __init__(self, message, content_range):
super().__init__(message)
self.headers = {
'Content-Type': 'text/plain',
"Content-Range": "bytes */%s" % (content_range.total,)
"Content-Type": "text/plain",
"Content-Range": "bytes */%s" % (content_range.total,),
}
@@ -217,6 +227,11 @@ class InvalidRangeType(ContentRangeError):
pass
class PyFileError(Exception):
def __init__(self, file):
super().__init__("could not execute config file %s", file)
@add_status_code(401)
class Unauthorized(SanicException):
"""
@@ -252,13 +267,14 @@ class Unauthorized(SanicException):
scheme="Bearer",
realm="Restricted Area")
"""
def __init__(self, message, status_code=None, scheme=None, **kwargs):
super().__init__(message, status_code)
# if auth-scheme is specified, set "WWW-Authenticate" header
if scheme is not None:
values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()]
challenge = ', '.join(values)
values = ['{!s}="{!s}"'.format(k, v) for k, v in kwargs.items()]
challenge = ", ".join(values)
self.headers = {
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
@@ -277,6 +293,6 @@ def abort(status_code, message=None):
if message is None:
message = STATUS_CODES.get(status_code)
# These are stored as bytes in the STATUS_CODES dict
message = message.decode('utf8')
message = message.decode("utf8")
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
raise sanic_exception(message=message, status_code=status_code)

View File

@@ -1,19 +1,21 @@
import sys
from traceback import format_exc, extract_tb
from traceback import extract_tb, format_exc
from sanic.exceptions import (
ContentRangeError,
HeaderNotFound,
INTERNAL_SERVER_ERROR_HTML,
InvalidRangeType,
SanicException,
TRACEBACK_BORDER,
TRACEBACK_LINE_HTML,
TRACEBACK_STYLE,
TRACEBACK_WRAPPER_HTML,
TRACEBACK_WRAPPER_INNER_HTML,
TRACEBACK_BORDER)
ContentRangeError,
HeaderNotFound,
InvalidRangeType,
SanicException,
)
from sanic.log import logger
from sanic.response import text, html
from sanic.response import html, text
class ErrorHandler:
@@ -36,7 +38,8 @@ class ErrorHandler:
return TRACEBACK_WRAPPER_INNER_HTML.format(
exc_name=exception.__class__.__name__,
exc_value=exception,
frame_html=''.join(frame_html))
frame_html="".join(frame_html),
)
def _render_traceback_html(self, exception, request):
exc_type, exc_value, tb = sys.exc_info()
@@ -51,13 +54,14 @@ class ErrorHandler:
exc_name=exception.__class__.__name__,
exc_value=exception,
inner_html=TRACEBACK_BORDER.join(reversed(exceptions)),
path=request.path)
path=request.path,
)
def add(self, exception, handler):
self.handlers.append((exception, handler))
def lookup(self, exception):
handler = self.cached_handlers.get(exception, self._missing)
handler = self.cached_handlers.get(type(exception), self._missing)
if handler is self._missing:
for exception_class, handler in self.handlers:
if isinstance(exception, exception_class):
@@ -79,45 +83,50 @@ class ErrorHandler:
response = None
try:
if handler:
response = handler(request=request, exception=exception)
response = handler(request, exception)
if response is None:
response = self.default(request=request, exception=exception)
response = self.default(request, exception)
except Exception:
self.log(format_exc())
if self.debug:
url = getattr(request, 'url', 'unknown')
response_message = ('Exception raised in exception handler '
'"%s" for uri: "%s"\n%s')
logger.error(response_message,
handler.__name__, url, format_exc())
try:
url = repr(request.url)
except AttributeError:
url = "unknown"
response_message = (
"Exception raised in exception handler " '"%s" for uri: %s'
)
logger.exception(response_message, handler.__name__, url)
return text(response_message % (
handler.__name__, url, format_exc()), 500)
if self.debug:
return text(response_message % (handler.__name__, url), 500)
else:
return text('An error occurred while handling an error', 500)
return text("An error occurred while handling an error", 500)
return response
def log(self, message, level='error'):
def log(self, message, level="error"):
"""
Override this method in an ErrorHandler subclass to prevent
logging exceptions.
Deprecated, do not use.
"""
getattr(logger, level)(message)
def default(self, request, exception):
self.log(format_exc())
try:
url = repr(request.url)
except AttributeError:
url = "unknown"
response_message = "Exception occurred while handling uri: %s"
logger.exception(response_message, url)
if issubclass(type(exception), SanicException):
return text(
'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500),
headers=getattr(exception, 'headers', dict())
"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: '
'"%s"\n%s')
logger.error(response_message, request.url, format_exc())
return html(html_output, status=500)
else:
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
@@ -125,47 +134,54 @@ class ErrorHandler:
class ContentRangeHandler:
"""Class responsible for parsing request header"""
__slots__ = ('start', 'end', 'size', 'total', 'headers')
__slots__ = ("start", "end", "size", "total", "headers")
def __init__(self, request, stats):
self.total = stats.st_size
_range = request.headers.get('Range')
_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 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('-')))
"%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)
"'%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)
"'%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)
"Invalid for Content Range parameters", self
)
else:
# this case represents `Content-Range: bytes 5-`
self.end = self.total
self.end = self.total - 1
else:
if self.start is None:
# this case represents `Content-Range: bytes -5`
self.start = self.total - self.end
self.end = self.total
self.end = self.total - 1
if self.start >= self.end:
raise ContentRangeError(
'Invalid for Content Range parameters', self)
self.size = self.end - self.start
"Invalid for Content Range parameters", self
)
self.size = self.end - self.start + 1
self.headers = {
'Content-Range': "bytes %s-%s/%s" % (
self.start, self.end, self.total)}
"Content-Range": "bytes %s-%s/%s"
% (self.start, self.end, self.total)
}
def __bool__(self):
return self.size > 0

133
sanic/helpers.py Normal file
View File

@@ -0,0 +1,133 @@
"""Defines basics of HTTP standard."""
STATUS_CODES = {
100: b"Continue",
101: b"Switching Protocols",
102: b"Processing",
200: b"OK",
201: b"Created",
202: b"Accepted",
203: b"Non-Authoritative Information",
204: b"No Content",
205: b"Reset Content",
206: b"Partial Content",
207: b"Multi-Status",
208: b"Already Reported",
226: b"IM Used",
300: b"Multiple Choices",
301: b"Moved Permanently",
302: b"Found",
303: b"See Other",
304: b"Not Modified",
305: b"Use Proxy",
307: b"Temporary Redirect",
308: b"Permanent Redirect",
400: b"Bad Request",
401: b"Unauthorized",
402: b"Payment Required",
403: b"Forbidden",
404: b"Not Found",
405: b"Method Not Allowed",
406: b"Not Acceptable",
407: b"Proxy Authentication Required",
408: b"Request Timeout",
409: b"Conflict",
410: b"Gone",
411: b"Length Required",
412: b"Precondition Failed",
413: b"Request Entity Too Large",
414: b"Request-URI Too Long",
415: b"Unsupported Media Type",
416: b"Requested Range Not Satisfiable",
417: b"Expectation Failed",
418: b"I'm a teapot",
422: b"Unprocessable Entity",
423: b"Locked",
424: b"Failed Dependency",
426: b"Upgrade Required",
428: b"Precondition Required",
429: b"Too Many Requests",
431: b"Request Header Fields Too Large",
451: b"Unavailable For Legal Reasons",
500: b"Internal Server Error",
501: b"Not Implemented",
502: b"Bad Gateway",
503: b"Service Unavailable",
504: b"Gateway Timeout",
505: b"HTTP Version Not Supported",
506: b"Variant Also Negotiates",
507: b"Insufficient Storage",
508: b"Loop Detected",
510: b"Not Extended",
511: b"Network Authentication Required",
}
# According to https://tools.ietf.org/html/rfc2616#section-7.1
_ENTITY_HEADERS = frozenset(
[
"allow",
"content-encoding",
"content-language",
"content-length",
"content-location",
"content-md5",
"content-range",
"content-type",
"expires",
"last-modified",
"extension-header",
]
)
# According to https://tools.ietf.org/html/rfc2616#section-13.5.1
_HOP_BY_HOP_HEADERS = frozenset(
[
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailers",
"transfer-encoding",
"upgrade",
]
)
def has_message_body(status):
"""
According to the following RFC message body and length SHOULD NOT
be included in responses status 1XX, 204 and 304.
https://tools.ietf.org/html/rfc2616#section-4.4
https://tools.ietf.org/html/rfc2616#section-4.3
"""
return status not in (204, 304) and not (100 <= status < 200)
def is_entity_header(header):
"""Checks if the given header is an Entity Header"""
return header.lower() in _ENTITY_HEADERS
def is_hop_by_hop_header(header):
"""Checks if the given header is a Hop By Hop header"""
return header.lower() in _HOP_BY_HOP_HEADERS
def remove_entity_headers(headers, allowed=("content-location", "expires")):
"""
Removes all the entity headers present in the headers given.
According to RFC 2616 Section 10.3.5,
Content-Location and Expires are allowed as for the
"strong cache validator".
https://tools.ietf.org/html/rfc2616#section-10.3.5
returns the headers without the entity headers
"""
allowed = set([h.lower() for h in allowed])
headers = {
header: value
for header, value in headers.items()
if not is_entity_header(header) or header.lower() in allowed
}
return headers

View File

@@ -5,59 +5,54 @@ import sys
LOGGING_CONFIG_DEFAULTS = dict(
version=1,
disable_existing_loggers=False,
loggers={
"root": {
"level": "INFO",
"handlers": ["console"]
},
"sanic.root": {"level": "INFO", "handlers": ["console"]},
"sanic.error": {
"level": "INFO",
"handlers": ["error_console"],
"propagate": True,
"qualname": "sanic.error"
"qualname": "sanic.error",
},
"sanic.access": {
"level": "INFO",
"handlers": ["access_console"],
"propagate": True,
"qualname": "sanic.access"
}
"qualname": "sanic.access",
},
},
handlers={
"console": {
"class": "logging.StreamHandler",
"formatter": "generic",
"stream": sys.stdout
"stream": sys.stdout,
},
"error_console": {
"class": "logging.StreamHandler",
"formatter": "generic",
"stream": sys.stderr
"stream": sys.stderr,
},
"access_console": {
"class": "logging.StreamHandler",
"formatter": "access",
"stream": sys.stdout
"stream": sys.stdout,
},
},
formatters={
"generic": {
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
"class": "logging.Formatter"
"class": "logging.Formatter",
},
"access": {
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " +
"%(request)s %(message)s %(status)d %(byte)d",
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
+ "%(request)s %(message)s %(status)d %(byte)d",
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
"class": "logging.Formatter"
"class": "logging.Formatter",
},
}
},
)
logger = logging.getLogger('root')
error_logger = logging.getLogger('sanic.error')
access_logger = logging.getLogger('sanic.access')
logger = logging.getLogger("sanic.root")
error_logger = logging.getLogger("sanic.error")
access_logger = logging.getLogger("sanic.access")

158
sanic/reloader_helpers.py Normal file
View File

@@ -0,0 +1,158 @@
import os
import signal
import subprocess
import sys
from multiprocessing import Process
from time import sleep
def _iter_module_files():
"""This iterates over all relevant Python files.
It goes through all
loaded files from modules, all files in folders of already loaded modules
as well as all files reachable through a package.
"""
# The list call is necessary on Python 3 in case the module
# dictionary modifies during iteration.
for module in list(sys.modules.values()):
if module is None:
continue
filename = getattr(module, "__file__", None)
if filename:
old = None
while not os.path.isfile(filename):
old = filename
filename = os.path.dirname(filename)
if filename == old:
break
else:
if filename[-4:] in (".pyc", ".pyo"):
filename = filename[:-1]
yield filename
def _get_args_for_reloading():
"""Returns the executable."""
rv = [sys.executable]
rv.extend(sys.argv)
return rv
def restart_with_reloader():
"""Create a new process and a subprocess in it with the same arguments as
this one.
"""
args = _get_args_for_reloading()
new_environ = os.environ.copy()
new_environ["SANIC_SERVER_RUNNING"] = "true"
cmd = " ".join(args)
worker_process = Process(
target=subprocess.call,
args=(cmd,),
kwargs=dict(shell=True, env=new_environ),
)
worker_process.start()
return worker_process
def kill_process_children_unix(pid):
"""Find and kill child processes of a process (maximum two level).
:param pid: PID of parent process (process ID)
:return: Nothing
"""
root_process_path = "/proc/{pid}/task/{pid}/children".format(pid=pid)
if not os.path.isfile(root_process_path):
return
with open(root_process_path) as children_list_file:
children_list_pid = children_list_file.read().split()
for child_pid in children_list_pid:
children_proc_path = "/proc/%s/task/%s/children" % (
child_pid,
child_pid,
)
if not os.path.isfile(children_proc_path):
continue
with open(children_proc_path) as children_list_file_2:
children_list_pid_2 = children_list_file_2.read().split()
for _pid in children_list_pid_2:
try:
os.kill(int(_pid), signal.SIGTERM)
except ProcessLookupError:
continue
try:
os.kill(int(child_pid), signal.SIGTERM)
except ProcessLookupError:
continue
def kill_process_children_osx(pid):
"""Find and kill child processes of a process.
:param pid: PID of parent process (process ID)
:return: Nothing
"""
subprocess.run(["pkill", "-P", str(pid)])
def kill_process_children(pid):
"""Find and kill child processes of a process.
:param pid: PID of parent process (process ID)
:return: Nothing
"""
if sys.platform == "darwin":
kill_process_children_osx(pid)
elif sys.platform == "linux":
kill_process_children_unix(pid)
else:
pass # should signal error here
def kill_program_completly(proc):
"""Kill worker and it's child processes and exit.
:param proc: worker process (process ID)
:return: Nothing
"""
kill_process_children(proc.pid)
proc.terminate()
os._exit(0)
def watchdog(sleep_interval):
"""Watch project files, restart worker process if a change happened.
:param sleep_interval: interval in second.
:return: Nothing
"""
mtimes = {}
worker_process = restart_with_reloader()
signal.signal(
signal.SIGTERM, lambda *args: kill_program_completly(worker_process)
)
signal.signal(
signal.SIGINT, lambda *args: kill_program_completly(worker_process)
)
while True:
for filename in _iter_module_files():
try:
mtime = os.stat(filename).st_mtime
except OSError:
continue
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
continue
elif mtime > old_time:
kill_process_children(worker_process.pid)
worker_process.terminate()
worker_process = restart_with_reloader()
mtimes[filename] = mtime
break
sleep(sleep_interval)

View File

@@ -1,23 +1,29 @@
import sys
import json
import sys
from cgi import parse_header
from collections import namedtuple
from http.cookies import SimpleCookie
from httptools import parse_url
from urllib.parse import parse_qs, urlunparse
from httptools import parse_url
from sanic.exceptions import InvalidUsage
from sanic.log import error_logger, logger
try:
from ujson import loads as json_loads
except ImportError:
if sys.version_info[:2] == (3, 5):
def json_loads(data):
# on Python 3.5 json.loads only supports str not bytes
return json.loads(data.decode())
else:
json_loads = json.loads
from sanic.exceptions import InvalidUsage
from sanic.log import error_logger
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
@@ -43,14 +49,32 @@ class RequestParameters(dict):
class Request(dict):
"""Properties of an HTTP request such as URL, headers, etc."""
__slots__ = (
'app', 'headers', 'version', 'method', '_cookies', 'transport',
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr',
'_socket', '_port'
"app",
"headers",
"version",
"method",
"_cookies",
"transport",
"body",
"parsed_json",
"parsed_args",
"parsed_form",
"parsed_files",
"_ip",
"_parsed_url",
"uri_template",
"stream",
"_remote_addr",
"_socket",
"_port",
"__weakref__",
"raw_url",
)
def __init__(self, url_bytes, headers, version, method, transport):
self.raw_url = url_bytes
# TODO: Content-Encoding detection
self._parsed_url = parse_url(url_bytes)
self.app = None
@@ -61,7 +85,7 @@ class Request(dict):
self.transport = transport
# Init but do not inhale
self.body = []
self.body_init()
self.parsed_json = None
self.parsed_form = None
self.parsed_files = None
@@ -72,10 +96,24 @@ class Request(dict):
def __repr__(self):
if self.method is None or not self.path:
return '<{0}>'.format(self.__class__.__name__)
return '<{0}: {1} {2}>'.format(self.__class__.__name__,
self.method,
self.path)
return "<{0}>".format(self.__class__.__name__)
return "<{0}: {1} {2}>".format(
self.__class__.__name__, self.method, self.path
)
def __bool__(self):
if self.transport:
return True
return False
def body_init(self):
self.body = []
def body_push(self, data):
self.body.append(data)
def body_finish(self):
self.body = b"".join(self.body)
@property
def json(self):
@@ -100,8 +138,8 @@ class Request(dict):
:return: token related to request
"""
prefixes = ('Bearer', 'Token')
auth_header = self.headers.get('Authorization')
prefixes = ("Bearer", "Token")
auth_header = self.headers.get("Authorization")
if auth_header is not None:
for prefix in prefixes:
@@ -116,17 +154,20 @@ class Request(dict):
self.parsed_form = RequestParameters()
self.parsed_files = RequestParameters()
content_type = self.headers.get(
'Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
)
content_type, parameters = parse_header(content_type)
try:
if content_type == 'application/x-www-form-urlencoded':
if content_type == "application/x-www-form-urlencoded":
self.parsed_form = RequestParameters(
parse_qs(self.body.decode('utf-8')))
elif content_type == 'multipart/form-data':
parse_qs(self.body.decode("utf-8"))
)
elif content_type == "multipart/form-data":
# TODO: Stream this instead of reading to/from memory
boundary = parameters['boundary'].encode('utf-8')
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
boundary = parameters["boundary"].encode("utf-8")
self.parsed_form, self.parsed_files = parse_multipart_form(
self.body, boundary
)
except Exception:
error_logger.exception("Failed when parsing form")
@@ -144,7 +185,8 @@ class Request(dict):
if self.parsed_args is None:
if self.query_string:
self.parsed_args = RequestParameters(
parse_qs(self.query_string))
parse_qs(self.query_string)
)
else:
self.parsed_args = RequestParameters()
return self.parsed_args
@@ -156,38 +198,42 @@ class Request(dict):
@property
def cookies(self):
if self._cookies is None:
cookie = self.headers.get('Cookie')
cookie = self.headers.get("Cookie")
if cookie is not None:
cookies = SimpleCookie()
cookies.load(cookie)
self._cookies = {name: cookie.value
for name, cookie in cookies.items()}
self._cookies = {
name: cookie.value for name, cookie in cookies.items()
}
else:
self._cookies = {}
return self._cookies
@property
def ip(self):
if not hasattr(self, '_socket'):
if not hasattr(self, "_socket"):
self._get_address()
return self._ip
@property
def port(self):
if not hasattr(self, '_socket'):
if not hasattr(self, "_socket"):
self._get_address()
return self._port
@property
def socket(self):
if not hasattr(self, '_socket'):
self._get_socket()
if not hasattr(self, "_socket"):
self._get_address()
return self._socket
def _get_address(self):
self._socket = (self.transport.get_extra_info('peername') or
(None, None))
self._ip, self._port = self._socket
self._socket = self.transport.get_extra_info("peername") or (
None,
None,
)
self._ip = self._socket[0]
self._port = self._socket[1]
@property
def remote_addr(self):
@@ -195,29 +241,31 @@ class Request(dict):
:return: original client ip.
"""
if not hasattr(self, '_remote_addr'):
forwarded_for = self.headers.get('X-Forwarded-For', '').split(',')
if not hasattr(self, "_remote_addr"):
forwarded_for = self.headers.get("X-Forwarded-For", "").split(",")
remote_addrs = [
addr for addr in [
addr.strip() for addr in forwarded_for
] if addr
]
addr
for addr in [addr.strip() for addr in forwarded_for]
if addr
]
if len(remote_addrs) > 0:
self._remote_addr = remote_addrs[0]
else:
self._remote_addr = ''
self._remote_addr = ""
return self._remote_addr
@property
def scheme(self):
if self.app.websocket_enabled \
and self.headers.get('upgrade') == 'websocket':
scheme = 'ws'
if (
self.app.websocket_enabled
and self.headers.get("upgrade") == "websocket"
):
scheme = "ws"
else:
scheme = 'http'
scheme = "http"
if self.transport.get_extra_info('sslcontext'):
scheme += 's'
if self.transport.get_extra_info("sslcontext"):
scheme += "s"
return scheme
@@ -225,11 +273,11 @@ class Request(dict):
def host(self):
# it appears that httptools doesn't return the host
# so pull it from the headers
return self.headers.get('Host', '')
return self.headers.get("Host", "")
@property
def content_type(self):
return self.headers.get('Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
@property
def match_info(self):
@@ -238,27 +286,23 @@ class Request(dict):
@property
def path(self):
return self._parsed_url.path.decode('utf-8')
return self._parsed_url.path.decode("utf-8")
@property
def query_string(self):
if self._parsed_url.query:
return self._parsed_url.query.decode('utf-8')
return self._parsed_url.query.decode("utf-8")
else:
return ''
return ""
@property
def url(self):
return urlunparse((
self.scheme,
self.host,
self.path,
None,
self.query_string,
None))
return urlunparse(
(self.scheme, self.host, self.path, None, self.query_string, None)
)
File = namedtuple('File', ['type', 'body', 'name'])
File = namedtuple("File", ["type", "body", "name"])
def parse_multipart_form(body, boundary):
@@ -274,42 +318,52 @@ def parse_multipart_form(body, boundary):
form_parts = body.split(boundary)
for form_part in form_parts[1:-1]:
file_name = None
file_type = None
content_type = "text/plain"
content_charset = "utf-8"
field_name = None
line_index = 2
line_end_index = 0
while not line_end_index == -1:
line_end_index = form_part.find(b'\r\n', line_index)
form_line = form_part[line_index:line_end_index].decode('utf-8')
line_end_index = form_part.find(b"\r\n", line_index)
form_line = form_part[line_index:line_end_index].decode("utf-8")
line_index = line_end_index + 2
if not form_line:
break
colon_index = form_line.index(':')
colon_index = form_line.index(":")
form_header_field = form_line[0:colon_index].lower()
form_header_value, form_parameters = parse_header(
form_line[colon_index + 2:])
form_line[colon_index + 2 :]
)
if form_header_field == 'content-disposition':
if 'filename' in form_parameters:
file_name = form_parameters['filename']
field_name = form_parameters.get('name')
elif form_header_field == 'content-type':
file_type = form_header_value
if form_header_field == "content-disposition":
file_name = form_parameters.get("filename")
field_name = form_parameters.get("name")
elif form_header_field == "content-type":
content_type = form_header_value
content_charset = form_parameters.get("charset", "utf-8")
post_data = form_part[line_index:-4]
if file_name or file_type:
file = File(type=file_type, name=file_name, body=post_data)
if field_name in files:
files[field_name].append(file)
if field_name:
post_data = form_part[line_index:-4]
if file_name:
form_file = File(
type=content_type, name=file_name, body=post_data
)
if field_name in files:
files[field_name].append(form_file)
else:
files[field_name] = [form_file]
else:
files[field_name] = [file]
value = post_data.decode(content_charset)
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
else:
value = post_data.decode('utf-8')
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
logger.debug(
"Form-data field does not have a 'name' parameter "
"in the Content-Disposition header"
)
return fields, files

View File

@@ -1,76 +1,23 @@
from functools import partial
from mimetypes import guess_type
from os import path
from urllib.parse import quote_plus
from aiofiles import open as open_async
from multidict import CIMultiDict
from sanic.cookies import CookieJar
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
try:
from ujson import dumps as json_dumps
except BaseException:
from json import dumps as json_dumps
from json import dumps
from aiofiles import open as open_async
from sanic.cookies import CookieJar
STATUS_CODES = {
100: b'Continue',
101: b'Switching Protocols',
102: b'Processing',
200: b'OK',
201: b'Created',
202: b'Accepted',
203: b'Non-Authoritative Information',
204: b'No Content',
205: b'Reset Content',
206: b'Partial Content',
207: b'Multi-Status',
208: b'Already Reported',
226: b'IM Used',
300: b'Multiple Choices',
301: b'Moved Permanently',
302: b'Found',
303: b'See Other',
304: b'Not Modified',
305: b'Use Proxy',
307: b'Temporary Redirect',
308: b'Permanent Redirect',
400: b'Bad Request',
401: b'Unauthorized',
402: b'Payment Required',
403: b'Forbidden',
404: b'Not Found',
405: b'Method Not Allowed',
406: b'Not Acceptable',
407: b'Proxy Authentication Required',
408: b'Request Timeout',
409: b'Conflict',
410: b'Gone',
411: b'Length Required',
412: b'Precondition Failed',
413: b'Request Entity Too Large',
414: b'Request-URI Too Long',
415: b'Unsupported Media Type',
416: b'Requested Range Not Satisfiable',
417: b'Expectation Failed',
418: b'I\'m a teapot',
422: b'Unprocessable Entity',
423: b'Locked',
424: b'Failed Dependency',
426: b'Upgrade Required',
428: b'Precondition Required',
429: b'Too Many Requests',
431: b'Request Header Fields Too Large',
451: b'Unavailable For Legal Reasons',
500: b'Internal Server Error',
501: b'Not Implemented',
502: b'Bad Gateway',
503: b'Service Unavailable',
504: b'Gateway Timeout',
505: b'HTTP Version Not Supported',
506: b'Variant Also Negotiates',
507: b'Insufficient Storage',
508: b'Loop Detected',
510: b'Not Extended',
511: b'Network Authentication Required'
}
# This is done in order to ensure that the JSON response is
# kept consistent across both ujson and inbuilt json usage.
json_dumps = partial(dumps, separators=(",", ":"))
class BaseHTTPResponse:
@@ -83,16 +30,18 @@ class BaseHTTPResponse:
return str(data).encode()
def _parse_headers(self):
headers = b''
headers = b""
for name, value in self.headers.items():
try:
headers += (
b'%b: %b\r\n' % (
name.encode(), value.encode('utf-8')))
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')))
headers += b"%b: %b\r\n" % (
str(name).encode(),
str(value).encode("utf-8"),
)
return headers
@@ -105,19 +54,24 @@ class BaseHTTPResponse:
class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = (
'transport', 'streaming_fn', 'status',
'content_type', 'headers', '_cookies'
"protocol",
"streaming_fn",
"status",
"content_type",
"headers",
"_cookies",
)
def __init__(self, streaming_fn, status=200, headers=None,
content_type='text/plain'):
def __init__(
self, streaming_fn, status=200, headers=None, content_type="text/plain"
):
self.content_type = content_type
self.streaming_fn = streaming_fn
self.status = status
self.headers = headers or {}
self.headers = CIMultiDict(headers or {})
self._cookies = None
def write(self, data):
async def write(self, data):
"""Writes a chunk of data to the streaming response.
:param data: bytes-ish data to be written.
@@ -125,58 +79,69 @@ class StreamingHTTPResponse(BaseHTTPResponse):
if type(data) != bytes:
data = self._encode_body(data)
self.transport.write(
b"%x\r\n%b\r\n" % (len(data), data))
self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
await self.protocol.drain()
async def stream(
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
self, version="1.1", keep_alive=False, keep_alive_timeout=None
):
"""Streams headers, runs the `streaming_fn` callback that writes
content to the response body, then finalizes the response body.
"""
headers = self.get_headers(
version, keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout)
self.transport.write(headers)
version,
keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout,
)
self.protocol.push_data(headers)
await self.protocol.drain()
await self.streaming_fn(self)
self.transport.write(b'0\r\n\r\n')
self.protocol.push_data(b"0\r\n\r\n")
# no need to await drain here after this write, because it is the
# very last thing we write and nothing needs to wait for it.
def get_headers(
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
self, version="1.1", keep_alive=False, keep_alive_timeout=None
):
# 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''
timeout_header = b""
if keep_alive and keep_alive_timeout is not None:
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
self.headers['Transfer-Encoding'] = 'chunked'
self.headers.pop('Content-Length', None)
self.headers['Content-Type'] = self.headers.get(
'Content-Type', self.content_type)
self.headers["Transfer-Encoding"] = "chunked"
self.headers.pop("Content-Length", None)
self.headers["Content-Type"] = self.headers.get(
"Content-Type", self.content_type
)
headers = self._parse_headers()
if self.status is 200:
status = b'OK'
if self.status == 200:
status = b"OK"
else:
status = STATUS_CODES.get(self.status)
return (b'HTTP/%b %d %b\r\n'
b'%b'
b'%b\r\n') % (
version.encode(),
self.status,
status,
timeout_header,
headers
)
return (b"HTTP/%b %d %b\r\n" b"%b" b"%b\r\n") % (
version.encode(),
self.status,
status,
timeout_header,
headers,
)
class HTTPResponse(BaseHTTPResponse):
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
__slots__ = ("body", "status", "content_type", "headers", "_cookies")
def __init__(self, body=None, status=200, headers=None,
content_type='text/plain', body_bytes=b''):
def __init__(
self,
body=None,
status=200,
headers=None,
content_type="text/plain",
body_bytes=b"",
):
self.content_type = content_type
if body is not None:
@@ -185,41 +150,48 @@ class HTTPResponse(BaseHTTPResponse):
self.body = body_bytes
self.status = status
self.headers = headers or {}
self.headers = CIMultiDict(headers or {})
self._cookies = None
def output(
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
# 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''
timeout_header = b""
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)
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
body = b""
if has_message_body(self.status):
body = self.body
self.headers["Content-Length"] = self.headers.get(
"Content-Length", len(self.body)
)
self.headers["Content-Type"] = self.headers.get(
"Content-Type", self.content_type
)
if self.status in (304, 412):
self.headers = remove_entity_headers(self.headers)
headers = self._parse_headers()
if self.status is 200:
status = b'OK'
if self.status == 200:
status = b"OK"
else:
status = STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE")
return (b'HTTP/%b %d %b\r\n'
b'Connection: %b\r\n'
b'%b'
b'%b\r\n'
b'%b') % (
version.encode(),
self.status,
status,
b'keep-alive' if keep_alive else b'close',
timeout_header,
headers,
self.body
)
return (
b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b"
) % (
version.encode(),
self.status,
status,
b"keep-alive" if keep_alive else b"close",
timeout_header,
headers,
body,
)
@property
def cookies(self):
@@ -228,9 +200,14 @@ class HTTPResponse(BaseHTTPResponse):
return self._cookies
def json(body, status=200, headers=None,
content_type="application/json", dumps=json_dumps,
**kwargs):
def json(
body,
status=200,
headers=None,
content_type="application/json",
dumps=json_dumps,
**kwargs
):
"""
Returns response object with body in json format.
@@ -239,12 +216,17 @@ def json(body, status=200, headers=None,
:param headers: Custom Headers.
:param kwargs: Remaining arguments that are passed to the json encoder.
"""
return HTTPResponse(dumps(body, **kwargs), headers=headers,
status=status, content_type=content_type)
return HTTPResponse(
dumps(body, **kwargs),
headers=headers,
status=status,
content_type=content_type,
)
def text(body, status=200, headers=None,
content_type="text/plain; charset=utf-8"):
def text(
body, status=200, headers=None, content_type="text/plain; charset=utf-8"
):
"""
Returns response object with body in text format.
@@ -254,12 +236,13 @@ def text(body, status=200, headers=None,
:param content_type: the content type (string) of the response
"""
return HTTPResponse(
body, status=status, headers=headers,
content_type=content_type)
body, status=status, headers=headers, content_type=content_type
)
def raw(body, status=200, headers=None,
content_type="application/octet-stream"):
def raw(
body, status=200, headers=None, content_type="application/octet-stream"
):
"""
Returns response object without encoding the body.
@@ -268,8 +251,12 @@ def raw(body, status=200, headers=None,
: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)
return HTTPResponse(
body_bytes=body,
status=status,
headers=headers,
content_type=content_type,
)
def html(body, status=200, headers=None):
@@ -280,12 +267,22 @@ def html(body, status=200, headers=None):
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8")
return HTTPResponse(
body,
status=status,
headers=headers,
content_type="text/html; charset=utf-8",
)
async def file(
location, mime_type=None, headers=None, filename=None, _range=None):
location,
status=200,
mime_type=None,
headers=None,
filename=None,
_range=None,
):
"""Return a response object with file data.
:param location: Location of file on system.
@@ -297,29 +294,41 @@ async def file(
headers = headers or {}
if filename:
headers.setdefault(
'Content-Disposition',
'attachment; filename="{}"'.format(filename))
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
)
filename = filename or path.split(location)[-1]
async with open_async(location, mode='rb') as _file:
async with open_async(location, mode="rb") as _file:
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)
headers["Content-Range"] = "bytes %s-%s/%s" % (
_range.start,
_range.end,
_range.total,
)
status = 206
else:
out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
return HTTPResponse(status=200,
headers=headers,
content_type=mime_type,
body_bytes=out_stream)
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
return HTTPResponse(
status=status,
headers=headers,
content_type=mime_type,
body_bytes=out_stream,
)
async def file_stream(
location, chunk_size=4096, mime_type=None, headers=None,
filename=None, _range=None):
location,
status=200,
chunk_size=4096,
mime_type=None,
headers=None,
filename=None,
_range=None,
):
"""Return a streaming response object with file data.
:param location: Location of file on system.
@@ -332,11 +341,11 @@ async def file_stream(
headers = headers or {}
if filename:
headers.setdefault(
'Content-Disposition',
'attachment; filename="{}"'.format(filename))
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
)
filename = filename or path.split(location)[-1]
_file = await open_async(location, mode='rb')
_file = await open_async(location, mode="rb")
async def _streaming_fn(response):
nonlocal _file, chunk_size
@@ -350,30 +359,39 @@ async def file_stream(
if len(content) < 1:
break
to_send -= len(content)
response.write(content)
await response.write(content)
else:
while True:
content = await _file.read(chunk_size)
if len(content) < 1:
break
response.write(content)
await response.write(content)
finally:
await _file.close()
return # Returning from this fn closes the stream
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
if _range:
headers['Content-Range'] = 'bytes %s-%s/%s' % (
_range.start, _range.end, _range.total)
return StreamingHTTPResponse(streaming_fn=_streaming_fn,
status=200,
headers=headers,
content_type=mime_type)
headers["Content-Range"] = "bytes %s-%s/%s" % (
_range.start,
_range.end,
_range.total,
)
status = 206
return StreamingHTTPResponse(
streaming_fn=_streaming_fn,
status=status,
headers=headers,
content_type=mime_type,
)
def stream(
streaming_fn, status=200, headers=None,
content_type="text/plain; charset=utf-8"):
streaming_fn,
status=200,
headers=None,
content_type="text/plain; charset=utf-8",
):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
@@ -393,15 +411,13 @@ def stream(
:param headers: Custom Headers.
"""
return StreamingHTTPResponse(
streaming_fn,
headers=headers,
content_type=content_type,
status=status
streaming_fn, headers=headers, content_type=content_type, status=status
)
def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"):
def redirect(
to, headers=None, status=302, content_type="text/html; charset=utf-8"
):
"""Abort execution and cause a 302 redirect (by default).
:param to: path or fully qualified URL to redirect to
@@ -412,10 +428,12 @@ def redirect(to, headers=None, status=302,
"""
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers['Location'] = to
headers["Location"] = safe_to
return HTTPResponse(
status=status,
headers=headers,
content_type=content_type)
status=status, headers=headers, content_type=content_type
)

View File

@@ -1,29 +1,38 @@
import re
import uuid
from collections import defaultdict, namedtuple
from collections.abc import Iterable
from functools import lru_cache
from urllib.parse import unquote
from sanic.exceptions import NotFound, InvalidUsage
from sanic.exceptions import MethodNotSupported, NotFound
from sanic.views import CompositionView
Route = namedtuple(
'Route',
['handler', 'methods', 'pattern', 'parameters', 'name', 'uri'])
Parameter = namedtuple('Parameter', ['name', 'cast'])
"Route", ["handler", "methods", "pattern", "parameters", "name", "uri"]
)
Parameter = namedtuple("Parameter", ["name", "cast"])
REGEX_TYPES = {
'string': (str, r'[^/]+'),
'int': (int, r'\d+'),
'number': (float, r'[0-9\\.]+'),
'alpha': (str, r'[A-Za-z]+'),
'path': (str, r'[^/].*?'),
"string": (str, r"[^/]+"),
"int": (int, r"\d+"),
"number": (float, r"[0-9\\.]+"),
"alpha": (str, r"[A-Za-z]+"),
"path": (str, r"[^/].*?"),
"uuid": (
uuid.UUID,
r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-"
r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}",
),
}
ROUTER_CACHE_SIZE = 1024
def url_hash(url):
return url.count('/')
return url.count("/")
class RouteExists(Exception):
@@ -34,6 +43,10 @@ class RouteDoesNotExist(Exception):
pass
class ParameterNameConflicts(Exception):
pass
class Router:
"""Router supports basic routing with parameters and method checks
@@ -60,10 +73,11 @@ class Router:
also be passed in as the type. The argument given to the function will
always be a string, independent of the type.
"""
routes_static = None
routes_dynamic = None
routes_always_check = None
parameter_pattern = re.compile(r'<(.+?)>')
parameter_pattern = re.compile(r"<(.+?)>")
def __init__(self):
self.routes_all = {}
@@ -90,9 +104,9 @@ class Router:
"""
# We could receive NAME or NAME:PATTERN
name = parameter_string
pattern = 'string'
if ':' in parameter_string:
name, pattern = parameter_string.split(':', 1)
pattern = "string"
if ":" in parameter_string:
name, pattern = parameter_string.split(":", 1)
if not name:
raise ValueError(
"Invalid parameter syntax: {}".format(parameter_string)
@@ -104,8 +118,16 @@ class Router:
return name, _type, pattern
def add(self, uri, methods, handler, host=None, strict_slashes=False,
version=None, name=None):
def add(
self,
uri,
methods,
handler,
host=None,
strict_slashes=False,
version=None,
name=None,
):
"""Add a handler to the route list
:param uri: path to match
@@ -119,33 +141,42 @@ class Router:
:return: Nothing
"""
if version is not None:
version = re.escape(str(version).strip('/').lstrip('v'))
uri = "/".join(["/v{}".format(version), uri.lstrip('/')])
version = re.escape(str(version).strip("/").lstrip("v"))
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
# add regular version
self._add(uri, methods, handler, host, name)
if strict_slashes:
return
if not isinstance(host, str) and host is not None:
# we have gotten back to the top of the recursion tree where the
# host was originally a list. By now, we've processed the strict
# slashes logic on the leaf nodes (the individual host strings in
# the list of host)
return
# Add versions with and without trailing /
slashed_methods = self.routes_all.get(uri + '/', frozenset({}))
slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
unslashed_methods = self.routes_all.get(uri[:-1], frozenset({}))
if isinstance(methods, Iterable):
_slash_is_missing = all(method in slashed_methods for
method in methods)
_slash_is_missing = all(
method in slashed_methods for method in methods
)
_without_slash_is_missing = all(
method in unslashed_methods for method in methods
)
else:
_slash_is_missing = methods in slashed_methods
_without_slash_is_missing = methods in unslashed_methods
slash_is_missing = (
not uri[-1] == '/' and not _slash_is_missing
)
slash_is_missing = not uri[-1] == "/" and not _slash_is_missing
without_slash_is_missing = (
uri[-1] == '/' and not
self.routes_all.get(uri[:-1], False) and not
uri == '/'
uri[-1] == "/" and not _without_slash_is_missing and not uri == "/"
)
# add version with trailing slash
if slash_is_missing:
self._add(uri + '/', methods, handler, host, name)
self._add(uri + "/", methods, handler, host, name)
# add version without trailing slash
elif without_slash_is_missing:
self._add(uri[:-1], methods, handler, host, name)
@@ -168,8 +199,10 @@ class Router:
else:
if not isinstance(host, Iterable):
raise ValueError("Expected either string or Iterable of "
"host strings, not {!r}".format(host))
raise ValueError(
"Expected either string or Iterable of "
"host strings, not {!r}".format(host)
)
for host_ in host:
self.add(uri, methods, handler, host_, name)
@@ -180,40 +213,48 @@ class Router:
methods = frozenset(methods)
parameters = []
parameter_names = set()
properties = {"unhashable": None}
def add_parameter(match):
name = match.group(1)
name, _type, pattern = self.parse_parameter_string(name)
parameter = Parameter(
name=name, cast=_type)
if name in parameter_names:
raise ParameterNameConflicts(
"Multiple parameter named <{name}> "
"in route uri {uri}".format(name=name, uri=uri)
)
parameter_names.add(name)
parameter = Parameter(name=name, cast=_type)
parameters.append(parameter)
# Mark the whole route as unhashable if it has the hash key in it
if re.search(r'(^|[^^]){1}/', pattern):
properties['unhashable'] = True
if re.search(r"(^|[^^]){1}/", pattern):
properties["unhashable"] = True
# Mark the route as unhashable if it matches the hash key
elif re.search(r'/', pattern):
properties['unhashable'] = True
elif re.search(r"/", pattern):
properties["unhashable"] = True
return '({})'.format(pattern)
return "({})".format(pattern)
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri)
pattern = re.compile(r'^{}$'.format(pattern_string))
pattern = re.compile(r"^{}$".format(pattern_string))
def merge_route(route, methods, handler):
# merge to the existing route when possible.
if not route.methods or not methods:
# method-unspecified routes are not mergeable.
raise RouteExists(
"Route already registered: {}".format(uri))
raise RouteExists("Route already registered: {}".format(uri))
elif route.methods.intersection(methods):
# already existing method is not overloadable.
duplicated = methods.intersection(route.methods)
raise RouteExists(
"Route already registered: {} [{}]".format(
uri, ','.join(list(duplicated))))
uri, ",".join(list(duplicated))
)
)
if isinstance(route.handler, CompositionView):
view = route.handler
else:
@@ -221,19 +262,22 @@ class Router:
view.add(route.methods, route.handler)
view.add(methods, handler)
route = route._replace(
handler=view, methods=methods.union(route.methods))
handler=view, methods=methods.union(route.methods)
)
return route
if parameters:
# TODO: This is too complex, we need to reduce the complexity
if properties['unhashable']:
if properties["unhashable"]:
routes_to_check = self.routes_always_check
ndx, route = self.check_dynamic_route_exists(
pattern, routes_to_check)
pattern, routes_to_check, parameters
)
else:
routes_to_check = self.routes_dynamic[url_hash(uri)]
ndx, route = self.check_dynamic_route_exists(
pattern, routes_to_check)
pattern, routes_to_check, parameters
)
if ndx != -1:
# Pop the ndx of the route, no dups of the same route
routes_to_check.pop(ndx)
@@ -244,35 +288,41 @@ class Router:
# if available
# special prefix for static files
is_static = False
if name and name.startswith('_static_'):
if name and name.startswith("_static_"):
is_static = True
name = name.split('_static_', 1)[-1]
name = name.split("_static_", 1)[-1]
if hasattr(handler, '__blueprintname__'):
handler_name = '{}.{}'.format(
handler.__blueprintname__, name or handler.__name__)
if hasattr(handler, "__blueprintname__"):
handler_name = "{}.{}".format(
handler.__blueprintname__, name or handler.__name__
)
else:
handler_name = name or getattr(handler, '__name__', None)
handler_name = name or getattr(handler, "__name__", None)
if route:
route = merge_route(route, methods, handler)
else:
route = Route(
handler=handler, methods=methods, pattern=pattern,
parameters=parameters, name=handler_name, uri=uri)
handler=handler,
methods=methods,
pattern=pattern,
parameters=parameters,
name=handler_name,
uri=uri,
)
self.routes_all[uri] = route
if is_static:
pair = self.routes_static_files.get(handler_name)
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
if not (pair and (pair[0] + "/" == uri or uri + "/" == pair[0])):
self.routes_static_files[handler_name] = (uri, route)
else:
pair = self.routes_names.get(handler_name)
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
if not (pair and (pair[0] + "/" == uri or uri + "/" == pair[0])):
self.routes_names[handler_name] = (uri, route)
if properties['unhashable']:
if properties["unhashable"]:
self.routes_always_check.append(route)
elif parameters:
self.routes_dynamic[url_hash(uri)].append(route)
@@ -280,9 +330,9 @@ class Router:
self.routes_static[uri] = route
@staticmethod
def check_dynamic_route_exists(pattern, routes_to_check):
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
for ndx, route in enumerate(routes_to_check):
if route.pattern == pattern:
if route.pattern == pattern and route.parameters == parameters:
return ndx, route
else:
return -1, None
@@ -307,8 +357,10 @@ class Router:
if route in self.routes_always_check:
self.routes_always_check.remove(route)
elif url_hash(uri) in self.routes_dynamic \
and route in self.routes_dynamic[url_hash(uri)]:
elif (
url_hash(uri) in self.routes_dynamic
and route in self.routes_dynamic[url_hash(uri)]
):
self.routes_dynamic[url_hash(uri)].remove(route)
else:
self.routes_static.pop(uri)
@@ -327,7 +379,7 @@ class Router:
if not view_name:
return (None, None)
if view_name == 'static' or view_name.endswith('.static'):
if view_name == "static" or view_name.endswith(".static"):
return self.routes_static_files.get(name, (None, None))
return self.routes_names.get(view_name, (None, None))
@@ -341,14 +393,25 @@ class Router:
"""
# No virtual hosts specified; default behavior
if not self.hosts:
return self._get(request.path, request.method, '')
return self._get(request.path, request.method, "")
# virtual hosts specified; try to match route to the host header
try:
return self._get(request.path, request.method,
request.headers.get("Host", ''))
return self._get(
request.path, request.method, request.headers.get("Host", "")
)
# try default hosts
except NotFound:
return self._get(request.path, request.method, '')
return self._get(request.path, request.method, "")
def get_supported_methods(self, url):
"""Get a list of supported methods for a url and optional host.
:param url: URL string (including host)
:return: frozenset of supported methods
"""
route = self.routes_all.get(url)
# if methods are None then this logic will prevent an error
return getattr(route, "methods", None) or frozenset()
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, url, method, host):
@@ -359,12 +422,14 @@ class Router:
:param method: request method
:return: handler, arguments, keyword arguments
"""
url = host + url
url = unquote(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)
method_not_supported = MethodNotSupported(
"Method {} not allowed for URL {}".format(method, url),
method=method,
allowed_methods=self.get_supported_methods(url),
)
if route:
if route.methods and method not in route.methods:
raise method_not_supported
@@ -390,13 +455,14 @@ class Router:
# Route was found but the methods didn't match
if route_found:
raise method_not_supported
raise NotFound('Requested URL {} not found'.format(url))
raise NotFound("Requested URL {} not found".format(url))
kwargs = {p.name: p.cast(value)
for value, p
in zip(match.groups(1), route.parameters)}
kwargs = {
p.name: p.cast(value)
for value, p in zip(match.groups(1), route.parameters)
}
route_handler = route.handler
if hasattr(route_handler, 'handlers'):
if hasattr(route_handler, "handlers"):
route_handler = route_handler.handlers[method]
return route_handler, [], kwargs, route.uri
@@ -407,9 +473,10 @@ class Router:
"""
try:
handler = self.get(request)[0]
except (NotFound, InvalidUsage):
except (NotFound, MethodNotSupported):
return False
if (hasattr(handler, 'view_class') and
hasattr(handler.view_class, request.method.lower())):
if hasattr(handler, "view_class") and hasattr(
handler.view_class, request.method.lower()
):
handler = getattr(handler.view_class, request.method.lower())
return hasattr(handler, 'is_stream')
return hasattr(handler, "is_stream")

View File

@@ -1,85 +1,105 @@
import asyncio
import os
import traceback
from functools import partial
from inspect import isawaitable
from multiprocessing import Process
from signal import (
SIGTERM, SIGINT,
signal as signal_func,
Signals
)
from socket import (
socket,
SOL_SOCKET,
SO_REUSEADDR,
)
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func
from socket import SO_REUSEADDR, SOL_SOCKET, socket
from time import time
from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
from multidict import CIMultiDict
from sanic.exceptions import (
InvalidUsage,
PayloadTooLarge,
RequestTimeout,
ServerError,
ServiceUnavailable,
)
from sanic.log import access_logger, logger
from sanic.request import Request
from sanic.response import HTTPResponse
try:
import uvloop as async_loop
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
async_loop = asyncio
from sanic.log import logger, access_logger
from sanic.response import HTTPResponse
from sanic.request import Request
from sanic.exceptions import (
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError,
ServiceUnavailable)
current_time = None
pass
class Signal:
stopped = False
class CIDict(dict):
"""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
"""
def get(self, key, default=None):
return super().get(key.casefold(), default)
def __getitem__(self, key):
return super().__getitem__(key.casefold())
def __setitem__(self, key, value):
return super().__setitem__(key.casefold(), value)
def __contains__(self, key):
return super().__contains__(key.casefold())
class HttpProtocol(asyncio.Protocol):
__slots__ = (
# event loop, connection
'loop', 'transport', 'connections', 'signal',
"loop",
"transport",
"connections",
"signal",
# request params
'parser', 'request', 'url', 'headers',
"parser",
"request",
"url",
"headers",
# request config
'request_handler', 'request_timeout', 'response_timeout',
'keep_alive_timeout', 'request_max_size', 'request_class',
'is_request_stream', 'router',
"request_handler",
"request_timeout",
"response_timeout",
"keep_alive_timeout",
"request_max_size",
"request_class",
"is_request_stream",
"router",
"error_handler",
# enable or disable access log purpose
'access_log',
"access_log",
# connection management
'_total_request_size', '_request_timeout_handler',
'_response_timeout_handler', '_keep_alive_timeout_handler',
'_last_request_time', '_last_response_time', '_is_stream_handler')
"_total_request_size",
"_request_timeout_handler",
"_response_timeout_handler",
"_keep_alive_timeout_handler",
"_last_request_time",
"_last_response_time",
"_is_stream_handler",
"_not_paused",
"_request_handler_task",
"_request_stream_task",
"_keep_alive",
"_header_fragment",
"state",
"_debug",
)
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections=set(), request_timeout=60,
response_timeout=60, keep_alive_timeout=5,
request_max_size=None, request_class=None, access_log=True,
keep_alive=True, is_request_stream=False, router=None,
state=None, debug=False, **kwargs):
def __init__(
self,
*,
loop,
request_handler,
error_handler,
signal=Signal(),
connections=set(),
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
request_max_size=None,
request_class=None,
access_log=True,
keep_alive=True,
is_request_stream=False,
router=None,
state=None,
debug=False,
**kwargs
):
self.loop = loop
self.transport = None
self.request = None
@@ -99,6 +119,7 @@ class HttpProtocol(asyncio.Protocol):
self.request_class = request_class or Request
self.is_request_stream = is_request_stream
self._is_stream_handler = False
self._not_paused = asyncio.Event(loop=loop)
self._total_request_size = 0
self._request_timeout_handler = None
self._response_timeout_handler = None
@@ -108,18 +129,20 @@ class HttpProtocol(asyncio.Protocol):
self._request_handler_task = None
self._request_stream_task = None
self._keep_alive = keep_alive
self._header_fragment = b''
self._header_fragment = b""
self.state = state if state else {}
if 'requests_count' not in self.state:
self.state['requests_count'] = 0
if "requests_count" not in self.state:
self.state["requests_count"] = 0
self._debug = debug
self._not_paused.set()
@property
def keep_alive(self):
return (
self._keep_alive and
not self.signal.stopped and
self.parser.should_keep_alive())
self._keep_alive
and not self.signal.stopped
and self.parser.should_keep_alive()
)
# -------------------------------------------- #
# Connection
@@ -128,12 +151,17 @@ class HttpProtocol(asyncio.Protocol):
def connection_made(self, transport):
self.connections.add(self)
self._request_timeout_handler = self.loop.call_later(
self.request_timeout, self.request_timeout_callback)
self.request_timeout, self.request_timeout_callback
)
self.transport = transport
self._last_request_time = current_time
self._last_request_time = time()
def connection_lost(self, exc):
self.connections.discard(self)
if self._request_handler_task:
self._request_handler_task.cancel()
if self._request_stream_task:
self._request_stream_task.cancel()
if self._request_timeout_handler:
self._request_timeout_handler.cancel()
if self._response_timeout_handler:
@@ -141,56 +169,57 @@ class HttpProtocol(asyncio.Protocol):
if self._keep_alive_timeout_handler:
self._keep_alive_timeout_handler.cancel()
def pause_writing(self):
self._not_paused.clear()
def resume_writing(self):
self._not_paused.set()
def request_timeout_callback(self):
# See the docstring in the RequestTimeout exception, to see
# exactly what this timeout is checking for.
# Check if elapsed time since request initiated exceeds our
# configured maximum request timeout value
time_elapsed = current_time - self._last_request_time
time_elapsed = time() - self._last_request_time
if time_elapsed < self.request_timeout:
time_left = self.request_timeout - time_elapsed
self._request_timeout_handler = (
self.loop.call_later(time_left,
self.request_timeout_callback)
self._request_timeout_handler = self.loop.call_later(
time_left, self.request_timeout_callback
)
else:
if self._request_stream_task:
self._request_stream_task.cancel()
if self._request_handler_task:
self._request_handler_task.cancel()
try:
raise RequestTimeout('Request Timeout')
except RequestTimeout as exception:
self.write_error(exception)
self.write_error(RequestTimeout("Request Timeout"))
def response_timeout_callback(self):
# Check if elapsed time since response was initiated exceeds our
# configured maximum request timeout value
time_elapsed = current_time - self._last_request_time
time_elapsed = time() - self._last_request_time
if time_elapsed < self.response_timeout:
time_left = self.response_timeout - time_elapsed
self._response_timeout_handler = (
self.loop.call_later(time_left,
self.response_timeout_callback)
self._response_timeout_handler = self.loop.call_later(
time_left, self.response_timeout_callback
)
else:
try:
raise ServiceUnavailable('Response Timeout')
except ServiceUnavailable as exception:
self.write_error(exception)
if self._request_stream_task:
self._request_stream_task.cancel()
if self._request_handler_task:
self._request_handler_task.cancel()
self.write_error(ServiceUnavailable("Response Timeout"))
def keep_alive_timeout_callback(self):
# Check if elapsed time since last response exceeds our configured
# maximum keep alive timeout value
time_elapsed = current_time - self._last_response_time
time_elapsed = time() - self._last_response_time
if time_elapsed < self.keep_alive_timeout:
time_left = self.keep_alive_timeout - time_elapsed
self._keep_alive_timeout_handler = (
self.loop.call_later(time_left,
self.keep_alive_timeout_callback)
self._keep_alive_timeout_handler = self.loop.call_later(
time_left, self.keep_alive_timeout_callback
)
else:
logger.info('KeepAlive Timeout. Closing connection.')
logger.debug("KeepAlive Timeout. Closing connection.")
self.transport.close()
self.transport = None
@@ -203,8 +232,7 @@ class HttpProtocol(asyncio.Protocol):
# memory limits
self._total_request_size += len(data)
if self._total_request_size > self.request_max_size:
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
self.write_error(PayloadTooLarge("Payload Too Large"))
# Create parser if this is the first time we're receiving data
if self.parser is None:
@@ -213,17 +241,16 @@ class HttpProtocol(asyncio.Protocol):
self.parser = HttpRequestParser(self)
# requests count
self.state['requests_count'] = self.state['requests_count'] + 1
self.state["requests_count"] = self.state["requests_count"] + 1
# Parse request chunk or close connection
try:
self.parser.feed_data(data)
except HttpParserError:
message = 'Bad Request'
message = "Bad Request"
if self._debug:
message += '\n' + traceback.format_exc()
exception = InvalidUsage(message)
self.write_error(exception)
message += "\n" + traceback.format_exc()
self.write_error(InvalidUsage(message))
def on_url(self, url):
if not self.url:
@@ -235,26 +262,28 @@ class HttpProtocol(asyncio.Protocol):
self._header_fragment += name
if value is not None:
if self._header_fragment == b'Content-Length' \
and int(value) > self.request_max_size:
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
if (
self._header_fragment == b"Content-Length"
and int(value) > self.request_max_size
):
self.write_error(PayloadTooLarge("Payload Too Large"))
try:
value = value.decode()
except UnicodeDecodeError:
value = value.decode('latin_1')
value = value.decode("latin_1")
self.headers.append(
(self._header_fragment.decode().casefold(), value))
(self._header_fragment.decode().casefold(), value)
)
self._header_fragment = b''
self._header_fragment = b""
def on_headers_complete(self):
self.request = self.request_class(
url_bytes=self.url,
headers=CIDict(self.headers),
headers=CIMultiDict(self.headers),
version=self.parser.get_http_version(),
method=self.parser.get_method().decode(),
transport=self.transport
transport=self.transport,
)
# Remove any existing KeepAlive handler here,
# It will be recreated if required on the new request.
@@ -263,7 +292,8 @@ class HttpProtocol(asyncio.Protocol):
self._keep_alive_timeout_handler = None
if self.is_request_stream:
self._is_stream_handler = self.router.is_stream_handler(
self.request)
self.request
)
if self._is_stream_handler:
self.request.stream = asyncio.Queue()
self.execute_request_handler()
@@ -271,9 +301,10 @@ class HttpProtocol(asyncio.Protocol):
def on_body(self, body):
if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task(
self.request.stream.put(body))
self.request.stream.put(body)
)
return
self.request.body.append(body)
self.request.body_push(body)
def on_message_complete(self):
# Entire request (headers and whole body) is received.
@@ -283,46 +314,49 @@ class HttpProtocol(asyncio.Protocol):
self._request_timeout_handler = None
if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task(
self.request.stream.put(None))
self.request.stream.put(None)
)
return
self.request.body = b''.join(self.request.body)
self.request.body_finish()
self.execute_request_handler()
def execute_request_handler(self):
self._response_timeout_handler = self.loop.call_later(
self.response_timeout, self.response_timeout_callback)
self._last_request_time = current_time
self.response_timeout, self.response_timeout_callback
)
self._last_request_time = time()
self._request_handler_task = self.loop.create_task(
self.request_handler(
self.request,
self.write_response,
self.stream_response))
self.request, self.write_response, self.stream_response
)
)
# -------------------------------------------- #
# Responding
# -------------------------------------------- #
def log_response(self, response):
if self.access_log:
extra = {
'status': getattr(response, 'status', 0),
}
extra = {"status": getattr(response, "status", 0)}
if isinstance(response, HTTPResponse):
extra['byte'] = len(response.body)
extra["byte"] = len(response.body)
else:
extra['byte'] = -1
extra["byte"] = -1
extra['host'] = 'UNKNOWN'
extra["host"] = "UNKNOWN"
if self.request is not None:
if self.request.ip:
extra['host'] = '{0[0]}:{0[1]}'.format(self.request.ip)
extra["host"] = "{0}:{1}".format(
self.request.ip, self.request.port
)
extra['request'] = '{0} {1}'.format(self.request.method,
self.request.url)
extra["request"] = "{0} {1}".format(
self.request.method, self.request.url
)
else:
extra['request'] = 'nil'
extra["request"] = "nil"
access_logger.info('', extra=extra)
access_logger.info("", extra=extra)
def write_response(self, response):
"""
@@ -335,34 +369,46 @@ class HttpProtocol(asyncio.Protocol):
keep_alive = self.keep_alive
self.transport.write(
response.output(
self.request.version, keep_alive,
self.keep_alive_timeout))
self.request.version, keep_alive, self.keep_alive_timeout
)
)
self.log_response(response)
except AttributeError:
logger.error('Invalid response object for url %s, '
'Expected Type: HTTPResponse, Actual Type: %s',
self.url, type(response))
self.write_error(ServerError('Invalid response type'))
logger.error(
"Invalid response object for url %s, "
"Expected Type: HTTPResponse, Actual Type: %s",
self.url,
type(response),
)
self.write_error(ServerError("Invalid response type"))
except RuntimeError:
if self._debug:
logger.error('Connection lost before response written @ %s',
self.request.ip)
logger.error(
"Connection lost before response written @ %s",
self.request.ip,
)
keep_alive = False
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(
repr(e)))
"Writing response failed, connection closed {}".format(repr(e))
)
finally:
if not keep_alive:
self.transport.close()
self.transport = None
else:
self._keep_alive_timeout_handler = self.loop.call_later(
self.keep_alive_timeout,
self.keep_alive_timeout_callback)
self._last_response_time = current_time
self.keep_alive_timeout, self.keep_alive_timeout_callback
)
self._last_response_time = time()
self.cleanup()
async def drain(self):
await self._not_paused.wait()
def push_data(self, data):
self.transport.write(data)
async def stream_response(self, response):
"""
Streams a response to the client asynchronously. Attaches
@@ -372,35 +418,42 @@ class HttpProtocol(asyncio.Protocol):
if self._response_timeout_handler:
self._response_timeout_handler.cancel()
self._response_timeout_handler = None
try:
keep_alive = self.keep_alive
response.transport = self.transport
response.protocol = self
await response.stream(
self.request.version, keep_alive, self.keep_alive_timeout)
self.request.version, keep_alive, self.keep_alive_timeout
)
self.log_response(response)
except AttributeError:
logger.error('Invalid response object for url %s, '
'Expected Type: HTTPResponse, Actual Type: %s',
self.url, type(response))
self.write_error(ServerError('Invalid response type'))
logger.error(
"Invalid response object for url %s, "
"Expected Type: HTTPResponse, Actual Type: %s",
self.url,
type(response),
)
self.write_error(ServerError("Invalid response type"))
except RuntimeError:
if self._debug:
logger.error('Connection lost before response written @ %s',
self.request.ip)
logger.error(
"Connection lost before response written @ %s",
self.request.ip,
)
keep_alive = False
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(
repr(e)))
"Writing response failed, connection closed {}".format(repr(e))
)
finally:
if not keep_alive:
self.transport.close()
self.transport = None
else:
self._keep_alive_timeout_handler = self.loop.call_later(
self.keep_alive_timeout,
self.keep_alive_timeout_callback)
self._last_response_time = current_time
self.keep_alive_timeout, self.keep_alive_timeout_callback
)
self._last_response_time = time()
self.cleanup()
def write_error(self, exception):
@@ -412,32 +465,39 @@ class HttpProtocol(asyncio.Protocol):
response = None
try:
response = self.error_handler.response(self.request, exception)
version = self.request.version if self.request else '1.1'
version = self.request.version if self.request else "1.1"
self.transport.write(response.output(version))
except RuntimeError:
if self._debug:
logger.error('Connection lost before error written @ %s',
self.request.ip if self.request else 'Unknown')
logger.error(
"Connection lost before error written @ %s",
self.request.ip if self.request else "Unknown",
)
except Exception as e:
self.bail_out(
"Writing error failed, connection closed {}".format(
repr(e)), from_error=True
"Writing error failed, connection closed {}".format(repr(e)),
from_error=True,
)
finally:
if self.parser and (self.keep_alive
or getattr(response, 'status', 0) == 408):
if self.parser and (
self.keep_alive or getattr(response, "status", 0) == 408
):
self.log_response(response)
self.transport.close()
try:
self.transport.close()
except AttributeError:
logger.debug("Connection lost before server could close it.")
def bail_out(self, message, from_error=False):
if from_error or self.transport.is_closing():
logger.error("Transport closed @ %s and exception "
"experienced during error handling",
self.transport.get_extra_info('peername'))
logger.debug('Exception:\n%s', traceback.format_exc())
logger.error(
"Transport closed @ %s and exception "
"experienced during error handling",
self.transport.get_extra_info("peername"),
)
logger.debug("Exception:", exc_info=True)
else:
exception = ServerError(message)
self.write_error(exception)
self.write_error(ServerError(message))
logger.error(message)
def cleanup(self):
@@ -496,16 +556,43 @@ def trigger_events(events, loop):
loop.run_until_complete(result)
def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None, debug=False,
request_timeout=60, response_timeout=60, keep_alive_timeout=5,
ssl=None, sock=None, request_max_size=None, reuse_port=False,
loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False, connections=None,
signal=Signal(), request_class=None, access_log=True,
keep_alive=True, is_request_stream=False, router=None,
websocket_max_size=None, websocket_max_queue=None, state=None,
graceful_shutdown_timeout=15.0):
def serve(
host,
port,
request_handler,
error_handler,
before_start=None,
after_start=None,
before_stop=None,
after_stop=None,
debug=False,
request_timeout=60,
response_timeout=60,
keep_alive_timeout=5,
ssl=None,
sock=None,
request_max_size=None,
reuse_port=False,
loop=None,
protocol=HttpProtocol,
backlog=100,
register_sys_signals=True,
run_multiple=False,
run_async=False,
connections=None,
signal=Signal(),
request_class=None,
access_log=True,
keep_alive=True,
is_request_stream=False,
router=None,
websocket_max_size=None,
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16,
state=None,
graceful_shutdown_timeout=15.0,
):
"""Start asynchronous HTTP Server on an individual process.
:param host: Address to host on
@@ -534,12 +621,25 @@ def serve(host, port, request_handler, error_handler, before_start=None,
:param protocol: subclass of asyncio protocol class
:param request_class: Request class to use
:param access_log: disable/enable access log
:param websocket_max_size: enforces the maximum size for
incoming messages in bytes.
:param websocket_max_queue: sets the maximum length of the queue
that holds incoming messages.
:param websocket_read_limit: sets the high-water limit of the buffer for
incoming bytes, the low-water limit is half
the high-water limit.
:param websocket_write_limit: sets the high-water limit of the buffer for
outgoing bytes, the low-water limit is a
quarter of the high-water limit.
:param is_request_stream: disable/enable Request.stream
:param router: Router object
:param graceful_shutdown_timeout: How long take to Force close non-idle
connection
:return: Nothing
"""
if not run_async:
loop = async_loop.new_event_loop()
# create new event_loop after fork
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if debug:
@@ -564,6 +664,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
router=router,
websocket_max_size=websocket_max_size,
websocket_max_queue=websocket_max_queue,
websocket_read_limit=websocket_read_limit,
websocket_write_limit=websocket_write_limit,
state=state,
debug=debug,
)
@@ -575,7 +677,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
ssl=ssl,
reuse_port=reuse_port,
sock=sock,
backlog=backlog
backlog=backlog,
)
# Instead of pulling time at the end of every request,
@@ -595,17 +697,24 @@ def serve(host, port, request_handler, error_handler, before_start=None,
trigger_events(after_start, loop)
# Ignore SIGINT when run_multiple
if run_multiple:
signal_func(SIGINT, SIG_IGN)
# Register signals for graceful termination
if register_sys_signals:
for _signal in (SIGINT, SIGTERM):
_singals = (SIGTERM,) if run_multiple else (SIGINT, SIGTERM)
for _signal in _singals:
try:
loop.add_signal_handler(_signal, loop.stop)
except NotImplementedError:
logger.warning('Sanic tried to use loop.add_signal_handler '
'but it is not implemented on this platform.')
logger.warning(
"Sanic tried to use loop.add_signal_handler "
"but it is not implemented on this platform."
)
pid = os.getpid()
try:
logger.info('Starting worker [%s]', pid)
logger.info("Starting worker [%s]", pid)
loop.run_forever()
finally:
logger.info("Stopping worker [%s]", pid)
@@ -636,9 +745,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
coros = []
for conn in connections:
if hasattr(conn, "websocket") and conn.websocket:
coros.append(
conn.websocket.close_connection(after_handshake=True)
)
coros.append(conn.websocket.close_connection())
else:
conn.close()
@@ -659,27 +766,29 @@ def serve_multiple(server_settings, workers):
:param stop_event: if provided, is used as a stop signal
:return:
"""
server_settings['reuse_port'] = True
server_settings["reuse_port"] = True
server_settings["run_multiple"] = True
# Handling when custom socket is not provided.
if server_settings.get('sock') is None:
if server_settings.get("sock") is None:
sock = socket()
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind((server_settings['host'], server_settings['port']))
sock.bind((server_settings["host"], server_settings["port"]))
sock.set_inheritable(True)
server_settings['sock'] = sock
server_settings['host'] = None
server_settings['port'] = None
server_settings["sock"] = sock
server_settings["host"] = None
server_settings["port"] = None
def sig_handler(signal, frame):
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
for process in processes:
os.kill(process.pid, SIGINT)
os.kill(process.pid, SIGTERM)
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
processes = []
for _ in range(workers):
process = Process(target=serve, kwargs=server_settings)
process.daemon = True
@@ -692,4 +801,4 @@ def serve_multiple(server_settings, workers):
# the above processes will block this until they're stopped
for process in processes:
process.terminate()
server_settings.get('sock').close()
server_settings.get("sock").close()

View File

@@ -1,7 +1,7 @@
from mimetypes import guess_type
from os import path
from re import sub
from time import strftime, gmtime
from time import gmtime, strftime
from urllib.parse import unquote
from aiofiles.os import stat
@@ -13,13 +13,22 @@ from sanic.exceptions import (
InvalidUsage,
)
from sanic.handlers import ContentRangeHandler
from sanic.response import file, file_stream, HTTPResponse
from sanic.response import HTTPResponse, file, file_stream
def register(app, uri, file_or_directory, pattern,
use_modified_since, use_content_range,
stream_large_files, name='static', host=None,
strict_slashes=None):
def register(
app,
uri,
file_or_directory,
pattern,
use_modified_since,
use_content_range,
stream_large_files,
name="static",
host=None,
strict_slashes=None,
content_type=None,
):
# 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
@@ -41,16 +50,17 @@ def register(app, uri, file_or_directory, pattern,
If this is an integer, this represents the
threshold size to switch to file_stream()
:param name: user defined name used for url_for
:param content_type: user defined content type for header
"""
# If we're not trying to match a file directly,
# serve from the folder
if not path.isfile(file_or_directory):
uri += '<file_uri:' + pattern + '>'
uri += "<file_uri:" + pattern + ">"
async def _handler(request, file_uri=None):
# Using this to determine if the URL is trying to break out of the path
# served. os.path.realpath seems to be very slow
if file_uri and '../' in file_uri:
if file_uri and "../" in file_uri:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python
@@ -58,15 +68,16 @@ def register(app, uri, file_or_directory, pattern,
root_path = file_path = file_or_directory
if file_uri:
file_path = path.join(
file_or_directory, sub('^[/]*', '', file_uri))
file_or_directory, sub("^[/]*", "", file_uri)
)
# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_path))):
raise FileNotFound('File not found',
path=file_or_directory,
relative_url=file_uri)
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)
try:
headers = {}
# Check if the client has been sent this file before
@@ -75,33 +86,35 @@ def register(app, uri, file_or_directory, pattern,
if use_modified_since:
stats = await stat(file_path)
modified_since = strftime(
'%a, %d %b %Y %H:%M:%S GMT', gmtime(stats.st_mtime))
if request.headers.get('If-Modified-Since') == modified_since:
"%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
headers["Last-Modified"] = modified_since
_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':
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']
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')
headers["Content-Type"] = (
content_type or guess_type(file_path)[0] or "text/plain"
)
if request.method == "HEAD":
return HTTPResponse(headers=headers)
else:
if stream_large_files:
if isinstance(stream_large_files, int):
if type(stream_large_files) == int:
threshold = stream_large_files
else:
threshold = 1024 * 1024
@@ -109,19 +122,25 @@ def register(app, uri, file_or_directory, pattern,
if not stats:
stats = await stat(file_path)
if stats.st_size >= threshold:
return await file_stream(file_path, headers=headers,
_range=_range)
return await file_stream(
file_path, headers=headers, _range=_range
)
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)
raise FileNotFound(
"File not found", path=file_or_directory, relative_url=file_uri
)
# special prefix for static files
if not name.startswith('_static_'):
name = '_static_{}'.format(name)
if not name.startswith("_static_"):
name = "_static_{}".format(name)
app.route(uri, methods=['GET', 'HEAD'], name=name, host=host,
strict_slashes=strict_slashes)(_handler)
app.route(
uri,
methods=["GET", "HEAD"],
name=name,
host=host,
strict_slashes=strict_slashes,
)(_handler)

View File

@@ -1,9 +1,11 @@
import traceback
from json import JSONDecodeError
from sanic.exceptions import MethodNotSupported
from sanic.log import logger
from sanic.response import text
HOST = '127.0.0.1'
HOST = "127.0.0.1"
PORT = 42101
@@ -14,61 +16,83 @@ class SanicTestClient:
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
import aiohttp
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")):
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=self.port, uri=uri)
url = "http://{host}:{port}{uri}".format(
host=HOST, port=self.port, uri=uri
)
logger.info(url)
conn = aiohttp.TCPConnector(verify_ssl=False)
conn = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(
cookies=cookies, connector=conn) as session:
async with getattr(
session, method.lower())(url, *args, **kwargs) as response:
cookies=cookies, connector=conn
) as session:
async with getattr(session, method.lower())(
url, *args, **kwargs
) as response:
try:
response.text = await response.text()
except UnicodeDecodeError as e:
except UnicodeDecodeError:
response.text = None
try:
response.json = await response.json()
except (JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError):
except (
JSONDecodeError,
UnicodeDecodeError,
aiohttp.ClientResponseError,
):
response.json = None
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):
self,
method="get",
uri="/",
gather_request=True,
debug=False,
server_kwargs={"auto_reload": False},
*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')
@self.app.exception(MethodNotSupported)
async def error_handler(request, exception):
if request.method in ["HEAD", "PATCH", "PUT", "DELETE"]:
return text(
"", exception.status_code, headers=exception.headers
)
else:
return self.app.error_handler.default(request, exception)
@self.app.listener("after_server_start")
async def _collect_response(sanic, loop):
try:
response = await self._local_request(
method, uri, *request_args,
**request_kwargs)
method, uri, *request_args, **request_kwargs
)
results[-1] = response
except Exception as e:
logger.error(
'Exception:\n{}'.format(traceback.format_exc()))
logger.exception("Exception")
exceptions.append(e)
self.app.stop()
self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
self.app.listeners['after_server_start'].pop()
self.app.listeners["after_server_start"].pop()
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))
@@ -80,31 +104,34 @@ class SanicTestClient:
except BaseException:
raise ValueError(
"Request and response object expected, got ({})".format(
results))
results
)
)
else:
try:
return results[-1]
except BaseException:
raise ValueError(
"Request object expected, got ({})".format(results))
"Request object expected, got ({})".format(results)
)
def get(self, *args, **kwargs):
return self._sanic_endpoint_test('get', *args, **kwargs)
return self._sanic_endpoint_test("get", *args, **kwargs)
def post(self, *args, **kwargs):
return self._sanic_endpoint_test('post', *args, **kwargs)
return self._sanic_endpoint_test("post", *args, **kwargs)
def put(self, *args, **kwargs):
return self._sanic_endpoint_test('put', *args, **kwargs)
return self._sanic_endpoint_test("put", *args, **kwargs)
def delete(self, *args, **kwargs):
return self._sanic_endpoint_test('delete', *args, **kwargs)
return self._sanic_endpoint_test("delete", *args, **kwargs)
def patch(self, *args, **kwargs):
return self._sanic_endpoint_test('patch', *args, **kwargs)
return self._sanic_endpoint_test("patch", *args, **kwargs)
def options(self, *args, **kwargs):
return self._sanic_endpoint_test('options', *args, **kwargs)
return self._sanic_endpoint_test("options", *args, **kwargs)
def head(self, *args, **kwargs):
return self._sanic_endpoint_test('head', *args, **kwargs)
return self._sanic_endpoint_test("head", *args, **kwargs)

View File

@@ -1,5 +1,5 @@
from sanic.exceptions import InvalidUsage
from sanic.constants import HTTP_METHODS
from sanic.exceptions import InvalidUsage
class HTTPMethodView:
@@ -48,6 +48,7 @@ class HTTPMethodView:
"""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)
return self.dispatch_request(*args, **kwargs)
@@ -94,11 +95,13 @@ class CompositionView:
for method in methods:
if method not in HTTP_METHODS:
raise InvalidUsage(
'{} is not a valid HTTP method.'.format(method))
"{} is not a valid HTTP method.".format(method)
)
if method in self.handlers:
raise InvalidUsage(
'Method {} is already registered.'.format(method))
"Method {} is already registered.".format(method)
)
self.handlers[method] = handler
def __call__(self, request, *args, **kwargs):

View File

@@ -1,17 +1,29 @@
from httptools import HttpParserUpgrade
from websockets import ConnectionClosed # noqa
from websockets import InvalidHandshake, WebSocketCommonProtocol, handshake
from sanic.exceptions import InvalidUsage
from sanic.server import HttpProtocol
from httptools import HttpParserUpgrade
from websockets import handshake, WebSocketCommonProtocol, InvalidHandshake
from websockets import ConnectionClosed # noqa
class WebSocketProtocol(HttpProtocol):
def __init__(self, *args, websocket_max_size=None,
websocket_max_queue=None, **kwargs):
def __init__(
self,
*args,
websocket_timeout=10,
websocket_max_size=None,
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16,
**kwargs
):
super().__init__(*args, **kwargs)
self.websocket = None
self.websocket_timeout = websocket_timeout
self.websocket_max_size = websocket_max_size
self.websocket_max_queue = websocket_max_queue
self.websocket_read_limit = websocket_read_limit
self.websocket_write_limit = websocket_write_limit
# timeouts make no sense for websocket routes
def request_timeout_callback(self):
@@ -51,42 +63,41 @@ class WebSocketProtocol(HttpProtocol):
async def websocket_handshake(self, request, subprotocols=None):
# let the websockets package do the handshake with the client
headers = []
def get_header(k):
return request.headers.get(k, '')
def set_header(k, v):
headers.append((k, v))
headers = {}
try:
key = handshake.check_request(get_header)
handshake.build_response(set_header, key)
key = handshake.check_request(request.headers)
handshake.build_response(headers, key)
except InvalidHandshake:
raise InvalidUsage('Invalid websocket request')
raise InvalidUsage("Invalid websocket request")
subprotocol = None
if subprotocols and 'Sec-Websocket-Protocol' in request.headers:
if subprotocols and "Sec-Websocket-Protocol" in request.headers:
# select a subprotocol
client_subprotocols = [p.strip() for p in request.headers[
'Sec-Websocket-Protocol'].split(',')]
client_subprotocols = [
p.strip()
for p in request.headers["Sec-Websocket-Protocol"].split(",")
]
for p in client_subprotocols:
if p in subprotocols:
subprotocol = p
set_header('Sec-Websocket-Protocol', subprotocol)
headers["Sec-Websocket-Protocol"] = subprotocol
break
# write the 101 response back to the client
rv = b'HTTP/1.1 101 Switching Protocols\r\n'
for k, v in headers:
rv += k.encode('utf-8') + b': ' + v.encode('utf-8') + b'\r\n'
rv += b'\r\n'
rv = b"HTTP/1.1 101 Switching Protocols\r\n"
for k, v in headers.items():
rv += k.encode("utf-8") + b": " + v.encode("utf-8") + b"\r\n"
rv += b"\r\n"
request.transport.write(rv)
# hook up the websocket protocol
self.websocket = WebSocketCommonProtocol(
timeout=self.websocket_timeout,
max_size=self.websocket_max_size,
max_queue=self.websocket_max_queue
max_queue=self.websocket_max_queue,
read_limit=self.websocket_read_limit,
write_limit=self.websocket_write_limit,
)
self.websocket.subprotocol = subprotocol
self.websocket.connection_made(request.transport)

View File

@@ -1,10 +1,16 @@
import os
import sys
import signal
import asyncio
import logging
import os
import signal
import sys
import traceback
import gunicorn.workers.base as base
from sanic.server import HttpProtocol, Signal, serve, trigger_events
from sanic.websocket import WebSocketProtocol
try:
import ssl
except ImportError:
@@ -12,13 +18,10 @@ except ImportError:
try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
import gunicorn.workers.base as base
from sanic.server import trigger_events, serve, HttpProtocol, Signal
from sanic.websocket import WebSocketProtocol
class GunicornWorker(base.Worker):
@@ -50,36 +53,43 @@ class GunicornWorker(base.Worker):
def run(self):
is_debug = self.log.loglevel == logging.DEBUG
protocol = (
self.websocket_protocol if self.app.callable.websocket_enabled
else self.http_protocol)
self.websocket_protocol
if self.app.callable.websocket_enabled
else self.http_protocol
)
self._server_settings = self.app.callable._helper(
loop=self.loop,
debug=is_debug,
protocol=protocol,
ssl=self.ssl_context,
run_async=True)
self._server_settings['signal'] = self.signal
self._server_settings.pop('sock')
trigger_events(self._server_settings.get('before_start', []),
self.loop)
self._server_settings['before_start'] = ()
run_async=True,
)
self._server_settings["signal"] = self.signal
self._server_settings.pop("sock")
trigger_events(
self._server_settings.get("before_start", []), self.loop
)
self._server_settings["before_start"] = ()
self._runner = asyncio.ensure_future(self._run(), loop=self.loop)
try:
self.loop.run_until_complete(self._runner)
self.app.callable.is_running = True
trigger_events(self._server_settings.get('after_start', []),
self.loop)
trigger_events(
self._server_settings.get("after_start", []), self.loop
)
self.loop.run_until_complete(self._check_alive())
trigger_events(self._server_settings.get('before_stop', []),
self.loop)
trigger_events(
self._server_settings.get("before_stop", []), self.loop
)
self.loop.run_until_complete(self.close())
except BaseException:
traceback.print_exc()
finally:
try:
trigger_events(self._server_settings.get('after_stop', []),
self.loop)
trigger_events(
self._server_settings.get("after_stop", []), self.loop
)
except BaseException:
traceback.print_exc()
finally:
@@ -90,8 +100,11 @@ class GunicornWorker(base.Worker):
async def close(self):
if self.servers:
# stop accepting connections
self.log.info("Stopping server: %s, connections: %s",
self.pid, len(self.connections))
self.log.info(
"Stopping server: %s, connections: %s",
self.pid,
len(self.connections),
)
for server in self.servers:
server.close()
await server.wait_closed()
@@ -105,8 +118,9 @@ class GunicornWorker(base.Worker):
# gracefully shutdown timeout
start_shutdown = 0
graceful_shutdown_timeout = self.cfg.graceful_timeout
while self.connections and \
(start_shutdown < graceful_shutdown_timeout):
while self.connections and (
start_shutdown < graceful_shutdown_timeout
):
await asyncio.sleep(0.1)
start_shutdown = start_shutdown + 0.1
@@ -115,9 +129,7 @@ class GunicornWorker(base.Worker):
coros = []
for conn in self.connections:
if hasattr(conn, "websocket") and conn.websocket:
coros.append(
conn.websocket.close_connection(after_handshake=False)
)
coros.append(conn.websocket.close_connection())
else:
conn.close()
_shutdown = asyncio.gather(*coros, loop=self.loop)
@@ -148,8 +160,9 @@ class GunicornWorker(base.Worker):
)
if self.max_requests and req_count > self.max_requests:
self.alive = False
self.log.info("Max requests exceeded, shutting down: %s",
self)
self.log.info(
"Max requests exceeded, shutting down: %s", self
)
elif pid == os.getpid() and self.ppid != os.getppid():
self.alive = False
self.log.info("Parent changed, shutting down: %s", self)
@@ -175,23 +188,29 @@ class GunicornWorker(base.Worker):
def init_signals(self):
# Set up signals through the event loop API.
self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit,
signal.SIGQUIT, None)
self.loop.add_signal_handler(
signal.SIGQUIT, self.handle_quit, signal.SIGQUIT, None
)
self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit,
signal.SIGTERM, None)
self.loop.add_signal_handler(
signal.SIGTERM, self.handle_exit, signal.SIGTERM, None
)
self.loop.add_signal_handler(signal.SIGINT, self.handle_quit,
signal.SIGINT, None)
self.loop.add_signal_handler(
signal.SIGINT, self.handle_quit, signal.SIGINT, None
)
self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch,
signal.SIGWINCH, None)
self.loop.add_signal_handler(
signal.SIGWINCH, self.handle_winch, signal.SIGWINCH, None
)
self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1,
signal.SIGUSR1, None)
self.loop.add_signal_handler(
signal.SIGUSR1, self.handle_usr1, signal.SIGUSR1, None
)
self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort,
signal.SIGABRT, None)
self.loop.add_signal_handler(
signal.SIGABRT, self.handle_abort, signal.SIGABRT, None
)
# Don't let SIGTERM and SIGUSR1 disturb active requests
# by interrupting system calls

17
setup.cfg Normal file
View File

@@ -0,0 +1,17 @@
[flake8]
# https://github.com/ambv/black#slices
# https://github.com/ambv/black#line-breaks--binary-operators
ignore = E203, W503
[isort]
atomic=true
default_section = THIRDPARTY
include_trailing_comma = true
known_first_party = sanic
known_third_party = pytest
line_length = 79
lines_after_imports = 2
lines_between_types = 1
multi_line_output = 3
not_skip = __init__.py

View File

@@ -21,7 +21,7 @@ def open_local(paths, mode='r', encoding='utf8'):
with open_local(['sanic', '__init__.py'], encoding='latin1') as fp:
try:
version = re.findall(r"^__version__ = '([^']+)'\r?$",
version = re.findall(r"^__version__ = \"([^']+)\"\r?$",
fp.read(), re.M)[0]
except IndexError:
raise RuntimeError('Unable to determine version.')
@@ -43,38 +43,35 @@ setup_kwargs = {
'packages': ['sanic'],
'platforms': 'any',
'classifiers': [
'Development Status :: 2 - Pre-Alpha',
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
],
}
ujson = 'ujson>=1.35'
uvloop = 'uvloop>=0.5.3'
env_dependency = '; sys_platform != "win32" and implementation_name == "cpython"'
ujson = 'ujson>=1.35' + env_dependency
uvloop = 'uvloop>=0.5.3' + env_dependency
requirements = [
'httptools>=0.0.9',
'httptools>=0.0.10',
uvloop,
ujson,
'aiofiles>=0.3.0',
'websockets>=4.0',
'websockets>=6.0,<7.0',
'multidict>=4.0,<5.0',
]
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
print("Installing without uJSON")
requirements.remove(ujson)
# 'nt' means windows OS
if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
print("Installing without uvLoop")
requirements.remove(uvloop)
try:
setup_kwargs['install_requires'] = requirements
setup(**setup_kwargs)
except DistutilsPlatformError as exception:
requirements.remove(ujson)
requirements.remove(uvloop)
print("Installing without uJSON or uvLoop")
setup_kwargs['install_requires'] = requirements
setup(**setup_kwargs)
setup_kwargs['install_requires'] = requirements
setup(**setup_kwargs)

12
tests/conftest.py Normal file
View File

@@ -0,0 +1,12 @@
import sys
import pytest
from sanic import Sanic
if sys.platform in ['win32', 'cygwin']:
collect_ignore = ["test_worker.py"]
@pytest.fixture
def app(request):
return Sanic(request.node.name)

26
tests/static/test.html Normal file
View File

@@ -0,0 +1,26 @@
<html>
<body>
<pre>
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀
</pre>
</body>
</html>

150
tests/test_app.py Normal file
View File

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

View File

@@ -1,10 +1,9 @@
import asyncio
from sanic import Sanic
def test_bad_request_response():
app = Sanic('test_bad_request_response')
def test_bad_request_response(app):
lines = []
@app.listener('after_server_start')
async def _request(sanic, loop):
connect = asyncio.open_connection('127.0.0.1', 42101)

View File

@@ -1,21 +1,43 @@
import asyncio
import inspect
import os
import pytest
from sanic import Sanic
from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.response import json, text
from sanic.exceptions import NotFound, ServerError, InvalidUsage
from sanic.constants import HTTP_METHODS
from sanic.exceptions import NotFound, ServerError, InvalidUsage
from sanic.request import Request
from sanic.response import text, json
from sanic.views import CompositionView
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
@pytest.fixture(scope='module')
def static_file_directory():
"""The static directory to serve"""
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, 'static')
return static_directory
def get_file_path(static_file_directory, file_name):
return os.path.join(static_file_directory, file_name)
def get_file_content(static_file_directory, file_name):
"""The content of the static file to check"""
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
return file.read()
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_routes_get(method):
app = Sanic('test_shorhand_routes_get')
def test_versioned_routes_get(app, method):
bp = Blueprint('test_text')
method = method.lower()
@@ -27,7 +49,7 @@ def test_versioned_routes_get(method):
return text('OK')
else:
print(func)
raise
raise Exception("{} is not callable".format(func))
app.blueprint(bp)
@@ -37,8 +59,7 @@ def test_versioned_routes_get(method):
assert response.status == 200
def test_bp():
app = Sanic('test_text')
def test_bp(app):
bp = Blueprint('test_text')
@bp.route('/')
@@ -51,23 +72,23 @@ def test_bp():
assert response.text == 'Hello'
def test_bp_strict_slash():
app = Sanic('test_route_strict_slash')
def test_bp_strict_slash(app):
bp = Blueprint('test_text')
@bp.get('/get', strict_slashes=True)
def handler(request):
def get_handler(request):
return text('OK')
@bp.post('/post/', strict_slashes=True)
def handler(request):
def post_handler(request):
return text('OK')
app.blueprint(bp)
request, response = app.test_client.get('/get')
assert response.text == 'OK'
assert response.json == None
assert response.json is None
request, response = app.test_client.get('/get/')
assert response.status == 404
@@ -78,16 +99,16 @@ def test_bp_strict_slash():
request, response = app.test_client.post('/post')
assert response.status == 404
def test_bp_strict_slash_default_value():
app = Sanic('test_route_strict_slash')
def test_bp_strict_slash_default_value(app):
bp = Blueprint('test_text', strict_slashes=True)
@bp.get('/get')
def handler(request):
def get_handler(request):
return text('OK')
@bp.post('/post/')
def handler(request):
def post_handler(request):
return text('OK')
app.blueprint(bp)
@@ -98,16 +119,16 @@ def test_bp_strict_slash_default_value():
request, response = app.test_client.post('/post')
assert response.status == 404
def test_bp_strict_slash_without_passing_default_value():
app = Sanic('test_route_strict_slash')
def test_bp_strict_slash_without_passing_default_value(app):
bp = Blueprint('test_text')
@bp.get('/get')
def handler(request):
def get_handler(request):
return text('OK')
@bp.post('/post/')
def handler(request):
def post_handler(request):
return text('OK')
app.blueprint(bp)
@@ -118,16 +139,16 @@ def test_bp_strict_slash_without_passing_default_value():
request, response = app.test_client.post('/post')
assert response.text == 'OK'
def test_bp_strict_slash_default_value_can_be_overwritten():
app = Sanic('test_route_strict_slash')
def test_bp_strict_slash_default_value_can_be_overwritten(app):
bp = Blueprint('test_text', strict_slashes=True)
@bp.get('/get', strict_slashes=False)
def handler(request):
def get_handler(request):
return text('OK')
@bp.post('/post/', strict_slashes=False)
def handler(request):
def post_handler(request):
return text('OK')
app.blueprint(bp)
@@ -138,8 +159,8 @@ def test_bp_strict_slash_default_value_can_be_overwritten():
request, response = app.test_client.post('/post')
assert response.text == 'OK'
def test_bp_with_url_prefix():
app = Sanic('test_text')
def test_bp_with_url_prefix(app):
bp = Blueprint('test_text', url_prefix='/test1')
@bp.route('/')
@@ -152,8 +173,7 @@ def test_bp_with_url_prefix():
assert response.text == 'Hello'
def test_several_bp_with_url_prefix():
app = Sanic('test_text')
def test_several_bp_with_url_prefix(app):
bp = Blueprint('test_text', url_prefix='/test1')
bp2 = Blueprint('test_text2', url_prefix='/test2')
@@ -173,16 +193,16 @@ def test_several_bp_with_url_prefix():
request, response = app.test_client.get('/test2/')
assert response.text == 'Hello2'
def test_bp_with_host():
app = Sanic('test_bp_host')
def test_bp_with_host(app):
bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com")
@bp.route('/')
def handler(request):
def handler1(request):
return text('Hello')
@bp.route('/', host="sub.example.com")
def handler(request):
def handler2(request):
return text('Hello subdomain!')
app.blueprint(bp)
@@ -200,8 +220,7 @@ def test_bp_with_host():
assert response.text == 'Hello subdomain!'
def test_several_bp_with_host():
app = Sanic('test_text')
def test_several_bp_with_host(app):
bp = Blueprint('test_text',
url_prefix='/test',
host="example.com")
@@ -214,14 +233,13 @@ def test_several_bp_with_host():
return text('Hello')
@bp2.route('/')
def handler2(request):
def handler1(request):
return text('Hello2')
@bp2.route('/other/')
def handler2(request):
return text('Hello3')
app.blueprint(bp)
app.blueprint(bp2)
@@ -244,8 +262,8 @@ def test_several_bp_with_host():
headers=headers)
assert response.text == 'Hello3'
def test_bp_middleware():
app = Sanic('test_middleware')
def test_bp_middleware(app):
blueprint = Blueprint('test_middleware')
@blueprint.middleware('response')
@@ -263,8 +281,8 @@ def test_bp_middleware():
assert response.status == 200
assert response.text == 'OK'
def test_bp_exception_handler():
app = Sanic('test_middleware')
def test_bp_exception_handler(app):
blueprint = Blueprint('test_middleware')
@blueprint.route('/1')
@@ -288,7 +306,6 @@ def test_bp_exception_handler():
request, response = app.test_client.get('/1')
assert response.status == 400
request, response = app.test_client.get('/2')
assert response.status == 200
assert response.text == 'OK'
@@ -296,8 +313,8 @@ def test_bp_exception_handler():
request, response = app.test_client.get('/3')
assert response.status == 200
def test_bp_listeners():
app = Sanic('test_middleware')
def test_bp_listeners(app):
blueprint = Blueprint('test_middleware')
order = []
@@ -330,14 +347,14 @@ def test_bp_listeners():
request, response = app.test_client.get('/')
assert order == [1,2,3,4,5,6]
assert order == [1, 2, 3, 4, 5, 6]
def test_bp_static():
def test_bp_static(app):
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static')
blueprint = Blueprint('test_static')
blueprint.static('/testing.file', current_file)
@@ -348,8 +365,30 @@ def test_bp_static():
assert response.status == 200
assert response.body == current_file_contents
def test_bp_shorthand():
app = Sanic('test_shorhand_routes')
@pytest.mark.parametrize('file_name', ['test.html'])
def test_bp_static_content_type(app, file_name):
# This is done here, since no other test loads a file here
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
static_directory = os.path.join(current_directory, 'static')
blueprint = Blueprint('test_static')
blueprint.static(
'/testing.file',
get_file_path(static_directory, file_name),
content_type='text/html; charset=utf-8'
)
app.blueprint(blueprint)
request, response = app.test_client.get('/testing.file')
assert response.status == 200
assert response.body == get_file_content(static_directory, file_name)
assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
def test_bp_shorthand(app):
blueprint = Blueprint('test_shorhand_routes')
ev = asyncio.Event()
@@ -359,37 +398,37 @@ def test_bp_shorthand():
return text('OK')
@blueprint.put('/put')
def handler(request):
def put_handler(request):
assert request.stream is None
return text('OK')
@blueprint.post('/post')
def handler(request):
def post_handler(request):
assert request.stream is None
return text('OK')
@blueprint.head('/head')
def handler(request):
def head_handler(request):
assert request.stream is None
return text('OK')
@blueprint.options('/options')
def handler(request):
def options_handler(request):
assert request.stream is None
return text('OK')
@blueprint.patch('/patch')
def handler(request):
def patch_handler(request):
assert request.stream is None
return text('OK')
@blueprint.delete('/delete')
def handler(request):
def delete_handler(request):
assert request.stream is None
return text('OK')
@blueprint.websocket('/ws')
async def handler(request, ws):
@blueprint.websocket('/ws/', strict_slashes=True)
async def websocket_handler(request, ws):
assert request.stream is None
ev.set()
@@ -439,10 +478,229 @@ def test_bp_shorthand():
request, response = app.test_client.get('/delete')
assert response.status == 405
request, response = app.test_client.get('/ws', headers={
request, response = app.test_client.get('/ws/', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'})
assert response.status == 101
assert ev.is_set()
def test_bp_group(app):
deep_0 = Blueprint('deep_0', url_prefix='/deep')
deep_1 = Blueprint('deep_1', url_prefix='/deep1')
@deep_0.route('/')
def handler(request):
return text('D0_OK')
@deep_1.route('/bottom')
def bottom_handler(request):
return text('D1B_OK')
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
@mid_1.route('/')
def handler1(request):
return text('M1_OK')
top = Blueprint.group(mid_0, mid_1)
app.blueprint(top)
@app.route('/')
def handler2(request):
return text('TOP_OK')
request, response = app.test_client.get('/')
assert response.text == 'TOP_OK'
request, response = app.test_client.get('/mid1')
assert response.text == 'M1_OK'
request, response = app.test_client.get('/mid/deep')
assert response.text == 'D0_OK'
request, response = app.test_client.get('/mid/deep1/bottom')
assert response.text == 'D1B_OK'
def test_bp_group_with_default_url_prefix(app):
from sanic.response import json
bp_resources = Blueprint('bp_resources')
@bp_resources.get('/')
def list_resources_handler(request):
resource = {}
return json([resource])
bp_resource = Blueprint('bp_resource', url_prefix='/<resource_id>')
@bp_resource.get('/')
def get_resource_hander(request, resource_id):
resource = {'resource_id': resource_id}
return json(resource)
bp_resources_group = Blueprint.group(bp_resources, bp_resource,
url_prefix='/resources')
bp_api_v1 = Blueprint('bp_api_v1')
@bp_api_v1.get('/info')
def api_v1_info(request):
return text('api_version: v1')
bp_api_v1_group = Blueprint.group(bp_api_v1, bp_resources_group,
url_prefix='/v1')
bp_api_group = Blueprint.group(bp_api_v1_group, url_prefix='/api')
app.blueprint(bp_api_group)
request, response = app.test_client.get('/api/v1/info')
assert response.text == 'api_version: v1'
request, response = app.test_client.get('/api/v1/resources')
assert response.json == [{}]
from uuid import uuid4
resource_id = str(uuid4())
request, response = app.test_client.get(
'/api/v1/resources/{0}'.format(resource_id))
assert response.json == {'resource_id': resource_id}
def test_blueprint_middleware_with_args(app: Sanic):
bp = Blueprint(name="with_args_bp", url_prefix="/wa")
@bp.middleware
def middleware_with_no_tag(request: Request):
if request.headers.get("content-type") == "application/json":
request.headers["accepts"] = "plain/text"
else:
request.headers["accepts"] = "application/json"
@bp.route("/")
def default_route(request):
if request.headers.get("accepts") == "application/json":
return json({"test": "value"})
else:
return text("value")
app.blueprint(bp)
_, response = app.test_client.get("/wa", headers={"content-type": "application/json"})
assert response.text == "value"
_, response = app.test_client.get("/wa", headers={"content-type": "plain/text"})
assert response.json.get("test") == "value"
d = {}
@pytest.mark.parametrize('file_name',
['test.file'])
def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
bp = Blueprint(name="static", url_prefix="/static", strict_slashes=False)
bp.static(
"/test.file/",
get_file_path(static_file_directory, file_name),
name="static.testing",
strict_slashes=True)
app.blueprint(bp)
uri = app.url_for('static', name='static.testing')
assert uri == "/static/test.file"
_, response = app.test_client.get("/static/test.file")
assert response.status == 404
_, response = app.test_client.get("/static/test.file/")
assert response.status == 200
def test_route_handler_add(app: Sanic):
view = CompositionView()
async def get_handler(request):
return json({
"response": "OK"
})
view.add(["GET"], get_handler, stream=False)
async def default_handler(request):
return text("OK")
bp = Blueprint(name="handler", url_prefix="/handler")
bp.add_route(
default_handler,
uri="/default/",
strict_slashes=True)
bp.add_route(view, uri="/view", name="test")
app.blueprint(bp)
_, response = app.test_client.get("/handler/default/")
assert response.text == "OK"
_, response = app.test_client.get("/handler/view")
assert response.json["response"] == "OK"
def test_websocket_route(app: Sanic):
event = asyncio.Event()
async def websocket_handler(request, ws):
assert ws.subprotocol is None
event.set()
bp = Blueprint(name="handler", url_prefix="/ws")
bp.add_websocket_route(websocket_handler, "/test", name="test")
app.blueprint(bp)
_, response = app.test_client.get("/ws/test", headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'
})
assert response.status == 101
assert event.is_set()
def test_duplicate_blueprint(app):
bp_name = 'bp'
bp = Blueprint(bp_name)
bp1 = Blueprint(bp_name)
app.blueprint(bp)
with pytest.raises(AssertionError) as excinfo:
app.blueprint(bp1)
assert str(excinfo.value) == (
'A blueprint with the name "{}" is already registered. '
'Blueprint names must be unique.'
).format(bp_name)
@pytest.mark.parametrize('debug', [True, False, None])
def test_register_blueprint(app, debug):
bp = Blueprint('bp')
app.debug = debug
with pytest.warns(DeprecationWarning) as record:
app.register_blueprint(bp)
assert record[0].message.args[0] == (
"Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method"
" instead"
)

View File

@@ -1,12 +1,22 @@
from os import environ
from pathlib import Path
from contextlib import contextmanager
from tempfile import TemporaryDirectory
from textwrap import dedent
import pytest
from tempfile import NamedTemporaryFile
from sanic import Sanic
from sanic.exceptions import PyFileError
def test_load_from_object():
app = Sanic('test_load_from_object')
@contextmanager
def temp_path():
""" a simple cross platform replacement for NamedTemporaryFile """
with TemporaryDirectory() as td:
yield Path(td, 'file')
def test_load_from_object(app):
class Config:
not_for_config = 'should not be used'
CONFIG_VALUE = 'should be used'
@@ -16,36 +26,52 @@ def test_load_from_object():
assert app.config.CONFIG_VALUE == 'should be used'
assert 'not_for_config' not in app.config
def test_auto_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic()
assert app.config.TEST_ANSWER == 42
del environ["SANIC_TEST_ANSWER"]
def test_dont_load_env():
environ["SANIC_TEST_ANSWER"] = "42"
app = Sanic(load_env=False)
assert getattr(app.config, 'TEST_ANSWER', None) == None
assert getattr(app.config, 'TEST_ANSWER', None) is None
del environ["SANIC_TEST_ANSWER"]
def test_load_env_prefix():
environ["MYAPP_TEST_ANSWER"] = "42"
app = Sanic(load_env='MYAPP_')
assert app.config.TEST_ANSWER == 42
del environ["MYAPP_TEST_ANSWER"]
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)
def test_load_env_prefix_float_values():
environ["MYAPP_TEST_ROI"] = "2.3"
app = Sanic(load_env="MYAPP_")
assert app.config.TEST_ROI == 2.3
del environ["MYAPP_TEST_ROI"]
def test_load_env_prefix_string_value():
environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken"
app = Sanic(load_env="MYAPP_")
assert app.config.TEST_TOKEN == "somerandomtesttoken"
del environ["MYAPP_TEST_TOKEN"]
def test_load_from_file(app):
config = dedent("""
VALUE = 'some value'
condition = 1 == 1
if condition:
CONDITIONAL = 'should be set'
""")
with temp_path() as config_path:
config_path.write_text(config)
app.config.from_pyfile(str(config_path))
assert 'VALUE' in app.config
assert app.config.VALUE == 'some value'
assert 'CONDITIONAL' in app.config
@@ -53,33 +79,41 @@ if condition:
assert 'condition' not in app.config
def test_load_from_missing_file():
app = Sanic('test_load_from_missing_file')
def test_load_from_missing_file(app):
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
def test_load_from_envvar(app):
config = "VALUE = 'some value'"
with temp_path() as config_path:
config_path.write_text(config)
environ['APP_CONFIG'] = str(config_path)
app.config.from_envvar('APP_CONFIG')
assert 'VALUE' in app.config
assert app.config.VALUE == 'some value'
def test_load_from_missing_envvar():
app = Sanic('test_load_from_missing_envvar')
with pytest.raises(RuntimeError):
def test_load_from_missing_envvar(app):
with pytest.raises(RuntimeError) as e:
app.config.from_envvar('non-existent variable')
assert str(e.value) == ("The environment variable 'non-existent "
"variable' is not set and thus configuration "
"could not be loaded.")
def test_overwrite_exisiting_config():
app = Sanic('test_overwrite_exisiting_config')
def test_load_config_from_file_invalid_syntax(app):
config = "VALUE = some value"
with temp_path() as config_path:
config_path.write_text(config)
with pytest.raises(PyFileError):
app.config.from_pyfile(config_path)
def test_overwrite_exisiting_config(app):
app.config.DEFAULT = 1
class Config:
DEFAULT = 2
@@ -87,7 +121,17 @@ def test_overwrite_exisiting_config():
assert app.config.DEFAULT == 2
def test_missing_config():
app = Sanic('test_missing_config')
with pytest.raises(AttributeError):
def test_overwrite_exisiting_config_ignore_lowercase(app):
app.config.default = 1
class Config:
default = 2
app.config.from_object(Config)
assert app.config.default == 1
def test_missing_config(app):
with pytest.raises(AttributeError) as e:
app.config.NON_EXISTENT
assert str(e.value) == ("Config has no 'NON_EXISTENT'")

View File

@@ -1,16 +1,14 @@
from datetime import datetime, timedelta
from http.cookies import SimpleCookie
from sanic import Sanic
from sanic.response import json, text
from sanic.response import text
import pytest
from sanic.cookies import Cookie
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
def test_cookies():
app = Sanic('test_text')
def test_cookies(app):
@app.route('/')
def handler(request):
@@ -25,12 +23,12 @@ def test_cookies():
assert response.text == 'Cookies are: working!'
assert response_cookies['right_back'].value == 'at you'
@pytest.mark.parametrize("httponly,expected", [
(False, False),
(True, True),
])
def test_false_cookies_encoded(httponly, expected):
app = Sanic('test_text')
def test_false_cookies_encoded(app, httponly, expected):
@app.route('/')
def handler(request):
@@ -48,8 +46,7 @@ def test_false_cookies_encoded(httponly, expected):
(False, False),
(True, True),
])
def test_false_cookies(httponly, expected):
app = Sanic('test_text')
def test_false_cookies(app, httponly, expected):
@app.route('/')
def handler(request):
@@ -64,8 +61,8 @@ def test_false_cookies(httponly, expected):
assert ('HttpOnly' in response_cookies['right_back'].output()) == expected
def test_http2_cookies():
app = Sanic('test_http2_cookies')
def test_http2_cookies(app):
@app.route('/')
async def handler(request):
@@ -77,15 +74,16 @@ def test_http2_cookies():
assert response.text == 'Cookies are: working!'
def test_cookie_options():
app = Sanic('test_text')
def test_cookie_options(app):
@app.route('/')
def handler(request):
response = text("OK")
response.cookies['test'] = 'at you'
response.cookies['test']['httponly'] = True
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
response.cookies['test']['expires'] = (datetime.now() +
timedelta(seconds=10))
return response
request, response = app.test_client.get('/')
@@ -93,10 +91,10 @@ def test_cookie_options():
response_cookies.load(response.headers.get('Set-Cookie', {}))
assert response_cookies['test'].value == 'at you'
assert response_cookies['test']['httponly'] == True
assert response_cookies['test']['httponly'] is True
def test_cookie_deletion():
app = Sanic('test_text')
def test_cookie_deletion(app):
@app.route('/')
def handler(request):
@@ -112,4 +110,82 @@ def test_cookie_deletion():
assert int(response_cookies['i_want_to_die']['max-age']) == 0
with pytest.raises(KeyError):
hold_my_beer = response.cookies['i_never_existed']
response.cookies['i_never_existed']
def test_cookie_reserved_cookie():
with pytest.raises(expected_exception=KeyError) as e:
Cookie("domain", "testdomain.com")
assert e.message == "Cookie name is a reserved word"
def test_cookie_illegal_key_format():
with pytest.raises(expected_exception=KeyError) as e:
Cookie("testå", "test")
assert e.message == "Cookie key contains illegal characters"
def test_cookie_set_unknown_property():
c = Cookie("test_cookie", "value")
with pytest.raises(expected_exception=KeyError) as e:
c["invalid"] = "value"
assert e.message == "Unknown cookie property"
def test_cookie_set_same_key(app):
cookies = {'test': 'wait'}
@app.get('/')
def handler(request):
response = text('pass')
response.cookies['test'] = 'modified'
response.cookies['test'] = 'pass'
return response
request, response = app.test_client.get('/', cookies=cookies)
assert response.status == 200
assert response.cookies['test'].value == 'pass'
@pytest.mark.parametrize('max_age', ['0', 30, '30'])
def test_cookie_max_age(app, max_age):
cookies = {'test': 'wait'}
@app.get('/')
def handler(request):
response = text('pass')
response.cookies['test'] = 'pass'
response.cookies['test']['max-age'] = max_age
return response
request, response = app.test_client.get('/', cookies=cookies)
assert response.status == 200
assert response.cookies['test'].value == 'pass'
assert response.cookies['test']['max-age'] == str(max_age)
@pytest.mark.parametrize('expires', [
datetime.now() + timedelta(seconds=60),
'Fri, 21-Dec-2018 15:30:00 GMT'
])
def test_cookie_expires(app, expires):
cookies = {'test': 'wait'}
@app.get('/')
def handler(request):
response = text('pass')
response.cookies['test'] = 'pass'
response.cookies['test']['expires'] = expires
return response
request, response = app.test_client.get('/', cookies=cookies)
assert response.status == 200
assert response.cookies['test'].value == 'pass'
if isinstance(expires, datetime):
expires = expires.strftime("%a, %d-%b-%Y %T GMT")
assert response.cookies['test']['expires'] == expires

View File

@@ -1,17 +1,16 @@
from sanic import Sanic
from sanic.response import text
from threading import Event
import asyncio
from queue import Queue
def test_create_task():
def test_create_task(app):
e = Event()
async def coro():
await asyncio.sleep(0.05)
e.set()
app = Sanic('test_create_task')
app.add_task(coro)
@app.route('/early')
@@ -28,3 +27,19 @@ def test_create_task():
request, response = app.test_client.get('/late')
assert response.body == b'True'
def test_create_task_with_app_arg(app):
q = Queue()
@app.route('/')
def not_set(request):
return "hello"
async def coro(app):
q.put(app.name)
app.add_task(coro)
request, response = app.test_client.get('/')
assert q.get() == 'test_create_task_with_app_arg'

View File

@@ -1,9 +1,6 @@
from sanic import Sanic
from sanic.server import HttpProtocol
from sanic.response import text
app = Sanic('test_custom_porotocol')
class CustomHttpProtocol(HttpProtocol):
@@ -16,12 +13,12 @@ class CustomHttpProtocol(HttpProtocol):
self.transport.close()
@app.route('/1')
async def handler_1(request):
return 'OK'
def test_use_custom_protocol(app):
@app.route('/1')
async def handler_1(request):
return 'OK'
def test_use_custom_protocol():
server_kwargs = {
'protocol': CustomHttpProtocol
}

View File

@@ -0,0 +1,53 @@
from io import BytesIO
from sanic import Sanic
from sanic.request import Request
from sanic.response import json_dumps, text
class CustomRequest(Request):
__slots__ = ("body_buffer",)
def body_init(self):
self.body_buffer = BytesIO()
def body_push(self, data):
self.body_buffer.write(data)
def body_finish(self):
self.body = self.body_buffer.getvalue()
self.body_buffer.close()
def test_custom_request():
app = Sanic(request_class=CustomRequest)
@app.route("/post", methods=["POST"])
async def post_handler(request):
return text("OK")
@app.route("/get")
async def get_handler(request):
return text("OK")
payload = {"test": "OK"}
headers = {"content-type": "application/json"}
request, response = app.test_client.post(
"/post", data=json_dumps(payload), headers=headers
)
assert isinstance(request.body_buffer, BytesIO)
assert request.body_buffer.closed
assert request.body == b'{"test":"OK"}'
assert request.json.get("test") == "OK"
assert response.text == "OK"
assert response.status == 200
request, response = app.test_client.get("/get")
assert isinstance(request.body_buffer, BytesIO)
assert request.body_buffer.closed
assert request.body == b""
assert response.text == "OK"
assert response.status == 200

View File

@@ -1,4 +1,3 @@
from sanic import Sanic
from sanic.response import text
from sanic.router import RouteExists
import pytest
@@ -10,8 +9,7 @@ import pytest
("put", "text", "OK2 test"),
("delete", "status", 405),
])
def test_overload_dynamic_routes(method, attr, expected):
app = Sanic('test_dynamic_route')
def test_overload_dynamic_routes(app, method, attr, expected):
@app.route('/overload/<param>', methods=['GET'])
async def handler1(request, param):
@@ -25,8 +23,7 @@ def test_overload_dynamic_routes(method, attr, expected):
assert getattr(response, attr) == expected
def test_overload_dynamic_routes_exist():
app = Sanic('test_dynamic_route')
def test_overload_dynamic_routes_exist(app):
@app.route('/overload/<param>', methods=['GET'])
async def handler1(request, param):

View File

@@ -66,6 +66,10 @@ def exception_app():
abort(500)
return text("OK")
@app.route('/abort/message')
def handler_abort_message(request):
abort(500, message='Abort')
@app.route('/divide_by_zero')
def handle_unhandled_exception(request):
1 / 0
@@ -81,8 +85,7 @@ def exception_app():
return app
def test_catch_exception_list():
app = Sanic('exception_list')
def test_catch_exception_list(app):
@app.exception([SanicExceptionTestException, NotFound])
def exception_list(request, exception):
@@ -138,7 +141,7 @@ def test_unauthorized_exception(exception_app):
request, response = exception_app.test_client.get('/401/basic')
assert response.status == 401
assert response.headers.get('WWW-Authenticate') is not None
assert response.headers.get('WWW-Authenticate') == "Basic realm='Sanic'"
assert response.headers.get('WWW-Authenticate') == 'Basic realm="Sanic"'
request, response = exception_app.test_client.get('/401/digest')
assert response.status == 401
@@ -146,10 +149,10 @@ def test_unauthorized_exception(exception_app):
auth_header = response.headers.get('WWW-Authenticate')
assert auth_header is not None
assert auth_header.startswith('Digest')
assert "qop='auth, auth-int'" in auth_header
assert "algorithm='MD5'" in auth_header
assert "nonce='abcdef'" in auth_header
assert "opaque='zyxwvu'" in auth_header
assert 'qop="auth, auth-int"' in auth_header
assert 'algorithm="MD5"' in auth_header
assert 'nonce="abcdef"' in auth_header
assert 'opaque="zyxwvu"' in auth_header
request, response = exception_app.test_client.get('/401/bearer')
assert response.status == 401
@@ -202,3 +205,7 @@ def test_abort(exception_app):
request, response = exception_app.test_client.get('/abort')
assert response.status == 500
request, response = exception_app.test_client.get('/abort/message')
assert response.status == 500
assert response.text == 'Error: Abort'

View File

@@ -131,7 +131,7 @@ def test_exception_handler_lookup():
try:
ModuleNotFoundError
except:
except Exception:
class ModuleNotFoundError(ImportError):
pass

74
tests/test_helpers.py Normal file
View File

@@ -0,0 +1,74 @@
from sanic import helpers
def test_has_message_body():
tests = (
(100, False),
(102, False),
(204, False),
(200, True),
(304, False),
(400, True),
)
for status_code, expected in tests:
assert helpers.has_message_body(status_code) is expected
def test_is_entity_header():
tests = (
("allow", True),
("extension-header", True),
("", False),
("test", False),
)
for header, expected in tests:
assert helpers.is_entity_header(header) is expected
def test_is_hop_by_hop_header():
tests = (
("connection", True),
("upgrade", True),
("", False),
("test", False),
)
for header, expected in tests:
assert helpers.is_hop_by_hop_header(header) is expected
def test_remove_entity_headers():
tests = (
(
{},
{}
),
(
{
"Allow": "GET, POST, HEAD",
},
{}
),
(
{
"Content-Type": "application/json",
"Expires": "Wed, 21 Oct 2015 07:28:00 GMT",
"Foo": "Bar"
},
{
"Expires": "Wed, 21 Oct 2015 07:28:00 GMT",
"Foo": "Bar"
},
),
(
{
"Allow": "GET, POST, HEAD",
"Content-Location": "/test"
},
{
"Content-Location": "/test"
},
),
)
for header, expected in tests:
assert helpers.remove_entity_headers(header) == expected

View File

@@ -15,10 +15,9 @@ class ReuseableTCPConnector(TCPConnector):
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
self.old_proto = None
@asyncio.coroutine
def connect(self, req):
new_conn = yield from super(ReuseableTCPConnector, self)\
.connect(req)
async def connect(self, req, *args, **kwargs):
new_conn = await super(ReuseableTCPConnector, self)\
.connect(req, *args, **kwargs)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
raise RuntimeError(
@@ -68,7 +67,7 @@ class ReuseableSanicTestClient(SanicTestClient):
import traceback
traceback.print_tb(e2.__traceback__)
exceptions.append(e2)
#Don't stop here! self.app.stop()
# Don't stop here! self.app.stop()
if self._server is not None:
_server = self._server
@@ -88,7 +87,7 @@ class ReuseableSanicTestClient(SanicTestClient):
raise e1
self._server = _server = http_server
server.trigger_events(
self.app.listeners['after_server_start'], loop)
self.app.listeners['after_server_start'], loop)
self.app.listeners['after_server_start'].pop()
if do_kill_server:
@@ -110,14 +109,14 @@ class ReuseableSanicTestClient(SanicTestClient):
try:
request, response = results
return request, response
except:
except Exception:
raise ValueError(
"Request and response object expected, got ({})".format(
results))
else:
try:
return results[-1]
except:
except Exception:
raise ValueError(
"Request object expected, got ({})".format(results))
@@ -133,7 +132,7 @@ class ReuseableSanicTestClient(SanicTestClient):
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=PORT, uri=uri)
host=HOST, port=self.port, uri=uri)
do_kill_session = kwargs.pop('end_session', False)
if self._session:
session = self._session
@@ -141,10 +140,11 @@ class ReuseableSanicTestClient(SanicTestClient):
if self._tcp_connector:
conn = self._tcp_connector
else:
conn = ReuseableTCPConnector(verify_ssl=False,
loop=self._loop,
keepalive_timeout=
request_keepalive)
conn = ReuseableTCPConnector(
ssl=False,
loop=self._loop,
keepalive_timeout=request_keepalive
)
self._tcp_connector = conn
session = aiohttp.ClientSession(cookies=cookies,
connector=conn,
@@ -167,7 +167,7 @@ class ReuseableSanicTestClient(SanicTestClient):
response.body = await response.read()
if do_kill_session:
session.close()
await session.close()
self._session = None
return response
@@ -266,4 +266,3 @@ def test_keep_alive_server_timeout():
assert isinstance(exception, ValueError)
assert "Connection reset" in exception.args[0] or \
"got a new connection" in exception.args[0]

View File

@@ -11,6 +11,7 @@ import sanic
from sanic.response import text
from sanic.log import LOGGING_CONFIG_DEFAULTS
from sanic import Sanic
from sanic.log import logger
logging_format = '''module: %(module)s; \
@@ -23,7 +24,7 @@ def reset_logging():
reload(logging)
def test_log():
def test_log(app):
log_stream = StringIO()
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
@@ -33,7 +34,6 @@ def test_log():
stream=log_stream
)
log = logging.getLogger()
app = Sanic('test_logging')
rand_string = str(uuid.uuid4())
@app.route('/')
@@ -47,10 +47,10 @@ def test_log():
def test_logging_defaults():
reset_logging()
# reset_logging()
app = Sanic("test_logging")
for fmt in [h.formatter for h in logging.getLogger('root').handlers]:
for fmt in [h.formatter for h in logging.getLogger('sanic.root').handlers]:
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format']
for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
@@ -61,7 +61,7 @@ def test_logging_defaults():
def test_logging_pass_customer_logconfig():
reset_logging()
# reset_logging()
modified_config = LOGGING_CONFIG_DEFAULTS
modified_config['formatters']['generic']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s'
@@ -69,7 +69,7 @@ def test_logging_pass_customer_logconfig():
app = Sanic("test_logging", log_config=modified_config)
for fmt in [h.formatter for h in logging.getLogger('root').handlers]:
for fmt in [h.formatter for h in logging.getLogger('sanic.root').handlers]:
assert fmt._fmt == modified_config['formatters']['generic']['format']
for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
@@ -80,11 +80,10 @@ def test_logging_pass_customer_logconfig():
@pytest.mark.parametrize('debug', (True, False, ))
def test_log_connection_lost(debug, monkeypatch):
def test_log_connection_lost(app, debug, monkeypatch):
""" Should not log Connection lost exception on non debug """
app = Sanic('connection_lost')
stream = StringIO()
root = logging.getLogger('root')
root = logging.getLogger('sanic.root')
root.addHandler(logging.StreamHandler(stream))
monkeypatch.setattr(sanic.server, 'logger', root)
@@ -101,7 +100,36 @@ def test_log_connection_lost(debug, monkeypatch):
log = stream.getvalue()
if debug:
assert log.startswith(
'Connection lost before response written @')
assert 'Connection lost before response written @' in log
else:
assert 'Connection lost before response written @' not in log
def test_logger(caplog):
rand_string = str(uuid.uuid4())
app = Sanic()
@app.get('/')
def log_info(request):
logger.info(rand_string)
return text('hello')
with caplog.at_level(logging.INFO):
request, response = app.test_client.get('/')
assert caplog.record_tuples[0] == ('sanic.root', logging.INFO, 'Goin\' Fast @ http://127.0.0.1:42101')
assert caplog.record_tuples[1] == ('sanic.root', logging.INFO, 'http://127.0.0.1:42101/')
assert caplog.record_tuples[2] == ('sanic.root', logging.INFO, rand_string)
assert caplog.record_tuples[-1] == ('sanic.root', logging.INFO, 'Server Stopped')
def test_logging_modified_root_logger_config():
# reset_logging()
modified_config = LOGGING_CONFIG_DEFAULTS
modified_config['loggers']['sanic.root']['level'] = 'DEBUG'
app = Sanic("test_logging", log_config=modified_config)
assert logging.getLogger('sanic.root').getEffectiveLevel() == logging.DEBUG

View File

@@ -1,25 +1,23 @@
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.exceptions import NotFound
import logging
from asyncio import CancelledError
from sanic.exceptions import NotFound
from sanic.request import Request
from sanic.response import HTTPResponse, text
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
def test_middleware_request():
app = Sanic('test_middleware_request')
def test_middleware_request(app):
results = []
@app.middleware
async def handler(request):
async def handler1(request):
results.append(request)
@app.route('/')
async def handler(request):
async def handler2(request):
return text('OK')
request, response = app.test_client.get('/')
@@ -28,13 +26,11 @@ def test_middleware_request():
assert type(results[0]) is Request
def test_middleware_response():
app = Sanic('test_middleware_response')
def test_middleware_response(app):
results = []
@app.middleware('request')
async def process_response(request):
async def process_request(request):
results.append(request)
@app.middleware('response')
@@ -54,8 +50,7 @@ def test_middleware_response():
assert isinstance(results[2], HTTPResponse)
def test_middleware_response_exception():
app = Sanic('test_middleware_response_exception')
def test_middleware_response_exception(app):
result = {'status_code': None}
@app.middleware('response')
@@ -75,8 +70,53 @@ def test_middleware_response_exception():
assert response.text == 'OK'
assert result['status_code'] == 404
def test_middleware_override_request():
app = Sanic('test_middleware_override_request')
def test_middleware_response_raise_cancelled_error(app, caplog):
@app.middleware('response')
async def process_response(request, response):
raise CancelledError('CancelledError at response middleware')
@app.get('/')
def handler(request):
return text('OK')
caplog.set_level(logging.ERROR, logger="sanic.root")
with caplog.at_level(logging.ERROR):
reqrequest, response = app.test_client.get('/')
assert response.status == 503
assert caplog.record_tuples[0] == (
'sanic.root',
logging.ERROR,
'Exception occurred while handling uri: \'http://127.0.0.1:42101/\''
)
def test_middleware_response_raise_exception(app, caplog):
@app.middleware('response')
async def process_response(request, response):
raise Exception('Exception at response middleware')
caplog.set_level(logging.ERROR, logger="sanic.root")
with caplog.at_level(logging.ERROR):
reqrequest, response = app.test_client.get('/')
assert response.status == 404
assert caplog.record_tuples[0] == (
'sanic.root',
logging.ERROR,
'Exception occurred while handling uri: \'http://127.0.0.1:42101/\''
)
assert caplog.record_tuples[1] == (
'sanic.error',
logging.ERROR,
'Exception occurred in one of response middleware handlers'
)
def test_middleware_override_request(app):
@app.middleware
async def halt_request(request):
@@ -92,8 +132,7 @@ def test_middleware_override_request():
assert response.text == 'OK'
def test_middleware_override_response():
app = Sanic('test_middleware_override_response')
def test_middleware_override_response(app):
@app.middleware('response')
async def process_response(request, response):
@@ -109,10 +148,7 @@ def test_middleware_override_response():
assert response.text == 'OK'
def test_middleware_order():
app = Sanic('test_middleware_order')
def test_middleware_order(app):
order = []
@app.middleware('request')
@@ -146,4 +182,4 @@ def test_middleware_order():
request, response = app.test_client.get('/')
assert response.status == 200
assert order == [1,2,3,4,5,6]
assert order == [1, 2, 3, 4, 5, 6]

View File

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

View File

@@ -4,7 +4,6 @@
import asyncio
import pytest
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import text
from sanic.exceptions import URLBuildError
@@ -16,8 +15,7 @@ from sanic.constants import HTTP_METHODS
# ------------------------------------------------------------ #
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_named_routes_get(method):
app = Sanic('test_shorhand_routes_get')
def test_versioned_named_routes_get(app, method):
bp = Blueprint('test_bp', url_prefix='/bp')
method = method.lower()
@@ -57,8 +55,7 @@ def test_versioned_named_routes_get(method):
app.url_for('handler')
def test_shorthand_default_routes_get():
app = Sanic('test_shorhand_routes_get')
def test_shorthand_default_routes_get(app):
@app.get('/get')
def handler(request):
@@ -68,8 +65,7 @@ def test_shorthand_default_routes_get():
assert app.url_for('handler') == '/get'
def test_shorthand_named_routes_get():
app = Sanic('test_shorhand_routes_get')
def test_shorthand_named_routes_get(app):
bp = Blueprint('test_bp', url_prefix='/bp')
@app.get('/get', name='route_get')
@@ -93,8 +89,7 @@ def test_shorthand_named_routes_get():
app.url_for('test_bp.handler2')
def test_shorthand_named_routes_post():
app = Sanic('test_shorhand_routes_post')
def test_shorthand_named_routes_post(app):
@app.post('/post', name='route_name')
def handler(request):
@@ -106,8 +101,7 @@ def test_shorthand_named_routes_post():
app.url_for('handler')
def test_shorthand_named_routes_put():
app = Sanic('test_shorhand_routes_put')
def test_shorthand_named_routes_put(app):
@app.put('/put', name='route_put')
def handler(request):
@@ -121,8 +115,7 @@ def test_shorthand_named_routes_put():
app.url_for('handler')
def test_shorthand_named_routes_delete():
app = Sanic('test_shorhand_routes_delete')
def test_shorthand_named_routes_delete(app):
@app.delete('/delete', name='route_delete')
def handler(request):
@@ -136,8 +129,7 @@ def test_shorthand_named_routes_delete():
app.url_for('handler')
def test_shorthand_named_routes_patch():
app = Sanic('test_shorhand_routes_patch')
def test_shorthand_named_routes_patch(app):
@app.patch('/patch', name='route_patch')
def handler(request):
@@ -151,8 +143,7 @@ def test_shorthand_named_routes_patch():
app.url_for('handler')
def test_shorthand_named_routes_head():
app = Sanic('test_shorhand_routes_head')
def test_shorthand_named_routes_head(app):
@app.head('/head', name='route_head')
def handler(request):
@@ -166,8 +157,7 @@ def test_shorthand_named_routes_head():
app.url_for('handler')
def test_shorthand_named_routes_options():
app = Sanic('test_shorhand_routes_options')
def test_shorthand_named_routes_options(app):
@app.options('/options', name='route_options')
def handler(request):
@@ -181,8 +171,7 @@ def test_shorthand_named_routes_options():
app.url_for('handler')
def test_named_static_routes():
app = Sanic('test_dynamic_route')
def test_named_static_routes(app):
@app.route('/test', name='route_test')
async def handler1(request):
@@ -205,9 +194,7 @@ def test_named_static_routes():
app.url_for('handler2')
def test_named_dynamic_route():
app = Sanic('test_dynamic_route')
def test_named_dynamic_route(app):
results = []
@app.route('/folder/<name>', name='route_dynamic')
@@ -221,8 +208,7 @@ def test_named_dynamic_route():
app.url_for('handler')
def test_dynamic_named_route_regex():
app = Sanic('test_dynamic_route_regex')
def test_dynamic_named_route_regex(app):
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>', name='route_re')
async def handler(request, folder_id):
@@ -235,8 +221,7 @@ def test_dynamic_named_route_regex():
app.url_for('handler')
def test_dynamic_named_route_path():
app = Sanic('test_dynamic_route_path')
def test_dynamic_named_route_path(app):
@app.route('/<path:path>/info', name='route_dynamic_path')
async def handler(request, path):
@@ -249,8 +234,7 @@ def test_dynamic_named_route_path():
app.url_for('handler')
def test_dynamic_named_route_unhashable():
app = Sanic('test_dynamic_route_unhashable')
def test_dynamic_named_route_unhashable(app):
@app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/',
name='route_unhashable')
@@ -265,8 +249,7 @@ def test_dynamic_named_route_unhashable():
app.url_for('handler')
def test_websocket_named_route():
app = Sanic('test_websocket_route')
def test_websocket_named_route(app):
ev = asyncio.Event()
@app.websocket('/ws', name='route_ws')
@@ -280,8 +263,7 @@ def test_websocket_named_route():
app.url_for('handler')
def test_websocket_named_route_with_subprotocols():
app = Sanic('test_websocket_route')
def test_websocket_named_route_with_subprotocols(app):
results = []
@app.websocket('/ws', subprotocols=['foo', 'bar'], name='route_ws')
@@ -294,8 +276,7 @@ def test_websocket_named_route_with_subprotocols():
app.url_for('handler')
def test_static_add_named_route():
app = Sanic('test_static_add_route')
def test_static_add_named_route(app):
async def handler1(request):
return text('OK1')
@@ -319,9 +300,7 @@ def test_static_add_named_route():
app.url_for('handler2')
def test_dynamic_add_named_route():
app = Sanic('test_dynamic_add_route')
def test_dynamic_add_named_route(app):
results = []
async def handler(request, name):
@@ -335,8 +314,7 @@ def test_dynamic_add_named_route():
app.url_for('handler')
def test_dynamic_add_named_route_unhashable():
app = Sanic('test_dynamic_add_route_unhashable')
def test_dynamic_add_named_route_unhashable(app):
async def handler(request, unhashable):
return text('OK')
@@ -351,15 +329,14 @@ def test_dynamic_add_named_route_unhashable():
app.url_for('handler')
def test_overload_routes():
app = Sanic('test_dynamic_route')
def test_overload_routes(app):
@app.route('/overload', methods=['GET'], name='route_first')
async def handler1(request):
return text('OK1')
@app.route('/overload', methods=['POST', 'PUT'], name='route_second')
async def handler1(request):
async def handler2(request):
return text('OK2')
request, response = app.test_client.get(app.url_for('route_first'))

View File

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

View File

@@ -1,12 +1,11 @@
import pytest
from urllib.parse import quote
from sanic import Sanic
from sanic.response import text, redirect
@pytest.fixture
def redirect_app():
app = Sanic('test_redirection')
def redirect_app(app):
@app.route('/redirect_init')
async def redirect_init(request):
@@ -21,17 +20,21 @@ def redirect_app():
return text('OK')
@app.route('/1')
def handler(request):
def handler1(request):
return redirect('/2')
@app.route('/2')
def handler(request):
def handler2(request):
return redirect('/3')
@app.route('/3')
def handler(request):
def handler3(request):
return text('OK')
@app.route('/redirect_with_header_injection')
async def redirect_with_header_injection(request):
return redirect("/unsafe\ntest-header: test-value\n\ntest-body")
return app
@@ -92,3 +95,38 @@ def test_chained_redirect(redirect_app):
assert response.url.endswith('/3')
except AttributeError:
assert response.url.path.endswith('/3')
def test_redirect_with_header_injection(redirect_app):
"""
Test redirection to a URL with header and body injections.
"""
request, response = redirect_app.test_client.get(
"/redirect_with_header_injection",
allow_redirects=False)
assert response.status == 302
assert "test-header" not in response.headers
assert not response.text.startswith('test-body')
@pytest.mark.parametrize("test_str", ["sanic-test", "sanictest", "sanic test"])
async def test_redirect_with_params(app, test_client, test_str):
@app.route("/api/v1/test/<test>/")
async def init_handler(request, test):
assert test == test_str
return redirect("/api/v2/test/{}/".format(quote(test)))
@app.route("/api/v2/test/<test>/")
async def target_handler(request, test):
assert test == test_str
return text("OK")
test_cli = await test_client(app)
response = await test_cli.get("/api/v1/test/{}/".format(quote(test_str)))
assert response.status == 200
txt = await response.text()
assert txt == "OK"

View File

@@ -0,0 +1,72 @@
import asyncio
import contextlib
from sanic.response import text, stream
async def test_request_cancel_when_connection_lost(loop, app, test_client):
app.still_serving_cancelled_request = False
@app.get('/')
async def handler(request):
await asyncio.sleep(1.0)
# at this point client is already disconnected
app.still_serving_cancelled_request = True
return text('OK')
test_cli = await test_client(app)
# schedule client call
task = loop.create_task(test_cli.get('/'))
loop.call_later(0.01, task)
await asyncio.sleep(0.5)
# cancelling request and closing connection after 0.5 sec
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
# Wait for server and check if it's still serving the cancelled request
await asyncio.sleep(1.0)
assert app.still_serving_cancelled_request is False
async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
app.still_serving_cancelled_request = False
@app.post('/post/<id>', stream=True)
async def post(request, id):
assert isinstance(request.stream, asyncio.Queue)
async def streaming(response):
while True:
body = await request.stream.get()
if body is None:
break
await response.write(body.decode('utf-8'))
await asyncio.sleep(1.0)
# at this point client is already disconnected
app.still_serving_cancelled_request = True
return stream(streaming)
test_cli = await test_client(app)
# schedule client call
task = loop.create_task(test_cli.post('/post/1'))
loop.call_later(0.01, task)
await asyncio.sleep(0.5)
# cancelling request and closing connection after 0.5 sec
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
# Wait for server and check if it's still serving the cancelled request
await asyncio.sleep(1.0)
assert app.still_serving_cancelled_request is False

View File

@@ -1,6 +1,5 @@
import random
from sanic import Sanic
from sanic.response import json
try:
@@ -9,8 +8,7 @@ except ImportError:
from json import loads
def test_storage():
app = Sanic('test_text')
def test_storage(app):
@app.middleware('request')
def store(request):
@@ -20,7 +18,10 @@ 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 = app.test_client.get('/')
@@ -29,8 +30,7 @@ def test_storage():
assert response_json.get('sidekick') is None
def test_app_injection():
app = Sanic('test_app_injection')
def test_app_injection(app):
expected = random.choice(range(0, 100))
@app.listener('after_server_start')

View File

@@ -1,5 +1,4 @@
import asyncio
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.views import CompositionView
from sanic.views import HTTPMethodView
@@ -9,11 +8,9 @@ from sanic.response import stream, text
data = "abc" * 100000
def test_request_stream_method_view():
def test_request_stream_method_view(app):
'''for self.is_request_stream = True'''
app = Sanic('test_request_stream_method_view')
class SimpleView(HTTPMethodView):
def get(self, request):
@@ -44,11 +41,9 @@ def test_request_stream_method_view():
assert response.text == data
def test_request_stream_app():
def test_request_stream_app(app):
'''for self.is_request_stream = True and decorators'''
app = Sanic('test_request_stream_app')
@app.get('/get')
async def get(request):
assert request.stream is None
@@ -83,7 +78,7 @@ def test_request_stream_app():
body = await request.stream.get()
if body is None:
break
response.write(body.decode('utf-8'))
await response.write(body.decode('utf-8'))
return stream(streaming)
@app.put('/_put')
@@ -100,7 +95,7 @@ def test_request_stream_app():
body = await request.stream.get()
if body is None:
break
response.write(body.decode('utf-8'))
await response.write(body.decode('utf-8'))
return stream(streaming)
@app.patch('/_patch')
@@ -117,7 +112,7 @@ def test_request_stream_app():
body = await request.stream.get()
if body is None:
break
response.write(body.decode('utf-8'))
await response.write(body.decode('utf-8'))
return stream(streaming)
assert app.is_request_stream is True
@@ -163,11 +158,9 @@ def test_request_stream_app():
assert response.text == data
def test_request_stream_handle_exception():
def test_request_stream_handle_exception(app):
'''for handling exceptions properly'''
app = Sanic('test_request_stream_exception')
@app.post('/post/<id>', stream=True)
async def post(request, id):
assert isinstance(request.stream, asyncio.Queue)
@@ -177,7 +170,7 @@ def test_request_stream_handle_exception():
body = await request.stream.get()
if body is None:
break
response.write(body.decode('utf-8'))
await response.write(body.decode('utf-8'))
return stream(streaming)
# 404
@@ -188,13 +181,12 @@ def test_request_stream_handle_exception():
# 405
request, response = app.test_client.get('/post/random_id', data=data)
assert response.status == 405
assert response.text == 'Error: Method GET not allowed for URL /post/random_id'
assert response.text == 'Error: Method GET not allowed for URL' \
' /post/random_id'
def test_request_stream_blueprint():
def test_request_stream_blueprint(app):
'''for self.is_request_stream = True'''
app = Sanic('test_request_stream_blueprint')
bp = Blueprint('test_blueprint_request_stream_blueprint')
@app.get('/get')
@@ -231,7 +223,7 @@ def test_request_stream_blueprint():
body = await request.stream.get()
if body is None:
break
response.write(body.decode('utf-8'))
await response.write(body.decode('utf-8'))
return stream(streaming)
@bp.put('/_put')
@@ -248,7 +240,7 @@ def test_request_stream_blueprint():
body = await request.stream.get()
if body is None:
break
response.write(body.decode('utf-8'))
await response.write(body.decode('utf-8'))
return stream(streaming)
@bp.patch('/_patch')
@@ -265,7 +257,7 @@ def test_request_stream_blueprint():
body = await request.stream.get()
if body is None:
break
response.write(body.decode('utf-8'))
await response.write(body.decode('utf-8'))
return stream(streaming)
app.blueprint(bp)
@@ -313,11 +305,9 @@ def test_request_stream_blueprint():
assert response.text == data
def test_request_stream_composition_view():
def test_request_stream_composition_view(app):
'''for self.is_request_stream = True'''
app = Sanic('test_request_stream__composition_view')
def get_handler(request):
assert request.stream is None
return text('OK')
@@ -348,11 +338,9 @@ def test_request_stream_composition_view():
assert response.text == data
def test_request_stream():
def test_request_stream(app):
'''test for complex application'''
bp = Blueprint('test_blueprint_request_stream')
app = Sanic('test_request_stream')
class SimpleView(HTTPMethodView):
@@ -380,7 +368,7 @@ def test_request_stream():
body = await request.stream.get()
if body is None:
break
response.write(body.decode('utf-8'))
await response.write(body.decode('utf-8'))
return stream(streaming)
@app.get('/get')

View File

@@ -6,7 +6,26 @@ from sanic.response import text
from sanic.config import Config
import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT
from sanic.testing import SanicTestClient, HOST
try:
try:
# direct use
import packaging
version = packaging.version
except (ImportError, AttributeError):
# setuptools v39.0 and above.
try:
from setuptools.extern import packaging
except ImportError:
# Before setuptools v39.0
from pkg_resources.extern import packaging
version = packaging.version
except ImportError:
raise RuntimeError("The 'packaging' library is missing.")
aiohttp_version = version.parse(aiohttp.__version__)
class DelayableTCPConnector(TCPConnector):
@@ -28,19 +47,20 @@ class DelayableTCPConnector(TCPConnector):
acting_as = self._acting_as
return getattr(acting_as, item)
@asyncio.coroutine
def start(self, connection, read_until_eof=False):
async def start(self, connection, read_until_eof=False):
if self.send_task is None:
raise RuntimeError("do a send() before you do a start()")
resp = yield from self.send_task
resp = await self.send_task
self.send_task = None
self.resp = resp
self._acting_as = self.resp
self.orig_start = getattr(resp, 'start')
try:
ret = yield from self.orig_start(connection,
read_until_eof)
if aiohttp_version >= version.parse("3.3.0"):
ret = await self.orig_start(connection)
else:
ret = await self.orig_start(connection, read_until_eof)
except Exception as e:
raise e
return ret
@@ -51,28 +71,47 @@ class DelayableTCPConnector(TCPConnector):
if self.send_task is not None:
self.send_task.cancel()
@asyncio.coroutine
def delayed_send(self, *args, **kwargs):
async def delayed_send(self, *args, **kwargs):
req = self.req
if self.delay and self.delay > 0:
#sync_sleep(self.delay)
_ = yield from asyncio.sleep(self.delay)
# sync_sleep(self.delay)
await asyncio.sleep(self.delay)
t = req.loop.time()
print("sending at {}".format(t), flush=True)
conn = next(iter(args)) # first arg is connection
try:
delayed_resp = self.orig_send(*args, **kwargs)
except Exception as e:
return aiohttp.ClientResponse(req.method, req.url)
return delayed_resp
next(iter(args)) # first arg is connection
def send(self, *args, **kwargs):
try:
return await self.orig_send(*args, **kwargs)
except Exception as e:
if aiohttp_version < version.parse("3.1.0"):
return aiohttp.ClientResponse(req.method, req.url)
kw = dict(
writer=None,
continue100=None,
timer=None,
request_info=None,
traces=[],
loop=req.loop,
session=None
)
if aiohttp_version < version.parse("3.3.0"):
kw['auto_decompress'] = None
return aiohttp.ClientResponse(req.method, req.url, **kw)
def _send(self, *args, **kwargs):
gen = self.delayed_send(*args, **kwargs)
task = self.req.loop.create_task(gen)
self.send_task = task
self._acting_as = task
return self
if aiohttp_version >= version.parse("3.1.0"):
# aiohttp changed the request.send method to async
async def send(self, *args, **kwargs):
return self._send(*args, **kwargs)
else:
send = _send
def __init__(self, *args, **kwargs):
_post_connect_delay = kwargs.pop('post_connect_delay', 0)
_pre_request_delay = kwargs.pop('pre_request_delay', 0)
@@ -80,14 +119,14 @@ class DelayableTCPConnector(TCPConnector):
self._post_connect_delay = _post_connect_delay
self._pre_request_delay = _pre_request_delay
@asyncio.coroutine
def connect(self, req):
async def connect(self, req, *args, **kwargs):
d_req = DelayableTCPConnector.\
RequestContextManager(req, self._pre_request_delay)
conn = yield from super(DelayableTCPConnector, self).connect(req)
conn = await super(DelayableTCPConnector, self).\
connect(req, *args, **kwargs)
if self._post_connect_delay and self._post_connect_delay > 0:
_ = yield from asyncio.sleep(self._post_connect_delay,
loop=self._loop)
await asyncio.sleep(self._post_connect_delay,
loop=self._loop)
req.send = d_req.send
t = req.loop.time()
print("Connected at {}".format(t), flush=True)
@@ -108,9 +147,9 @@ class DelayableSanicTestClient(SanicTestClient):
url = uri
else:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=PORT, uri=uri)
host=HOST, port=self.port, uri=uri)
conn = DelayableTCPConnector(pre_request_delay=self._request_delay,
verify_ssl=False, loop=self._loop)
ssl=False, loop=self._loop)
async with aiohttp.ClientSession(cookies=cookies, connector=conn,
loop=self._loop) as session:
# Insert a delay after creating the connection
@@ -134,7 +173,7 @@ class DelayableSanicTestClient(SanicTestClient):
return response
Config.REQUEST_TIMEOUT = 2
Config.REQUEST_TIMEOUT = 0.6
request_timeout_default_app = Sanic('test_request_timeout_default')
request_no_timeout_app = Sanic('test_request_no_timeout')
@@ -149,15 +188,36 @@ async def handler2(request):
return text('OK')
@request_timeout_default_app.websocket('/ws1')
async def ws_handler1(request, ws):
await ws.send('OK')
def test_default_server_error_request_timeout():
client = DelayableSanicTestClient(request_timeout_default_app, None, 3)
client = DelayableSanicTestClient(request_timeout_default_app, None, 2)
request, response = client.get('/1')
assert response.status == 408
assert response.text == 'Error: Request Timeout'
def test_default_server_error_request_dont_timeout():
client = DelayableSanicTestClient(request_no_timeout_app, None, 1)
client = DelayableSanicTestClient(request_no_timeout_app, None, 0.2)
request, response = client.get('/1')
assert response.status == 200
assert response.text == 'OK'
def test_default_server_error_websocket_request_timeout():
headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'
}
client = DelayableSanicTestClient(request_timeout_default_app, None, 2)
request, response = client.get('/ws1', headers=headers)
assert response.status == 408
assert response.text == 'Error: Request Timeout'

View File

@@ -1,23 +1,22 @@
from json import loads as json_loads, dumps as json_dumps
from urllib.parse import urlparse
import logging
import os
import ssl
from json import dumps as json_dumps
from json import loads as json_loads
from urllib.parse import urlparse
import pytest
from sanic import Sanic
from sanic.exceptions import ServerError
from sanic.response import json, text
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE
from sanic.response import json, text
from sanic.testing import HOST, PORT
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
def test_sync():
app = Sanic('test_text')
def test_sync(app):
@app.route('/')
def handler(request):
@@ -27,8 +26,8 @@ def test_sync():
assert response.text == 'Hello'
def test_remote_address():
app = Sanic('test_text')
def test_remote_address(app):
@app.route('/')
def handler(request):
@@ -38,8 +37,8 @@ def test_remote_address():
assert response.text == '127.0.0.1'
def test_text():
app = Sanic('test_text')
def test_text(app):
@app.route('/')
async def handler(request):
@@ -50,8 +49,7 @@ def test_text():
assert response.text == 'Hello'
def test_headers():
app = Sanic('test_text')
def test_headers(app):
@app.route('/')
async def handler(request):
@@ -63,8 +61,7 @@ def test_headers():
assert response.headers.get('spam') == 'great'
def test_non_str_headers():
app = Sanic('test_text')
def test_non_str_headers(app):
@app.route('/')
async def handler(request):
@@ -75,8 +72,8 @@ def test_non_str_headers():
assert response.headers.get('answer') == '42'
def test_invalid_response():
app = Sanic('test_invalid_response')
def test_invalid_response(app):
@app.exception(ServerError)
def handler_exception(request, exception):
@@ -91,8 +88,7 @@ def test_invalid_response():
assert response.text == "Internal Server Error."
def test_json():
app = Sanic('test_json')
def test_json(app):
@app.route('/')
async def handler(request):
@@ -102,22 +98,22 @@ def test_json():
results = json_loads(response.text)
assert results.get('test') == True
assert results.get('test') is True
def test_empty_json():
app = Sanic('test_json')
def test_empty_json(app):
@app.route('/')
async def handler(request):
assert request.json == None
assert request.json is None
return json(request.json)
request, response = app.test_client.get('/')
assert response.status == 200
assert response.text == 'null'
def test_invalid_json():
app = Sanic('test_json')
def test_invalid_json(app):
@app.route('/')
async def handler(request):
@@ -129,8 +125,7 @@ def test_invalid_json():
assert response.status == 400
def test_query_string():
app = Sanic('test_query_string')
def test_query_string(app):
@app.route('/')
async def handler(request):
@@ -143,8 +138,7 @@ def test_query_string():
assert request.args.get('test2') == 'false'
def test_uri_template():
app = Sanic('test_uri_template')
def test_uri_template(app):
@app.route('/foo/<id:int>/bar/<name:[A-z]+>')
async def handler(request):
@@ -154,8 +148,7 @@ def test_uri_template():
assert request.uri_template == '/foo/<id:int>/bar/<name:[A-z]+>'
def test_token():
app = Sanic('test_post_token')
def test_token(app):
@app.route('/')
async def handler(request):
@@ -202,8 +195,7 @@ def test_token():
assert request.token is None
def test_content_type():
app = Sanic('test_content_type')
def test_content_type(app):
@app.route('/')
async def handler(request):
@@ -221,8 +213,7 @@ def test_content_type():
assert response.text == 'application/json'
def test_remote_addr():
app = Sanic('test_content_type')
def test_remote_addr(app):
@app.route('/')
async def handler(request):
@@ -247,8 +238,7 @@ def test_remote_addr():
assert response.text == '127.0.0.1'
def test_match_info():
app = Sanic('test_match_info')
def test_match_info(app):
@app.route('/api/v1/user/<user_id>/')
async def handler(request, user_id):
@@ -264,8 +254,7 @@ def test_match_info():
# POST
# ------------------------------------------------------------ #
def test_post_json():
app = Sanic('test_post_json')
def test_post_json(app):
@app.route('/', methods=['POST'])
async def handler(request):
@@ -278,11 +267,11 @@ def test_post_json():
'/', data=json_dumps(payload), headers=headers)
assert request.json.get('test') == 'OK'
assert request.json.get('test') == 'OK' # for request.parsed_json
assert response.text == 'OK'
def test_post_form_urlencoded():
app = Sanic('test_post_form_urlencoded')
def test_post_form_urlencoded(app):
@app.route('/', methods=['POST'])
async def handler(request):
@@ -291,26 +280,27 @@ def test_post_form_urlencoded():
payload = 'test=OK'
headers = {'content-type': 'application/x-www-form-urlencoded'}
request, response = app.test_client.post('/', data=payload, headers=headers)
request, response = app.test_client.post('/', data=payload,
headers=headers)
assert request.form.get('test') == 'OK'
assert request.form.get('test') == 'OK' # For request.parsed_form
@pytest.mark.parametrize(
'payload', [
'------sanic\r\n' \
'Content-Disposition: form-data; name="test"\r\n' \
'\r\n' \
'OK\r\n' \
'------sanic\r\n'
'Content-Disposition: form-data; name="test"\r\n'
'\r\n'
'OK\r\n'
'------sanic--\r\n',
'------sanic\r\n' \
'content-disposition: form-data; name="test"\r\n' \
'\r\n' \
'OK\r\n' \
'------sanic\r\n'
'content-disposition: form-data; name="test"\r\n'
'\r\n'
'OK\r\n'
'------sanic--\r\n',
])
def test_post_form_multipart_form_data(payload):
app = Sanic('test_post_form_multipart_form_data')
def test_post_form_multipart_form_data(app, payload):
@app.route('/', methods=['POST'])
async def handler(request):
@@ -329,8 +319,7 @@ def test_post_form_multipart_form_data(payload):
('/bar/baz', '', 'http://{}:{}/bar/baz'),
('/moo/boo', 'arg1=val1', 'http://{}:{}/moo/boo?arg1=val1')
])
def test_url_attributes_no_ssl(path, query, expected_url):
app = Sanic('test_url_attrs_no_ssl')
def test_url_attributes_no_ssl(app, path, query, expected_url):
async def handler(request):
return text('OK')
@@ -354,9 +343,7 @@ def test_url_attributes_no_ssl(path, query, expected_url):
('/bar/baz', '', 'https://{}:{}/bar/baz'),
('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1')
])
def test_url_attributes_with_ssl(path, query, expected_url):
app = Sanic('test_url_attrs_with_ssl')
def test_url_attributes_with_ssl_context(app, path, query, expected_url):
current_dir = os.path.dirname(os.path.realpath(__file__))
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(
@@ -379,3 +366,342 @@ def test_url_attributes_with_ssl(path, query, expected_url):
assert parsed.path == request.path
assert parsed.query == request.query_string
assert parsed.netloc == request.host
@pytest.mark.parametrize(
'path,query,expected_url', [
('/foo', '', 'https://{}:{}/foo'),
('/bar/baz', '', 'https://{}:{}/bar/baz'),
('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1')
])
def test_url_attributes_with_ssl_dict(app, path, query, expected_url):
current_dir = os.path.dirname(os.path.realpath(__file__))
ssl_cert = os.path.join(current_dir, 'certs/selfsigned.cert')
ssl_key = os.path.join(current_dir, 'certs/selfsigned.key')
ssl_dict = {
'cert': ssl_cert,
'key': ssl_key
}
async def handler(request):
return text('OK')
app.add_route(handler, path)
request, response = app.test_client.get(
'https://{}:{}'.format(HOST, PORT) + path + '?{}'.format(query),
server_kwargs={'ssl': ssl_dict})
assert request.url == expected_url.format(HOST, PORT)
parsed = urlparse(request.url)
assert parsed.scheme == request.scheme
assert parsed.path == request.path
assert parsed.query == request.query_string
assert parsed.netloc == request.host
def test_invalid_ssl_dict(app):
@app.get('/test')
async def handler(request):
return text('ssl test')
ssl_dict = {
'cert': None,
'key': None
}
with pytest.raises(ValueError) as excinfo:
request, response = app.test_client.get('/test', server_kwargs={'ssl': ssl_dict})
assert str(excinfo.value) == 'SSLContext or certificate and key required.'
def test_form_with_multiple_values(app):
@app.route('/', methods=['POST'])
async def handler(request):
return text("OK")
payload="selectedItems=v1&selectedItems=v2&selectedItems=v3"
headers = {'content-type': 'application/x-www-form-urlencoded'}
request, response = app.test_client.post('/', data=payload,
headers=headers)
assert request.form.getlist("selectedItems") == ["v1", "v2", "v3"]
def test_request_string_representation(app):
@app.route('/', methods=["GET"])
async def get(request):
return text("OK")
request, _ = app.test_client.get("/")
assert repr(request) == '<Request: GET />'
@pytest.mark.parametrize(
'payload', [
'------sanic\r\n'
'Content-Disposition: form-data; filename="filename"; name="test"\r\n'
'\r\n'
'OK\r\n'
'------sanic--\r\n',
'------sanic\r\n'
'content-disposition: form-data; filename="filename"; name="test"\r\n'
'\r\n'
'content-type: application/json; {"field": "value"}\r\n'
'------sanic--\r\n',
])
def test_request_multipart_files(app, payload):
@app.route("/", methods=["POST"])
async def post(request):
return text("OK")
headers = {'content-type': 'multipart/form-data; boundary=----sanic'}
request, _ = app.test_client.post(data=payload, headers=headers)
assert request.files.get('test').name == "filename"
def test_request_multipart_file_with_json_content_type(app):
@app.route("/", methods=["POST"])
async def post(request):
return text("OK")
payload = (
'------sanic\r\n'
'Content-Disposition: form-data; name="file"; filename="test.json"\r\n'
'Content-Type: application/json\r\n'
'Content-Length: 0'
'\r\n'
'\r\n'
'------sanic--'
)
headers = {'content-type': 'multipart/form-data; boundary=------sanic'}
request, _ = app.test_client.post(data=payload, headers=headers)
assert request.files.get('file').type == 'application/json'
def test_request_multipart_file_without_field_name(app, caplog):
@app.route("/", methods=["POST"])
async def post(request):
return text("OK")
payload = (
'------sanic\r\nContent-Disposition: form-data; filename="test.json"'
'\r\nContent-Type: application/json\r\n\r\n\r\n------sanic--'
)
headers = {'content-type': 'multipart/form-data; boundary=------sanic'}
request, _ = app.test_client.post(data=payload, headers=headers, debug=True)
with caplog.at_level(logging.DEBUG):
request.form
assert caplog.record_tuples[-1] == ('sanic.root', logging.DEBUG,
"Form-data field does not have a 'name' parameter "
"in the Content-Disposition header"
)
def test_request_multipart_file_duplicate_filed_name(app):
@app.route("/", methods=["POST"])
async def post(request):
return text("OK")
payload = (
'--e73ffaa8b1b2472b8ec848de833cb05b\r\n'
'Content-Disposition: form-data; name="file"\r\n'
'Content-Type: application/octet-stream\r\n'
'Content-Length: 15\r\n'
'\r\n'
'{"test":"json"}\r\n'
'--e73ffaa8b1b2472b8ec848de833cb05b\r\n'
'Content-Disposition: form-data; name="file"\r\n'
'Content-Type: application/octet-stream\r\n'
'Content-Length: 15\r\n'
'\r\n'
'{"test":"json2"}\r\n'
'--e73ffaa8b1b2472b8ec848de833cb05b--\r\n'
)
headers = {
'Content-Type': 'multipart/form-data; boundary=e73ffaa8b1b2472b8ec848de833cb05b'
}
request, _ = app.test_client.post(data=payload, headers=headers, debug=True)
assert request.form.getlist('file') == ['{"test":"json"}', '{"test":"json2"}']
def test_request_multipart_with_multiple_files_and_type(app):
@app.route("/", methods=["POST"])
async def post(request):
return text("OK")
payload = '------sanic\r\nContent-Disposition: form-data; name="file"; filename="test.json"' \
'\r\nContent-Type: application/json\r\n\r\n\r\n' \
'------sanic\r\nContent-Disposition: form-data; name="file"; filename="some_file.pdf"\r\n' \
'Content-Type: application/pdf\r\n\r\n\r\n------sanic--'
headers = {'content-type': 'multipart/form-data; boundary=------sanic'}
request, _ = app.test_client.post(data=payload, headers=headers)
assert len(request.files.getlist('file')) == 2
assert request.files.getlist('file')[0].type == 'application/json'
assert request.files.getlist('file')[1].type == 'application/pdf'
def test_request_repr(app):
@app.get('/')
def handler(request):
return text('pass')
request, response = app.test_client.get('/')
assert repr(request) == '<Request: GET />'
request.method = None
assert repr(request) == '<Request>'
def test_request_bool(app):
@app.get('/')
def handler(request):
return text('pass')
request, response = app.test_client.get('/')
assert bool(request)
request.transport = False
assert not bool(request)
def test_request_parsing_form_failed(app, caplog):
@app.route('/', methods=['POST'])
async def handler(request):
return text('OK')
payload = 'test=OK'
headers = {'content-type': 'multipart/form-data'}
request, response = app.test_client.post('/', data=payload, headers=headers)
with caplog.at_level(logging.ERROR):
request.form
assert caplog.record_tuples[-1] == ('sanic.error', logging.ERROR, 'Failed when parsing form')
def test_request_args_no_query_string(app):
@app.get('/')
def handler(request):
return text('pass')
request, response = app.test_client.get('/')
assert request.args == {}
def test_request_raw_args(app):
params = {'test': 'OK'}
@app.get('/')
def handler(request):
return text('pass')
request, response = app.test_client.get('/', params=params)
assert request.raw_args == params
def test_request_cookies(app):
cookies = {'test': 'OK'}
@app.get('/')
def handler(request):
return text('OK')
request, response = app.test_client.get('/', cookies=cookies)
assert request.cookies == cookies
assert request.cookies == cookies # For request._cookies
def test_request_cookies_without_cookies(app):
@app.get('/')
def handler(request):
return text('OK')
request, response = app.test_client.get('/')
assert request.cookies == {}
def test_request_port(app):
@app.get('/')
def handler(request):
return text('OK')
request, response = app.test_client.get('/')
port = request.port
assert isinstance(port, int)
delattr(request, '_socket')
delattr(request, '_port')
port = request.port
assert isinstance(port, int)
assert hasattr(request, '_socket')
assert hasattr(request, '_port')
def test_request_socket(app):
@app.get('/')
def handler(request):
return text('OK')
request, response = app.test_client.get('/')
socket = request.socket
assert isinstance(socket, tuple)
ip = socket[0]
port = socket[1]
assert ip == request.ip
assert port == request.port
delattr(request, '_socket')
socket = request.socket
assert isinstance(socket, tuple)
assert hasattr(request, '_socket')
def test_request_form_invalid_content_type(app):
@app.route("/", methods=["POST"])
async def post(request):
return text("OK")
request, response = app.test_client.post('/', json={'test': 'OK'})
assert request.form == {}

View File

@@ -1,25 +1,25 @@
import asyncio
import inspect
import os
from aiofiles import os as async_os
from collections import namedtuple
from mimetypes import guess_type
from random import choice
from unittest.mock import MagicMock
from urllib.parse import unquote
import pytest
from random import choice
from aiofiles import os as async_os
from sanic import Sanic
from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json
from sanic.response import (HTTPResponse, StreamingHTTPResponse, file,
file_stream, json, raw, stream, text)
from sanic.server import HttpProtocol
from sanic.testing import HOST, PORT
from unittest.mock import MagicMock
JSON_DATA = {'ok': True}
def test_response_body_not_a_string():
def test_response_body_not_a_string(app):
"""Test when a response body sent from the application is not a string"""
app = Sanic('response_body_not_a_string')
random_num = choice(range(1000))
@app.route('/hello')
@@ -31,19 +31,126 @@ def test_response_body_not_a_string():
async def sample_streaming_fn(response):
response.write('foo,')
await response.write('foo,')
await asyncio.sleep(.001)
response.write('bar')
await response.write('bar')
def test_method_not_allowed(app):
@app.get('/')
async def test_get(request):
return response.json({'hello': 'world'})
request, response = app.test_client.head('/')
assert response.headers['Allow'] == 'GET'
request, response = app.test_client.post('/')
assert response.headers['Allow'] == 'GET'
@app.post('/')
async def test_post(request):
return response.json({'hello': 'world'})
request, response = app.test_client.head('/')
assert response.status == 405
assert set(response.headers['Allow'].split(', ')) == {'GET', 'POST'}
assert response.headers['Content-Length'] == '0'
request, response = app.test_client.patch('/')
assert response.status == 405
assert set(response.headers['Allow'].split(', ')) == {'GET', 'POST'}
assert response.headers['Content-Length'] == '0'
def test_response_header(app):
@app.get('/')
async def test(request):
return json({
"ok": True
}, headers={
'CONTENT-TYPE': 'application/json'
})
request, response = app.test_client.get('/')
assert dict(response.headers) == {
'Connection': 'keep-alive',
'Keep-Alive': str(app.config.KEEP_ALIVE_TIMEOUT),
'Content-Length': '11',
'Content-Type': 'application/json',
}
def test_response_content_length(app):
@app.get("/response_with_space")
async def response_with_space(request):
return json({
"message": "Data",
"details": "Some Details"
}, headers={
'CONTENT-TYPE': 'application/json'
})
@app.get("/response_without_space")
async def response_without_space(request):
return json({
"message":"Data",
"details":"Some Details"
}, headers={
'CONTENT-TYPE': 'application/json'
})
_, response = app.test_client.get("/response_with_space")
content_length_for_response_with_space = response.headers.get("Content-Length")
_, response = app.test_client.get("/response_without_space")
content_length_for_response_without_space = response.headers.get("Content-Length")
assert content_length_for_response_with_space == content_length_for_response_without_space
assert content_length_for_response_with_space == '43'
def test_response_content_length_with_different_data_types(app):
@app.get("/")
async def get_data_with_different_types(request):
# Indentation issues in the Response is intentional. Please do not fix
return json({
'bool': True,
'none': None,
'string':'string',
'number': -1},
headers={
'CONTENT-TYPE': 'application/json'
})
_, response = app.test_client.get("/")
assert response.headers.get("Content-Length") == '55'
@pytest.fixture
def json_app():
app = Sanic('json')
def json_app(app):
@app.route("/")
async def test(request):
return json(JSON_DATA)
@app.get("/no-content")
async def no_content_handler(request):
return json(JSON_DATA, status=204)
@app.get("/no-content/unmodified")
async def no_content_unmodified_handler(request):
return json(None, status=304)
@app.get("/unmodified")
async def unmodified_handler(request):
return json(JSON_DATA, status=304)
@app.delete("/")
async def delete_handler(request):
return json(None, status=204)
return app
@@ -54,9 +161,33 @@ def test_json_response(json_app):
assert response.text == json_dumps(JSON_DATA)
assert response.json == JSON_DATA
def test_no_content(json_app):
request, response = json_app.test_client.get('/no-content')
assert response.status == 204
assert response.text == ''
assert 'Content-Length' not in response.headers
request, response = json_app.test_client.get('/no-content/unmodified')
assert response.status == 304
assert response.text == ''
assert 'Content-Length' not in response.headers
assert 'Content-Type' not in response.headers
request, response = json_app.test_client.get('/unmodified')
assert response.status == 304
assert response.text == ''
assert 'Content-Length' not in response.headers
assert 'Content-Type' not in response.headers
request, response = json_app.test_client.delete('/')
assert response.status == 204
assert response.text == ''
assert 'Content-Length' not in response.headers
@pytest.fixture
def streaming_app():
app = Sanic('streaming')
def streaming_app(app):
@app.route("/")
async def test(request):
@@ -101,20 +232,30 @@ def test_stream_response_includes_chunked_header():
def test_stream_response_writes_correct_content_to_transport(streaming_app):
response = StreamingHTTPResponse(sample_streaming_fn)
response.transport = MagicMock(asyncio.Transport)
response.protocol = MagicMock(HttpProtocol)
response.protocol.transport = MagicMock(asyncio.Transport)
async def mock_drain():
pass
def mock_push_data(data):
response.protocol.transport.write(data)
response.protocol.push_data = mock_push_data
response.protocol.drain = mock_drain
@streaming_app.listener('after_server_start')
async def run_stream(app, loop):
await response.stream()
assert response.transport.write.call_args_list[1][0][0] == (
assert response.protocol.transport.write.call_args_list[1][0][0] == (
b'4\r\nfoo,\r\n'
)
assert response.transport.write.call_args_list[2][0][0] == (
assert response.protocol.transport.write.call_args_list[2][0][0] == (
b'3\r\nbar\r\n'
)
assert response.transport.write.call_args_list[3][0][0] == (
assert response.protocol.transport.write.call_args_list[3][0][0] == (
b'0\r\n\r\n'
)
@@ -123,6 +264,29 @@ def test_stream_response_writes_correct_content_to_transport(streaming_app):
streaming_app.run(host=HOST, port=PORT)
def test_stream_response_with_cookies(app):
@app.route("/")
async def test(request):
response = stream(sample_streaming_fn, content_type='text/csv')
response.cookies['test'] = 'modified'
response.cookies['test'] = 'pass'
return response
request, response = app.test_client.get('/')
assert response.cookies['test'].value == 'pass'
def test_stream_response_without_cookies(app):
@app.route("/")
async def test(request):
return stream(sample_streaming_fn, content_type='text/csv')
request, response = app.test_client.get('/')
assert response.cookies == {}
@pytest.fixture
def static_file_directory():
"""The static directory to serve"""
@@ -137,24 +301,35 @@ def get_file_content(static_file_directory, file_name):
with open(os.path.join(static_file_directory, file_name), 'rb') as file:
return file.read()
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_file_response(file_name, static_file_directory):
app = Sanic('test_file_helper')
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png'])
@pytest.mark.parametrize('status', [200, 401])
def test_file_response(app, file_name, static_file_directory, status):
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file(file_path, mime_type=guess_type(file_path)[0] or 'text/plain')
return file(file_path, status=status,
mime_type=guess_type(file_path)[0] or 'text/plain')
request, response = app.test_client.get('/files/{}'.format(file_name))
assert response.status == 200
assert response.status == status
assert response.body == get_file_content(static_file_directory, file_name)
assert 'Content-Disposition' not in response.headers
@pytest.mark.parametrize('source,dest', [
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')])
def test_file_response_custom_filename(source, dest, static_file_directory):
app = Sanic('test_file_helper')
@pytest.mark.parametrize(
'source,dest',
[
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'),
('python.png', 'logo.png')
]
)
def test_file_response_custom_filename(app, source, dest,
static_file_directory):
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
@@ -164,11 +339,13 @@ def test_file_response_custom_filename(source, dest, static_file_directory):
request, response = app.test_client.get('/files/{}'.format(source))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, source)
assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest)
assert response.headers['Content-Disposition'] == \
'attachment; filename="{}"'.format(dest)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_file_head_response(file_name, static_file_directory):
app = Sanic('test_file_helper')
def test_file_head_response(app, file_name, static_file_directory):
@app.route('/files/<filename>', methods=['GET', 'HEAD'])
async def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
@@ -193,25 +370,34 @@ def test_file_head_response(file_name, static_file_directory):
'Content-Length']) == len(
get_file_content(static_file_directory, file_name))
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_file_stream_response(file_name, static_file_directory):
app = Sanic('test_file_helper')
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png'])
def test_file_stream_response(app, file_name, static_file_directory):
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file_stream(file_path, chunk_size=32,
mime_type=guess_type(file_path)[0] or 'text/plain')
mime_type=guess_type(file_path)[0] or 'text/plain')
request, response = app.test_client.get('/files/{}'.format(file_name))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
assert 'Content-Disposition' not in response.headers
@pytest.mark.parametrize('source,dest', [
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')])
def test_file_stream_response_custom_filename(source, dest, static_file_directory):
app = Sanic('test_file_helper')
@pytest.mark.parametrize(
'source,dest',
[
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'),
('python.png', 'logo.png')
]
)
def test_file_stream_response_custom_filename(app, source, dest,
static_file_directory):
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
@@ -221,11 +407,13 @@ def test_file_stream_response_custom_filename(source, dest, static_file_director
request, response = app.test_client.get('/files/{}'.format(source))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, source)
assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest)
assert response.headers['Content-Disposition'] == \
'attachment; filename="{}"'.format(dest)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_file_stream_head_response(file_name, static_file_directory):
app = Sanic('test_file_helper')
def test_file_stream_head_response(app, file_name, static_file_directory):
@app.route('/files/<filename>', methods=['GET', 'HEAD'])
async def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
@@ -241,8 +429,10 @@ def test_file_stream_head_response(file_name, static_file_directory):
headers=headers,
content_type=guess_type(file_path)[0] or 'text/plain')
else:
return file_stream(file_path, chunk_size=32, headers=headers,
mime_type=guess_type(file_path)[0] or 'text/plain')
return file_stream(
file_path, chunk_size=32, headers=headers,
mime_type=guess_type(file_path)[0] or 'text/plain'
)
request, response = app.test_client.head('/files/{}'.format(file_name))
assert response.status == 200
@@ -254,4 +444,43 @@ def test_file_stream_head_response(file_name, static_file_directory):
assert 'Content-Length' in response.headers
assert int(response.headers[
'Content-Length']) == len(
get_file_content(static_file_directory, file_name))
get_file_content(static_file_directory, file_name))
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png'])
@pytest.mark.parametrize('size,start,end', [
(1024, 0, 1024),
(4096, 1024, 8192),
])
def test_file_stream_response_range(app, file_name, static_file_directory, size, start, end):
Range = namedtuple('Range', ['size', 'start', 'end', 'total'])
total = len(get_file_content(static_file_directory, file_name))
range = Range(size=size, start=start, end=end, total=total)
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file_stream(
file_path,
chunk_size=32,
mime_type=guess_type(file_path)[0] or 'text/plain',
_range=range)
request, response = app.test_client.get('/files/{}'.format(file_name))
assert response.status == 206
assert 'Content-Range' in response.headers
assert response.headers['Content-Range'] == 'bytes {}-{}/{}'.format(range.start, range.end, range.total)
def test_raw_response(app):
@app.get('/test')
def handler(request):
return raw(b'raw_response')
request, response = app.test_client.get('/test')
assert response.content_type == 'application/octet-stream'
assert response.body == b'raw_response'

View File

@@ -7,6 +7,7 @@ from sanic.config import Config
Config.RESPONSE_TIMEOUT = 1
response_timeout_app = Sanic('test_response_timeout')
response_timeout_default_app = Sanic('test_response_timeout_default')
response_handler_cancelled_app = Sanic('test_response_handler_cancelled')
@response_timeout_app.route('/1')
@@ -36,3 +37,29 @@ def test_default_server_error_response_timeout():
request, response = response_timeout_default_app.test_client.get('/1')
assert response.status == 503
assert response.text == 'Error: Response Timeout'
response_handler_cancelled_app.flag = False
@response_handler_cancelled_app.exception(asyncio.CancelledError)
def handler_cancelled(request, exception):
# If we get a CancelledError, it means sanic has already sent a response,
# we should not ever have to handle a CancelledError.
response_handler_cancelled_app.flag = True
return text("App received CancelledError!", 500)
# The client will never receive this response, because the socket
# is already closed when we get a CancelledError.
@response_handler_cancelled_app.route('/1')
async def handler_3(request):
await asyncio.sleep(2)
return text('OK')
def test_response_handler_cancelled():
request, response = response_handler_cancelled_app.test_client.get('/1')
assert response.status == 503
assert response.text == 'Error: Response Timeout'
assert response_handler_cancelled_app.flag is False

View File

@@ -1,20 +1,18 @@
import asyncio
import pytest
from sanic import Sanic
from sanic.response import text
from sanic.router import RouteExists, RouteDoesNotExist
from sanic.constants import HTTP_METHODS
from sanic.response import json, text
from sanic.router import ParameterNameConflicts, RouteDoesNotExist, RouteExists
# ------------------------------------------------------------ #
# UTF-8
# ------------------------------------------------------------ #
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_routes_get(method):
app = Sanic('test_shorhand_routes_get')
def test_versioned_routes_get(app, method):
method = method.lower()
func = getattr(app, method)
@@ -29,10 +27,10 @@ def test_versioned_routes_get(method):
client_method = getattr(app.test_client, method)
request, response = client_method('/v1/{}'.format(method))
assert response.status== 200
assert response.status == 200
def test_shorthand_routes_get():
app = Sanic('test_shorhand_routes_get')
def test_shorthand_routes_get(app):
@app.get('/get')
def handler(request):
@@ -44,8 +42,8 @@ def test_shorthand_routes_get():
request, response = app.test_client.post('/get')
assert response.status == 405
def test_shorthand_routes_multiple():
app = Sanic('test_shorthand_routes_multiple')
def test_shorthand_routes_multiple(app):
@app.get('/get')
def get_handler(request):
@@ -62,16 +60,16 @@ def test_shorthand_routes_multiple():
request, response = app.test_client.options('/get/')
assert response.status == 200
def test_route_strict_slash():
app = Sanic('test_route_strict_slash')
def test_route_strict_slash(app):
@app.get('/get', strict_slashes=True)
def handler(request):
def handler1(request):
assert request.stream is None
return text('OK')
@app.post('/post/', strict_slashes=True)
def handler(request):
def handler2(request):
assert request.stream is None
return text('OK')
@@ -89,9 +87,9 @@ def test_route_strict_slash():
request, response = app.test_client.post('/post')
assert response.status == 404
def test_route_invalid_parameter_syntax():
def test_route_invalid_parameter_syntax(app):
with pytest.raises(ValueError):
app = Sanic('test_route_invalid_param_syntax')
@app.get('/get/<:string>', strict_slashes=True)
def handler(request):
@@ -99,6 +97,7 @@ def test_route_invalid_parameter_syntax():
request, response = app.test_client.get('/get')
def test_route_strict_slash_default_value():
app = Sanic('test_route_strict_slash', strict_slashes=True)
@@ -109,8 +108,8 @@ def test_route_strict_slash_default_value():
request, response = app.test_client.get('/get/')
assert response.status == 404
def test_route_strict_slash_without_passing_default_value():
app = Sanic('test_route_strict_slash')
def test_route_strict_slash_without_passing_default_value(app):
@app.get('/get')
def handler(request):
@@ -119,6 +118,7 @@ def test_route_strict_slash_without_passing_default_value():
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
def test_route_strict_slash_default_value_can_be_overwritten():
app = Sanic('test_route_strict_slash', strict_slashes=True)
@@ -129,8 +129,31 @@ def test_route_strict_slash_default_value_can_be_overwritten():
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
def test_route_optional_slash():
app = Sanic('test_route_optional_slash')
def test_route_slashes_overload(app):
@app.get('/hello/')
def handler_get(request):
return text('OK')
@app.post('/hello/')
def handler_post(request):
return text('OK')
request, response = app.test_client.get('/hello')
assert response.text == 'OK'
request, response = app.test_client.get('/hello/')
assert response.text == 'OK'
request, response = app.test_client.post('/hello')
assert response.text == 'OK'
request, response = app.test_client.post('/hello/')
assert response.text == 'OK'
def test_route_optional_slash(app):
@app.get('/get')
def handler(request):
@@ -142,8 +165,43 @@ def test_route_optional_slash():
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
def test_shorthand_routes_post():
app = Sanic('test_shorhand_routes_post')
def test_route_strict_slashes_set_to_false_and_host_is_a_list(app):
# Part of regression test for issue #1120
site1 = '127.0.0.1:{}'.format(app.test_client.port)
# before fix, this raises a RouteExists error
@app.get('/get', host=[site1, 'site2.com'], strict_slashes=False)
def get_handler(request):
return text('OK')
request, response = app.test_client.get('http://' + site1 + '/get')
assert response.text == 'OK'
@app.post('/post', host=[site1, 'site2.com'], strict_slashes=False)
def post_handler(request):
return text('OK')
request, response = app.test_client.post('http://' + site1 + '/post')
assert response.text == 'OK'
@app.put('/put', host=[site1, 'site2.com'], strict_slashes=False)
def put_handler(request):
return text('OK')
request, response = app.test_client.put('http://' + site1 + '/put')
assert response.text == 'OK'
@app.delete('/delete', host=[site1, 'site2.com'], strict_slashes=False)
def delete_handler(request):
return text('OK')
request, response = app.test_client.delete('http://' + site1 + '/delete')
assert response.text == 'OK'
def test_shorthand_routes_post(app):
@app.post('/post')
def handler(request):
@@ -155,8 +213,8 @@ def test_shorthand_routes_post():
request, response = app.test_client.get('/post')
assert response.status == 405
def test_shorthand_routes_put():
app = Sanic('test_shorhand_routes_put')
def test_shorthand_routes_put(app):
@app.put('/put')
def handler(request):
@@ -171,8 +229,8 @@ def test_shorthand_routes_put():
request, response = app.test_client.get('/put')
assert response.status == 405
def test_shorthand_routes_delete():
app = Sanic('test_shorhand_routes_delete')
def test_shorthand_routes_delete(app):
@app.delete('/delete')
def handler(request):
@@ -187,8 +245,8 @@ def test_shorthand_routes_delete():
request, response = app.test_client.get('/delete')
assert response.status == 405
def test_shorthand_routes_patch():
app = Sanic('test_shorhand_routes_patch')
def test_shorthand_routes_patch(app):
@app.patch('/patch')
def handler(request):
@@ -203,8 +261,8 @@ def test_shorthand_routes_patch():
request, response = app.test_client.get('/patch')
assert response.status == 405
def test_shorthand_routes_head():
app = Sanic('test_shorhand_routes_head')
def test_shorthand_routes_head(app):
@app.head('/head')
def handler(request):
@@ -219,8 +277,8 @@ def test_shorthand_routes_head():
request, response = app.test_client.get('/head')
assert response.status == 405
def test_shorthand_routes_options():
app = Sanic('test_shorhand_routes_options')
def test_shorthand_routes_options(app):
@app.options('/options')
def handler(request):
@@ -235,8 +293,8 @@ def test_shorthand_routes_options():
request, response = app.test_client.get('/options')
assert response.status == 405
def test_static_routes():
app = Sanic('test_dynamic_route')
def test_static_routes(app):
@app.route('/test')
async def handler1(request):
@@ -253,9 +311,7 @@ def test_static_routes():
assert response.text == 'OK2'
def test_dynamic_route():
app = Sanic('test_dynamic_route')
def test_dynamic_route(app):
results = []
@app.route('/folder/<name>')
@@ -269,9 +325,7 @@ def test_dynamic_route():
assert results[0] == 'test123'
def test_dynamic_route_string():
app = Sanic('test_dynamic_route_string')
def test_dynamic_route_string(app):
results = []
@app.route('/folder/<name:string>')
@@ -290,9 +344,7 @@ def test_dynamic_route_string():
assert results[1] == 'favicon.ico'
def test_dynamic_route_int():
app = Sanic('test_dynamic_route_int')
def test_dynamic_route_int(app):
results = []
@app.route('/folder/<folder_id:int>')
@@ -308,9 +360,7 @@ def test_dynamic_route_int():
assert response.status == 404
def test_dynamic_route_number():
app = Sanic('test_dynamic_route_number')
def test_dynamic_route_number(app):
results = []
@app.route('/weight/<weight:number>')
@@ -329,8 +379,7 @@ def test_dynamic_route_number():
assert response.status == 404
def test_dynamic_route_regex():
app = Sanic('test_dynamic_route_regex')
def test_dynamic_route_regex(app):
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>')
async def handler(request, folder_id):
@@ -349,8 +398,29 @@ def test_dynamic_route_regex():
assert response.status == 200
def test_dynamic_route_path():
app = Sanic('test_dynamic_route_path')
def test_dynamic_route_uuid(app):
import uuid
results = []
@app.route('/quirky/<unique_id:uuid>')
async def handler(request, unique_id):
results.append(unique_id)
return text('OK')
url = '/quirky/123e4567-e89b-12d3-a456-426655440000'
request, response = app.test_client.get(url)
assert response.text == 'OK'
assert type(results[0]) is uuid.UUID
request, response = app.test_client.get('/quirky/{}'.format(uuid.uuid4()))
assert response.status == 200
request, response = app.test_client.get('/quirky/non-existing')
assert response.status == 404
def test_dynamic_route_path(app):
@app.route('/<path:path>/info')
async def handler(request, path):
@@ -373,8 +443,7 @@ def test_dynamic_route_path():
assert response.status == 200
def test_dynamic_route_unhashable():
app = Sanic('test_dynamic_route_unhashable')
def test_dynamic_route_unhashable(app):
@app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
async def handler(request, unhashable):
@@ -393,12 +462,13 @@ def test_dynamic_route_unhashable():
assert response.status == 404
def test_websocket_route():
app = Sanic('test_websocket_route')
@pytest.mark.parametrize('url', ['/ws', 'ws'])
def test_websocket_route(app, url):
ev = asyncio.Event()
@app.websocket('/ws')
@app.websocket(url)
async def handler(request, ws):
assert request.scheme == 'ws'
assert ws.subprotocol is None
ev.set()
@@ -411,8 +481,7 @@ def test_websocket_route():
assert ev.is_set()
def test_websocket_route_with_subprotocols():
app = Sanic('test_websocket_route')
def test_websocket_route_with_subprotocols(app):
results = []
@app.websocket('/ws', subprotocols=['foo', 'bar'])
@@ -453,8 +522,25 @@ def test_websocket_route_with_subprotocols():
assert results == ['bar', 'bar', None, None]
def test_route_duplicate():
app = Sanic('test_route_duplicate')
@pytest.mark.parametrize('strict_slashes', [True, False, None])
def test_add_webscoket_route(app, strict_slashes):
ev = asyncio.Event()
async def handler(request, ws):
assert ws.subprotocol is None
ev.set()
app.add_websocket_route(handler, '/ws', strict_slashes=strict_slashes)
request, response = app.test_client.get('/ws', headers={
'Upgrade': 'websocket',
'Connection': 'upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13'})
assert response.status == 101
assert ev.is_set()
def test_route_duplicate(app):
with pytest.raises(RouteExists):
@app.route('/test')
@@ -467,16 +553,15 @@ def test_route_duplicate():
with pytest.raises(RouteExists):
@app.route('/test/<dynamic>/')
async def handler1(request, dynamic):
async def handler3(request, dynamic):
pass
@app.route('/test/<dynamic>/')
async def handler2(request, dynamic):
async def handler4(request, dynamic):
pass
def test_method_not_allowed():
app = Sanic('test_method_not_allowed')
def test_method_not_allowed(app):
@app.route('/test', methods=['GET'])
async def handler(request):
@@ -489,8 +574,8 @@ def test_method_not_allowed():
assert response.status == 405
def test_static_add_route():
app = Sanic('test_static_add_route')
@pytest.mark.parametrize('strict_slashes', [True, False, None])
def test_static_add_route(app, strict_slashes):
async def handler1(request):
return text('OK1')
@@ -498,8 +583,8 @@ def test_static_add_route():
async def handler2(request):
return text('OK2')
app.add_route(handler1, '/test')
app.add_route(handler2, '/test2')
app.add_route(handler1, '/test', strict_slashes=strict_slashes)
app.add_route(handler2, '/test2', strict_slashes=strict_slashes)
request, response = app.test_client.get('/test')
assert response.text == 'OK1'
@@ -508,8 +593,7 @@ def test_static_add_route():
assert response.text == 'OK2'
def test_dynamic_add_route():
app = Sanic('test_dynamic_add_route')
def test_dynamic_add_route(app):
results = []
@@ -524,8 +608,7 @@ def test_dynamic_add_route():
assert results[0] == 'test123'
def test_dynamic_add_route_string():
app = Sanic('test_dynamic_add_route_string')
def test_dynamic_add_route_string(app):
results = []
@@ -545,9 +628,7 @@ def test_dynamic_add_route_string():
assert results[1] == 'favicon.ico'
def test_dynamic_add_route_int():
app = Sanic('test_dynamic_add_route_int')
def test_dynamic_add_route_int(app):
results = []
async def handler(request, folder_id):
@@ -564,9 +645,7 @@ def test_dynamic_add_route_int():
assert response.status == 404
def test_dynamic_add_route_number():
app = Sanic('test_dynamic_add_route_number')
def test_dynamic_add_route_number(app):
results = []
async def handler(request, weight):
@@ -586,8 +665,7 @@ def test_dynamic_add_route_number():
assert response.status == 404
def test_dynamic_add_route_regex():
app = Sanic('test_dynamic_route_int')
def test_dynamic_add_route_regex(app):
async def handler(request, folder_id):
return text('OK')
@@ -607,8 +685,7 @@ def test_dynamic_add_route_regex():
assert response.status == 200
def test_dynamic_add_route_unhashable():
app = Sanic('test_dynamic_add_route_unhashable')
def test_dynamic_add_route_unhashable(app):
async def handler(request, unhashable):
return text('OK')
@@ -628,8 +705,7 @@ def test_dynamic_add_route_unhashable():
assert response.status == 404
def test_add_route_duplicate():
app = Sanic('test_add_route_duplicate')
def test_add_route_duplicate(app):
with pytest.raises(RouteExists):
async def handler1(request):
@@ -652,8 +728,7 @@ def test_add_route_duplicate():
app.add_route(handler2, '/test/<dynamic>/')
def test_add_route_method_not_allowed():
app = Sanic('test_add_route_method_not_allowed')
def test_add_route_method_not_allowed(app):
async def handler(request):
return text('OK')
@@ -667,8 +742,7 @@ def test_add_route_method_not_allowed():
assert response.status == 405
def test_remove_static_route():
app = Sanic('test_remove_static_route')
def test_remove_static_route(app):
async def handler1(request):
return text('OK1')
@@ -695,8 +769,7 @@ def test_remove_static_route():
assert response.status == 404
def test_remove_dynamic_route():
app = Sanic('test_remove_dynamic_route')
def test_remove_dynamic_route(app):
async def handler(request, name):
return text('OK')
@@ -711,14 +784,16 @@ def test_remove_dynamic_route():
assert response.status == 404
def test_remove_inexistent_route():
app = Sanic('test_remove_inexistent_route')
def test_remove_inexistent_route(app):
with pytest.raises(RouteDoesNotExist):
app.remove_route('/test')
uri = '/test'
with pytest.raises(RouteDoesNotExist) as excinfo:
app.remove_route(uri)
def test_removing_slash():
app = Sanic(__name__)
assert str(excinfo.value) == 'Route was not registered: {}'.format(uri)
def test_removing_slash(app):
@app.get('/rest/<resource>')
def get(_):
@@ -731,8 +806,7 @@ def test_removing_slash():
assert len(app.router.routes_all.keys()) == 2
def test_remove_unhashable_route():
app = Sanic('test_remove_unhashable_route')
def test_remove_unhashable_route(app):
async def handler(request, unhashable):
return text('OK')
@@ -760,8 +834,7 @@ def test_remove_unhashable_route():
assert response.status == 404
def test_remove_route_without_clean_cache():
app = Sanic('test_remove_static_route')
def test_remove_route_without_clean_cache(app):
async def handler(request):
return text('OK')
@@ -788,8 +861,7 @@ def test_remove_route_without_clean_cache():
assert response.status == 200
def test_overload_routes():
app = Sanic('test_dynamic_route')
def test_overload_routes(app):
@app.route('/overload', methods=['GET'])
async def handler1(request):
@@ -817,8 +889,7 @@ def test_overload_routes():
return text('Duplicated')
def test_unmergeable_overload_routes():
app = Sanic('test_dynamic_route')
def test_unmergeable_overload_routes(app):
@app.route('/overload_whole', methods=None)
async def handler1(request):
@@ -835,14 +906,13 @@ def test_unmergeable_overload_routes():
request, response = app.test_client.post('/overload_whole')
assert response.text == 'OK1'
@app.route('/overload_part', methods=['GET'])
async def handler1(request):
async def handler3(request):
return text('OK1')
with pytest.raises(RouteExists):
@app.route('/overload_part')
async def handler2(request):
async def handler4(request):
return text('Duplicated')
request, response = app.test_client.get('/overload_part')
@@ -850,3 +920,64 @@ def test_unmergeable_overload_routes():
request, response = app.test_client.post('/overload_part')
assert response.status == 405
def test_unicode_routes(app):
@app.get('/你好')
def handler1(request):
return text('OK1')
request, response = app.test_client.get('/你好')
assert response.text == 'OK1'
@app.route('/overload/<param>', methods=['GET'])
async def handler2(request, param):
return text('OK2 ' + param)
request, response = app.test_client.get('/overload/你好')
assert response.text == 'OK2 你好'
def test_uri_with_different_method_and_different_params(app):
@app.route('/ads/<ad_id>', methods=['GET'])
async def ad_get(request, ad_id):
return json({'ad_id': ad_id})
@app.route('/ads/<action>', methods=['POST'])
async def ad_post(request, action):
return json({'action': action})
request, response = app.test_client.get('/ads/1234')
assert response.status == 200
assert response.json == {
'ad_id': '1234'
}
request, response = app.test_client.post('/ads/post')
assert response.status == 200
assert response.json == {
'action': 'post'
}
def test_route_raise_ParameterNameConflicts(app):
with pytest.raises(ParameterNameConflicts):
@app.get('/api/v1/<user>/<user>/')
def handler(request, user):
return text('OK')
def test_route_invalid_host(app):
host = 321
with pytest.raises(ValueError) as excinfo:
@app.get('/test', host=host)
def handler(request):
return text('pass')
assert str(excinfo.value) == (
"Expected either string or Iterable of "
"host strings, not {!r}"
).format(host)

View File

@@ -1,11 +1,7 @@
from io import StringIO
from random import choice
from string import ascii_letters
import signal
import pytest
from sanic import Sanic
from sanic.testing import HOST, PORT
AVAILABLE_LISTENERS = [
@@ -15,6 +11,12 @@ AVAILABLE_LISTENERS = [
'after_server_stop'
]
skipif_no_alarm = pytest.mark.skipif(
not hasattr(signal, 'SIGALRM'),
reason='SIGALRM is not implemented for this platform, we have to come '
'up with another timeout strategy to test these'
)
def create_listener(listener_name, in_list):
async def _listener(app, loop):
@@ -36,38 +38,49 @@ def start_stop_app(random_name_app, **run_kwargs):
pass
@skipif_no_alarm
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
def test_single_listener(listener_name):
def test_single_listener(app, listener_name):
"""Test that listeners on their own work"""
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
output = []
# Register listener
random_name_app.listener(listener_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()
start_stop_app(app)
assert app.name + listener_name == output.pop()
def test_all_listeners():
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
@skipif_no_alarm
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
def test_register_listener(app, listener_name):
"""
Test that listeners on their own work with
app.register_listener method
"""
output = []
# Register listener
listener = create_listener(listener_name, output)
app.register_listener(listener, event=listener_name)
start_stop_app(app)
assert app.name + listener_name == output.pop()
@skipif_no_alarm
def test_all_listeners(app):
output = []
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)
app.listener(listener_name)(listener)
start_stop_app(app)
for listener_name in AVAILABLE_LISTENERS:
assert random_name_app.name + listener_name == output.pop()
assert app.name + listener_name == output.pop()
async def test_trigger_before_events_create_server():
async def test_trigger_before_events_create_server(app):
class MySanicDb:
pass
app = Sanic("test_sanic_app")
@app.listener('before_server_start')
async def init_db(app, loop):
app.db = MySanicDb()

View File

@@ -1,8 +1,6 @@
from sanic import Sanic
from sanic.response import HTTPResponse
from sanic.testing import HOST, PORT
from unittest.mock import MagicMock
import pytest
import asyncio
from queue import Queue
@@ -13,15 +11,17 @@ async def stop(app, loop):
calledq = Queue()
def set_loop(app, loop):
loop.add_signal_handler = MagicMock()
def after(app, loop):
calledq.put(loop.add_signal_handler.called)
def test_register_system_signals():
def test_register_system_signals(app):
"""Test if sanic register system signals"""
app = Sanic('test_register_system_signals')
@app.route('/hello')
async def hello_route(request):
@@ -32,12 +32,11 @@ def test_register_system_signals():
app.listener('after_server_stop')(after)
app.run(HOST, PORT)
assert calledq.get() == True
assert calledq.get() is True
def test_dont_register_system_signals():
def test_dont_register_system_signals(app):
"""Test if sanic don't register system signals"""
app = Sanic('test_register_system_signals')
@app.route('/hello')
async def hello_route(request):
@@ -48,4 +47,4 @@ def test_dont_register_system_signals():
app.listener('after_server_stop')(after)
app.run(HOST, PORT, register_sys_signals=False)
assert calledq.get() == False
assert calledq.get() is False

View File

@@ -1,10 +1,9 @@
import inspect
import os
from time import gmtime, strftime
import pytest
from sanic import Sanic
@pytest.fixture(scope='module')
def static_file_directory():
@@ -25,9 +24,42 @@ def get_file_content(static_file_directory, file_name):
return file.read()
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_static_file(static_file_directory, file_name):
app = Sanic('test_static')
@pytest.fixture(scope='module')
def large_file(static_file_directory):
large_file_path = os.path.join(static_file_directory, 'large.file')
size = 2 * 1024 * 1024
with open(large_file_path, 'w') as f:
f.write('a' * size)
yield large_file_path
os.remove(large_file_path)
@pytest.fixture(autouse=True, scope='module')
def symlink(static_file_directory):
src = os.path.abspath(os.path.join(os.path.dirname(static_file_directory), 'conftest.py'))
symlink = 'symlink'
dist = os.path.join(static_file_directory, symlink)
os.symlink(src, dist)
yield symlink
os.remove(dist)
@pytest.fixture(autouse=True, scope='module')
def hard_link(static_file_directory):
src = os.path.abspath(os.path.join(os.path.dirname(static_file_directory), 'conftest.py'))
hard_link = 'hard_link'
dist = os.path.join(static_file_directory, hard_link)
os.link(src, dist)
yield hard_link
os.remove(dist)
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png', 'symlink', 'hard_link'])
def test_static_file(app, static_file_directory, file_name):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name))
@@ -36,11 +68,23 @@ def test_static_file(static_file_directory, file_name):
assert response.body == get_file_content(static_file_directory, file_name)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
def test_static_directory(file_name, base_uri, static_file_directory):
@pytest.mark.parametrize('file_name', ['test.html'])
def test_static_file_content_type(app, static_file_directory, file_name):
app.static(
'/testing.file',
get_file_path(static_file_directory, file_name),
content_type='text/html; charset=utf-8'
)
app = Sanic('test_static')
request, response = app.test_client.get('/testing.file')
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'symlink', 'hard_link'])
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
def test_static_directory(app, file_name, base_uri, static_file_directory):
app.static(base_uri, static_file_directory)
request, response = app.test_client.get(
@@ -50,8 +94,7 @@ def test_static_directory(file_name, base_uri, static_file_directory):
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_head_request(file_name, static_file_directory):
app = Sanic('test_static')
def test_static_head_request(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
@@ -66,8 +109,7 @@ def test_static_head_request(file_name, static_file_directory):
@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')
def test_static_content_range_correct(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
@@ -76,19 +118,18 @@ def test_static_content_range_correct(file_name, static_file_directory):
'Range': 'bytes=12-19'
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 200
assert response.status == 206
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[12:19]
static_file_directory, file_name))[12:20]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_front(file_name, static_file_directory):
app = Sanic('test_static')
def test_static_content_range_front(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
@@ -97,7 +138,7 @@ def test_static_content_range_front(file_name, static_file_directory):
'Range': 'bytes=12-'
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 200
assert response.status == 206
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
@@ -108,8 +149,7 @@ def test_static_content_range_front(file_name, static_file_directory):
@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')
def test_static_content_range_back(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
@@ -118,7 +158,7 @@ def test_static_content_range_back(file_name, static_file_directory):
'Range': 'bytes=-12'
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 200
assert response.status == 206
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
@@ -128,26 +168,28 @@ def test_static_content_range_back(file_name, static_file_directory):
assert response.body == static_content
@pytest.mark.parametrize('use_modified_since', [True, False])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_empty(file_name, static_file_directory):
app = Sanic('test_static')
def test_static_content_range_empty(app, file_name, static_file_directory, use_modified_since):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
'/testing.file',
get_file_path(static_file_directory, file_name),
use_content_range=True,
use_modified_since=use_modified_since
)
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 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')
def test_static_content_range_error(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
@@ -163,9 +205,72 @@ def test_static_content_range_error(file_name, static_file_directory):
len(get_file_content(static_file_directory, file_name)),)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_static_file_specified_host(static_file_directory, file_name):
app = Sanic('test_static')
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_invalid_unit(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
unit = 'bit'
headers = {
'Range': '{}=1-0'.format(unit)
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 416
assert response.text == "Error: {} is not a valid Range Type".format(unit)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_invalid_start(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
start = 'start'
headers = {
'Range': 'bytes={}-0'.format(start)
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 416
assert response.text == "Error: '{}' is invalid for Content Range".format(start)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_invalid_end(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
end = 'end'
headers = {
'Range': 'bytes=1-{}'.format(end)
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 416
assert response.text == "Error: '{}' is invalid for Content Range".format(end)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_invalid_parameters(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
headers = {
'Range': 'bytes=-'
}
request, response = app.test_client.get('/testing.file', headers=headers)
assert response.status == 416
assert response.text == "Error: Invalid for Content Range parameters"
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png'])
def test_static_file_specified_host(app, static_file_directory, file_name):
app.static(
'/testing.file',
get_file_path(static_file_directory, file_name),
@@ -178,3 +283,73 @@ def test_static_file_specified_host(static_file_directory, file_name):
assert response.body == get_file_content(static_file_directory, file_name)
request, response = app.test_client.get('/testing.file')
assert response.status == 404
@pytest.mark.parametrize('use_modified_since', [True, False])
@pytest.mark.parametrize('stream_large_files', [True, 1024])
@pytest.mark.parametrize('file_name', ['test.file', 'large.file'])
def test_static_stream_large_file(app, static_file_directory, file_name, use_modified_since, stream_large_files, large_file):
app.static(
'/testing.file',
get_file_path(static_file_directory, file_name),
use_modified_since=use_modified_since,
stream_large_files=stream_large_files
)
request, response = app.test_client.get('/testing.file')
assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_use_modified_since(app, static_file_directory, file_name):
file_stat = os.stat(get_file_path(static_file_directory, file_name))
modified_since = strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime(file_stat.st_mtime))
app.static(
'/testing.file',
get_file_path(static_file_directory, file_name),
use_modified_since=True
)
request, response = app.test_client.get(
'/testing.file', headers={'If-Modified-Since': modified_since})
assert response.status == 304
def test_file_not_found(app, static_file_directory):
app.static('/static', static_file_directory)
request, response = app.test_client.get('/static/not_found')
assert response.status == 404
assert response.text == 'Error: File not found'
@pytest.mark.parametrize('static_name', ['_static_name', 'static'])
@pytest.mark.parametrize('file_name', ['test.html'])
def test_static_name(app, static_file_directory, static_name, file_name):
app.static('/static', static_file_directory, name=static_name)
request, response = app.test_client.get('/static/{}'.format(file_name))
assert response.status == 200
@pytest.mark.parametrize('file_name',
['test.file'])
def test_static_remove_route(app, static_file_directory, file_name):
app.static(
'/testing.file',
get_file_path(static_file_directory, file_name)
)
request, response = app.test_client.get('/testing.file')
assert response.status == 200
app.remove_route('/testing.file')
request, response = app.test_client.get('/testing.file')
assert response.status == 404

View File

@@ -1,7 +1,6 @@
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
@@ -14,12 +13,16 @@ URL_FOR_ARGS1 = dict(arg1=['v1', 'v2'])
URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2'
URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor')
URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
_server='{}:{}'.format(test_host, test_port), _external=True)
URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
URL_FOR_ARGS3 = dict(
arg1='v1', _anchor='anchor', _scheme='http',
_server='{}:{}'.format(test_host, test_port), _external=True
)
URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host,
test_port)
URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True,
_server='http://{}:{}'.format(test_host, test_port),)
URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
_server='http://{}:{}'.format(test_host, test_port))
URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host,
test_port)
def _generate_handlers_from_names(app, l):
@@ -30,8 +33,7 @@ def _generate_handlers_from_names(app, l):
@pytest.fixture
def simple_app():
app = Sanic('simple_app')
def simple_app(app):
handler_names = list(string.ascii_letters)
_generate_handlers_from_names(app, handler_names)
@@ -54,8 +56,7 @@ def test_simple_url_for_getting(simple_app):
(URL_FOR_ARGS2, URL_FOR_VALUE2),
(URL_FOR_ARGS3, URL_FOR_VALUE3),
(URL_FOR_ARGS4, URL_FOR_VALUE4)])
def test_simple_url_for_getting_with_more_params(args, url):
app = Sanic('more_url_build')
def test_simple_url_for_getting_with_more_params(app, args, url):
@app.route('/myurl')
def passes(request):
@@ -67,11 +68,29 @@ def test_simple_url_for_getting_with_more_params(args, url):
assert response.text == 'this should pass'
def test_fails_if_endpoint_not_found():
app = Sanic('fail_url_build')
def test_url_for_with_server_name(app):
server_name = '{}:{}'.format(test_host, test_port)
app.config.update({
'SERVER_NAME': server_name
})
path = '/myurl'
@app.route(path)
def passes(request):
return text('this should pass')
url = 'http://{}{}'.format(server_name, path)
assert url == app.url_for('passes', _server=None, _external=True)
request, response = app.test_client.get(url)
assert response.status == 200
assert response.text == 'this should pass'
def test_fails_if_endpoint_not_found(app):
@app.route('/fail')
def fail():
def fail(request):
return text('this should fail')
with pytest.raises(URLBuildError) as e:
@@ -80,16 +99,14 @@ def test_fails_if_endpoint_not_found():
assert str(e.value) == 'Endpoint with name `passes` was not found'
def test_fails_url_build_if_param_not_passed():
def test_fails_url_build_if_param_not_passed(app):
url = '/'
for letter in string.ascii_letters:
url += '<{}>/'.format(letter)
app = Sanic('fail_url_build')
@app.route(url)
def fail():
def fail(request):
return text('this should fail')
fail_args = list(string.ascii_letters)
@@ -103,11 +120,10 @@ def test_fails_url_build_if_param_not_passed():
assert 'Required parameter `Z` was not passed to url_for' in str(e.value)
def test_fails_url_build_if_params_not_passed():
app = Sanic('fail_url_build')
def test_fails_url_build_if_params_not_passed(app):
@app.route('/fail')
def fail():
def fail(request):
return text('this should fail')
with pytest.raises(ValueError) as e:
@@ -126,11 +142,10 @@ PASSING_KWARGS = {
EXPECTED_BUILT_URL = '/4/woof/ba/normal/1.001'
def test_fails_with_int_message():
app = Sanic('fail_url_build')
def test_fails_with_int_message(app):
@app.route(COMPLEX_PARAM_URL)
def fail():
def fail(request):
return text('this should fail')
failing_kwargs = dict(PASSING_KWARGS)
@@ -140,16 +155,15 @@ def test_fails_with_int_message():
app.url_for('fail', **failing_kwargs)
expected_error = (
'Value "not_int" for parameter `foo` '
'does not match pattern for type `int`: \d+')
r'Value "not_int" for parameter `foo` '
r'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')
def test_fails_with_two_letter_string_message(app):
@app.route(COMPLEX_PARAM_URL)
def fail():
def fail(request):
return text('this should fail')
failing_kwargs = dict(PASSING_KWARGS)
@@ -165,11 +179,10 @@ def test_fails_with_two_letter_string_message():
assert str(e.value) == expected_error
def test_fails_with_number_message():
app = Sanic('fail_url_build')
def test_fails_with_number_message(app):
@app.route(COMPLEX_PARAM_URL)
def fail():
def fail(request):
return text('this should fail')
failing_kwargs = dict(PASSING_KWARGS)
@@ -185,11 +198,10 @@ def test_fails_with_number_message():
assert str(e.value) == expected_error
def test_adds_other_supplied_values_as_query_string():
app = Sanic('passes')
def test_adds_other_supplied_values_as_query_string(app):
@app.route(COMPLEX_PARAM_URL)
def passes():
def passes(request):
return text('this should pass')
new_kwargs = dict(PASSING_KWARGS)
@@ -205,14 +217,13 @@ def test_adds_other_supplied_values_as_query_string():
@pytest.fixture
def blueprint_app():
app = Sanic('blueprints')
def blueprint_app(app):
first_print = Blueprint('first', url_prefix='/first')
second_print = Blueprint('second', url_prefix='/second')
@first_print.route('/foo')
def foo():
def foo(request):
return text('foo from first')
@first_print.route('/foo/<param>')
@@ -221,7 +232,7 @@ def blueprint_app():
'foo from first : {}'.format(param))
@second_print.route('/foo') # noqa
def foo():
def foo(request):
return text('foo from second')
@second_print.route('/foo/<param>') # noqa
@@ -252,8 +263,7 @@ def test_blueprints_work_with_params(blueprint_app):
@pytest.fixture
def methodview_app():
app = Sanic('methodview')
def methodview_app(app):
class ViewOne(HTTPMethodView):
def get(self, request):

View File

@@ -3,7 +3,6 @@ import os
import pytest
from sanic import Sanic
from sanic.blueprints import Blueprint
@@ -26,9 +25,9 @@ def get_file_content(static_file_directory, file_name):
return file.read()
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_static_file(static_file_directory, file_name):
app = Sanic('test_static')
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png'])
def test_static_file(app, static_file_directory, file_name):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name))
app.static(
@@ -102,9 +101,7 @@ def test_static_file(static_file_directory, file_name):
@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')
def test_static_directory(app, file_name, base_uri, static_file_directory):
app.static(base_uri, static_file_directory)
base_uri2 = base_uri + '/2'
app.static(base_uri2, static_file_directory, name='uploads')
@@ -156,10 +153,8 @@ def test_static_directory(file_name, base_uri, static_file_directory):
assert response.body == get_file_content(static_file_directory, file_name)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_head_request(file_name, static_file_directory):
app = Sanic('test_static')
def test_static_head_request(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
@@ -198,8 +193,7 @@ def test_static_head_request(file_name, static_file_directory):
@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')
def test_static_content_range_correct(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
@@ -218,11 +212,11 @@ def test_static_content_range_correct(file_name, static_file_directory):
assert uri == app.url_for('static', name='static', filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert response.status == 206
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[12:19]
static_file_directory, file_name))[12:20]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
@@ -239,19 +233,18 @@ def test_static_content_range_correct(file_name, static_file_directory):
filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert response.status == 206
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
static_file_directory, file_name))[12:19]
static_file_directory, file_name))[12:20]
assert int(response.headers[
'Content-Length']) == len(static_content)
assert response.body == static_content
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_static_content_range_front(file_name, static_file_directory):
app = Sanic('test_static')
def test_static_content_range_front(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
@@ -270,7 +263,7 @@ def test_static_content_range_front(file_name, static_file_directory):
assert uri == app.url_for('static', name='static', filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert response.status == 206
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
@@ -291,7 +284,7 @@ def test_static_content_range_front(file_name, static_file_directory):
filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert response.status == 206
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
@@ -302,8 +295,7 @@ def test_static_content_range_front(file_name, static_file_directory):
@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')
def test_static_content_range_back(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
@@ -322,7 +314,7 @@ def test_static_content_range_back(file_name, static_file_directory):
assert uri == app.url_for('static', name='static', filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert response.status == 206
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
@@ -343,7 +335,7 @@ def test_static_content_range_back(file_name, static_file_directory):
filename='any')
request, response = app.test_client.get(uri, headers=headers)
assert response.status == 200
assert response.status == 206
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(get_file_content(
@@ -354,8 +346,7 @@ def test_static_content_range_back(file_name, static_file_directory):
@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')
def test_static_content_range_empty(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)
@@ -374,8 +365,8 @@ def test_static_content_range_empty(file_name, static_file_directory):
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 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))
@@ -394,15 +385,14 @@ def test_static_content_range_empty(file_name, static_file_directory):
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 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')
def test_static_content_range_error(app, file_name, static_file_directory):
app.static(
'/testing.file', get_file_path(static_file_directory, file_name),
use_content_range=True)

View File

@@ -1,14 +1,12 @@
from json import loads as json_loads, dumps as json_dumps
from sanic import Sanic
from sanic.response import json, text
from json import dumps as json_dumps
from sanic.response import text
# ------------------------------------------------------------ #
# UTF-8
# ------------------------------------------------------------ #
def test_utf8_query_string():
app = Sanic('test_utf8_query_string')
def test_utf8_query_string(app):
@app.route('/')
async def handler(request):
@@ -18,8 +16,7 @@ def test_utf8_query_string():
assert request.args.get('utf8') == ''
def test_utf8_response():
app = Sanic('test_utf8_response')
def test_utf8_response(app):
@app.route('/')
async def handler(request):
@@ -29,8 +26,7 @@ def test_utf8_response():
assert response.text == ''
def skip_test_utf8_route():
app = Sanic('skip_test_utf8_route')
def skip_test_utf8_route(app):
@app.route('/')
async def handler(request):
@@ -41,8 +37,7 @@ def skip_test_utf8_route():
assert response.text == 'OK'
def test_utf8_post_json():
app = Sanic('test_utf8_post_json')
def test_utf8_post_json(app):
@app.route('/')
async def handler(request):

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