Compare commits

...

139 Commits

Author SHA1 Message Date
Adam Hopkins
c8fa52e2d2 Allow application creation from Blueprint at startup 2021-12-15 09:36:40 +02:00
Adam Hopkins
266af1e279 Allow empty lazy, and multiple pre-registrations 2021-12-15 09:12:56 +02:00
Adam Hopkins
6d3f1e9982 Add lazy classmethod 2021-12-15 01:44:56 +02:00
Adam Hopkins
a0a3840094 Add pre-registry 2021-12-15 01:07:42 +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
5308fec354 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 21:05:44 +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
Adam Hopkins
5a48b94089 Bump version 21.6.1 2021-07-28 11:41:26 +03:00
L. Kärkkäinen
ba1c73d947 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:40:34 +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
a6e78b70ab Resolve regressions in exceptions (#2181) 2021-07-28 11:37:24 +03:00
L. Kärkkäinen
bb1174afc5 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-28 11:36:56 +03:00
Adam Hopkins
df8abe9cfd Manually reset the buffer when streaming request body (#2183) 2021-07-28 11:34:57 +03:00
Robert Palmer
c3bca97ee1 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-28 11:33:53 +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
Adam Hopkins
8df80e276b sanic-routing==0.7.0 2021-06-27 23:01:47 +03:00
Adam Hopkins
30572c972d 21.6 Changelog, release version, and deprecations (#2172)
* Changelog and version

* Rearrange API docs for easier navigation

* Version 21.6 docs

* Change release workflow

* Disable Windows tests
2021-06-27 22:52:56 +03:00
Adam Hopkins
53da4dd091 Allow blueprints and groups to be infinitely reusable (#2150)
* Allow blueprints and groups to be infinitely reusable
2021-06-21 18:41:04 +03:00
Adam Hopkins
108a4a99c7 v2 AST router (#2133)
* Update some tests

* Update some tests

* Resolve #2122 route decorator returning tuple

* Use rc sanic-routing version

* Update unit tests to <:str>
2021-06-21 15:10:26 +03:00
Adam Hopkins
7c180376d6 Add Simple Server and Coverage action (#2168)
* Add Simple Server and CodeCov action

* Remove token

* Codecov to tox.ini

* fix tox

* Set coverage location

* Add ignore to codecov

* Try glob ignore

* Setup CodeClimate

* Allow coverage check to run

* Change coverage check

* Add codeclimate exclusions
2021-06-21 14:53:09 +03:00
Adam Hopkins
f39b8b32f7 Make sure ASGI ws subprotocols is a list (#2127)
* Ensure protocols is a list for ASGI

* Subprotocol updates
2021-06-21 14:39:06 +03:00
Adam Hopkins
c543d19f8a CBV alternate attach; CompositionView deprecate (#2170)
* Deprecate composition view and add alternate methods to attach CBV

* Add args to CBV attaching
2021-06-21 14:26:42 +03:00
Adam Hopkins
80fca9aef7 Better exception handling (#2128)
* WIP for better exception handling

* Note about removal

* resolve conditional to reduce lookups

* Cleanup logic
2021-06-21 14:14:07 +03:00
Adam Hopkins
5bb9aa0c2c Add reloading on addtional directories (#2167) 2021-06-18 11:39:09 +03:00
Stephen Sadowski
83c746ee57 Added new client_ip accessor (#2114)
* Added new client_ip accessor for ConnInfo class, updated request to use client_ip instead of client to be more representative of what will be returned (actual ipv6 ip instead of bracket wrapped ip)

* Fix ConnInfo init

* add ipv6 test - maybe will work?

* fixed silly indentation error

* Bump testing client

* Extend testing

* Fix text

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-06-16 22:34:52 +03:00
Aymeric Augustin
aff6604636 Upgrade websockets dependency. (#2154)
* Upgrade websockets dependency.

Fix #2142.

* Bumpt sanic-testing version

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-06-16 21:49:50 +03:00
sanjeevanahilan
2c80571a8a Update listeners.py (#2164)
fix typo

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-06-16 16:00:29 +03:00
Adam Hopkins
d964b552af HTTPMethod enum (#2165)
* Add HTTP enum constants

* Tests

* Add some more string compat
2021-06-16 15:13:55 +03:00
Thomas Grainger
48f8b37b74 support app factory patten in CLI (#2157)
* support app factory patten in CLI

* Update sanic/__main__.py

* fix mypy errors

* Update mypy further

* Update sanic/utils.py

* Update sanic/utils.py

* support hypercorn/gunicorn style 'asgi.app:create_app()'

* add test for app factory
2021-06-09 12:05:56 +03:00
Adam Hopkins
141be0028d Allow 8192 header max to be breached (#2155)
* Allow 8192 header max to be breached

* Add REQUEST_MAX_HEADER_SIZE as config value

* remove queue size
2021-06-04 13:56:29 +03:00
L. Kärkkäinen
a140c47195 Remove config.REQUEST_BUFFER_QUEUE_SIZE which was not being used since 21.03. (#2156)
Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
2021-06-03 11:26:32 +03:00
Adam Hopkins
0c3a8392f2 Event autoregister (#2140)
* Add event autoregistration

* Setup tests

* Cleanup IDE added imports

* Remove unused imports
2021-06-01 10:44:07 +03:00
Adam Hopkins
16875b1f41 Disable MacOS Tests (#2151)
* Update pr-python37.yml

* Update workflows
2021-06-01 10:23:52 +03:00
Adam Hopkins
b1f31f2eeb Alternatate classes on instantiation for Config and Sanic.ctx (#2119) 2021-06-01 00:21:31 +03:00
Adam Hopkins
d16b9e5a02 Cleanup conftest and fix warning message (#2147) 2021-05-31 22:41:41 +03:00
Adam Hopkins
680484bdc8 Update README.rst 2021-05-31 22:10:15 +03:00
Adam Hopkins
05cd44b5dd Remove travis from repo (#2149)
* Remove travis from repo

* Use PR branch for Windows tests to add --user to pip args
2021-05-31 21:56:51 +03:00
Adam Hopkins
ba374139f4 Require stricter object names (#2146) 2021-05-30 15:37:44 +03:00
Adam Hopkins
72a745bfd5 Small improvements to CLI experience (#2136)
* Small improvements to CLI experience

* Add tests

* Add test server for cli testing

* Add LOGO logging to reloader and some additional context to logging debug

* Cleanup tests
2021-05-20 15:35:19 +03:00
Adam Hopkins
3a6fac7d59 Version prefix (#2137)
* Add version prefixing

* Versioning tests

* Testing BP group properties
2021-05-19 13:32:40 +03:00
Adam Hopkins
28ba8e53df Implement 0.6 routing and some cleanup (#2117)
* Implement 0.6 routing and some cleanup

* Additional tests and annotation cleanup

* Resolve sorting

* cleanup test with encoding
2021-04-20 00:53:42 +03:00
Ajay Gupta
9b26358e63 add eof method to close stream (#2094)
* add eof method to close stream

* Add eof test

Co-authored-by: Ajay Gupta <ajay.gupta@1mg.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-04-18 02:46:34 +03:00
Johnathan Raymond
e21521f45c Fix type hinting for load_env (#2107)
* Deprecate `load_env` in favor of `env_prefix`

`load_env` both enabled/disabled environment variable parsing, while
also letting the user modify the env prefix. Deprecate the ability to
disable environment parsing, and add a new config variable to track the
user's desired prefix for environment-based configuration.

Resolves: #2102

* Add a few common .gitignore patterns
2021-04-12 21:31:35 +03:00
Stephen Sadowski
30479765cb Fix request.args.pop removes parameters inconsistently (#2110)
* Fix https://github.com/sanic-org/sanic/issues/2106

* style

* also apply fix to request.query_args

* add test

Co-authored-by: Arthur Goldberg <arthur.c.goldberg@gmail.com>
Co-authored-by: artcg <arthurgoldbergfwd@gmail.com>
2021-04-11 01:04:33 +03:00
ENT8R
53a571ec6c Consistent use of error loggers (#2109)
* Consistent use of error loggers

* Fix tests
2021-04-10 21:35:53 +03:00
ENT8R
ad97cac313 Explicit usage of CIMultiDict getters (#2104) 2021-04-08 13:30:12 +03:00
Harsha Narayana
1a352ddf55 GIT-2023: Enable GitHub Actions support (#2050)
* GIT-2023: Enable GitHub Actions support

* GIT-2023: fix tox runtime trigger

* GIT-2023: add top level action name

* GIT-2023: rename tox step name

* GIT-2023: rename build task names

* GIT-2023: remove macos and windows + nightly versions

* GIT-2023: add macos and windows back to os matrix

* GIT-2023: expermiental flag to conditionally skip failure

* GIT-2023: enable using custom actions

* GIT-2023: fix matrix config rendering type

* GIT-2023: fix naming issue with os label

* GIT-2023: enable type-checking env for tox

* GIT-2023: enable pypy3.7 support

* GIT-2023: enable pypy experimental flag

* GIT-2023: add pypy tox env config

* add max timeout of 5 min for pypy tests

* GIT-2023: add timeout for each actions

* GIT-2023: fix codeQL workflow actions

* GIT-2023: limit test matrix to ubuntu and support on demand

* GIT-2023: enable docker image publish on release

* GIT-2023: fix on-demand pypy action

* GIT-2023: enable pypi publish workflow

* GIT-2023: enable verbose logs for py3.9

* GIT-2023: reduce py3.9 verbosity

* GIT-2023: enable docs linter

* GIT-2023: extend test matrix to include macos + windows

* GIT-2023: move windows based workflow to standalone task

* GIT-2023: fix windows test matrix

* GIT-2023: mark py39-no-ext as flaky test

* GIT-2023: mark flaky test

* GIT-2023: make timeout internal to steps for ease of management

* GIT-2023: rename image publish step name

* GIT-2023: mark keep alive client timeout for linux only

* GIT-2023: enable retries on test failure

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-04-06 23:32:01 +03:00
Lu
5ba43decf2 FIX UserWarning when using ASGI mode (#2091)
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-04-06 22:36:50 +03:00
Frederik Gelder
8f06d035cb fixing static request handler logging exception on 404 (#2099)
* fixing static request handler logging exception when not necessary, adding test to verify exception is gone on 404

* Fixup tests

* Fix tests

* resolve test failure

Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-04-06 22:20:25 +03:00
Kyle Verhoog
b716f48c84 Update outdated handle_request docstring (#2100) 2021-04-06 13:41:18 +03:00
Arthur Goldberg
42b1e7143e deprecate abort (#2077) 2021-04-05 18:01:48 +03:00
ENT8R
eba7821a6d Allow case-insensitive HTTP Upgrade header (#2097)
* Allow case-insensitive HTTP Upgrade header

* Allow case-insensitive Upgrade header when checking the scheme

* Fix reference to headers

* Add None check

* Simplify HTTP Upgrade checks

* Fix newlines at end of file

* Run make pretty
2021-04-05 14:15:45 +03:00
Adam Hopkins
93a0246c03 Bump version: 2021-03-23 02:31:17 +02:00
Adam Hopkins
dfd1787a49 Make sure that blueprints with no slash is maintained when applied (#2085)
* Make sure that blueprints with no slash is maintained when applied

* Remove unneeded import
2021-03-23 02:28:42 +02:00
Adam Hopkins
4998fd54c0 Disable response timeout on websocket connections (#2081)
* Disable response timeout on websocket connections

* Add response timeout ignore test to websockets

* add logging assertion

* Move test items inside test context
2021-03-23 01:20:17 +02:00
Adam Hopkins
7be5f0ed3d CHANGELOG for 21.3.1 2021-03-21 15:04:27 +02:00
Adam Hopkins
938d2b5923 Static dir 2075 (#2076)
* Add support for nested static directories

* Add support for nested static directories

* Bump version 21.3.1
2021-03-21 15:03:54 +02:00
Adam Hopkins
13630a79ad Update sanic-org URL on setup.py 2021-03-21 12:00:32 +02:00
191 changed files with 11549 additions and 3403 deletions

28
.codeclimate.yml Normal file
View File

@@ -0,0 +1,28 @@
exclude_patterns:
- "sanic/__main__.py"
- "sanic/application/logo.py"
- "sanic/application/motd.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
- ".github/"
- "changelogs/"
- "docker/"
- "docs/"
- "examples/"
- "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

@@ -1,7 +1,15 @@
[run]
branch = True
source = sanic
omit = site-packages, sanic/utils.py, sanic/__main__.py
omit =
site-packages
sanic/application/logo.py
sanic/application/motd.py
sanic/cli
sanic/__main__.py
sanic/reloader_helpers.py
sanic/simple.py
sanic/utils.py
[html]
directory = coverage

View File

@@ -1,27 +1,17 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
branches: [ main ]
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
@@ -29,39 +19,18 @@ jobs:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

36
.github/workflows/coverage.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Coverage check
on:
push:
branches:
- main
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:
python-version: [3.9]
os: [ubuntu-latest]
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies 🔨
run: |
python -m pip install --upgrade pip
pip install tox
- uses: paambaati/codeclimate-action@v2.5.3
if: always()
env:
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }}
with:
coverageCommand: tox -e coverage

39
.github/workflows/on-demand.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: On Demand Task
on:
workflow_dispatch:
inputs:
python-version:
description: 'Version of Python to use for running Test'
required: false
default: "3.8"
tox-env:
description: 'Test Environment to Run'
required: true
default: ''
os:
description: 'Operating System to Run Test on'
required: false
default: ubuntu-latest
jobs:
onDemand:
name: tox-${{ matrix.config.tox-env }}-on-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["${{ github.event.inputs.os}}"]
config:
- { tox-env: "${{ github.event.inputs.tox-env }}", py-version: "${{ github.event.inputs.python-version }}"}
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Run tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.py-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
experimental-ignore-error: "yes"

35
.github/workflows/pr-bandit.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Security Analysis
on:
pull_request:
branches:
- main
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:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
- { 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
id: checkout-branch
- name: Run Linter Checks
id: linter-check
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 }}"

31
.github/workflows/pr-docs.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Document Linter
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
docsLinter:
if: github.event.pull_request.draft == false
name: Lint Documentation
runs-on: ubuntu-latest
strategy:
matrix:
config:
- {python-version: "3.8", tox-env: "docs"}
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Run Document Linter
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 }}"

32
.github/workflows/pr-linter.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Linter Checks
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
linter:
if: github.event.pull_request.draft == false
name: lint
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
- { python-version: 3.8, tox-env: lint}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Linter Checks
id: linter-check
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 }}"

41
.github/workflows/pr-python-pypy.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Python PyPy Tests
on:
workflow_dispatch:
inputs:
tox-env:
description: "Tox Env to run on the PyPy Infra"
required: false
default: "pypy37"
pypy-version:
description: "Version of PyPy to use"
required: false
default: "pypy-3.7"
jobs:
testPyPy:
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: "${{ github.event.inputs.pypy-version }}",
tox-env: "${{ github.event.inputs.tox-env }}",
}
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 }}"
experimental-ignore-error: "true"
command-timeout: "600000"

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

@@ -0,0 +1,46 @@
name: Python 3.10 Tests
on:
pull_request:
branches:
- main
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"

34
.github/workflows/pr-python37.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Python 3.7 Tests
on:
pull_request:
branches:
- main
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: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- { python-version: 3.7, tox-env: py37 }
- { python-version: 3.7, tox-env: py37-no-ext }
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 }}"
test-failure-retry: "3"

34
.github/workflows/pr-python38.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Python 3.8 Tests
on:
pull_request:
branches:
- main
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: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- { python-version: 3.8, tox-env: py38 }
- { python-version: 3.8, tox-env: py38-no-ext }
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 }}"
test-failure-retry: "3"

46
.github/workflows/pr-python39.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Python 3.9 Tests
on:
pull_request:
branches:
- main
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: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: 3.9,
tox-env: py39,
ignore-error-flake: "false",
command-timeout: "0",
}
- {
python-version: 3.9,
tox-env: py39-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"

35
.github/workflows/pr-type-check.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Typing Checks
on:
pull_request:
branches:
- main
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:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
- { 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
id: checkout-branch
- name: Run Linter Checks
id: linter-check
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 }}"

37
.github/workflows/pr-windows.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Run Unit Tests on Windows
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
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
- 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"

48
.github/workflows/publish-images.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Publish Docker Images
on:
workflow_run:
workflows:
- 'Publish Artifacts'
types:
- completed
jobs:
publishDockerImages:
name: Docker Image Build [${{ matrix.python-version }}]
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Build Latest Base images for ${{ matrix.python-version }}
uses: harshanarayana/custom-actions@main
with:
docker-image-base-name: sanicframework/sanic-build
ignore-python-setup: 'true'
dockerfile-base-dir: './docker'
action: 'image-publish'
docker-image-tag: "${{ matrix.python-version }}"
docker-file-suffix: "base"
docker-build-args: "PYTHON_VERSION=${{ matrix.python-version }}"
registry-auth-user: ${{ secrets.DOCKER_ACCESS_USER }}
registry-auth-password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
push-images: 'true'
- name: Publish Sanic Docker Image for ${{ matrix.python-version }}
uses: harshanarayana/custom-actions@main
with:
docker-image-base-name: sanicframework/sanic
ignore-python-setup: 'true'
dockerfile-base-dir: './docker'
action: 'image-publish'
docker-build-args: "BASE_IMAGE_TAG=${{ matrix.python-version }}"
docker-image-prefix: "${{ matrix.python-version }}"
registry-auth-user: ${{ secrets.DOCKER_ACCESS_USER }}
registry-auth-password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
push-images: 'true'

28
.github/workflows/publish-package.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Publish Artifacts
on:
release:
types: [created]
jobs:
publishPythonPackage:
name: Publishing Sanic Release Artifacts
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.8"]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Publish Python Package
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.python-version }}
package-infra-name: "twine"
pypi-user: __token__
pypi-access-token: ${{ secrets.PYPI_ACCESS_TOKEN }}
action: "package-publish"
pypi-verify-metadata: "true"

4
.gitignore vendored
View File

@@ -6,6 +6,7 @@
.coverage
.coverage.*
coverage
coverage.xml
.tox
settings.py
.idea/*
@@ -18,3 +19,6 @@ build/*
.DS_Store
dist/*
pip-wheel-metadata/
.pytest_cache/*
.venv/*
.vscode/*

View File

@@ -1,94 +0,0 @@
sudo: false
language: python
cache:
directories:
- $HOME/.cache/pip
matrix:
include:
- env: TOX_ENV=py37
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 with Extensions"
- env: TOX_ENV=py37-no-ext
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 without Extensions"
- env: TOX_ENV=py38
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 with Extensions"
- env: TOX_ENV=py38-no-ext
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 without Extensions"
- env: TOX_ENV=py39
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 with Extensions"
- env: TOX_ENV=py39-no-ext
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 without Extensions"
- env: TOX_ENV=type-checking
python: 3.7
name: "Python 3.7 Type checks"
- env: TOX_ENV=type-checking
python: 3.8
name: "Python 3.8 Type checks"
- env: TOX_ENV=type-checking
python: 3.9
dist: bionic
name: "Python 3.9 Type checks"
- env: TOX_ENV=security
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Bandit security scan"
- env: TOX_ENV=security
python: 3.8
dist: xenial
sudo: true
name: "Python 3.8 Bandit security scan"
- env: TOX_ENV=security
python: 3.9
dist: bionic
sudo: true
name: "Python 3.9 Bandit security scan"
- env: TOX_ENV=docs
python: 3.7
dist: xenial
sudo: true
name: "Python 3.7 Documentation tests"
- env: TOX_ENV=pyNightly
python: "nightly"
name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext
python: "nightly"
name: "Python nightly without Extensions"
allow_failures:
- env: TOX_ENV=pyNightly
python: "nightly"
name: "Python nightly with Extensions"
- env: TOX_ENV=pyNightly-no-ext
python: "nightly"
name: "Python nightly without Extensions"
install:
- pip install -U tox
- pip install codecov
script: travis_retry tox -e $TOX_ENV
after_success:
- codecov
deploy:
provider: pypi
user: brewmaster
password:
secure: "GoawLwmbtJOgKB6AJ0ZSYUUnNwIoonseHBxaAUH3zu79TS/Afrq+yB3lsVaMSG0CbyDgN4FrfD1phT1NzbvZ1VcLIOTDtCrmpQ1kLDw+zwgF40ab8sp8fPkKVHHHfCCs1mjltHIpxQa5lZTJcAs6Bpi/lbUWWwYxFzSV8pHw4W4hY09EHUd2o+evLTSVxaploetSt725DJUYKICUr2eAtCC11IDnIW4CzBJEx6krVV3uhzfTJW0Ls17x0c6sdZ9icMnV/G9xO/eQH6RIHe4xcrWJ6cmLDNKoGAkJp+BKr1CeVVg7Jw/MzPjvZKL2/ki6Beue1y6GUIy7lOS7jPVaOEhJ23b0zQwFcLMZw+Tt+E3v6QfHk+B/WBBBnM3zUZed9UI+QyW8+lqLLt39sQX0FO0P3eaDh8qTXtUuon2jTyFMMAMTFRTNpJmpAzuBH9yeMmDeALPTh0HphI+BkoUl5q1QbWFYjjnZMH2CatApxpLybt9A7rwm//PbOG0TSI93GEKNQ4w5DYryKTfwHzRBptNSephJSuxZYEfJsmUtas5es1D7Fe0PkyjxNNSU+eO+8wsTlitLUsJO4k0jAgy+cEKdU7YJ3J0GZVXocSkrNnUfd2hQPcJ3UtEJx3hLqqr8EM7EZBAasc1yGHh36NFetclzFY24YPih0G1+XurhTys="
on:
tags: true
distributions: "sdist bdist_wheel"

View File

@@ -1,3 +1,135 @@
.. note::
From v21.9, CHANGELOG files are maintained in ``./docs/sanic/releases``
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
********
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
Add ``response.eof()`` method for closing a stream in a handler
* `#2097 <https://github.com/sanic-org/sanic/pull/2097>`_
Allow case-insensitive HTTP Upgrade header
* `#2104 <https://github.com/sanic-org/sanic/pull/2104>`_
Explicit usage of CIMultiDict getters
* `#2109 <https://github.com/sanic-org/sanic/pull/2109>`_
Consistent use of error loggers
* `#2114 <https://github.com/sanic-org/sanic/pull/2114>`_
New ``client_ip`` access of connection info instance
* `#2119 <https://github.com/sanic-org/sanic/pull/2119>`_
Alternatate classes on instantiation for ``Config`` and ``Sanic.ctx``
* `#2133 <https://github.com/sanic-org/sanic/pull/2133>`_
Implement new version of AST router
* Proper differentiation between ``alpha`` and ``string`` param types
* Adds a ``slug`` param type, example: ``<foo:slug>``
* Deprecates ``<foo:string>`` in favor of ``<foo:str>``
* Deprecates ``<foo:number>`` in favor of ``<foo:float>``
* Adds a ``route.uri`` accessor
* `#2136 <https://github.com/sanic-org/sanic/pull/2136>`_
CLI improvements with new optional params
* `#2137 <https://github.com/sanic-org/sanic/pull/2137>`_
Add ``version_prefix`` to URL builders
* `#2140 <https://github.com/sanic-org/sanic/pull/2140>`_
Event autoregistration with ``EVENT_AUTOREGISTER``
* `#2146 <https://github.com/sanic-org/sanic/pull/2146>`_, `#2147 <https://github.com/sanic-org/sanic/pull/2147>`_
Require stricter names on ``Sanic()`` and ``Blueprint()``
* `#2150 <https://github.com/sanic-org/sanic/pull/2150>`_
Infinitely reusable and nestable ``Blueprint`` and ``BlueprintGroup``
* `#2154 <https://github.com/sanic-org/sanic/pull/2154>`_
Upgrade ``websockets`` dependency to min version
* `#2155 <https://github.com/sanic-org/sanic/pull/2155>`_
Allow for maximum header sizes to be increased: ``REQUEST_MAX_HEADER_SIZE``
* `#2157 <https://github.com/sanic-org/sanic/pull/2157>`_
Allow app factory pattern in CLI
* `#2165 <https://github.com/sanic-org/sanic/pull/2165>`_
Change HTTP methods to enums
* `#2167 <https://github.com/sanic-org/sanic/pull/2167>`_
Allow auto-reloading on additional directories
* `#2168 <https://github.com/sanic-org/sanic/pull/2168>`_
Add simple HTTP server to CLI
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
Additional methods for attaching ``HTTPMethodView``
Bugfixes
********
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
* `#2099 <https://github.com/sanic-org/sanic/pull/2099>`_
Fix static request handler logging exception on 404
* `#2110 <https://github.com/sanic-org/sanic/pull/2110>`_
Fix request.args.pop removes parameters inconsistently
* `#2107 <https://github.com/sanic-org/sanic/pull/2107>`_
Fix type hinting for load_env
* `#2127 <https://github.com/sanic-org/sanic/pull/2127>`_
Make sure ASGI ws subprotocols is a list
* `#2128 <https://github.com/sanic-org/sanic/pull/2128>`_
Fix issue where Blueprint exception handlers do not consistently route to proper handler
Deprecations and Removals
*************************
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
``CompositionView`` deprecated and marked for removal in 21.12
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
Deprecate StreamingHTTPResponse
Developer infrastructure
************************
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
Remove Travis CI in favor of GitHub Actions
Improved Documentation
**********************
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
Fix typo in documentation
* `#2100 <https://github.com/sanic-org/sanic/pull/2100>`_
Remove documentation for non-existent arguments
Version 21.3.2
--------------
Bugfixes
********
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
Disable response timeout on websocket connections
* `#2085 <https://github.com/sanic-org/sanic/pull/2085>`_
Make sure that blueprints with no slash is maintained when applied
Version 21.3.1
--------------
Bugfixes
********
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
Static files inside subfolders are not accessible (404)
Version 21.3.0
--------------
@@ -525,7 +657,7 @@ 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
@@ -958,7 +1090,7 @@ 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
@@ -997,7 +1129,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)

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
------------------
@@ -87,7 +87,7 @@ Permform ``flake8``\ , ``black`` and ``isort`` checks.
tox -e lint
Run type annotation checks
---------------
--------------------------
``tox`` environment -> ``[testenv:type-checking]``

View File

@@ -49,6 +49,9 @@ test: clean
test-coverage: clean
python setup.py test --pytest-args="--cov sanic --cov-report term --cov-append "
view-coverage:
sanic ./coverage --simple
install:
python setup.py install
@@ -85,8 +88,7 @@ docs-test: docs-clean
cd docs && make dummy
docs-serve:
# python -m http.server --directory=./docs/_build/html 9999
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./sanic
sphinx-autobuild docs docs/_build/html --port 9999 --watch ./
changelog:
python scripts/changelog.py

View File

@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
:stub-columns: 1
* - Build
- | |Build Status| |AppVeyor Build Status| |Codecov|
- | |Py39Test| |Py38Test| |Py37Test|
* - Docs
- | |UserGuide| |Documentation|
* - Package
@@ -27,12 +27,12 @@ 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
.. |Build Status| image:: https://travis-ci.com/sanic-org/sanic.svg?branch=master
:target: https://travis-ci.com/sanic-org/sanic
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
:target: https://ci.appveyor.com/project/sanic-org/sanic
.. |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
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml
.. |Py37Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
@@ -75,17 +75,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
------------
@@ -170,3 +164,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

View File

@@ -1,14 +0,0 @@
codecov:
require_ci_to_pass: no
coverage:
precision: 3
round: nearest
status:
project:
default:
target: auto
threshold: 0.5%
patch:
default:
target: auto
threshold: 0.75%

View File

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

9
docker/Dockerfile-base Normal file
View File

@@ -0,0 +1,9 @@
ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION}-alpine
RUN apk update
RUN apk add --no-cache --update build-base \
ca-certificates \
openssl
RUN update-ca-certificates
RUN rm -rf /var/cache/apk/*

View File

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

View File

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

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,
}

17
docs/sanic/api/app.rst Normal file
View File

@@ -0,0 +1,17 @@
Application
===========
sanic.app
---------
.. automodule:: sanic.app
:members:
:show-inheritance:
:inherited-members:
sanic.config
------------
.. automodule:: sanic.config
:members:
:show-inheritance:

View File

@@ -0,0 +1,17 @@
Blueprints
==========
sanic.blueprints
----------------
.. automodule:: sanic.blueprints
:members:
:show-inheritance:
:inherited-members:
sanic.blueprint_group
---------------------
.. automodule:: sanic.blueprint_group
:members:
:special-members:

47
docs/sanic/api/core.rst Normal file
View File

@@ -0,0 +1,47 @@
Core
====
sanic.cookies
-------------
.. automodule:: sanic.cookies
:members:
:show-inheritance:
sanic.handlers
--------------
.. automodule:: sanic.handlers
:members:
:show-inheritance:
sanic.request
-------------
.. automodule:: sanic.request
:members:
:show-inheritance:
sanic.response
--------------
.. automodule:: sanic.response
:members:
:show-inheritance:
sanic.views
-----------
.. automodule:: sanic.views
:members:
:show-inheritance:
sanic.websocket
---------------
.. automodule:: sanic.websocket
:members:
:show-inheritance:

View File

@@ -0,0 +1,16 @@
Exceptions
==========
sanic.errorpages
----------------
.. automodule:: sanic.errorpages
:members:
:show-inheritance:
sanic.exceptions
----------------
.. automodule:: sanic.exceptions
:members:
:show-inheritance:

18
docs/sanic/api/router.rst Normal file
View File

@@ -0,0 +1,18 @@
Routing
=======
sanic_routing models
--------------------
.. autoclass:: sanic_routing.route::Route
:members:
.. autoclass:: sanic_routing.group::RouteGroup
:members:
sanic.router
------------
.. automodule:: sanic.router
:members:
:show-inheritance:

25
docs/sanic/api/server.rst Normal file
View File

@@ -0,0 +1,25 @@
Sanic Server
============
sanic.http
----------
.. automodule:: sanic.http
:members:
:show-inheritance:
sanic.server
------------
.. automodule:: sanic.server
:members:
:show-inheritance:
sanic.worker
------------
.. automodule:: sanic.worker
:members:
:show-inheritance:

View File

@@ -0,0 +1,16 @@
Utility
=======
sanic.compat
------------
.. automodule:: sanic.compat
:members:
:show-inheritance:
sanic.log
---------
.. automodule:: sanic.log
:members:
:show-inheritance:

View File

@@ -1,132 +1,13 @@
📑 API Reference
================
sanic.app
---------
.. toctree::
:maxdepth: 2
.. automodule:: sanic.app
:members:
:show-inheritance:
:inherited-members:
sanic.blueprints
----------------
.. automodule:: sanic.blueprints
:members:
:show-inheritance:
:inherited-members:
sanic.blueprint_group
---------------------
.. automodule:: sanic.blueprint_group
:members:
:special-members:
sanic.compat
------------
.. automodule:: sanic.compat
:members:
:show-inheritance:
sanic.config
------------
.. automodule:: sanic.config
:members:
:show-inheritance:
sanic.cookies
-------------
.. automodule:: sanic.cookies
:members:
:show-inheritance:
sanic.errorpages
----------------
.. automodule:: sanic.errorpages
:members:
:show-inheritance:
sanic.exceptions
----------------
.. automodule:: sanic.exceptions
:members:
:show-inheritance:
sanic.handlers
--------------
.. automodule:: sanic.handlers
:members:
:show-inheritance:
sanic.http
----------
.. automodule:: sanic.http
:members:
:show-inheritance:
sanic.log
---------
.. automodule:: sanic.log
:members:
:show-inheritance:
sanic.request
-------------
.. automodule:: sanic.request
:members:
:show-inheritance:
sanic.response
--------------
.. automodule:: sanic.response
:members:
:show-inheritance:
sanic.router
------------
.. automodule:: sanic.router
:members:
:show-inheritance:
sanic.server
------------
.. automodule:: sanic.server
:members:
:show-inheritance:
sanic.views
-----------
.. automodule:: sanic.views
:members:
:show-inheritance:
sanic.websocket
---------------
.. automodule:: sanic.websocket
:members:
:show-inheritance:
sanic.worker
------------
.. automodule:: sanic.worker
:members:
:show-inheritance:
api/app
api/blueprints
api/core
api/exceptions
api/router
api/server
api/utility

View File

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

View File

@@ -0,0 +1,40 @@
## Version 21.9
### 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(__name__)
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(__name__)
@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(__name__)
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__)
bp = Blueprint("bp_"+__name__)
bp = Blueprint("bp_" + __name__)
@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,6 +1,7 @@
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")

View File

@@ -2,17 +2,20 @@ from asyncio import sleep
from sanic import Sanic, response
app = Sanic(__name__, 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(__name__, 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,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__)
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
@@ -49,8 +49,7 @@ app = Sanic(__name__, 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,10 +32,12 @@ 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)
@@ -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

@@ -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,9 +1,10 @@
from sanic import Sanic
from sanic import response
from signal import signal, SIGINT
import asyncio
import uvloop
from sanic import Sanic, response
app = Sanic(__name__)
@@ -11,12 +12,19 @@ app = Sanic(__name__)
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
from sanic import Sanic, response
from sanic.server import AsyncioServer
app = Sanic(__name__)
@app.listener('after_server_start')
async def after_start_test(app, loop):
print("Async Server Started!")
@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.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

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

BIN
examples/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

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

View File

@@ -1,9 +1,9 @@
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__)
@@ -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,7 +1,8 @@
from sanic import Sanic
from sanic import response
import socket
import os
import socket
from sanic import Sanic, response
app = Sanic(__name__)
@@ -10,14 +11,15 @@ app = Sanic(__name__)
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
from sanic import Sanic, response
app = Sanic(__name__)
@app.route('/')
@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

@@ -1,24 +1,27 @@
from sanic import Sanic
from sanic.response import file
from sanic.response import redirect
app = Sanic(__name__)
@app.route('/')
async def index(request):
return await file('websocket.html')
app.static("index.html", "websocket.html")
@app.websocket('/feed')
@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)

3
pyproject.toml Normal file
View File

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

View File

@@ -1,6 +1,7 @@
from sanic.__version__ import __version__
from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.constants import HTTPMethod
from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
@@ -9,6 +10,7 @@ __all__ = (
"__version__",
"Sanic",
"Blueprint",
"HTTPMethod",
"HTTPResponse",
"Request",
"html",

View File

@@ -1,131 +1,15 @@
import os
import sys
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from importlib import import_module
from typing import Any, Dict, Optional
from sanic import __version__
from sanic.app import Sanic
from sanic.config import BASE_LOGO
from sanic.log import logger
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"] = "no " + kwargs["help"]
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=RawDescriptionHelpFormatter,
)
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",
)
parser.add_argument(
"--cert", dest="cert", type=str, help="location of certificate for SSL"
)
parser.add_argument(
"--key", dest="key", type=str, help="location of keyfile for SSL."
)
parser.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="number of worker processes [default 1]",
)
parser.add_argument("--debug", dest="debug", action="store_true")
parser.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"Sanic {__version__}",
)
parser.add_argument(
"module", help="path to your Sanic app. Example: path.to.server:app"
)
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 ":" in args.module:
module_name, app_name = args.module.rsplit(":", 1)
else:
module_parts = args.module.split(".")
module_name = ".".join(module_parts[:-1])
app_name = module_parts[-1]
module = import_module(module_name)
app = getattr(module, app_name, None)
app_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_name}. "
f"Perhaps you meant {args.module}.app?"
)
if args.cert is not None or args.key is not None:
ssl = {
"cert": args.cert,
"key": args.key,
} # type: Optional[Dict[str, Any]]
else:
ssl = None
app.run(
host=args.host,
port=args.port,
unix=args.unix,
workers=args.workers,
debug=args.debug,
access_log=args.access_log,
ssl=ssl,
)
except ImportError as e:
logger.error(
f"No module named {e.name} found.\n"
f" Example File: project/sanic_server.py -> app\n"
f" Example Module: project.sanic_server.app"
)
except ValueError:
logger.exception("Failed to run app")
cli = SanicCLI()
cli.attach()
cli.run()
if __name__ == "__main__":

View File

@@ -1 +1 @@
__version__ = "21.3.0"
__version__ = "21.12.0dev"

File diff suppressed because it is too large Load Diff

View File

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

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

@@ -0,0 +1,146 @@
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 __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
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,74 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from typing import TYPE_CHECKING, Any, 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="")
mode: Mode = field(default=Mode.PRODUCTION)
port: int = field(default=0)
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,32 @@ import sanic.app # noqa
from sanic.compat import Header
from sanic.exceptions import ServerError
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 +49,9 @@ 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", [])
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._startup()
await self.asgi_app.sanic_app._server_event("init", "before")
await self.asgi_app.sanic_app._server_event("init", "after")
async def shutdown(self) -> None:
"""
@@ -65,16 +62,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 +85,8 @@ class ASGIApp:
transport: MockTransport
lifespan: Lifespan
ws: Optional[WebSocketConnection]
stage: Stage
response: Optional[BaseHTTPResponse]
def __init__(self) -> None:
self.ws = None
@@ -108,6 +99,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(
@@ -140,7 +133,6 @@ class ASGIApp:
instance.ws = instance.transport.create_websocket_connection(
send, receive
)
await instance.ws.accept()
else:
raise ServerError("Received unknown ASGI scope")
@@ -163,11 +155,15 @@ 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):
self.request_body = False
return None
return message.get("body", b"")
if not body:
return None
return body
async def __aiter__(self):
while self.request_body:
@@ -175,11 +171,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(
@@ -206,4 +208,8 @@ class ASGIApp:
"""
Handle the incoming request.
"""
await self.sanic_app.handle_request(self.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)

View File

@@ -1,6 +1,9 @@
import re
from typing import Any, Tuple
from warnings import warn
from sanic.exceptions import SanicException
from sanic.mixins.exceptions import ExceptionMixin
from sanic.mixins.listeners import ListenerMixin
from sanic.mixins.middleware import MiddlewareMixin
@@ -8,6 +11,9 @@ from sanic.mixins.routes import RouteMixin
from sanic.mixins.signals import SignalMixin
VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
class BaseSanic(
RouteMixin,
MiddlewareMixin,
@@ -17,7 +23,25 @@ class BaseSanic(
):
__fake_slots__: Tuple[str, ...]
def __init__(self, *args, **kwargs) -> None:
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
class_name = self.__class__.__name__
if name is None:
raise SanicException(
f"{class_name} instance cannot be unnamed. "
"Please use Sanic(name='your_application_name') instead.",
)
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,
)
self.name = name
for base in BaseSanic.__bases__:
base.__init__(self, *args, **kwargs) # type: ignore
@@ -34,8 +58,9 @@ class BaseSanic(
if name not in self.__fake_slots__:
warn(
f"Setting variables on {self.__class__.__name__} instances is "
"deprecated and will be removed in version 21.9. You should "
"deprecated and will be removed in version 21.12. You should "
f"change your {self.__class__.__name__} instance to use "
f"instance.ctx.{name} instead."
f"instance.ctx.{name} instead.",
DeprecationWarning,
)
super().__setattr__(name, value)

View File

@@ -1,7 +1,12 @@
from collections.abc import MutableSequence
from typing import List, Optional, Union
from __future__ import annotations
import sanic
from collections.abc import MutableSequence
from functools import partial
from typing import TYPE_CHECKING, List, Optional, Union
if TYPE_CHECKING:
from sanic.blueprints import Blueprint
class BlueprintGroup(MutableSequence):
@@ -54,9 +59,21 @@ class BlueprintGroup(MutableSequence):
app.blueprint(bpg)
"""
__slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes")
__slots__ = (
"_blueprints",
"_url_prefix",
"_version",
"_strict_slashes",
"_version_prefix",
)
def __init__(self, url_prefix=None, version=None, strict_slashes=None):
def __init__(
self,
url_prefix: Optional[str] = None,
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
):
"""
Create a new Blueprint Group
@@ -65,13 +82,14 @@ class BlueprintGroup(MutableSequence):
inherited by each of the Blueprint
:param strict_slashes: URL Strict slash behavior indicator
"""
self._blueprints = []
self._blueprints: List[Blueprint] = []
self._url_prefix = url_prefix
self._version = version
self._version_prefix = version_prefix
self._strict_slashes = strict_slashes
@property
def url_prefix(self) -> str:
def url_prefix(self) -> Optional[Union[int, str, float]]:
"""
Retrieve the URL prefix being used for the Current Blueprint Group
@@ -80,7 +98,7 @@ class BlueprintGroup(MutableSequence):
return self._url_prefix
@property
def blueprints(self) -> List["sanic.Blueprint"]:
def blueprints(self) -> List[Blueprint]:
"""
Retrieve a list of all the available blueprints under this group.
@@ -107,6 +125,15 @@ class BlueprintGroup(MutableSequence):
"""
return self._strict_slashes
@property
def version_prefix(self) -> str:
"""
Version prefix; defaults to ``/v``
:return: str
"""
return self._version_prefix
def __iter__(self):
"""
Tun the class Blueprint Group into an Iterable item
@@ -161,34 +188,37 @@ class BlueprintGroup(MutableSequence):
"""
return len(self._blueprints)
def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint":
"""
Sanitize the Blueprint Entity to override the Version and strict slash
behaviors as required.
:param bp: Sanic Blueprint entity Object
:return: Modified Blueprint
"""
if self._url_prefix:
merged_prefix = "/".join(
u.strip("/") for u in [self._url_prefix, bp.url_prefix or ""]
).rstrip("/")
bp.url_prefix = f"/{merged_prefix}"
for _attr in ["version", "strict_slashes"]:
if getattr(bp, _attr) is None:
setattr(bp, _attr, getattr(self, _attr))
return bp
def append(self, value: "sanic.Blueprint") -> None:
def append(self, value: Blueprint) -> None:
"""
The Abstract class `MutableSequence` leverages this append method to
perform the `BlueprintGroup.append` operation.
:param value: New `Blueprint` object.
:return: None
"""
self._blueprints.append(self._sanitize_blueprint(bp=value))
self._blueprints.append(value)
def insert(self, index: int, item: "sanic.Blueprint") -> None:
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
perform the `BlueprintGroup.append` operation.
@@ -197,7 +227,7 @@ class BlueprintGroup(MutableSequence):
:param item: New `Blueprint` object.
:return: None
"""
self._blueprints.insert(index, self._sanitize_blueprint(item))
self._blueprints.insert(index, item)
def middleware(self, *args, **kwargs):
"""
@@ -221,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,8 +3,23 @@ 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
@@ -12,6 +27,7 @@ from sanic_routing.route import Route # type: ignore
from sanic.base 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,
@@ -24,6 +40,32 @@ if TYPE_CHECKING:
from sanic import Sanic # noqa
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):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
@@ -37,10 +79,10 @@ 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__ = (
@@ -62,32 +104,32 @@ class Blueprint(BaseSanic):
"strict_slashes",
"url_prefix",
"version",
"version_prefix",
"websocket_routes",
)
__pre_registry__: Dict[Union[str, Default], Any] = defaultdict(list)
def __init__(
self,
name: str,
name: str = None,
url_prefix: Optional[str] = None,
host: Optional[str] = None,
version: Optional[int] = 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__()
self._apps: Set[Sanic] = set()
super().__init__(name=name)
self.reset()
self.ctx = SimpleNamespace()
self.exceptions: List[RouteHandler] = []
self.host = host
self.listeners: Dict[str, List[ListenerType]] = {}
self.middlewares: List[MiddlewareType] = []
self.name = name
self.routes: List[Route] = []
self.statics: List[RouteHandler] = []
self.strict_slashes = strict_slashes
self.url_prefix = url_prefix
self.url_prefix = (
url_prefix[:-1]
if url_prefix and url_prefix.endswith("/")
else url_prefix
)
self.version = version
self.websocket_routes: List[Route] = []
self.version_prefix = version_prefix
def __repr__(self) -> str:
args = ", ".join(
@@ -114,32 +156,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):
def group(
*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.
@@ -156,8 +266,6 @@ class Blueprint(BaseSanic):
for i in nested:
if isinstance(i, (list, tuple)):
yield from chain(i)
elif isinstance(i, BlueprintGroup):
yield from i.blueprints
else:
yield i
@@ -165,6 +273,7 @@ class Blueprint(BaseSanic):
url_prefix=url_prefix,
version=version,
strict_slashes=strict_slashes,
version_prefix=version_prefix,
)
for bp in chain(blueprints):
bps.append(bp)
@@ -182,11 +291,18 @@ class Blueprint(BaseSanic):
self._apps.add(app)
url_prefix = options.get("url_prefix", self.url_prefix)
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:
@@ -196,30 +312,49 @@ class Blueprint(BaseSanic):
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
strict_slashes = (
self.strict_slashes
if future.strict_slashes is None
and self.strict_slashes is not None
else future.strict_slashes
version_prefix = self.version_prefix
for prefix in (
future.version_prefix,
opt_version_prefix,
):
if prefix and prefix != "/v":
version_prefix = prefix
break
version = self._extract_value(
future.version, opt_version, self.version
)
strict_slashes = self._extract_value(
future.strict_slashes, opt_strict_slashes, self.strict_slashes
)
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,
future.version or self.version,
version,
name,
future.ignore_body,
future.websocket,
future.subprotocols,
future.unquote,
future.static,
version_prefix,
error_format,
)
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
@@ -231,37 +366,64 @@ 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})
app._apply_signal(future)
self.routes = [route for route in routes if isinstance(route, Route)]
# Deprecate these in 21.6
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", {})
@@ -284,3 +446,46 @@ class Blueprint(BaseSanic):
return_when=asyncio.FIRST_COMPLETED,
timeout=timeout,
)
@staticmethod
def _extract_value(*values):
value = values[-1]
for v in values:
if v is not None:
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))
def pre_register(
self,
name: Union[str, Default],
url_prefix: Optional[str] = None,
host: Optional[Union[List[str], str]] = None,
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: Union[str, Default] = _default,
) -> None:
if not hasattr(self.ctx, "_prereg"):
self.ctx._prereg = []
self.ctx._prereg.append(name)
self.__class__.__pre_registry__[name].append(
{
k: v
for k, v in {
"bp": self,
"url_prefix": url_prefix,
"host": host,
"version": version,
"strict_slashes": strict_slashes,
"version_prefix": version_prefix,
}.items()
if v is not None and v is not _default
}
)

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

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

@@ -0,0 +1,202 @@
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.blueprints import Blueprint
from sanic.cli.arguments import Group
from sanic.helpers import _default
from sanic.log import error_logger
from sanic.simple import create_simple_server
class SanicArgumentParser(ArgumentParser):
...
class SanicCLI:
DEFAULT_APP_NAME = "SANIC"
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 isinstance(app, Blueprint):
bp = app
name = (
bp.ctx._prereg[0]
if hasattr(bp.ctx, "_prereg")
else _default
)
if name is _default:
name = self.DEFAULT_APP_NAME
app = Sanic(name)
elif 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

@@ -10,6 +10,13 @@ from multidict import CIMultiDict # type: ignore
OS_IS_WINDOWS = os.name == "nt"
def enable_windows_color_support():
import ctypes
kernel = ctypes.windll.kernel32
kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7)
class Header(CIMultiDict):
"""
Container used for both request and response headers. It is a subclass of

View File

@@ -1,58 +1,114 @@
from __future__ import annotations
from inspect import isclass
from os import environ
from pathlib import Path
from typing import Any, Union
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from warnings import warn
from .utils import load_module_from_file_location, str_to_bool
from sanic.errorpages import check_error_format
from sanic.http import Http
from sanic.utils import load_module_from_file_location, str_to_bool
if TYPE_CHECKING: # no cov
from sanic import Sanic
SANIC_PREFIX = "SANIC_"
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_BUFFER_QUEUE_SIZE": 100,
"ACCESS_LOG": True,
"AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False,
"FALLBACK_ERROR_FORMAT": "auto",
"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,
"REQUEST_BUFFER_SIZE": 65536, # 64 KiB
"REQUEST_MAX_HEADER_SIZE": 8192, # 8 KiB, but cannot exceed 16384
"REQUEST_ID_HEADER": "X-Request-ID",
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"KEEP_ALIVE": True,
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_MAX_QUEUE": 32,
"WEBSOCKET_READ_LIMIT": 2 ** 16,
"WEBSOCKET_WRITE_LIMIT": 2 ** 16,
"WEBSOCKET_PING_TIMEOUT": 20,
"WEBSOCKET_PING_INTERVAL": 20,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"ACCESS_LOG": True,
"FORWARDED_SECRET": None,
"REAL_IP_HEADER": None,
"PROXIES_COUNT": None,
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"REQUEST_ID_HEADER": "X-Request-ID",
"FALLBACK_ERROR_FORMAT": "html",
"REGISTER": True,
"WEBSOCKET_PING_TIMEOUT": 20,
}
class Config(dict):
def __init__(self, defaults=None, load_env=True, keep_alive=None):
ACCESS_LOG: 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
REQUEST_BUFFER_SIZE: int
REQUEST_MAX_HEADER_SIZE: int
REQUEST_ID_HEADER: str
REQUEST_MAX_SIZE: int
REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int
SERVER_NAME: str
WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int
WEBSOCKET_PING_TIMEOUT: 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,
*,
app: Optional[Sanic] = None,
):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
self.LOGO = BASE_LOGO
self._app = app
self._LOGO = ""
if keep_alive is not None:
self.KEEP_ALIVE = keep_alive
if load_env:
prefix = SANIC_PREFIX if load_env is True else load_env
self.load_environment_vars(prefix=prefix)
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:
@@ -60,16 +116,69 @@ class Config(dict):
except KeyError as ke:
raise AttributeError(f"Config has no '{ke.args[0]}'")
def __setattr__(self, attr, value):
self[attr] = value
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:
other_mapping = {k: v for item in other for k, v in dict(item).items()}
super().update(*other, **kwargs)
for attr, value in {**other_mapping, **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 == "FALLBACK_ERROR_FORMAT":
self._check_error_format()
if self.app and value != self.app.error_handler.fallback:
if self.app.error_handler.fallback != "auto":
warn(
"Overriding non-default ErrorHandler fallback "
"value. Changing from "
f"{self.app.error_handler.fallback} to {value}."
)
self.app.error_handler.fallback = value
elif attr == "LOGO":
self._LOGO = value
warn(
"Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.",
DeprecationWarning,
)
@property
def app(self):
return self._app
@property
def LOGO(self):
return self._LOGO
def _configure_header_size(self):
Http.set_header_max_size(
self.REQUEST_MAX_HEADER_SIZE,
self.REQUEST_BUFFER_SIZE - 4096,
self.REQUEST_MAX_SIZE,
)
def _check_error_format(self):
check_error_format(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``
@@ -77,19 +186,18 @@ class Config(dict):
Anything else will be imported as a ``str``.
"""
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 (int, float, str_to_bool, str):
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]):
"""

View File

@@ -1,2 +1,28 @@
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")
from enum import Enum, auto
class HTTPMethod(str, Enum):
def _generate_next_value_(name, start, count, last_values):
return name.upper()
def __eq__(self, value: object) -> bool:
value = str(value).upper()
return super().__eq__(value)
def __hash__(self) -> int:
return hash(self.value)
def __str__(self) -> str:
return self.value
GET = auto()
POST = auto()
PUT = auto()
HEAD = auto()
OPTIONS = auto()
PATCH = auto()
DELETE = auto()
HTTP_METHODS = tuple(HTTPMethod.__members__.values())
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"

View File

@@ -25,12 +25,13 @@ 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
FALLBACK_TEXT = (
@@ -45,6 +46,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 +115,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 +143,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 +162,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 +173,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 +187,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 +256,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 +267,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 +277,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 +314,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 +329,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 +342,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 +394,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.get(
"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,28 +1,28 @@
from typing import Optional, Union
from typing import Any, Dict, Optional, Union
from sanic.helpers import STATUS_CODES
_sanic_exceptions = {}
def add_status_code(code, quiet=None):
"""
Decorator used for adding exceptions to :class:`SanicException`.
"""
def class_decorator(cls):
cls.status_code = code
if quiet or quiet is None and code != 500:
cls.quiet = True
_sanic_exceptions[code] = cls
return cls
return class_decorator
class SanicException(Exception):
def __init__(self, message, status_code=None, quiet=None):
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:
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)
if status_code is not None:
@@ -33,45 +33,45 @@ class SanicException(Exception):
self.quiet = True
@add_status_code(404)
class NotFound(SanicException):
"""
**Status**: 404 Not Found
"""
pass
status_code = 404
quiet = True
@add_status_code(400)
class InvalidUsage(SanicException):
"""
**Status**: 400 Bad Request
"""
pass
status_code = 400
quiet = True
@add_status_code(405)
class MethodNotSupported(SanicException):
"""
**Status**: 405 Method Not Allowed
"""
status_code = 405
quiet = True
def __init__(self, message, method, allowed_methods):
super().__init__(message)
self.headers = {"Allow": ", ".join(allowed_methods)}
@add_status_code(500)
class ServerError(SanicException):
"""
**Status**: 500 Internal Server Error
"""
pass
status_code = 500
@add_status_code(503)
class ServiceUnavailable(SanicException):
"""
**Status**: 503 Service Unavailable
@@ -80,7 +80,8 @@ class ServiceUnavailable(SanicException):
down for maintenance). Generally, this is a temporary state.
"""
pass
status_code = 503
quiet = True
class URLBuildError(ServerError):
@@ -88,7 +89,7 @@ class URLBuildError(ServerError):
**Status**: 500 Internal Server Error
"""
pass
status_code = 500
class FileNotFound(NotFound):
@@ -102,7 +103,6 @@ class FileNotFound(NotFound):
self.relative_url = relative_url
@add_status_code(408)
class RequestTimeout(SanicException):
"""The Web server (running the Web site) thinks that there has been too
long an interval of time between 1) the establishment of an IP
@@ -112,16 +112,17 @@ class RequestTimeout(SanicException):
server has 'timed out' on that particular socket connection.
"""
pass
status_code = 408
quiet = True
@add_status_code(413)
class PayloadTooLarge(SanicException):
"""
**Status**: 413 Payload Too Large
"""
pass
status_code = 413
quiet = True
class HeaderNotFound(InvalidUsage):
@@ -129,36 +130,42 @@ class HeaderNotFound(InvalidUsage):
**Status**: 400 Bad Request
"""
pass
class InvalidHeader(InvalidUsage):
"""
**Status**: 400 Bad Request
"""
@add_status_code(416)
class ContentRangeError(SanicException):
"""
**Status**: 416 Range Not Satisfiable
"""
status_code = 416
quiet = True
def __init__(self, message, content_range):
super().__init__(message)
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
@add_status_code(417)
class HeaderExpectationFailed(SanicException):
"""
**Status**: 417 Expectation Failed
"""
pass
status_code = 417
quiet = True
@add_status_code(403)
class Forbidden(SanicException):
"""
**Status**: 403 Forbidden
"""
pass
status_code = 403
quiet = True
class InvalidRangeType(ContentRangeError):
@@ -166,7 +173,8 @@ class InvalidRangeType(ContentRangeError):
**Status**: 416 Range Not Satisfiable
"""
pass
status_code = 416
quiet = True
class PyFileError(Exception):
@@ -174,7 +182,6 @@ class PyFileError(Exception):
super().__init__("could not execute config file %s", file)
@add_status_code(401)
class Unauthorized(SanicException):
"""
**Status**: 401 Unauthorized
@@ -210,6 +217,9 @@ class Unauthorized(SanicException):
realm="Restricted Area")
"""
status_code = 401
quiet = True
def __init__(self, message, status_code=None, scheme=None, **kwargs):
super().__init__(message, status_code)
@@ -231,6 +241,11 @@ class InvalidSignal(SanicException):
pass
class WebsocketClosed(SanicException):
quiet = True
message = "Client has closed the websocket connection"
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
"""
Raise an exception based on SanicException. Returns the HTTP response
@@ -241,9 +256,13 @@ def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
:param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages in
"""
if message is None:
msg: bytes = STATUS_CODES[status_code]
# These are stored as bytes in the STATUS_CODES dict
message = msg.decode("utf8")
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
raise sanic_exception(message=message, status_code=status_code)
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)

View File

@@ -1,12 +1,15 @@
from traceback import format_exc
from inspect import signature
from typing import Dict, List, Optional, Tuple, Type
from warnings import warn
from sanic.errorpages import exception_response
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
from sanic.exceptions import (
ContentRangeError,
HeaderNotFound,
InvalidRangeType,
)
from sanic.log import logger
from sanic.log import error_logger
from sanic.models.handler_types import RouteHandler
from sanic.response import text
@@ -23,16 +26,53 @@ class ErrorHandler:
"""
handlers = None
cached_handlers = None
_missing = object()
def __init__(self):
self.handlers = []
self.cached_handlers = {}
# Beginning in v22.3, the base renderer will be TextRenderer
def __init__(
self, fallback: str = "auto", 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):
@classmethod
def finalize(cls, error_handler, fallback: Optional[str] = None):
if (
fallback
and fallback != "auto"
and error_handler.fallback == "auto"
):
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:
warn(
"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.",
DeprecationWarning,
)
error_handler._lookup = error_handler._legacy_lookup
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.
@@ -45,9 +85,16 @@ class ErrorHandler:
:return: None
"""
# self.handlers is deprecated and will be removed in version 22.3
self.handlers.append((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.
@@ -61,16 +108,32 @@ class ErrorHandler:
:return: Registered function if found ``None`` otherwise
"""
handler = self.cached_handlers.get(type(exception), self._missing)
if handler is self._missing:
for exception_class, handler in self.handlers:
if isinstance(exception, exception_class):
self.cached_handlers[type(exception)] = handler
exception_class = type(exception)
for name in (route_name, None):
exception_key = (exception_class, name)
handler = self.cached_handlers.get(exception_key)
if handler:
return handler
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
self.cached_handlers[type(exception)] = None
handler = None
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
@@ -85,7 +148,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:
@@ -93,7 +157,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:
@@ -101,7 +164,7 @@ class ErrorHandler:
response_message = (
"Exception raised in exception handler " '"%s" for uri: %s'
)
logger.exception(response_message, handler.__name__, url)
error_logger.exception(response_message, handler.__name__, url)
if self.debug:
return text(response_message % (handler.__name__, url), 500)
@@ -109,11 +172,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`.
@@ -129,17 +187,28 @@ class ErrorHandler:
:class:`Exception`
:return:
"""
self.log(request, exception)
return exception_response(
request,
exception,
debug=self.debug,
base=self.base,
fallback=self.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())
logger.exception("Exception occurred while handling uri: %s", url)
return exception_response(request, exception, self.debug)
error_logger.exception(
"Exception occurred while handling uri: %s", url
)
class ContentRangeHandler:
@@ -165,7 +234,7 @@ class ContentRangeHandler:
def __init__(self, request, stats):
self.total = stats.st_size
_range = request.headers.get("Range")
_range = request.headers.getone("range", None)
if _range is None:
raise HeaderNotFound("Range Header Not Found")
unit, _, value = tuple(map(str.strip, _range.partition("=")))

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.
@@ -102,7 +274,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
"""Parse traditional proxy headers."""
real_ip_header = config.REAL_IP_HEADER
proxies_count = config.PROXIES_COUNT
addr = real_ip_header and headers.get(real_ip_header)
addr = real_ip_header and headers.getone(real_ip_header, None)
if not addr and proxies_count:
assert proxies_count > 0
try:
@@ -131,7 +303,7 @@ def parse_xforwarded(headers, config) -> Optional[Options]:
("port", "x-forwarded-port"),
("path", "x-forwarded-path"),
):
yield key, headers.get(header)
yield key, headers.getone(header, None)
return fwd_normalize(options())
@@ -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

@@ -20,7 +20,8 @@ from sanic.exceptions import (
)
from sanic.headers import format_http1_response
from sanic.helpers import has_message_body
from sanic.log import access_logger, logger
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
@@ -64,9 +65,18 @@ class Http:
:raises RuntimeError:
"""
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",
@@ -82,6 +92,7 @@ class Http:
"request_max_size",
"response",
"response_func",
"response_size",
"response_bytes_left",
"upgrade_websocket",
]
@@ -91,19 +102,23 @@ class Http:
self._receive_more = protocol.receive_more
self.recv_buffer = protocol.recv_buffer
self.protocol = protocol
self.expecting_continue: bool = False
self.keep_alive = True
self.stage: Stage = Stage.IDLE
self.dispatch = self.protocol.app.dispatch
def init_for_request(self):
"""Init/reset all per-request variables."""
self.exception = None
self.expecting_continue: bool = False
self.head_only = None
self.request_body = None
self.request_bytes = None
self.request_bytes_left = None
self.request_max_size = protocol.request_max_size
self.keep_alive = True
self.head_only = None
self.request_max_size = self.protocol.request_max_size
self.request: Request = None
self.response: BaseHTTPResponse = None
self.exception = None
self.url = None
self.upgrade_websocket = False
self.url = None
def __bool__(self):
"""Test if request handling is in progress"""
@@ -113,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)
@@ -132,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
@@ -143,8 +170,11 @@ class Http:
# Try to consume any remaining request body
if self.request_body:
if self.response and 200 <= self.response.status < 300:
logger.error(f"{self.request} body not consumed.")
error_logger.error(f"{self.request} body not consumed.")
# Limit the size because the handler may have set it infinite
self.request_max_size = min(
self.request_max_size, self.protocol.request_max_size
)
try:
async for _ in self:
pass
@@ -156,19 +186,16 @@ class Http:
await sleep(0.001)
self.keep_alive = False
# Exit and disconnect if no more requests can be taken
if self.stage is not Stage.IDLE or not self.keep_alive:
break
# Clean up to free memory and for the next request
if self.request:
self.request.stream = None
if self.response:
self.response.stream = None
# Wait for 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.
"""
HEADER_MAX_SIZE = min(8192, self.request_max_size)
# Receive until full header is in buffer
buf = self.recv_buffer
pos = 0
@@ -179,12 +206,12 @@ class Http:
break
pos = max(0, len(buf) - 3)
if pos >= HEADER_MAX_SIZE:
if pos >= self.HEADER_MAX_SIZE:
break
await self._receive_more()
if pos >= HEADER_MAX_SIZE:
if pos >= self.HEADER_MAX_SIZE:
raise PayloadTooLarge("Request header exceeds the size limit")
# Parse header content
@@ -194,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":
@@ -218,7 +251,9 @@ class Http:
raise InvalidUsage("Bad Request")
headers_instance = Header(headers)
self.upgrade_websocket = headers_instance.get("upgrade") == "websocket"
self.upgrade_websocket = (
headers_instance.getone("upgrade", "").lower() == "websocket"
)
# Prepare a Request object
request = self.protocol.request_class(
@@ -230,12 +265,17 @@ 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
if request_body:
headers = request.headers
expect = headers.get("expect")
expect = headers.getone("expect", None)
if expect is not None:
if expect.lower() == "100-continue":
@@ -243,7 +283,7 @@ class Http:
else:
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
if headers.get("transfer-encoding") == "chunked":
if headers.getone("transfer-encoding", None) == "chunked":
self.request_body = "chunked"
pos -= 2 # One CRLF stays in buffer
else:
@@ -254,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
@@ -270,6 +309,7 @@ class Http:
size = len(data)
headers = res.headers
status = res.status
self.response_size = size
if not isinstance(status, int) or status < 200:
raise RuntimeError(f"Invalid response status {status!r}")
@@ -424,13 +464,15 @@ class Http:
req, res = self.request, self.response
extra = {
"status": getattr(res, "status", 0),
"byte": getattr(self, "response_bytes_left", -1),
"byte": getattr(
self, "response_bytes_left", getattr(self, "response_size", -1)
),
"host": "UNKNOWN",
"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)
@@ -446,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.
"""
@@ -478,8 +520,6 @@ class Http:
self.keep_alive = False
raise InvalidUsage("Bad chunked encoding")
del buf[: pos + 2]
if size <= 0:
self.request_body = None
@@ -487,8 +527,17 @@ class Http:
self.keep_alive = False
raise InvalidUsage("Bad chunked encoding")
# Consume CRLF, chunk size 0 and the two CRLF that follow
pos += 4
# Might need to wait for the final CRLF
while len(buf) < pos:
await self._receive_more()
del buf[:pos]
return None
# Remove CRLF, chunk size and the CRLF that follows
del buf[: pos + 2]
self.request_bytes_left = size
self.request_bytes += size
@@ -513,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
@@ -529,9 +584,21 @@ 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
@property
def send(self):
return self.response_func
@classmethod
def set_header_max_size(cls, *sizes: int):
cls.HEADER_MAX_SIZE = min(
*sizes,
cls.HEADER_CEILING,
)

View File

@@ -1,8 +1,11 @@
import logging
import sys
from enum import Enum
from typing import Any, Dict
LOGGING_CONFIG_DEFAULTS = dict(
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
version=1,
disable_existing_loggers=False,
loggers={
@@ -53,6 +56,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

View File

@@ -1,18 +1,19 @@
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.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()
@@ -26,16 +27,14 @@ 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.
To be used as a deocrator:
To be used as a decorator:
.. code-block:: python
@@ -63,20 +62,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,17 +1,20 @@
from ast import NodeVisitor, Return, parse
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 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.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.errorpages import RESPONSE_MAPPING
from sanic.exceptions import (
ContentRangeError,
FileNotFound,
@@ -21,15 +24,22 @@ from sanic.exceptions import (
from sanic.handlers import ContentRangeHandler
from sanic.log import 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
RouteWrapper = Callable[
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
]
class RouteMixin:
name: str
def __init__(self, *args, **kwargs) -> None:
self._future_routes: Set[FutureRoute] = set()
self._future_statics: Set[FutureStatic] = set()
self.name = ""
self.strict_slashes: Optional[bool] = False
def _apply_route(self, route: FutureRoute) -> List[Route]:
@@ -42,10 +52,10 @@ 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[int] = None,
version: Optional[Union[int, str, float]] = None,
name: Optional[str] = None,
ignore_body: bool = False,
apply: bool = True,
@@ -53,7 +63,9 @@ class RouteMixin:
websocket: bool = False,
unquote: bool = False,
static: bool = False,
):
version_prefix: str = "/v",
error_format: Optional[str] = None,
) -> RouteWrapper:
"""
Decorate a function to be registered as a route
@@ -66,12 +78,14 @@ class RouteMixin:
:param name: user defined route name for url_for
:param ignore_body: whether the handler should ignore request
body (eg. GET requests)
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: tuple of routes, decorated function
"""
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
if not uri.startswith("/"):
if not uri.startswith("/") and (uri or hasattr(self, "router")):
uri = "/" + uri
if strict_slashes is None:
@@ -92,6 +106,8 @@ class RouteMixin:
nonlocal subprotocols
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
@@ -110,10 +126,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,
@@ -128,6 +150,8 @@ class RouteMixin:
subprotocols,
unquote,
static,
version_prefix,
error_format,
)
self._future_routes.add(route)
@@ -154,21 +178,25 @@ class RouteMixin:
if apply:
self._apply_route(route)
return route, handler
if static:
return route, handler
return handler
return decorator
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,
) -> RouteHandler:
"""A helper method to register class instance or
functions as a handler to the application url
routes.
@@ -182,6 +210,8 @@ class RouteMixin:
:param version:
:param name: user defined route name for url_for
: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``
:return: function or class instance
"""
# Handle HTTPMethodView differently
@@ -189,7 +219,8 @@ 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"):
@@ -214,6 +245,8 @@ class RouteMixin:
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
)(handler)
return handler
@@ -221,12 +254,14 @@ 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,
) -> RouteWrapper:
"""
Add an API URL under the **GET** *HTTP* method
@@ -236,6 +271,8 @@ class RouteMixin:
URLs need to terminate with a */*
:param version: API Version
: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``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -246,17 +283,21 @@ class RouteMixin:
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
)
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,
) -> RouteWrapper:
"""
Add an API URL under the **POST** *HTTP* method
@@ -266,6 +307,8 @@ class RouteMixin:
URLs need to terminate with a */*
:param version: API Version
: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``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -276,17 +319,21 @@ class RouteMixin:
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
)
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,
) -> RouteWrapper:
"""
Add an API URL under the **PUT** *HTTP* method
@@ -296,6 +343,8 @@ class RouteMixin:
URLs need to terminate with a */*
:param version: API Version
: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``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -306,17 +355,21 @@ class RouteMixin:
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
)
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,
) -> RouteWrapper:
"""
Add an API URL under the **HEAD** *HTTP* method
@@ -334,6 +387,8 @@ class RouteMixin:
:param ignore_body: whether the handler should ignore request
body (eg. GET requests), defaults to True
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -344,17 +399,21 @@ class RouteMixin:
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
)
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,
) -> RouteWrapper:
"""
Add an API URL under the **OPTIONS** *HTTP* method
@@ -372,6 +431,8 @@ class RouteMixin:
:param ignore_body: whether the handler should ignore request
body (eg. GET requests), defaults to True
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -382,17 +443,21 @@ class RouteMixin:
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
)
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,
) -> RouteWrapper:
"""
Add an API URL under the **PATCH** *HTTP* method
@@ -412,6 +477,8 @@ class RouteMixin:
:param ignore_body: whether the handler should ignore request
body (eg. GET requests), defaults to True
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -422,17 +489,21 @@ class RouteMixin:
stream=stream,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
)
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,
) -> RouteWrapper:
"""
Add an API URL under the **DELETE** *HTTP* method
@@ -442,6 +513,8 @@ class RouteMixin:
URLs need to terminate with a */*
:param version: API Version
: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``
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -452,17 +525,21 @@ class RouteMixin:
version=version,
name=name,
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
)
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,
):
"""
Decorate a function to be registered as a websocket route
@@ -474,6 +551,8 @@ class RouteMixin:
:param subprotocols: optional list of str with supported subprotocols
:param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: tuple of routes, decorated function
"""
return self.route(
@@ -486,17 +565,21 @@ class RouteMixin:
apply=apply,
subprotocols=subprotocols,
websocket=True,
version_prefix=version_prefix,
error_format=error_format,
)
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,
):
"""
A helper method to register a function as a websocket route.
@@ -513,6 +596,8 @@ class RouteMixin:
handshake
:param name: A unique name assigned to the URL so that it can
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:return: Objected decorated by :func:`websocket`
"""
return self.websocket(
@@ -522,6 +607,8 @@ class RouteMixin:
subprotocols=subprotocols,
version=version,
name=name,
version_prefix=version_prefix,
error_format=error_format,
)(handler)
def static(
@@ -537,6 +624,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
@@ -586,6 +674,7 @@ class RouteMixin:
host,
strict_slashes,
content_type,
resource_type,
)
self._future_statics.add(static)
@@ -665,7 +754,10 @@ class RouteMixin:
modified_since = strftime(
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
)
if request.headers.get("If-Modified-Since") == modified_since:
if (
request.headers.getone("if-modified-since", None)
== modified_since
):
return HTTPResponse(status=304)
headers["Last-Modified"] = modified_since
_range = None
@@ -718,16 +810,19 @@ class RouteMixin:
return await file(file_path, headers=headers, _range=_range)
except ContentRangeError:
raise
except Exception:
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
except FileNotFoundError:
raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
except Exception:
error_logger.exception(
f"Exception in static request handler: "
f"path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise
def _register_static(
self,
@@ -775,8 +870,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):
uri += "/<__file_uri__>"
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_"):
@@ -793,7 +907,7 @@ class RouteMixin:
)
)
route, _ = self.route(
route, _ = self.route( # type: ignore
uri=uri,
methods=["GET", "HEAD"],
name=name,
@@ -803,3 +917,43 @@ class RouteMixin:
)(_handler)
return route
def _determine_error_format(self, handler) -> Optional[str]:
if not isinstance(handler, CompositionView):
try:
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))
except (OSError, TypeError):
...
return None
def _get_response_types(self, node):
types = set()
class HttpResponseVisitor(NodeVisitor):
def visit_Return(self, node: Return) -> Any:
nonlocal types
try:
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])
except AttributeError:
...
HttpResponseVisitor().visit(node)
return types

View File

@@ -1,4 +1,5 @@
from typing import Any, Callable, Dict, Set
from enum import Enum
from typing import Any, Callable, Dict, Optional, Set, Union
from sanic.models.futures import FutureSignal
from sanic.models.handler_types import SignalHandler
@@ -19,11 +20,11 @@ class SignalMixin:
def signal(
self,
event: str,
event: Union[str, Enum],
*,
apply: bool = True,
condition: Dict[str, Any] = None,
) -> Callable[[SignalHandler], FutureSignal]:
) -> Callable[[SignalHandler], SignalHandler]:
"""
For creating a signal handler, used similar to a route handler:
@@ -41,29 +42,33 @@ class SignalMixin:
filtering, defaults to None
: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 {})
)
self._future_signals.add(future_signal)
if apply:
self._apply_signal(future_signal)
return future_signal
return handler
return decorator
def add_signal(
self,
handler,
handler: Optional[Callable[..., Any]],
event: str,
condition: Dict[str, Any] = None,
):
if not handler:
async def noop():
...
handler = noop
self.signal(event=event, condition=condition)(handler)
return handler

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

@@ -13,7 +13,7 @@ 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]
@@ -23,6 +23,8 @@ class FutureRoute(NamedTuple):
subprotocols: Optional[List[str]]
unquote: bool
static: bool
version_prefix: str
error_format: Optional[str]
class FutureListener(NamedTuple):
@@ -51,9 +53,14 @@ 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]]
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

@@ -1,3 +1,4 @@
import itertools
import os
import signal
import subprocess
@@ -46,17 +47,37 @@ 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,
},
)
def watchdog(sleep_interval):
def _check_file(filename, mtimes):
need_reload = False
mtime = os.stat(filename).st_mtime
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
elif mtime > old_time:
mtimes[filename] = mtime
need_reload = True
return need_reload
def watchdog(sleep_interval, app):
"""Watch project files, restart worker process if a change happened.
:param sleep_interval: interval in second.
@@ -75,25 +96,27 @@ def watchdog(sleep_interval):
try:
while True:
need_reload = False
for filename in _iter_module_files():
changed = set()
for filename in itertools.chain(
_iter_module_files(),
*(d.glob("**/*") for d in app.reload_dirs),
):
try:
mtime = os.stat(filename).st_mtime
if _check_file(filename, mtimes):
path = (
filename
if isinstance(filename, str)
else filename.resolve()
)
changed.add(str(path))
except OSError:
continue
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
elif mtime > old_time:
mtimes[filename] = mtime
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

@@ -18,7 +18,6 @@ from sanic_routing.route import Route # type: ignore
if TYPE_CHECKING:
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",
@@ -125,7 +129,7 @@ class Request:
self._name: Optional[str] = None
self.app = app
self.headers = headers
self.headers = Header(headers)
self.version = version
self.method = method
self.transport = transport
@@ -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):
@@ -262,7 +290,7 @@ class Request:
app = Sanic("MyApp", request_class=IntRequest)
"""
if not self._id:
self._id = self.headers.get(
self._id = self.headers.getone(
self.app.config.REQUEST_ID_HEADER,
self.__class__.generate_id(self), # type: ignore
)
@@ -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.
@@ -303,7 +338,7 @@ class Request:
:return: token related to request
"""
prefixes = ("Bearer", "Token")
auth_header = self.headers.get("Authorization")
auth_header = self.headers.getone("authorization", None)
if auth_header is not None:
for prefix in prefixes:
@@ -317,8 +352,8 @@ class Request:
if self.parsed_form is None:
self.parsed_form = RequestParameters()
self.parsed_files = RequestParameters()
content_type = self.headers.get(
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
content_type = self.headers.getone(
"content-type", DEFAULT_HTTP_CONTENT_TYPE
)
content_type, parameters = parse_content_header(content_type)
try:
@@ -378,9 +413,12 @@ class Request:
:type errors: str
:return: RequestParameters
"""
if not self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
]:
if (
keep_blank_values,
strict_parsing,
encoding,
errors,
) not in self.parsed_args:
if self.query_string:
self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors)
@@ -434,9 +472,12 @@ class Request:
:type errors: str
:return: list
"""
if not self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
]:
if (
keep_blank_values,
strict_parsing,
encoding,
errors,
) not in self.parsed_not_grouped_args:
if self.query_string:
self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors)
@@ -465,7 +506,7 @@ class Request:
"""
if self._cookies is None:
cookie = self.headers.get("Cookie")
cookie = self.headers.getone("cookie", None)
if cookie is not None:
cookies: SimpleCookie = SimpleCookie()
cookies.load(cookie)
@@ -482,7 +523,7 @@ class Request:
:return: Content-Type header form the request
:rtype: str
"""
return self.headers.get("Content-Type", DEFAULT_HTTP_CONTENT_TYPE)
return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE)
@property
def match_info(self):
@@ -491,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
@@ -499,7 +544,7 @@ class Request:
:return: peer ip of the socket
:rtype: str
"""
return self.conn_info.client if self.conn_info else ""
return self.conn_info.client_ip if self.conn_info else ""
@property
def port(self) -> int:
@@ -581,7 +626,7 @@ class Request:
if (
self.app.websocket_enabled
and self.headers.get("upgrade") == "websocket"
and self.headers.getone("upgrade", "").lower() == "websocket"
):
scheme = "ws"
else:
@@ -608,7 +653,9 @@ class Request:
server_name = self.app.config.get("SERVER_NAME")
if server_name:
return server_name.split("//", 1)[-1].split("/", 1)[0]
return str(self.forwarded.get("host") or self.headers.get("host", ""))
return str(
self.forwarded.get("host") or self.headers.getone("host", "")
)
@property
def server_name(self) -> str:
@@ -737,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

@@ -3,6 +3,7 @@ from mimetypes import guess_type
from os import path
from pathlib import PurePath
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Callable,
@@ -19,11 +20,15 @@ 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
try:
from ujson import dumps as json_dumps
except ImportError:
@@ -45,7 +50,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 +106,7 @@ class BaseHTTPResponse:
async def send(
self,
data: Optional[Union[AnyStr]] = None,
data: Optional[AnyStr] = None,
end_stream: Optional[bool] = None,
) -> None:
"""
@@ -112,8 +117,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")
@@ -143,7 +157,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
.. warning::
**Deprecated** and set for removal in v21.6. You can now achieve the
**Deprecated** and set for removal in v21.12. You can now achieve the
same functionality without a callback.
.. code-block:: python
@@ -174,12 +188,16 @@ class StreamingHTTPResponse(BaseHTTPResponse):
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: str = "text/plain; charset=utf-8",
chunked="deprecated",
ignore_deprecation_notice: bool = False,
):
if chunked != "deprecated":
if not ignore_deprecation_notice:
warn(
"The chunked argument has been deprecated and will be "
"removed in v21.6"
"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__()
@@ -203,6 +221,9 @@ class StreamingHTTPResponse(BaseHTTPResponse):
self.streaming_fn = None
await super().send(*args, **kwargs)
async def eof(self):
raise NotImplementedError
class HTTPResponse(BaseHTTPResponse):
"""
@@ -235,6 +256,15 @@ class HTTPResponse(BaseHTTPResponse):
self.headers = Header(headers or {})
self._cookies = None
async def eof(self):
await self.send("", True)
async def __aenter__(self):
return self.send
async def __aexit__(self, *_):
await self.eof()
def empty(
status=204, headers: Optional[Dict[str, str]] = None
@@ -396,7 +426,6 @@ async def file_stream(
mime_type: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
filename: Optional[str] = None,
chunked="deprecated",
_range: Optional[Range] = None,
) -> StreamingHTTPResponse:
"""Return a streaming response object with file data.
@@ -409,12 +438,6 @@ async def file_stream(
:param chunked: Deprecated
:param _range:
"""
if chunked != "deprecated":
warn(
"The chunked argument has been deprecated and will be "
"removed in v21.6"
)
headers = headers or {}
if filename:
headers.setdefault(
@@ -453,6 +476,7 @@ async def file_stream(
status=status,
headers=headers,
content_type=mime_type,
ignore_deprecation_notice=True,
)
@@ -461,7 +485,6 @@ def stream(
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
chunked="deprecated",
):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
@@ -482,17 +505,12 @@ def stream(
:param headers: Custom Headers.
:param chunked: Deprecated
"""
if chunked != "deprecated":
warn(
"The chunked argument has been deprecated and will be "
"removed in v21.6"
)
return StreamingHTTPResponse(
streaming_fn,
headers=headers,
content_type=content_type,
status=status,
ignore_deprecation_notice=True,
)

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