diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5f09fd51ce..d8a76c6cc0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,12 +1,12 @@ .. note:: - From v21.9, CHANGELOG files are maintained in ``./docs/sanic/releases`` + CHANGELOG files are maintained in ``./docs/sanic/releases``. To view the full CHANGELOG, please visit https://sanic.readthedocs.io/en/stable/sanic/changelog.html. + Version 21.6.1 -------------- -Bugfixes -******** +**Bugfixes** * `#2178 `_ Update sanic-routing to allow for better splitting of complex URI templates @@ -20,8 +20,7 @@ Bugfixes Version 21.6.0 -------------- -Features -******** +**Features** * `#2094 `_ Add ``response.eof()`` method for closing a stream in a handler @@ -68,8 +67,7 @@ Features * `#2170 `_ Additional methods for attaching ``HTTPMethodView`` -Bugfixes -******** +**Bugfixes** * `#2091 `_ Fix ``UserWarning`` in ASGI mode for missing ``__slots__`` @@ -85,8 +83,7 @@ Bugfixes Fix issue where Blueprint exception handlers do not consistently route to proper handler -Deprecations and Removals -************************* +**Deprecations and Removals** * `#2156 `_ Remove config value ``REQUEST_BUFFER_QUEUE_SIZE`` @@ -95,14 +92,12 @@ Deprecations and Removals * `#2172 `_ Deprecate StreamingHTTPResponse -Developer infrastructure -************************ +**Developer infrastructure** * `#2149 `_ Remove Travis CI in favor of GitHub Actions -Improved Documentation -********************** +**Improved Documentation** * `#2164 `_ Fix typo in documentation @@ -112,8 +107,7 @@ Improved Documentation Version 21.3.2 -------------- -Bugfixes -******** +**Bugfixes** * `#2081 `_ Disable response timeout on websocket connections @@ -124,8 +118,7 @@ Bugfixes Version 21.3.1 -------------- -Bugfixes -******** +**Bugfixes** * `#2076 `_ Static files inside subfolders are not accessible (404) @@ -135,8 +128,7 @@ Version 21.3.0 `Release Notes `_ -Features -******** +**Features** * `#1876 `_ @@ -189,8 +181,7 @@ Features `#2063 `_ App and connection level context objects -Bugfixes and issues resolved -**************************** +**Bugfixes** * Resolve `#1420 `_ ``url_for`` where ``strict_slashes`` are on for a path ending in ``/`` @@ -220,8 +211,7 @@ Bugfixes and issues resolved `#2001 `_ Raise ValueError when cookie max-age is not an integer -Deprecations and Removals -************************* +**Deprecations and Removals** * `#2007 `_ @@ -240,8 +230,7 @@ Deprecations and Removals * ``Request.endpoint`` deprecated in favor of ``Request.name`` * handler type name prefixes removed (static, websocket, etc) -Developer infrastructure -************************ +**Developer infrastructure** * `#1995 `_ @@ -259,8 +248,7 @@ Developer infrastructure `#2049 `_ Updated setup.py to use ``find_packages`` -Improved Documentation -********************** +**Improved Documentation** * `#1218 `_ @@ -282,8 +270,7 @@ Improved Documentation `#2052 `_ Fix some examples and docs -Miscellaneous -************* +**Miscellaneous** * ``Request.route`` property * Better websocket subprotocols support @@ -329,8 +316,7 @@ Miscellaneous Version 20.12.3 --------------- -Bugfixes -******** +**Bugfixes** * `#2021 `_ @@ -339,8 +325,7 @@ Bugfixes Version 20.12.2 --------------- -Dependencies -************ +**Dependencies** * `#2026 `_ @@ -353,8 +338,7 @@ Dependencies Version 19.12.5 --------------- -Dependencies -************ +**Dependencies** * `#2025 `_ @@ -367,8 +351,7 @@ Dependencies Version 20.12.0 --------------- -Features -******** +**Features** * `#1993 `_ @@ -377,8 +360,7 @@ Features Version 20.12.0 --------------- -Features -******** +**Features** * `#1945 `_ @@ -416,22 +398,19 @@ Features `#1979 `_ Add app registry and Sanic class level app retrieval -Bugfixes -******** +**Bugfixes** * `#1965 `_ Fix Chunked Transport-Encoding in ASGI streaming response -Deprecations and Removals -************************* +**Deprecations and Removals** * `#1981 `_ Cleanup and remove deprecated code -Developer infrastructure -************************ +**Developer infrastructure** * `#1956 `_ @@ -445,8 +424,7 @@ Developer infrastructure `#1986 `_ Update tox requirements -Improved Documentation -********************** +**Improved Documentation** * `#1951 `_ @@ -464,8 +442,7 @@ Improved Documentation Version 20.9.1 --------------- -Bugfixes -******** +**Bugfixes** * `#1954 `_ @@ -478,8 +455,7 @@ Bugfixes Version 19.12.3 --------------- -Bugfixes -******** +**Bugfixes** * `#1959 `_ @@ -490,8 +466,7 @@ Version 20.9.0 --------------- -Features -******** +**Features** * `#1887 `_ @@ -518,22 +493,19 @@ Features `#1937 `_ Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto) -Bugfixes -******** +**Bugfixes** * `#1897 `_ Resolves exception from unread bytes in stream -Deprecations and Removals -************************* +**Deprecations and Removals** * `#1903 `_ config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3 -Developer infrastructure -************************ +**Developer infrastructure** * `#1890 `_, @@ -548,8 +520,7 @@ Developer infrastructure `#1924 `_ Adding --strict-markers for pytest -Improved Documentation -********************** +**Improved Documentation** * `#1922 `_ @@ -559,8 +530,7 @@ Improved Documentation Version 20.6.3 --------------- -Bugfixes -******** +**Bugfixes** * `#1884 `_ @@ -570,8 +540,7 @@ Bugfixes Version 20.6.2 --------------- -Features -******** +**Features** * `#1641 `_ @@ -581,8 +550,7 @@ Features Version 20.6.1 --------------- -Features -******** +**Features** * `#1760 `_ @@ -596,8 +564,7 @@ Features `#1880 `_ Add handler names for websockets for url_for usage -Bugfixes -******** +**Bugfixes** * `#1776 `_ @@ -619,15 +586,13 @@ Bugfixes `#1853 `_ Fix pickle error when attempting to pickle an application which contains websocket routes -Deprecations and Removals -************************* +**Deprecations and Removals** * `#1739 `_ Deprecate body_bytes to merge into body -Developer infrastructure -************************ +**Developer infrastructure** * `#1852 `_ @@ -642,8 +607,7 @@ Developer infrastructure Wrap run()'s "protocol" type annotation in Optional[] -Improved Documentation -********************** +**Improved Documentation** * `#1846 `_ @@ -663,8 +627,7 @@ Version 20.6.0 Version 20.3.0 --------------- -Features -******** +**Features** * `#1762 `_ @@ -695,8 +658,7 @@ Features `#1820 `_ Do not set content-type and content-length headers in exceptions -Bugfixes -******** +**Bugfixes** * `#1748 `_ @@ -714,8 +676,7 @@ Bugfixes `#1808 `_ Fix Ctrl+C and tests on Windows -Deprecations and Removals -************************* +**Deprecations and Removals** * `#1800 `_ @@ -733,8 +694,7 @@ Deprecations and Removals `#1818 `_ Complete deprecation of ``app.remove_route`` and ``request.raw_args`` -Dependencies -************ +**Dependencies** * `#1794 `_ @@ -744,15 +704,13 @@ Dependencies `#1806 `_ Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation) -Developer infrastructure -************************ +**Developer infrastructure** * `#1833 `_ Resolve broken documentation builds -Improved Documentation -********************** +**Improved Documentation** * `#1755 `_ @@ -794,8 +752,7 @@ Improved Documentation Version 19.12.0 --------------- -Bugfixes -******** +**Bugfixes** - Fix blueprint middleware application @@ -814,8 +771,7 @@ Bugfixes due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 `__) -Improved Documentation -********************** +**Improved Documentation** - Move docs from MD to RST @@ -829,8 +785,7 @@ Improved Documentation Version 19.6.3 -------------- -Features -******** +**Features** - Enable Towncrier Support @@ -838,8 +793,7 @@ Features of generating and managing change logs as part of each of pull requests. (`#1631 `__) -Improved Documentation -********************** +**Improved Documentation** - Documentation infrastructure changes @@ -852,8 +806,7 @@ Improved Documentation Version 19.6.2 -------------- -Features -******** +**Features** * `#1562 `_ @@ -869,8 +822,7 @@ Features Add Configure support from object string -Bugfixes -******** +**Bugfixes** * `#1587 `_ @@ -888,8 +840,7 @@ Bugfixes `#1594 `_ Strict Slashes behavior fix -Deprecations and Removals -************************* +**Deprecations and Removals** * `#1544 `_ @@ -913,8 +864,7 @@ Deprecations and Removals Version 19.3 ------------ -Features -******** +**Features** * `#1497 `_ @@ -982,8 +932,7 @@ Features This is a breaking change. -Bugfixes -******** +**Bugfixes** * @@ -1019,8 +968,7 @@ Bugfixes This allows the access log to be disabled for example when running via gunicorn. -Developer infrastructure -************************ +**Developer infrastructure** * `#1529 `_ Update project PyPI credentials * `#1515 `_ fix linter issue causing travis build failures (fix #1514) @@ -1028,8 +976,7 @@ Developer infrastructure * `#1478 `_ Upgrade setuptools version and use native docutils in doc build * `#1464 `_ Upgrade pytest, and fix caplog unit tests -Improved Documentation -********************** +**Improved Documentation** * `#1516 `_ Fix typo at the exception documentation * `#1510 `_ fix typo in Asyncio example @@ -1096,15 +1043,13 @@ Version 18.12 Version 0.8 ----------- -0.8.3 -***** +**0.8.3** * Changes: * Ownership changed to org 'sanic-org' -0.8.0 -***** +**0.8.0** * Changes: @@ -1184,19 +1129,16 @@ Version 0.1 ----------- -0.1.7 -***** +**0.1.7** * Reversed static url and directory arguments to meet spec -0.1.6 -***** +**0.1.6** * Static files * Lazy Cookie Loading -0.1.5 -***** +**0.1.5** * Cookies * Blueprint listeners and ordering @@ -1204,23 +1146,19 @@ Version 0.1 * Fix: Incomplete file reads on medium+ sized post requests * Breaking: after_start and before_stop now pass sanic as their first argument -0.1.4 -***** +**0.1.4** * Multiprocessing -0.1.3 -***** +**0.1.3** * Blueprint support * Faster Response processing -0.1.1 - 0.1.2 -************* +**0.1.1 - 0.1.2** * Struggling to update pypi via CI -0.1.0 -***** +**0.1.0** * Released to public diff --git a/docs/sanic/api/core.rst b/docs/sanic/api/core.rst index 1891c84302..1ed9070a26 100644 --- a/docs/sanic/api/core.rst +++ b/docs/sanic/api/core.rst @@ -38,10 +38,3 @@ sanic.views .. automodule:: sanic.views :members: :show-inheritance: - -sanic.websocket ---------------- - -.. automodule:: sanic.websocket - :members: - :show-inheritance: diff --git a/docs/sanic/changelog.rst b/docs/sanic/changelog.rst index 516b858762..3bcb03d214 100644 --- a/docs/sanic/changelog.rst +++ b/docs/sanic/changelog.rst @@ -1,6 +1,6 @@ 📜 Changelog ============ -.. mdinclude:: ./releases/21.9.md - +.. mdinclude:: ./releases/21/21.12.md +.. mdinclude:: ./releases/21/21.9.md .. include:: ../../CHANGELOG.rst diff --git a/docs/sanic/releases/21/21.12.md b/docs/sanic/releases/21/21.12.md new file mode 100644 index 0000000000..6c4dc4192d --- /dev/null +++ b/docs/sanic/releases/21/21.12.md @@ -0,0 +1,58 @@ +## Version 21.12.0 + +### Features +- [#2260](https://github.com/sanic-org/sanic/pull/2260) Allow early Blueprint registrations to still apply later added objects +- [#2262](https://github.com/sanic-org/sanic/pull/2262) Noisy exceptions - force logging of all exceptions +- [#2264](https://github.com/sanic-org/sanic/pull/2264) Optional `uvloop` by configuration +- [#2270](https://github.com/sanic-org/sanic/pull/2270) Vhost support using multiple TLS certificates +- [#2277](https://github.com/sanic-org/sanic/pull/2277) Change signal routing for increased consistency + - *BREAKING CHANGE*: If you were manually routing signals there is a breaking change. The signal router's `get` is no longer 100% determinative. There is now an additional step to loop thru the returned signals for proper matching on the requirements. If signals are being dispatched using `app.dispatch` or `bp.dispatch`, there is no change. +- [#2290](https://github.com/sanic-org/sanic/pull/2290) Add contextual exceptions +- [#2291](https://github.com/sanic-org/sanic/pull/2291) Increase join concat performance +- [#2295](https://github.com/sanic-org/sanic/pull/2295), [#2316](https://github.com/sanic-org/sanic/pull/2316), [#2331](https://github.com/sanic-org/sanic/pull/2331) Restructure of CLI and application state with new displays and more command parity with `app.run` +- [#2302](https://github.com/sanic-org/sanic/pull/2302) Add route context at definition time +- [#2304](https://github.com/sanic-org/sanic/pull/2304) Named tasks and new API for managing background tasks +- [#2307](https://github.com/sanic-org/sanic/pull/2307) On app auto-reload, provide insight of changed files +- [#2308](https://github.com/sanic-org/sanic/pull/2308) Auto extend application with [Sanic Extensions](https://sanicframework.org/en/plugins/sanic-ext/getting-started.html) if it is installed, and provide first class support for accessing the extensions +- [#2309](https://github.com/sanic-org/sanic/pull/2309) Builtin signals changed to `Enum` +- [#2313](https://github.com/sanic-org/sanic/pull/2313) Support additional config implementation use case +- [#2321](https://github.com/sanic-org/sanic/pull/2321) Refactor environment variable hydration logic +- [#2327](https://github.com/sanic-org/sanic/pull/2327) Prevent sending multiple or mixed responses on a single request +- [#2330](https://github.com/sanic-org/sanic/pull/2330) Custom type casting on environment variables +- [#2332](https://github.com/sanic-org/sanic/pull/2332) Make all deprecation notices consistent +- [#2335](https://github.com/sanic-org/sanic/pull/2335) Allow underscore to start instance names + +### Bugfixes +- [#2273](https://github.com/sanic-org/sanic/pull/2273) Replace assignation by typing for `websocket_handshake` +- [#2285](https://github.com/sanic-org/sanic/pull/2285) Fix IPv6 display in startup logs +- [#2299](https://github.com/sanic-org/sanic/pull/2299) Dispatch `http.lifecyle.response` from exception handler + +### Deprecations and Removals +- [#2306](https://github.com/sanic-org/sanic/pull/2306) Removal of deprecated items + - `Sanic` and `Blueprint` may no longer have arbitrary properties attached to them + - `Sanic` and `Blueprint` forced to have compliant names + - alphanumeric + `_` + `-` + - must start with letter or `_` + - `load_env` keyword argument of `Sanic` + - `sanic.exceptions.abort` + - `sanic.views.CompositionView` + - `sanic.response.StreamingHTTPResponse` + - *NOTE:* the `stream()` response method (where you pass a callable streaming function) has been deprecated and will be removed in v22.6. You should upgrade all streaming responses to the new style: https://sanicframework.org/en/guide/advanced/streaming.html#response-streaming +- [#2320](https://github.com/sanic-org/sanic/pull/2320) Remove app instance from Config for error handler setting + +### Developer infrastructure +- [#2251](https://github.com/sanic-org/sanic/pull/2251) Change dev install command +- [#2286](https://github.com/sanic-org/sanic/pull/2286) Change codeclimate complexity threshold from 5 to 10 +- [#2287](https://github.com/sanic-org/sanic/pull/2287) Update host test function names so they are not overwritten +- [#2292](https://github.com/sanic-org/sanic/pull/2292) Fail CI on error +- [#2311](https://github.com/sanic-org/sanic/pull/2311), [#2324](https://github.com/sanic-org/sanic/pull/2324) Do not run tests for draft PRs +- [#2336](https://github.com/sanic-org/sanic/pull/2336) Remove paths from coverage checks +- [#2338](https://github.com/sanic-org/sanic/pull/2338) Cleanup ports on tests + +### Improved Documentation +- [#2269](https://github.com/sanic-org/sanic/pull/2269), [#2329](https://github.com/sanic-org/sanic/pull/2329), [#2333](https://github.com/sanic-org/sanic/pull/2333) Cleanup typos and fix language + +### Miscellaneous +- [#2257](https://github.com/sanic-org/sanic/pull/2257), [#2294](https://github.com/sanic-org/sanic/pull/2294), [#2341](https://github.com/sanic-org/sanic/pull/2341) Add Python 3.10 support +- [#2279](https://github.com/sanic-org/sanic/pull/2279), [#2317](https://github.com/sanic-org/sanic/pull/2317), [#2322](https://github.com/sanic-org/sanic/pull/2322) Add/correct missing type annotations +- [#2305](https://github.com/sanic-org/sanic/pull/2305) Fix examples to use modern implementations diff --git a/docs/sanic/releases/21.9.md b/docs/sanic/releases/21/21.9.md similarity index 87% rename from docs/sanic/releases/21.9.md rename to docs/sanic/releases/21/21.9.md index 8900340d5c..f95c65c1e9 100644 --- a/docs/sanic/releases/21.9.md +++ b/docs/sanic/releases/21/21.9.md @@ -1,4 +1,14 @@ -## Version 21.9 +## Version 21.9.3 +*Rerelease of v21.9.2 with some cleanup* + +## Version 21.9.2 +- [#2268](https://github.com/sanic-org/sanic/pull/2268) Make HTTP connections start in IDLE stage, avoiding delays and error messages +- [#2310](https://github.com/sanic-org/sanic/pull/2310) More consistent config setting with post-FALLBACK_ERROR_FORMAT apply + +## Version 21.9.1 +- [#2259](https://github.com/sanic-org/sanic/pull/2259) Allow non-conforming ErrorHandlers + +## Version 21.9.0 ### Features - [#2158](https://github.com/sanic-org/sanic/pull/2158), [#2248](https://github.com/sanic-org/sanic/pull/2248) Complete overhaul of I/O to websockets diff --git a/examples/add_task_sanic.py b/examples/add_task_sanic.py index ece264333d..2487b85ec1 100644 --- a/examples/add_task_sanic.py +++ b/examples/add_task_sanic.py @@ -5,7 +5,7 @@ from sanic import Sanic -app = Sanic(__name__) +app = Sanic("Example") async def notify_server_started_after_five_seconds(): diff --git a/examples/amending_request_object.py b/examples/amending_request_object.py index 366dd67d9f..2188b68053 100644 --- a/examples/amending_request_object.py +++ b/examples/amending_request_object.py @@ -4,7 +4,7 @@ from sanic.response import text -app = Sanic(__name__) +app = Sanic("Example") @app.middleware("request") diff --git a/examples/authorized_sanic.py b/examples/authorized_sanic.py index 33e54a4b4d..40f0f5a43d 100644 --- a/examples/authorized_sanic.py +++ b/examples/authorized_sanic.py @@ -6,7 +6,7 @@ from sanic.response import json -app = Sanic(__name__) +app = Sanic("Example") def check_request_for_authorization_status(request): diff --git a/examples/blueprint_middlware_execution_order.py b/examples/blueprint_middlware_execution_order.py index e179c36d7a..b858061a5a 100644 --- a/examples/blueprint_middlware_execution_order.py +++ b/examples/blueprint_middlware_execution_order.py @@ -8,9 +8,9 @@ On a valid request, it should print "1 2 3 6 5 4" to terminal """ -app = Sanic(__name__) +app = Sanic("Example") -bp = Blueprint("bp_" + __name__) +bp = Blueprint("bp_example") @bp.on_request diff --git a/examples/blueprints.py b/examples/blueprints.py index 62340a0d19..4109f485fd 100644 --- a/examples/blueprints.py +++ b/examples/blueprints.py @@ -2,10 +2,10 @@ from sanic.response import file, json -app = Sanic(__name__) -blueprint = Blueprint("name", url_prefix="/my_blueprint") -blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2") -blueprint3 = Blueprint("name3", url_prefix="/my_blueprint3") +app = Sanic("Example") +blueprint = Blueprint("bp_example", url_prefix="/my_blueprint") +blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2") +blueprint3 = Blueprint("bp_example3", url_prefix="/my_blueprint3") @blueprint.route("/foo") diff --git a/examples/delayed_response.py b/examples/delayed_response.py index 5923d10ae5..3fab68121b 100644 --- a/examples/delayed_response.py +++ b/examples/delayed_response.py @@ -3,7 +3,7 @@ from sanic import Sanic, response -app = Sanic(__name__, strict_slashes=True) +app = Sanic("DelayedResponseApp", strict_slashes=True) @app.get("/") diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py index 3d853d326f..eb52ed9a59 100644 --- a/examples/exception_monitoring.py +++ b/examples/exception_monitoring.py @@ -41,7 +41,7 @@ def default(self, request, exception): handler = CustomHandler() -app = Sanic(__name__, error_handler=handler) +app = Sanic("Example", error_handler=handler) @app.route("/") diff --git a/examples/hello_world.py b/examples/hello_world.py index 948090c403..9e45b856a5 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -1,7 +1,7 @@ -from sanic import Sanic -from sanic import response +from sanic import Sanic, response -app = Sanic(__name__) + +app = Sanic("Example") @app.route("/") @@ -9,5 +9,5 @@ async def test(request): return response.json({"test": True}) -if __name__ == '__main__': +if __name__ == "__main__": app.run(host="0.0.0.0", port=8000) diff --git a/examples/limit_concurrency.py b/examples/limit_concurrency.py index 429a312b15..37015817b2 100644 --- a/examples/limit_concurrency.py +++ b/examples/limit_concurrency.py @@ -6,7 +6,7 @@ from sanic.response import json -app = Sanic(__name__) +app = Sanic("Example") sem = None diff --git a/examples/log_request_id.py b/examples/log_request_id.py index c0d2d6f983..87825640df 100644 --- a/examples/log_request_id.py +++ b/examples/log_request_id.py @@ -44,7 +44,7 @@ def filter(self, record): } -app = Sanic(__name__, log_config=LOG_SETTINGS) +app = Sanic("Example", log_config=LOG_SETTINGS) @app.on_request diff --git a/examples/logdna_example.py b/examples/logdna_example.py index 01236d98ad..9e9303b783 100644 --- a/examples/logdna_example.py +++ b/examples/logdna_example.py @@ -43,7 +43,7 @@ def get_mac_address(): logdna.setLevel(logging.INFO) logdna.addHandler(logdna_handler) -app = Sanic(__name__) +app = Sanic("Example") @app.middleware diff --git a/examples/modify_header_example.py b/examples/modify_header_example.py index f13e5f00af..6455cb85b5 100644 --- a/examples/modify_header_example.py +++ b/examples/modify_header_example.py @@ -2,27 +2,29 @@ Modify header or status in response """ -from sanic import Sanic -from sanic import response +from sanic import Sanic, response -app = Sanic(__name__) +app = Sanic("Example") -@app.route('/') + +@app.route("/") def handle_request(request): return response.json( - {'message': 'Hello world!'}, - headers={'X-Served-By': 'sanic'}, - status=200 + {"message": "Hello world!"}, + headers={"X-Served-By": "sanic"}, + status=200, ) -@app.route('/unauthorized') +@app.route("/unauthorized") def handle_request(request): return response.json( - {'message': 'You are not authorized'}, - headers={'X-Served-By': 'sanic'}, - status=404 + {"message": "You are not authorized"}, + headers={"X-Served-By": "sanic"}, + status=404, ) -app.run(host="0.0.0.0", port=8000, debug=True) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/examples/pytest_xdist.py b/examples/pytest_xdist.py index 7c2019035e..7adeb93aec 100644 --- a/examples/pytest_xdist.py +++ b/examples/pytest_xdist.py @@ -32,7 +32,7 @@ def test_port(worker_id): @pytest.fixture(scope="session") def app(): - app = Sanic() + app = Sanic("Example") @app.route("/") async def index(request): diff --git a/examples/raygun_example.py b/examples/raygun_example.py index fcb4a5eb43..2270ea9d18 100644 --- a/examples/raygun_example.py +++ b/examples/raygun_example.py @@ -8,7 +8,6 @@ class RaygunExceptionReporter(ErrorHandler): - def __init__(self, raygun_api_key=None): super().__init__() if raygun_api_key is None: @@ -22,16 +21,13 @@ def default(self, request, exception): raygun_error_reporter = RaygunExceptionReporter() -app = Sanic(__name__, error_handler=raygun_error_reporter) +app = Sanic("Example", error_handler=raygun_error_reporter) @app.route("/raise") async def test(request): - raise SanicException('You Broke It!') + raise SanicException("You Broke It!") -if __name__ == '__main__': - app.run( - host="0.0.0.0", - port=getenv("PORT", 8080) - ) +if __name__ == "__main__": + app.run(host="0.0.0.0", port=getenv("PORT", 8080)) diff --git a/examples/redirect_example.py b/examples/redirect_example.py index f73ad178b5..74fb10de11 100644 --- a/examples/redirect_example.py +++ b/examples/redirect_example.py @@ -1,18 +1,18 @@ -from sanic import Sanic -from sanic import response +from sanic import Sanic, response -app = Sanic(__name__) - -@app.route('/') +app = Sanic("Example") + + +@app.route("/") def handle_request(request): - return response.redirect('/redirect') + return response.redirect("/redirect") -@app.route('/redirect') +@app.route("/redirect") async def test(request): return response.json({"Redirected": True}) -if __name__ == '__main__': - app.run(host="0.0.0.0", port=8000) \ No newline at end of file +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) diff --git a/examples/request_stream/server.py b/examples/request_stream/server.py index d3d35aef2c..bffc7451ab 100644 --- a/examples/request_stream/server.py +++ b/examples/request_stream/server.py @@ -1,65 +1,63 @@ from sanic import Sanic -from sanic.views import CompositionView -from sanic.views import HTTPMethodView -from sanic.views import stream as stream_decorator from sanic.blueprints import Blueprint from sanic.response import stream, text +from sanic.views import HTTPMethodView +from sanic.views import stream as stream_decorator -bp = Blueprint('blueprint_request_stream') -app = Sanic('request_stream') +bp = Blueprint("bp_example") +app = Sanic("Example") -class SimpleView(HTTPMethodView): +class SimpleView(HTTPMethodView): @stream_decorator async def post(self, request): - result = '' + result = "" while True: body = await request.stream.get() if body is None: break - result += body.decode('utf-8') + result += body.decode("utf-8") return text(result) -@app.post('/stream', stream=True) +@app.post("/stream", stream=True) async def handler(request): async def streaming(response): while True: body = await request.stream.get() if body is None: break - body = body.decode('utf-8').replace('1', 'A') + body = body.decode("utf-8").replace("1", "A") await response.write(body) + return stream(streaming) -@bp.put('/bp_stream', stream=True) +@bp.put("/bp_stream", stream=True) async def bp_handler(request): - result = '' + result = "" while True: body = await request.stream.get() if body is None: break - result += body.decode('utf-8').replace('1', 'A') + result += body.decode("utf-8").replace("1", "A") return text(result) async def post_handler(request): - result = '' + result = "" while True: body = await request.stream.get() if body is None: break - result += body.decode('utf-8') + result += body.decode("utf-8") return text(result) + app.blueprint(bp) -app.add_route(SimpleView.as_view(), '/method_view') -view = CompositionView() -view.add(['POST'], post_handler, stream=True) -app.add_route(view, '/composition_view') +app.add_route(SimpleView.as_view(), "/method_view") -if __name__ == '__main__': - app.run(host='0.0.0.0', port=8000) +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) diff --git a/examples/request_timeout.py b/examples/request_timeout.py index 0c2c489c00..1c7885eeb5 100644 --- a/examples/request_timeout.py +++ b/examples/request_timeout.py @@ -1,21 +1,23 @@ import asyncio -from sanic import Sanic -from sanic import response + +from sanic import Sanic, response from sanic.config import Config from sanic.exceptions import RequestTimeout + Config.REQUEST_TIMEOUT = 1 -app = Sanic(__name__) +app = Sanic("Example") -@app.route('/') +@app.route("/") async def test(request): await asyncio.sleep(3) - return response.text('Hello, world!') + return response.text("Hello, world!") @app.exception(RequestTimeout) def timeout(request, exception): - return response.text('RequestTimeout from error_handler.', 408) + return response.text("RequestTimeout from error_handler.", 408) + -app.run(host='0.0.0.0', port=8000) +app.run(host="0.0.0.0", port=8000) diff --git a/examples/rollbar_example.py b/examples/rollbar_example.py index cf37244f6f..53d53c5a9b 100644 --- a/examples/rollbar_example.py +++ b/examples/rollbar_example.py @@ -1,21 +1,22 @@ +from os import getenv + import rollbar -from sanic.handlers import ErrorHandler from sanic import Sanic from sanic.exceptions import SanicException -from os import getenv +from sanic.handlers import ErrorHandler + rollbar.init(getenv("ROLLBAR_API_KEY")) class RollbarExceptionHandler(ErrorHandler): - def default(self, request, exception): rollbar.report_message(str(exception)) return super().default(request, exception) -app = Sanic(__name__, error_handler=RollbarExceptionHandler()) +app = Sanic("Example", error_handler=RollbarExceptionHandler()) @app.route("/raise") @@ -24,7 +25,4 @@ def create_error(request): if __name__ == "__main__": - app.run( - host="0.0.0.0", - port=getenv("PORT", 8080) - ) + app.run(host="0.0.0.0", port=getenv("PORT", 8080)) diff --git a/examples/run_asgi.py b/examples/run_asgi.py index c29c5fbb96..bc55978975 100644 --- a/examples/run_asgi.py +++ b/examples/run_asgi.py @@ -11,7 +11,7 @@ from sanic import Sanic, response -app = Sanic(__name__) +app = Sanic("Example") @app.route("/text") diff --git a/examples/run_async.py b/examples/run_async.py index 64f6842de9..84c8a6be0c 100644 --- a/examples/run_async.py +++ b/examples/run_async.py @@ -5,7 +5,7 @@ from sanic import Sanic, response -app = Sanic(__name__) +app = Sanic("Example") @app.route("/") diff --git a/examples/run_async_advanced.py b/examples/run_async_advanced.py index 7ea30dd773..dc8d71c660 100644 --- a/examples/run_async_advanced.py +++ b/examples/run_async_advanced.py @@ -8,7 +8,7 @@ from sanic.server import AsyncioServer -app = Sanic(__name__) +app = Sanic("Example") @app.before_server_start diff --git a/examples/sentry_example.py b/examples/sentry_example.py index 1ea75a0f09..1dfc1ba4df 100644 --- a/examples/sentry_example.py +++ b/examples/sentry_example.py @@ -6,20 +6,19 @@ from sanic import Sanic from sanic.response import json + sentry_init( dsn=getenv("SENTRY_DSN"), integrations=[SanicIntegration()], ) -app = Sanic(__name__) +app = Sanic("Example") # noinspection PyUnusedLocal @app.route("/working") async def working_path(request): - return json({ - "response": "Working API Response" - }) + return json({"response": "Working API Response"}) # noinspection PyUnusedLocal @@ -28,8 +27,5 @@ async def raise_error(request): raise Exception("Testing Sentry Integration") -if __name__ == '__main__': - app.run( - host="0.0.0.0", - port=getenv("PORT", 8080) - ) +if __name__ == "__main__": + app.run(host="0.0.0.0", port=getenv("PORT", 8080)) diff --git a/examples/static_assets.py b/examples/static_assets.py index 23f324effe..347b489c6c 100644 --- a/examples/static_assets.py +++ b/examples/static_assets.py @@ -1,6 +1,6 @@ from sanic import Sanic -app = Sanic(__name__) +app = Sanic("Example") app.static("/", "./static") diff --git a/examples/teapot.py b/examples/teapot.py index 897f78366b..5d742efc1c 100644 --- a/examples/teapot.py +++ b/examples/teapot.py @@ -1,13 +1,14 @@ from sanic import Sanic from sanic import response as res -app = Sanic(__name__) + +app = Sanic("Example") @app.route("/") async def test(req): - return res.text("I\'m a teapot", status=418) + return res.text("I'm a teapot", status=418) -if __name__ == '__main__': +if __name__ == "__main__": app.run(host="0.0.0.0", port=8000) diff --git a/examples/try_everything.py b/examples/try_everything.py index 8e4a8e099c..f137706040 100644 --- a/examples/try_everything.py +++ b/examples/try_everything.py @@ -5,7 +5,7 @@ from sanic.log import logger as log -app = Sanic(__name__) +app = Sanic("Example") @app.route("/") diff --git a/examples/unix_socket.py b/examples/unix_socket.py index a64b205d6d..0963a625e6 100644 --- a/examples/unix_socket.py +++ b/examples/unix_socket.py @@ -4,7 +4,7 @@ from sanic import Sanic, response -app = Sanic(__name__) +app = Sanic("Example") @app.route("/test") diff --git a/examples/url_for_example.py b/examples/url_for_example.py index f0d3614b9a..c3d936fa68 100644 --- a/examples/url_for_example.py +++ b/examples/url_for_example.py @@ -1,7 +1,7 @@ from sanic import Sanic, response -app = Sanic(__name__) +app = Sanic("Example") @app.route("/") diff --git a/examples/vhosts.py b/examples/vhosts.py index 57d42e9d30..335a3be8a4 100644 --- a/examples/vhosts.py +++ b/examples/vhosts.py @@ -8,7 +8,7 @@ # curl -H "Host: bp.example.com" localhost:8000/question # curl -H "Host: bp.example.com" localhost:8000/answer -app = Sanic(__name__) +app = Sanic("Example") bp = Blueprint("bp", host="bp.example.com") diff --git a/examples/websocket.py b/examples/websocket.py index 7bcd2cd1ef..ce5a105ceb 100644 --- a/examples/websocket.py +++ b/examples/websocket.py @@ -2,7 +2,7 @@ from sanic.response import redirect -app = Sanic(__name__) +app = Sanic("Example") app.static("index.html", "websocket.html") diff --git a/sanic/__version__.py b/sanic/__version__.py index 02ed01d4dc..4f86417dcb 100644 --- a/sanic/__version__.py +++ b/sanic/__version__.py @@ -1 +1 @@ -__version__ = "21.12.0dev" +__version__ = "21.12.0" diff --git a/sanic/app.py b/sanic/app.py index 27e0c65026..9f48835902 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -44,7 +44,7 @@ Union, ) from urllib.parse import urlencode, urlunparse -from warnings import filterwarnings, warn +from warnings import filterwarnings from sanic_routing.exceptions import ( # type: ignore FinalizationError, @@ -57,7 +57,7 @@ from sanic.application.motd import MOTD from sanic.application.state import ApplicationState, Mode from sanic.asgi import ASGIApp -from sanic.base import BaseSanic +from sanic.base.root import BaseSanic from sanic.blueprint_group import BlueprintGroup from sanic.blueprints import Blueprint from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support @@ -71,7 +71,13 @@ from sanic.handlers import ErrorHandler from sanic.helpers import _default from sanic.http import Stage -from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, error_logger, logger +from sanic.log import ( + LOGGING_CONFIG_DEFAULTS, + Colors, + deprecation, + error_logger, + logger, +) from sanic.mixins.listeners import ListenerEvent from sanic.models.futures import ( FutureException, @@ -85,7 +91,7 @@ from sanic.models.handler_types import ListenerType, MiddlewareType from sanic.models.handler_types import Sanic as SanicVar from sanic.request import Request -from sanic.response import BaseHTTPResponse, HTTPResponse +from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream from sanic.router import Router from sanic.server import AsyncioServer, HttpProtocol from sanic.server import Signal as ServerSignal @@ -114,8 +120,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): "_run_response_middleware", "_run_request_middleware", ) - __fake_slots__ = ( - "_app_registry", + __slots__ = ( "_asgi_app", "_asgi_client", "_blueprint_order", @@ -131,19 +136,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): "_task_registry", "_test_client", "_test_manager", - "_uvloop_setting", # TODO: Remove in v22.6 - "asgi", - "auto_reload", - "auto_reload", "blueprints", "config", "configure_logging", "ctx", - "debug", "error_handler", "go_fast", - "is_running", - "is_stopping", "listeners", "name", "named_request_middleware", @@ -155,13 +153,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): "signal_router", "sock", "strict_slashes", - "test_mode", "websocket_enabled", "websocket_tasks", ) _app_registry: Dict[str, "Sanic"] = {} - _uvloop_setting = None + _uvloop_setting = None # TODO: Remove in v22.6 test_mode = False def __init__( @@ -172,7 +169,6 @@ def __init__( router: Optional[Router] = None, signal_router: Optional[SignalRouter] = None, error_handler: Optional[ErrorHandler] = None, - load_env: Union[bool, str] = True, env_prefix: Optional[str] = SANIC_PREFIX, request_class: Optional[Type[Request]] = None, strict_slashes: bool = False, @@ -188,17 +184,16 @@ def __init__( dict_config = log_config or LOGGING_CONFIG_DEFAULTS logging.config.dictConfig(dict_config) # type: ignore - if config and (load_env is not True or env_prefix != SANIC_PREFIX): + if config and env_prefix != SANIC_PREFIX: raise SanicException( "When instantiating Sanic with config, you cannot also pass " - "load_env or env_prefix" + "env_prefix" ) - self.config: Config = config or Config( - load_env=load_env, - env_prefix=env_prefix, - ) + # First setup config + self.config: Config = config or Config(env_prefix=env_prefix) + # Then we can do the rest self._asgi_client: Any = None self._blueprint_order: List[Blueprint] = [] self._delayed_tasks: List[str] = [] @@ -231,6 +226,12 @@ def __init__( self.go_fast = self.run if register is not None: + deprecation( + "The register argument is deprecated and will stop working " + "in v22.6. After v22.6 all apps will be added to the Sanic " + "app registry.", + 22.6, + ) self.config.REGISTER = register if self.config.REGISTER: self.__class__.register_app(self) @@ -740,7 +741,7 @@ async def handle_exception( exception, request.name if request else None ) if handler: - warn( + deprecation( "An error occurred while handling the request after at " "least some part of the response was sent to the client. " "Therefore, the response from your custom exception " @@ -755,7 +756,7 @@ async def handle_exception( "For further information, please see the docs: " "https://sanicframework.org/en/guide/advanced/" "signals.html", - DeprecationWarning, + 22.6, ) try: response = self.error_handler.response(request, exception) @@ -808,6 +809,9 @@ async def handle_exception( else: if request.stream: response = request.stream.response + + # Marked for cleanup and DRY with handle_request/handle_exception + # when ResponseStream is no longer supporder if isinstance(response, BaseHTTPResponse): await self.dispatch( "http.lifecycle.response", @@ -818,6 +822,17 @@ async def handle_exception( }, ) await response.send(end_stream=True) + elif isinstance(response, ResponseStream): + resp = await response(request) + await self.dispatch( + "http.lifecycle.response", + inline=True, + context={ + "request": request, + "response": resp, + }, + ) + await response.eof() else: raise ServerError( f"Invalid response type {response!r} (need HTTPResponse)" @@ -921,7 +936,8 @@ async def handle_request(self, request: Request): # no cov elif not hasattr(handler, "is_websocket"): response = request.stream.response # type: ignore - # Make sure that response is finished / run StreamingHTTP callback + # Marked for cleanup and DRY with handle_request/handle_exception + # when ResponseStream is no longer supporder if isinstance(response, BaseHTTPResponse): await self.dispatch( "http.lifecycle.response", @@ -932,6 +948,17 @@ async def handle_request(self, request: Request): # no cov }, ) await response.send(end_stream=True) + elif isinstance(response, ResponseStream): + resp = await response(request) + await self.dispatch( + "http.lifecycle.response", + inline=True, + context={ + "request": request, + "response": resp, + }, + ) + await response.eof() else: if not hasattr(handler, "is_websocket"): raise ServerError( diff --git a/sanic/base/__init__.py b/sanic/base/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sanic/base/meta.py b/sanic/base/meta.py new file mode 100644 index 0000000000..2c6870c259 --- /dev/null +++ b/sanic/base/meta.py @@ -0,0 +1,6 @@ +class SanicMeta(type): + @classmethod + def __prepare__(metaclass, name, bases, **kwds): + cls = super().__prepare__(metaclass, name, bases, **kwds) + cls["__slots__"] = () + return cls diff --git a/sanic/base.py b/sanic/base/root.py similarity index 60% rename from sanic/base.py rename to sanic/base/root.py index 973b76ed6d..7fe769882b 100644 --- a/sanic/base.py +++ b/sanic/base/root.py @@ -1,8 +1,8 @@ import re -from typing import Any, Tuple -from warnings import warn +from typing import Any +from sanic.base.meta import SanicMeta from sanic.exceptions import SanicException from sanic.mixins.exceptions import ExceptionMixin from sanic.mixins.listeners import ListenerMixin @@ -20,8 +20,9 @@ class BaseSanic( ListenerMixin, ExceptionMixin, SignalMixin, + metaclass=SanicMeta, ): - __fake_slots__: Tuple[str, ...] + __slots__ = ("name",) def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None: class_name = self.__class__.__name__ @@ -33,11 +34,10 @@ def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None: ) if not VALID_NAME.match(name): - warn( - f"{class_name} instance named '{name}' uses a format that is" - f"deprecated. Starting in version 21.12, {class_name} objects " - "must be named only using alphanumeric characters, _, or -.", - DeprecationWarning, + raise SanicException( + f"{class_name} instance named '{name}' uses an invalid " + "format. Names must begin with a character and may only " + "contain alphanumeric characters, _, or -." ) self.name = name @@ -52,15 +52,12 @@ def __repr__(self) -> str: return f'{self.__class__.__name__}(name="{self.name}")' def __setattr__(self, name: str, value: Any) -> None: - # This is a temporary compat layer so we can raise a warning until - # setting attributes on the app instance can be removed and deprecated - # with a proper implementation of __slots__ - if name not in self.__fake_slots__: - warn( + try: + super().__setattr__(name, value) + except AttributeError as e: + raise AttributeError( f"Setting variables on {self.__class__.__name__} instances is " - "deprecated and will be removed in version 21.12. You should " - f"change your {self.__class__.__name__} instance to use " + "not allowed. You should change your " + f"{self.__class__.__name__} instance to use " f"instance.ctx.{name} instead.", - DeprecationWarning, - ) - super().__setattr__(name, value) + ) from e diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 98308bc4f1..7ef8f58d56 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -24,7 +24,7 @@ from sanic_routing.exceptions import NotFound # type: ignore from sanic_routing.route import Route # type: ignore -from sanic.base import BaseSanic +from sanic.base.root import BaseSanic from sanic.blueprint_group import BlueprintGroup from sanic.exceptions import SanicException from sanic.helpers import Default, _default @@ -85,7 +85,7 @@ class Blueprint(BaseSanic): trailing */* """ - __fake_slots__ = ( + __slots__ = ( "_apps", "_future_routes", "_future_statics", @@ -98,7 +98,6 @@ class Blueprint(BaseSanic): "host", "listeners", "middlewares", - "name", "routes", "statics", "strict_slashes", diff --git a/sanic/config.py b/sanic/config.py index 1e770f6745..d0b3c62029 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -4,12 +4,11 @@ from os import environ from pathlib import Path from typing import Any, Callable, Dict, Optional, Sequence, Union -from warnings import warn from sanic.errorpages import DEFAULT_FORMAT, check_error_format from sanic.helpers import Default, _default from sanic.http import Http -from sanic.log import error_logger +from sanic.log import deprecation, error_logger from sanic.utils import load_module_from_file_location, str_to_bool @@ -88,7 +87,6 @@ class Config(dict, metaclass=DescriptorMeta): 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, *, @@ -110,15 +108,6 @@ def __init__( 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) @@ -161,10 +150,10 @@ def _post_set(self, attr, value) -> None: self._configure_header_size() elif attr == "LOGO": self._LOGO = value - warn( + deprecation( "Setting the config.LOGO is deprecated and will no longer " "be supported starting in v22.6.", - DeprecationWarning, + 22.6, ) @property diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 6459f15a42..d3bd2613b7 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -244,25 +244,3 @@ class InvalidSignal(SanicException): 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 - message appropriate for the given status code, unless provided. - - STATUS_CODES from sanic.helpers for the given status code. - - :param status_code: The HTTP status code to return. - :param message: The HTTP response body. Defaults to the messages in - """ - import warnings - - warnings.warn( - "sanic.exceptions.abort has been marked as deprecated, and will be " - "removed in release 21.12.\n To migrate your code, simply replace " - "abort(status_code, msg) with raise SanicException(msg, status_code), " - "or even better, raise an appropriate SanicException subclass." - ) - - raise SanicException(message=message, status_code=status_code) diff --git a/sanic/handlers.py b/sanic/handlers.py index d564663d6b..44ff77a74e 100644 --- a/sanic/handlers.py +++ b/sanic/handlers.py @@ -2,7 +2,6 @@ from inspect import signature from typing import Dict, List, Optional, Tuple, Type, Union -from warnings import warn from sanic.config import Config from sanic.errorpages import ( @@ -18,7 +17,7 @@ SanicException, ) from sanic.helpers import Default, _default -from sanic.log import error_logger +from sanic.log import deprecation, error_logger from sanic.models.handler_types import RouteHandler from sanic.response import text @@ -71,12 +70,12 @@ def fallback(self, value: str): @staticmethod def _warn_fallback_deprecation(): - warn( + deprecation( "Setting the ErrorHandler fallback value directly is " "deprecated and no longer supported. This feature will " "be removed in v22.6. Instead, use " "app.config.FALLBACK_ERROR_FORMAT.", - DeprecationWarning, + 22.6, ) @classmethod @@ -100,19 +99,19 @@ def finalize( config: Optional[Config] = None, ): if fallback: - warn( + deprecation( "Setting the ErrorHandler fallback value via finalize() " "is deprecated and no longer supported. This feature will " "be removed in v22.6. Instead, use " "app.config.FALLBACK_ERROR_FORMAT.", - DeprecationWarning, + 22.6, ) if config is None: - warn( + deprecation( "Starting in v22.3, config will be a required argument " "for ErrorHandler.finalize().", - DeprecationWarning, + 22.3, ) if fallback and fallback != DEFAULT_FORMAT: @@ -131,7 +130,7 @@ def finalize( sig = signature(error_handler.lookup) if len(sig.parameters) == 1: - warn( + deprecation( "You are using a deprecated error handler. The lookup " "method should accept two positional parameters: " "(exception, route_name: Optional[str]). " @@ -139,7 +138,7 @@ def finalize( "specific exceptions will not work properly. Beginning " "in v22.3, the legacy style lookup method will not " "work at all.", - DeprecationWarning, + 22.3, ) legacy_lookup = error_handler._legacy_lookup error_handler._lookup = legacy_lookup # type: ignore diff --git a/sanic/log.py b/sanic/log.py index 99c8b73200..000b3ed898 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -3,6 +3,7 @@ from enum import Enum from typing import Any, Dict +from warnings import warn LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( @@ -78,3 +79,11 @@ class Colors(str, Enum): """ Logger used by Sanic for access logging """ + + +def deprecation(message: str, version: float): + version_info = f"[DEPRECATION v{version}] " + if sys.stdout.isatty(): + version_info = f"{Colors.RED}{version_info}" + message = f"{Colors.YELLOW}{message}{Colors.END}" + warn(version_info + message, DeprecationWarning) diff --git a/sanic/mixins/exceptions.py b/sanic/mixins/exceptions.py index aed1ae5fa8..fc5b82ef58 100644 --- a/sanic/mixins/exceptions.py +++ b/sanic/mixins/exceptions.py @@ -1,9 +1,10 @@ from typing import Set +from sanic.base.meta import SanicMeta from sanic.models.futures import FutureException -class ExceptionMixin: +class ExceptionMixin(metaclass=SanicMeta): def __init__(self, *args, **kwargs) -> None: self._future_exceptions: Set[FutureException] = set() diff --git a/sanic/mixins/listeners.py b/sanic/mixins/listeners.py index 39c969b861..cc8375f0d0 100644 --- a/sanic/mixins/listeners.py +++ b/sanic/mixins/listeners.py @@ -2,6 +2,7 @@ from functools import partial from typing import List, Optional, Union +from sanic.base.meta import SanicMeta from sanic.models.futures import FutureListener from sanic.models.handler_types import ListenerType, Sanic @@ -18,7 +19,7 @@ def _generate_next_value_(name: str, *args) -> str: # type: ignore MAIN_PROCESS_STOP = auto() -class ListenerMixin: +class ListenerMixin(metaclass=SanicMeta): def __init__(self, *args, **kwargs) -> None: self._future_listeners: List[FutureListener] = [] diff --git a/sanic/mixins/middleware.py b/sanic/mixins/middleware.py index bd006062ec..1049aacf6b 100644 --- a/sanic/mixins/middleware.py +++ b/sanic/mixins/middleware.py @@ -1,10 +1,11 @@ from functools import partial from typing import List +from sanic.base.meta import SanicMeta from sanic.models.futures import FutureMiddleware -class MiddlewareMixin: +class MiddlewareMixin(metaclass=SanicMeta): def __init__(self, *args, **kwargs) -> None: self._future_middleware: List[FutureMiddleware] = [] diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py index bfadd2df87..9e2cf96fc4 100644 --- a/sanic/mixins/routes.py +++ b/sanic/mixins/routes.py @@ -1,4 +1,5 @@ from ast import NodeVisitor, Return, parse +from contextlib import suppress from functools import partial, wraps from inspect import getsource, signature from mimetypes import guess_type @@ -12,6 +13,7 @@ from sanic_routing.route import Route # type: ignore +from sanic.base.meta import SanicMeta from sanic.compat import stat_async from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS from sanic.errorpages import RESPONSE_MAPPING @@ -22,12 +24,11 @@ InvalidUsage, ) from sanic.handlers import ContentRangeHandler -from sanic.log import error_logger +from sanic.log import deprecation, error_logger from sanic.models.futures import FutureRoute, FutureStatic from sanic.models.handler_types import RouteHandler from sanic.response import HTTPResponse, file, file_stream from sanic.types import HashableDict -from sanic.views import CompositionView RouteWrapper = Callable[ @@ -43,7 +44,7 @@ ) -class RouteMixin: +class RouteMixin(metaclass=SanicMeta): name: str def __init__(self, *args, **kwargs) -> None: @@ -253,14 +254,6 @@ def add_route( if hasattr(_handler, "is_stream"): stream = True - # handle composition view differently - if isinstance(handler, CompositionView): - methods = handler.handlers.keys() - for _handler in handler.handlers.values(): - if hasattr(_handler, "is_stream"): - stream = True - break - if strict_slashes is None: strict_slashes = self.strict_slashes @@ -982,19 +975,16 @@ def _register_static( 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) + def _determine_error_format(self, handler) -> str: + with suppress(OSError, TypeError): + src = dedent(getsource(handler)) + tree = parse(src) + http_response_types = self._get_response_types(tree) - if len(http_response_types) == 1: - return next(iter(http_response_types)) - except (OSError, TypeError): - ... + if len(http_response_types) == 1: + return next(iter(http_response_types)) - return None + return "" def _get_response_types(self, node): types = set() @@ -1003,7 +993,18 @@ class HttpResponseVisitor(NodeVisitor): def visit_Return(self, node: Return) -> Any: nonlocal types - try: + with suppress(AttributeError): + if node.value.func.id == "stream": # type: ignore + deprecation( + "The sanic.response.stream method has been " + "deprecated and will be removed in v22.6. Please " + "upgrade your application to use the new style " + "streaming pattern. See " + "https://sanicframework.org/en/guide/advanced/" + "streaming.html#response-streaming for more " + "information.", + 22.6, + ) checks = [node.value.func.id] # type: ignore if node.value.keywords: # type: ignore checks += [ @@ -1015,8 +1016,6 @@ def visit_Return(self, node: Return) -> Any: for check in checks: if check in RESPONSE_MAPPING: types.add(RESPONSE_MAPPING[check]) - except AttributeError: - ... HttpResponseVisitor().visit(node) diff --git a/sanic/mixins/signals.py b/sanic/mixins/signals.py index 86d3b98b9a..a838316a5c 100644 --- a/sanic/mixins/signals.py +++ b/sanic/mixins/signals.py @@ -1,13 +1,14 @@ from enum import Enum from typing import Any, Callable, Dict, Optional, Set, Union +from sanic.base.meta import SanicMeta from sanic.models.futures import FutureSignal from sanic.models.handler_types import SignalHandler from sanic.signals import Signal from sanic.types import HashableDict -class SignalMixin: +class SignalMixin(metaclass=SanicMeta): def __init__(self, *args, **kwargs) -> None: self._future_signals: Set[FutureSignal] = set() diff --git a/sanic/response.py b/sanic/response.py index 357668e682..8525d381bb 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from functools import partial from mimetypes import guess_type from os import path @@ -12,10 +14,10 @@ Iterator, Optional, Tuple, + TypeVar, Union, ) from urllib.parse import quote_plus -from warnings import warn from sanic.compat import Header, open_async from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE @@ -28,6 +30,10 @@ if TYPE_CHECKING: from sanic.asgi import ASGIApp + from sanic.request import Request +else: + Request = TypeVar("Request") + try: from ujson import dumps as json_dumps @@ -136,95 +142,6 @@ async def send( await self.stream.send(data, end_stream=end_stream) -StreamingFunction = Callable[[BaseHTTPResponse], Coroutine[Any, Any, None]] - - -class StreamingHTTPResponse(BaseHTTPResponse): - """ - Old style streaming response where you pass a streaming function: - - .. code-block:: python - - async def sample_streaming_fn(response): - await response.write("foo") - await asyncio.sleep(1) - await response.write("bar") - await asyncio.sleep(1) - - @app.post("/") - async def test(request): - return stream(sample_streaming_fn) - - .. warning:: - - **Deprecated** and set for removal in v21.12. You can now achieve the - same functionality without a callback. - - .. code-block:: python - - @app.post("/") - async def test(request): - response = await request.respond() - await response.send("foo", False) - await asyncio.sleep(1) - await response.send("bar", False) - await asyncio.sleep(1) - await response.send("", True) - return response - - """ - - __slots__ = ( - "streaming_fn", - "status", - "content_type", - "headers", - "_cookies", - ) - - def __init__( - self, - streaming_fn: StreamingFunction, - status: int = 200, - headers: Optional[Union[Header, Dict[str, str]]] = None, - content_type: str = "text/plain; charset=utf-8", - ignore_deprecation_notice: bool = False, - ): - if not ignore_deprecation_notice: - warn( - "Use of the StreamingHTTPResponse is deprecated in v21.6, and " - "will be removed in v21.12. Please upgrade your streaming " - "response implementation. You can learn more here: " - "https://sanicframework.org/en/guide/advanced/streaming.html" - "#response-streaming. If you use the builtin stream() or " - "file_stream() methods, this upgrade will be be done for you." - ) - - super().__init__() - - self.content_type = content_type - self.streaming_fn = streaming_fn - self.status = status - self.headers = Header(headers or {}) - self._cookies = None - - async def write(self, data): - """Writes a chunk of data to the streaming response. - - :param data: str or bytes-ish data to be written. - """ - await super().send(self._encode_body(data)) - - async def send(self, *args, **kwargs): - if self.streaming_fn is not None: - await self.streaming_fn(self) - self.streaming_fn = None - await super().send(*args, **kwargs) - - async def eof(self): - raise NotImplementedError - - class HTTPResponse(BaseHTTPResponse): """ HTTP response to be sent back to the client. @@ -419,6 +336,109 @@ async def file( ) +def redirect( + to: str, + headers: Optional[Dict[str, str]] = None, + status: int = 302, + content_type: str = "text/html; charset=utf-8", +) -> HTTPResponse: + """ + Abort execution and cause a 302 redirect (by default) by setting a + Location header. + + :param to: path or fully qualified URL to redirect to + :param headers: optional dict of headers to include in the new request + :param status: status code (int) of the new request, defaults to 302 + :param content_type: the content type (string) of the response + """ + headers = headers or {} + + # URL Quote the URL before redirecting + safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;") + + # According to RFC 7231, a relative URI is now permitted. + headers["Location"] = safe_to + + return HTTPResponse( + status=status, headers=headers, content_type=content_type + ) + + +class ResponseStream: + """ + ResponseStream is a compat layer to bridge the gap after the deprecation + of StreamingHTTPResponse. In v22.6 it will be removed when: + - stream is removed + - file_stream is moved to new style streaming + - file and file_stream are combined into a single API + """ + + __slots__ = ( + "_cookies", + "content_type", + "headers", + "request", + "response", + "status", + "streaming_fn", + ) + + def __init__( + self, + streaming_fn: Callable[ + [Union[BaseHTTPResponse, ResponseStream]], + Coroutine[Any, Any, None], + ], + status: int = 200, + headers: Optional[Union[Header, Dict[str, str]]] = None, + content_type: Optional[str] = None, + ): + self.streaming_fn = streaming_fn + self.status = status + self.headers = headers or Header() + self.content_type = content_type + self.request: Optional[Request] = None + self._cookies: Optional[CookieJar] = None + + async def write(self, message: str): + await self.response.send(message) + + async def stream(self) -> HTTPResponse: + if not self.request: + raise ServerError("Attempted response to unknown request") + self.response = await self.request.respond( + headers=self.headers, + status=self.status, + content_type=self.content_type, + ) + await self.streaming_fn(self) + return self.response + + async def eof(self) -> None: + await self.response.eof() + + @property + def cookies(self) -> CookieJar: + if self._cookies is None: + self._cookies = CookieJar(self.headers) + return self._cookies + + @property + def processed_headers(self): + return self.response.processed_headers + + @property + def body(self): + return self.response.body + + def __call__(self, request: Request) -> ResponseStream: + self.request = request + return self + + def __await__(self): + return self.stream().__await__() + + async def file_stream( location: Union[str, PurePath], status: int = 200, @@ -427,7 +447,7 @@ async def file_stream( headers: Optional[Dict[str, str]] = None, filename: Optional[str] = None, _range: Optional[Range] = None, -) -> StreamingHTTPResponse: +) -> ResponseStream: """Return a streaming response object with file data. :param location: Location of file on system. @@ -435,7 +455,6 @@ async def file_stream( :param mime_type: Specific mime_type. :param headers: Custom Headers. :param filename: Override filename. - :param chunked: Deprecated :param _range: """ headers = headers or {} @@ -471,23 +490,24 @@ async def _streaming_fn(response): break await response.write(content) - return StreamingHTTPResponse( + return ResponseStream( streaming_fn=_streaming_fn, status=status, headers=headers, content_type=mime_type, - ignore_deprecation_notice=True, ) def stream( - streaming_fn: StreamingFunction, + streaming_fn: Callable[ + [Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None] + ], status: int = 200, headers: Optional[Dict[str, str]] = None, content_type: str = "text/plain; charset=utf-8", -): - """Accepts an coroutine `streaming_fn` which can be used to - write chunks to a streaming response. Returns a `StreamingHTTPResponse`. +) -> ResponseStream: + """Accepts a coroutine `streaming_fn` which can be used to + write chunks to a streaming response. Returns a `ResponseStream`. Example usage:: @@ -501,42 +521,13 @@ async def streaming_fn(response): :param streaming_fn: A coroutine accepts a response and writes content to that response. - :param mime_type: Specific mime_type. + :param status: HTTP status. + :param content_type: Specific content_type. :param headers: Custom Headers. - :param chunked: Deprecated """ - return StreamingHTTPResponse( + return ResponseStream( streaming_fn, headers=headers, content_type=content_type, status=status, - ignore_deprecation_notice=True, - ) - - -def redirect( - to: str, - headers: Optional[Dict[str, str]] = None, - status: int = 302, - content_type: str = "text/html; charset=utf-8", -) -> HTTPResponse: - """ - Abort execution and cause a 302 redirect (by default) by setting a - Location header. - - :param to: path or fully qualified URL to redirect to - :param headers: optional dict of headers to include in the new request - :param status: status code (int) of the new request, defaults to 302 - :param content_type: the content type (string) of the response - """ - headers = headers or {} - - # URL Quote the URL before redirecting - safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;") - - # According to RFC 7231, a relative URI is now permitted. - headers["Location"] = safe_to - - return HTTPResponse( - status=status, headers=headers, content_type=content_type ) diff --git a/sanic/server/async_server.py b/sanic/server/async_server.py index 1ce5688d0b..c13af4646e 100644 --- a/sanic/server/async_server.py +++ b/sanic/server/async_server.py @@ -3,9 +3,9 @@ import asyncio from typing import TYPE_CHECKING -from warnings import warn from sanic.exceptions import SanicException +from sanic.log import deprecation if TYPE_CHECKING: @@ -37,10 +37,10 @@ def __init__( @property def init(self): - warn( + deprecation( "AsyncioServer.init has been deprecated and will be removed " "in v22.6. Use Sanic.state.is_started instead.", - DeprecationWarning, + 22.6, ) return self.app.state.is_started diff --git a/sanic/server/protocols/websocket_protocol.py b/sanic/server/protocols/websocket_protocol.py index ffc0e8a4d1..ad6f8f952b 100644 --- a/sanic/server/protocols/websocket_protocol.py +++ b/sanic/server/protocols/websocket_protocol.py @@ -1,12 +1,11 @@ from typing import TYPE_CHECKING, Optional, Sequence, cast -from warnings import warn from websockets.connection import CLOSED, CLOSING, OPEN from websockets.server import ServerConnection from websockets.typing import Subprotocol from sanic.exceptions import ServerError -from sanic.log import error_logger +from sanic.log import deprecation, error_logger from sanic.server import HttpProtocol from ..websockets.impl import WebsocketImplProtocol @@ -17,6 +16,14 @@ class WebSocketProtocol(HttpProtocol): + __slots__ = ( + "websocket", + "websocket_timeout", + "websocket_max_size", + "websocket_ping_interval", + "websocket_ping_timeout", + ) + def __init__( self, *args, @@ -35,24 +42,24 @@ def __init__( self.websocket_max_size = websocket_max_size if websocket_max_queue is not None and websocket_max_queue > 0: # TODO: Reminder remove this warning in v22.3 - warn( + deprecation( "Websocket no longer uses queueing, so websocket_max_queue" " is no longer required.", - DeprecationWarning, + 22.3, ) if websocket_read_limit is not None and websocket_read_limit > 0: # TODO: Reminder remove this warning in v22.3 - warn( + deprecation( "Websocket no longer uses read buffers, so " "websocket_read_limit is not required.", - DeprecationWarning, + 22.3, ) if websocket_write_limit is not None and websocket_write_limit > 0: # TODO: Reminder remove this warning in v22.3 - warn( + deprecation( "Websocket no longer uses write buffers, so " "websocket_write_limit is not required.", - DeprecationWarning, + 22.3, ) self.websocket_ping_interval = websocket_ping_interval self.websocket_ping_timeout = websocket_ping_timeout diff --git a/sanic/touchup/meta.py b/sanic/touchup/meta.py index 9f60af387f..682600d627 100644 --- a/sanic/touchup/meta.py +++ b/sanic/touchup/meta.py @@ -1,9 +1,10 @@ +from sanic.base.meta import SanicMeta from sanic.exceptions import SanicException from .service import TouchUp -class TouchUpMeta(type): +class TouchUpMeta(SanicMeta): def __new__(cls, name, bases, attrs, **kwargs): gen_class = super().__new__(cls, name, bases, attrs, **kwargs) diff --git a/sanic/views.py b/sanic/views.py index c983bef750..da44f8b73f 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -9,10 +9,7 @@ Optional, Union, ) -from warnings import warn -from sanic.constants import HTTP_METHODS -from sanic.exceptions import InvalidUsage from sanic.models.handler_types import RouteHandler @@ -136,48 +133,3 @@ def attach( def stream(func): func.is_stream = True return func - - -class CompositionView: - """Simple method-function mapped view for the sanic. - You can add handler functions to methods (get, post, put, patch, delete) - for every HTTP method you want to support. - - For example: - - .. code-block:: python - - view = CompositionView() - view.add(['GET'], lambda request: text('I am get method')) - view.add(['POST', 'PUT'], lambda request: text('I am post/put method')) - - If someone tries to use a non-implemented method, there will be a - 405 response. - """ - - def __init__(self): - self.handlers = {} - self.name = self.__class__.__name__ - warn( - "CompositionView has been deprecated and will be removed in " - "v21.12. Please update your view to HTTPMethodView.", - DeprecationWarning, - ) - - def __name__(self): - return self.name - - def add(self, methods, handler, stream=False): - if stream: - handler.is_stream = stream - for method in methods: - if method not in HTTP_METHODS: - raise InvalidUsage(f"{method} is not a valid HTTP method.") - - if method in self.handlers: - raise InvalidUsage(f"Method {method} is already registered.") - self.handlers[method] = handler - - def __call__(self, request, *args, **kwargs): - handler = self.handlers[request.method.upper()] - return handler(request, *args, **kwargs) diff --git a/tests/fake/server.py b/tests/fake/server.py index 43f6d27faf..1220c23e84 100644 --- a/tests/fake/server.py +++ b/tests/fake/server.py @@ -1,5 +1,4 @@ import json -import logging from sanic import Sanic, text from sanic.log import LOGGING_CONFIG_DEFAULTS, logger @@ -9,7 +8,7 @@ LOGGING_CONFIG["formatters"]["generic"]["format"] = "%(message)s" LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG" -app = Sanic(__name__, log_config=LOGGING_CONFIG) +app = Sanic("FakeServer", log_config=LOGGING_CONFIG) @app.get("/") diff --git a/tests/test_app.py b/tests/test_app.py index 6ce8ae65c0..467aaee46c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -8,14 +8,15 @@ from unittest.mock import Mock, patch import pytest + import sanic from sanic import Sanic -from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED +from sanic.compat import OS_IS_WINDOWS from sanic.config import Config from sanic.exceptions import SanicException -from sanic.response import text from sanic.helpers import _default +from sanic.response import text @pytest.fixture(autouse=True) @@ -392,6 +393,22 @@ def test_app_no_registry(): Sanic.get_app("no-register") +def test_app_no_registry_deprecation_message(): + with pytest.warns(DeprecationWarning) as records: + Sanic("no-register", register=False) + Sanic("yes-register", register=True) + + message = ( + "[DEPRECATION v22.6] The register argument is deprecated and will " + "stop working in v22.6. After v22.6 all apps will be added to the " + "Sanic app registry." + ) + + assert len(records) == 2 + for record in records: + assert record.message.args[0] == message + + def test_app_no_registry_env(): environ["SANIC_REGISTER"] = "False" Sanic("no-register") @@ -403,15 +420,12 @@ def test_app_no_registry_env(): def test_app_set_attribute_warning(app): - with pytest.warns(DeprecationWarning) as record: - app.foo = 1 - - assert len(record) == 1 - assert record[0].message.args[0] == ( - "Setting variables on Sanic instances is deprecated " - "and will be removed in version 21.12. You should change your " - "Sanic instance to use instance.ctx.foo instead." + message = ( + "Setting variables on Sanic instances is not allowed. You should " + "change your Sanic instance to use instance.ctx.foo instead." ) + with pytest.raises(AttributeError, match=message): + app.foo = 1 def test_app_set_context(app): @@ -433,15 +447,7 @@ def test_bad_custom_config(): SanicException, match=( "When instantiating Sanic with config, you cannot also pass " - "load_env or env_prefix" - ), - ): - Sanic("test", config=1, load_env=1) - with pytest.raises( - SanicException, - match=( - "When instantiating Sanic with config, you cannot also pass " - "load_env or env_prefix" + "env_prefix" ), ): Sanic("test", config=1, env_prefix=1) @@ -494,11 +500,7 @@ def handler(request): def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch): - apps = ( - Sanic("default-uvloop"), - Sanic("no-uvloop"), - Sanic("yes-uvloop") - ) + apps = (Sanic("default-uvloop"), Sanic("no-uvloop"), Sanic("yes-uvloop")) apps[1].config.USE_UVLOOP = False apps[2].config.USE_UVLOOP = True @@ -512,7 +514,7 @@ def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch): for app in apps: srv_coro = app.create_server( return_asyncio_server=True, - asyncio_server_kwargs=dict(start_serving=False) + asyncio_server_kwargs=dict(start_serving=False), ) loop.run_until_complete(srv_coro) @@ -547,7 +549,7 @@ def test_multiple_uvloop_configs_display_warning(caplog): for app in (default_uvloop, no_uvloop, yes_uvloop): srv_coro = app.create_server( return_asyncio_server=True, - asyncio_server_kwargs=dict(start_serving=False) + asyncio_server_kwargs=dict(start_serving=False), ) srv = loop.run_until_complete(srv_coro) loop.run_until_complete(srv.startup()) diff --git a/tests/test_base.py b/tests/test_base.py index 11d275a144..3230730905 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,6 +1,7 @@ import pytest from sanic import Blueprint, Sanic +from sanic.exceptions import SanicException @pytest.fixture @@ -79,24 +80,18 @@ def test_names_okay(name): ) def test_names_not_okay(name): app_message = ( - f"Sanic instance named '{name}' uses a format that isdeprecated. " - "Starting in version 21.12, Sanic objects must be named only using " - "alphanumeric characters, _, or -." + f"Sanic instance named '{name}' uses an invalid format. Names must " + "begin with a character and may only contain alphanumeric " + "characters, _, or -." ) bp_message = ( - f"Blueprint instance named '{name}' uses a format that isdeprecated. " - "Starting in version 21.12, Blueprint objects must be named only using " - "alphanumeric characters, _, or -." + f"Blueprint instance named '{name}' uses an invalid format. Names " + "must begin with a character and may only contain alphanumeric " + "characters, _, or -." ) - with pytest.warns(DeprecationWarning) as app_e: - app = Sanic(name) + with pytest.raises(SanicException, match=app_message): + Sanic(name) - with pytest.warns(DeprecationWarning) as bp_e: - bp = Blueprint(name) - - assert app.name == name - assert bp.name == name - - assert app_e[0].message.args[0] == app_message - assert bp_e[0].message.args[0] == bp_message + with pytest.raises(SanicException, match=bp_message): + Blueprint(name) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 3aa4487a2b..9c326dede1 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -15,7 +15,6 @@ ) from sanic.request import Request from sanic.response import json, text -from sanic.views import CompositionView # ------------------------------------------------------------ # @@ -833,7 +832,7 @@ def test_static_blueprint_name(static_file_directory, file_name): @pytest.mark.parametrize("file_name", ["test.file"]) def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name): - current_file = inspect.getfile(inspect.currentframe()) + current_file = inspect.getfile(inspect.currentframe()) # type: ignore with open(current_file, "rb") as file: file.read() @@ -862,31 +861,6 @@ def bp_mw1(request): assert triggered is True -def test_route_handler_add(app: Sanic): - view = CompositionView() - - async def get_handler(request): - return json({"response": "OK"}) - - view.add(["GET"], get_handler, stream=False) - - async def default_handler(request): - return text("OK") - - bp = Blueprint(name="handler", url_prefix="/handler") - bp.add_route(default_handler, uri="/default/", strict_slashes=True) - - bp.add_route(view, uri="/view", name="test") - - app.blueprint(bp) - - _, response = app.test_client.get("/handler/default/") - assert response.text == "OK" - - _, response = app.test_client.get("/handler/view") - assert response.json["response"] == "OK" - - def test_websocket_route(app: Sanic): event = asyncio.Event() @@ -1079,15 +1053,12 @@ async def handler(request): def test_bp_set_attribute_warning(): bp = Blueprint("bp") - with pytest.warns(DeprecationWarning) as record: - bp.foo = 1 - - assert len(record) == 1 - assert record[0].message.args[0] == ( - "Setting variables on Blueprint instances is deprecated " - "and will be removed in version 21.12. You should change your " - "Blueprint instance to use instance.ctx.foo instead." + message = ( + "Setting variables on Blueprint instances is not allowed. You should " + "change your Blueprint instance to use instance.ctx.foo instead." ) + with pytest.raises(AttributeError, match=message): + bp.foo = 1 def test_early_registration(app): diff --git a/tests/test_config.py b/tests/test_config.py index 7c5bab05cd..a85bbf3916 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -81,26 +81,6 @@ def test_auto_bool_env_prefix(): del environ["SANIC_TEST_ANSWER"] -def test_dont_load_env(): - environ["SANIC_TEST_ANSWER"] = "42" - app = Sanic(name=__name__, load_env=False) - assert getattr(app.config, "TEST_ANSWER", None) is None - del environ["SANIC_TEST_ANSWER"] - - -@pytest.mark.parametrize("load_env", [None, False, "", "MYAPP_"]) -def test_load_env_deprecation(load_env): - with pytest.warns(DeprecationWarning, match=r"21\.12"): - _ = Sanic(name=__name__, load_env=load_env) - - -def test_load_env_prefix(): - environ["MYAPP_TEST_ANSWER"] = "42" - app = Sanic(name=__name__, load_env="MYAPP_") - assert app.config.TEST_ANSWER == 42 - del environ["MYAPP_TEST_ANSWER"] - - @pytest.mark.parametrize("env_prefix", [None, ""]) def test_empty_load_env_prefix(env_prefix): environ["SANIC_TEST_ANSWER"] = "42" @@ -109,20 +89,6 @@ def test_empty_load_env_prefix(env_prefix): del environ["SANIC_TEST_ANSWER"] -def test_load_env_prefix_float_values(): - environ["MYAPP_TEST_ROI"] = "2.3" - app = Sanic(name=__name__, load_env="MYAPP_") - assert app.config.TEST_ROI == 2.3 - del environ["MYAPP_TEST_ROI"] - - -def test_load_env_prefix_string_value(): - environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken" - app = Sanic(name=__name__, load_env="MYAPP_") - assert app.config.TEST_TOKEN == "somerandomtesttoken" - del environ["MYAPP_TEST_TOKEN"] - - def test_env_prefix(): environ["MYAPP_TEST_ANSWER"] = "42" app = Sanic(name=__name__, env_prefix="MYAPP_") diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py new file mode 100644 index 0000000000..3bf5bd3674 --- /dev/null +++ b/tests/test_deprecation.py @@ -0,0 +1,9 @@ +import pytest + +from sanic.log import deprecation + + +def test_deprecation(): + message = r"\[DEPRECATION v9\.9\] hello" + with pytest.warns(DeprecationWarning, match=message): + deprecation("hello", 9.9) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index cfc0f83252..3ec2959d38 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -13,7 +13,6 @@ SanicException, ServerError, Unauthorized, - abort, ) from sanic.response import text @@ -88,10 +87,6 @@ def handler_401_error(request): def handler_500_error(request): raise SanicException(status_code=500) - @app.route("/old_abort") - def handler_old_abort_error(request): - abort(500) - @app.route("/abort/message") def handler_abort_message(request): raise SanicException(message="Custom Message", status_code=500) @@ -239,11 +234,6 @@ def test_sanic_exception(exception_app): assert response.status == 500 assert "Custom Message" in response.text - with warnings.catch_warnings(record=True) as w: - request, response = exception_app.test_client.get("/old_abort") - assert response.status == 500 - assert len(w) == 1 and "deprecated" in w[0].message.args[0] - def test_custom_exception_default_message(exception_app): class TeaError(SanicException): @@ -262,7 +252,7 @@ def tempest(_): def test_exception_in_ws_logged(caplog): - app = Sanic(__file__) + app = Sanic(__name__) @app.websocket("/feed") async def feed(request, ws): diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index a0de67373a..8be3d28e82 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -226,11 +226,12 @@ def lookup(self, exception): exception_handler_app.error_handler = CustomErrorHandler() message = ( - "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." + "[DEPRECATION v22.3] 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." ) with pytest.warns(DeprecationWarning) as record: _, response = exception_handler_app.test_client.get("/1") diff --git a/tests/test_json_encoding.py b/tests/test_json_encoding.py index ab1858b916..da04ebdb47 100644 --- a/tests/test_json_encoding.py +++ b/tests/test_json_encoding.py @@ -45,7 +45,7 @@ def default_back_to_ujson(): def test_change_encoder(): - Sanic("...", dumps=sdumps) + Sanic("Test", dumps=sdumps) assert BaseHTTPResponse._dumps == sdumps @@ -53,7 +53,7 @@ def test_change_encoder_to_some_custom(): def my_custom_encoder(): return "foo" - Sanic("...", dumps=my_custom_encoder) + Sanic("Test", dumps=my_custom_encoder) assert BaseHTTPResponse._dumps == my_custom_encoder @@ -68,7 +68,7 @@ def test_json_response_ujson(payload): ): json(payload, dumps=sdumps) - Sanic("...", dumps=sdumps) + Sanic("Test", dumps=sdumps) with pytest.raises( TypeError, match="Object of type Foo is not JSON serializable" ): @@ -87,6 +87,6 @@ def test_json_response_json(): response = json(too_big_for_ujson, dumps=sdumps) assert sys.getsizeof(response.body) == 54 - Sanic("...", dumps=sdumps) + Sanic("Test", dumps=sdumps) response = json(too_big_for_ujson) assert sys.getsizeof(response.body) == 54 diff --git a/tests/test_request_cancel.py b/tests/test_request_cancel.py index b5d908829b..8b5de18dc9 100644 --- a/tests/test_request_cancel.py +++ b/tests/test_request_cancel.py @@ -8,13 +8,13 @@ @pytest.mark.asyncio async def test_request_cancel_when_connection_lost(app): - app.still_serving_cancelled_request = False + app.ctx.still_serving_cancelled_request = False @app.get("/") async def handler(request): await asyncio.sleep(1.0) # at this point client is already disconnected - app.still_serving_cancelled_request = True + app.ctx.still_serving_cancelled_request = True return text("OK") # schedule client call @@ -32,12 +32,12 @@ async def handler(request): # Wait for server and check if it's still serving the cancelled request await asyncio.sleep(1.0) - assert app.still_serving_cancelled_request is False + assert app.ctx.still_serving_cancelled_request is False @pytest.mark.asyncio async def test_stream_request_cancel_when_conn_lost(app): - app.still_serving_cancelled_request = False + app.ctx.still_serving_cancelled_request = False @app.post("/post/", stream=True) async def post(request, id): @@ -52,7 +52,7 @@ async def streaming(response): await asyncio.sleep(1.0) # at this point client is already disconnected - app.still_serving_cancelled_request = True + app.ctx.still_serving_cancelled_request = True return stream(streaming) @@ -71,4 +71,4 @@ async def streaming(response): # Wait for server and check if it's still serving the cancelled request await asyncio.sleep(1.0) - assert app.still_serving_cancelled_request is False + assert app.ctx.still_serving_cancelled_request is False diff --git a/tests/test_request_data.py b/tests/test_request_data.py index a1b78e956b..51f2b230ca 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -68,11 +68,11 @@ def test_app_injection(app): @app.listener("after_server_start") async def inject_data(app, loop): - app.injected = expected + app.ctx.injected = expected @app.get("/") async def handler(request): - return json({"injected": request.app.injected}) + return json({"injected": request.app.ctx.injected}) request, response = app.test_client.get("/") diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index dd0b35309c..37cbc04b54 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -8,7 +8,7 @@ from sanic import Sanic from sanic.blueprints import Blueprint from sanic.response import json, text -from sanic.views import CompositionView, HTTPMethodView +from sanic.views import HTTPMethodView from sanic.views import stream as stream_decorator @@ -423,33 +423,6 @@ async def post_add_route(request): assert response.text == data -def test_request_stream_composition_view(app): - def get_handler(request): - return text("OK") - - async def post_handler(request): - result = "" - while True: - body = await request.stream.read() - if body is None: - break - result += body.decode("utf-8") - return text(result) - - view = CompositionView() - view.add(["GET"], get_handler) - view.add(["POST"], post_handler, stream=True) - app.add_route(view, "/composition_view") - - request, response = app.test_client.get("/composition_view") - assert response.status == 200 - assert response.text == "OK" - - request, response = app.test_client.post("/composition_view", data=data) - assert response.status == 200 - assert response.text == data - - def test_request_stream(app): """test for complex application""" bp = Blueprint("test_blueprint_request_stream") @@ -510,14 +483,8 @@ async def post_handler(request): app.add_route(SimpleView.as_view(), "/method_view") - view = CompositionView() - view.add(["GET"], get_handler) - view.add(["POST"], post_handler, stream=True) - app.blueprint(bp) - app.add_route(view, "/composition_view") - request, response = app.test_client.get("/method_view") assert response.status == 200 assert response.text == "OK" @@ -526,14 +493,6 @@ async def post_handler(request): assert response.status == 200 assert response.text == data - request, response = app.test_client.get("/composition_view") - assert response.status == 200 - assert response.text == "OK" - - request, response = app.test_client.post("/composition_view", data=data) - assert response.status == 200 - assert response.text == data - request, response = app.test_client.get("/get") assert response.status == 200 assert response.text == "OK" diff --git a/tests/test_response.py b/tests/test_response.py index 8d301abfee..526f0c733f 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -15,6 +15,8 @@ from pytest import LogCaptureFixture from sanic import Request, Sanic +from sanic.compat import Header +from sanic.cookies import CookieJar from sanic.response import ( HTTPResponse, empty, @@ -277,7 +279,7 @@ def test_non_chunked_streaming_returns_correct_content( assert response.text == "foo,bar" -def test_stream_response_with_cookies(app): +def test_stream_response_with_cookies_legacy(app): @app.route("/") async def test(request: Request): response = stream(sample_streaming_fn, content_type="text/csv") @@ -289,6 +291,25 @@ async def test(request: Request): assert response.cookies["test"] == "pass" +def test_stream_response_with_cookies(app): + @app.route("/") + async def test(request: Request): + headers = Header() + cookies = CookieJar(headers) + cookies["test"] = "modified" + cookies["test"] = "pass" + response = await request.respond( + content_type="text/csv", headers=headers + ) + + await response.send("foo,") + await asyncio.sleep(0.001) + await response.send("bar") + + request, response = app.test_client.get("/") + assert response.cookies["test"] == "pass" + + def test_stream_response_without_cookies(app): @app.route("/") async def test(request: Request): @@ -561,37 +582,37 @@ def test_multiple_responses( message_in_records: Callable[[List[LogRecord], str], bool], ): @app.route("/1") - async def handler(request: Request): + async def handler1(request: Request): response = await request.respond() await response.send("foo") response = await request.respond() @app.route("/2") - async def handler(request: Request): + async def handler2(request: Request): response = await request.respond() response = await request.respond() await response.send("foo") @app.get("/3") - async def handler(request: Request): + async def handler3(request: Request): response = await request.respond() await response.send("foo,") response = await request.respond() await response.send("bar") @app.get("/4") - async def handler(request: Request): + async def handler4(request: Request): response = await request.respond(headers={"one": "one"}) return json({"foo": "bar"}, headers={"one": "two"}) @app.get("/5") - async def handler(request: Request): + async def handler5(request: Request): response = await request.respond(headers={"one": "one"}) await response.send("foo") return json({"foo": "bar"}, headers={"one": "two"}) @app.get("/6") - async def handler(request: Request): + async def handler6(request: Request): response = await request.respond(headers={"one": "one"}) await response.send("foo, ") json_response = json({"foo": "bar"}, headers={"one": "two"}) diff --git a/tests/test_server_events.py b/tests/test_server_events.py index 7ce1859ca3..058a7cf69c 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -101,7 +101,7 @@ class MySanicDb: @app.listener("before_server_start") async def init_db(app, loop): - app.db = MySanicDb() + app.ctx.db = MySanicDb() srv = await app.create_server( debug=True, return_asyncio_server=True, port=PORT @@ -109,8 +109,8 @@ async def init_db(app, loop): await srv.startup() await srv.before_start() - assert hasattr(app, "db") - assert isinstance(app.db, MySanicDb) + assert hasattr(app.ctx, "db") + assert isinstance(app.ctx.db, MySanicDb) @pytest.mark.asyncio @@ -122,9 +122,9 @@ class MySanicDb: @app.listener async def init_db(app, loop): - app.db = MySanicDb() + app.ctx.db = MySanicDb() - assert not hasattr(app, "db") + assert not hasattr(app.ctx, "db") def test_create_server_trigger_events(app): diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 449bfd7f02..a1b98a812c 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -76,8 +76,11 @@ async def test_purge_tasks(app: Sanic): @pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7") -def test_shutdown_tasks_on_app_stop(app: Sanic): - app.shutdown_tasks = Mock() +def test_shutdown_tasks_on_app_stop(): + class TestSanic(Sanic): + shutdown_tasks = Mock() + + app = TestSanic("Test") @app.route("/") async def handler(_): diff --git a/tests/test_unix_socket.py b/tests/test_unix_socket.py index 207141a9f1..c26ebe0644 100644 --- a/tests/test_unix_socket.py +++ b/tests/test_unix_socket.py @@ -15,12 +15,12 @@ from sanic.response import text -httpx_version = tuple( - map(int, httpx.__version__.strip(ascii_lowercase).split(".")) -) pytestmark = pytest.mark.skipif(os.name != "posix", reason="UNIX only") SOCKPATH = "/tmp/sanictest.sock" SOCKPATH2 = "/tmp/sanictest2.sock" +httpx_version = tuple( + map(int, httpx.__version__.strip(ascii_lowercase).split(".")) +) @pytest.fixture(autouse=True) @@ -222,7 +222,10 @@ def spawn(): processes = [spawn()] while not os.path.exists(SOCKPATH): if processes[0].poll() is not None: - raise Exception("Worker did not start properly") + raise Exception( + "Worker did not start properly. " + f"stderr: {processes[0].stderr.read()}" + ) await asyncio.sleep(0.0001) ino = os.stat(SOCKPATH).st_ino task = asyncio.get_event_loop().create_task(client()) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index dbada37fef..396629b301 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -19,7 +19,7 @@ def test_route(app, handler): def test_bp(app, handler): - bp = Blueprint(__file__, version=1) + bp = Blueprint(__name__, version=1) bp.route("/")(handler) app.blueprint(bp) @@ -28,7 +28,7 @@ def test_bp(app, handler): def test_bp_use_route(app, handler): - bp = Blueprint(__file__, version=1) + bp = Blueprint(__name__, version=1) bp.route("/", version=1.1)(handler) app.blueprint(bp) @@ -37,7 +37,7 @@ def test_bp_use_route(app, handler): def test_bp_group(app, handler): - bp = Blueprint(__file__) + bp = Blueprint(__name__) bp.route("/")(handler) group = Blueprint.group(bp, version=1) app.blueprint(group) @@ -47,7 +47,7 @@ def test_bp_group(app, handler): def test_bp_group_use_bp(app, handler): - bp = Blueprint(__file__, version=1.1) + bp = Blueprint(__name__, version=1.1) bp.route("/")(handler) group = Blueprint.group(bp, version=1) app.blueprint(group) @@ -57,7 +57,7 @@ def test_bp_group_use_bp(app, handler): def test_bp_group_use_registration(app, handler): - bp = Blueprint(__file__, version=1.1) + bp = Blueprint(__name__, version=1.1) bp.route("/")(handler) group = Blueprint.group(bp, version=1) app.blueprint(group, version=1.2) @@ -67,7 +67,7 @@ def test_bp_group_use_registration(app, handler): def test_bp_group_use_route(app, handler): - bp = Blueprint(__file__, version=1.1) + bp = Blueprint(__name__, version=1.1) bp.route("/", version=1.3)(handler) group = Blueprint.group(bp, version=1) app.blueprint(group, version=1.2) @@ -84,7 +84,7 @@ def test_version_prefix_route(app, handler): def test_version_prefix_bp(app, handler): - bp = Blueprint(__file__, version=1, version_prefix="/api/v") + bp = Blueprint(__name__, version=1, version_prefix="/api/v") bp.route("/")(handler) app.blueprint(bp) @@ -93,7 +93,7 @@ def test_version_prefix_bp(app, handler): def test_version_prefix_bp_use_route(app, handler): - bp = Blueprint(__file__, version=1, version_prefix="/ignore/v") + bp = Blueprint(__name__, version=1, version_prefix="/ignore/v") bp.route("/", version=1.1, version_prefix="/api/v")(handler) app.blueprint(bp) @@ -102,7 +102,7 @@ def test_version_prefix_bp_use_route(app, handler): def test_version_prefix_bp_group(app, handler): - bp = Blueprint(__file__) + bp = Blueprint(__name__) bp.route("/")(handler) group = Blueprint.group(bp, version=1, version_prefix="/api/v") app.blueprint(group) @@ -112,7 +112,7 @@ def test_version_prefix_bp_group(app, handler): def test_version_prefix_bp_group_use_bp(app, handler): - bp = Blueprint(__file__, version=1.1, version_prefix="/api/v") + bp = Blueprint(__name__, version=1.1, version_prefix="/api/v") bp.route("/")(handler) group = Blueprint.group(bp, version=1, version_prefix="/ignore/v") app.blueprint(group) @@ -122,7 +122,7 @@ def test_version_prefix_bp_group_use_bp(app, handler): def test_version_prefix_bp_group_use_registration(app, handler): - bp = Blueprint(__file__, version=1.1, version_prefix="/alsoignore/v") + bp = Blueprint(__name__, version=1.1, version_prefix="/alsoignore/v") bp.route("/")(handler) group = Blueprint.group(bp, version=1, version_prefix="/ignore/v") app.blueprint(group, version=1.2, version_prefix="/api/v") @@ -132,7 +132,7 @@ def test_version_prefix_bp_group_use_registration(app, handler): def test_version_prefix_bp_group_use_route(app, handler): - bp = Blueprint(__file__, version=1.1, version_prefix="/alsoignore/v") + bp = Blueprint(__name__, version=1.1, version_prefix="/alsoignore/v") bp.route("/", version=1.3, version_prefix="/api/v")(handler) group = Blueprint.group(bp, version=1, version_prefix="/ignore/v") app.blueprint(group, version=1.2, version_prefix="/stillignoring/v") diff --git a/tests/test_views.py b/tests/test_views.py index 23cbd9ce98..dd9eac7b64 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -5,7 +5,7 @@ from sanic.exceptions import InvalidUsage from sanic.request import Request from sanic.response import HTTPResponse, text -from sanic.views import CompositionView, HTTPMethodView +from sanic.views import HTTPMethodView @pytest.mark.parametrize("method", HTTP_METHODS) @@ -225,81 +225,3 @@ def get(self, request): request, response = app.test_client.get("/") assert response.text == "I am get method" assert results[0] == 1 - - -def test_composition_view_rejects_incorrect_methods(): - def foo(request): - return text("Foo") - - view = CompositionView() - - with pytest.raises(InvalidUsage) as e: - view.add(["GET", "FOO"], foo) - - assert str(e.value) == "FOO is not a valid HTTP method." - - -def test_composition_view_rejects_duplicate_methods(): - def foo(request): - return text("Foo") - - view = CompositionView() - - with pytest.raises(InvalidUsage) as e: - view.add(["GET", "POST", "GET"], foo) - - assert str(e.value) == "Method GET is already registered." - - -@pytest.mark.parametrize("method", HTTP_METHODS) -def test_composition_view_runs_methods_as_expected(app, method): - view = CompositionView() - - def first(request): - return text("first method") - - view.add(["GET", "POST", "PUT"], first) - view.add(["DELETE", "PATCH"], lambda x: text("second method")) - - app.add_route(view, "/") - - if method in ["GET", "POST", "PUT"]: - request, response = getattr(app.test_client, method.lower())("/") - assert response.status == 200 - assert response.text == "first method" - - response = view(request) - assert response.body.decode() == "first method" - - if method in ["DELETE", "PATCH"]: - request, response = getattr(app.test_client, method.lower())("/") - assert response.text == "second method" - - response = view(request) - assert response.body.decode() == "second method" - - -@pytest.mark.parametrize("method", HTTP_METHODS) -def test_composition_view_rejects_invalid_methods(app, method): - view = CompositionView() - view.add(["GET", "POST", "PUT"], lambda x: text("first method")) - - app.add_route(view, "/") - - if method in ["GET", "POST", "PUT"]: - request, response = getattr(app.test_client, method.lower())("/") - assert response.status == 200 - assert response.text == "first method" - - if method in ["DELETE", "PATCH"]: - request, response = getattr(app.test_client, method.lower())("/") - assert response.status == 405 - - -def test_composition_view_deprecation(): - message = ( - "CompositionView has been deprecated and will be removed in v21.12. " - "Please update your view to HTTPMethodView." - ) - with pytest.warns(DeprecationWarning, match=message): - CompositionView()