Compare commits

...

164 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
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
7
a7dd73c657 Merge pull request #23 from channelcat/master
py37 (#1256)
2018-07-03 22:12:02 -07:00
7
f770e16f6d Merge pull request #22 from channelcat/master
merge upstream master branch
2018-06-26 23:33:35 -07:00
7
c1222175b3 Merge pull request #21 from channelcat/master
remote tracking
2018-06-10 20:17:27 -07:00
7
7928b9b3a2 Merge pull request #20 from channelcat/master
merge upstream master branch
2018-04-29 21:50:07 -07:00
86 changed files with 4462 additions and 2109 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

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

@@ -21,17 +21,21 @@ matrix:
python: 3.7
dist: xenial
sudo: true
- env: TOX_ENV=flake8
- env: TOX_ENV=lint
python: 3.6
- env: TOX_ENV=check
python: 3.6
install: pip install -U tox
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,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
-------------------
@@ -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
--------------

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

View File

@@ -21,8 +21,11 @@ Guides
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,7 @@ 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
@@ -87,7 +87,7 @@ from sanic import Blueprint
from .static import static
from .authors import authors
content = Blueprint.group(assets, authors, url_prefix='/content')
content = Blueprint.group(static, authors, url_prefix='/content')
```
```python
# api/info.py
@@ -254,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

@@ -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

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

@@ -47,6 +47,36 @@ 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

@@ -8,6 +8,7 @@ A list of Sanic extensions created by the community.
- [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.
- [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.
@@ -31,4 +32,5 @@ A list of Sanic extensions created by the community.
- [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

@@ -17,7 +17,7 @@ string representing its type: `'request'` or `'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")
@@ -33,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')
@@ -60,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')
@@ -79,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()
@@ -101,16 +101,16 @@ async def close_db(app, loop):
await app.db.close()
```
It's also possible to register a listener using the `register_listener` method.
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.
```python
```
app = Sanic()
async def setup_db(app, loop):
app.db = await db_setup()
app.register_listener(setup_db, 'before_server_start')
```
@@ -118,7 +118,7 @@ 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!')
@@ -128,7 +128,7 @@ 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:
```python
```
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)
@@ -138,7 +138,7 @@ app.add_task(notify_server_started_after_five_seconds)
Or you can pass the app explicitly for the same effect:
```python
```
async def notify_server_started_after_five_seconds(app):
await asyncio.sleep(5)
print(app.name)

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

@@ -43,6 +43,7 @@ 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

View File

@@ -12,9 +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
- websockets>=6.0
- sphinxcontrib-asyncio>=0.2.0
- multidict>=4.0,<5.0
- https://github.com/channelcat/docutils-fork/zipball/master

2
pyproject.toml Normal file
View File

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

View File

@@ -3,7 +3,7 @@ aiohttp>=2.3.0,<=3.2.1
chardet<=2.3.0
beautifulsoup4
coverage
httptools
httptools>=0.0.10
flake8
pytest==3.3.2
tox

View File

@@ -1,6 +1,6 @@
aiofiles
httptools
httptools>=0.0.10
ujson; sys_platform != "win32" and implementation_name == "cpython"
uvloop; sys_platform != "win32" and implementation_name == "cpython"
websockets>=5.0,<6.0
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.8.1'
__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,21 +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,13 +58,14 @@ class Blueprint:
self.strict_slashes = strict_slashes
@staticmethod
def group(*blueprints, url_prefix=''):
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:
@@ -52,8 +73,11 @@ class Blueprint:
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
@@ -61,7 +85,7 @@ class Blueprint:
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:
@@ -73,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
@@ -88,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)
@@ -111,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.
@@ -131,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,
@@ -152,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:
@@ -166,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.
@@ -181,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,
@@ -204,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)
@@ -226,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):
@@ -238,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)
@@ -251,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):
@@ -63,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):
@@ -74,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
@@ -53,7 +52,7 @@ class CookieJar(dict):
# If this cookie doesn't exist, add it to the header keys
if not self.cookie_headers.get(key):
cookie = Cookie(key, value)
cookie['path'] = '/'
cookie["path"] = "/"
self.cookie_headers[key] = self.header_key
self.headers.add(self.header_key, cookie)
return super().__setitem__(key, cookie)
@@ -62,8 +61,8 @@ class CookieJar(dict):
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]
# remove it from header
@@ -77,6 +76,7 @@ class CookieJar(dict):
class Cookie(dict):
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
_keys = {
"expires": "expires",
"path": "Path",
@@ -88,7 +88,7 @@ class Cookie(dict):
"version": "Version",
"samesite": "SameSite",
}
_flags = {'secure', 'httponly'}
_flags = {"secure", "httponly"}
def __init__(self, key, value):
if key in self._keys:
@@ -106,24 +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)

View File

@@ -1,6 +1,7 @@
from sanic.http 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)
@@ -156,8 +158,8 @@ class MethodNotSupported(SanicException):
super().__init__(message)
self.headers = dict()
self.headers["Allow"] = ", ".join(allowed_methods)
if method in ['HEAD', 'PATCH', 'PUT', 'DELETE']:
self.headers['Content-Length'] = 0
if method in ["HEAD", "PATCH", "PUT", "DELETE"]:
self.headers["Content-Length"] = 0
@add_status_code(500)
@@ -169,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
@@ -192,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
@@ -209,8 +213,8 @@ class ContentRangeError(SanicException):
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,),
}
@@ -223,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):
"""
@@ -258,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}="{!s}"'.format(k, v) for k, v in kwargs.items()]
challenge = ', '.join(values)
challenge = ", ".join(values)
self.headers = {
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
@@ -283,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):
@@ -84,40 +88,45 @@ class ErrorHandler:
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

@@ -1,128 +0,0 @@
"""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)
and header.lower() not 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")

View File

@@ -1,9 +1,10 @@
import os
import sys
import signal
import subprocess
from time import sleep
import sys
from multiprocessing import Process
from time import sleep
def _iter_module_files():
@@ -18,7 +19,7 @@ def _iter_module_files():
for module in list(sys.modules.values()):
if module is None:
continue
filename = getattr(module, '__file__', None)
filename = getattr(module, "__file__", None)
if filename:
old = None
while not os.path.isfile(filename):
@@ -27,7 +28,7 @@ def _iter_module_files():
if filename == old:
break
else:
if filename[-4:] in ('.pyc', '.pyo'):
if filename[-4:] in (".pyc", ".pyo"):
filename = filename[:-1]
yield filename
@@ -45,11 +46,13 @@ def restart_with_reloader():
"""
args = _get_args_for_reloading()
new_environ = os.environ.copy()
new_environ['SANIC_SERVER_RUNNING'] = 'true'
cmd = ' '.join(args)
new_environ["SANIC_SERVER_RUNNING"] = "true"
cmd = " ".join(args)
worker_process = Process(
target=subprocess.call, args=(cmd,),
kwargs=dict(shell=True, env=new_environ))
target=subprocess.call,
args=(cmd,),
kwargs=dict(shell=True, env=new_environ),
)
worker_process.start()
return worker_process
@@ -67,8 +70,10 @@ def kill_process_children_unix(pid):
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)
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:
@@ -90,7 +95,7 @@ def kill_process_children_osx(pid):
:param pid: PID of parent process (process ID)
:return: Nothing
"""
subprocess.run(['pkill', '-P', str(pid)])
subprocess.run(["pkill", "-P", str(pid)])
def kill_process_children(pid):
@@ -99,12 +104,12 @@ def kill_process_children(pid):
:param pid: PID of parent process (process ID)
:return: Nothing
"""
if sys.platform == 'darwin':
if sys.platform == "darwin":
kill_process_children_osx(pid)
elif sys.platform == 'linux':
elif sys.platform == "linux":
kill_process_children_unix(pid)
else:
pass # should signal error here
pass # should signal error here
def kill_program_completly(proc):
@@ -127,9 +132,11 @@ def watchdog(sleep_interval):
mtimes = {}
worker_process = restart_with_reloader()
signal.signal(
signal.SIGTERM, lambda *args: kill_program_completly(worker_process))
signal.SIGTERM, lambda *args: kill_program_completly(worker_process)
)
signal.signal(
signal.SIGINT, lambda *args: kill_program_completly(worker_process))
signal.SIGINT, lambda *args: kill_program_completly(worker_process)
)
while True:
for filename in _iter_module_files():
try:

View File

@@ -1,24 +1,29 @@
import sys
import json
import socket
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, logger
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
@@ -44,11 +49,28 @@ 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', '__weakref__', 'raw_url'
"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):
@@ -63,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
@@ -74,16 +96,25 @@ 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):
if self.parsed_json is None:
@@ -107,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:
@@ -123,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")
@@ -151,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
@@ -163,47 +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'):
if not hasattr(self, "_socket"):
self._get_address()
return self._socket
def _get_address(self):
sock = self.transport.get_extra_info('socket')
if sock.family == socket.AF_INET:
self._socket = (self.transport.get_extra_info('peername') or
(None, None))
self._ip, self._port = self._socket
elif sock.family == socket.AF_INET6:
self._socket = (self.transport.get_extra_info('peername') or
(None, None, None, None))
self._ip, self._port, *_ = self._socket
else:
self._ip, self._port = (None, None)
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):
@@ -211,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
@@ -241,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):
@@ -254,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):
@@ -290,37 +318,38 @@ def parse_multipart_form(body, boundary):
form_parts = body.split(boundary)
for form_part in form_parts[1:-1]:
file_name = None
content_type = 'text/plain'
content_charset = 'utf-8'
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':
file_name = form_parameters.get('filename')
field_name = form_parameters.get('name')
elif form_header_field == 'content-type':
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')
content_charset = form_parameters.get("charset", "utf-8")
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)
form_file = File(
type=content_type, name=file_name, body=post_data
)
if field_name in files:
files[field_name].append(form_file)
else:
@@ -332,7 +361,9 @@ def parse_multipart_form(body, boundary):
else:
fields[field_name] = [value]
else:
logger.debug('Form-data field does not have a \'name\' parameter \
in the Content-Disposition header')
logger.debug(
"Form-data field does not have a 'name' parameter "
"in the Content-Disposition header"
)
return fields, files

View File

@@ -1,17 +1,23 @@
from functools import partial
from mimetypes import guess_type
from os import path
from urllib.parse import quote_plus
try:
from ujson import dumps as json_dumps
except BaseException:
from json import dumps as json_dumps
from aiofiles import open as open_async
from multidict import CIMultiDict
from sanic import http
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
# 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:
@@ -24,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
@@ -46,12 +54,17 @@ class BaseHTTPResponse:
class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = (
'protocol', '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
@@ -66,61 +79,69 @@ class StreamingHTTPResponse(BaseHTTPResponse):
if type(data) != bytes:
data = self._encode_body(data)
self.protocol.push_data(
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)
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.protocol.push_data(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 = http.STATUS_CODES.get(self.status)
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:
@@ -132,46 +153,45 @@ class HTTPResponse(BaseHTTPResponse):
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
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
body = b''
if http.has_message_body(self.status):
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-Length"] = self.headers.get(
"Content-Length", len(self.body)
)
self.headers['Content-Type'] = self.headers.get(
'Content-Type', self.content_type)
self.headers["Content-Type"] = self.headers.get(
"Content-Type", self.content_type
)
if self.status in (304, 412):
self.headers = http.remove_entity_headers(self.headers)
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 = http.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,
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):
@@ -180,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.
@@ -191,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.
@@ -206,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.
@@ -220,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):
@@ -232,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, status=200, mime_type=None, headers=None,
filename=None, _range=None):
async def file(
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.
@@ -249,28 +294,41 @@ async def file(location, status=200, mime_type=None, headers=None,
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=status,
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, status=200, chunk_size=4096, mime_type=None,
headers=None, filename=None, _range=None):
async def file_stream(
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.
@@ -283,11 +341,11 @@ async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
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
@@ -312,19 +370,28 @@ async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
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=status,
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`.
@@ -344,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
@@ -364,12 +429,11 @@ def redirect(to, headers=None, status=302,
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/#?&=@[]!$&'()*+,;")
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers['Location'] = safe_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,33 +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, MethodNotSupported
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'[^/].*?'),
'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}')
"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):
@@ -38,6 +43,10 @@ class RouteDoesNotExist(Exception):
pass
class ParameterNameConflicts(Exception):
pass
class Router:
"""Router supports basic routing with parameters and method checks
@@ -64,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 = {}
@@ -94,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)
@@ -108,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
@@ -123,8 +141,8 @@ 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)
@@ -139,28 +157,26 @@ class Router:
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)
_without_slash_is_missing = all(method in unslashed_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
_without_slash_is_missing 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)
@@ -183,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)
@@ -195,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:
@@ -236,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, parameters)
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, parameters)
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)
@@ -259,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)
@@ -322,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)
@@ -342,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))
@@ -356,14 +393,15 @@ 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.
@@ -373,7 +411,7 @@ class Router:
"""
route = self.routes_all.get(url)
# if methods are None then this logic will prevent an error
return getattr(route, 'methods', None) or frozenset()
return getattr(route, "methods", None) or frozenset()
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get(self, url, method, host):
@@ -388,9 +426,10 @@ class Router:
# Check against known static routes
route = self.routes_static.get(url)
method_not_supported = MethodNotSupported(
'Method {} not allowed for URL {}'.format(method, url),
"Method {} not allowed for URL {}".format(method, url),
method=method,
allowed_methods=self.get_supported_methods(url))
allowed_methods=self.get_supported_methods(url),
)
if route:
if route.methods and method not in route.methods:
raise method_not_supported
@@ -416,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
@@ -435,7 +475,8 @@ class Router:
handler = self.get(request)[0]
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,40 +1,38 @@
import asyncio
import os
import traceback
from functools import partial
from inspect import isawaitable
from multiprocessing import Process
from signal import (
SIGTERM, SIGINT, SIG_IGN,
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
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
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
class Signal:
stopped = False
@@ -43,27 +41,65 @@ class Signal:
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',
'_not_paused')
"_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
@@ -93,19 +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
@@ -114,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:
@@ -138,55 +180,46 @@ class HttpProtocol(asyncio.Protocol):
# 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:
if self._request_stream_task:
self._request_stream_task.cancel()
if self._request_handler_task:
self._request_handler_task.cancel()
try:
raise ServiceUnavailable('Response Timeout')
except ServiceUnavailable as exception:
self.write_error(exception)
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.debug('KeepAlive Timeout. Closing connection.')
logger.debug("KeepAlive Timeout. Closing connection.")
self.transport.close()
self.transport = None
@@ -199,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:
@@ -209,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:
@@ -231,18 +262,20 @@ 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(
@@ -250,7 +283,7 @@ class HttpProtocol(asyncio.Protocol):
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.
@@ -259,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()
@@ -267,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.
@@ -279,47 +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}:{1}'.format(self.request.ip,
self.request.port)
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):
"""
@@ -332,32 +369,38 @@ 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):
@@ -380,31 +423,37 @@ class HttpProtocol(asyncio.Protocol):
keep_alive = self.keep_alive
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):
@@ -416,35 +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)
try:
self.transport.close()
except AttributeError as e:
logger.debug('Connection lost before server could close it.')
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):
@@ -503,17 +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_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):
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
@@ -554,6 +633,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
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:
@@ -596,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,
@@ -627,11 +708,13 @@ def serve(host, port, request_handler, error_handler, before_start=None,
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)
@@ -662,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()
)
coros.append(conn.websocket.close_connection())
else:
conn.close()
@@ -685,18 +766,18 @@ 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['run_multiple'] = 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)
@@ -720,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, content_type=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
@@ -46,12 +55,12 @@ def register(app, uri, file_or_directory, pattern,
# 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
@@ -59,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
@@ -76,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
headers['Content-Type'] = content_type \
or guess_type(file_path)[0] or 'text/plain'
if request.method == 'HEAD':
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
@@ -110,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,11 +1,11 @@
import traceback
from json import JSONDecodeError
from sanic.log import logger
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
@@ -16,70 +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={"auto_reload": False},
*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.exception(MethodNotSupported)
async def error_handler(request, exception):
if request.method in ['HEAD', 'PATCH', 'PUT', 'DELETE']:
if request.method in ["HEAD", "PATCH", "PUT", "DELETE"]:
return text(
'', exception.status_code, headers=exception.headers
"", exception.status_code, headers=exception.headers
)
else:
return self.app.error_handler.default(request, exception)
@self.app.listener('after_server_start')
@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))
@@ -91,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,16 +1,22 @@
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_timeout=10,
websocket_max_size=None,
websocket_max_queue=None,
websocket_read_limit=2 ** 16,
websocket_write_limit=2 ** 16, **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
@@ -57,36 +63,32 @@ 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
@@ -95,7 +97,7 @@ class WebSocketProtocol(HttpProtocol):
max_size=self.websocket_max_size,
max_queue=self.websocket_max_queue,
read_limit=self.websocket_read_limit,
write_limit=self.websocket_write_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()
)
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.')
@@ -48,6 +48,7 @@ setup_kwargs = {
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
],
}
@@ -56,11 +57,11 @@ 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>=5.0,<6.0',
'websockets>=6.0,<7.0',
'multidict>=4.0,<5.0',
]
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):

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)

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,30 +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()
@@ -36,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)
@@ -46,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('/')
@@ -60,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
@@ -87,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)
@@ -107,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)
@@ -127,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)
@@ -147,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('/')
@@ -161,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')
@@ -182,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)
@@ -209,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")
@@ -223,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)
@@ -253,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')
@@ -272,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')
@@ -297,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'
@@ -305,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 = []
@@ -339,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)
@@ -357,14 +365,14 @@ def test_bp_static():
assert response.status == 200
assert response.body == current_file_contents
@pytest.mark.parametrize('file_name', ['test.html'])
def test_bp_static_content_type(file_name):
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')
app = Sanic('test_static')
blueprint = Blueprint('test_static')
blueprint.static(
'/testing.file',
@@ -379,8 +387,8 @@ def test_bp_static_content_type(file_name):
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 = Sanic('test_shorhand_routes')
def test_bp_shorthand(app):
blueprint = Blueprint('test_shorhand_routes')
ev = asyncio.Event()
@@ -390,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()
@@ -470,7 +478,7 @@ 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==',
@@ -478,25 +486,24 @@ def test_bp_shorthand():
assert response.status == 101
assert ev.is_set()
def test_bp_group():
app = Sanic('test_nested_bp_groups')
def test_bp_group(app):
deep_0 = Blueprint('deep_0', url_prefix='/deep')
deep_1 = Blueprint('deep_1', url_prefix = '/deep1')
deep_1 = Blueprint('deep_1', url_prefix='/deep1')
@deep_0.route('/')
def handler(request):
return text('D0_OK')
@deep_1.route('/bottom')
def handler(request):
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 handler(request):
def handler1(request):
return text('M1_OK')
top = Blueprint.group(mid_0, mid_1)
@@ -504,7 +511,7 @@ def test_bp_group():
app.blueprint(top)
@app.route('/')
def handler(request):
def handler2(request):
return text('TOP_OK')
request, response = app.test_client.get('/')
@@ -518,3 +525,182 @@ def test_bp_group():
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):
@@ -30,8 +28,7 @@ def test_cookies():
(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):
@@ -49,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):
@@ -65,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):
@@ -78,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('/')
@@ -94,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):
@@ -113,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,18 +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')
@@ -30,8 +28,8 @@ def test_create_task():
request, response = app.test_client.get('/late')
assert response.body == b'True'
def test_create_task_with_app_arg():
app = Sanic('test_add_task')
def test_create_task_with_app_arg(app):
q = Queue()
@app.route('/')
@@ -44,4 +42,4 @@ def test_create_task_with_app_arg():
app.add_task(coro)
request, response = app.test_client.get('/')
assert q.get() == 'test_add_task'
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):
@@ -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

@@ -9,60 +9,22 @@ import aiohttp
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST, PORT
try:
try:
import packaging # direct use
except ImportError:
# 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 ReuseableTCPConnector(TCPConnector):
def __init__(self, *args, **kwargs):
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
self.old_proto = None
if aiohttp_version >= version.parse('3.3.0'):
async def connect(self, req, traces, timeout):
new_conn = await super(ReuseableTCPConnector, self)\
.connect(req, traces, timeout)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
raise RuntimeError(
"We got a new connection, wanted the same one!")
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
elif aiohttp_version >= version.parse('3.0.0'):
async def connect(self, req, traces=None):
new_conn = await super(ReuseableTCPConnector, self)\
.connect(req, traces=traces)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
raise RuntimeError(
"We got a new connection, wanted the same one!")
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
else:
async def connect(self, req):
new_conn = await super(ReuseableTCPConnector, self)\
.connect(req)
if self.old_proto is not None:
if self.old_proto != new_conn._protocol:
raise RuntimeError(
"We got a new connection, wanted the same one!")
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
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(
"We got a new connection, wanted the same one!")
print(new_conn.__dict__)
self.old_proto = new_conn._protocol
return new_conn
class ReuseableSanicTestClient(SanicTestClient):
@@ -147,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))
@@ -178,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,

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)
@@ -104,3 +103,33 @@ def test_log_connection_lost(debug, monkeypatch):
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):
@@ -23,3 +29,58 @@ def test_multiprocessing():
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,15 +20,15 @@ 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')
@@ -109,3 +108,25 @@ def test_redirect_with_header_injection(redirect_app):
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
@@ -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)
@@ -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')
@@ -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):

View File

@@ -5,13 +5,15 @@ import asyncio
from sanic.response import text
from sanic.config import Config
import aiohttp
from aiohttp import TCPConnector, ClientResponse
from sanic.testing import SanicTestClient, HOST, PORT
from aiohttp import TCPConnector
from sanic.testing import SanicTestClient, HOST
try:
try:
import packaging # direct use
except ImportError:
# direct use
import packaging
version = packaging.version
except (ImportError, AttributeError):
# setuptools v39.0 and above.
try:
from setuptools.extern import packaging
@@ -22,8 +24,10 @@ try:
except ImportError:
raise RuntimeError("The 'packaging' library is missing.")
aiohttp_version = version.parse(aiohttp.__version__)
class DelayableTCPConnector(TCPConnector):
class RequestContextManager(object):
@@ -56,8 +60,7 @@ class DelayableTCPConnector(TCPConnector):
if aiohttp_version >= version.parse("3.3.0"):
ret = await self.orig_start(connection)
else:
ret = await self.orig_start(connection,
read_until_eof)
ret = await self.orig_start(connection, read_until_eof)
except Exception as e:
raise e
return ret
@@ -71,57 +74,43 @@ class DelayableTCPConnector(TCPConnector):
async def delayed_send(self, *args, **kwargs):
req = self.req
if self.delay and self.delay > 0:
#sync_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
next(iter(args)) # first arg is connection
if aiohttp_version >= version.parse("3.1.0"):
try:
delayed_resp = await self.orig_send(*args, **kwargs)
except Exception as e:
if aiohttp_version >= version.parse("3.3.0"):
return aiohttp.ClientResponse(req.method, req.url,
writer=None,
continue100=None,
timer=None,
request_info=None,
traces=[],
loop=req.loop,
session=None)
else:
return aiohttp.ClientResponse(req.method, req.url,
writer=None,
continue100=None,
timer=None,
request_info=None,
auto_decompress=None,
traces=[],
loop=req.loop,
session=None)
else:
try:
delayed_resp = self.orig_send(*args, **kwargs)
except Exception as e:
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)
return delayed_resp
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):
gen = self.delayed_send(*args, **kwargs)
task = self.req.loop.create_task(gen)
self.send_task = task
self._acting_as = task
return self
return self._send(*args, **kwargs)
else:
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
send = _send
def __init__(self, *args, **kwargs):
_post_connect_delay = kwargs.pop('post_connect_delay', 0)
@@ -130,45 +119,18 @@ class DelayableTCPConnector(TCPConnector):
self._post_connect_delay = _post_connect_delay
self._pre_request_delay = _pre_request_delay
if aiohttp_version >= version.parse("3.3.0"):
async def connect(self, req, traces, timeout):
d_req = DelayableTCPConnector.\
RequestContextManager(req, self._pre_request_delay)
conn = await super(DelayableTCPConnector, self).\
connect(req, traces, timeout)
if self._post_connect_delay and self._post_connect_delay > 0:
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)
return conn
elif aiohttp_version >= version.parse("3.0.0"):
async def connect(self, req, traces=None):
d_req = DelayableTCPConnector.\
RequestContextManager(req, self._pre_request_delay)
conn = await super(DelayableTCPConnector, self).\
connect(req, traces=traces)
if self._post_connect_delay and self._post_connect_delay > 0:
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)
return conn
else:
async def connect(self, req):
d_req = DelayableTCPConnector.\
RequestContextManager(req, self._pre_request_delay)
conn = await super(DelayableTCPConnector, self).connect(req)
if self._post_connect_delay and self._post_connect_delay > 0:
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)
return conn
async def connect(self, req, *args, **kwargs):
d_req = DelayableTCPConnector.\
RequestContextManager(req, self._pre_request_delay)
conn = await super(DelayableTCPConnector, self).\
connect(req, *args, **kwargs)
if self._post_connect_delay and self._post_connect_delay > 0:
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)
return conn
class DelayableSanicTestClient(SanicTestClient):
@@ -187,7 +149,7 @@ class DelayableSanicTestClient(SanicTestClient):
url = 'http://{host}:{port}{uri}'.format(
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
@@ -211,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')
@@ -226,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,11 +98,10 @@ 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):
@@ -118,8 +113,7 @@ def test_empty_json():
assert response.text == 'null'
def test_invalid_json():
app = Sanic('test_json')
def test_invalid_json(app):
@app.route('/')
async def handler(request):
@@ -131,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):
@@ -145,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):
@@ -156,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):
@@ -204,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):
@@ -223,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):
@@ -249,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):
@@ -266,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):
@@ -280,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):
@@ -293,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):
@@ -331,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')
@@ -356,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(
@@ -381,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')
@@ -36,12 +36,9 @@ async def sample_streaming_fn(response):
await response.write('bar')
def test_method_not_allowed():
app = Sanic('method_not_allowed')
def test_method_not_allowed(app):
@app.get('/')
async def test(request):
async def test_get(request):
return response.json({'hello': 'world'})
request, response = app.test_client.head('/')
@@ -50,24 +47,23 @@ def test_method_not_allowed():
request, response = app.test_client.post('/')
assert response.headers['Allow'] == 'GET'
@app.post('/')
async def test(request):
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(', ')) == set(['GET', 'POST'])
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(', ')) == set(['GET', 'POST'])
assert set(response.headers['Allow'].split(', ')) == {'GET', 'POST'}
assert response.headers['Content-Length'] == '0'
def test_response_header():
app = Sanic('test_response_header')
def test_response_header(app):
@app.get('/')
async def test(request):
return json({
@@ -79,15 +75,61 @@ def test_response_header():
request, response = app.test_client.get('/')
assert dict(response.headers) == {
'Connection': 'keep-alive',
'Keep-Alive': '2',
'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):
@@ -145,8 +187,7 @@ def test_no_content(json_app):
@pytest.fixture
def streaming_app():
app = Sanic('streaming')
def streaming_app(app):
@app.route("/")
async def test(request):
@@ -223,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"""
@@ -238,10 +302,10 @@ def get_file_content(static_file_directory, file_name):
return file.read()
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', 'python.png'])
@pytest.mark.parametrize('status', [200, 401])
def test_file_response(file_name, static_file_directory, status):
app = Sanic('test_file_helper')
def test_file_response(app, file_name, static_file_directory, status):
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
@@ -256,10 +320,15 @@ def test_file_response(file_name, static_file_directory, status):
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):
@@ -270,12 +339,12 @@ 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):
@@ -302,9 +371,9 @@ def test_file_head_response(file_name, static_file_directory):
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):
@@ -319,10 +388,15 @@ def test_file_stream_response(file_name, static_file_directory):
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):
@@ -333,12 +407,12 @@ 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):
@@ -355,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
@@ -369,3 +445,42 @@ def test_file_stream_head_response(file_name, static_file_directory):
assert int(response.headers[
'Content-Length']) == len(
get_file_content(static_file_directory, file_name))
@pytest.mark.parametrize('file_name',
['test.file', 'decode me.txt', '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

@@ -1,20 +1,18 @@
import asyncio
import pytest
from sanic import Sanic
from sanic.response import text, json
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)
@@ -32,8 +30,7 @@ def test_versioned_routes_get(method):
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):
@@ -46,8 +43,7 @@ def test_shorthand_routes_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):
@@ -65,16 +61,15 @@ def test_shorthand_routes_multiple():
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')
@@ -93,9 +88,8 @@ def test_route_strict_slash():
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):
@@ -115,8 +109,7 @@ def test_route_strict_slash_default_value():
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):
@@ -137,15 +130,14 @@ def test_route_strict_slash_default_value_can_be_overwritten():
assert response.text == 'OK'
def test_route_slashes_overload():
app = Sanic('test_route_slashes_overload')
def test_route_slashes_overload(app):
@app.get('/hello/')
def handler(request):
def handler_get(request):
return text('OK')
@app.post('/hello/')
def handler(request):
def handler_post(request):
return text('OK')
request, response = app.test_client.get('/hello')
@@ -161,8 +153,7 @@ def test_route_slashes_overload():
assert response.text == 'OK'
def test_route_optional_slash():
app = Sanic('test_route_optional_slash')
def test_route_optional_slash(app):
@app.get('/get')
def handler(request):
@@ -174,43 +165,43 @@ def test_route_optional_slash():
request, response = app.test_client.get('/get/')
assert response.text == 'OK'
def test_route_strict_slashes_set_to_false_and_host_is_a_list():
#Part of regression test for issue #1120
app = Sanic('test_route_strict_slashes_set_to_false_and_host_is_a_list')
site1 = 'localhost:{}'.format(app.test_client.port)
def test_route_strict_slashes_set_to_false_and_host_is_a_list(app):
# Part of regression test for issue #1120
#before fix, this raises a RouteExists error
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 handler(request):
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 handler(request):
def post_handler(request):
return text('OK')
request, response = app.test_client.post('http://' + site1 +'/post')
request, response = app.test_client.post('http://' + site1 + '/post')
assert response.text == 'OK'
@app.put('/put', host=[site1, 'site2.com'], strict_slashes=False)
def handler(request):
def put_handler(request):
return text('OK')
request, response = app.test_client.put('http://' + site1 +'/put')
request, response = app.test_client.put('http://' + site1 + '/put')
assert response.text == 'OK'
@app.delete('/delete', host=[site1, 'site2.com'], strict_slashes=False)
def handler(request):
def delete_handler(request):
return text('OK')
request, response = app.test_client.delete('http://' + site1 +'/delete')
request, response = app.test_client.delete('http://' + site1 + '/delete')
assert response.text == 'OK'
def test_shorthand_routes_post():
app = Sanic('test_shorhand_routes_post')
def test_shorthand_routes_post(app):
@app.post('/post')
def handler(request):
@@ -223,8 +214,7 @@ def test_shorthand_routes_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):
@@ -240,8 +230,7 @@ def test_shorthand_routes_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):
@@ -257,8 +246,7 @@ def test_shorthand_routes_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):
@@ -274,8 +262,7 @@ def test_shorthand_routes_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):
@@ -291,8 +278,7 @@ def test_shorthand_routes_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):
@@ -308,8 +294,7 @@ def test_shorthand_routes_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):
@@ -326,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>')
@@ -342,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>')
@@ -363,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>')
@@ -381,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>')
@@ -402,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):
@@ -422,9 +398,8 @@ def test_dynamic_route_regex():
assert response.status == 200
def test_dynamic_route_uuid():
def test_dynamic_route_uuid(app):
import uuid
app = Sanic('test_dynamic_route_uuid')
results = []
@@ -433,7 +408,8 @@ def test_dynamic_route_uuid():
results.append(unique_id)
return text('OK')
request, response = app.test_client.get('/quirky/123e4567-e89b-12d3-a456-426655440000')
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
@@ -444,8 +420,7 @@ def test_dynamic_route_uuid():
assert response.status == 404
def test_dynamic_route_path():
app = Sanic('test_dynamic_route_path')
def test_dynamic_route_path(app):
@app.route('/<path:path>/info')
async def handler(request, path):
@@ -468,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):
@@ -488,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()
@@ -506,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'])
@@ -548,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')
@@ -562,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):
@@ -584,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')
@@ -593,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'
@@ -603,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 = []
@@ -619,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 = []
@@ -640,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):
@@ -659,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):
@@ -681,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')
@@ -702,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')
@@ -723,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):
@@ -747,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')
@@ -762,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')
@@ -790,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')
@@ -806,15 +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)
assert str(excinfo.value) == 'Route was not registered: {}'.format(uri)
def test_removing_slash():
app = Sanic(__name__)
def test_removing_slash(app):
@app.get('/rest/<resource>')
def get(_):
@@ -827,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')
@@ -856,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')
@@ -884,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):
@@ -913,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):
@@ -932,12 +907,12 @@ def test_unmergeable_overload_routes():
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')
@@ -947,8 +922,7 @@ def test_unmergeable_overload_routes():
assert response.status == 405
def test_unicode_routes():
app = Sanic('test_unicode_routes')
def test_unicode_routes(app):
@app.get('/你好')
def handler1(request):
@@ -965,8 +939,7 @@ def test_unicode_routes():
assert response.text == 'OK2 你好'
def test_uri_with_different_method_and_different_params():
app = Sanic('test_uri')
def test_uri_with_different_method_and_different_params(app):
@app.route('/ads/<ad_id>', methods=['GET'])
async def ad_get(request, ad_id):
@@ -987,3 +960,24 @@ def test_uri_with_different_method_and_different_params():
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,55 +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()
@skipif_no_alarm
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
def test_register_listener(listener_name):
def test_register_listener(app, listener_name):
"""
Test that listeners on their own work with
app.register_listener method
"""
random_name_app = Sanic(''.join(
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
output = list()
output = []
# Register listener
listener = create_listener(listener_name, output)
random_name_app.register_listener(listener,
event=listener_name)
start_stop_app(random_name_app)
assert random_name_app.name + listener_name == output.pop()
app.register_listener(listener, event=listener_name)
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
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,4 +1,3 @@
from sanic import Sanic
from sanic.response import HTTPResponse
from sanic.testing import HOST, PORT
from unittest.mock import MagicMock
@@ -12,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):
@@ -31,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):
@@ -47,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))
@@ -37,8 +69,7 @@ def test_static_file(static_file_directory, file_name):
@pytest.mark.parametrize('file_name', ['test.html'])
def test_static_file_content_type(static_file_directory, file_name):
app = Sanic('test_static')
def test_static_file_content_type(app, static_file_directory, file_name):
app.static(
'/testing.file',
get_file_path(static_file_directory, file_name),
@@ -51,11 +82,9 @@ def test_static_file_content_type(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'])
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'symlink', 'hard_link'])
@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)
request, response = app.test_client.get(
@@ -65,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)
@@ -81,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)
@@ -91,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)
@@ -112,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(
@@ -123,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)
@@ -133,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(
@@ -143,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)
@@ -178,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),
@@ -193,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)
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,8 +68,26 @@ 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(request):
@@ -80,14 +99,12 @@ 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(request):
return text('this should fail')
@@ -103,8 +120,7 @@ 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(request):
@@ -126,8 +142,7 @@ 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(request):
@@ -140,13 +155,12 @@ 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(request):
@@ -165,8 +179,7 @@ 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(request):
@@ -185,8 +198,7 @@ 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(request):
@@ -205,8 +217,7 @@ 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')
@@ -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):

View File

@@ -1,16 +1,14 @@
from sanic import Sanic
from sanic.response import json, text
from sanic.response import text
def test_vhosts():
app = Sanic('test_vhosts')
def test_vhosts(app):
@app.route('/', host="example.com")
async def handler(request):
async def handler1(request):
return text("You're at example.com!")
@app.route('/', host="subdomain.example.com")
async def handler(request):
async def handler2(request):
return text("You're at subdomain.example.com!")
headers = {"Host": "example.com"}
@@ -22,8 +20,7 @@ def test_vhosts():
assert response.text == "You're at subdomain.example.com!"
def test_vhosts_with_list():
app = Sanic('test_vhosts')
def test_vhosts_with_list(app):
@app.route('/', host=["hello.com", "world.com"])
async def handler(request):
@@ -37,15 +34,15 @@ def test_vhosts_with_list():
request, response = app.test_client.get('/', headers=headers)
assert response.text == "Hello, world!"
def test_vhosts_with_defaults():
app = Sanic('test_vhosts')
def test_vhosts_with_defaults(app):
@app.route('/', host="hello.com")
async def handler(request):
async def handler1(request):
return text("Hello, world!")
@app.route('/')
async def handler(request):
async def handler2(request):
return text("default")
headers = {"Host": "hello.com"}
@@ -54,3 +51,18 @@ def test_vhosts_with_defaults():
request, response = app.test_client.get('/')
assert response.text == "default"
def test_remove_vhost_route(app):
@app.route('/', host="example.com")
async def handler1(request):
return text("You're at example.com!")
headers = {"Host": "example.com"}
request, response = app.test_client.get('/', headers=headers)
assert response.status == 200
app.remove_route('/', host="example.com")
request, response = app.test_client.get('/', headers=headers)
assert response.status == 404

View File

@@ -1,6 +1,5 @@
import pytest as pytest
from sanic import Sanic
from sanic.exceptions import InvalidUsage
from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView, CompositionView
@@ -10,8 +9,7 @@ from sanic.constants import HTTP_METHODS
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_methods(method):
app = Sanic('test_methods')
def test_methods(app, method):
class DummyView(HTTPMethodView):
@@ -44,8 +42,7 @@ def test_methods(method):
assert response.headers['method'] == method
def test_unexisting_methods():
app = Sanic('test_unexisting_methods')
def test_unexisting_methods(app):
class DummyView(HTTPMethodView):
@@ -59,8 +56,7 @@ def test_unexisting_methods():
assert response.text == 'Error: Method POST not allowed for URL /'
def test_argument_methods():
app = Sanic('test_argument_methods')
def test_argument_methods(app):
class DummyView(HTTPMethodView):
@@ -74,8 +70,7 @@ def test_argument_methods():
assert response.text == 'I am get method with test123'
def test_with_bp():
app = Sanic('test_with_bp')
def test_with_bp(app):
bp = Blueprint('test_text')
class DummyView(HTTPMethodView):
@@ -93,8 +88,7 @@ def test_with_bp():
assert response.text == 'I am get method'
def test_with_bp_with_url_prefix():
app = Sanic('test_with_bp_with_url_prefix')
def test_with_bp_with_url_prefix(app):
bp = Blueprint('test_text', url_prefix='/test1')
class DummyView(HTTPMethodView):
@@ -110,8 +104,7 @@ def test_with_bp_with_url_prefix():
assert response.text == 'I am get method'
def test_with_middleware():
app = Sanic('test_with_middleware')
def test_with_middleware(app):
class DummyView(HTTPMethodView):
@@ -132,13 +125,11 @@ def test_with_middleware():
assert type(results[0]) is Request
def test_with_middleware_response():
app = Sanic('test_with_middleware_response')
def test_with_middleware_response(app):
results = []
@app.middleware('request')
async def process_response(request):
async def process_request(request):
results.append(request)
@app.middleware('response')
@@ -161,8 +152,7 @@ def test_with_middleware_response():
assert isinstance(results[2], HTTPResponse)
def test_with_custom_class_methods():
app = Sanic('test_with_custom_class_methods')
def test_with_custom_class_methods(app):
class DummyView(HTTPMethodView):
global_var = 0
@@ -172,16 +162,15 @@ def test_with_custom_class_methods():
def get(self, request):
self._iternal_method()
return text('I am get method and global var is {}'.format(self.global_var))
return text('I am get method and global var '
'is {}'.format(self.global_var))
app.add_route(DummyView.as_view(), '/')
request, response = app.test_client.get('/')
assert response.text == 'I am get method and global var is 10'
def test_with_decorator():
app = Sanic('test_with_decorator')
def test_with_decorator(app):
results = []
def stupid_decorator(view):
@@ -227,9 +216,7 @@ def test_composition_view_rejects_duplicate_methods():
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_composition_view_runs_methods_as_expected(method):
app = Sanic('test_composition_view')
def test_composition_view_runs_methods_as_expected(app, method):
view = CompositionView()
def first(request):
@@ -245,15 +232,19 @@ def test_composition_view_runs_methods_as_expected(method):
request, response = getattr(app.test_client, method.lower())('/')
assert response.text == 'first method'
response = view(request)
assert response.body.decode() == 'first method'
if method in ['DELETE', 'PATCH']:
request, response = getattr(app.test_client, method.lower())('/')
assert response.text == 'second method'
response = view(request)
assert response.body.decode() == 'second method'
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_composition_view_rejects_invalid_methods(method):
app = Sanic('test_composition_view')
def test_composition_view_rejects_invalid_methods(app, method):
view = CompositionView()
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))

View File

@@ -7,13 +7,17 @@ from unittest import mock
from sanic.worker import GunicornWorker
from sanic.app import Sanic
import asyncio
import logging
import pytest
@pytest.fixture(scope='module')
def gunicorn_worker():
command = 'gunicorn --bind 127.0.0.1:1337 --worker-class sanic.worker.GunicornWorker examples.simple_server:app'
command = (
'gunicorn '
'--bind 127.0.0.1:1337 '
'--worker-class sanic.worker.GunicornWorker '
'examples.simple_server:app'
)
worker = subprocess.Popen(shlex.split(command))
time.sleep(3)
yield
@@ -96,10 +100,11 @@ def test_run_max_requests_exceeded(worker):
_runner = asyncio.ensure_future(worker._check_alive(), loop=loop)
loop.run_until_complete(_runner)
assert worker.alive == False
assert not worker.alive
worker.notify.assert_called_with()
worker.log.info.assert_called_with("Max requests exceeded, shutting down: %s",
worker)
worker.log.info.assert_called_with("Max requests exceeded, shutting "
"down: %s", worker)
def test_worker_close(worker):
loop = asyncio.new_event_loop()
@@ -113,14 +118,15 @@ def test_worker_close(worker):
conn = mock.Mock()
conn.websocket = mock.Mock()
conn.websocket.close_connection = mock.Mock(
wraps=asyncio.coroutine(lambda *a, **kw: None)
)
wraps=asyncio.coroutine(lambda *a, **kw: None)
)
worker.connections = set([conn])
worker.log = mock.Mock()
worker.loop = loop
server = mock.Mock()
server.close = mock.Mock(wraps=lambda *a, **kw: None)
server.wait_closed = mock.Mock(wraps=asyncio.coroutine(lambda *a, **kw: None))
server.wait_closed = mock.Mock(wraps=asyncio.coroutine(
lambda *a, **kw: None))
worker.servers = {
server: {"requests_count": 14},
}
@@ -130,6 +136,6 @@ def test_worker_close(worker):
_close = asyncio.ensure_future(worker.close(), loop=loop)
loop.run_until_complete(_close)
assert worker.signal.stopped == True
assert conn.websocket.close_connection.called == True
assert worker.signal.stopped
assert conn.websocket.close_connection.called
assert len(worker.servers) == 0

11
tox.ini
View File

@@ -1,5 +1,5 @@
[tox]
envlist = py35, py36, py37, {py35,py36,py37}-no-ext, flake8, check
envlist = py35, py36, py37, {py35,py36,py37}-no-ext, lint, check
[testenv]
usedevelop = True
@@ -8,7 +8,7 @@ setenv =
{py35,py36,py37}-no-ext: SANIC_NO_UVLOOP=1
deps =
coverage
pytest==3.3.2
pytest>=3.6
pytest-cov
pytest-sanic
pytest-sugar
@@ -20,13 +20,18 @@ commands =
pytest tests --cov sanic --cov-report= {posargs}
- coverage combine --append
coverage report -m
coverage html -i
[testenv:flake8]
[testenv:lint]
deps =
flake8
black
isort
commands =
flake8 sanic
black --check --verbose sanic
isort --check-only --recursive sanic
[testenv:check]
deps =