Compare commits

...

104 Commits

Author SHA1 Message Date
Adam Hopkins
0b750593da Version Bump 2022-07-31 13:03:51 +03:00
Adam Hopkins
5b1686ceba Use path.parts instead of match (#2508) 2022-07-31 12:58:21 +03:00
Adam Hopkins
86baaef1ec Use pathlib for path resolution (#2506) 2022-07-31 12:58:18 +03:00
Adam Hopkins
2b4b78da88 Fix dotted test 2022-07-31 12:56:57 +03:00
Néstor Pérez
ee6d8cfe11 Prevent directory traversion with static files (#2495)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Zhiwei Liang <zhi.wei.liang@outlook.com>
2022-07-31 12:56:52 +03:00
Adam Hopkins
c4da66bf1f Update changelog 2022-01-06 12:26:35 +02:00
Adam Hopkins
d50d3b8448 Bump version 2022-01-06 12:21:44 +02:00
Adam Hopkins
313f97ac77 Only display MOTD in ASGI on startup (#2349) 2022-01-06 11:22:57 +02:00
Adam Hopkins
a23547d73b Ignore name argument on Python 3.7 (#2355)
Co-authored-by: Néstor Pérez <25409753+prryplatypus@users.noreply.github.com>
Co-authored-by: Ryu juheon <saidbysolo@gmail.com>
2022-01-06 10:57:24 +02:00
Adam Hopkins
34d1dee407 Add config.update support for setters (#2354) 2022-01-06 09:55:03 +02:00
Adam Hopkins
dc3ccba527 Auto extend with Sanic Extensions (#2308) 2021-12-25 22:20:06 +02:00
Adam Hopkins
b91ffed010 Change signal routing for increased consistency (#2277) 2021-12-24 01:27:54 +02:00
Adam Hopkins
8c07e388cd LTS v21.12 Deprecations (#2306)
Co-authored-by: Néstor Pérez <25409753+prryplatypus@users.noreply.github.com>
2021-12-24 00:30:27 +02:00
Néstor Pérez
98ce4bdeb2 Optional uvloop use (#2264)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-12-23 11:57:33 +02:00
Adam Hopkins
4659069350 Add route context (#2302) 2021-12-21 22:56:12 +02:00
Adam Hopkins
080d41627a Env custom type casting (#2330) 2021-12-21 00:50:45 +02:00
Adam Hopkins
d799c5f03c Add named tasks (#2304) 2021-12-20 23:50:04 +02:00
Adam Hopkins
abe062b371 Remove app instance from Config for error handler setting (#2320) 2021-12-18 18:58:14 +02:00
Zhiwei
b5a00ac1ca Add Py310 Tests Badge to README (#2341) 2021-12-15 13:09:13 +02:00
Adam Hopkins
f282865362 Cleanup ports on tests (#2338) 2021-12-13 19:48:30 +02:00
Adam Hopkins
377c2ada38 Remove paths from coverage checks (#2336) 2021-12-13 15:03:51 +02:00
Adam Hopkins
264453459e Add runtime checking to create_server to verify that startup has been run (#2328) 2021-12-13 09:36:41 +02:00
Adam Hopkins
3d383d7b97 Update README.rst 2021-12-12 13:58:34 +02:00
Adam Hopkins
c0cc26021b Allow underscore to start instance names (#2335) 2021-12-10 00:04:34 +02:00
Zhiwei
96c027bad5 Prevent sending multiple or mixed responses on a single request (#2327)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-12-09 12:00:18 +02:00
Adam Hopkins
b2a1bc69f5 Make warnings for DeprecationWarning consistent (#2332) 2021-12-08 21:01:28 +02:00
Zhiwei
426742b3e2 Fix Sanic.handle_exception Method's Type Doc (#2333)
* Fix type doc

* Remove explicit type defines in docstring
2021-12-07 12:10:04 +02:00
Kian Meng Ang
ab35121864 Fix typos (#2329)
* Fix typos

* Downgrade mistune version

* Fix blueprint host param

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2021-12-06 09:17:01 +02:00
Zhiwei
cf3c205fa5 Fixed: CI Runs on Ready PRs Only (#2324)
* Revert "Update tests to not run all the time (#2311)"

This reverts commit 2c03eee329.

* Make CI only runs on PRs that are ready

* Remove CI Tasks on Push
2021-12-06 00:05:07 +02:00
Adam Hopkins
19f6544923 Resolve potential bug in MOTD in Docker (#2331)
* Resolve potential bug in MOTD in Docker
2021-12-03 13:27:16 +02:00
Clay Sweetser
f641830d26 Refactor environment variable hydration logic. (#2321)
- Refactor environment variable hydration logic to be less nested. This allows possible future extension of the hydration logic.
- Fix a spelling mistake in `load_environment_vars` docstring.

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-12-03 00:01:20 +02:00
Zhiwei
a8d55e180c Fix Type Hint (#2322)
* Fix Type Hint

Union is not necessary here? My IDE pops an error for it.

* Rerun workflows

* empty commit
2021-12-02 23:17:22 +02:00
Adam Hopkins
55c36e0240 Fix examples to work as expected (#2305)
* Fix examples to work as expected

* Clean up examples

* Update worker test

* Merge in from main and cleanup example
2021-11-23 23:00:25 +02:00
Adam Hopkins
2c03eee329 Update tests to not run all the time (#2311) 2021-11-23 15:53:46 +02:00
Adam Hopkins
65e28b8c22 Mergeback (#2319)
* Mergeback
2021-11-21 15:13:34 +02:00
Adam Hopkins
dfd33dd63d Update README.rst 2021-11-21 10:27:23 +02:00
Hussein Mohamed
722a6db8d9 Dispatch http.lifecyle.response from exception handler (#2299) 2021-11-19 08:50:02 +02:00
Xavier Petit
9c576c74db Fix typehints in route shorthand methods (#2317)
* Fix typehints in route shorthand methods

Change typehint of the version variable in RouteMixin  to allow int string and float types values
Resolves #2314

* fix lint error in version variable
2021-11-19 08:28:05 +02:00
Adam Hopkins
523db190a7 Add contextual exceptions (#2290) 2021-11-18 17:47:27 +02:00
Adam Hopkins
95631b9686 Coffee please (#2316)
* Coffee please

* Add unit tests
2021-11-18 14:53:06 +02:00
Adam Hopkins
0860bfe1f1 Merge release 21.9.2 (#2313) 2021-11-17 19:36:36 +02:00
Adam Hopkins
85e7b712b9 Allow early Blueprint registrations to still apply later added objects (#2260) 2021-11-17 17:29:41 +02:00
L. Kärkkäinen
b731a6b48c Make HTTP connections start in IDLE stage, avoiding delays and error messages (#2268)
* Make all new connections start in IDLE stage, and switch to REQUEST stage only once any bytes are received from client. This makes new connections without any request obey keepalive timeout rather than request timeout like they currently do.

* Revert typo

* Remove request timeout endpoint test which is no longer working (still tested by mocking). Fix mock timeout test setup.

Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com>
2021-11-16 23:03:27 +02:00
Adam Hopkins
cde02b5936 More consistent config setting with post-FALLBACK_ERROR_FORMAT apply (#2310)
* Update unit testing and add more consistent config

* Change init and app values to private

* Cleanup line lengths
2021-11-16 13:07:33 +02:00
Adam Hopkins
abeb8d0bc0 Provide list of reloaded files (#2307)
* Allow access to reloaded files

* Return to simple boolean values

* Resolve before adding to changed files
2021-11-16 10:16:32 +02:00
Adam Hopkins
9a9f72ad64 Move builtin signals to enum (#2309)
* Move builtin signals to enum

* Fix annotations
2021-11-14 23:21:14 +02:00
Adam Hopkins
392a497366 Restructure of CLI and application state (#2295)
* Initial work on restructure of application state

* Updated MOTD with more flexible input and add basic version

* Remove unnecessary type ignores

* Add wrapping and smarter output per process type

* Add support for ASGI MOTD

* Add Windows color support ernable

* Refactor __main__ into submodule

* Renest arguments

* Passing unit tests

* Passing unit tests

* Typing

* Fix num worker test

* Add context to assert failure

* Add some type annotations

* Some linting

* Line aware searching in test

* Test abstractions

* Fix some flappy tests

* Bump up timeout on CLI tests

* Change test for no access logs on gunicornworker

* Add some basic test converage

* Some new tests, and disallow workers and fast on app.run
2021-11-07 21:39:03 +02:00
Adam Hopkins
36e6a6c506 Increase join concat performance (#2291) 2021-10-29 12:55:09 +03:00
Stephen Sadowski
a361b345ad Py310 loop fixup (#2294)
* Fixup for 3.8+; Sanic still supports 3.7 where loop is required

* Added branching statement to hanle asyncio.Event() loop parameter removal in 3.10, and optional supply in 3.9
2021-10-28 17:16:23 -05:00
Adam Hopkins
f5bd6e3b2f Add Python 3.10 testing (and support) (#2257)
Administratively merging because @vltr and @ahopkins are the release managers, and @ahopkins is originator of the PR

* Add Python 3.10 testing (and support)

* fixed py310 tox environment for windows, quoted '3.10' in python-310 tests to avoid numeric compression

* updated tox.ini for py310

* quoted the rest of the bare 3.10 references in the workflows

* Issue with pytest requires version bump to 6.2.5 for python 3.10

Co-authored-by: Stephen Sadowski <stephen.sadowski@sjsadowski.com>
2021-10-28 13:41:57 -05:00
L. Kärkkäinen
6c7df68c7c Vhost support using multiple TLS certificates (#2270)
* Initial support for using multiple SSL certificates.

* Also list IP address subjectAltNames on log.

* Use Python 3.7+ way of specifying TLSv1.2 as the minimum version. Linter fixes.

* isort

* Cleanup, store server name for later use. Add RSA ciphers. Log rejected SNIs.

* Cleanup, linter.

* Alter the order of initial log messages and handling. In particular, enable debug mode early so that debug messages during init can be shown.

* Store server name (SNI) to conn_info.

* Update test with new error message.

* Refactor for readability.

* Cleanup

* Replace old expired test cert with new ones and a script for regenerating them as needed.

* Refactor TLS tests to a separate file.

* Add cryptography to dev deps for rebuilding TLS certs.

* Minor adjustment to messages.

* Tests added for new TLS code.

* Find the correct log row before testing for message. The order was different on CI.

* More log message order fixup. The tests do not account for the logo being printed first.

* Another attempt at log message indexing fixup.

* Major TLS refactoring.

CertSelector now allows dicts and SSLContext within its list.
Server names are stored even when no list is used.
SSLContext.sanic now contains a dict with any setting passed and information extracted from cert.
That information is available on request.conn_info.cert.
Type annotations added.
More tests incl. a handler for faking hostname in tests.

* Remove a problematic logger test that apparently was not adding any coverage or value to anything.

* Revert accidental commit of uvloop disable.

* Typing fixes / refactoring.

* Additional test for cert selection. Certs recreated without DNS:localhost on sanic.example cert.

* Add tests for single certificate path shorthand and SNI information.

* Move TLS dict processing to CertSimple, make the names field optional and use names from the cert if absent.

* Sanic CLI options --tls and --tls-strict-host to use the new features.

* SSL argument typing updated

* Use ValueError for internal message passing to avoid CertificateError's odd message formatting.

* Linter

* Test CLI TLS options.

* Maybe the right codeclimate option now...

* Improved TLS argument help, removed support for combining --cert/--key with --tls.

* Removed support for strict checking without any certs, black forced fscked up formatting.

* Update CLI tests for stricter TLS options.

Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-10-28 16:50:05 +03:00
Meysam
5b82884f8b ci: fail on error 👷 (#2292)
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-10-27 11:55:34 +03:00
Néstor Pérez
f0f81ec458 Add ability to log all exceptions (#2262)
* Add ability to log all exceptions

* Fix linting 🙄

* Remove shorthand

* Make `ErrorHandler.log` backwards-compat

* Ignore mypy error

* Don't store `noisy_exceptions` attribute in app

* Added tests

* Store noisy exceptions setting in config

* Default to not-noisy if config key not available

* Add CLI tests for `noisy-exceptions`

* Remove debugging line I left in 😅

* Fix tests

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-10-27 10:43:58 +03:00
Cyril Nicodème
71cc30e5cd Replacing assignation by typing for websocket_handshake (#2273)
* Replacing assignation by typing for `websocket_handshake`

Related to #2272

* Fix some type hinting issues

* Cleanup websocket handchake response concat

* Optimize concat encoding

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2021-10-27 10:00:04 +03:00
L. Kärkkäinen
645310cff6 Change codeclimate complexity threshold from 5 to 10. (#2286)
Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com>
Co-authored-by: Stephen Sadowski <stephen.sadowski@sjsadowski.com>
2021-10-24 22:30:35 +03:00
Stephen Sadowski
2f30b5748a Update host test function names so they are not overwritten (#2287)
Fix function name reuse in cli tests
2021-10-24 12:29:16 -05:00
Stephen Sadowski
5e1ef96934 fix ipv6 display in startup info log (#2285)
* fix ipv6 display in startup info log

* refactored to oneliner by request

* Added test for passing ipv4 host

* Added test for passing ipv6 any host

* Added test for passing ipv6 loopback host
2021-10-24 11:14:00 -05:00
Ryu juheon
57e98b62b3 style: add some type hints (#2279)
* style: add some type hints

* fix: *args is a tuple, but overridden as a list

* fix: if touch this, it will be a maybe breaking change

* fix: remove unused import

* style(app): more correct type
2021-10-20 15:15:39 +03:00
Néstor Pérez
3262878ebd Fix docs error (#2269) 2021-10-10 22:41:45 +03:00
Adam Hopkins
5e12edbc38 Allow non-conforming ErrorHandlers (#2259)
* Allow non-conforming ErrorHandlers

* Rename to legacy lookup

* Updated depnotice

* Bump version

* Fix formatting

* Remove unused import

* Fix error messages
2021-10-03 01:02:56 +03:00
Adam Hopkins
50a606adee Merge pull request #2256 from sanic-org/current-release
Mergeback
2021-10-02 22:21:34 +03:00
Adam Hopkins
f995612073 Merge branch 'main' of github.com:sanic-org/sanic into current-release 2021-10-02 21:57:58 +03:00
Adam Hopkins
bc08383acd Merge in main to current-release (#2254)
* Remove unnecessary import in test_constants.py, which also fixes an error on win (#2180)

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>

* Manually reset the buffer when streaming request body (#2183)

* Remove Duplicated Dependencies and PEP 517 Support (#2173)

* Remove duplicated dependencies

* Specify setuptools as the tool for generating distribution (PEP 517)

* Add `isort` to `dev_require`

* manage all dependencies in setup.py

* Execute `make pretty`

* Set usedevelop to true (revert previous change)

* Fix the handling of the end of a chunked request. (#2188)

* Fix the handling of the end of a chunked request.

* Avoid hardcoding final chunk header size.

* Add some unit tests for pipeline body reading

* Decode bytes for json serialization

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Resolve regressions in exceptions (#2181)

* Update sanic-routing to fix path issues plus lookahead / lookbehind support (#2178)

* Update sanic-routing to fix path issues plus lookahead / lookbehind support

* Update setup.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>

* style(app,blueprints): add some type hints (#2196)

* style(app,blueprints): add some type hints

* style(app): option is Any

* style(blueprints): url prefix default value is ``""``

* style(app): backward compatible

* style(app): backward compatible

* style(blueprints): defult is None

* style(app): apply code style (black)

* Update some CC config (#2199)

* Update README.rst

* raise exception for `_static_request_handler` unknown exception; add test with custom error (#2195)

Co-authored-by: n.feofanov <n.feofanov@visionlabs.ru>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>

* Change dumps to AnyStr (#2193)

* HTTP tests (#2194)

* Fix issues with after request handling in HTTP pipelining (#2201)

* Clean up after a request is complete, before the next pipelined request.

* Limit the size of request body consumed after handler has finished.

* Linter error.

* Add unit test re: bad headers

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Update CHANGELOG

* Log remote address if available (#2207)

* Log remote address if available

* Add tests

* Fix testing version

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Fixed for handling exceptions of asgi app call. (#2211)

@cansarigol3megawatt Thanks for looking into this and getting the quick turnaround on this. I will 🍒 pick this into the 21.6 branch and get it out a little later tonight.

* Signals Integration (#2160)

* Update some tests

* Resolve #2122 route decorator returning tuple

* Use rc sanic-routing version

* Update unit tests to <:str>

* Minimal working version with some signals implemented

* Add more http signals

* Update ASGI and change listeners to signals

* Allow for dynamic ODE signals

* Allow signals to be stacked

* Begin tests

* Prioritize match_info on keyword argument injection

* WIP on tests

* Compat with signals

* Work through some test coverage

* Passing tests

* Post linting

* Setup proper resets

* coverage reporting

* Fixes from vltr comments

* clear delayed tasks

* Fix bad test

* rm pycache

* uncomment windows tests (#2214)

* Add convenience methods to BP groups (#2209)

* Fix bug where ws exceptions not being logged (#2213)

* Fix bug where ws exceptions not being logged

* Fix t\est

* Style: add type hints (#2217)

* style(routes): add_route argument, return typing

* style(listeners): typing

* style(views): typing as_view

* style(routes): change type hint

* style(listeners): change type hint

* style(routes): change type hint

* add some more types

* Change as_view typing

* Add some cleaner type annotations

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Add default messages to SanicExceptions (#2216)

* Add default messages to SanicExceptions

* Cleaner exception message setting

* Copy Blueprints Implementation (#2184)

* Accept header parsing (#2200)

* Add some tests

* docstring

* Add accept matching

* Add some more tests on matching

* Add matching flags for wildcards

* Add mathing controls to accept

* Limit uvicorn 14 in testing

* Add convenience for annotated handlers (#2225)

* Split HttpProtocol parts into base SanicProtocol and HTTPProtocol subclass (#2229)

* Split HttpProtocol parts into base SanicProtocol and HTTPProtocol subclass.

* lint fixes

* re-black server.py

* Move server.py into its own module (#2230)

* Move server.py into its own module

* Change monkeypatch path on test_logging.py

* Blueprint specific exception handlers (#2208)

* Call abort() on sockets after close() to prevent dangling sockets (#2231)

* Add ability to return Falsey but not-None from handlers (#2236)

* Adds Blueprint Group exception decorator (#2238)

* Add exception decorator

* Added tests

* Fix line too long

* Static DIR and FILE resource types (#2244)

* Explicit static directive for serving file or dir


Co-authored-by: anbuhckr <36891836+anbuhckr@users.noreply.github.com>
Co-authored-by: anbuhckr <miki.suhendra@gmail.com>

* Close HTTP loop when connection task cancelled (#2245)

* Terminate loop when no transport exists

* Add log when closing HTTP loop because of shutdown

* Add unit test

* New websockets (#2158)

* First attempt at new Websockets implementation based on websockets >= 9.0, with sans-i/o features. Requires more work.

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* wip, update websockets code to new Sans/IO API

* Refactored new websockets impl into own modules
Incorporated other suggestions made by team

* Another round of work on the new websockets impl
* Added websocket_timeout support (matching previous/legacy support)
* Lots more comments
* Incorporated suggested changes from previous round of review
* Changed RuntimeError usage to ServerError
* Changed SanicException usage to ServerError
* Removed some redundant asserts
* Change remaining asserts to ServerErrors
* Fixed some timeout handling issues
* Fixed websocket.close() handling, and made it more robust
* Made auto_close task smarter and more error-resilient
* Made fail_connection routine smarter and more error-resilient

* Further new websockets impl fixes
* Update compatibility with Websockets v10
* Track server connection state in a more precise way
* Try to handle the shutdown process more gracefully
* Add a new end_connection() helper, to use as an alterative to close() or fail_connection()
* Kill the auto-close task and keepalive-timeout task when sanic is shutdown
* Deprecate WEBSOCKET_READ_LIMIT and WEBSOCKET_WRITE_LIMIT configs, they are not used in this implementation.

* Change a warning message to debug level
Remove default values for deprecated websocket parameters

* Fix flake8 errors

* Fix a couple of missed failing tests

* remove websocket bench from examples

* Integrate suggestions from code reviews
Use Optional[T] instead of union[T,None]
Fix mypy type logic errors
change "is not None" to truthy checks where appropriate
change "is None" to falsy checks were appropriate
Add more debug logging when debug mode is on
Change to using sanic.logger for debug logging rather than error_logger.

* Fix long line lengths of debug messages
Add some new debug messages when websocket IO is paused and unpaused for flow control
Fix websocket example to use app.static()

* remove unused import in websocket example app

* re-run isort after Flake8 fixes

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>

* Account for BP with exception handler but no routes (#2246)

* Don't log "enabled" if auto-reload disabled (#2247)

Fixes #2240

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>

* Smarter auto fallback (#2162)

* Smarter auto fallback

* remove config from blueprints

* Add tests for error formatting

* Add check for proper format

* Fix some tests

* Add some tests

* docstring

* Add accept matching

* Add some more tests on matching

* Fix contains bug, earlier return on MediaType eq

* Add matching flags for wildcards

* Add mathing controls to accept

* Cleanup dev cruft

* Add cleanup and resolve OSError relating to test implementation

* Fix test

* Fix some typos

* Some fixes to the new Websockets impl (#2248)

* First attempt at new Websockets implementation based on websockets >= 9.0, with sans-i/o features. Requires more work.

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* wip, update websockets code to new Sans/IO API

* Refactored new websockets impl into own modules
Incorporated other suggestions made by team

* Another round of work on the new websockets impl
* Added websocket_timeout support (matching previous/legacy support)
* Lots more comments
* Incorporated suggested changes from previous round of review
* Changed RuntimeError usage to ServerError
* Changed SanicException usage to ServerError
* Removed some redundant asserts
* Change remaining asserts to ServerErrors
* Fixed some timeout handling issues
* Fixed websocket.close() handling, and made it more robust
* Made auto_close task smarter and more error-resilient
* Made fail_connection routine smarter and more error-resilient

* Further new websockets impl fixes
* Update compatibility with Websockets v10
* Track server connection state in a more precise way
* Try to handle the shutdown process more gracefully
* Add a new end_connection() helper, to use as an alterative to close() or fail_connection()
* Kill the auto-close task and keepalive-timeout task when sanic is shutdown
* Deprecate WEBSOCKET_READ_LIMIT and WEBSOCKET_WRITE_LIMIT configs, they are not used in this implementation.

* Change a warning message to debug level
Remove default values for deprecated websocket parameters

* Fix flake8 errors

* Fix a couple of missed failing tests

* remove websocket bench from examples

* Integrate suggestions from code reviews
Use Optional[T] instead of union[T,None]
Fix mypy type logic errors
change "is not None" to truthy checks where appropriate
change "is None" to falsy checks were appropriate
Add more debug logging when debug mode is on
Change to using sanic.logger for debug logging rather than error_logger.

* Fix long line lengths of debug messages
Add some new debug messages when websocket IO is paused and unpaused for flow control
Fix websocket example to use app.static()

* remove unused import in websocket example app

* re-run isort after Flake8 fixes

* Some fixes to the new Websockets impl
Will throw WebsocketClosed exception instead of ServerException now when attempting to read or write to closed websocket, this makes it easier to catch
The various ws.recv() methods now have the ability to raise CancelledError into your websocket handler
Fix a niche close-socket negotiation bug
Fix bug where http protocol thought the websocket never sent any response.
Allow data to still send in some cases after websocket enters CLOSING state.
Fix some badly formatted and badly placed comments

* allow eof_received to send back data too, if the connection is in CLOSING state

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>

* 21.9 release docs (#2218)

* Beging 21.9 release docs

* Add PRs to changelog

* Change deprecation version

* Update logging tests

* Bump version

* Update changelog

* Change dev install command (#2251)

Co-authored-by: Zhiwei <zhi.wei.liang@outlook.com>
Co-authored-by: L. Kärkkäinen <98187+Tronic@users.noreply.github.com>
Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
Co-authored-by: Robert Palmer <robd003@users.noreply.github.com>
Co-authored-by: Ryu JuHeon <saidbysolo@gmail.com>
Co-authored-by: gluhar2006 <49654448+gluhar2006@users.noreply.github.com>
Co-authored-by: n.feofanov <n.feofanov@visionlabs.ru>
Co-authored-by: Néstor Pérez <25409753+prryplatypus@users.noreply.github.com>
Co-authored-by: Can Sarigol <56863826+cansarigol3megawatt@users.noreply.github.com>
Co-authored-by: Zhiwei <chihwei.public@outlook.com>
Co-authored-by: YongChan Cho <h3236516@gmail.com>
Co-authored-by: Zhiwei <zhiwei@sinatra.ai>
Co-authored-by: Ashley Sommer <ashleysommer@gmail.com>
Co-authored-by: anbuhckr <36891836+anbuhckr@users.noreply.github.com>
Co-authored-by: anbuhckr <miki.suhendra@gmail.com>
2021-10-02 21:55:23 +03:00
L. Kärkkäinen
b83a1a184c Change dev install command (#2251) 2021-10-02 19:57:02 +03:00
Adam Hopkins
59dd6814f8 21.9 release docs (#2218)
* Beging 21.9 release docs

* Add PRs to changelog

* Change deprecation version

* Update logging tests

* Bump version

* Update changelog
2021-09-30 22:36:34 +03:00
Ashley Sommer
f7abf3db1b Some fixes to the new Websockets impl (#2248)
* First attempt at new Websockets implementation based on websockets >= 9.0, with sans-i/o features. Requires more work.

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* wip, update websockets code to new Sans/IO API

* Refactored new websockets impl into own modules
Incorporated other suggestions made by team

* Another round of work on the new websockets impl
* Added websocket_timeout support (matching previous/legacy support)
* Lots more comments
* Incorporated suggested changes from previous round of review
* Changed RuntimeError usage to ServerError
* Changed SanicException usage to ServerError
* Removed some redundant asserts
* Change remaining asserts to ServerErrors
* Fixed some timeout handling issues
* Fixed websocket.close() handling, and made it more robust
* Made auto_close task smarter and more error-resilient
* Made fail_connection routine smarter and more error-resilient

* Further new websockets impl fixes
* Update compatibility with Websockets v10
* Track server connection state in a more precise way
* Try to handle the shutdown process more gracefully
* Add a new end_connection() helper, to use as an alterative to close() or fail_connection()
* Kill the auto-close task and keepalive-timeout task when sanic is shutdown
* Deprecate WEBSOCKET_READ_LIMIT and WEBSOCKET_WRITE_LIMIT configs, they are not used in this implementation.

* Change a warning message to debug level
Remove default values for deprecated websocket parameters

* Fix flake8 errors

* Fix a couple of missed failing tests

* remove websocket bench from examples

* Integrate suggestions from code reviews
Use Optional[T] instead of union[T,None]
Fix mypy type logic errors
change "is not None" to truthy checks where appropriate
change "is None" to falsy checks were appropriate
Add more debug logging when debug mode is on
Change to using sanic.logger for debug logging rather than error_logger.

* Fix long line lengths of debug messages
Add some new debug messages when websocket IO is paused and unpaused for flow control
Fix websocket example to use app.static()

* remove unused import in websocket example app

* re-run isort after Flake8 fixes

* Some fixes to the new Websockets impl
Will throw WebsocketClosed exception instead of ServerException now when attempting to read or write to closed websocket, this makes it easier to catch
The various ws.recv() methods now have the ability to raise CancelledError into your websocket handler
Fix a niche close-socket negotiation bug
Fix bug where http protocol thought the websocket never sent any response.
Allow data to still send in some cases after websocket enters CLOSING state.
Fix some badly formatted and badly placed comments

* allow eof_received to send back data too, if the connection is in CLOSING state

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-09-30 22:20:57 +03:00
Adam Hopkins
cf1d2148ac Smarter auto fallback (#2162)
* Smarter auto fallback

* remove config from blueprints

* Add tests for error formatting

* Add check for proper format

* Fix some tests

* Add some tests

* docstring

* Add accept matching

* Add some more tests on matching

* Fix contains bug, earlier return on MediaType eq

* Add matching flags for wildcards

* Add mathing controls to accept

* Cleanup dev cruft

* Add cleanup and resolve OSError relating to test implementation

* Fix test

* Fix some typos
2021-09-29 23:53:49 +03:00
Ashley Sommer
b5f2bd9b0e Don't log "enabled" if auto-reload disabled (#2247)
Fixes #2240

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-09-29 14:08:46 +03:00
Adam Hopkins
ba2670e99c Account for BP with exception handler but no routes (#2246) 2021-09-29 13:47:31 +03:00
Ashley Sommer
6ffc4d9756 New websockets (#2158)
* First attempt at new Websockets implementation based on websockets >= 9.0, with sans-i/o features. Requires more work.

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* Update sanic/websocket.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>

* wip, update websockets code to new Sans/IO API

* Refactored new websockets impl into own modules
Incorporated other suggestions made by team

* Another round of work on the new websockets impl
* Added websocket_timeout support (matching previous/legacy support)
* Lots more comments
* Incorporated suggested changes from previous round of review
* Changed RuntimeError usage to ServerError
* Changed SanicException usage to ServerError
* Removed some redundant asserts
* Change remaining asserts to ServerErrors
* Fixed some timeout handling issues
* Fixed websocket.close() handling, and made it more robust
* Made auto_close task smarter and more error-resilient
* Made fail_connection routine smarter and more error-resilient

* Further new websockets impl fixes
* Update compatibility with Websockets v10
* Track server connection state in a more precise way
* Try to handle the shutdown process more gracefully
* Add a new end_connection() helper, to use as an alterative to close() or fail_connection()
* Kill the auto-close task and keepalive-timeout task when sanic is shutdown
* Deprecate WEBSOCKET_READ_LIMIT and WEBSOCKET_WRITE_LIMIT configs, they are not used in this implementation.

* Change a warning message to debug level
Remove default values for deprecated websocket parameters

* Fix flake8 errors

* Fix a couple of missed failing tests

* remove websocket bench from examples

* Integrate suggestions from code reviews
Use Optional[T] instead of union[T,None]
Fix mypy type logic errors
change "is not None" to truthy checks where appropriate
change "is None" to falsy checks were appropriate
Add more debug logging when debug mode is on
Change to using sanic.logger for debug logging rather than error_logger.

* Fix long line lengths of debug messages
Add some new debug messages when websocket IO is paused and unpaused for flow control
Fix websocket example to use app.static()

* remove unused import in websocket example app

* re-run isort after Flake8 fixes

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-09-29 13:09:23 +03:00
Adam Hopkins
595d2c76ac Close HTTP loop when connection task cancelled (#2245)
* Terminate loop when no transport exists

* Add log when closing HTTP loop because of shutdown

* Add unit test
2021-09-27 10:22:30 +03:00
Adam Hopkins
d9796e9b1e Static DIR and FILE resource types (#2244)
* Explicit static directive for serving file or dir


Co-authored-by: anbuhckr <36891836+anbuhckr@users.noreply.github.com>
Co-authored-by: anbuhckr <miki.suhendra@gmail.com>
2021-09-26 01:01:23 +03:00
Néstor Pérez
404c5f9f9e Adds Blueprint Group exception decorator (#2238)
* Add exception decorator

* Added tests

* Fix line too long
2021-09-12 22:02:59 +03:00
Adam Hopkins
a937e08ef0 Add ability to return Falsey but not-None from handlers (#2236) 2021-09-12 07:19:26 +03:00
Ashley Sommer
ef4f058a6c Call abort() on sockets after close() to prevent dangling sockets (#2231) 2021-09-01 08:14:11 +03:00
Adam Hopkins
69c5dde9bf Blueprint specific exception handlers (#2208) 2021-08-31 12:32:51 +03:00
Adam Hopkins
945885d501 Move server.py into its own module (#2230)
* Move server.py into its own module

* Change monkeypatch path on test_logging.py
2021-08-31 11:51:32 +03:00
Ashley Sommer
9d0b54c90d Split HttpProtocol parts into base SanicProtocol and HTTPProtocol subclass (#2229)
* Split HttpProtocol parts into base SanicProtocol and HTTPProtocol subclass.

* lint fixes

* re-black server.py
2021-08-31 10:49:58 +03:00
Adam Hopkins
2e5c288fea Add convenience for annotated handlers (#2225) 2021-08-30 20:04:44 +03:00
Adam Hopkins
f32ef20b74 Accept header parsing (#2200)
* Add some tests

* docstring

* Add accept matching

* Add some more tests on matching

* Add matching flags for wildcards

* Add mathing controls to accept

* Limit uvicorn 14 in testing
2021-08-19 21:09:40 +03:00
Zhiwei
e2eefaac55 Copy Blueprints Implementation (#2184) 2021-08-10 01:07:04 +03:00
Adam Hopkins
e1cfbf0fd9 Add default messages to SanicExceptions (#2216)
* Add default messages to SanicExceptions

* Cleaner exception message setting
2021-08-09 21:14:15 +03:00
YongChan Cho
08c5689441 Style: add type hints (#2217)
* style(routes): add_route argument, return typing

* style(listeners): typing

* style(views): typing as_view

* style(routes): change type hint

* style(listeners): change type hint

* style(routes): change type hint

* add some more types

* Change as_view typing

* Add some cleaner type annotations

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2021-08-08 21:37:34 +03:00
Adam Hopkins
8dbda247d6 Fix bug where ws exceptions not being logged (#2213)
* Fix bug where ws exceptions not being logged

* Fix t\est
2021-08-07 23:24:48 +03:00
Adam Hopkins
71a631237d Add convenience methods to BP groups (#2209) 2021-08-07 23:06:11 +03:00
Zhiwei
e22ff3828b uncomment windows tests (#2214) 2021-08-06 11:52:38 +03:00
Adam Hopkins
b1b12e004e Signals Integration (#2160)
* Update some tests

* Resolve #2122 route decorator returning tuple

* Use rc sanic-routing version

* Update unit tests to <:str>

* Minimal working version with some signals implemented

* Add more http signals

* Update ASGI and change listeners to signals

* Allow for dynamic ODE signals

* Allow signals to be stacked

* Begin tests

* Prioritize match_info on keyword argument injection

* WIP on tests

* Compat with signals

* Work through some test coverage

* Passing tests

* Post linting

* Setup proper resets

* coverage reporting

* Fixes from vltr comments

* clear delayed tasks

* Fix bad test

* rm pycache
2021-08-05 22:55:42 +03:00
Can Sarigol
0ba57d4701 Fixed for handling exceptions of asgi app call. (#2211)
@cansarigol3megawatt Thanks for looking into this and getting the quick turnaround on this. I will 🍒 pick this into the 21.6 branch and get it out a little later tonight.
2021-08-02 19:12:12 +03:00
Néstor Pérez
54ca6a6178 Log remote address if available (#2207)
* Log remote address if available

* Add tests

* Fix testing version

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2021-08-02 12:41:47 +03:00
Adam Hopkins
7dd4a78cf2 Update CHANGELOG 2021-07-28 12:12:30 +03:00
Adam Hopkins
52ff49512a Merge branch 'current-release' of github.com:sanic-org/sanic 2021-07-28 12:10:47 +03:00
L. Kärkkäinen
4732b6bdfa Fix issues with after request handling in HTTP pipelining (#2201)
* Clean up after a request is complete, before the next pipelined request.

* Limit the size of request body consumed after handler has finished.

* Linter error.

* Add unit test re: bad headers

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2021-07-28 11:38:28 +03:00
Adam Hopkins
c3b6fa1bba HTTP tests (#2194) 2021-07-19 16:52:33 +03:00
Adam Hopkins
94d496afe1 Change dumps to AnyStr (#2193) 2021-07-19 16:25:36 +03:00
gluhar2006
7b7a572f9b raise exception for _static_request_handler unknown exception; add test with custom error (#2195)
Co-authored-by: n.feofanov <n.feofanov@visionlabs.ru>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-07-19 09:23:02 +03:00
Adam Hopkins
1b8cb742f9 Update README.rst 2021-07-18 23:22:19 +03:00
Adam Hopkins
3492d180a8 Update some CC config (#2199) 2021-07-18 12:32:35 +03:00
Ryu JuHeon
021da38373 style(app,blueprints): add some type hints (#2196)
* style(app,blueprints): add some type hints

* style(app): option is Any

* style(blueprints): url prefix default value is ``""``

* style(app): backward compatible

* style(app): backward compatible

* style(blueprints): defult is None

* style(app): apply code style (black)
2021-07-13 10:32:54 +03:00
Robert Palmer
ac784759d5 Update sanic-routing to fix path issues plus lookahead / lookbehind support (#2178)
* Update sanic-routing to fix path issues plus lookahead / lookbehind support

* Update setup.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-07-12 08:22:53 +03:00
Adam Hopkins
36eda2cd62 Resolve regressions in exceptions (#2181) 2021-07-12 00:35:27 +03:00
L. Kärkkäinen
08a4b3013f Fix the handling of the end of a chunked request. (#2188)
* Fix the handling of the end of a chunked request.

* Avoid hardcoding final chunk header size.

* Add some unit tests for pipeline body reading

* Decode bytes for json serialization

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2021-07-11 13:44:40 +03:00
Zhiwei
1dd0332e8b Remove Duplicated Dependencies and PEP 517 Support (#2173)
* Remove duplicated dependencies

* Specify setuptools as the tool for generating distribution (PEP 517)

* Add `isort` to `dev_require`

* manage all dependencies in setup.py

* Execute `make pretty`

* Set usedevelop to true (revert previous change)
2021-07-08 09:12:56 +03:00
Adam Hopkins
a90877ac31 Manually reset the buffer when streaming request body (#2183) 2021-07-06 08:24:20 +03:00
Zhiwei
8b7ea27a48 Remove unnecessary import in test_constants.py, which also fixes an error on win (#2180)
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-07-05 11:51:36 +03:00
190 changed files with 10811 additions and 3832 deletions

View File

@@ -1,5 +1,7 @@
exclude_patterns:
- "sanic/__main__.py"
- "sanic/application/logo.py"
- "sanic/application/motd.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
@@ -8,5 +10,19 @@ exclude_patterns:
- "docker/"
- "docs/"
- "examples/"
- "hack/"
- "scripts/"
- "tests/"
checks:
argument-count:
enabled: false
file-lines:
config:
threshold: 1000
method-count:
config:
threshold: 40
complex-logic:
enabled: false
method-complexity:
config:
threshold: 10

View File

@@ -3,6 +3,9 @@ branch = True
source = sanic
omit =
site-packages
sanic/application/logo.py
sanic/application/motd.py
sanic/cli
sanic/__main__.py
sanic/reloader_helpers.py
sanic/simple.py

View File

@@ -2,14 +2,20 @@ name: "CodeQL"
on:
push:
branches: [ main ]
branches:
- main
- "*LTS"
pull_request:
branches: [ main ]
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
schedule:
- cron: '25 16 * * 0'
jobs:
analyze:
if: github.event.pull_request.draft == false
name: Analyze
runs-on: ubuntu-latest

View File

@@ -1,19 +1,16 @@
name: Coverage check
# on:
# push:
# branches:
# - main
# tags:
# - "!*" # Do not execute on tags
# paths:
# - sanic/*
# - tests/*
# pull_request:
# paths:
# - "!*.MD"
on: [push, pull_request]
on:
push:
branches:
- main
- "*LTS"
tags:
- "!*" # Do not execute on tags
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
test:
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.os }}
strategy:
matrix:

View File

@@ -3,9 +3,12 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
bandit:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
@@ -16,6 +19,7 @@ jobs:
- { python-version: 3.7, tox-env: security}
- { python-version: 3.8, tox-env: security}
- { python-version: 3.9, tox-env: security}
- { python-version: "3.10", tox-env: security}
steps:
- name: Checkout the repository
uses: actions/checkout@v2

View File

@@ -3,9 +3,12 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
docsLinter:
if: github.event.pull_request.draft == false
name: Lint Documentation
runs-on: ubuntu-latest
strategy:

View File

@@ -3,9 +3,12 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
linter:
if: github.event.pull_request.draft == false
name: lint
runs-on: ${{ matrix.os }}
strategy:

47
.github/workflows/pr-python310.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Python 3.10 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy310:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: "3.10",
tox-env: py310,
ignore-error-flake: "false",
command-timeout: "0",
}
- {
python-version: "3.10",
tox-env: py310-no-ext,
ignore-error-flake: "true",
command-timeout: "600000",
}
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
command-timeout: "${{ matrix.config.command-timeout }}"
test-failure-retry: "3"

View File

@@ -3,19 +3,16 @@ on:
pull_request:
branches:
- main
push:
branches:
- main
paths:
- sanic/*
- tests/*
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy37:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]

View File

@@ -3,19 +3,16 @@ on:
pull_request:
branches:
- main
push:
branches:
- main
paths:
- sanic/*
- tests/*
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy38:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]

View File

@@ -3,19 +3,16 @@ on:
pull_request:
branches:
- main
push:
branches:
- main
paths:
- sanic/*
- tests/*
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy39:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]

View File

@@ -3,9 +3,12 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
typeChecking:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
@@ -13,9 +16,10 @@ jobs:
matrix:
os: [ubuntu-latest]
config:
- { python-version: 3.7, tox-env: type-checking}
# - { python-version: 3.7, tox-env: type-checking}
- { python-version: 3.8, tox-env: type-checking}
- { python-version: 3.9, tox-env: type-checking}
- { python-version: "3.10", tox-env: type-checking}
steps:
- name: Checkout the repository
uses: actions/checkout@v2

View File

@@ -1,34 +1,38 @@
# name: Run Unit Tests on Windows
# on:
# pull_request:
# branches:
# - main
name: Run Unit Tests on Windows
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
# jobs:
# testsOnWindows:
# name: ut-${{ matrix.config.tox-env }}
# runs-on: windows-latest
# strategy:
# fail-fast: false
# matrix:
# config:
# - { python-version: 3.7, tox-env: py37-no-ext }
# - { python-version: 3.8, tox-env: py38-no-ext }
# - { python-version: 3.9, tox-env: py39-no-ext }
# - { python-version: pypy-3.7, tox-env: pypy37-no-ext }
jobs:
testsOnWindows:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
config:
- { python-version: 3.7, tox-env: py37-no-ext }
- { python-version: 3.8, tox-env: py38-no-ext }
- { python-version: 3.9, tox-env: py39-no-ext }
- { python-version: "3.10", tox-env: py310-no-ext }
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
# steps:
# - name: Checkout Repository
# uses: actions/checkout@v2
steps:
- name: Checkout Repository
uses: actions/checkout@v2
# - name: Run Unit Tests
# uses: ahopkins/custom-actions@pip-extra-args
# with:
# python-version: ${{ matrix.config.python-version }}
# test-infra-tool: tox
# test-infra-version: latest
# action: tests
# test-additional-args: "-e=${{ matrix.config.tox-env }}"
# experimental-ignore-error: "true"
# command-timeout: "600000"
# pip-extra-args: "--user"
- name: Run Unit Tests
uses: ahopkins/custom-actions@pip-extra-args
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
experimental-ignore-error: "true"
command-timeout: "600000"
pip-extra-args: "--user"

View File

@@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: true
matrix:
python-version: ["3.7", "3.8", "3.9"]
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- name: Checkout repository

View File

@@ -1,8 +1,26 @@
.. note::
CHANGELOG files are maintained in ``./docs/sanic/releases``. To view the full CHANGELOG, please visit https://sanic.readthedocs.io/en/stable/sanic/changelog.html.
Version 21.6.1
--------------
**Bugfixes**
* `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_
Update sanic-routing to allow for better splitting of complex URI templates
* `#2183 <https://github.com/sanic-org/sanic/pull/2183>`_
Proper handling of chunked request bodies to resolve phantom 503 in logs
* `#2181 <https://github.com/sanic-org/sanic/pull/2181>`_
Resolve regression in exception logging
* `#2201 <https://github.com/sanic-org/sanic/pull/2201>`_
Cleanup request info in pipelined requests
Version 21.6.0
--------------
Features
********
**Features**
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
Add ``response.eof()`` method for closing a stream in a handler
@@ -49,8 +67,7 @@ Features
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
Additional methods for attaching ``HTTPMethodView``
Bugfixes
********
**Bugfixes**
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
@@ -66,8 +83,7 @@ Bugfixes
Fix issue where Blueprint exception handlers do not consistently route to proper handler
Deprecations and Removals
*************************
**Deprecations and Removals**
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
@@ -76,14 +92,12 @@ Deprecations and Removals
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
Deprecate StreamingHTTPResponse
Developer infrastructure
************************
**Developer infrastructure**
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
Remove Travis CI in favor of GitHub Actions
Improved Documentation
**********************
**Improved Documentation**
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
Fix typo in documentation
@@ -93,8 +107,7 @@ Improved Documentation
Version 21.3.2
--------------
Bugfixes
********
**Bugfixes**
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
Disable response timeout on websocket connections
@@ -105,8 +118,7 @@ Bugfixes
Version 21.3.1
--------------
Bugfixes
********
**Bugfixes**
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
Static files inside subfolders are not accessible (404)
@@ -116,8 +128,7 @@ Version 21.3.0
`Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_
Features
********
**Features**
*
`#1876 <https://github.com/sanic-org/sanic/pull/1876>`_
@@ -170,8 +181,7 @@ Features
`#2063 <https://github.com/sanic-org/sanic/pull/2063>`_
App and connection level context objects
Bugfixes and issues resolved
****************************
**Bugfixes**
* Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_
``url_for`` where ``strict_slashes`` are on for a path ending in ``/``
@@ -201,8 +211,7 @@ Bugfixes and issues resolved
`#2001 <https://github.com/sanic-org/sanic/pull/2001>`_
Raise ValueError when cookie max-age is not an integer
Deprecations and Removals
*************************
**Deprecations and Removals**
*
`#2007 <https://github.com/sanic-org/sanic/pull/2007>`_
@@ -221,8 +230,7 @@ Deprecations and Removals
* ``Request.endpoint`` deprecated in favor of ``Request.name``
* handler type name prefixes removed (static, websocket, etc)
Developer infrastructure
************************
**Developer infrastructure**
*
`#1995 <https://github.com/sanic-org/sanic/pull/1995>`_
@@ -240,8 +248,7 @@ Developer infrastructure
`#2049 <https://github.com/sanic-org/sanic/pull/2049>`_
Updated setup.py to use ``find_packages``
Improved Documentation
**********************
**Improved Documentation**
*
`#1218 <https://github.com/sanic-org/sanic/pull/1218>`_
@@ -263,8 +270,7 @@ Improved Documentation
`#2052 <https://github.com/sanic-org/sanic/pull/2052>`_
Fix some examples and docs
Miscellaneous
*************
**Miscellaneous**
* ``Request.route`` property
* Better websocket subprotocols support
@@ -310,8 +316,7 @@ Miscellaneous
Version 20.12.3
---------------
Bugfixes
********
**Bugfixes**
*
`#2021 <https://github.com/sanic-org/sanic/pull/2021>`_
@@ -320,8 +325,7 @@ Bugfixes
Version 20.12.2
---------------
Dependencies
************
**Dependencies**
*
`#2026 <https://github.com/sanic-org/sanic/pull/2026>`_
@@ -334,8 +338,7 @@ Dependencies
Version 19.12.5
---------------
Dependencies
************
**Dependencies**
*
`#2025 <https://github.com/sanic-org/sanic/pull/2025>`_
@@ -348,8 +351,7 @@ Dependencies
Version 20.12.0
---------------
Features
********
**Features**
*
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
@@ -358,8 +360,7 @@ Features
Version 20.12.0
---------------
Features
********
**Features**
*
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
@@ -397,22 +398,19 @@ Features
`#1979 <https://github.com/sanic-org/sanic/pull/1979>`_
Add app registry and Sanic class level app retrieval
Bugfixes
********
**Bugfixes**
*
`#1965 <https://github.com/sanic-org/sanic/pull/1965>`_
Fix Chunked Transport-Encoding in ASGI streaming response
Deprecations and Removals
*************************
**Deprecations and Removals**
*
`#1981 <https://github.com/sanic-org/sanic/pull/1981>`_
Cleanup and remove deprecated code
Developer infrastructure
************************
**Developer infrastructure**
*
`#1956 <https://github.com/sanic-org/sanic/pull/1956>`_
@@ -426,8 +424,7 @@ Developer infrastructure
`#1986 <https://github.com/sanic-org/sanic/pull/1986>`_
Update tox requirements
Improved Documentation
**********************
**Improved Documentation**
*
`#1951 <https://github.com/sanic-org/sanic/pull/1951>`_
@@ -445,8 +442,7 @@ Improved Documentation
Version 20.9.1
---------------
Bugfixes
********
**Bugfixes**
*
`#1954 <https://github.com/sanic-org/sanic/pull/1954>`_
@@ -459,8 +455,7 @@ Bugfixes
Version 19.12.3
---------------
Bugfixes
********
**Bugfixes**
*
`#1959 <https://github.com/sanic-org/sanic/pull/1959>`_
@@ -471,8 +466,7 @@ Version 20.9.0
---------------
Features
********
**Features**
*
`#1887 <https://github.com/sanic-org/sanic/pull/1887>`_
@@ -499,22 +493,19 @@ Features
`#1937 <https://github.com/sanic-org/sanic/pull/1937>`_
Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto)
Bugfixes
********
**Bugfixes**
*
`#1897 <https://github.com/sanic-org/sanic/pull/1897>`_
Resolves exception from unread bytes in stream
Deprecations and Removals
*************************
**Deprecations and Removals**
*
`#1903 <https://github.com/sanic-org/sanic/pull/1903>`_
config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3
Developer infrastructure
************************
**Developer infrastructure**
*
`#1890 <https://github.com/sanic-org/sanic/pull/1890>`_,
@@ -529,8 +520,7 @@ Developer infrastructure
`#1924 <https://github.com/sanic-org/sanic/pull/1924>`_
Adding --strict-markers for pytest
Improved Documentation
**********************
**Improved Documentation**
*
`#1922 <https://github.com/sanic-org/sanic/pull/1922>`_
@@ -540,8 +530,7 @@ Improved Documentation
Version 20.6.3
---------------
Bugfixes
********
**Bugfixes**
*
`#1884 <https://github.com/sanic-org/sanic/pull/1884>`_
@@ -551,8 +540,7 @@ Bugfixes
Version 20.6.2
---------------
Features
********
**Features**
*
`#1641 <https://github.com/sanic-org/sanic/pull/1641>`_
@@ -562,8 +550,7 @@ Features
Version 20.6.1
---------------
Features
********
**Features**
*
`#1760 <https://github.com/sanic-org/sanic/pull/1760>`_
@@ -577,8 +564,7 @@ Features
`#1880 <https://github.com/sanic-org/sanic/pull/1880>`_
Add handler names for websockets for url_for usage
Bugfixes
********
**Bugfixes**
*
`#1776 <https://github.com/sanic-org/sanic/pull/1776>`_
@@ -600,15 +586,13 @@ Bugfixes
`#1853 <https://github.com/sanic-org/sanic/pull/1853>`_
Fix pickle error when attempting to pickle an application which contains websocket routes
Deprecations and Removals
*************************
**Deprecations and Removals**
*
`#1739 <https://github.com/sanic-org/sanic/pull/1739>`_
Deprecate body_bytes to merge into body
Developer infrastructure
************************
**Developer infrastructure**
*
`#1852 <https://github.com/sanic-org/sanic/pull/1852>`_
@@ -623,8 +607,7 @@ Developer infrastructure
Wrap run()'s "protocol" type annotation in Optional[]
Improved Documentation
**********************
**Improved Documentation**
*
`#1846 <https://github.com/sanic-org/sanic/pull/1846>`_
@@ -638,14 +621,13 @@ Improved Documentation
Version 20.6.0
---------------
*Released, but unintentionally ommitting PR #1880, so was replaced by 20.6.1*
*Released, but unintentionally omitting PR #1880, so was replaced by 20.6.1*
Version 20.3.0
---------------
Features
********
**Features**
*
`#1762 <https://github.com/sanic-org/sanic/pull/1762>`_
@@ -676,8 +658,7 @@ Features
`#1820 <https://github.com/sanic-org/sanic/pull/1820>`_
Do not set content-type and content-length headers in exceptions
Bugfixes
********
**Bugfixes**
*
`#1748 <https://github.com/sanic-org/sanic/pull/1748>`_
@@ -695,8 +676,7 @@ Bugfixes
`#1808 <https://github.com/sanic-org/sanic/pull/1808>`_
Fix Ctrl+C and tests on Windows
Deprecations and Removals
*************************
**Deprecations and Removals**
*
`#1800 <https://github.com/sanic-org/sanic/pull/1800>`_
@@ -714,8 +694,7 @@ Deprecations and Removals
`#1818 <https://github.com/sanic-org/sanic/pull/1818>`_
Complete deprecation of ``app.remove_route`` and ``request.raw_args``
Dependencies
************
**Dependencies**
*
`#1794 <https://github.com/sanic-org/sanic/pull/1794>`_
@@ -725,15 +704,13 @@ Dependencies
`#1806 <https://github.com/sanic-org/sanic/pull/1806>`_
Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation)
Developer infrastructure
************************
**Developer infrastructure**
*
`#1833 <https://github.com/sanic-org/sanic/pull/1833>`_
Resolve broken documentation builds
Improved Documentation
**********************
**Improved Documentation**
*
`#1755 <https://github.com/sanic-org/sanic/pull/1755>`_
@@ -775,8 +752,7 @@ Improved Documentation
Version 19.12.0
---------------
Bugfixes
********
**Bugfixes**
- Fix blueprint middleware application
@@ -795,8 +771,7 @@ Bugfixes
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/sanic-org/sanic/issues/1707>`__)
Improved Documentation
**********************
**Improved Documentation**
- Move docs from MD to RST
@@ -810,8 +785,7 @@ Improved Documentation
Version 19.6.3
--------------
Features
********
**Features**
- Enable Towncrier Support
@@ -819,8 +793,7 @@ Features
of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/sanic-org/sanic/issues/1631>`__)
Improved Documentation
**********************
**Improved Documentation**
- Documentation infrastructure changes
@@ -833,8 +806,7 @@ Improved Documentation
Version 19.6.2
--------------
Features
********
**Features**
*
`#1562 <https://github.com/sanic-org/sanic/pull/1562>`_
@@ -850,8 +822,7 @@ Features
Add Configure support from object string
Bugfixes
********
**Bugfixes**
*
`#1587 <https://github.com/sanic-org/sanic/pull/1587>`_
@@ -869,8 +840,7 @@ Bugfixes
`#1594 <https://github.com/sanic-org/sanic/pull/1594>`_
Strict Slashes behavior fix
Deprecations and Removals
*************************
**Deprecations and Removals**
*
`#1544 <https://github.com/sanic-org/sanic/pull/1544>`_
@@ -894,8 +864,7 @@ Deprecations and Removals
Version 19.3
------------
Features
********
**Features**
*
`#1497 <https://github.com/sanic-org/sanic/pull/1497>`_
@@ -963,8 +932,7 @@ Features
This is a breaking change.
Bugfixes
********
**Bugfixes**
*
@@ -1000,8 +968,7 @@ Bugfixes
This allows the access log to be disabled for example when running via
gunicorn.
Developer infrastructure
************************
**Developer infrastructure**
* `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials
* `#1515 <https://github.com/sanic-org/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514)
@@ -1009,8 +976,7 @@ Developer infrastructure
* `#1478 <https://github.com/sanic-org/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build
* `#1464 <https://github.com/sanic-org/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests
Improved Documentation
**********************
**Improved Documentation**
* `#1516 <https://github.com/sanic-org/sanic/pull/1516>`_ Fix typo at the exception documentation
* `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example
@@ -1071,21 +1037,19 @@ Version 18.12
* Fix Range header handling for static files (#1402)
* Fix the logger and make it work (#1397)
* Fix type pikcle->pickle in multiprocessing test
* 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).
* 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 requirement 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).
* Fix document for logging
Version 0.8
-----------
0.8.3
*****
**0.8.3**
* Changes:
* Ownership changed to org 'sanic-org'
0.8.0
*****
**0.8.0**
* Changes:
@@ -1110,7 +1074,7 @@ Version 0.8
* 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)
* KeepAlive Timeout 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)
@@ -1165,19 +1129,16 @@ Version 0.1
-----------
0.1.7
*****
**0.1.7**
* Reversed static url and directory arguments to meet spec
0.1.6
*****
**0.1.6**
* Static files
* Lazy Cookie Loading
0.1.5
*****
**0.1.5**
* Cookies
* Blueprint listeners and ordering
@@ -1185,23 +1146,19 @@ Version 0.1
* 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
*****
**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
*****
**0.1.0**
* Released to public

View File

@@ -19,7 +19,7 @@ a virtual environment already set up, then run:
.. code-block:: bash
pip3 install -e . ".[dev]"
pip install -e ".[dev]"
Dependency Changes
------------------

View File

@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
:stub-columns: 1
* - Build
- | |Py39Test| |Py38Test| |Py37Test| |Codecov|
- | |Py310Test| |Py39Test| |Py38Test| |Py37Test|
* - Docs
- | |UserGuide| |Documentation|
* - Package
@@ -27,8 +27,8 @@ Sanic | Build fast. Run fast.
:target: https://community.sanicframework.org/
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
:target: https://discord.gg/FARQzAEMAA
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/sanic-org/sanic
.. |Py310Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
@@ -77,17 +77,11 @@ The goal of the project is to provide a simple way to get up and running a highl
Sponsor
-------
|Try CodeStream|
Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
.. |Try CodeStream| image:: https://alt-images.codestream.com/codestream_logo_sanicorg.png
:target: https://codestream.com/?utm_source=github&amp;utm_campaign=sanicorg&amp;utm_medium=banner
:alt: Try CodeStream
Thanks to `Linode <https://www.linode.com>`_ for their contribution towards the development and community of Sanic.
Manage pull requests and conduct code reviews in your IDE with full source-tree context. Comment on any line, not just the diffs. Use jump-to-definition, your favorite keybindings, and code intelligence with more of your workflow.
`Learn More <https://codestream.com/?utm_source=github&amp;utm_campaign=sanicorg&amp;utm_medium=banner>`_
Thank you to our sponsor. Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
|Linode|
Installation
------------
@@ -172,3 +166,8 @@ Contribution
------------
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_.
.. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg
:alt: Linode
:target: https://www.linode.com
:width: 200px

28
codecov.yml Normal file
View File

@@ -0,0 +1,28 @@
coverage:
status:
patch:
default:
target: auto
threshold: 0.75
informational: true
project:
default:
target: auto
threshold: 0.5
precision: 3
codecov:
require_ci_to_pass: false
ignore:
- "sanic/__main__.py"
- "sanic/compat.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
- "sanic/cli"
- ".github/"
- "changelogs/"
- "docker/"
- "docs/"
- "examples/"
- "scripts/"
- "tests/"

View File

@@ -10,10 +10,8 @@
import os
import sys
# Add support for auto-doc
import recommonmark
from recommonmark.transform import AutoStructify
# Add support for auto-doc
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
@@ -26,7 +24,7 @@ import sanic
# -- General configuration ------------------------------------------------
extensions = ["sphinx.ext.autodoc", "recommonmark"]
extensions = ["sphinx.ext.autodoc", "m2r2"]
templates_path = ["_templates"]
@@ -162,20 +160,6 @@ autodoc_default_options = {
"member-order": "groupwise",
}
# app setup hook
def setup(app):
app.add_config_value(
"recommonmark_config",
{
"enable_eval_rst": True,
"enable_auto_doc_ref": False,
},
True,
)
app.add_transform(AutoStructify)
html_theme_options = {
"style_external_links": False,
}

View File

@@ -38,10 +38,3 @@ sanic.views
.. automodule:: sanic.views
:members:
:show-inheritance:
sanic.websocket
---------------
.. automodule:: sanic.websocket
:members:
:show-inheritance:

View File

@@ -1,4 +1,6 @@
📜 Changelog
============
.. mdinclude:: ./releases/21/21.12.md
.. mdinclude:: ./releases/21/21.9.md
.. include:: ../../CHANGELOG.rst

View File

@@ -1,4 +1,4 @@
♥️ Contributing
==============
===============
.. include:: ../../CONTRIBUTING.rst

View File

@@ -0,0 +1,64 @@
## Version 21.12.1
- [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup
- [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7
- [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values
## Version 21.12.0
### Features
- [#2260](https://github.com/sanic-org/sanic/pull/2260) Allow early Blueprint registrations to still apply later added objects
- [#2262](https://github.com/sanic-org/sanic/pull/2262) Noisy exceptions - force logging of all exceptions
- [#2264](https://github.com/sanic-org/sanic/pull/2264) Optional `uvloop` by configuration
- [#2270](https://github.com/sanic-org/sanic/pull/2270) Vhost support using multiple TLS certificates
- [#2277](https://github.com/sanic-org/sanic/pull/2277) Change signal routing for increased consistency
- *BREAKING CHANGE*: If you were manually routing signals there is a breaking change. The signal router's `get` is no longer 100% determinative. There is now an additional step to loop thru the returned signals for proper matching on the requirements. If signals are being dispatched using `app.dispatch` or `bp.dispatch`, there is no change.
- [#2290](https://github.com/sanic-org/sanic/pull/2290) Add contextual exceptions
- [#2291](https://github.com/sanic-org/sanic/pull/2291) Increase join concat performance
- [#2295](https://github.com/sanic-org/sanic/pull/2295), [#2316](https://github.com/sanic-org/sanic/pull/2316), [#2331](https://github.com/sanic-org/sanic/pull/2331) Restructure of CLI and application state with new displays and more command parity with `app.run`
- [#2302](https://github.com/sanic-org/sanic/pull/2302) Add route context at definition time
- [#2304](https://github.com/sanic-org/sanic/pull/2304) Named tasks and new API for managing background tasks
- [#2307](https://github.com/sanic-org/sanic/pull/2307) On app auto-reload, provide insight of changed files
- [#2308](https://github.com/sanic-org/sanic/pull/2308) Auto extend application with [Sanic Extensions](https://sanicframework.org/en/plugins/sanic-ext/getting-started.html) if it is installed, and provide first class support for accessing the extensions
- [#2309](https://github.com/sanic-org/sanic/pull/2309) Builtin signals changed to `Enum`
- [#2313](https://github.com/sanic-org/sanic/pull/2313) Support additional config implementation use case
- [#2321](https://github.com/sanic-org/sanic/pull/2321) Refactor environment variable hydration logic
- [#2327](https://github.com/sanic-org/sanic/pull/2327) Prevent sending multiple or mixed responses on a single request
- [#2330](https://github.com/sanic-org/sanic/pull/2330) Custom type casting on environment variables
- [#2332](https://github.com/sanic-org/sanic/pull/2332) Make all deprecation notices consistent
- [#2335](https://github.com/sanic-org/sanic/pull/2335) Allow underscore to start instance names
### Bugfixes
- [#2273](https://github.com/sanic-org/sanic/pull/2273) Replace assignation by typing for `websocket_handshake`
- [#2285](https://github.com/sanic-org/sanic/pull/2285) Fix IPv6 display in startup logs
- [#2299](https://github.com/sanic-org/sanic/pull/2299) Dispatch `http.lifecyle.response` from exception handler
### Deprecations and Removals
- [#2306](https://github.com/sanic-org/sanic/pull/2306) Removal of deprecated items
- `Sanic` and `Blueprint` may no longer have arbitrary properties attached to them
- `Sanic` and `Blueprint` forced to have compliant names
- alphanumeric + `_` + `-`
- must start with letter or `_`
- `load_env` keyword argument of `Sanic`
- `sanic.exceptions.abort`
- `sanic.views.CompositionView`
- `sanic.response.StreamingHTTPResponse`
- *NOTE:* the `stream()` response method (where you pass a callable streaming function) has been deprecated and will be removed in v22.6. You should upgrade all streaming responses to the new style: https://sanicframework.org/en/guide/advanced/streaming.html#response-streaming
- [#2320](https://github.com/sanic-org/sanic/pull/2320) Remove app instance from Config for error handler setting
### Developer infrastructure
- [#2251](https://github.com/sanic-org/sanic/pull/2251) Change dev install command
- [#2286](https://github.com/sanic-org/sanic/pull/2286) Change codeclimate complexity threshold from 5 to 10
- [#2287](https://github.com/sanic-org/sanic/pull/2287) Update host test function names so they are not overwritten
- [#2292](https://github.com/sanic-org/sanic/pull/2292) Fail CI on error
- [#2311](https://github.com/sanic-org/sanic/pull/2311), [#2324](https://github.com/sanic-org/sanic/pull/2324) Do not run tests for draft PRs
- [#2336](https://github.com/sanic-org/sanic/pull/2336) Remove paths from coverage checks
- [#2338](https://github.com/sanic-org/sanic/pull/2338) Cleanup ports on tests
### Improved Documentation
- [#2269](https://github.com/sanic-org/sanic/pull/2269), [#2329](https://github.com/sanic-org/sanic/pull/2329), [#2333](https://github.com/sanic-org/sanic/pull/2333) Cleanup typos and fix language
### Miscellaneous
- [#2257](https://github.com/sanic-org/sanic/pull/2257), [#2294](https://github.com/sanic-org/sanic/pull/2294), [#2341](https://github.com/sanic-org/sanic/pull/2341) Add Python 3.10 support
- [#2279](https://github.com/sanic-org/sanic/pull/2279), [#2317](https://github.com/sanic-org/sanic/pull/2317), [#2322](https://github.com/sanic-org/sanic/pull/2322) Add/correct missing type annotations
- [#2305](https://github.com/sanic-org/sanic/pull/2305) Fix examples to use modern implementations

View File

@@ -0,0 +1,50 @@
## Version 21.9.3
*Rerelease of v21.9.2 with some cleanup*
## Version 21.9.2
- [#2268](https://github.com/sanic-org/sanic/pull/2268) Make HTTP connections start in IDLE stage, avoiding delays and error messages
- [#2310](https://github.com/sanic-org/sanic/pull/2310) More consistent config setting with post-FALLBACK_ERROR_FORMAT apply
## Version 21.9.1
- [#2259](https://github.com/sanic-org/sanic/pull/2259) Allow non-conforming ErrorHandlers
## Version 21.9.0
### Features
- [#2158](https://github.com/sanic-org/sanic/pull/2158), [#2248](https://github.com/sanic-org/sanic/pull/2248) Complete overhaul of I/O to websockets
- [#2160](https://github.com/sanic-org/sanic/pull/2160) Add new 17 signals into server and request lifecycles
- [#2162](https://github.com/sanic-org/sanic/pull/2162) Smarter `auto` fallback formatting upon exception
- [#2184](https://github.com/sanic-org/sanic/pull/2184) Introduce implementation for copying a Blueprint
- [#2200](https://github.com/sanic-org/sanic/pull/2200) Accept header parsing
- [#2207](https://github.com/sanic-org/sanic/pull/2207) Log remote address if available
- [#2209](https://github.com/sanic-org/sanic/pull/2209) Add convenience methods to BP groups
- [#2216](https://github.com/sanic-org/sanic/pull/2216) Add default messages to SanicExceptions
- [#2225](https://github.com/sanic-org/sanic/pull/2225) Type annotation convenience for annotated handlers with path parameters
- [#2236](https://github.com/sanic-org/sanic/pull/2236) Allow Falsey (but not-None) responses from route handlers
- [#2238](https://github.com/sanic-org/sanic/pull/2238) Add `exception` decorator to Blueprint Groups
- [#2244](https://github.com/sanic-org/sanic/pull/2244) Explicit static directive for serving file or dir (ex: `static(..., resource_type="file")`)
- [#2245](https://github.com/sanic-org/sanic/pull/2245) Close HTTP loop when connection task cancelled
### Bugfixes
- [#2188](https://github.com/sanic-org/sanic/pull/2188) Fix the handling of the end of a chunked request
- [#2195](https://github.com/sanic-org/sanic/pull/2195) Resolve unexpected error handling on static requests
- [#2208](https://github.com/sanic-org/sanic/pull/2208) Make blueprint-based exceptions attach and trigger in a more intuitive manner
- [#2211](https://github.com/sanic-org/sanic/pull/2211) Fixed for handling exceptions of asgi app call
- [#2213](https://github.com/sanic-org/sanic/pull/2213) Fix bug where ws exceptions not being logged
- [#2231](https://github.com/sanic-org/sanic/pull/2231) Cleaner closing of tasks by using `abort()` in strategic places to avoid dangling sockets
- [#2247](https://github.com/sanic-org/sanic/pull/2247) Fix logging of auto-reload status in debug mode
- [#2246](https://github.com/sanic-org/sanic/pull/2246) Account for BP with exception handler but no routes
### Developer infrastructure
- [#2194](https://github.com/sanic-org/sanic/pull/2194) HTTP unit tests with raw client
- [#2199](https://github.com/sanic-org/sanic/pull/2199) Switch to codeclimate
- [#2214](https://github.com/sanic-org/sanic/pull/2214) Try Reopening Windows Tests
- [#2229](https://github.com/sanic-org/sanic/pull/2229) Refactor `HttpProtocol` into a base class
- [#2230](https://github.com/sanic-org/sanic/pull/2230) Refactor `server.py` into multi-file module
### Miscellaneous
- [#2173](https://github.com/sanic-org/sanic/pull/2173) Remove Duplicated Dependencies and PEP 517 Support
- [#2193](https://github.com/sanic-org/sanic/pull/2193), [#2196](https://github.com/sanic-org/sanic/pull/2196), [#2217](https://github.com/sanic-org/sanic/pull/2217) Type annotation changes

View File

@@ -4,12 +4,14 @@ import asyncio
from sanic import Sanic
app = Sanic()
app = Sanic("Example")
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print('Server successfully started!')
print("Server successfully started!")
app.add_task(notify_server_started_after_five_seconds())

View File

@@ -1,30 +1,29 @@
from sanic import Sanic
from sanic.response import text
from random import randint
app = Sanic()
from sanic import Sanic
from sanic.response import text
@app.middleware('request')
app = Sanic("Example")
@app.middleware("request")
def append_request(request):
# Add new key with random value
request['num'] = randint(0, 100)
request.ctx.num = randint(0, 100)
@app.get('/pop')
@app.get("/pop")
def pop_handler(request):
# Pop key from request object
num = request.pop('num')
return text(num)
return text(request.ctx.num)
@app.get('/key_exist')
@app.get("/key_exist")
def key_exist_handler(request):
# Check the key is exist or not
if 'num' in request:
return text('num exist in request')
if hasattr(request.ctx, "num"):
return text("num exist in request")
return text('num does not exist in reqeust')
return text("num does not exist in request")
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
from sanic import Sanic
from functools import wraps
from sanic import Sanic
from sanic.response import json
app = Sanic()
app = Sanic("Example")
def check_request_for_authorization_status(request):
@@ -27,14 +29,16 @@ def authorized(f):
return response
else:
# the user is not authorized.
return json({'status': 'not_authorized'}, 403)
return json({"status": "not_authorized"}, 403)
return decorated_function
@app.route("/")
@authorized
async def test(request):
return json({'status': 'authorized'})
return json({"status": "authorized"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,43 +1,53 @@
from sanic import Sanic, Blueprint
from sanic import Blueprint, Sanic
from sanic.response import text
'''
Demonstrates that blueprint request middleware are executed in the order they
"""
Demonstrates that blueprint request middleware are executed in the order they
are added. And blueprint response middleware are executed in _reverse_ order.
On a valid request, it should print "1 2 3 6 5 4" to terminal
'''
"""
app = Sanic(__name__)
app = Sanic("Example")
bp = Blueprint("bp_"+__name__)
bp = Blueprint("bp_example")
@bp.middleware('request')
@bp.on_request
def request_middleware_1(request):
print('1')
print("1")
@bp.middleware('request')
@bp.on_request
def request_middleware_2(request):
print('2')
print("2")
@bp.middleware('request')
@bp.on_request
def request_middleware_3(request):
print('3')
print("3")
@bp.middleware('response')
@bp.on_response
def resp_middleware_4(request, response):
print('4')
print("4")
@bp.middleware('response')
@bp.on_response
def resp_middleware_5(request, response):
print('5')
print("5")
@bp.middleware('response')
@bp.on_response
def resp_middleware_6(request, response):
print('6')
print("6")
@bp.route('/')
@bp.route("/")
def pop_handler(request):
return text('hello world')
return text("hello world")
app.blueprint(bp, url_prefix='/bp')
app.blueprint(bp, url_prefix="/bp")
app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False)

View File

@@ -1,10 +1,11 @@
from sanic import Blueprint, Sanic
from sanic.response import file, json
app = Sanic(__name__)
blueprint = Blueprint("name", url_prefix="/my_blueprint")
blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2")
blueprint3 = Blueprint("name3", url_prefix="/my_blueprint3")
app = Sanic("Example")
blueprint = Blueprint("bp_example", url_prefix="/my_blueprint")
blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2")
blueprint3 = Blueprint("bp_example3", url_prefix="/my_blueprint3")
@blueprint.route("/foo")

View File

@@ -2,17 +2,20 @@ from asyncio import sleep
from sanic import Sanic, response
app = Sanic(__name__, strict_slashes=True)
app = Sanic("DelayedResponseApp", strict_slashes=True)
@app.get("/")
async def handler(request):
return response.redirect("/sleep/3")
@app.get("/sleep/<t:number>")
async def handler2(request, t=0.3):
await sleep(t)
return response.text(f"Slept {t:.1f} seconds.\n")
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@@ -7,8 +7,10 @@ and pass in an instance of it when we create our Sanic instance. Inside this
class' default handler, we can do anything including sending exceptions to
an external service.
"""
from sanic.handlers import ErrorHandler
from sanic.exceptions import SanicException
from sanic.handlers import ErrorHandler
"""
Imports and code relevant for our CustomHandler class
(Ordinarily this would be in a separate file)
@@ -16,7 +18,6 @@ Imports and code relevant for our CustomHandler class
class CustomHandler(ErrorHandler):
def default(self, request, exception):
# Here, we have access to the exception object
# and can do anything with it (log, send to external service, etc)
@@ -38,17 +39,17 @@ server's error_handler to an instance of our CustomHandler
from sanic import Sanic
app = Sanic(__name__)
handler = CustomHandler()
app.error_handler = handler
app = Sanic("Example", error_handler=handler)
@app.route("/")
async def test(request):
# Here, something occurs which causes an unexpected exception
# This exception will flow to our custom handler.
raise SanicException('You Broke It!')
raise SanicException("You Broke It!")
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,7 +1,7 @@
from sanic import Sanic
from sanic import response
from sanic import Sanic, response
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/")
@@ -9,5 +9,5 @@ async def test(request):
return response.json({"test": True})
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,4 +1,6 @@
from sanic import Sanic, response, text
from sanic.handlers import ErrorHandler
from sanic.server.async_server import AsyncioServer
HTTP_PORT = 9999
@@ -32,20 +34,40 @@ def proxy(request, path):
return response.redirect(url)
@https.listener("main_process_start")
@https.main_process_start
async def start(app, _):
global http
app.http_server = await http.create_server(
http_server = await http.create_server(
port=HTTP_PORT, return_asyncio_server=True
)
app.http_server.after_start()
app.add_task(runner(http, http_server))
app.ctx.http_server = http_server
app.ctx.http = http
@https.listener("main_process_stop")
@https.main_process_stop
async def stop(app, _):
app.http_server.before_stop()
await app.http_server.close()
app.http_server.after_stop()
await app.ctx.http_server.before_stop()
await app.ctx.http_server.close()
for connection in app.ctx.http_server.connections:
connection.close_if_idle()
await app.ctx.http_server.after_stop()
app.ctx.http = False
async def runner(app: Sanic, app_server: AsyncioServer):
app.is_running = True
try:
app.signalize()
app.finalize()
ErrorHandler.finalize(app.error_handler)
app_server.init = True
await app_server.before_start()
await app_server.after_start()
await app_server.serve_forever()
finally:
app.is_running = False
app.is_stopping = True
https.run(port=HTTPS_PORT, debug=True)

View File

@@ -1,26 +1,30 @@
import asyncio
import httpx
from sanic import Sanic
from sanic.response import json
import asyncio
import aiohttp
app = Sanic(__name__)
app = Sanic("Example")
sem = None
@app.listener('before_server_start')
def init(sanic, loop):
@app.before_server_start
def init(sanic, _):
global sem
concurrency_per_worker = 4
sem = asyncio.Semaphore(concurrency_per_worker, loop=loop)
sem = asyncio.Semaphore(concurrency_per_worker)
async def bounded_fetch(session, url):
"""
Use session object to perform 'get' request on url
"""
async with sem, session.get(url) as response:
return await response.json()
async with sem:
response = await session.get(url)
return response.json()
@app.route("/")
@@ -28,9 +32,9 @@ async def test(request):
"""
Download and serve example JSON
"""
url = "https://api.github.com/repos/channelcat/sanic"
url = "https://api.github.com/repos/sanic-org/sanic"
async with aiohttp.ClientSession() as session:
async with httpx.AsyncClient() as session:
response = await bounded_fetch(session, url)
return json(response)

View File

@@ -1,6 +1,6 @@
import logging
import aiotask_context as context
from contextvars import ContextVar
from sanic import Sanic, response
@@ -11,8 +11,8 @@ log = logging.getLogger(__name__)
class RequestIdFilter(logging.Filter):
def filter(self, record):
try:
record.request_id = context.get("X-Request-ID")
except ValueError:
record.request_id = app.ctx.request_id.get(None) or "n/a"
except AttributeError:
record.request_id = "n/a"
return True
@@ -44,13 +44,12 @@ LOG_SETTINGS = {
}
app = Sanic(__name__, log_config=LOG_SETTINGS)
app = Sanic("Example", log_config=LOG_SETTINGS)
@app.on_request
async def set_request_id(request):
request_id = request.id
context.set("X-Request-ID", request_id)
request.app.ctx.request_id.set(request.id)
log.info(f"Setting {request.id=}")
@@ -61,14 +60,14 @@ async def set_request_header(request, response):
@app.route("/")
async def test(request):
log.debug("X-Request-ID: %s", context.get("X-Request-ID"))
log.debug("X-Request-ID: %s", request.id)
log.info("Hello from test!")
return response.json({"test": True})
@app.before_server_start
def setup(app, loop):
loop.set_task_factory(context.task_factory)
app.ctx.request_id = ContextVar("request_id")
if __name__ == "__main__":

View File

@@ -1,5 +1,6 @@
import logging
import socket
from os import getenv
from platform import node
from uuid import getnode as get_mac
@@ -7,10 +8,11 @@ from uuid import getnode as get_mac
from logdna import LogDNAHandler
from sanic import Sanic
from sanic.response import json
from sanic.request import Request
from sanic.response import json
log = logging.getLogger('logdna')
log = logging.getLogger("logdna")
log.setLevel(logging.INFO)
@@ -30,16 +32,18 @@ logdna_options = {
"index_meta": True,
"hostname": node(),
"ip": get_my_ip_address(),
"mac": get_mac_address()
"mac": get_mac_address(),
}
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
logdna_handler = LogDNAHandler(
getenv("LOGDNA_API_KEY"), options=logdna_options
)
logdna = logging.getLogger(__name__)
logdna.setLevel(logging.INFO)
logdna.addHandler(logdna_handler)
app = Sanic(__name__)
app = Sanic("Example")
@app.middleware
@@ -49,13 +53,8 @@ def log_request(request: Request):
@app.route("/")
def default(request):
return json({
"response": "I was here"
})
return json({"response": "I was here"})
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)
app.run(host="0.0.0.0", port=getenv("PORT", 8080))

View File

@@ -2,27 +2,29 @@
Modify header or status in response
"""
from sanic import Sanic
from sanic import response
app = Sanic(__name__)
from sanic import Sanic, response
@app.route('/')
app = Sanic("Example")
@app.route("/")
def handle_request(request):
return response.json(
{'message': 'Hello world!'},
headers={'X-Served-By': 'sanic'},
status=200
{"message": "Hello world!"},
headers={"X-Served-By": "sanic"},
status=200,
)
@app.route('/unauthorized')
@app.route("/unauthorized")
def handle_request(request):
return response.json(
{'message': 'You are not authorized'},
headers={'X-Served-By': 'sanic'},
status=404
{"message": "You are not authorized"},
headers={"X-Served-By": "sanic"},
status=404,
)
app.run(host="0.0.0.0", port=8000, debug=True)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -32,7 +32,7 @@ def test_port(worker_id):
@pytest.fixture(scope="session")
def app():
app = Sanic()
app = Sanic("Example")
@app.route("/")
async def index(request):

View File

@@ -8,7 +8,6 @@ from sanic.handlers import ErrorHandler
class RaygunExceptionReporter(ErrorHandler):
def __init__(self, raygun_api_key=None):
super().__init__()
if raygun_api_key is None:
@@ -22,16 +21,13 @@ class RaygunExceptionReporter(ErrorHandler):
raygun_error_reporter = RaygunExceptionReporter()
app = Sanic(__name__, error_handler=raygun_error_reporter)
app = Sanic("Example", error_handler=raygun_error_reporter)
@app.route("/raise")
async def test(request):
raise SanicException('You Broke It!')
raise SanicException("You Broke It!")
if __name__ == '__main__':
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=getenv("PORT", 8080))

View File

@@ -1,18 +1,18 @@
from sanic import Sanic
from sanic import response
from sanic import Sanic, response
app = Sanic(__name__)
@app.route('/')
app = Sanic("Example")
@app.route("/")
def handle_request(request):
return response.redirect('/redirect')
return response.redirect("/redirect")
@app.route('/redirect')
@app.route("/redirect")
async def test(request):
return response.json({"Redirected": True})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,65 +1,63 @@
from sanic import Sanic
from sanic.views import CompositionView
from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator
from sanic.blueprints import Blueprint
from sanic.response import stream, text
from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator
bp = Blueprint('blueprint_request_stream')
app = Sanic('request_stream')
bp = Blueprint("bp_example")
app = Sanic("Example")
class SimpleView(HTTPMethodView):
@stream_decorator
async def post(self, request):
result = ''
result = ""
while True:
body = await request.stream.get()
if body is None:
break
result += body.decode('utf-8')
result += body.decode("utf-8")
return text(result)
@app.post('/stream', stream=True)
@app.post("/stream", stream=True)
async def handler(request):
async def streaming(response):
while True:
body = await request.stream.get()
if body is None:
break
body = body.decode('utf-8').replace('1', 'A')
body = body.decode("utf-8").replace("1", "A")
await response.write(body)
return stream(streaming)
@bp.put('/bp_stream', stream=True)
@bp.put("/bp_stream", stream=True)
async def bp_handler(request):
result = ''
result = ""
while True:
body = await request.stream.get()
if body is None:
break
result += body.decode('utf-8').replace('1', 'A')
result += body.decode("utf-8").replace("1", "A")
return text(result)
async def post_handler(request):
result = ''
result = ""
while True:
body = await request.stream.get()
if body is None:
break
result += body.decode('utf-8')
result += body.decode("utf-8")
return text(result)
app.blueprint(bp)
app.add_route(SimpleView.as_view(), '/method_view')
view = CompositionView()
view.add(['POST'], post_handler, stream=True)
app.add_route(view, '/composition_view')
app.add_route(SimpleView.as_view(), "/method_view")
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,21 +1,23 @@
import asyncio
from sanic import Sanic
from sanic import response
from sanic import Sanic, response
from sanic.config import Config
from sanic.exceptions import RequestTimeout
Config.REQUEST_TIMEOUT = 1
app = Sanic(__name__)
app = Sanic("Example")
@app.route('/')
@app.route("/")
async def test(request):
await asyncio.sleep(3)
return response.text('Hello, world!')
return response.text("Hello, world!")
@app.exception(RequestTimeout)
def timeout(request, exception):
return response.text('RequestTimeout from error_handler.', 408)
return response.text("RequestTimeout from error_handler.", 408)
app.run(host='0.0.0.0', port=8000)
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,21 +1,22 @@
from os import getenv
import rollbar
from sanic.handlers import ErrorHandler
from sanic import Sanic
from sanic.exceptions import SanicException
from os import getenv
from sanic.handlers import ErrorHandler
rollbar.init(getenv("ROLLBAR_API_KEY"))
class RollbarExceptionHandler(ErrorHandler):
def default(self, request, exception):
rollbar.report_message(str(exception))
return super().default(request, exception)
app = Sanic(__name__, error_handler=RollbarExceptionHandler())
app = Sanic("Example", error_handler=RollbarExceptionHandler())
@app.route("/raise")
@@ -24,7 +25,4 @@ def create_error(request):
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)
app.run(host="0.0.0.0", port=getenv("PORT", 8080))

View File

@@ -11,7 +11,7 @@ from pathlib import Path
from sanic import Sanic, response
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/text")
@@ -59,31 +59,31 @@ async def handler_stream(request):
return response.stream(body)
@app.listener("before_server_start")
@app.before_server_start
async def listener_before_server_start(*args, **kwargs):
print("before_server_start")
@app.listener("after_server_start")
@app.after_server_start
async def listener_after_server_start(*args, **kwargs):
print("after_server_start")
@app.listener("before_server_stop")
@app.before_server_stop
async def listener_before_server_stop(*args, **kwargs):
print("before_server_stop")
@app.listener("after_server_stop")
@app.after_server_stop
async def listener_after_server_stop(*args, **kwargs):
print("after_server_stop")
@app.middleware("request")
@app.on_request
async def print_on_request(request):
print("print_on_request")
@app.middleware("response")
@app.on_response
async def print_on_response(request, response):
print("print_on_response")

View File

@@ -1,22 +1,30 @@
from sanic import Sanic
from sanic import response
from signal import signal, SIGINT
import asyncio
import uvloop
app = Sanic(__name__)
from sanic import Sanic, response
app = Sanic("Example")
@app.route("/")
async def test(request):
return response.json({"answer": "42"})
asyncio.set_event_loop(uvloop.new_event_loop())
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
signal(SIGINT, lambda s, f: loop.stop())
try:
loop.run_forever()
except:
loop.stop()
async def main():
server = await app.create_server(
port=8000, host="0.0.0.0", return_asyncio_server=True
)
if server is None:
return
await server.startup()
await server.serve_forever()
if __name__ == "__main__":
asyncio.set_event_loop(uvloop.new_event_loop())
asyncio.run(main())

View File

@@ -1,32 +1,62 @@
from sanic import Sanic
from sanic import response
from signal import signal, SIGINT
import asyncio
from signal import SIGINT, signal
import uvloop
app = Sanic(__name__)
from sanic import Sanic, response
from sanic.server import AsyncioServer
app = Sanic("Example")
@app.before_server_start
async def before_server_start(app, loop):
print("Async Server starting")
@app.after_server_start
async def after_server_start(app, loop):
print("Async Server started")
@app.before_server_stop
async def before_server_stop(app, loop):
print("Async Server stopping")
@app.after_server_stop
async def after_server_stop(app, loop):
print("Async Server stopped")
@app.listener('after_server_start')
async def after_start_test(app, loop):
print("Async Server Started!")
@app.route("/")
async def test(request):
return response.json({"answer": "42"})
asyncio.set_event_loop(uvloop.new_event_loop())
serv_coro = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
serv_coro = app.create_server(
host="0.0.0.0", port=8000, return_asyncio_server=True
)
loop = asyncio.get_event_loop()
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
signal(SIGINT, lambda s, f: loop.stop())
server = loop.run_until_complete(serv_task)
server.after_start()
server: AsyncioServer = loop.run_until_complete(serv_task)
loop.run_until_complete(server.startup())
# When using app.run(), this actually triggers before the serv_coro.
# But, in this example, we are using the convenience method, even if it is
# out of order.
loop.run_until_complete(server.before_start())
loop.run_until_complete(server.after_start())
try:
loop.run_forever()
except KeyboardInterrupt as e:
except KeyboardInterrupt:
loop.stop()
finally:
server.before_stop()
loop.run_until_complete(server.before_stop())
# Wait for server to close
close_task = server.close()
@@ -35,4 +65,4 @@ finally:
# Complete all tasks on the loop
for connection in server.connections:
connection.close_if_idle()
server.after_stop()
loop.run_until_complete(server.after_stop())

View File

@@ -6,20 +6,19 @@ from sentry_sdk.integrations.sanic import SanicIntegration
from sanic import Sanic
from sanic.response import json
sentry_init(
dsn=getenv("SENTRY_DSN"),
integrations=[SanicIntegration()],
)
app = Sanic(__name__)
app = Sanic("Example")
# noinspection PyUnusedLocal
@app.route("/working")
async def working_path(request):
return json({
"response": "Working API Response"
})
return json({"response": "Working API Response"})
# noinspection PyUnusedLocal
@@ -28,8 +27,5 @@ async def raise_error(request):
raise Exception("Testing Sentry Integration")
if __name__ == '__main__':
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=getenv("PORT", 8080))

View File

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

View File

@@ -1,6 +1,6 @@
from sanic import Sanic
app = Sanic(__name__)
app = Sanic("Example")
app.static("/", "./static")

View File

@@ -1,13 +1,14 @@
from sanic import Sanic
from sanic import response as res
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/")
async def test(req):
return res.text("I\'m a teapot", status=418)
return res.text("I'm a teapot", status=418)
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,11 +1,11 @@
import os
from sanic import Sanic
from sanic.log import logger as log
from sanic import response
from sanic import Sanic, response
from sanic.exceptions import ServerError
from sanic.log import logger as log
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/")
@@ -13,7 +13,7 @@ async def test_async(request):
return response.json({"test": True})
@app.route("/sync", methods=['GET', 'POST'])
@app.route("/sync", methods=["GET", "POST"])
def test_sync(request):
return response.json({"test": True})
@@ -31,6 +31,7 @@ def exception(request):
@app.route("/await")
async def test_await(request):
import asyncio
await asyncio.sleep(5)
return response.text("I'm feeling sleepy")
@@ -42,8 +43,10 @@ async def test_file(request):
@app.route("/file_stream")
async def test_file_stream(request):
return await response.file_stream(os.path.abspath("setup.py"),
chunk_size=1024)
return await response.file_stream(
os.path.abspath("setup.py"), chunk_size=1024
)
# ----------------------------------------------- #
# Exceptions
@@ -52,14 +55,17 @@ async def test_file_stream(request):
@app.exception(ServerError)
async def test(request, exception):
return response.json({"exception": "{}".format(exception), "status": exception.status_code},
status=exception.status_code)
return response.json(
{"exception": str(exception), "status": exception.status_code},
status=exception.status_code,
)
# ----------------------------------------------- #
# Read from request
# ----------------------------------------------- #
@app.route("/json")
def post_json(request):
return response.json({"received": True, "message": request.json})
@@ -67,38 +73,51 @@ def post_json(request):
@app.route("/form")
def post_form_json(request):
return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})
return response.json(
{
"received": True,
"form_data": request.form,
"test": request.form.get("test"),
}
)
@app.route("/query_string")
def query_string(request):
return response.json({"parsed": True, "args": request.args, "url": request.url,
"query_string": request.query_string})
return response.json(
{
"parsed": True,
"args": request.args,
"url": request.url,
"query_string": request.query_string,
}
)
# ----------------------------------------------- #
# Run Server
# ----------------------------------------------- #
@app.listener('before_server_start')
@app.before_server_start
def before_start(app, loop):
log.info("SERVER STARTING")
@app.listener('after_server_start')
@app.after_server_start
def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH")
@app.listener('before_server_stop')
@app.before_server_stop
def before_stop(app, loop):
log.info("SERVER STOPPING")
@app.listener('after_server_stop')
@app.after_server_stop
def after_stop(app, loop):
log.info("TRIED EVERYTHING")
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,23 +1,25 @@
from sanic import Sanic
from sanic import response
import socket
import os
import socket
app = Sanic(__name__)
from sanic import Sanic, response
app = Sanic("Example")
@app.route("/test")
async def test(request):
return response.text("OK")
if __name__ == '__main__':
server_address = './uds_socket'
if __name__ == "__main__":
server_address = "./uds_socket"
# Make sure the socket does not already exist
try:
os.unlink(server_address)
os.unlink(server_address)
except OSError:
if os.path.exists(server_address):
raise
if os.path.exists(server_address):
raise
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_address)
app.run(sock=sock)

View File

@@ -1,20 +1,21 @@
from sanic import Sanic
from sanic import response
app = Sanic(__name__)
from sanic import Sanic, response
@app.route('/')
app = Sanic("Example")
@app.route("/")
async def index(request):
# generate a URL for the endpoint `post_handler`
url = app.url_for('post_handler', post_id=5)
url = app.url_for("post_handler", post_id=5)
# the URL is `/posts/5`, redirect to it
return response.redirect(url)
@app.route('/posts/<post_id>')
@app.route("/posts/<post_id>")
async def post_handler(request, post_id):
return response.text('Post - {}'.format(post_id))
if __name__ == '__main__':
return response.text("Post - {}".format(post_id))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -8,7 +8,9 @@ app = Sanic(name="blue-print-group-version-example")
bp1 = Blueprint(name="ultron", url_prefix="/ultron")
bp2 = Blueprint(name="vision", url_prefix="/vision", strict_slashes=None)
bpg = Blueprint.group([bp1, bp2], url_prefix="/sentient/robot", version=1, strict_slashes=True)
bpg = Blueprint.group(
bp1, bp2, url_prefix="/sentient/robot", version=1, strict_slashes=True
)
@bp1.get("/name")
@@ -31,5 +33,5 @@ async def bp2_revised_name(request):
app.blueprint(bpg)
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@@ -8,7 +8,7 @@ from sanic.blueprints import Blueprint
# curl -H "Host: bp.example.com" localhost:8000/question
# curl -H "Host: bp.example.com" localhost:8000/answer
app = Sanic(__name__)
app = Sanic("Example")
bp = Blueprint("bp", host="bp.example.com")

View File

@@ -1,24 +1,27 @@
from sanic import Sanic
from sanic.response import file
app = Sanic(__name__)
from sanic.response import redirect
@app.route('/')
async def index(request):
return await file('websocket.html')
app = Sanic("Example")
@app.websocket('/feed')
app.static("index.html", "websocket.html")
@app.route("/")
def index(request):
return redirect("index.html")
@app.websocket("/feed")
async def feed(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
data = "hello!"
print("Sending: " + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
print("Received: " + data)
if __name__ == '__main__':
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,6 +0,0 @@
FROM catthehacker/ubuntu:act-latest
SHELL [ "/bin/bash", "-c" ]
ENTRYPOINT []
RUN apt-get update
RUN apt-get install gcc -y
RUN apt-get install -y --no-install-recommends g++

3
pyproject.toml Normal file
View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

View File

@@ -1,196 +1,15 @@
import os
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from typing import Any, Dict, Optional
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
from sanic.app import Sanic
from sanic.config import BASE_LOGO
from sanic.log import error_logger
from sanic.simple import create_simple_server
from sanic.cli.app import SanicCLI
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
class SanicArgumentParser(ArgumentParser):
def add_bool_arguments(self, *args, **kwargs):
group = self.add_mutually_exclusive_group()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = f"no {kwargs['help']}\n "
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
if OS_IS_WINDOWS:
enable_windows_color_support()
def main():
parser = SanicArgumentParser(
prog="sanic",
description=BASE_LOGO,
formatter_class=lambda prog: RawTextHelpFormatter(
prog, max_help_position=33
),
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"Sanic {__version__}; Routing {__routing_version__}",
)
parser.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable"
),
)
parser.add_argument(
"-s",
"--simple",
dest="simple",
action="store_true",
help="Run Sanic as a Simple Server (module arg should be a path)\n ",
)
parser.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
parser.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
parser.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket\n ",
)
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\n "
)
parser.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
parser.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="number of worker processes [default 1]\n ",
)
parser.add_argument("-d", "--debug", dest="debug", action="store_true")
parser.add_argument(
"-r",
"--reload",
"--auto-reload",
dest="auto_reload",
action="store_true",
help="Watch source directory for file changes and reload on changes",
)
parser.add_argument(
"-R",
"--reload-dir",
dest="path",
action="append",
help="Extra directories to watch and reload on changes\n ",
)
parser.add_argument(
"module",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
),
)
args = parser.parse_args()
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if args.simple:
path = Path(args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in args.module else "."
module_name, app_name = args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}. "
f"Perhaps you meant {args.module}.app?"
)
if args.cert is not None or args.key is not None:
ssl: Optional[Dict[str, Any]] = {
"cert": args.cert,
"key": args.key,
}
else:
ssl = None
kwargs = {
"host": args.host,
"port": args.port,
"unix": args.unix,
"workers": args.workers,
"debug": args.debug,
"access_log": args.access_log,
"ssl": ssl,
}
if args.auto_reload:
kwargs["auto_reload"] = True
if args.path:
if args.auto_reload or args.debug:
kwargs["reload_dir"] = args.path
else:
error_logger.warning(
"Ignoring '--reload-dir' since auto reloading was not "
"enabled. If you would like to watch directories for "
"changes, consider using --debug or --auto-reload."
)
app.run(**kwargs)
except ImportError as e:
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
else:
raise e
except ValueError:
error_logger.exception("Failed to run app")
cli = SanicCLI()
cli.attach()
cli.run()
if __name__ == "__main__":

View File

@@ -1 +1 @@
__version__ = "21.6.2"
__version__ = "21.12.2"

File diff suppressed because it is too large Load Diff

View File

39
sanic/application/ext.py Normal file
View File

@@ -0,0 +1,39 @@
from __future__ import annotations
from contextlib import suppress
from importlib import import_module
from typing import TYPE_CHECKING
if TYPE_CHECKING: # no cov
from sanic import Sanic
try:
from sanic_ext import Extend # type: ignore
except ImportError:
...
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
if not app.config.AUTO_EXTEND:
return
sanic_ext = None
with suppress(ModuleNotFoundError):
sanic_ext = import_module("sanic_ext")
if not sanic_ext:
if fail:
raise RuntimeError(
"Sanic Extensions is not installed. You can add it to your "
"environment using:\n$ pip install sanic[ext]\nor\n$ pip "
"install sanic-ext"
)
return
if not getattr(app, "_ext", None):
Ext: Extend = getattr(sanic_ext, "Extend")
app._ext = Ext(app, **kwargs)
return app.ext

57
sanic/application/logo.py Normal file
View File

@@ -0,0 +1,57 @@
import re
import sys
from os import environ
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
COFFEE_LOGO = """\033[48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▄████████▄ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ██ ██▀▀▄ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ███████████ █ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ███████████▄▄▀ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▀███████▀ \033[0m
\033[48;2;255;13;104m \033[0m
Dark roast. No sugar."""
COLOR_LOGO = """\033[48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▄███ █████ ██ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▀███████ ███▄ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ████ ████████▀ \033[0m
\033[48;2;255;13;104m \033[0m
Build Fast. Run Fast."""
FULL_COLOR_LOGO = """
\033[38;2;255;13;104m ▄███ █████ ██ \033[0m ▄█▄ ██ █ █ ▄██████████
\033[38;2;255;13;104m ██ \033[0m █ █ █ ██ █ █ ██
\033[38;2;255;13;104m ▀███████ ███▄ \033[0m ▀ █ █ ██ ▄ █ ██
\033[38;2;255;13;104m ██\033[0m █████████ █ ██ █ █ ▄▄
\033[38;2;255;13;104m ████ ████████▀ \033[0m █ █ █ ██ █ ▀██ ███████
""" # noqa
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
def get_logo(full=False, coffee=False):
logo = (
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
if sys.stdout.isatty()
else BASE_LOGO
)
if (
sys.platform == "darwin"
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
):
logo = ansi_pattern.sub("", logo)
return logo

143
sanic/application/motd.py Normal file
View File

@@ -0,0 +1,143 @@
import sys
from abc import ABC, abstractmethod
from shutil import get_terminal_size
from textwrap import indent, wrap
from typing import Dict, Optional
from sanic import __version__
from sanic.log import logger
class MOTD(ABC):
def __init__(
self,
logo: Optional[str],
serve_location: str,
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
self.logo = logo
self.serve_location = serve_location
self.data = data
self.extra = extra
self.key_width = 0
self.value_width = 0
@abstractmethod
def display(self):
... # noqa
@classmethod
def output(
cls,
logo: Optional[str],
serve_location: str,
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
motd_class = MOTDTTY if sys.stdout.isatty() else MOTDBasic
motd_class(logo, serve_location, data, extra).display()
class MOTDBasic(MOTD):
def display(self):
if self.logo:
logger.debug(self.logo)
lines = [f"Sanic v{__version__}"]
if self.serve_location:
lines.append(f"Goin' Fast @ {self.serve_location}")
lines += [
*(f"{key}: {value}" for key, value in self.data.items()),
*(f"{key}: {value}" for key, value in self.extra.items()),
]
for line in lines:
logger.info(line)
class MOTDTTY(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.set_variables()
def set_variables(self): # no cov
fallback = (108, 24)
terminal_width = max(
get_terminal_size(fallback=fallback).columns, fallback[0]
)
self.max_value_width = terminal_width - fallback[0] + 36
self.key_width = 4
self.value_width = self.max_value_width
if self.data:
self.key_width = max(map(len, self.data.keys()))
self.value_width = min(
max(map(len, self.data.values())), self.max_value_width
)
self.logo_lines = self.logo.split("\n") if self.logo else []
self.logo_line_length = 24
self.centering_length = (
self.key_width + self.value_width + 2 + self.logo_line_length
)
self.display_length = self.key_width + self.value_width + 2
def display(self):
version = f"Sanic v{__version__}".center(self.centering_length)
running = (
f"Goin' Fast @ {self.serve_location}"
if self.serve_location
else ""
).center(self.centering_length)
length = len(version) + 2 - self.logo_line_length
first_filler = "" * (self.logo_line_length - 1)
second_filler = "" * length
display_filler = "" * (self.display_length + 2)
lines = [
f"\n{first_filler}{second_filler}",
f"{version}",
f"{running}",
f"{first_filler}{second_filler}",
]
self._render_data(lines, self.data, 0)
if self.extra:
logo_part = self._get_logo_part(len(lines) - 4)
lines.append(f"| {logo_part}{display_filler}")
self._render_data(lines, self.extra, len(lines) - 4)
self._render_fill(lines)
lines.append(f"{first_filler}{second_filler}\n")
logger.info(indent("\n".join(lines), " "))
def _render_data(self, lines, data, start):
offset = 0
for idx, (key, value) in enumerate(data.items(), start=start):
key = key.rjust(self.key_width)
wrapped = wrap(value, self.max_value_width, break_on_hyphens=False)
for wrap_index, part in enumerate(wrapped):
part = part.ljust(self.value_width)
logo_part = self._get_logo_part(idx + offset + wrap_index)
display = (
f"{key}: {part}"
if wrap_index == 0
else (" " * len(key) + f" {part}")
)
lines.append(f"{logo_part}{display}")
if wrap_index:
offset += 1
def _render_fill(self, lines):
filler = " " * self.display_length
idx = len(lines) - 5
for i in range(1, len(self.logo_lines) - idx):
logo_part = self.logo_lines[idx + i]
lines.append(f"{logo_part}{filler}")
def _get_logo_part(self, idx):
try:
logo_part = self.logo_lines[idx]
except IndexError:
logo_part = " " * (self.logo_line_length - 3)
return logo_part

View File

@@ -0,0 +1,79 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from socket import socket
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Optional, Set, Union
from sanic.log import logger
if TYPE_CHECKING:
from sanic import Sanic
class StrEnum(str, Enum):
def _generate_next_value_(name: str, *args) -> str: # type: ignore
return name.lower()
class Server(StrEnum):
SANIC = auto()
ASGI = auto()
GUNICORN = auto()
class Mode(StrEnum):
PRODUCTION = auto()
DEBUG = auto()
@dataclass
class ApplicationState:
app: Sanic
asgi: bool = field(default=False)
coffee: bool = field(default=False)
fast: bool = field(default=False)
host: str = field(default="")
port: int = field(default=0)
ssl: Optional[SSLContext] = field(default=None)
sock: Optional[socket] = field(default=None)
unix: Optional[str] = field(default=None)
mode: Mode = field(default=Mode.PRODUCTION)
reload_dirs: Set[Path] = field(default_factory=set)
server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False)
is_started: bool = field(default=False)
is_stopping: bool = field(default=False)
verbosity: int = field(default=0)
workers: int = field(default=0)
# This property relates to the ApplicationState instance and should
# not be changed except in the __post_init__ method
_init: bool = field(default=False)
def __post_init__(self) -> None:
self._init = True
def __setattr__(self, name: str, value: Any) -> None:
if self._init and name == "_init":
raise RuntimeError(
"Cannot change the value of _init after instantiation"
)
super().__setattr__(name, value)
if self._init and hasattr(self, f"set_{name}"):
getattr(self, f"set_{name}")(value)
def set_mode(self, value: Union[str, Mode]):
if hasattr(self.app, "error_handler"):
self.app.error_handler.debug = self.app.debug
if getattr(self.app, "configure_logging", False) and self.app.debug:
logger.setLevel(logging.DEBUG)
@property
def is_debug(self):
return self.mode is Mode.DEBUG

View File

@@ -1,6 +1,5 @@
import warnings
from inspect import isawaitable
from typing import Optional
from urllib.parse import quote
@@ -8,24 +7,33 @@ import sanic.app # noqa
from sanic.compat import Header
from sanic.exceptions import ServerError
from sanic.helpers import _default
from sanic.http import Stage
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
from sanic.request import Request
from sanic.response import BaseHTTPResponse
from sanic.server import ConnInfo
from sanic.websocket import WebSocketConnection
from sanic.server.websockets.connection import WebSocketConnection
class Lifespan:
def __init__(self, asgi_app: "ASGIApp") -> None:
self.asgi_app = asgi_app
if "before_server_start" in self.asgi_app.sanic_app.listeners:
if (
"server.init.before"
in self.asgi_app.sanic_app.signal_router.name_index
):
warnings.warn(
'You have set a listener for "before_server_start" '
"in ASGI mode. "
"It will be executed as early as possible, but not before "
"the ASGI server is started."
)
if "after_server_stop" in self.asgi_app.sanic_app.listeners:
if (
"server.shutdown.after"
in self.asgi_app.sanic_app.signal_router.name_index
):
warnings.warn(
'You have set a listener for "after_server_stop" '
"in ASGI mode. "
@@ -42,19 +50,16 @@ class Lifespan:
in sequence since the ASGI lifespan protocol only supports a single
startup event.
"""
self.asgi_app.sanic_app.router.finalize()
if self.asgi_app.sanic_app.signal_router.routes:
self.asgi_app.sanic_app.signal_router.finalize()
listeners = self.asgi_app.sanic_app.listeners.get(
"before_server_start", []
) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])
await self.asgi_app.sanic_app._startup()
await self.asgi_app.sanic_app._server_event("init", "before")
await self.asgi_app.sanic_app._server_event("init", "after")
for handler in listeners:
response = handler(
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default:
warnings.warn(
"You have set the USE_UVLOOP configuration option, but Sanic "
"cannot control the event loop when running in ASGI mode."
"This option will be ignored."
)
if response and isawaitable(response):
await response
async def shutdown(self) -> None:
"""
@@ -65,16 +70,8 @@ class Lifespan:
in sequence since the ASGI lifespan protocol only supports a single
shutdown event.
"""
listeners = self.asgi_app.sanic_app.listeners.get(
"before_server_stop", []
) + self.asgi_app.sanic_app.listeners.get("after_server_stop", [])
for handler in listeners:
response = handler(
self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop
)
if response and isawaitable(response):
await response
await self.asgi_app.sanic_app._server_event("shutdown", "before")
await self.asgi_app.sanic_app._server_event("shutdown", "after")
async def __call__(
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
@@ -96,6 +93,8 @@ class ASGIApp:
transport: MockTransport
lifespan: Lifespan
ws: Optional[WebSocketConnection]
stage: Stage
response: Optional[BaseHTTPResponse]
def __init__(self) -> None:
self.ws = None
@@ -108,6 +107,8 @@ class ASGIApp:
instance.sanic_app = sanic_app
instance.transport = MockTransport(scope, receive, send)
instance.transport.loop = sanic_app.loop
instance.stage = Stage.IDLE
instance.response = None
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
headers = Header(
@@ -162,6 +163,8 @@ class ASGIApp:
"""
Read and stream the body in chunks from an incoming ASGI message.
"""
if self.stage is Stage.IDLE:
self.stage = Stage.REQUEST
message = await self.transport.receive()
body = message.get("body", b"")
if not message.get("more_body", False):
@@ -176,11 +179,17 @@ class ASGIApp:
if data:
yield data
def respond(self, response):
def respond(self, response: BaseHTTPResponse):
if self.stage is not Stage.HANDLER:
self.stage = Stage.FAILED
raise RuntimeError("Response already started")
if self.response is not None:
self.response.stream = None
response.stream, self.response = self, response
return response
async def send(self, data, end_stream):
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
if self.response:
response, self.response = self.response, None
await self.transport.send(
@@ -208,6 +217,7 @@ class ASGIApp:
Handle the incoming request.
"""
try:
self.stage = Stage.HANDLER
await self.sanic_app.handle_request(self.request)
except Exception as e:
await self.sanic_app.handle_exception(self.request, e)

0
sanic/base/__init__.py Normal file
View File

6
sanic/base/meta.py Normal file
View File

@@ -0,0 +1,6 @@
class SanicMeta(type):
@classmethod
def __prepare__(metaclass, name, bases, **kwds):
cls = super().__prepare__(metaclass, name, bases, **kwds)
cls["__slots__"] = ()
return cls

View File

@@ -1,8 +1,8 @@
import re
from typing import Any, Tuple
from warnings import warn
from typing import Any
from sanic.base.meta import SanicMeta
from sanic.exceptions import SanicException
from sanic.mixins.exceptions import ExceptionMixin
from sanic.mixins.listeners import ListenerMixin
@@ -11,7 +11,7 @@ from sanic.mixins.routes import RouteMixin
from sanic.mixins.signals import SignalMixin
VALID_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_\-]*$")
VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
class BaseSanic(
@@ -20,10 +20,11 @@ class BaseSanic(
ListenerMixin,
ExceptionMixin,
SignalMixin,
metaclass=SanicMeta,
):
__fake_slots__: Tuple[str, ...]
__slots__ = ("name",)
def __init__(self, name: str = None, *args, **kwargs) -> None:
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
class_name = self.__class__.__name__
if name is None:
@@ -33,11 +34,10 @@ class BaseSanic(
)
if not VALID_NAME.match(name):
warn(
f"{class_name} instance named '{name}' uses a format that is"
f"deprecated. Starting in version 21.12, {class_name} objects "
"must be named only using alphanumeric characters, _, or -.",
DeprecationWarning,
raise SanicException(
f"{class_name} instance named '{name}' uses an invalid "
"format. Names must begin with a character and may only "
"contain alphanumeric characters, _, or -."
)
self.name = name
@@ -52,15 +52,12 @@ class BaseSanic(
return f'{self.__class__.__name__}(name="{self.name}")'
def __setattr__(self, name: str, value: Any) -> None:
# This is a temporary compat layer so we can raise a warning until
# setting attributes on the app instance can be removed and deprecated
# with a proper implementation of __slots__
if name not in self.__fake_slots__:
warn(
try:
super().__setattr__(name, value)
except AttributeError as e:
raise AttributeError(
f"Setting variables on {self.__class__.__name__} instances is "
"deprecated and will be removed in version 21.9. You should "
f"change your {self.__class__.__name__} instance to use "
"not allowed. You should change your "
f"{self.__class__.__name__} instance to use "
f"instance.ctx.{name} instead.",
DeprecationWarning,
)
super().__setattr__(name, value)
) from e

View File

@@ -1,10 +1,11 @@
from __future__ import annotations
from collections.abc import MutableSequence
from functools import partial
from typing import TYPE_CHECKING, List, Optional, Union
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.blueprints import Blueprint
@@ -196,6 +197,27 @@ class BlueprintGroup(MutableSequence):
"""
self._blueprints.append(value)
def exception(self, *exceptions, **kwargs):
"""
A decorator that can be used to implement a global exception handler
for all the Blueprints that belong to this Blueprint Group.
In case of nested Blueprint Groups, the same handler is applied
across each of the Blueprints recursively.
:param args: List of Python exceptions to be caught by the handler
:param kwargs: Additional optional arguments to be passed to the
exception handler
:return: a decorated method to handle global exceptions for any
blueprint registered under this group.
"""
def register_exception_handler_for_blueprints(fn):
for blueprint in self.blueprints:
blueprint.exception(*exceptions, **kwargs)(fn)
return register_exception_handler_for_blueprints
def insert(self, index: int, item: Blueprint) -> None:
"""
The Abstract class `MutableSequence` leverages this insert method to
@@ -229,3 +251,15 @@ class BlueprintGroup(MutableSequence):
args = list(args)[1:]
return register_middleware_for_blueprints(fn)
return register_middleware_for_blueprints
def on_request(self, middleware=None):
if callable(middleware):
return self.middleware(middleware, "request")
else:
return partial(self.middleware, attach_to="request")
def on_response(self, middleware=None):
if callable(middleware):
return self.middleware(middleware, "response")
else:
return partial(self.middleware, attach_to="response")

View File

@@ -3,15 +3,31 @@ from __future__ import annotations
import asyncio
from collections import defaultdict
from copy import deepcopy
from functools import wraps
from inspect import isfunction
from itertools import chain
from types import SimpleNamespace
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
)
from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.route import Route # type: ignore
from sanic.base import BaseSanic
from sanic.base.root import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.exceptions import SanicException
from sanic.helpers import Default, _default
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import (
ListenerType,
@@ -20,8 +36,34 @@ from sanic.models.handler_types import (
)
if TYPE_CHECKING:
from sanic import Sanic # noqa
if TYPE_CHECKING: # no cov
from sanic import Sanic
def lazy(func, as_decorator=True):
@wraps(func)
def decorator(bp, *args, **kwargs):
nonlocal as_decorator
kwargs["apply"] = False
pass_handler = None
if args and isfunction(args[0]):
as_decorator = False
def wrapper(handler):
future = func(bp, *args, **kwargs)
if as_decorator:
future = future(handler)
if bp.registered:
for app in bp.apps:
bp.register(app, {})
return future
return wrapper if as_decorator else wrapper(pass_handler)
return decorator
class Blueprint(BaseSanic):
@@ -37,13 +79,13 @@ class Blueprint(BaseSanic):
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param host: IP Address of FQDN for the sanic server to use.
:param host: IP Address or FQDN for the sanic server to use.
:param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a
training */*
trailing */*
"""
__fake_slots__ = (
__slots__ = (
"_apps",
"_future_routes",
"_future_statics",
@@ -56,7 +98,6 @@ class Blueprint(BaseSanic):
"host",
"listeners",
"middlewares",
"name",
"routes",
"statics",
"strict_slashes",
@@ -70,21 +111,15 @@ class Blueprint(BaseSanic):
self,
name: str = None,
url_prefix: Optional[str] = None,
host: Optional[str] = None,
host: Optional[Union[List[str], str]] = None,
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
):
super().__init__(name=name)
self._apps: Set[Sanic] = set()
self.reset()
self.ctx = SimpleNamespace()
self.exceptions: List[RouteHandler] = []
self.host = host
self.listeners: Dict[str, List[ListenerType]] = {}
self.middlewares: List[MiddlewareType] = []
self.routes: List[Route] = []
self.statics: List[RouteHandler] = []
self.strict_slashes = strict_slashes
self.url_prefix = (
url_prefix[:-1]
@@ -93,7 +128,6 @@ class Blueprint(BaseSanic):
)
self.version = version
self.version_prefix = version_prefix
self.websocket_routes: List[Route] = []
def __repr__(self) -> str:
args = ", ".join(
@@ -120,38 +154,100 @@ class Blueprint(BaseSanic):
)
return self._apps
def route(self, *args, **kwargs):
kwargs["apply"] = False
return super().route(*args, **kwargs)
@property
def registered(self) -> bool:
return bool(self._apps)
def static(self, *args, **kwargs):
kwargs["apply"] = False
return super().static(*args, **kwargs)
exception = lazy(BaseSanic.exception)
listener = lazy(BaseSanic.listener)
middleware = lazy(BaseSanic.middleware)
route = lazy(BaseSanic.route)
signal = lazy(BaseSanic.signal)
static = lazy(BaseSanic.static, as_decorator=False)
def middleware(self, *args, **kwargs):
kwargs["apply"] = False
return super().middleware(*args, **kwargs)
def reset(self):
self._apps: Set[Sanic] = set()
self.exceptions: List[RouteHandler] = []
self.listeners: Dict[str, List[ListenerType[Any]]] = {}
self.middlewares: List[MiddlewareType] = []
self.routes: List[Route] = []
self.statics: List[RouteHandler] = []
self.websocket_routes: List[Route] = []
def listener(self, *args, **kwargs):
kwargs["apply"] = False
return super().listener(*args, **kwargs)
def copy(
self,
name: str,
url_prefix: Optional[Union[str, Default]] = _default,
version: Optional[Union[int, str, float, Default]] = _default,
version_prefix: Union[str, Default] = _default,
strict_slashes: Optional[Union[bool, Default]] = _default,
with_registration: bool = True,
with_ctx: bool = False,
):
"""
Copy a blueprint instance with some optional parameters to
override the values of attributes in the old instance.
def exception(self, *args, **kwargs):
kwargs["apply"] = False
return super().exception(*args, **kwargs)
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param version: Blueprint Version
:param version_prefix: the prefix of the version number shown in the
URL.
:param strict_slashes: Enforce the API urls are requested with a
trailing */*
:param with_registration: whether register new blueprint instance with
sanic apps that were registered with the old instance or not.
:param with_ctx: whether ``ctx`` will be copied or not.
"""
def signal(self, event: str, *args, **kwargs):
kwargs["apply"] = False
return super().signal(event, *args, **kwargs)
attrs_backup = {
"_apps": self._apps,
"routes": self.routes,
"websocket_routes": self.websocket_routes,
"middlewares": self.middlewares,
"exceptions": self.exceptions,
"listeners": self.listeners,
"statics": self.statics,
}
self.reset()
new_bp = deepcopy(self)
new_bp.name = name
if not isinstance(url_prefix, Default):
new_bp.url_prefix = url_prefix
if not isinstance(version, Default):
new_bp.version = version
if not isinstance(strict_slashes, Default):
new_bp.strict_slashes = strict_slashes
if not isinstance(version_prefix, Default):
new_bp.version_prefix = version_prefix
for key, value in attrs_backup.items():
setattr(self, key, value)
if with_registration and self._apps:
if new_bp._future_statics:
raise SanicException(
"Static routes registered with the old blueprint instance,"
" cannot be registered again."
)
for app in self._apps:
app.blueprint(new_bp)
if not with_ctx:
new_bp.ctx = SimpleNamespace()
return new_bp
@staticmethod
def group(
*blueprints,
url_prefix="",
version=None,
strict_slashes=None,
*blueprints: Union[Blueprint, BlueprintGroup],
url_prefix: Optional[str] = None,
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
):
) -> BlueprintGroup:
"""
Create a list of blueprints, optionally grouping them under a
general URL prefix.
@@ -196,11 +292,15 @@ class Blueprint(BaseSanic):
opt_version = options.get("version", None)
opt_strict_slashes = options.get("strict_slashes", None)
opt_version_prefix = options.get("version_prefix", self.version_prefix)
error_format = options.get(
"error_format", app.config.FALLBACK_ERROR_FORMAT
)
routes = []
middleware = []
exception_handlers = []
listeners = defaultdict(list)
registered = set()
# Routes
for future in self._future_routes:
@@ -227,12 +327,15 @@ class Blueprint(BaseSanic):
)
name = app._generate_name(future.name)
host = future.host or self.host
if isinstance(host, list):
host = tuple(host)
apply_route = FutureRoute(
future.handler,
uri[1:] if uri.startswith("//") else uri,
future.methods,
future.host or self.host,
host,
strict_slashes,
future.stream,
version,
@@ -243,8 +346,14 @@ class Blueprint(BaseSanic):
future.unquote,
future.static,
version_prefix,
error_format,
future.route_context,
)
if (self, apply_route) in app._future_registry:
continue
registered.add(apply_route)
route = app._apply_route(apply_route)
operation = (
routes.extend if isinstance(route, list) else routes.append
@@ -256,39 +365,69 @@ class Blueprint(BaseSanic):
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
apply_route = FutureStatic(uri, *future[1:])
if (self, apply_route) in app._future_registry:
continue
registered.add(apply_route)
route = app._apply_static(apply_route)
routes.append(route)
route_names = [route.name for route in routes if route]
# Middleware
if route_names:
# Middleware
for future in self._future_middleware:
if (self, future) in app._future_registry:
continue
middleware.append(app._apply_middleware(future, route_names))
# Exceptions
for future in self._future_exceptions:
exception_handlers.append(app._apply_exception_handler(future))
# Exceptions
for future in self._future_exceptions:
if (self, future) in app._future_registry:
continue
exception_handlers.append(
app._apply_exception_handler(future, route_names)
)
# Event listeners
for listener in self._future_listeners:
listeners[listener.event].append(app._apply_listener(listener))
for future in self._future_listeners:
if (self, future) in app._future_registry:
continue
listeners[future.event].append(app._apply_listener(future))
for signal in self._future_signals:
signal.condition.update({"blueprint": self.name})
app._apply_signal(signal)
# Signals
for future in self._future_signals:
if (self, future) in app._future_registry:
continue
future.condition.update({"__blueprint__": self.name})
# Force exclusive to be False
app._apply_signal(tuple((*future[:-1], False)))
self.routes = [route for route in routes if isinstance(route, Route)]
self.websocket_routes = [
self.routes += [route for route in routes if isinstance(route, Route)]
self.websocket_routes += [
route for route in self.routes if route.ctx.websocket
]
self.middlewares = middleware
self.exceptions = exception_handlers
self.listeners = dict(listeners)
self.middlewares += middleware
self.exceptions += exception_handlers
self.listeners.update(dict(listeners))
if self.registered:
self.register_futures(
self.apps,
self,
chain(
registered,
self._future_middleware,
self._future_exceptions,
self._future_listeners,
self._future_signals,
),
)
async def dispatch(self, *args, **kwargs):
condition = kwargs.pop("condition", {})
condition.update({"blueprint": self.name})
condition.update({"__blueprint__": self.name})
kwargs["condition"] = condition
await asyncio.gather(
*[app.dispatch(*args, **kwargs) for app in self.apps]
@@ -316,3 +455,10 @@ class Blueprint(BaseSanic):
value = v
break
return value
@staticmethod
def register_futures(
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
):
for app in apps:
app._future_registry.update(set((bp, item) for item in futures))

0
sanic/cli/__init__.py Normal file
View File

189
sanic/cli/app.py Normal file
View File

@@ -0,0 +1,189 @@
import os
import shutil
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from textwrap import indent
from typing import Any, List, Union
from sanic.app import Sanic
from sanic.application.logo import get_logo
from sanic.cli.arguments import Group
from sanic.log import error_logger
from sanic.simple import create_simple_server
class SanicArgumentParser(ArgumentParser):
...
class SanicCLI:
DESCRIPTION = indent(
f"""
{get_logo(True)}
To start running a Sanic application, provide a path to the module, where
app is a Sanic() instance:
$ sanic path.to.server:app
Or, a path to a callable that returns a Sanic() instance:
$ sanic path.to.factory:create_app --factory
Or, a path to a directory to run as a simple HTTP server:
$ sanic ./path/to/static --simple
""",
prefix=" ",
)
def __init__(self) -> None:
width = shutil.get_terminal_size().columns
self.parser = SanicArgumentParser(
prog="sanic",
description=self.DESCRIPTION,
formatter_class=lambda prog: RawTextHelpFormatter(
prog,
max_help_position=36 if width > 96 else 24,
indent_increment=4,
width=None,
),
)
self.parser._positionals.title = "Required\n========\n Positional"
self.parser._optionals.title = "Optional\n========\n General"
self.main_process = (
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
)
self.args: List[Any] = []
def attach(self):
for group in Group._registry:
group.create(self.parser).attach()
def run(self):
# This is to provide backwards compat -v to display version
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None
self.args = self.parser.parse_args(args=parse_args)
self._precheck()
try:
app = self._get_app()
kwargs = self._build_run_kwargs()
app.run(**kwargs)
except ValueError:
error_logger.exception("Failed to run app")
def _precheck(self):
if self.args.debug and self.main_process:
error_logger.warning(
"Starting in v22.3, --debug will no "
"longer automatically run the auto-reloader.\n Switch to "
"--dev to continue using that functionality."
)
# # Custom TLS mismatch handling for better diagnostics
if self.main_process and (
# one of cert/key missing
bool(self.args.cert) != bool(self.args.key)
# new and old style self.args used together
or self.args.tls
and self.args.cert
# strict host checking without certs would always fail
or self.args.tlshost
and not self.args.tls
and not self.args.cert
):
self.parser.print_usage(sys.stderr)
message = (
"TLS certificates must be specified by either of:\n"
" --cert certdir/fullchain.pem --key certdir/privkey.pem\n"
" --tls certdir (equivalent to the above)"
)
error_logger.error(message)
sys.exit(1)
def _get_app(self):
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if self.args.simple:
path = Path(self.args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in self.args.module else "."
module_name, app_name = self.args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
self.args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if self.args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}.app?"
)
except ImportError as e:
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
else:
raise e
return app
def _build_run_kwargs(self):
ssl: Union[None, dict, str, list] = []
if self.args.tlshost:
ssl.append(None)
if self.args.cert is not None or self.args.key is not None:
ssl.append(dict(cert=self.args.cert, key=self.args.key))
if self.args.tls:
ssl += self.args.tls
if not ssl:
ssl = None
elif len(ssl) == 1 and ssl[0] is not None:
# Use only one cert, no TLSSelector.
ssl = ssl[0]
kwargs = {
"access_log": self.args.access_log,
"debug": self.args.debug,
"fast": self.args.fast,
"host": self.args.host,
"motd": self.args.motd,
"noisy_exceptions": self.args.noisy_exceptions,
"port": self.args.port,
"ssl": ssl,
"unix": self.args.unix,
"verbosity": self.args.verbosity or 0,
"workers": self.args.workers,
}
if self.args.auto_reload:
kwargs["auto_reload"] = True
if self.args.path:
if self.args.auto_reload or self.args.debug:
kwargs["reload_dir"] = self.args.path
else:
error_logger.warning(
"Ignoring '--reload-dir' since auto reloading was not "
"enabled. If you would like to watch directories for "
"changes, consider using --debug or --auto-reload."
)
return kwargs

237
sanic/cli/arguments.py Normal file
View File

@@ -0,0 +1,237 @@
from __future__ import annotations
from argparse import ArgumentParser, _ArgumentGroup
from typing import List, Optional, Type, Union
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
class Group:
name: Optional[str]
container: Union[ArgumentParser, _ArgumentGroup]
_registry: List[Type[Group]] = []
def __init_subclass__(cls) -> None:
Group._registry.append(cls)
def __init__(self, parser: ArgumentParser, title: Optional[str]):
self.parser = parser
if title:
self.container = self.parser.add_argument_group(title=f" {title}")
else:
self.container = self.parser
@classmethod
def create(cls, parser: ArgumentParser):
instance = cls(parser, cls.name)
return instance
def add_bool_arguments(self, *args, **kwargs):
group = self.container.add_mutually_exclusive_group()
kwargs["help"] = kwargs["help"].capitalize()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = f"no {kwargs['help'].lower()}".capitalize()
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
class GeneralGroup(Group):
name = None
def attach(self):
self.container.add_argument(
"--version",
action="version",
version=f"Sanic {__version__}; Routing {__routing_version__}",
)
self.container.add_argument(
"module",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
),
)
class ApplicationGroup(Group):
name = "Application"
def attach(self):
self.container.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable"
),
)
self.container.add_argument(
"-s",
"--simple",
dest="simple",
action="store_true",
help=(
"Run Sanic as a Simple Server, and serve the contents of "
"a directory\n(module arg should be a path)"
),
)
class SocketGroup(Group):
name = "Socket binding"
def attach(self):
self.container.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
self.container.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
self.container.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket",
)
class TLSGroup(Group):
name = "TLS certificate"
def attach(self):
self.container.add_argument(
"--cert",
dest="cert",
type=str,
help="Location of fullchain.pem, bundle.crt or equivalent",
)
self.container.add_argument(
"--key",
dest="key",
type=str,
help="Location of privkey.pem or equivalent .key file",
)
self.container.add_argument(
"--tls",
metavar="DIR",
type=str,
action="append",
help=(
"TLS certificate folder with fullchain.pem and privkey.pem\n"
"May be specified multiple times to choose multiple "
"certificates"
),
)
self.container.add_argument(
"--tls-strict-host",
dest="tlshost",
action="store_true",
help="Only allow clients that send an SNI matching server certs",
)
class WorkerGroup(Group):
name = "Worker"
def attach(self):
group = self.container.add_mutually_exclusive_group()
group.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="Number of worker processes [default 1]",
)
group.add_argument(
"--fast",
dest="fast",
action="store_true",
help="Set the number of workers to max allowed",
)
self.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
class DevelopmentGroup(Group):
name = "Development"
def attach(self):
self.container.add_argument(
"--debug",
dest="debug",
action="store_true",
help="Run the server in debug mode",
)
self.container.add_argument(
"-d",
"--dev",
dest="debug",
action="store_true",
help=(
"Currently is an alias for --debug. But starting in v22.3, \n"
"--debug will no longer automatically trigger auto_restart. \n"
"However, --dev will continue, effectively making it the \n"
"same as debug + auto_reload."
),
)
self.container.add_argument(
"-r",
"--reload",
"--auto-reload",
dest="auto_reload",
action="store_true",
help=(
"Watch source directory for file changes and reload on "
"changes"
),
)
self.container.add_argument(
"-R",
"--reload-dir",
dest="path",
action="append",
help="Extra directories to watch and reload on changes",
)
class OutputGroup(Group):
name = "Output"
def attach(self):
self.add_bool_arguments(
"--motd",
dest="motd",
default=True,
help="Show the startup display",
)
self.container.add_argument(
"-v",
"--verbosity",
action="count",
help="Control logging noise, eg. -vv or --verbosity=2 [default 0]",
)
self.add_bool_arguments(
"--noisy-exceptions",
dest="noisy_exceptions",
help="Output stack traces for all exceptions",
)

View File

@@ -8,6 +8,21 @@ from multidict import CIMultiDict # type: ignore
OS_IS_WINDOWS = os.name == "nt"
UVLOOP_INSTALLED = False
try:
import uvloop # type: ignore # noqa
UVLOOP_INSTALLED = True
except ImportError:
pass
def enable_windows_color_support():
import ctypes
kernel = ctypes.windll.kernel32
kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7)
class Header(CIMultiDict):

View File

@@ -1,31 +1,34 @@
from inspect import isclass
from __future__ import annotations
from inspect import getmembers, isclass, isdatadescriptor
from os import environ
from pathlib import Path
from typing import Any, Dict, Optional, Union
from warnings import warn
from typing import Any, Callable, Dict, Optional, Sequence, Union
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
from sanic.helpers import Default, _default
from sanic.http import Http
from .utils import load_module_from_file_location, str_to_bool
from sanic.log import deprecation, error_logger
from sanic.utils import load_module_from_file_location, str_to_bool
SANIC_PREFIX = "SANIC_"
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
DEFAULT_CONFIG = {
"_FALLBACK_ERROR_FORMAT": _default,
"ACCESS_LOG": True,
"AUTO_EXTEND": True,
"AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False,
"FALLBACK_ERROR_FORMAT": "html",
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FORWARDED_SECRET": None,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"KEEP_ALIVE": True,
"MOTD": True,
"MOTD_DISPLAY": {},
"NOISY_EXCEPTIONS": False,
"PROXIES_COUNT": None,
"REAL_IP_HEADER": None,
"REGISTER": True,
@@ -35,24 +38,39 @@ DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"WEBSOCKET_MAX_QUEUE": 32,
"USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20,
"WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
}
# These values will be removed from the Config object in v22.6 and moved
# to the application state
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
class Config(dict):
class DescriptorMeta(type):
def __init__(cls, *_):
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
@staticmethod
def _is_setter(member: object):
return isdatadescriptor(member) and hasattr(member, "setter")
class Config(dict, metaclass=DescriptorMeta):
ACCESS_LOG: bool
AUTO_EXTEND: bool
AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool
FALLBACK_ERROR_FORMAT: str
FORWARDED_FOR_HEADER: str
FORWARDED_SECRET: Optional[str]
GRACEFUL_SHUTDOWN_TIMEOUT: float
KEEP_ALIVE_TIMEOUT: int
KEEP_ALIVE: bool
NOISY_EXCEPTIONS: bool
MOTD: bool
MOTD_DISPLAY: Dict[str, str]
PROXIES_COUNT: Optional[int]
REAL_IP_HEADER: Optional[str]
REGISTER: bool
@@ -62,24 +80,29 @@ class Config(dict):
REQUEST_MAX_SIZE: int
REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int
WEBSOCKET_MAX_QUEUE: int
SERVER_NAME: str
USE_UVLOOP: Union[Default, bool]
WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int
WEBSOCKET_PING_TIMEOUT: int
WEBSOCKET_READ_LIMIT: int
WEBSOCKET_WRITE_LIMIT: int
def __init__(
self,
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
load_env: Optional[Union[bool, str]] = True,
env_prefix: Optional[str] = SANIC_PREFIX,
keep_alive: Optional[bool] = None,
*,
converters: Optional[Sequence[Callable[[str], Any]]] = None,
):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
self.LOGO = BASE_LOGO
self._converters = [str, str_to_bool, float, int]
self._LOGO = ""
if converters:
for converter in converters:
self.register_type(converter)
if keep_alive is not None:
self.KEEP_ALIVE = keep_alive
@@ -87,19 +110,12 @@ class Config(dict):
if env_prefix != SANIC_PREFIX:
if env_prefix:
self.load_environment_vars(env_prefix)
elif load_env is not True:
if load_env:
self.load_environment_vars(prefix=load_env)
warn(
"Use of load_env is deprecated and will be removed in "
"21.12. Modify the configuration prefix by passing "
"env_prefix instead.",
DeprecationWarning,
)
else:
self.load_environment_vars(SANIC_PREFIX)
self._configure_header_size()
self._check_error_format()
self._init = True
def __getattr__(self, attr):
try:
@@ -107,14 +123,68 @@ class Config(dict):
except KeyError as ke:
raise AttributeError(f"Config has no '{ke.args[0]}'")
def __setattr__(self, attr, value):
self[attr] = value
if attr in (
"REQUEST_MAX_HEADER_SIZE",
"REQUEST_BUFFER_SIZE",
"REQUEST_MAX_SIZE",
def __setattr__(self, attr, value) -> None:
self.update({attr: value})
def __setitem__(self, attr, value) -> None:
self.update({attr: value})
def update(self, *other, **kwargs) -> None:
kwargs.update({k: v for item in other for k, v in dict(item).items()})
setters: Dict[str, Any] = {
k: kwargs.pop(k)
for k in {**kwargs}.keys()
if k in self.__class__.__setters__
}
for key, value in setters.items():
try:
super().__setattr__(key, value)
except AttributeError:
...
super().update(**kwargs)
for attr, value in {**setters, **kwargs}.items():
self._post_set(attr, value)
def _post_set(self, attr, value) -> None:
if self.get("_init"):
if attr in (
"REQUEST_MAX_HEADER_SIZE",
"REQUEST_BUFFER_SIZE",
"REQUEST_MAX_SIZE",
):
self._configure_header_size()
elif attr == "LOGO":
self._LOGO = value
deprecation(
"Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.",
22.6,
)
@property
def LOGO(self):
return self._LOGO
@property
def FALLBACK_ERROR_FORMAT(self) -> str:
if self._FALLBACK_ERROR_FORMAT is _default:
return DEFAULT_FORMAT
return self._FALLBACK_ERROR_FORMAT
@FALLBACK_ERROR_FORMAT.setter
def FALLBACK_ERROR_FORMAT(self, value):
self._check_error_format(value)
if (
self._FALLBACK_ERROR_FORMAT is not _default
and value != self._FALLBACK_ERROR_FORMAT
):
self._configure_header_size()
error_logger.warning(
"Setting config.FALLBACK_ERROR_FORMAT on an already "
"configured value may have unintended consequences."
)
self._FALLBACK_ERROR_FORMAT = value
def _configure_header_size(self):
Http.set_header_max_size(
@@ -123,33 +193,51 @@ class Config(dict):
self.REQUEST_MAX_SIZE,
)
def _check_error_format(self, format: Optional[str] = None):
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
def load_environment_vars(self, prefix=SANIC_PREFIX):
"""
Looks for prefixed environment variables and applies
them to the configuration if present. This is called automatically when
Sanic starts up to load environment variables into config.
Looks for prefixed environment variables and applies them to the
configuration if present. This is called automatically when Sanic
starts up to load environment variables into config.
It will automatically hyrdate the following types:
It will automatically hydrate the following types:
- ``int``
- ``float``
- ``bool``
Anything else will be imported as a ``str``.
Anything else will be imported as a ``str``. If you would like to add
additional types to this list, you can use
:meth:`sanic.config.Config.register_type`. Just make sure that they
are registered before you instantiate your application.
.. code-block:: python
class Foo:
def __init__(self, name) -> None:
self.name = name
config = Config(converters=[Foo])
app = Sanic(__name__, config=config)
`See user guide re: config
<https://sanicframework.org/guide/deployment/configuration.html>`__
"""
for k, v in environ.items():
if k.startswith(prefix):
_, config_key = k.split(prefix, 1)
for key, value in environ.items():
if not key.startswith(prefix):
continue
_, config_key = key.split(prefix, 1)
for converter in reversed(self._converters):
try:
self[config_key] = int(v)
self[config_key] = converter(value)
break
except ValueError:
try:
self[config_key] = float(v)
except ValueError:
try:
self[config_key] = str_to_bool(v)
except ValueError:
self[config_key] = v
pass
def update_config(self, config: Union[bytes, str, dict, Any]):
"""
@@ -219,3 +307,17 @@ class Config(dict):
self.update(config)
load = update_config
def register_type(self, converter: Callable[[str], Any]) -> None:
"""
Allows for adding custom function to cast from a string value to any
other type. The function should raise ValueError if it is not the
correct type.
"""
if converter in self._converters:
error_logger.warning(
f"Configuration value converter '{converter.__name__}' has "
"already been registered"
)
return
self._converters.append(converter)

View File

@@ -25,14 +25,16 @@ from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
dumps: t.Callable[..., str]
try:
from ujson import dumps
dumps = partial(dumps, escape_forward_slashes=False)
except ImportError: # noqa
from json import dumps # type: ignore
from json import dumps
DEFAULT_FORMAT = "auto"
FALLBACK_TEXT = (
"The server encountered an internal error and "
"cannot complete your request."
@@ -45,6 +47,8 @@ class BaseRenderer:
Base class that all renderers must inherit from.
"""
dumps = staticmethod(dumps)
def __init__(self, request, exception, debug):
self.request = request
self.exception = exception
@@ -112,14 +116,16 @@ class HTMLRenderer(BaseRenderer):
TRACEBACK_STYLE = """
html { font-family: sans-serif }
h2 { color: #888; }
.tb-wrapper p { margin: 0 }
.tb-wrapper p, dl, dd { margin: 0 }
.frame-border { margin: 1rem }
.frame-line > * { padding: 0.3rem 0.6rem }
.frame-line { margin-bottom: 0.3rem }
.frame-code { font-size: 16px; padding-left: 4ch }
.tb-wrapper { border: 1px solid #eee }
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
.frame-descriptor { background: #e2eafb; font-size: 14px }
.frame-line > *, dt, dd { padding: 0.3rem 0.6rem }
.frame-line, dl { margin-bottom: 0.3rem }
.frame-code, dd { font-size: 16px; padding-left: 4ch }
.tb-wrapper, dl { border: 1px solid #eee }
.tb-header,.obj-header {
background: #eee; padding: 0.3rem; font-weight: bold
}
.frame-descriptor, dt { background: #e2eafb; font-size: 14px }
"""
TRACEBACK_WRAPPER_HTML = (
"<div class=tb-header>{exc_name}: {exc_value}</div>"
@@ -138,6 +144,11 @@ class HTMLRenderer(BaseRenderer):
"<p class=frame-code><code>{0.line}</code>"
"</div>"
)
OBJECT_WRAPPER_HTML = (
"<div class=obj-header>{title}</div>"
"<dl class={obj_type}>{display_html}</dl>"
)
OBJECT_DISPLAY_HTML = "<dt>{key}</dt><dd><code>{value}</code></dd>"
OUTPUT_HTML = (
"<!DOCTYPE html><html lang=en>"
"<meta charset=UTF-8><title>{title}</title>\n"
@@ -152,7 +163,7 @@ class HTMLRenderer(BaseRenderer):
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body=self._generate_body(),
body=self._generate_body(full=True),
),
status=self.status,
)
@@ -163,7 +174,7 @@ class HTMLRenderer(BaseRenderer):
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body="",
body=self._generate_body(full=False),
),
status=self.status,
headers=self.headers,
@@ -177,27 +188,49 @@ class HTMLRenderer(BaseRenderer):
def title(self):
return escape(f"⚠️ {super().title}")
def _generate_body(self):
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
def _generate_body(self, *, full):
lines = []
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines += [
f"<h2>Traceback of {appname} " "(most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> "
f"while handling path <code>{path}</code>",
"</div>",
]
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
lines.append(self._generate_object_display(info, attr))
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines = [
f"<h2>Traceback of {appname} (most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
"</div>",
]
return "\n".join(lines)
def _generate_object_display(
self, obj: t.Dict[str, t.Any], descriptor: str
) -> str:
display = "".join(
self.OBJECT_DISPLAY_HTML.format(key=key, value=value)
for key, value in obj.items()
)
return self.OBJECT_WRAPPER_HTML.format(
title=descriptor.title(),
display_html=display,
obj_type=descriptor.lower(),
)
def _format_exc(self, exc):
frames = extract_tb(exc.__traceback__)
frame_html = "".join(
@@ -224,7 +257,7 @@ class TextRenderer(BaseRenderer):
title=self.title,
text=self.text,
bar=("=" * len(self.title)),
body=self._generate_body(),
body=self._generate_body(full=True),
),
status=self.status,
)
@@ -235,7 +268,7 @@ class TextRenderer(BaseRenderer):
title=self.title,
text=self.text,
bar=("=" * len(self.title)),
body="",
body=self._generate_body(full=False),
),
status=self.status,
headers=self.headers,
@@ -245,21 +278,31 @@ class TextRenderer(BaseRenderer):
def title(self):
return f"⚠️ {super().title}"
def _generate_body(self):
_, exc_value, __ = sys.exc_info()
exceptions = []
def _generate_body(self, *, full):
lines = []
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
lines = [
f"{self.exception.__class__.__name__}: {self.exception} while "
f"handling path {self.request.path}",
f"Traceback of {self.request.app.name} (most recent call last):\n",
]
lines += [
f"{self.exception.__class__.__name__}: {self.exception} while "
f"handling path {self.request.path}",
f"Traceback of {self.request.app.name} "
"(most recent call last):\n",
]
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
return "\n".join(lines + exceptions[::-1])
lines += exceptions[::-1]
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
lines += self._generate_object_display_list(info, attr)
return "\n".join(lines)
def _format_exc(self, exc):
frames = "\n\n".join(
@@ -272,6 +315,13 @@ class TextRenderer(BaseRenderer):
)
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
def _generate_object_display_list(self, obj, descriptor):
lines = [f"\n{descriptor.title()}"]
for key, value in obj.items():
display = self.dumps(value)
lines.append(f"{self.SPACER * 2}{key}: {display}")
return lines
class JSONRenderer(BaseRenderer):
"""
@@ -280,11 +330,11 @@ class JSONRenderer(BaseRenderer):
def full(self) -> HTTPResponse:
output = self._generate_output(full=True)
return json(output, status=self.status, dumps=dumps)
return json(output, status=self.status, dumps=self.dumps)
def minimal(self) -> HTTPResponse:
output = self._generate_output(full=False)
return json(output, status=self.status, dumps=dumps)
return json(output, status=self.status, dumps=self.dumps)
def _generate_output(self, *, full):
output = {
@@ -293,6 +343,11 @@ class JSONRenderer(BaseRenderer):
"message": self.text,
}
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
output[attr] = info
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
@@ -340,41 +395,139 @@ RENDERERS_BY_CONFIG = {
}
RENDERERS_BY_CONTENT_TYPE = {
"multipart/form-data": HTMLRenderer,
"application/json": JSONRenderer,
"text/plain": TextRenderer,
"application/json": JSONRenderer,
"multipart/form-data": HTMLRenderer,
"text/html": HTMLRenderer,
}
CONTENT_TYPE_BY_RENDERERS = {
v: k for k, v in RENDERERS_BY_CONTENT_TYPE.items()
}
RESPONSE_MAPPING = {
"empty": "html",
"json": "json",
"text": "text",
"raw": "text",
"html": "html",
"file": "html",
"file_stream": "text",
"stream": "text",
"redirect": "html",
"text/plain": "text",
"text/html": "html",
"application/json": "json",
}
def check_error_format(format):
if format not in RENDERERS_BY_CONFIG and format != "auto":
raise SanicException(f"Unknown format: {format}")
def exception_response(
request: Request,
exception: Exception,
debug: bool,
fallback: str,
base: t.Type[BaseRenderer],
renderer: t.Type[t.Optional[BaseRenderer]] = None,
) -> HTTPResponse:
"""
Render a response for the default FALLBACK exception handler.
"""
content_type = None
if not renderer:
renderer = HTMLRenderer
# Make sure we have something set
renderer = base
render_format = fallback
if request:
if request.app.config.FALLBACK_ERROR_FORMAT == "auto":
# If there is a request, try and get the format
# from the route
if request.route:
try:
renderer = JSONRenderer if request.json else HTMLRenderer
except InvalidUsage:
if request.route.ctx.error_format:
render_format = request.route.ctx.error_format
except AttributeError:
...
content_type = request.headers.getone("content-type", "").split(
";"
)[0]
acceptable = request.accept
# If the format is auto still, make a guess
if render_format == "auto":
# First, if there is an Accept header, check if text/html
# is the first option
# According to MDN Web Docs, all major browsers use text/html
# as the primary value in Accept (with the exception of IE 8,
# and, well, if you are supporting IE 8, then you have bigger
# problems to concern yourself with than what default exception
# renderer is used)
# Source:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values
if acceptable and acceptable[0].match(
"text/html",
allow_type_wildcard=False,
allow_subtype_wildcard=False,
):
renderer = HTMLRenderer
content_type, *_ = request.headers.getone(
"content-type", ""
).split(";")
renderer = RENDERERS_BY_CONTENT_TYPE.get(
content_type, renderer
)
# Second, if there is an Accept header, check if
# application/json is an option, or if the content-type
# is application/json
elif (
acceptable
and acceptable.match(
"application/json",
allow_type_wildcard=False,
allow_subtype_wildcard=False,
)
or content_type == "application/json"
):
renderer = JSONRenderer
# Third, if there is no Accept header, assume we want text.
# The likely use case here is a raw socket.
elif not acceptable:
renderer = TextRenderer
else:
# Fourth, look to see if there was a JSON body
# When in this situation, the request is probably coming
# from curl, an API client like Postman or Insomnia, or a
# package like requests or httpx
try:
# Give them the benefit of the doubt if they did:
# $ curl localhost:8000 -d '{"foo": "bar"}'
# And provide them with JSONRenderer
renderer = JSONRenderer if request.json else base
except InvalidUsage:
renderer = base
else:
render_format = request.app.config.FALLBACK_ERROR_FORMAT
renderer = RENDERERS_BY_CONFIG.get(render_format, renderer)
# Lastly, if there is an Accept header, make sure
# our choice is okay
if acceptable:
type_ = CONTENT_TYPE_BY_RENDERERS.get(renderer) # type: ignore
if type_ and type_ not in acceptable:
# If the renderer selected is not in the Accept header
# look through what is in the Accept header, and select
# the first option that matches. Otherwise, just drop back
# to the original default
for accept in acceptable:
mtype = f"{accept.type_}/{accept.subtype}"
maybe = RENDERERS_BY_CONTENT_TYPE.get(mtype)
if maybe:
renderer = maybe
break
else:
renderer = base
renderer = t.cast(t.Type[BaseRenderer], renderer)
return renderer(request, exception, debug).render()

View File

@@ -1,19 +1,27 @@
from typing import Optional, Union
from typing import Any, Dict, Optional, Union
from sanic.helpers import STATUS_CODES
class SanicException(Exception):
message: str = ""
def __init__(
self,
message: Optional[Union[str, bytes]] = None,
status_code: Optional[int] = None,
quiet: Optional[bool] = None,
context: Optional[Dict[str, Any]] = None,
extra: Optional[Dict[str, Any]] = None,
) -> None:
if message is None and status_code is not None:
msg: bytes = STATUS_CODES.get(status_code, b"")
message = msg.decode("utf8")
self.context = context
self.extra = extra
if message is None:
if self.message:
message = self.message
elif status_code is not None:
msg: bytes = STATUS_CODES.get(status_code, b"")
message = msg.decode("utf8")
super().__init__(message)
@@ -122,8 +130,11 @@ class HeaderNotFound(InvalidUsage):
**Status**: 400 Bad Request
"""
status_code = 400
quiet = True
class InvalidHeader(InvalidUsage):
"""
**Status**: 400 Bad Request
"""
class ContentRangeError(SanicException):
@@ -230,23 +241,6 @@ class InvalidSignal(SanicException):
pass
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
"""
Raise an exception based on SanicException. Returns the HTTP response
message appropriate for the given status code, unless provided.
STATUS_CODES from sanic.helpers for the given status code.
:param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages in
"""
import warnings
warnings.warn(
"sanic.exceptions.abort has been marked as deprecated, and will be "
"removed in release 21.12.\n To migrate your code, simply replace "
"abort(status_code, msg) with raise SanicException(msg, status_code), "
"or even better, raise an appropriate SanicException subclass."
)
raise SanicException(message=message, status_code=status_code)
class WebsocketClosed(SanicException):
quiet = True
message = "Client has closed the websocket connection"

View File

@@ -1,12 +1,24 @@
from traceback import format_exc
from __future__ import annotations
from sanic.errorpages import exception_response
from inspect import signature
from typing import Dict, List, Optional, Tuple, Type, Union
from sanic.config import Config
from sanic.errorpages import (
DEFAULT_FORMAT,
BaseRenderer,
HTMLRenderer,
exception_response,
)
from sanic.exceptions import (
ContentRangeError,
HeaderNotFound,
InvalidRangeType,
SanicException,
)
from sanic.log import error_logger
from sanic.helpers import Default, _default
from sanic.log import deprecation, error_logger
from sanic.models.handler_types import RouteHandler
from sanic.response import text
@@ -23,15 +35,121 @@ class ErrorHandler:
"""
handlers = None
cached_handlers = None
def __init__(self):
self.handlers = []
self.cached_handlers = {}
# Beginning in v22.3, the base renderer will be TextRenderer
def __init__(
self,
fallback: Union[str, Default] = _default,
base: Type[BaseRenderer] = HTMLRenderer,
):
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
self.cached_handlers: Dict[
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
] = {}
self.debug = False
self._fallback = fallback
self.base = base
def add(self, exception, handler):
if fallback is not _default:
self._warn_fallback_deprecation()
@property
def fallback(self):
# This is for backwards compat and can be removed in v22.6
if self._fallback is _default:
return DEFAULT_FORMAT
return self._fallback
@fallback.setter
def fallback(self, value: str):
self._warn_fallback_deprecation()
if not isinstance(value, str):
raise SanicException(
f"Cannot set error handler fallback to: value={value}"
)
self._fallback = value
@staticmethod
def _warn_fallback_deprecation():
deprecation(
"Setting the ErrorHandler fallback value directly is "
"deprecated and no longer supported. This feature will "
"be removed in v22.6. Instead, use "
"app.config.FALLBACK_ERROR_FORMAT.",
22.6,
)
@classmethod
def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config):
if error_handler._fallback is not _default:
if config._FALLBACK_ERROR_FORMAT is _default:
return error_handler.fallback
error_logger.warning(
"Conflicting error fallback values were found in the "
"error handler and in the app.config while handling an "
"exception. Using the value from app.config."
)
return config.FALLBACK_ERROR_FORMAT
@classmethod
def finalize(
cls,
error_handler: ErrorHandler,
fallback: Optional[str] = None,
config: Optional[Config] = None,
):
if fallback:
deprecation(
"Setting the ErrorHandler fallback value via finalize() "
"is deprecated and no longer supported. This feature will "
"be removed in v22.6. Instead, use "
"app.config.FALLBACK_ERROR_FORMAT.",
22.6,
)
if config is None:
deprecation(
"Starting in v22.3, config will be a required argument "
"for ErrorHandler.finalize().",
22.3,
)
if fallback and fallback != DEFAULT_FORMAT:
if error_handler._fallback is not _default:
error_logger.warning(
f"Setting the fallback value to {fallback}. This changes "
"the current non-default value "
f"'{error_handler._fallback}'."
)
error_handler._fallback = fallback
if not isinstance(error_handler, cls):
error_logger.warning(
f"Error handler is non-conforming: {type(error_handler)}"
)
sig = signature(error_handler.lookup)
if len(sig.parameters) == 1:
deprecation(
"You are using a deprecated error handler. The lookup "
"method should accept two positional parameters: "
"(exception, route_name: Optional[str]). "
"Until you upgrade your ErrorHandler.lookup, Blueprint "
"specific exceptions will not work properly. Beginning "
"in v22.3, the legacy style lookup method will not "
"work at all.",
22.3,
)
legacy_lookup = error_handler._legacy_lookup
error_handler._lookup = legacy_lookup # type: ignore
def _full_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception, route_name)
def _legacy_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception)
def add(self, exception, handler, route_names: Optional[List[str]] = None):
"""
Add a new exception handler to an already existing handler object.
@@ -44,11 +162,16 @@ class ErrorHandler:
:return: None
"""
# self.handlers to be deprecated and removed in version 21.12
# self.handlers is deprecated and will be removed in version 22.3
self.handlers.append((exception, handler))
self.cached_handlers[exception] = handler
def lookup(self, exception):
if route_names:
for route in route_names:
self.cached_handlers[(exception, route)] = handler
else:
self.cached_handlers[(exception, None)] = handler
def lookup(self, exception, route_name: Optional[str] = None):
"""
Lookup the existing instance of :class:`ErrorHandler` and fetch the
registered handler for a specific type of exception.
@@ -63,20 +186,31 @@ class ErrorHandler:
:return: Registered function if found ``None`` otherwise
"""
exception_class = type(exception)
if exception_class in self.cached_handlers:
return self.cached_handlers[exception_class]
for ancestor in type.mro(exception_class):
if ancestor in self.cached_handlers:
handler = self.cached_handlers[ancestor]
self.cached_handlers[exception_class] = handler
for name in (route_name, None):
exception_key = (exception_class, name)
handler = self.cached_handlers.get(exception_key)
if handler:
return handler
if ancestor is BaseException:
break
self.cached_handlers[exception_class] = None
for name in (route_name, None):
for ancestor in type.mro(exception_class):
exception_key = (ancestor, name)
if exception_key in self.cached_handlers:
handler = self.cached_handlers[exception_key]
self.cached_handlers[
(exception_class, route_name)
] = handler
return handler
if ancestor is BaseException:
break
self.cached_handlers[(exception_class, route_name)] = None
handler = None
return handler
_lookup = _full_lookup
def response(self, request, exception):
"""Fetches and executes an exception handler and returns a response
object
@@ -91,7 +225,8 @@ class ErrorHandler:
:return: Wrap the return value obtained from :func:`default`
or registered handler for that type of exception.
"""
handler = self.lookup(exception)
route_name = request.name if request else None
handler = self._lookup(exception, route_name)
response = None
try:
if handler:
@@ -99,7 +234,6 @@ class ErrorHandler:
if response is None:
response = self.default(request, exception)
except Exception:
self.log(format_exc())
try:
url = repr(request.url)
except AttributeError:
@@ -115,11 +249,6 @@ class ErrorHandler:
return text("An error occurred while handling an error", 500)
return response
def log(self, message, level="error"):
"""
Deprecated, do not use.
"""
def default(self, request, exception):
"""
Provide a default behavior for the objects of :class:`ErrorHandler`.
@@ -135,20 +264,30 @@ class ErrorHandler:
:class:`Exception`
:return:
"""
self.log(request, exception)
fallback = ErrorHandler._get_fallback_value(self, request.app.config)
return exception_response(
request,
exception,
debug=self.debug,
base=self.base,
fallback=fallback,
)
@staticmethod
def log(request, exception):
quiet = getattr(exception, "quiet", False)
if quiet is False:
noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False)
if quiet is False or noisy is True:
try:
url = repr(request.url)
except AttributeError:
url = "unknown"
self.log(format_exc())
error_logger.exception(
"Exception occurred while handling uri: %s", url
)
return exception_response(request, exception, self.debug)
class ContentRangeHandler:
"""

View File

@@ -1,8 +1,11 @@
from __future__ import annotations
import re
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from urllib.parse import unquote
from sanic.exceptions import InvalidHeader
from sanic.helpers import STATUS_CODES
@@ -25,11 +28,180 @@ _host_re = re.compile(
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
# curl all have different escaping, that we try to handle as well as possible,
# even though no client espaces in a way that would allow perfect handling.
# even though no client escapes in a way that would allow perfect handling.
# For more information, consult ../tests/test_requests.py
def parse_arg_as_accept(f):
def func(self, other, *args, **kwargs):
if not isinstance(other, Accept) and other:
other = Accept.parse(other)
return f(self, other, *args, **kwargs)
return func
class MediaType(str):
def __new__(cls, value: str):
return str.__new__(cls, value)
def __init__(self, value: str) -> None:
self.value = value
self.is_wildcard = self.check_if_wildcard(value)
def __eq__(self, other):
if self.is_wildcard:
return True
if self.match(other):
return True
other_is_wildcard = (
other.is_wildcard
if isinstance(other, MediaType)
else self.check_if_wildcard(other)
)
return other_is_wildcard
def match(self, other):
other_value = other.value if isinstance(other, MediaType) else other
return self.value == other_value
@staticmethod
def check_if_wildcard(value):
return value == "*"
class Accept(str):
def __new__(cls, value: str, *args, **kwargs):
return str.__new__(cls, value)
def __init__(
self,
value: str,
type_: MediaType,
subtype: MediaType,
*,
q: str = "1.0",
**kwargs: str,
):
qvalue = float(q)
if qvalue > 1 or qvalue < 0:
raise InvalidHeader(
f"Accept header qvalue must be between 0 and 1, not: {qvalue}"
)
self.value = value
self.type_ = type_
self.subtype = subtype
self.qvalue = qvalue
self.params = kwargs
def _compare(self, other, method):
try:
return method(self.qvalue, other.qvalue)
except (AttributeError, TypeError):
return NotImplemented
@parse_arg_as_accept
def __lt__(self, other: Union[str, Accept]):
return self._compare(other, lambda s, o: s < o)
@parse_arg_as_accept
def __le__(self, other: Union[str, Accept]):
return self._compare(other, lambda s, o: s <= o)
@parse_arg_as_accept
def __eq__(self, other: Union[str, Accept]): # type: ignore
return self._compare(other, lambda s, o: s == o)
@parse_arg_as_accept
def __ge__(self, other: Union[str, Accept]):
return self._compare(other, lambda s, o: s >= o)
@parse_arg_as_accept
def __gt__(self, other: Union[str, Accept]):
return self._compare(other, lambda s, o: s > o)
@parse_arg_as_accept
def __ne__(self, other: Union[str, Accept]): # type: ignore
return self._compare(other, lambda s, o: s != o)
@parse_arg_as_accept
def match(
self,
other,
*,
allow_type_wildcard: bool = True,
allow_subtype_wildcard: bool = True,
) -> bool:
type_match = (
self.type_ == other.type_
if allow_type_wildcard
else (
self.type_.match(other.type_)
and not self.type_.is_wildcard
and not other.type_.is_wildcard
)
)
subtype_match = (
self.subtype == other.subtype
if allow_subtype_wildcard
else (
self.subtype.match(other.subtype)
and not self.subtype.is_wildcard
and not other.subtype.is_wildcard
)
)
return type_match and subtype_match
@classmethod
def parse(cls, raw: str) -> Accept:
invalid = False
mtype = raw.strip()
try:
media, *raw_params = mtype.split(";")
type_, subtype = media.split("/")
except ValueError:
invalid = True
if invalid or not type_ or not subtype:
raise InvalidHeader(f"Header contains invalid Accept value: {raw}")
params = dict(
[
(key.strip(), value.strip())
for key, value in (param.split("=", 1) for param in raw_params)
]
)
return cls(mtype, MediaType(type_), MediaType(subtype), **params)
class AcceptContainer(list):
def __contains__(self, o: object) -> bool:
return any(item.match(o) for item in self)
def match(
self,
o: object,
*,
allow_type_wildcard: bool = True,
allow_subtype_wildcard: bool = True,
) -> bool:
return any(
item.match(
o,
allow_type_wildcard=allow_type_wildcard,
allow_subtype_wildcard=allow_subtype_wildcard,
)
for item in self
)
def parse_content_header(value: str) -> Tuple[str, Options]:
"""Parse content-type and content-disposition header values.
@@ -194,3 +366,31 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes:
ret += b"%b: %b\r\n" % h
ret += b"\r\n"
return ret
def _sort_accept_value(accept: Accept):
return (
accept.qvalue,
len(accept.params),
accept.subtype != "*",
accept.type_ != "*",
)
def parse_accept(accept: str) -> AcceptContainer:
"""Parse an Accept header and order the acceptable media types in
accorsing to RFC 7231, s. 5.3.2
https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
"""
media_types = accept.split(",")
accept_list: List[Accept] = []
for mtype in media_types:
if not mtype:
continue
accept_list.append(Accept.parse(mtype))
return AcceptContainer(
sorted(accept_list, key=_sort_accept_value, reverse=True)
)

View File

@@ -144,7 +144,7 @@ def import_string(module_name, package=None):
import a module or class by string path.
:module_name: str with path of module or path to import and
instanciate a class
instantiate a class
:returns: a module object or one instance from class if
module_name is a valid path to class
@@ -155,3 +155,17 @@ def import_string(module_name, package=None):
if ismodule(obj):
return obj
return obj()
class Default:
"""
It is used to replace `None` or `object()` as a sentinel
that represents a default value. Sometimes we want to set
a value to `None` so we cannot use `None` to represent the
default value, and `object()` is hard to be typed.
"""
pass
_default = Default()

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.request import Request
from sanic.response import BaseHTTPResponse
@@ -21,6 +21,7 @@ from sanic.exceptions import (
from sanic.headers import format_http1_response
from sanic.helpers import has_message_body
from sanic.log import access_logger, error_logger, logger
from sanic.touchup import TouchUpMeta
class Stage(Enum):
@@ -45,7 +46,7 @@ class Stage(Enum):
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
class Http:
class Http(metaclass=TouchUpMeta):
"""
Internal helper for managing the HTTP request/response cycle
@@ -67,9 +68,15 @@ class Http:
HEADER_CEILING = 16_384
HEADER_MAX_SIZE = 0
__touchup__ = (
"http1_request_header",
"http1_response_header",
"read",
)
__slots__ = [
"_send",
"_receive_more",
"dispatch",
"recv_buffer",
"protocol",
"expecting_continue",
@@ -97,7 +104,7 @@ class Http:
self.protocol = protocol
self.keep_alive = True
self.stage: Stage = Stage.IDLE
self.init_for_request()
self.dispatch = self.protocol.app.dispatch
def init_for_request(self):
"""Init/reset all per-request variables."""
@@ -121,14 +128,20 @@ class Http:
"""
HTTP 1.1 connection handler
"""
while True: # As long as connection stays keep-alive
# Handle requests while the connection stays reusable
while self.keep_alive and self.stage is Stage.IDLE:
self.init_for_request()
# Wait for incoming bytes (in IDLE stage)
if not self.recv_buffer:
await self._receive_more()
self.stage = Stage.REQUEST
try:
# Receive and handle a request
self.stage = Stage.REQUEST
self.response_func = self.http1_response_header
await self.http1_request_header()
self.stage = Stage.HANDLER
self.request.conn_info = self.protocol.conn_info
await self.protocol.request_handler(self.request)
@@ -140,6 +153,12 @@ class Http:
await self.response.send(end_stream=True)
except CancelledError:
# Write an appropriate response before exiting
if not self.protocol.transport:
logger.info(
f"Request: {self.request.method} {self.request.url} "
"stopped. Transport is closed."
)
return
e = self.exception or ServiceUnavailable("Cancelled")
self.exception = None
self.keep_alive = False
@@ -173,17 +192,7 @@ class Http:
if self.response:
self.response.stream = None
self.init_for_request()
# Exit and disconnect if no more requests can be taken
if self.stage is not Stage.IDLE or not self.keep_alive:
break
# Wait for the next request
if not self.recv_buffer:
await self._receive_more()
async def http1_request_header(self):
async def http1_request_header(self): # no cov
"""
Receive and parse request header into self.request.
"""
@@ -212,6 +221,12 @@ class Http:
reqline, *split_headers = raw_headers.split("\r\n")
method, self.url, protocol = reqline.split(" ")
await self.dispatch(
"http.lifecycle.read_head",
inline=True,
context={"head": bytes(head)},
)
if protocol == "HTTP/1.1":
self.keep_alive = True
elif protocol == "HTTP/1.0":
@@ -250,6 +265,11 @@ class Http:
transport=self.protocol.transport,
app=self.protocol.app,
)
await self.dispatch(
"http.lifecycle.request",
inline=True,
context={"request": request},
)
# Prepare for request body
self.request_bytes_left = self.request_bytes = 0
@@ -274,13 +294,12 @@ class Http:
# Remove header and its trailing CRLF
del buf[: pos + 4]
self.stage = Stage.HANDLER
self.request, request.stream = request, self
self.protocol.state["requests_count"] += 1
async def http1_response_header(
self, data: bytes, end_stream: bool
) -> None:
) -> None: # no cov
res = self.response
# Compatibility with simple response body
@@ -452,8 +471,8 @@ class Http:
"request": "nil",
}
if req is not None:
if req.ip:
extra["host"] = f"{req.ip}:{req.port}"
if req.remote_addr or req.ip:
extra["host"] = f"{req.remote_addr or req.ip}:{req.port}"
extra["request"] = f"{req.method} {req.url}"
access_logger.info("", extra=extra)
@@ -469,7 +488,7 @@ class Http:
if data:
yield data
async def read(self) -> Optional[bytes]:
async def read(self) -> Optional[bytes]: # no cov
"""
Read some bytes of request body.
"""
@@ -543,6 +562,12 @@ class Http:
self.request_bytes_left -= size
await self.dispatch(
"http.lifecycle.read_body",
inline=True,
context={"body": data},
)
return data
# Response methods
@@ -559,6 +584,11 @@ class Http:
self.stage = Stage.FAILED
raise RuntimeError("Response already started")
# Disconnect any earlier but unused response object
if self.response is not None:
self.response.stream = None
# Connect and return the response
self.response, response.stream = response, self
return response

View File

@@ -1,8 +1,12 @@
import logging
import sys
from enum import Enum
from typing import Any, Dict
from warnings import warn
LOGGING_CONFIG_DEFAULTS = dict(
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
version=1,
disable_existing_loggers=False,
loggers={
@@ -53,6 +57,14 @@ LOGGING_CONFIG_DEFAULTS = dict(
)
class Colors(str, Enum):
END = "\033[0m"
BLUE = "\033[01;34m"
GREEN = "\033[01;32m"
YELLOW = "\033[01;33m"
RED = "\033[01;31m"
logger = logging.getLogger("sanic.root")
"""
General Sanic logger
@@ -67,3 +79,11 @@ access_logger = logging.getLogger("sanic.access")
"""
Logger used by Sanic for access logging
"""
def deprecation(message: str, version: float):
version_info = f"[DEPRECATION v{version}] "
if sys.stdout.isatty():
version_info = f"{Colors.RED}{version_info}"
message = f"{Colors.YELLOW}{message}{Colors.END}"
warn(version_info + message, DeprecationWarning)

View File

@@ -1,9 +1,10 @@
from typing import Set
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureException
class ExceptionMixin:
class ExceptionMixin(metaclass=SanicMeta):
def __init__(self, *args, **kwargs) -> None:
self._future_exceptions: Set[FutureException] = set()

View File

@@ -1,23 +1,25 @@
from enum import Enum, auto
from functools import partial
from typing import Any, Callable, Coroutine, List, Optional, Union
from typing import List, Optional, Union
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureListener
from sanic.models.handler_types import ListenerType, Sanic
class ListenerEvent(str, Enum):
def _generate_next_value_(name: str, *args) -> str: # type: ignore
return name.lower()
BEFORE_SERVER_START = auto()
AFTER_SERVER_START = auto()
BEFORE_SERVER_STOP = auto()
AFTER_SERVER_STOP = auto()
BEFORE_SERVER_START = "server.init.before"
AFTER_SERVER_START = "server.init.after"
BEFORE_SERVER_STOP = "server.shutdown.before"
AFTER_SERVER_STOP = "server.shutdown.after"
MAIN_PROCESS_START = auto()
MAIN_PROCESS_STOP = auto()
class ListenerMixin:
class ListenerMixin(metaclass=SanicMeta):
def __init__(self, *args, **kwargs) -> None:
self._future_listeners: List[FutureListener] = []
@@ -26,12 +28,10 @@ class ListenerMixin:
def listener(
self,
listener_or_event: Union[
Callable[..., Coroutine[Any, Any, None]], str
],
listener_or_event: Union[ListenerType[Sanic], str],
event_or_none: Optional[str] = None,
apply: bool = True,
):
) -> ListenerType[Sanic]:
"""
Create a listener from a decorated function.
@@ -63,20 +63,32 @@ class ListenerMixin:
else:
return partial(register_listener, event=listener_or_event)
def main_process_start(self, listener):
def main_process_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "main_process_start")
def main_process_stop(self, listener):
def main_process_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "main_process_stop")
def before_server_start(self, listener):
def before_server_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "before_server_start")
def after_server_start(self, listener):
def after_server_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "after_server_start")
def before_server_stop(self, listener):
def before_server_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "before_server_stop")
def after_server_stop(self, listener):
def after_server_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
return self.listener(listener, "after_server_stop")

View File

@@ -1,10 +1,11 @@
from functools import partial
from typing import List
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureMiddleware
class MiddlewareMixin:
class MiddlewareMixin(metaclass=SanicMeta):
def __init__(self, *args, **kwargs) -> None:
self._future_middleware: List[FutureMiddleware] = []

View File

@@ -1,31 +1,44 @@
from ast import NodeVisitor, Return, parse
from contextlib import suppress
from functools import partial, wraps
from inspect import signature
from inspect import getsource, signature
from mimetypes import guess_type
from os import path
from pathlib import PurePath
from re import sub
from pathlib import Path, PurePath
from textwrap import dedent
from time import gmtime, strftime
from typing import Iterable, List, Optional, Set, Union
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
from urllib.parse import unquote
from sanic_routing.route import Route # type: ignore
from sanic.base.meta import SanicMeta
from sanic.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.exceptions import (
ContentRangeError,
FileNotFound,
HeaderNotFound,
InvalidUsage,
)
from sanic.errorpages import RESPONSE_MAPPING
from sanic.exceptions import ContentRangeError, FileNotFound, HeaderNotFound
from sanic.handlers import ContentRangeHandler
from sanic.log import error_logger
from sanic.log import deprecation, error_logger
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import RouteHandler
from sanic.response import HTTPResponse, file, file_stream
from sanic.views import CompositionView
from sanic.types import HashableDict
class RouteMixin:
RouteWrapper = Callable[
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
]
RESTRICTED_ROUTE_CONTEXT = (
"ignore_body",
"stream",
"hosts",
"static",
"error_format",
"websocket",
)
class RouteMixin(metaclass=SanicMeta):
name: str
def __init__(self, *args, **kwargs) -> None:
@@ -43,7 +56,7 @@ class RouteMixin:
self,
uri: str,
methods: Optional[Iterable[str]] = None,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
stream: bool = False,
version: Optional[Union[int, str, float]] = None,
@@ -55,10 +68,21 @@ class RouteMixin:
unquote: bool = False,
static: bool = False,
version_prefix: str = "/v",
):
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteWrapper:
"""
Decorate a function to be registered as a route
**Example using context kwargs**
.. code-block:: python
@app.route(..., ctx_foo="foobar")
async def route_handler(request: Request):
assert request.route.ctx.foo == "foobar"
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:param host: the host, if required
@@ -70,6 +94,8 @@ class RouteMixin:
body (eg. GET requests)
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: tuple of routes, decorated function
"""
@@ -84,6 +110,8 @@ class RouteMixin:
if not methods and not websocket:
methods = frozenset({"GET"})
route_context = self._build_route_context(ctx_kwargs)
def decorator(handler):
nonlocal uri
nonlocal methods
@@ -97,6 +125,7 @@ class RouteMixin:
nonlocal websocket
nonlocal static
nonlocal version_prefix
nonlocal error_format
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
@@ -115,10 +144,16 @@ class RouteMixin:
"Expected either string or Iterable of host strings, "
"not %s" % host
)
if isinstance(subprotocols, (list, tuple, set)):
if isinstance(subprotocols, list):
# Ordered subprotocols, maintain order
subprotocols = tuple(subprotocols)
elif isinstance(subprotocols, set):
# subprotocol is unordered, keep it unordered
subprotocols = frozenset(subprotocols)
if not error_format or error_format == "auto":
error_format = self._determine_error_format(handler)
route = FutureRoute(
handler,
uri,
@@ -134,6 +169,8 @@ class RouteMixin:
unquote,
static,
version_prefix,
error_format,
route_context,
)
self._future_routes.add(route)
@@ -168,16 +205,18 @@ class RouteMixin:
def add_route(
self,
handler,
handler: RouteHandler,
uri: str,
methods: Iterable[str] = frozenset({"GET"}),
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
stream: bool = False,
version_prefix: str = "/v",
):
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteHandler:
"""A helper method to register class instance or
functions as a handler to the application url
routes.
@@ -193,6 +232,8 @@ class RouteMixin:
:param stream: boolean specifying if the handler is a stream handler
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: function or class instance
"""
# Handle HTTPMethodView differently
@@ -200,20 +241,13 @@ class RouteMixin:
methods = set()
for method in HTTP_METHODS:
_handler = getattr(handler.view_class, method.lower(), None)
view_class = getattr(handler, "view_class")
_handler = getattr(view_class, method.lower(), None)
if _handler:
methods.add(method)
if hasattr(_handler, "is_stream"):
stream = True
# handle composition view differently
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
for _handler in handler.handlers.values():
if hasattr(_handler, "is_stream"):
stream = True
break
if strict_slashes is None:
strict_slashes = self.strict_slashes
@@ -226,6 +260,8 @@ class RouteMixin:
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)(handler)
return handler
@@ -233,13 +269,15 @@ class RouteMixin:
def get(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
):
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **GET** *HTTP* method
@@ -251,6 +289,8 @@ class RouteMixin:
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -262,18 +302,22 @@ class RouteMixin:
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def post(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
stream: bool = False,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
):
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **POST** *HTTP* method
@@ -285,6 +329,8 @@ class RouteMixin:
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -296,18 +342,22 @@ class RouteMixin:
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def put(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
stream: bool = False,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
):
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **PUT** *HTTP* method
@@ -319,6 +369,8 @@ class RouteMixin:
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -330,18 +382,22 @@ class RouteMixin:
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def head(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
):
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **HEAD** *HTTP* method
@@ -361,6 +417,8 @@ class RouteMixin:
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -372,18 +430,22 @@ class RouteMixin:
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def options(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
):
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **OPTIONS** *HTTP* method
@@ -403,6 +465,8 @@ class RouteMixin:
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -414,18 +478,22 @@ class RouteMixin:
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def patch(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
stream=False,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
):
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **PATCH** *HTTP* method
@@ -447,6 +515,8 @@ class RouteMixin:
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -458,18 +528,22 @@ class RouteMixin:
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def delete(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
):
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **DELETE** *HTTP* method
@@ -481,6 +555,8 @@ class RouteMixin:
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -492,18 +568,22 @@ class RouteMixin:
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def websocket(
self,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
subprotocols: Optional[List[str]] = None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
apply: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
):
"""
Decorate a function to be registered as a websocket route
@@ -517,6 +597,8 @@ class RouteMixin:
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: tuple of routes, decorated function
"""
return self.route(
@@ -530,18 +612,22 @@ class RouteMixin:
subprotocols=subprotocols,
websocket=True,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def add_websocket_route(
self,
handler,
uri: str,
host: Optional[str] = None,
host: Optional[Union[str, List[str]]] = None,
strict_slashes: Optional[bool] = None,
subprotocols=None,
version: Optional[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
):
"""
A helper method to register a function as a websocket route.
@@ -560,6 +646,8 @@ class RouteMixin:
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Objected decorated by :func:`websocket`
"""
return self.websocket(
@@ -570,6 +658,8 @@ class RouteMixin:
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)(handler)
def static(
@@ -585,6 +675,7 @@ class RouteMixin:
strict_slashes=None,
content_type=None,
apply=True,
resource_type=None,
):
"""
Register a root to serve files from. The input can either be a
@@ -634,6 +725,7 @@ class RouteMixin:
host,
strict_slashes,
content_type,
resource_type,
)
self._future_statics.add(static)
@@ -677,32 +769,40 @@ class RouteMixin:
content_type=None,
__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__:
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
# from herping a derp and treating the uri as an absolute path
root_path = file_path = file_or_directory
if __file_uri__:
file_path = path.join(
file_or_directory, sub("^[/]*", "", __file_uri__)
)
file_path_raw = Path(unquote(file_or_directory))
root_path = file_path = file_path_raw.resolve()
not_found = FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__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))):
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
if __file_uri__:
# Strip all / that in the beginning of the URL to help prevent
# python from herping a derp and treating the uri as an
# absolute path
unquoted_file_uri = unquote(__file_uri__).lstrip("/")
file_path_raw = Path(file_or_directory, unquoted_file_uri)
file_path = file_path_raw.resolve()
if (
file_path < root_path and not file_path_raw.is_symlink()
) or ".." in file_path_raw.parts:
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise not_found
try:
file_path.relative_to(root_path)
except ValueError:
if not file_path_raw.is_symlink():
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise not_found
try:
headers = {}
# Check if the client has been sent this file before
@@ -770,17 +870,14 @@ class RouteMixin:
except ContentRangeError:
raise
except FileNotFoundError:
raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
raise not_found
except Exception:
error_logger.exception(
f"Exception in static request handler:\
path={file_or_directory}, "
f"Exception in static request handler: "
f"path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise
def _register_static(
self,
@@ -828,8 +925,27 @@ class RouteMixin:
name = static.name
# If we're not trying to match a file directly,
# serve from the folder
if not path.isfile(file_or_directory):
if not static.resource_type:
if not path.isfile(file_or_directory):
uri += "/<__file_uri__:path>"
elif static.resource_type == "dir":
if path.isfile(file_or_directory):
raise TypeError(
"Resource type improperly identified as directory. "
f"'{file_or_directory}'"
)
uri += "/<__file_uri__:path>"
elif static.resource_type == "file" and not path.isfile(
file_or_directory
):
raise TypeError(
"Resource type improperly identified as file. "
f"'{file_or_directory}'"
)
elif static.resource_type != "file":
raise ValueError(
"The resource_type should be set to 'file' or 'dir'"
)
# special prefix for static files
# if not static.name.startswith("_static_"):
@@ -846,7 +962,7 @@ class RouteMixin:
)
)
route, _ = self.route(
route, _ = self.route( # type: ignore
uri=uri,
methods=["GET", "HEAD"],
name=name,
@@ -856,3 +972,74 @@ class RouteMixin:
)(_handler)
return route
def _determine_error_format(self, handler) -> str:
with suppress(OSError, TypeError):
src = dedent(getsource(handler))
tree = parse(src)
http_response_types = self._get_response_types(tree)
if len(http_response_types) == 1:
return next(iter(http_response_types))
return ""
def _get_response_types(self, node):
types = set()
class HttpResponseVisitor(NodeVisitor):
def visit_Return(self, node: Return) -> Any:
nonlocal types
with suppress(AttributeError):
if node.value.func.id == "stream": # type: ignore
deprecation(
"The sanic.response.stream method has been "
"deprecated and will be removed in v22.6. Please "
"upgrade your application to use the new style "
"streaming pattern. See "
"https://sanicframework.org/en/guide/advanced/"
"streaming.html#response-streaming for more "
"information.",
22.6,
)
checks = [node.value.func.id] # type: ignore
if node.value.keywords: # type: ignore
checks += [
k.value
for k in node.value.keywords # type: ignore
if k.arg == "content_type"
]
for check in checks:
if check in RESPONSE_MAPPING:
types.add(RESPONSE_MAPPING[check])
HttpResponseVisitor().visit(node)
return types
def _build_route_context(self, raw):
ctx_kwargs = {
key.replace("ctx_", ""): raw.pop(key)
for key in {**raw}.keys()
if key.startswith("ctx_")
}
restricted = [
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
]
if restricted:
restricted_arguments = ", ".join(restricted)
raise AttributeError(
"Cannot use restricted route context: "
f"{restricted_arguments}. This limitation is only in place "
"until v22.3 when the restricted names will no longer be in"
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
"for more information."
)
if raw:
unexpected_arguments = ", ".join(raw.keys())
raise TypeError(
f"Unexpected keyword arguments: {unexpected_arguments}"
)
return HashableDict(ctx_kwargs)

View File

@@ -1,16 +1,14 @@
from typing import Any, Callable, Dict, Optional, Set
from enum import Enum
from typing import Any, Callable, Dict, Optional, Set, Union
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureSignal
from sanic.models.handler_types import SignalHandler
from sanic.signals import Signal
from sanic.types import HashableDict
class HashableDict(dict):
def __hash__(self):
return hash(tuple(sorted(self.items())))
class SignalMixin:
class SignalMixin(metaclass=SanicMeta):
def __init__(self, *args, **kwargs) -> None:
self._future_signals: Set[FutureSignal] = set()
@@ -19,11 +17,12 @@ class SignalMixin:
def signal(
self,
event: str,
event: Union[str, Enum],
*,
apply: bool = True,
condition: Dict[str, Any] = None,
) -> Callable[[SignalHandler], FutureSignal]:
exclusive: bool = True,
) -> Callable[[SignalHandler], SignalHandler]:
"""
For creating a signal handler, used similar to a route handler:
@@ -35,26 +34,29 @@ class SignalMixin:
:param event: Representation of the event in ``one.two.three`` form
:type event: str
:param apply: For lazy evaluation, defaults to True
:param apply: For lazy evaluation, defaults to ``True``
:type apply: bool, optional
:param condition: For use with the ``condition`` argument in dispatch
filtering, defaults to None
filtering, defaults to ``None``
:param exclusive: When ``True``, the signal can only be dispatched
when the condition has been met. When ``False``, the signal can
be dispatched either with or without it. *THIS IS INAPPLICABLE TO
BLUEPRINT SIGNALS. THEY ARE ALWAYS NON-EXCLUSIVE*, defaults
to ``True``
:type condition: Dict[str, Any], optional
"""
event_value = str(event.value) if isinstance(event, Enum) else event
def decorator(handler: SignalHandler):
nonlocal event
nonlocal apply
future_signal = FutureSignal(
handler, event, HashableDict(condition or {})
handler, event_value, HashableDict(condition or {}), exclusive
)
self._future_signals.add(future_signal)
if apply:
self._apply_signal(future_signal)
return future_signal
return handler
return decorator
@@ -63,6 +65,7 @@ class SignalMixin:
handler: Optional[Callable[..., Any]],
event: str,
condition: Dict[str, Any] = None,
exclusive: bool = True,
):
if not handler:
@@ -70,7 +73,9 @@ class SignalMixin:
...
handler = noop
self.signal(event=event, condition=condition)(handler)
self.signal(event=event, condition=condition, exclusive=exclusive)(
handler
)
return handler
def event(self, event: str):

View File

@@ -1,9 +1,10 @@
import asyncio
import sys
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
from sanic.exceptions import InvalidUsage
from sanic.websocket import WebSocketConnection
from sanic.server.websockets.connection import WebSocketConnection
ASGIScope = MutableMapping[str, Any]
@@ -14,10 +15,20 @@ ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol:
def __init__(self, transport: "MockTransport", loop):
# This should be refactored when < 3.8 support is dropped
self.transport = transport
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
# Fixup for 3.8+; Sanic still supports 3.7 where loop is required
loop = loop if sys.version_info[:2] < (3, 8) else None
# Optional in 3.9, necessary in 3.10 because the parameter "loop"
# was completely removed
if not loop:
self._not_paused = asyncio.Event()
self._not_paused.set()
self._complete = asyncio.Event()
else:
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
def pause_writing(self) -> None:
self._not_paused.clear()

View File

@@ -7,13 +7,14 @@ from sanic.models.handler_types import (
MiddlewareType,
SignalHandler,
)
from sanic.types import HashableDict
class FutureRoute(NamedTuple):
handler: str
uri: str
methods: Optional[Iterable[str]]
host: str
host: Union[str, List[str]]
strict_slashes: bool
stream: bool
version: Optional[int]
@@ -24,6 +25,8 @@ class FutureRoute(NamedTuple):
unquote: bool
static: bool
version_prefix: str
error_format: Optional[str]
route_context: HashableDict
class FutureListener(NamedTuple):
@@ -52,9 +55,15 @@ class FutureStatic(NamedTuple):
host: Optional[str]
strict_slashes: Optional[bool]
content_type: Optional[bool]
resource_type: Optional[str]
class FutureSignal(NamedTuple):
handler: SignalHandler
event: str
condition: Optional[Dict[str, str]]
exclusive: bool
class FutureRegistry(set):
...

View File

@@ -21,5 +21,5 @@ MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType]
ListenerType = Callable[
[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]
]
RouteHandler = Callable[..., Coroutine[Any, Any, HTTPResponse]]
RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]]
SignalHandler = Callable[..., Coroutine[Any, Any, None]]

View File

@@ -0,0 +1,64 @@
from ssl import SSLObject
from types import SimpleNamespace
from typing import Any, Dict, Optional
from sanic.models.protocol_types import TransportProtocol
class Signal:
stopped = False
class ConnInfo:
"""
Local and remote addresses and SSL status info.
"""
__slots__ = (
"client_port",
"client",
"client_ip",
"ctx",
"peername",
"server_port",
"server",
"server_name",
"sockname",
"ssl",
"cert",
)
def __init__(self, transport: TransportProtocol, unix=None):
self.ctx = SimpleNamespace()
self.peername = None
self.server = self.client = ""
self.server_port = self.client_port = 0
self.client_ip = ""
self.sockname = addr = transport.get_extra_info("sockname")
self.ssl = False
self.server_name = ""
self.cert: Dict[str, Any] = {}
sslobj: Optional[SSLObject] = transport.get_extra_info(
"ssl_object"
) # type: ignore
if sslobj:
self.ssl = True
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
self.cert = dict(getattr(sslobj.context, "sanic", {}))
if isinstance(addr, str): # UNIX socket
self.server = unix or addr
return
# IPv4 (ip, port) or IPv6 (ip, port, flowinfo, scopeid)
if isinstance(addr, tuple):
self.server = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
self.server_port = addr[1]
# self.server gets non-standard port appended
if addr[1] != (443 if self.ssl else 80):
self.server = f"{self.server}:{addr[1]}"
self.peername = addr = transport.get_extra_info("peername")
if isinstance(addr, tuple):
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
self.client_ip = addr[0]
self.client_port = addr[1]

View File

@@ -6,9 +6,6 @@ import sys
from time import sleep
from sanic.config import BASE_LOGO
from sanic.log import logger
def _iter_module_files():
"""This iterates over all relevant Python files.
@@ -50,13 +47,19 @@ def _get_args_for_reloading():
return [sys.executable] + sys.argv
def restart_with_reloader():
def restart_with_reloader(changed=None):
"""Create a new process and a subprocess in it with the same arguments as
this one.
"""
reloaded = ",".join(changed) if changed else ""
return subprocess.Popen(
_get_args_for_reloading(),
env={**os.environ, "SANIC_SERVER_RUNNING": "true"},
env={
**os.environ,
"SANIC_SERVER_RUNNING": "true",
"SANIC_RELOADER_PROCESS": "true",
"SANIC_RELOADED_FILES": reloaded,
},
)
@@ -91,31 +94,29 @@ def watchdog(sleep_interval, app):
worker_process = restart_with_reloader()
if app.config.LOGO:
logger.debug(
app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO
)
try:
while True:
need_reload = False
changed = set()
for filename in itertools.chain(
_iter_module_files(),
*(d.glob("**/*") for d in app.reload_dirs),
):
try:
check = _check_file(filename, mtimes)
if _check_file(filename, mtimes):
path = (
filename
if isinstance(filename, str)
else filename.resolve()
)
changed.add(str(path))
except OSError:
continue
if check:
need_reload = True
if need_reload:
if changed:
worker_process.terminate()
worker_process.wait()
worker_process = restart_with_reloader()
worker_process = restart_with_reloader(changed)
sleep(sleep_interval)
except KeyboardInterrupt:

View File

@@ -15,10 +15,9 @@ from typing import (
from sanic_routing.route import Route # type: ignore
if TYPE_CHECKING:
if TYPE_CHECKING: # no cov
from sanic.server import ConnInfo
from sanic.app import Sanic
from sanic.http import Http
import email.utils
import uuid
@@ -32,14 +31,17 @@ from httptools import parse_url # type: ignore
from sanic.compat import CancelledErrors, Header
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.exceptions import InvalidUsage
from sanic.exceptions import InvalidUsage, ServerError
from sanic.headers import (
AcceptContainer,
Options,
parse_accept,
parse_content_header,
parse_forwarded,
parse_host,
parse_xforwarded,
)
from sanic.http import Http, Stage
from sanic.log import error_logger, logger
from sanic.models.protocol_types import TransportProtocol
from sanic.response import BaseHTTPResponse, HTTPResponse
@@ -94,6 +96,7 @@ class Request:
"head",
"headers",
"method",
"parsed_accept",
"parsed_args",
"parsed_not_grouped_args",
"parsed_files",
@@ -101,6 +104,7 @@ class Request:
"parsed_json",
"parsed_forwarded",
"raw_url",
"responded",
"request_middleware_started",
"route",
"stream",
@@ -136,6 +140,7 @@ class Request:
self.conn_info: Optional[ConnInfo] = None
self.ctx = SimpleNamespace()
self.parsed_forwarded: Optional[Options] = None
self.parsed_accept: Optional[AcceptContainer] = None
self.parsed_json = None
self.parsed_form = None
self.parsed_files = None
@@ -151,6 +156,7 @@ class Request:
self.stream: Optional[Http] = None
self.route: Optional[Route] = None
self._protocol = None
self.responded: bool = False
def __repr__(self):
class_name = self.__class__.__name__
@@ -160,6 +166,21 @@ class Request:
def generate_id(*_):
return uuid.uuid4()
def reset_response(self):
try:
if (
self.stream is not None
and self.stream.stage is not Stage.HANDLER
):
raise ServerError(
"Cannot reset response because previous response was sent."
)
self.stream.response.stream = None
self.stream.response = None
self.responded = False
except AttributeError:
pass
async def respond(
self,
response: Optional[BaseHTTPResponse] = None,
@@ -168,13 +189,19 @@ class Request:
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
try:
if self.stream is not None and self.stream.response:
raise ServerError("Second respond call is not allowed.")
except AttributeError:
pass
# This logic of determining which response to use is subject to change
if response is None:
response = (self.stream and self.stream.response) or HTTPResponse(
response = HTTPResponse(
status=status,
headers=headers,
content_type=content_type,
)
# Connect the response
if isinstance(response, BaseHTTPResponse) and self.stream:
response = self.stream.respond(response)
@@ -189,6 +216,7 @@ class Request:
error_logger.exception(
"Exception occurred in one of response middleware handlers"
)
self.responded = True
return response
async def receive_body(self):
@@ -296,6 +324,13 @@ class Request:
return self.parsed_json
@property
def accept(self) -> AcceptContainer:
if self.parsed_accept is None:
accept_header = self.headers.getone("accept", "")
self.parsed_accept = parse_accept(accept_header)
return self.parsed_accept
@property
def token(self):
"""Attempt to return the auth header token.
@@ -497,6 +532,10 @@ class Request:
"""
return self._match_info
@match_info.setter
def match_info(self, value):
self._match_info = value
# Transport properties (obtained from local interface only)
@property
@@ -745,9 +784,10 @@ def parse_multipart_form(body, boundary):
break
colon_index = form_line.index(":")
idx = colon_index + 2
form_header_field = form_line[0:colon_index].lower()
form_header_value, form_parameters = parse_content_header(
form_line[colon_index + 2 :]
form_line[idx:]
)
if form_header_field == "content-disposition":

View File

@@ -1,8 +1,11 @@
from __future__ import annotations
from functools import partial
from mimetypes import guess_type
from os import path
from pathlib import PurePath
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Callable,
@@ -11,19 +14,27 @@ from typing import (
Iterator,
Optional,
Tuple,
TypeVar,
Union,
)
from urllib.parse import quote_plus
from warnings import warn
from sanic.compat import Header, open_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.cookies import CookieJar
from sanic.exceptions import SanicException, ServerError
from sanic.helpers import has_message_body, remove_entity_headers
from sanic.http import Http
from sanic.models.protocol_types import HTMLProtocol, Range
if TYPE_CHECKING:
from sanic.asgi import ASGIApp
from sanic.request import Request
else:
Request = TypeVar("Request")
try:
from ujson import dumps as json_dumps
except ImportError:
@@ -45,7 +56,7 @@ class BaseHTTPResponse:
self.asgi: bool = False
self.body: Optional[bytes] = None
self.content_type: Optional[str] = None
self.stream: Http = None
self.stream: Optional[Union[Http, ASGIApp]] = None
self.status: int = None
self.headers = Header({})
self._cookies: Optional[CookieJar] = None
@@ -101,7 +112,7 @@ class BaseHTTPResponse:
async def send(
self,
data: Optional[Union[AnyStr]] = None,
data: Optional[AnyStr] = None,
end_stream: Optional[bool] = None,
) -> None:
"""
@@ -112,8 +123,17 @@ class BaseHTTPResponse:
"""
if data is None and end_stream is None:
end_stream = True
if end_stream and not data and self.stream.send is None:
return
if self.stream is None:
raise SanicException(
"No stream is connected to the response object instance."
)
if self.stream.send is None:
if end_stream and not data:
return
raise ServerError(
"Response stream was ended, no more response data is "
"allowed to be sent."
)
data = (
data.encode() # type: ignore
if hasattr(data, "encode")
@@ -122,95 +142,6 @@ class BaseHTTPResponse:
await self.stream.send(data, end_stream=end_stream)
StreamingFunction = Callable[[BaseHTTPResponse], Coroutine[Any, Any, None]]
class StreamingHTTPResponse(BaseHTTPResponse):
"""
Old style streaming response where you pass a streaming function:
.. code-block:: python
async def sample_streaming_fn(response):
await response.write("foo")
await asyncio.sleep(1)
await response.write("bar")
await asyncio.sleep(1)
@app.post("/")
async def test(request):
return stream(sample_streaming_fn)
.. warning::
**Deprecated** and set for removal in v21.12. You can now achieve the
same functionality without a callback.
.. code-block:: python
@app.post("/")
async def test(request):
response = await request.respond()
await response.send("foo", False)
await asyncio.sleep(1)
await response.send("bar", False)
await asyncio.sleep(1)
await response.send("", True)
return response
"""
__slots__ = (
"streaming_fn",
"status",
"content_type",
"headers",
"_cookies",
)
def __init__(
self,
streaming_fn: StreamingFunction,
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: str = "text/plain; charset=utf-8",
ignore_deprecation_notice: bool = False,
):
if not ignore_deprecation_notice:
warn(
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
"will be removed in v21.12. Please upgrade your streaming "
"response implementation. You can learn more here: "
"https://sanicframework.org/en/guide/advanced/streaming.html"
"#response-streaming. If you use the builtin stream() or "
"file_stream() methods, this upgrade will be be done for you."
)
super().__init__()
self.content_type = content_type
self.streaming_fn = streaming_fn
self.status = status
self.headers = Header(headers or {})
self._cookies = None
async def write(self, data):
"""Writes a chunk of data to the streaming response.
:param data: str or bytes-ish data to be written.
"""
await super().send(self._encode_body(data))
async def send(self, *args, **kwargs):
if self.streaming_fn is not None:
await self.streaming_fn(self)
self.streaming_fn = None
await super().send(*args, **kwargs)
async def eof(self):
raise NotImplementedError
class HTTPResponse(BaseHTTPResponse):
"""
HTTP response to be sent back to the client.
@@ -405,6 +336,109 @@ async def file(
)
def redirect(
to: str,
headers: Optional[Dict[str, str]] = None,
status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""
Abort execution and cause a 302 redirect (by default) by setting a
Location header.
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type: the content type (string) of the response
"""
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers["Location"] = safe_to
return HTTPResponse(
status=status, headers=headers, content_type=content_type
)
class ResponseStream:
"""
ResponseStream is a compat layer to bridge the gap after the deprecation
of StreamingHTTPResponse. In v22.6 it will be removed when:
- stream is removed
- file_stream is moved to new style streaming
- file and file_stream are combined into a single API
"""
__slots__ = (
"_cookies",
"content_type",
"headers",
"request",
"response",
"status",
"streaming_fn",
)
def __init__(
self,
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]],
Coroutine[Any, Any, None],
],
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
self.streaming_fn = streaming_fn
self.status = status
self.headers = headers or Header()
self.content_type = content_type
self.request: Optional[Request] = None
self._cookies: Optional[CookieJar] = None
async def write(self, message: str):
await self.response.send(message)
async def stream(self) -> HTTPResponse:
if not self.request:
raise ServerError("Attempted response to unknown request")
self.response = await self.request.respond(
headers=self.headers,
status=self.status,
content_type=self.content_type,
)
await self.streaming_fn(self)
return self.response
async def eof(self) -> None:
await self.response.eof()
@property
def cookies(self) -> CookieJar:
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
@property
def processed_headers(self):
return self.response.processed_headers
@property
def body(self):
return self.response.body
def __call__(self, request: Request) -> ResponseStream:
self.request = request
return self
def __await__(self):
return self.stream().__await__()
async def file_stream(
location: Union[str, PurePath],
status: int = 200,
@@ -413,7 +447,7 @@ async def file_stream(
headers: Optional[Dict[str, str]] = None,
filename: Optional[str] = None,
_range: Optional[Range] = None,
) -> StreamingHTTPResponse:
) -> ResponseStream:
"""Return a streaming response object with file data.
:param location: Location of file on system.
@@ -421,7 +455,6 @@ async def file_stream(
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param filename: Override filename.
:param chunked: Deprecated
:param _range:
"""
headers = headers or {}
@@ -457,23 +490,24 @@ async def file_stream(
break
await response.write(content)
return StreamingHTTPResponse(
return ResponseStream(
streaming_fn=_streaming_fn,
status=status,
headers=headers,
content_type=mime_type,
ignore_deprecation_notice=True,
)
def stream(
streaming_fn: StreamingFunction,
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None]
],
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
) -> ResponseStream:
"""Accepts a coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `ResponseStream`.
Example usage::
@@ -487,42 +521,13 @@ def stream(
:param streaming_fn: A coroutine accepts a response and
writes content to that response.
:param mime_type: Specific mime_type.
:param status: HTTP status.
:param content_type: Specific content_type.
:param headers: Custom Headers.
:param chunked: Deprecated
"""
return StreamingHTTPResponse(
return ResponseStream(
streaming_fn,
headers=headers,
content_type=content_type,
status=status,
ignore_deprecation_notice=True,
)
def redirect(
to: str,
headers: Optional[Dict[str, str]] = None,
status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""
Abort execution and cause a 302 redirect (by default) by setting a
Location header.
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type: the content type (string) of the response
"""
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers["Location"] = safe_to
return HTTPResponse(
status=status, headers=headers, content_type=content_type
)

View File

@@ -1,5 +1,9 @@
from __future__ import annotations
from functools import lru_cache
from inspect import signature
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from uuid import UUID
from sanic_routing import BaseRouter # type: ignore
from sanic_routing.exceptions import NoMethod # type: ignore
@@ -9,6 +13,7 @@ from sanic_routing.exceptions import (
from sanic_routing.route import Route # type: ignore
from sanic.constants import HTTP_METHODS
from sanic.errorpages import check_error_format
from sanic.exceptions import MethodNotSupported, NotFound, SanicException
from sanic.models.handler_types import RouteHandler
@@ -49,7 +54,7 @@ class Router(BaseRouter):
self, path: str, method: str, host: Optional[str]
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
"""
Retrieve a `Route` object containg the details about how to handle
Retrieve a `Route` object containing the details about how to handle
a response for a given request
:param request: the incoming request object
@@ -74,6 +79,7 @@ class Router(BaseRouter):
unquote: bool = False,
static: bool = False,
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> Union[Route, List[Route]]:
"""
Add a handler to the router
@@ -106,6 +112,8 @@ class Router(BaseRouter):
version = str(version).strip("/").lstrip("v")
uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")])
uri = self._normalize(uri, handler)
params = dict(
path=uri,
handler=handler,
@@ -131,6 +139,10 @@ class Router(BaseRouter):
route.ctx.stream = stream
route.ctx.hosts = hosts
route.ctx.static = static
route.ctx.error_format = error_format
if error_format:
check_error_format(route.ctx.error_format)
routes.append(route)
@@ -187,3 +199,24 @@ class Router(BaseRouter):
raise SanicException(
f"Invalid route: {route}. Parameter names cannot use '__'."
)
def _normalize(self, uri: str, handler: RouteHandler) -> str:
if "<" not in uri:
return uri
sig = signature(handler)
mapping = {
param.name: param.annotation.__name__.lower()
for param in sig.parameters.values()
if param.annotation in (str, int, float, UUID)
}
reconstruction = []
for part in uri.split("/"):
if part.startswith("<") and ":" not in part:
name = part[1:-1]
annotation = mapping.get(name)
if annotation:
part = f"<{name}:{annotation}>"
reconstruction.append(part)
return "/".join(reconstruction)

View File

@@ -1,793 +0,0 @@
from __future__ import annotations
from ssl import SSLContext
from types import SimpleNamespace
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
Optional,
Type,
Union,
)
from sanic.models.handler_types import ListenerType
if TYPE_CHECKING:
from sanic.app import Sanic
import asyncio
import multiprocessing
import os
import secrets
import socket
import stat
from asyncio import CancelledError
from asyncio.transports import Transport
from functools import partial
from inspect import isawaitable
from ipaddress import ip_address
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func
from time import monotonic as current_time
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.config import Config
from sanic.exceptions import RequestTimeout, ServiceUnavailable
from sanic.http import Http, Stage
from sanic.log import error_logger, logger
from sanic.models.protocol_types import TransportProtocol
from sanic.request import Request
try:
import uvloop # type: ignore
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
class Signal:
stopped = False
class ConnInfo:
"""
Local and remote addresses and SSL status info.
"""
__slots__ = (
"client_port",
"client",
"client_ip",
"ctx",
"peername",
"server_port",
"server",
"sockname",
"ssl",
)
def __init__(self, transport: TransportProtocol, unix=None):
self.ctx = SimpleNamespace()
self.peername = None
self.server = self.client = ""
self.server_port = self.client_port = 0
self.client_ip = ""
self.sockname = addr = transport.get_extra_info("sockname")
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))
if isinstance(addr, str): # UNIX socket
self.server = unix or addr
return
# IPv4 (ip, port) or IPv6 (ip, port, flowinfo, scopeid)
if isinstance(addr, tuple):
self.server = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
self.server_port = addr[1]
# self.server gets non-standard port appended
if addr[1] != (443 if self.ssl else 80):
self.server = f"{self.server}:{addr[1]}"
self.peername = addr = transport.get_extra_info("peername")
if isinstance(addr, tuple):
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
self.client_ip = addr[0]
self.client_port = addr[1]
class HttpProtocol(asyncio.Protocol):
"""
This class provides a basic HTTP implementation of the sanic framework.
"""
__slots__ = (
# app
"app",
# event loop, connection
"loop",
"transport",
"connections",
"signal",
"conn_info",
"ctx",
# request params
"request",
# request config
"request_handler",
"request_timeout",
"response_timeout",
"keep_alive_timeout",
"request_max_size",
"request_class",
"error_handler",
# enable or disable access log purpose
"access_log",
# connection management
"state",
"url",
"_handler_task",
"_can_write",
"_data_received",
"_time",
"_task",
"_http",
"_exception",
"recv_buffer",
"_unix",
)
def __init__(
self,
*,
loop,
app: Sanic,
signal=None,
connections=None,
state=None,
unix=None,
**kwargs,
):
asyncio.set_event_loop(loop)
self.loop = loop
self.app: Sanic = app
self.url = None
self.transport: Optional[Transport] = None
self.conn_info: Optional[ConnInfo] = None
self.request: Optional[Request] = None
self.signal = signal or Signal()
self.access_log = self.app.config.ACCESS_LOG
self.connections = connections if connections is not None else set()
self.request_handler = self.app.handle_request
self.error_handler = self.app.error_handler
self.request_timeout = self.app.config.REQUEST_TIMEOUT
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
self.request_class = self.app.request_class or Request
self.state = state if state else {}
if "requests_count" not in self.state:
self.state["requests_count"] = 0
self._data_received = asyncio.Event()
self._can_write = asyncio.Event()
self._can_write.set()
self._exception = None
self._unix = unix
def _setup_connection(self):
self._http = Http(self)
self._time = current_time()
self.check_timeouts()
async def connection_task(self):
"""
Run a HTTP connection.
Timeouts and some additional error handling occur here, while most of
everything else happens in class Http or in code called from there.
"""
try:
self._setup_connection()
await self._http.http1()
except CancelledError:
pass
except Exception:
error_logger.exception("protocol.connection_task uncaught")
finally:
if self.app.debug and self._http:
ip = self.transport.get_extra_info("peername")
error_logger.error(
"Connection lost before response written"
f" @ {ip} {self._http.request}"
)
self._http = None
self._task = None
try:
self.close()
except BaseException:
error_logger.exception("Closing failed")
async def receive_more(self):
"""
Wait until more data is received into the Server protocol's buffer
"""
self.transport.resume_reading()
self._data_received.clear()
await self._data_received.wait()
def check_timeouts(self):
"""
Runs itself periodically to enforce any expired timeouts.
"""
try:
if not self._task:
return
duration = current_time() - self._time
stage = self._http.stage
if stage is Stage.IDLE and duration > self.keep_alive_timeout:
logger.debug("KeepAlive Timeout. Closing connection.")
elif stage is Stage.REQUEST and duration > self.request_timeout:
logger.debug("Request Timeout. Closing connection.")
self._http.exception = RequestTimeout("Request Timeout")
elif stage is Stage.HANDLER and self._http.upgrade_websocket:
logger.debug("Handling websocket. Timeouts disabled.")
return
elif (
stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED)
and duration > self.response_timeout
):
logger.debug("Response Timeout. Closing connection.")
self._http.exception = ServiceUnavailable("Response Timeout")
else:
interval = (
min(
self.keep_alive_timeout,
self.request_timeout,
self.response_timeout,
)
/ 2
)
self.loop.call_later(max(0.1, interval), self.check_timeouts)
return
self._task.cancel()
except Exception:
error_logger.exception("protocol.check_timeouts")
async def send(self, data):
"""
Writes data with backpressure control.
"""
await self._can_write.wait()
if self.transport.is_closing():
raise CancelledError
self.transport.write(data)
self._time = current_time()
def close_if_idle(self) -> bool:
"""
Close the connection if a request is not being sent or received
:return: boolean - True if closed, false if staying open
"""
if self._http is None or self._http.stage is Stage.IDLE:
self.close()
return True
return False
def close(self):
"""
Force close the connection.
"""
# Cause a call to connection_lost where further cleanup occurs
if self.transport:
self.transport.close()
self.transport = None
# -------------------------------------------- #
# Only asyncio.Protocol callbacks below this
# -------------------------------------------- #
def connection_made(self, transport):
try:
# TODO: Benchmark to find suitable write buffer limits
transport.set_write_buffer_limits(low=16384, high=65536)
self.connections.add(self)
self.transport = transport
self._task = self.loop.create_task(self.connection_task())
self.recv_buffer = bytearray()
self.conn_info = ConnInfo(self.transport, unix=self._unix)
except Exception:
error_logger.exception("protocol.connect_made")
def connection_lost(self, exc):
try:
self.connections.discard(self)
self.resume_writing()
if self._task:
self._task.cancel()
except Exception:
error_logger.exception("protocol.connection_lost")
def pause_writing(self):
self._can_write.clear()
def resume_writing(self):
self._can_write.set()
def data_received(self, data: bytes):
try:
self._time = current_time()
if not data:
return self.close()
self.recv_buffer += data
if (
len(self.recv_buffer) > self.app.config.REQUEST_BUFFER_SIZE
and self.transport
):
self.transport.pause_reading()
if self._data_received:
self._data_received.set()
except Exception:
error_logger.exception("protocol.data_received")
def trigger_events(events: Optional[Iterable[Callable[..., Any]]], loop):
"""
Trigger event callbacks (functions or async)
:param events: one or more sync or async functions to execute
:param loop: event loop
"""
if events:
for event in events:
result = event(loop)
if isawaitable(result):
loop.run_until_complete(result)
class AsyncioServer:
"""
Wraps an asyncio server with functionality that might be useful to
a user who needs to manage the server lifecycle manually.
"""
__slots__ = (
"loop",
"serve_coro",
"_after_start",
"_before_stop",
"_after_stop",
"server",
"connections",
)
def __init__(
self,
loop,
serve_coro,
connections,
after_start: Optional[Iterable[ListenerType]],
before_stop: Optional[Iterable[ListenerType]],
after_stop: Optional[Iterable[ListenerType]],
):
# Note, Sanic already called "before_server_start" events
# before this helper was even created. So we don't need it here.
self.loop = loop
self.serve_coro = serve_coro
self._after_start = after_start
self._before_stop = before_stop
self._after_stop = after_stop
self.server = None
self.connections = connections
def after_start(self):
"""
Trigger "after_server_start" events
"""
trigger_events(self._after_start, self.loop)
def before_stop(self):
"""
Trigger "before_server_stop" events
"""
trigger_events(self._before_stop, self.loop)
def after_stop(self):
"""
Trigger "after_server_stop" events
"""
trigger_events(self._after_stop, self.loop)
def is_serving(self) -> bool:
if self.server:
return self.server.is_serving()
return False
def wait_closed(self):
if self.server:
return self.server.wait_closed()
def close(self):
if self.server:
self.server.close()
coro = self.wait_closed()
task = asyncio.ensure_future(coro, loop=self.loop)
return task
def start_serving(self):
if self.server:
try:
return self.server.start_serving()
except AttributeError:
raise NotImplementedError(
"server.start_serving not available in this version "
"of asyncio or uvloop."
)
def serve_forever(self):
if self.server:
try:
return self.server.serve_forever()
except AttributeError:
raise NotImplementedError(
"server.serve_forever not available in this version "
"of asyncio or uvloop."
)
def __await__(self):
"""
Starts the asyncio server, returns AsyncServerCoro
"""
task = asyncio.ensure_future(self.serve_coro)
while not task.done():
yield
self.server = task.result()
return self
def serve(
host,
port,
app,
before_start: Optional[Iterable[ListenerType]] = None,
after_start: Optional[Iterable[ListenerType]] = None,
before_stop: Optional[Iterable[ListenerType]] = None,
after_stop: Optional[Iterable[ListenerType]] = None,
ssl: Optional[SSLContext] = None,
sock: Optional[socket.socket] = None,
unix: Optional[str] = None,
reuse_port: bool = False,
loop=None,
protocol: Type[asyncio.Protocol] = HttpProtocol,
backlog: int = 100,
register_sys_signals: bool = True,
run_multiple: bool = False,
run_async: bool = False,
connections=None,
signal=Signal(),
state=None,
asyncio_server_kwargs=None,
):
"""Start asynchronous HTTP Server on an individual process.
:param host: Address to host on
:param port: Port to host on
:param before_start: function to be executed before the server starts
listening. Takes arguments `app` instance and `loop`
:param after_start: function to be executed after the server starts
listening. Takes arguments `app` instance and `loop`
:param before_stop: function to be executed when a stop signal is
received before it is respected. Takes arguments
`app` instance and `loop`
:param after_stop: function to be executed when a stop signal is
received after it is respected. Takes arguments
`app` instance and `loop`
:param ssl: SSLContext
:param sock: Socket for the server to accept connections from
:param unix: Unix socket to listen on instead of TCP port
:param reuse_port: `True` for multiple workers
:param loop: asyncio compatible event loop
:param run_async: bool: Do not create a new event loop for the server,
and return an AsyncServer object rather than running it
:param asyncio_server_kwargs: key-value args for asyncio/uvloop
create_server method
:return: Nothing
"""
if not run_async and not loop:
# create new event_loop after fork
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if app.debug:
loop.set_debug(app.debug)
app.asgi = False
connections = connections if connections is not None else set()
protocol_kwargs = _build_protocol_kwargs(protocol, app.config)
server = partial(
protocol,
loop=loop,
connections=connections,
signal=signal,
app=app,
state=state,
unix=unix,
**protocol_kwargs,
)
asyncio_server_kwargs = (
asyncio_server_kwargs if asyncio_server_kwargs else {}
)
# UNIX sockets are always bound by us (to preserve semantics between modes)
if unix:
sock = bind_unix_socket(unix, backlog=backlog)
server_coroutine = loop.create_server(
server,
None if sock else host,
None if sock else port,
ssl=ssl,
reuse_port=reuse_port,
sock=sock,
backlog=backlog,
**asyncio_server_kwargs,
)
if run_async:
return AsyncioServer(
loop=loop,
serve_coro=server_coroutine,
connections=connections,
after_start=after_start,
before_stop=before_stop,
after_stop=after_stop,
)
trigger_events(before_start, loop)
try:
http_server = loop.run_until_complete(server_coroutine)
except BaseException:
error_logger.exception("Unable to start server")
return
trigger_events(after_start, loop)
# Ignore SIGINT when run_multiple
if run_multiple:
signal_func(SIGINT, SIG_IGN)
# Register signals for graceful termination
if register_sys_signals:
if OS_IS_WINDOWS:
ctrlc_workaround_for_windows(app)
else:
for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]:
loop.add_signal_handler(_signal, app.stop)
pid = os.getpid()
try:
logger.info("Starting worker [%s]", pid)
loop.run_forever()
finally:
logger.info("Stopping worker [%s]", pid)
# Run the on_stop function if provided
trigger_events(before_stop, loop)
# Wait for event loop to finish and all connections to drain
http_server.close()
loop.run_until_complete(http_server.wait_closed())
# Complete all tasks on the loop
signal.stopped = True
for connection in connections:
connection.close_if_idle()
# Gracefully shutdown timeout.
# We should provide graceful_shutdown_timeout,
# instead of letting connection hangs forever.
# Let's roughly calcucate time.
graceful = app.config.GRACEFUL_SHUTDOWN_TIMEOUT
start_shutdown: float = 0
while connections and (start_shutdown < graceful):
loop.run_until_complete(asyncio.sleep(0.1))
start_shutdown = start_shutdown + 0.1
# Force close non-idle connection after waiting for
# graceful_shutdown_timeout
coros = []
for conn in connections:
if hasattr(conn, "websocket") and conn.websocket:
coros.append(conn.websocket.close_connection())
else:
conn.close()
_shutdown = asyncio.gather(*coros)
loop.run_until_complete(_shutdown)
trigger_events(after_stop, loop)
remove_unix_socket(unix)
def _build_protocol_kwargs(
protocol: Type[asyncio.Protocol], config: Config
) -> Dict[str, Union[int, float]]:
if hasattr(protocol, "websocket_handshake"):
return {
"websocket_max_size": config.WEBSOCKET_MAX_SIZE,
"websocket_max_queue": config.WEBSOCKET_MAX_QUEUE,
"websocket_read_limit": config.WEBSOCKET_READ_LIMIT,
"websocket_write_limit": config.WEBSOCKET_WRITE_LIMIT,
"websocket_ping_timeout": config.WEBSOCKET_PING_TIMEOUT,
"websocket_ping_interval": config.WEBSOCKET_PING_INTERVAL,
}
return {}
def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket:
"""Create TCP server socket.
:param host: IPv4, IPv6 or hostname may be specified
:param port: TCP port number
:param backlog: Maximum number of connections to queue
:return: socket.socket object
"""
try: # IP address: family must be specified for IPv6 at least
ip = ip_address(host)
host = str(ip)
sock = socket.socket(
socket.AF_INET6 if ip.version == 6 else socket.AF_INET
)
except ValueError: # Hostname, may become AF_INET or AF_INET6
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))
sock.listen(backlog)
return sock
def bind_unix_socket(path: str, *, mode=0o666, backlog=100) -> socket.socket:
"""Create unix socket.
:param path: filesystem path
:param backlog: Maximum number of connections to queue
:return: socket.socket object
"""
"""Open or atomically replace existing socket with zero downtime."""
# Sanitise and pre-verify socket path
path = os.path.abspath(path)
folder = os.path.dirname(path)
if not os.path.isdir(folder):
raise FileNotFoundError(f"Socket folder does not exist: {folder}")
try:
if not stat.S_ISSOCK(os.stat(path, follow_symlinks=False).st_mode):
raise FileExistsError(f"Existing file is not a socket: {path}")
except FileNotFoundError:
pass
# Create new socket with a random temporary name
tmp_path = f"{path}.{secrets.token_urlsafe()}"
sock = socket.socket(socket.AF_UNIX)
try:
# Critical section begins (filename races)
sock.bind(tmp_path)
try:
os.chmod(tmp_path, mode)
# Start listening before rename to avoid connection failures
sock.listen(backlog)
os.rename(tmp_path, path)
except: # noqa: E722
try:
os.unlink(tmp_path)
finally:
raise
except: # noqa: E722
try:
sock.close()
finally:
raise
return sock
def remove_unix_socket(path: Optional[str]) -> None:
"""Remove dead unix socket during server exit."""
if not path:
return
try:
if stat.S_ISSOCK(os.stat(path, follow_symlinks=False).st_mode):
# Is it actually dead (doesn't belong to a new server instance)?
with socket.socket(socket.AF_UNIX) as testsock:
try:
testsock.connect(path)
except ConnectionRefusedError:
os.unlink(path)
except FileNotFoundError:
pass
def serve_single(server_settings):
main_start = server_settings.pop("main_start", None)
main_stop = server_settings.pop("main_stop", None)
if not server_settings.get("run_async"):
# create new event_loop after fork
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
server_settings["loop"] = loop
trigger_events(main_start, server_settings["loop"])
serve(**server_settings)
trigger_events(main_stop, server_settings["loop"])
server_settings["loop"].close()
def serve_multiple(server_settings, workers):
"""Start multiple server processes simultaneously. Stop on interrupt
and terminate signals, and drain connections when complete.
:param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch
:param stop_event: if provided, is used as a stop signal
:return:
"""
server_settings["reuse_port"] = True
server_settings["run_multiple"] = True
main_start = server_settings.pop("main_start", None)
main_stop = server_settings.pop("main_stop", None)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
trigger_events(main_start, loop)
# Create a listening socket or use the one in settings
sock = server_settings.get("sock")
unix = server_settings["unix"]
backlog = server_settings["backlog"]
if unix:
sock = bind_unix_socket(unix, backlog=backlog)
server_settings["unix"] = unix
if sock is None:
sock = bind_socket(
server_settings["host"], server_settings["port"], backlog=backlog
)
sock.set_inheritable(True)
server_settings["sock"] = sock
server_settings["host"] = None
server_settings["port"] = None
processes = []
def sig_handler(signal, frame):
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
for process in processes:
os.kill(process.pid, SIGTERM)
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
mp = multiprocessing.get_context("fork")
for _ in range(workers):
process = mp.Process(target=serve, kwargs=server_settings)
process.daemon = True
process.start()
processes.append(process)
for process in processes:
process.join()
# the above processes will block this until they're stopped
for process in processes:
process.terminate()
trigger_events(main_stop, loop)
sock.close()
loop.close()
remove_unix_socket(unix)

17
sanic/server/__init__.py Normal file
View File

@@ -0,0 +1,17 @@
from sanic.models.server_types import ConnInfo, Signal
from sanic.server.async_server import AsyncioServer
from sanic.server.loop import try_use_uvloop
from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.runners import serve, serve_multiple, serve_single
__all__ = (
"AsyncioServer",
"ConnInfo",
"HttpProtocol",
"Signal",
"serve",
"serve_multiple",
"serve_single",
"try_use_uvloop",
)

View File

@@ -0,0 +1,132 @@
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING
from sanic.exceptions import SanicException
from sanic.log import deprecation
if TYPE_CHECKING:
from sanic import Sanic
class AsyncioServer:
"""
Wraps an asyncio server with functionality that might be useful to
a user who needs to manage the server lifecycle manually.
"""
__slots__ = ("app", "connections", "loop", "serve_coro", "server")
def __init__(
self,
app: Sanic,
loop,
serve_coro,
connections,
):
# Note, Sanic already called "before_server_start" events
# before this helper was even created. So we don't need it here.
self.app = app
self.connections = connections
self.loop = loop
self.serve_coro = serve_coro
self.server = None
@property
def init(self):
deprecation(
"AsyncioServer.init has been deprecated and will be removed "
"in v22.6. Use Sanic.state.is_started instead.",
22.6,
)
return self.app.state.is_started
def startup(self):
"""
Trigger "before_server_start" events
"""
return self.app._startup()
def before_start(self):
"""
Trigger "before_server_start" events
"""
return self._server_event("init", "before")
def after_start(self):
"""
Trigger "after_server_start" events
"""
return self._server_event("init", "after")
def before_stop(self):
"""
Trigger "before_server_stop" events
"""
return self._server_event("shutdown", "before")
def after_stop(self):
"""
Trigger "after_server_stop" events
"""
return self._server_event("shutdown", "after")
def is_serving(self) -> bool:
if self.server:
return self.server.is_serving()
return False
def wait_closed(self):
if self.server:
return self.server.wait_closed()
def close(self):
if self.server:
self.server.close()
coro = self.wait_closed()
task = asyncio.ensure_future(coro, loop=self.loop)
return task
def start_serving(self):
return self._serve(self.server.start_serving)
def serve_forever(self):
return self._serve(self.server.serve_forever)
def _serve(self, serve_func):
if self.server:
if not self.app.state.is_started:
raise SanicException(
"Cannot run Sanic server without first running "
"await server.startup()"
)
try:
return serve_func()
except AttributeError:
name = serve_func.__name__
raise NotImplementedError(
f"server.{name} not available in this version "
"of asyncio or uvloop."
)
def _server_event(self, concern: str, action: str):
if not self.app.state.is_started:
raise SanicException(
"Cannot dispatch server event without "
"first running await server.startup()"
)
return self.app._server_event(concern, action, loop=self.loop)
def __await__(self):
"""
Starts the asyncio server, returns AsyncServerCoro
"""
task = asyncio.ensure_future(self.serve_coro)
while not task.done():
yield
self.server = task.result()
return self

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