From 01717ee604e1dbe2f9983faf3659c6f1aa7455dc Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 3 Oct 2022 20:32:45 +0200 Subject: [PATCH 01/14] Version 1.0.0 --- docs/deprecation.md | 54 +++++++++++++ docs/release-notes.md | 14 ++++ mkdocs.yml | 1 + starlette/__init__.py | 2 +- starlette/concurrency.py | 18 ----- starlette/exceptions.py | 24 ------ starlette/middleware/base.py | 9 +++ starlette/middleware/wsgi.py | 140 --------------------------------- starlette/routing.py | 15 ---- starlette/status.py | 106 ------------------------- tests/middleware/test_wsgi.py | 144 ---------------------------------- tests/test_concurrency.py | 22 ------ tests/test_exceptions.py | 2 - 13 files changed, 79 insertions(+), 472 deletions(-) create mode 100644 docs/deprecation.md delete mode 100644 starlette/middleware/wsgi.py delete mode 100644 tests/middleware/test_wsgi.py diff --git a/docs/deprecation.md b/docs/deprecation.md new file mode 100644 index 000000000..3edd0941a --- /dev/null +++ b/docs/deprecation.md @@ -0,0 +1,54 @@ +# Deprecation Policy + +The goal of this policy is to reduce the impact of changes on users and developers of the project by providing +clear guidelines and a well-defined process for deprecating functionalities. This policy applies to both features +and API interfaces. + +## Starlette Versions + +Starlette follows [Semantic Versioning](https://semver.org/), with some additional constraints. + +## Deprecation Types + +We'll consider two kinds of deprecations: **Python version** and **feature** deprecations. + +### Python Version Deprecation + +Starlette will aim to support a Python version until the [EOL date of that version](https://endoflife.date/python). +When a Python version reaches EOL, Starlette will drop support for that version in the next **minor** release. + +The drop of Python version support will be documented in the release notes, but the user will **not** be warned it. + +### Feature Deprecation + +Starlette will deprecate a feature in the next **minor** release after the feature is marked as deprecated. + +The deprecation of a feature needs to be followed by a warning message using `warnings.warn` in the code that +uses the deprecated feature. The warning message should include the version in which the feature will be removed. + +The format of the message should follow: + +> *`code` is deprecated and will be removed in version `version`.* + +The `code` can be a *function*, *module* or *feature* name, and the `version` should be the next major release. + +The deprecation warning may include an advice on how to replace the deprecated feature. + +> *Use `alternative` instead.* + +As a full example, imagine we are in version 1.0.0, and we want to deprecate the `potato` function. +We would add the follow warning: + +```python +def potato(): + warnings.warn( + "potato is deprecated and will be removed in version 2.0.0. " + "Use banana instead.", + DeprecationWarning, + ) + +def banana(): + ... +``` + +The deprecation of a feature will be documented in the release notes, and the user will be warned about it. diff --git a/docs/release-notes.md b/docs/release-notes.md index aaf3b7d4c..ed7affdd6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,3 +1,17 @@ +## 1.0.0 + +Wow!!! 1.0.0 is here! 🎉 + +### Removed + +* Removed `WSGIMiddleware`, which is deprecated since `0.19.0`. Please use [`a2wsgi`](https://github.com/abersheeran/a2wsgi) instead. +* Removed `run_until_first_complete`, which is deprecated since `0.19.0`. +* Removed `iscoroutinefunction_or_partial`, which is deprecated since `0.20.1`. + It was an internal function, which we have replaced by `_utils.is_async_callable`. +* Removed `WS_1004_NO_STATUS_RCVD` and `WS_1005_ABNORMAL_CLOSURE` from the `status` module, which were deprecated since `0.19.1`. +* Removed `ExceptionMiddleware` from the `exceptions` module, which was deprecated since `0.19.1`. + The same middleware can be found in the `middleware.exceptions` module. + ## 0.28.0 June 7, 2023 diff --git a/mkdocs.yml b/mkdocs.yml index 1677fe0e9..a2d1a4678 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,6 +47,7 @@ nav: - Test Client: 'testclient.md' - Third Party Packages: 'third-party-packages.md' - Contributing: 'contributing.md' + - Deprecation Policy: 'deprecation.md' - Release Notes: 'release-notes.md' markdown_extensions: diff --git a/starlette/__init__.py b/starlette/__init__.py index df7db8648..5becc17c0 100644 --- a/starlette/__init__.py +++ b/starlette/__init__.py @@ -1 +1 @@ -__version__ = "0.28.0" +__version__ = "1.0.0" diff --git a/starlette/concurrency.py b/starlette/concurrency.py index 5c76cb3df..2905aa570 100644 --- a/starlette/concurrency.py +++ b/starlette/concurrency.py @@ -1,7 +1,6 @@ import functools import sys import typing -import warnings import anyio @@ -15,23 +14,6 @@ P = ParamSpec("P") -async def run_until_first_complete(*args: typing.Tuple[typing.Callable, dict]) -> None: - warnings.warn( - "run_until_first_complete is deprecated " - "and will be removed in a future version.", - DeprecationWarning, - ) - - async with anyio.create_task_group() as task_group: - - async def run(func: typing.Callable[[], typing.Coroutine]) -> None: - await func() - task_group.cancel_scope.cancel() - - for func, kwargs in args: - task_group.start_soon(run, functools.partial(func, **kwargs)) - - async def run_in_threadpool( func: typing.Callable[P, T], *args: P.args, **kwargs: P.kwargs ) -> T: diff --git a/starlette/exceptions.py b/starlette/exceptions.py index 87da73591..d64c75abf 100644 --- a/starlette/exceptions.py +++ b/starlette/exceptions.py @@ -1,8 +1,5 @@ import http import typing -import warnings - -__all__ = ("HTTPException", "WebSocketException") class HTTPException(Exception): @@ -31,24 +28,3 @@ def __init__(self, code: int, reason: typing.Optional[str] = None) -> None: def __repr__(self) -> str: class_name = self.__class__.__name__ return f"{class_name}(code={self.code!r}, reason={self.reason!r})" - - -__deprecated__ = "ExceptionMiddleware" - - -def __getattr__(name: str) -> typing.Any: # pragma: no cover - if name == __deprecated__: - from starlette.middleware.exceptions import ExceptionMiddleware - - warnings.warn( - f"{__deprecated__} is deprecated on `starlette.exceptions`. " - f"Import it from `starlette.middleware.exceptions` instead.", - category=DeprecationWarning, - stacklevel=3, - ) - return ExceptionMiddleware - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -def __dir__() -> typing.List[str]: - return sorted(list(__all__) + [__deprecated__]) # pragma: no cover diff --git a/starlette/middleware/base.py b/starlette/middleware/base.py index 2ff0e047b..4cbf0062e 100644 --- a/starlette/middleware/base.py +++ b/starlette/middleware/base.py @@ -1,4 +1,5 @@ import typing +import warnings import anyio @@ -13,6 +14,14 @@ ] T = typing.TypeVar("T") +warnings.warn( + "The 'BaseHTTPMiddleware' is deprecated, and will be removed in version 2.0.0." + "Refer to https://www.starlette.io/middleware/#pure-asgi-middleware to learn " + "how to create middlewares.\nIf you need help, please create a discussion on: " + "https://github.com/encode/starlette/discussions.", + DeprecationWarning, +) + class _CachedRequest(Request): """ diff --git a/starlette/middleware/wsgi.py b/starlette/middleware/wsgi.py deleted file mode 100644 index 9dbd06528..000000000 --- a/starlette/middleware/wsgi.py +++ /dev/null @@ -1,140 +0,0 @@ -import io -import math -import sys -import typing -import warnings - -import anyio - -from starlette.types import Receive, Scope, Send - -warnings.warn( - "starlette.middleware.wsgi is deprecated and will be removed in a future release. " - "Please refer to https://github.com/abersheeran/a2wsgi as a replacement.", - DeprecationWarning, -) - - -def build_environ(scope: Scope, body: bytes) -> dict: - """ - Builds a scope and request body into a WSGI environ object. - """ - environ = { - "REQUEST_METHOD": scope["method"], - "SCRIPT_NAME": scope.get("root_path", "").encode("utf8").decode("latin1"), - "PATH_INFO": scope["path"].encode("utf8").decode("latin1"), - "QUERY_STRING": scope["query_string"].decode("ascii"), - "SERVER_PROTOCOL": f"HTTP/{scope['http_version']}", - "wsgi.version": (1, 0), - "wsgi.url_scheme": scope.get("scheme", "http"), - "wsgi.input": io.BytesIO(body), - "wsgi.errors": sys.stdout, - "wsgi.multithread": True, - "wsgi.multiprocess": True, - "wsgi.run_once": False, - } - - # Get server name and port - required in WSGI, not in ASGI - server = scope.get("server") or ("localhost", 80) - environ["SERVER_NAME"] = server[0] - environ["SERVER_PORT"] = server[1] - - # Get client IP address - if scope.get("client"): - environ["REMOTE_ADDR"] = scope["client"][0] - - # Go through headers and make them into environ entries - for name, value in scope.get("headers", []): - name = name.decode("latin1") - if name == "content-length": - corrected_name = "CONTENT_LENGTH" - elif name == "content-type": - corrected_name = "CONTENT_TYPE" - else: - corrected_name = f"HTTP_{name}".upper().replace("-", "_") - # HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in - # case - value = value.decode("latin1") - if corrected_name in environ: - value = environ[corrected_name] + "," + value - environ[corrected_name] = value - return environ - - -class WSGIMiddleware: - def __init__(self, app: typing.Callable) -> None: - self.app = app - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - assert scope["type"] == "http" - responder = WSGIResponder(self.app, scope) - await responder(receive, send) - - -class WSGIResponder: - def __init__(self, app: typing.Callable, scope: Scope) -> None: - self.app = app - self.scope = scope - self.status = None - self.response_headers = None - self.stream_send, self.stream_receive = anyio.create_memory_object_stream( - math.inf - ) - self.response_started = False - self.exc_info: typing.Any = None - - async def __call__(self, receive: Receive, send: Send) -> None: - body = b"" - more_body = True - while more_body: - message = await receive() - body += message.get("body", b"") - more_body = message.get("more_body", False) - environ = build_environ(self.scope, body) - - async with anyio.create_task_group() as task_group: - task_group.start_soon(self.sender, send) - async with self.stream_send: - await anyio.to_thread.run_sync(self.wsgi, environ, self.start_response) - if self.exc_info is not None: - raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2]) - - async def sender(self, send: Send) -> None: - async with self.stream_receive: - async for message in self.stream_receive: - await send(message) - - def start_response( - self, - status: str, - response_headers: typing.List[typing.Tuple[str, str]], - exc_info: typing.Any = None, - ) -> None: - self.exc_info = exc_info - if not self.response_started: - self.response_started = True - status_code_string, _ = status.split(" ", 1) - status_code = int(status_code_string) - headers = [ - (name.strip().encode("ascii").lower(), value.strip().encode("ascii")) - for name, value in response_headers - ] - anyio.from_thread.run( - self.stream_send.send, - { - "type": "http.response.start", - "status": status_code, - "headers": headers, - }, - ) - - def wsgi(self, environ: dict, start_response: typing.Callable) -> None: - for chunk in self.app(environ, start_response): - anyio.from_thread.run( - self.stream_send.send, - {"type": "http.response.body", "body": chunk, "more_body": True}, - ) - - anyio.from_thread.run( - self.stream_send.send, {"type": "http.response.body", "body": b""} - ) diff --git a/starlette/routing.py b/starlette/routing.py index 8e01c8562..d01353d36 100644 --- a/starlette/routing.py +++ b/starlette/routing.py @@ -39,21 +39,6 @@ class Match(Enum): FULL = 2 -def iscoroutinefunction_or_partial(obj: typing.Any) -> bool: # pragma: no cover - """ - Correctly determines if an object is a coroutine function, - including those wrapped in functools.partial objects. - """ - warnings.warn( - "iscoroutinefunction_or_partial is deprecated, " - "and will be removed in a future release.", - DeprecationWarning, - ) - while isinstance(obj, functools.partial): - obj = obj.func - return inspect.iscoroutinefunction(obj) - - def request_response(func: typing.Callable) -> ASGIApp: """ Takes a function or coroutine `func(request) -> response`, diff --git a/starlette/status.py b/starlette/status.py index 1689328a4..8d2800774 100644 --- a/starlette/status.py +++ b/starlette/status.py @@ -5,89 +5,6 @@ And RFC 2324 - https://tools.ietf.org/html/rfc2324 """ -import warnings -from typing import List - -__all__ = ( - "HTTP_100_CONTINUE", - "HTTP_101_SWITCHING_PROTOCOLS", - "HTTP_102_PROCESSING", - "HTTP_103_EARLY_HINTS", - "HTTP_200_OK", - "HTTP_201_CREATED", - "HTTP_202_ACCEPTED", - "HTTP_203_NON_AUTHORITATIVE_INFORMATION", - "HTTP_204_NO_CONTENT", - "HTTP_205_RESET_CONTENT", - "HTTP_206_PARTIAL_CONTENT", - "HTTP_207_MULTI_STATUS", - "HTTP_208_ALREADY_REPORTED", - "HTTP_226_IM_USED", - "HTTP_300_MULTIPLE_CHOICES", - "HTTP_301_MOVED_PERMANENTLY", - "HTTP_302_FOUND", - "HTTP_303_SEE_OTHER", - "HTTP_304_NOT_MODIFIED", - "HTTP_305_USE_PROXY", - "HTTP_306_RESERVED", - "HTTP_307_TEMPORARY_REDIRECT", - "HTTP_308_PERMANENT_REDIRECT", - "HTTP_400_BAD_REQUEST", - "HTTP_401_UNAUTHORIZED", - "HTTP_402_PAYMENT_REQUIRED", - "HTTP_403_FORBIDDEN", - "HTTP_404_NOT_FOUND", - "HTTP_405_METHOD_NOT_ALLOWED", - "HTTP_406_NOT_ACCEPTABLE", - "HTTP_407_PROXY_AUTHENTICATION_REQUIRED", - "HTTP_408_REQUEST_TIMEOUT", - "HTTP_409_CONFLICT", - "HTTP_410_GONE", - "HTTP_411_LENGTH_REQUIRED", - "HTTP_412_PRECONDITION_FAILED", - "HTTP_413_REQUEST_ENTITY_TOO_LARGE", - "HTTP_414_REQUEST_URI_TOO_LONG", - "HTTP_415_UNSUPPORTED_MEDIA_TYPE", - "HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE", - "HTTP_417_EXPECTATION_FAILED", - "HTTP_418_IM_A_TEAPOT", - "HTTP_421_MISDIRECTED_REQUEST", - "HTTP_422_UNPROCESSABLE_ENTITY", - "HTTP_423_LOCKED", - "HTTP_424_FAILED_DEPENDENCY", - "HTTP_425_TOO_EARLY", - "HTTP_426_UPGRADE_REQUIRED", - "HTTP_428_PRECONDITION_REQUIRED", - "HTTP_429_TOO_MANY_REQUESTS", - "HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE", - "HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS", - "HTTP_500_INTERNAL_SERVER_ERROR", - "HTTP_501_NOT_IMPLEMENTED", - "HTTP_502_BAD_GATEWAY", - "HTTP_503_SERVICE_UNAVAILABLE", - "HTTP_504_GATEWAY_TIMEOUT", - "HTTP_505_HTTP_VERSION_NOT_SUPPORTED", - "HTTP_506_VARIANT_ALSO_NEGOTIATES", - "HTTP_507_INSUFFICIENT_STORAGE", - "HTTP_508_LOOP_DETECTED", - "HTTP_510_NOT_EXTENDED", - "HTTP_511_NETWORK_AUTHENTICATION_REQUIRED", - "WS_1000_NORMAL_CLOSURE", - "WS_1001_GOING_AWAY", - "WS_1002_PROTOCOL_ERROR", - "WS_1003_UNSUPPORTED_DATA", - "WS_1005_NO_STATUS_RCVD", - "WS_1006_ABNORMAL_CLOSURE", - "WS_1007_INVALID_FRAME_PAYLOAD_DATA", - "WS_1008_POLICY_VIOLATION", - "WS_1009_MESSAGE_TOO_BIG", - "WS_1010_MANDATORY_EXT", - "WS_1011_INTERNAL_ERROR", - "WS_1012_SERVICE_RESTART", - "WS_1013_TRY_AGAIN_LATER", - "WS_1014_BAD_GATEWAY", - "WS_1015_TLS_HANDSHAKE", -) HTTP_100_CONTINUE = 100 HTTP_101_SWITCHING_PROTOCOLS = 101 @@ -174,26 +91,3 @@ WS_1013_TRY_AGAIN_LATER = 1013 WS_1014_BAD_GATEWAY = 1014 WS_1015_TLS_HANDSHAKE = 1015 - - -__deprecated__ = {"WS_1004_NO_STATUS_RCVD": 1004, "WS_1005_ABNORMAL_CLOSURE": 1005} - - -def __getattr__(name: str) -> int: - deprecation_changes = { - "WS_1004_NO_STATUS_RCVD": "WS_1005_NO_STATUS_RCVD", - "WS_1005_ABNORMAL_CLOSURE": "WS_1006_ABNORMAL_CLOSURE", - } - deprecated = __deprecated__.get(name) - if deprecated: - warnings.warn( - f"'{name}' is deprecated. Use '{deprecation_changes[name]}' instead.", - category=DeprecationWarning, - stacklevel=3, - ) - return deprecated - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -def __dir__() -> List[str]: - return sorted(list(__all__) + list(__deprecated__.keys())) # pragma: no cover diff --git a/tests/middleware/test_wsgi.py b/tests/middleware/test_wsgi.py deleted file mode 100644 index bcb4cd6ff..000000000 --- a/tests/middleware/test_wsgi.py +++ /dev/null @@ -1,144 +0,0 @@ -import sys - -import pytest - -from starlette.middleware.wsgi import WSGIMiddleware, build_environ - - -def hello_world(environ, start_response): - status = "200 OK" - output = b"Hello World!\n" - headers = [ - ("Content-Type", "text/plain; charset=utf-8"), - ("Content-Length", str(len(output))), - ] - start_response(status, headers) - return [output] - - -def echo_body(environ, start_response): - status = "200 OK" - output = environ["wsgi.input"].read() - headers = [ - ("Content-Type", "text/plain; charset=utf-8"), - ("Content-Length", str(len(output))), - ] - start_response(status, headers) - return [output] - - -def raise_exception(environ, start_response): - raise RuntimeError("Something went wrong") - - -def return_exc_info(environ, start_response): - try: - raise RuntimeError("Something went wrong") - except RuntimeError: - status = "500 Internal Server Error" - output = b"Internal Server Error" - headers = [ - ("Content-Type", "text/plain; charset=utf-8"), - ("Content-Length", str(len(output))), - ] - start_response(status, headers, exc_info=sys.exc_info()) - return [output] - - -def test_wsgi_get(test_client_factory): - app = WSGIMiddleware(hello_world) - client = test_client_factory(app) - response = client.get("/") - assert response.status_code == 200 - assert response.text == "Hello World!\n" - - -def test_wsgi_post(test_client_factory): - app = WSGIMiddleware(echo_body) - client = test_client_factory(app) - response = client.post("/", json={"example": 123}) - assert response.status_code == 200 - assert response.text == '{"example": 123}' - - -def test_wsgi_exception(test_client_factory): - # Note that we're testing the WSGI app directly here. - # The HTTP protocol implementations would catch this error and return 500. - app = WSGIMiddleware(raise_exception) - client = test_client_factory(app) - with pytest.raises(RuntimeError): - client.get("/") - - -def test_wsgi_exc_info(test_client_factory): - # Note that we're testing the WSGI app directly here. - # The HTTP protocol implementations would catch this error and return 500. - app = WSGIMiddleware(return_exc_info) - client = test_client_factory(app) - with pytest.raises(RuntimeError): - response = client.get("/") - - app = WSGIMiddleware(return_exc_info) - client = test_client_factory(app, raise_server_exceptions=False) - response = client.get("/") - assert response.status_code == 500 - assert response.text == "Internal Server Error" - - -def test_build_environ(): - scope = { - "type": "http", - "http_version": "1.1", - "method": "GET", - "scheme": "https", - "path": "/", - "query_string": b"a=123&b=456", - "headers": [ - (b"host", b"www.example.org"), - (b"content-type", b"application/json"), - (b"content-length", b"18"), - (b"accept", b"application/json"), - (b"accept", b"text/plain"), - ], - "client": ("134.56.78.4", 1453), - "server": ("www.example.org", 443), - } - body = b'{"example":"body"}' - environ = build_environ(scope, body) - stream = environ.pop("wsgi.input") - assert stream.read() == b'{"example":"body"}' - assert environ == { - "CONTENT_LENGTH": "18", - "CONTENT_TYPE": "application/json", - "HTTP_ACCEPT": "application/json,text/plain", - "HTTP_HOST": "www.example.org", - "PATH_INFO": "/", - "QUERY_STRING": "a=123&b=456", - "REMOTE_ADDR": "134.56.78.4", - "REQUEST_METHOD": "GET", - "SCRIPT_NAME": "", - "SERVER_NAME": "www.example.org", - "SERVER_PORT": 443, - "SERVER_PROTOCOL": "HTTP/1.1", - "wsgi.errors": sys.stdout, - "wsgi.multiprocess": True, - "wsgi.multithread": True, - "wsgi.run_once": False, - "wsgi.url_scheme": "https", - "wsgi.version": (1, 0), - } - - -def test_build_environ_encoding() -> None: - scope = { - "type": "http", - "http_version": "1.1", - "method": "GET", - "path": "/小星", - "root_path": "/中国", - "query_string": b"a=123&b=456", - "headers": [], - } - environ = build_environ(scope, b"") - assert environ["SCRIPT_NAME"] == "/中国".encode().decode("latin-1") - assert environ["PATH_INFO"] == "/小星".encode().decode("latin-1") diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 22b9da0e8..6ac1b39b2 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -1,33 +1,11 @@ from contextvars import ContextVar -import anyio -import pytest - from starlette.applications import Starlette -from starlette.concurrency import run_until_first_complete from starlette.requests import Request from starlette.responses import Response from starlette.routing import Route -@pytest.mark.anyio -async def test_run_until_first_complete(): - task1_finished = anyio.Event() - task2_finished = anyio.Event() - - async def task1(): - task1_finished.set() - - async def task2(): - await task1_finished.wait() - await anyio.sleep(0) # pragma: nocover - task2_finished.set() # pragma: nocover - - await run_until_first_complete((task1, {}), (task2, {})) - assert task1_finished.is_set() - assert not task2_finished.is_set() - - def test_accessing_context_from_threaded_sync_endpoint(test_client_factory) -> None: ctxvar: ContextVar[bytes] = ContextVar("ctxvar") ctxvar.set(b"data") diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 2f2b89167..fbc87fe72 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,5 +1,3 @@ -import warnings - import pytest from starlette.exceptions import HTTPException, WebSocketException From 1555f17a005d9b66eecf5e45222f4f514a076447 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 3 Oct 2022 21:00:33 +0200 Subject: [PATCH 02/14] Improve wording around the deprecation policy --- docs/deprecation.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/deprecation.md b/docs/deprecation.md index 3edd0941a..2b16cd6f5 100644 --- a/docs/deprecation.md +++ b/docs/deprecation.md @@ -4,24 +4,29 @@ The goal of this policy is to reduce the impact of changes on users and develope clear guidelines and a well-defined process for deprecating functionalities. This policy applies to both features and API interfaces. +!!! info "Terminology" + **Deprecation** and **removal** of a feature have different meanings. **Deprecation** refers to the process of + communicating to users that a feature will be removed in the future. **Removal** refers to the actual + deletion of the feature from the codebase. + ## Starlette Versions -Starlette follows [Semantic Versioning](https://semver.org/), with some additional constraints. +Starlette follows [Semantic Versioning](https://semver.org/). -## Deprecation Types +## Process to Remove a Feature -We'll consider two kinds of deprecations: **Python version** and **feature** deprecations. +We'll consider two kinds of processes: **drop Python version support** and **feature removal**. -### Python Version Deprecation +### Python Version Starlette will aim to support a Python version until the [EOL date of that version](https://endoflife.date/python). When a Python version reaches EOL, Starlette will drop support for that version in the next **minor** release. -The drop of Python version support will be documented in the release notes, but the user will **not** be warned it. +The drop of Python version support will be documented in the release notes, but the user will **not** be warned it in code. -### Feature Deprecation +### Feature Removal -Starlette will deprecate a feature in the next **minor** release after the feature is marked as deprecated. +The first step to remove a feature is to **deprecate** it, which is done in **major** releases. The deprecation of a feature needs to be followed by a warning message using `warnings.warn` in the code that uses the deprecated feature. The warning message should include the version in which the feature will be removed. @@ -52,3 +57,5 @@ def banana(): ``` The deprecation of a feature will be documented in the release notes, and the user will be warned about it. + +Also, in the above example, on version 2.0.0, the `potato` function will be removed from the codebase. From b584057fbf775f4f136129223e5be76a0c906103 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 3 Oct 2022 21:01:47 +0200 Subject: [PATCH 03/14] Ignore BaseHTTPMiddleware deprecation warning --- starlette/middleware/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starlette/middleware/base.py b/starlette/middleware/base.py index 4cbf0062e..8f9b47c14 100644 --- a/starlette/middleware/base.py +++ b/starlette/middleware/base.py @@ -15,7 +15,7 @@ T = typing.TypeVar("T") warnings.warn( - "The 'BaseHTTPMiddleware' is deprecated, and will be removed in version 2.0.0." + "The 'BaseHTTPMiddleware' is deprecated, and will be removed in version 2.0.0. " "Refer to https://www.starlette.io/middleware/#pure-asgi-middleware to learn " "how to create middlewares.\nIf you need help, please create a discussion on: " "https://github.com/encode/starlette/discussions.", From c7a37b387abc2306bf2820470b61428ca3ad972f Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 3 Oct 2022 21:04:26 +0200 Subject: [PATCH 04/14] Remove `test_status.py` file --- tests/test_status.py | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 tests/test_status.py diff --git a/tests/test_status.py b/tests/test_status.py deleted file mode 100644 index 04719e87e..000000000 --- a/tests/test_status.py +++ /dev/null @@ -1,25 +0,0 @@ -import importlib - -import pytest - - -@pytest.mark.parametrize( - "constant,msg", - ( - ( - "WS_1004_NO_STATUS_RCVD", - "'WS_1004_NO_STATUS_RCVD' is deprecated. " - "Use 'WS_1005_NO_STATUS_RCVD' instead.", - ), - ( - "WS_1005_ABNORMAL_CLOSURE", - "'WS_1005_ABNORMAL_CLOSURE' is deprecated. " - "Use 'WS_1006_ABNORMAL_CLOSURE' instead.", - ), - ), -) -def test_deprecated_types(constant: str, msg: str) -> None: - with pytest.warns(DeprecationWarning) as record: - getattr(importlib.import_module("starlette.status"), constant) - assert len(record) == 1 - assert msg in str(record.list[0]) From 856fb5618dc14711774f4d68b595ce86c01eb23b Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 5 Oct 2022 22:34:37 +0200 Subject: [PATCH 05/14] Replace deprecation by removal --- starlette/applications.py | 128 ------------------------ starlette/middleware/base.py | 8 -- starlette/routing.py | 112 --------------------- starlette/testclient.py | 88 ++++++---------- tests/middleware/test_https_redirect.py | 8 +- tests/test_responses.py | 2 +- tests/test_routing.py | 18 ---- tests/test_staticfiles.py | 25 ----- 8 files changed, 37 insertions(+), 352 deletions(-) diff --git a/starlette/applications.py b/starlette/applications.py index 5fc11f955..3cecd5296 100644 --- a/starlette/applications.py +++ b/starlette/applications.py @@ -3,7 +3,6 @@ from starlette.datastructures import State, URLPath from starlette.middleware import Middleware -from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.errors import ServerErrorMiddleware from starlette.middleware.exceptions import ExceptionMiddleware from starlette.requests import Request @@ -121,19 +120,6 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: self.middleware_stack = self.build_middleware_stack() await self.middleware_stack(scope, receive, send) - def on_event(self, event_type: str) -> typing.Callable: # pragma: nocover - return self.router.on_event(event_type) - - def mount( - self, path: str, app: ASGIApp, name: typing.Optional[str] = None - ) -> None: # pragma: nocover - self.router.mount(path, app=app, name=name) - - def host( - self, host: str, app: ASGIApp, name: typing.Optional[str] = None - ) -> None: # pragma: no cover - self.router.host(host, app=app, name=name) - def add_middleware(self, middleware_class: type, **options: typing.Any) -> None: if self.middleware_stack is not None: # pragma: no cover raise RuntimeError("Cannot add middleware after an application has started") @@ -145,117 +131,3 @@ def add_exception_handler( handler: typing.Callable, ) -> None: # pragma: no cover self.exception_handlers[exc_class_or_status_code] = handler - - def add_event_handler( - self, event_type: str, func: typing.Callable - ) -> None: # pragma: no cover - self.router.add_event_handler(event_type, func) - - def add_route( - self, - path: str, - route: typing.Callable, - methods: typing.Optional[typing.List[str]] = None, - name: typing.Optional[str] = None, - include_in_schema: bool = True, - ) -> None: # pragma: no cover - self.router.add_route( - path, route, methods=methods, name=name, include_in_schema=include_in_schema - ) - - def add_websocket_route( - self, path: str, route: typing.Callable, name: typing.Optional[str] = None - ) -> None: # pragma: no cover - self.router.add_websocket_route(path, route, name=name) - - def exception_handler( - self, exc_class_or_status_code: typing.Union[int, typing.Type[Exception]] - ) -> typing.Callable: - warnings.warn( - "The `exception_handler` decorator is deprecated, and will be removed in version 1.0.0. " # noqa: E501 - "Refer to https://www.starlette.io/exceptions/ for the recommended approach.", # noqa: E501 - DeprecationWarning, - ) - - def decorator(func: typing.Callable) -> typing.Callable: - self.add_exception_handler(exc_class_or_status_code, func) - return func - - return decorator - - def route( - self, - path: str, - methods: typing.Optional[typing.List[str]] = None, - name: typing.Optional[str] = None, - include_in_schema: bool = True, - ) -> typing.Callable: - """ - We no longer document this decorator style API, and its usage is discouraged. - Instead you should use the following approach: - - >>> routes = [Route(path, endpoint=...), ...] - >>> app = Starlette(routes=routes) - """ - warnings.warn( - "The `route` decorator is deprecated, and will be removed in version 1.0.0. " # noqa: E501 - "Refer to https://www.starlette.io/routing/ for the recommended approach.", # noqa: E501 - DeprecationWarning, - ) - - def decorator(func: typing.Callable) -> typing.Callable: - self.router.add_route( - path, - func, - methods=methods, - name=name, - include_in_schema=include_in_schema, - ) - return func - - return decorator - - def websocket_route( - self, path: str, name: typing.Optional[str] = None - ) -> typing.Callable: - """ - We no longer document this decorator style API, and its usage is discouraged. - Instead you should use the following approach: - - >>> routes = [WebSocketRoute(path, endpoint=...), ...] - >>> app = Starlette(routes=routes) - """ - warnings.warn( - "The `websocket_route` decorator is deprecated, and will be removed in version 1.0.0. " # noqa: E501 - "Refer to https://www.starlette.io/routing/#websocket-routing for the recommended approach.", # noqa: E501 - DeprecationWarning, - ) - - def decorator(func: typing.Callable) -> typing.Callable: - self.router.add_websocket_route(path, func, name=name) - return func - - return decorator - - def middleware(self, middleware_type: str) -> typing.Callable: - """ - We no longer document this decorator style API, and its usage is discouraged. - Instead you should use the following approach: - - >>> middleware = [Middleware(...), ...] - >>> app = Starlette(middleware=middleware) - """ - warnings.warn( - "The `middleware` decorator is deprecated, and will be removed in version 1.0.0. " # noqa: E501 - "Refer to https://www.starlette.io/middleware/#using-middleware for recommended approach.", # noqa: E501 - DeprecationWarning, - ) - assert ( - middleware_type == "http" - ), 'Currently only middleware("http") is supported.' - - def decorator(func: typing.Callable) -> typing.Callable: - self.add_middleware(BaseHTTPMiddleware, dispatch=func) - return func - - return decorator diff --git a/starlette/middleware/base.py b/starlette/middleware/base.py index 8f9b47c14..d6629a8b5 100644 --- a/starlette/middleware/base.py +++ b/starlette/middleware/base.py @@ -14,14 +14,6 @@ ] T = typing.TypeVar("T") -warnings.warn( - "The 'BaseHTTPMiddleware' is deprecated, and will be removed in version 2.0.0. " - "Refer to https://www.starlette.io/middleware/#pure-asgi-middleware to learn " - "how to create middlewares.\nIf you need help, please create a discussion on: " - "https://github.com/encode/starlette/discussions.", - DeprecationWarning, -) - class _CachedRequest(Request): """ diff --git a/starlette/routing.py b/starlette/routing.py index d01353d36..c55ebf705 100644 --- a/starlette/routing.py +++ b/starlette/routing.py @@ -742,115 +742,3 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: def __eq__(self, other: typing.Any) -> bool: return isinstance(other, Router) and self.routes == other.routes - - def mount( - self, path: str, app: ASGIApp, name: typing.Optional[str] = None - ) -> None: # pragma: nocover - route = Mount(path, app=app, name=name) - self.routes.append(route) - - def host( - self, host: str, app: ASGIApp, name: typing.Optional[str] = None - ) -> None: # pragma: no cover - route = Host(host, app=app, name=name) - self.routes.append(route) - - def add_route( - self, - path: str, - endpoint: typing.Callable, - methods: typing.Optional[typing.List[str]] = None, - name: typing.Optional[str] = None, - include_in_schema: bool = True, - ) -> None: # pragma: nocover - route = Route( - path, - endpoint=endpoint, - methods=methods, - name=name, - include_in_schema=include_in_schema, - ) - self.routes.append(route) - - def add_websocket_route( - self, path: str, endpoint: typing.Callable, name: typing.Optional[str] = None - ) -> None: # pragma: no cover - route = WebSocketRoute(path, endpoint=endpoint, name=name) - self.routes.append(route) - - def route( - self, - path: str, - methods: typing.Optional[typing.List[str]] = None, - name: typing.Optional[str] = None, - include_in_schema: bool = True, - ) -> typing.Callable: - """ - We no longer document this decorator style API, and its usage is discouraged. - Instead you should use the following approach: - - >>> routes = [Route(path, endpoint=...), ...] - >>> app = Starlette(routes=routes) - """ - warnings.warn( - "The `route` decorator is deprecated, and will be removed in version 1.0.0." - "Refer to https://www.starlette.io/routing/#http-routing for the recommended approach.", # noqa: E501 - DeprecationWarning, - ) - - def decorator(func: typing.Callable) -> typing.Callable: - self.add_route( - path, - func, - methods=methods, - name=name, - include_in_schema=include_in_schema, - ) - return func - - return decorator - - def websocket_route( - self, path: str, name: typing.Optional[str] = None - ) -> typing.Callable: - """ - We no longer document this decorator style API, and its usage is discouraged. - Instead you should use the following approach: - - >>> routes = [WebSocketRoute(path, endpoint=...), ...] - >>> app = Starlette(routes=routes) - """ - warnings.warn( - "The `websocket_route` decorator is deprecated, and will be removed in version 1.0.0. Refer to " # noqa: E501 - "https://www.starlette.io/routing/#websocket-routing for the recommended approach.", # noqa: E501 - DeprecationWarning, - ) - - def decorator(func: typing.Callable) -> typing.Callable: - self.add_websocket_route(path, func, name=name) - return func - - return decorator - - def add_event_handler( - self, event_type: str, func: typing.Callable - ) -> None: # pragma: no cover - assert event_type in ("startup", "shutdown") - - if event_type == "startup": - self.on_startup.append(func) - else: - self.on_shutdown.append(func) - - def on_event(self, event_type: str) -> typing.Callable: - warnings.warn( - "The `on_event` decorator is deprecated, and will be removed in version 1.0.0. " # noqa: E501 - "Refer to https://www.starlette.io/lifespan/ for recommended approach.", - DeprecationWarning, - ) - - def decorator(func: typing.Callable) -> typing.Callable: - self.add_event_handler(event_type, func) - return func - - return decorator diff --git a/starlette/testclient.py b/starlette/testclient.py index 7d4f3e396..7b8005e20 100644 --- a/starlette/testclient.py +++ b/starlette/testclient.py @@ -6,7 +6,6 @@ import queue import sys import typing -import warnings from concurrent.futures import Future from types import GeneratorType from urllib.parse import unquote, urljoin @@ -423,29 +422,6 @@ def _portal_factory(self) -> typing.Generator[anyio.abc.BlockingPortal, None, No ) as portal: yield portal - def _choose_redirect_arg( - self, - follow_redirects: typing.Optional[bool], - allow_redirects: typing.Optional[bool], - ) -> typing.Union[bool, httpx._client.UseClientDefault]: - redirect: typing.Union[ - bool, httpx._client.UseClientDefault - ] = httpx._client.USE_CLIENT_DEFAULT - if allow_redirects is not None: - message = ( - "The `allow_redirects` argument is deprecated. " - "Use `follow_redirects` instead." - ) - warnings.warn(message, DeprecationWarning) - redirect = allow_redirects - if follow_redirects is not None: - redirect = follow_redirects - elif allow_redirects is not None and follow_redirects is not None: - raise RuntimeError( # pragma: no cover - "Cannot use both `allow_redirects` and `follow_redirects`." - ) - return redirect - def request( # type: ignore[override] self, method: str, @@ -461,15 +437,15 @@ def request( # type: ignore[override] auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: typing.Optional[bool] = None, - allow_redirects: typing.Optional[bool] = None, + follow_redirects: typing.Union[ + bool, httpx._client.UseClientDefault + ] = httpx._client.USE_CLIENT_DEFAULT, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: url = self.base_url.join(url) - redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().request( method, url, @@ -481,7 +457,7 @@ def request( # type: ignore[override] headers=headers, cookies=cookies, auth=auth, - follow_redirects=redirect, + follow_redirects=follow_redirects, timeout=timeout, extensions=extensions, ) @@ -496,21 +472,21 @@ def get( # type: ignore[override] auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: typing.Optional[bool] = None, - allow_redirects: typing.Optional[bool] = None, + follow_redirects: typing.Union[ + bool, httpx._client.UseClientDefault + ] = httpx._client.USE_CLIENT_DEFAULT, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: - redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().get( url, params=params, headers=headers, cookies=cookies, auth=auth, - follow_redirects=redirect, + follow_redirects=follow_redirects, timeout=timeout, extensions=extensions, ) @@ -525,21 +501,21 @@ def options( # type: ignore[override] auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: typing.Optional[bool] = None, - allow_redirects: typing.Optional[bool] = None, + follow_redirects: typing.Union[ + bool, httpx._client.UseClientDefault + ] = httpx._client.USE_CLIENT_DEFAULT, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: - redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().options( url, params=params, headers=headers, cookies=cookies, auth=auth, - follow_redirects=redirect, + follow_redirects=follow_redirects, timeout=timeout, extensions=extensions, ) @@ -554,21 +530,21 @@ def head( # type: ignore[override] auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: typing.Optional[bool] = None, - allow_redirects: typing.Optional[bool] = None, + follow_redirects: typing.Union[ + bool, httpx._client.UseClientDefault + ] = httpx._client.USE_CLIENT_DEFAULT, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: - redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().head( url, params=params, headers=headers, cookies=cookies, auth=auth, - follow_redirects=redirect, + follow_redirects=follow_redirects, timeout=timeout, extensions=extensions, ) @@ -587,14 +563,14 @@ def post( # type: ignore[override] auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: typing.Optional[bool] = None, - allow_redirects: typing.Optional[bool] = None, + follow_redirects: typing.Union[ + bool, httpx._client.UseClientDefault + ] = httpx._client.USE_CLIENT_DEFAULT, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: - redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().post( url, content=content, @@ -605,7 +581,7 @@ def post( # type: ignore[override] headers=headers, cookies=cookies, auth=auth, - follow_redirects=redirect, + follow_redirects=follow_redirects, timeout=timeout, extensions=extensions, ) @@ -624,14 +600,14 @@ def put( # type: ignore[override] auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: typing.Optional[bool] = None, - allow_redirects: typing.Optional[bool] = None, + follow_redirects: typing.Union[ + bool, httpx._client.UseClientDefault + ] = httpx._client.USE_CLIENT_DEFAULT, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: - redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().put( url, content=content, @@ -642,7 +618,7 @@ def put( # type: ignore[override] headers=headers, cookies=cookies, auth=auth, - follow_redirects=redirect, + follow_redirects=follow_redirects, timeout=timeout, extensions=extensions, ) @@ -661,14 +637,14 @@ def patch( # type: ignore[override] auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: typing.Optional[bool] = None, - allow_redirects: typing.Optional[bool] = None, + follow_redirects: typing.Union[ + bool, httpx._client.UseClientDefault + ] = httpx._client.USE_CLIENT_DEFAULT, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: - redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().patch( url, content=content, @@ -679,7 +655,7 @@ def patch( # type: ignore[override] headers=headers, cookies=cookies, auth=auth, - follow_redirects=redirect, + follow_redirects=follow_redirects, timeout=timeout, extensions=extensions, ) @@ -694,21 +670,21 @@ def delete( # type: ignore[override] auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: typing.Optional[bool] = None, - allow_redirects: typing.Optional[bool] = None, + follow_redirects: typing.Union[ + bool, httpx._client.UseClientDefault + ] = httpx._client.USE_CLIENT_DEFAULT, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: - redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().delete( url, params=params, headers=headers, cookies=cookies, auth=auth, - follow_redirects=redirect, + follow_redirects=follow_redirects, timeout=timeout, extensions=extensions, ) diff --git a/tests/middleware/test_https_redirect.py b/tests/middleware/test_https_redirect.py index 5e498c146..962b9c1c1 100644 --- a/tests/middleware/test_https_redirect.py +++ b/tests/middleware/test_https_redirect.py @@ -19,21 +19,21 @@ def homepage(request): assert response.status_code == 200 client = test_client_factory(app) - response = client.get("/", allow_redirects=False) + response = client.get("/", follow_redirects=False) assert response.status_code == 307 assert response.headers["location"] == "https://testserver/" client = test_client_factory(app, base_url="http://testserver:80") - response = client.get("/", allow_redirects=False) + response = client.get("/", follow_redirects=False) assert response.status_code == 307 assert response.headers["location"] == "https://testserver/" client = test_client_factory(app, base_url="http://testserver:443") - response = client.get("/", allow_redirects=False) + response = client.get("/", follow_redirects=False) assert response.status_code == 307 assert response.headers["location"] == "https://testserver/" client = test_client_factory(app, base_url="http://testserver:123") - response = client.get("/", allow_redirects=False) + response = client.get("/", follow_redirects=False) assert response.status_code == 307 assert response.headers["location"] == "https://testserver:123/" diff --git a/tests/test_responses.py b/tests/test_responses.py index 284bda1ef..b9f7079b0 100644 --- a/tests/test_responses.py +++ b/tests/test_responses.py @@ -87,7 +87,7 @@ async def app(scope, receive, send): await response(scope, receive, send) client: TestClient = test_client_factory(app) - response = client.request("GET", "/redirect", allow_redirects=False) + response = client.request("GET", "/redirect", follow_redirects=False) assert response.url == "http://testserver/redirect" assert response.headers["content-length"] == "0" diff --git a/tests/test_routing.py b/tests/test_routing.py index 129293224..001c00d1b 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -952,24 +952,6 @@ def test_mount_asgi_app_with_middleware_url_path_for() -> None: mounted_app_with_middleware.url_path_for("route") -def test_add_route_to_app_after_mount( - test_client_factory: typing.Callable[..., TestClient], -) -> None: - """Checks that Mount will pick up routes - added to the underlying app after it is mounted - """ - inner_app = Router() - app = Mount("/http", app=inner_app) - inner_app.add_route( - "/inner", - endpoint=homepage, - methods=["GET"], - ) - client = test_client_factory(app) - response = client.get("/http/inner") - assert response.status_code == 200 - - def test_exception_on_mounted_apps(test_client_factory): def exc(request): raise Exception("Exc") diff --git a/tests/test_staticfiles.py b/tests/test_staticfiles.py index 23d0b57fc..ece3356ac 100644 --- a/tests/test_staticfiles.py +++ b/tests/test_staticfiles.py @@ -9,9 +9,6 @@ from starlette.applications import Starlette from starlette.exceptions import HTTPException -from starlette.middleware import Middleware -from starlette.middleware.base import BaseHTTPMiddleware -from starlette.requests import Request from starlette.routing import Mount from starlette.staticfiles import StaticFiles @@ -40,28 +37,6 @@ def test_staticfiles_with_pathlib(tmp_path: Path, test_client_factory): assert response.text == "" -def test_staticfiles_head_with_middleware(tmpdir, test_client_factory): - """ - see https://github.com/encode/starlette/pull/935 - """ - path = os.path.join(tmpdir, "example.txt") - with open(path, "w") as file: - file.write("x" * 100) - - async def does_nothing_middleware(request: Request, call_next): - response = await call_next(request) - return response - - routes = [Mount("/static", app=StaticFiles(directory=tmpdir), name="static")] - middleware = [Middleware(BaseHTTPMiddleware, dispatch=does_nothing_middleware)] - app = Starlette(routes=routes, middleware=middleware) - - client = test_client_factory(app) - response = client.head("/static/example.txt") - assert response.status_code == 200 - assert response.headers.get("content-length") == "100" - - def test_staticfiles_with_package(test_client_factory): app = StaticFiles(packages=["tests"]) client = test_client_factory(app) From c9eeec599b5af37b40bd7b02dfaeb6ff36897e53 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 12 Oct 2022 10:02:19 +0200 Subject: [PATCH 06/14] Update removal of decorators --- starlette/applications.py | 1 + starlette/routing.py | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/starlette/applications.py b/starlette/applications.py index 3cecd5296..9dfa205e3 100644 --- a/starlette/applications.py +++ b/starlette/applications.py @@ -3,6 +3,7 @@ from starlette.datastructures import State, URLPath from starlette.middleware import Middleware +from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.errors import ServerErrorMiddleware from starlette.middleware.exceptions import ExceptionMiddleware from starlette.requests import Request diff --git a/starlette/routing.py b/starlette/routing.py index c55ebf705..ad78994a7 100644 --- a/starlette/routing.py +++ b/starlette/routing.py @@ -742,3 +742,48 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: def __eq__(self, other: typing.Any) -> bool: return isinstance(other, Router) and self.routes == other.routes + + def mount( + self, path: str, app: ASGIApp, name: typing.Optional[str] = None + ) -> None: # pragma: nocover + route = Mount(path, app=app, name=name) + self.routes.append(route) + + def host( + self, host: str, app: ASGIApp, name: typing.Optional[str] = None + ) -> None: # pragma: no cover + route = Host(host, app=app, name=name) + self.routes.append(route) + + def add_route( + self, + path: str, + endpoint: typing.Callable, + methods: typing.Optional[typing.List[str]] = None, + name: typing.Optional[str] = None, + include_in_schema: bool = True, + ) -> None: # pragma: nocover + route = Route( + path, + endpoint=endpoint, + methods=methods, + name=name, + include_in_schema=include_in_schema, + ) + self.routes.append(route) + + def add_websocket_route( + self, path: str, endpoint: typing.Callable, name: typing.Optional[str] = None + ) -> None: # pragma: no cover + route = WebSocketRoute(path, endpoint=endpoint, name=name) + self.routes.append(route) + + def add_event_handler( + self, event_type: str, func: typing.Callable + ) -> None: # pragma: no cover + assert event_type in ("startup", "shutdown") + + if event_type == "startup": + self.on_startup.append(func) + else: + self.on_shutdown.append(func) From 8d9f2fbde4df4078de8f8cf2e57cd73b94a640db Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 12 Oct 2022 10:12:33 +0200 Subject: [PATCH 07/14] Remove BaseHTTPMiddleware import on applications.py --- starlette/applications.py | 1 - 1 file changed, 1 deletion(-) diff --git a/starlette/applications.py b/starlette/applications.py index 9dfa205e3..3cecd5296 100644 --- a/starlette/applications.py +++ b/starlette/applications.py @@ -3,7 +3,6 @@ from starlette.datastructures import State, URLPath from starlette.middleware import Middleware -from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.errors import ServerErrorMiddleware from starlette.middleware.exceptions import ExceptionMiddleware from starlette.requests import Request From 1bb03db2446d488799a22c3e30272e117cee20cf Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 12 Oct 2022 10:17:22 +0200 Subject: [PATCH 08/14] Add pragma no cover on receive method for now --- starlette/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starlette/requests.py b/starlette/requests.py index dbcaad875..838c95998 100644 --- a/starlette/requests.py +++ b/starlette/requests.py @@ -207,7 +207,7 @@ def method(self) -> str: @property def receive(self) -> Receive: - return self._receive + return self._receive # pragma: no cover async def stream(self) -> typing.AsyncGenerator[bytes, None]: if hasattr(self, "_body"): From 3dae5e8b4616328ad58f6ae682898d3191c61edd Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 20 Mar 2023 09:12:24 +0100 Subject: [PATCH 09/14] Remove deprecation warning tests --- tests/test_applications.py | 57 -------------------------------------- tests/test_routing.py | 17 ------------ 2 files changed, 74 deletions(-) diff --git a/tests/test_applications.py b/tests/test_applications.py index e30ec9295..56b5cf48b 100644 --- a/tests/test_applications.py +++ b/tests/test_applications.py @@ -437,63 +437,6 @@ def lifespan(app): assert cleanup_complete -def test_decorator_deprecations() -> None: - app = Starlette() - - with pytest.deprecated_call( - match=( - "The `exception_handler` decorator is deprecated, " - "and will be removed in version 1.0.0." - ) - ) as record: - app.exception_handler(500)(http_exception) - assert len(record) == 1 - - with pytest.deprecated_call( - match=( - "The `middleware` decorator is deprecated, " - "and will be removed in version 1.0.0." - ) - ) as record: - - async def middleware(request, call_next): - ... # pragma: no cover - - app.middleware("http")(middleware) - assert len(record) == 1 - - with pytest.deprecated_call( - match=( - "The `route` decorator is deprecated, " - "and will be removed in version 1.0.0." - ) - ) as record: - app.route("/")(async_homepage) - assert len(record) == 1 - - with pytest.deprecated_call( - match=( - "The `websocket_route` decorator is deprecated, " - "and will be removed in version 1.0.0." - ) - ) as record: - app.websocket_route("/ws")(websocket_endpoint) - assert len(record) == 1 - - with pytest.deprecated_call( - match=( - "The `on_event` decorator is deprecated, " - "and will be removed in version 1.0.0." - ) - ) as record: - - async def startup(): - ... # pragma: no cover - - app.on_event("startup")(startup) - assert len(record) == 1 - - def test_middleware_stack_init(test_client_factory: Callable[[ASGIApp], httpx.Client]): class NoOpMiddleware: def __init__(self, app: ASGIApp): diff --git a/tests/test_routing.py b/tests/test_routing.py index 001c00d1b..34b379073 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -1086,20 +1086,3 @@ def test_host_named_repr() -> None: ) # test for substring because repr(Router) returns unique object ID assert repr(route).startswith("Host(host='example.com', name='app', app=") - - -def test_decorator_deprecations() -> None: - router = Router() - - with pytest.deprecated_call(): - router.route("/")(homepage) - - with pytest.deprecated_call(): - router.websocket_route("/ws")(websocket_endpoint) - - with pytest.deprecated_call(): - - async def startup() -> None: - ... # pragma: nocover - - router.on_event("startup")(startup) From 0bf553f6d78c4a6ed922f1aa6176fceba49ad0c6 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 20 Mar 2023 09:15:21 +0100 Subject: [PATCH 10/14] Fix linter --- starlette/applications.py | 1 - starlette/middleware/base.py | 1 - 2 files changed, 2 deletions(-) diff --git a/starlette/applications.py b/starlette/applications.py index 3cecd5296..b1931fdf2 100644 --- a/starlette/applications.py +++ b/starlette/applications.py @@ -1,5 +1,4 @@ import typing -import warnings from starlette.datastructures import State, URLPath from starlette.middleware import Middleware diff --git a/starlette/middleware/base.py b/starlette/middleware/base.py index d6629a8b5..2ff0e047b 100644 --- a/starlette/middleware/base.py +++ b/starlette/middleware/base.py @@ -1,5 +1,4 @@ import typing -import warnings import anyio From e8ff20015cdda11c1b7df1dae05b4c97bf3293d7 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 19 Jun 2023 08:48:57 +0200 Subject: [PATCH 11/14] Remove old test --- tests/test_exceptions.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index fbc87fe72..4f96d6ea0 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -172,19 +172,6 @@ class CustomWebSocketException(WebSocketException): ) -def test_exception_middleware_deprecation() -> None: - # this test should be removed once the deprecation shim is removed - with pytest.warns(DeprecationWarning): - from starlette.exceptions import ExceptionMiddleware # noqa: F401 - - with warnings.catch_warnings(): - warnings.simplefilter("error") - import starlette.exceptions - - with pytest.warns(DeprecationWarning): - starlette.exceptions.ExceptionMiddleware - - def test_request_in_app_and_handler_is_the_same_object(client) -> None: response = client.post("/consume_body_in_endpoint_and_handler", content=b"Hello!") assert response.status_code == 422 From 3d37d52a34c2989d0b5bb5cc825feb1a0b8d38ed Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 19 Jun 2023 09:03:25 +0200 Subject: [PATCH 12/14] Remove Jinja2Templates deprecation warning --- starlette/templating.py | 32 +++++++++++++------------------- tests/test_templates.py | 8 ++------ 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/starlette/templating.py b/starlette/templating.py index ec9ca193d..9b29186ae 100644 --- a/starlette/templating.py +++ b/starlette/templating.py @@ -1,5 +1,4 @@ import typing -import warnings from os import PathLike from starlette.background import BackgroundTask @@ -20,7 +19,11 @@ else: # pragma: nocover pass_context = jinja2.contextfunction # type: ignore[attr-defined] except ModuleNotFoundError: # pragma: nocover - jinja2 = None # type: ignore[assignment] + raise RuntimeError( + "Jinja2Templates requires the jinja2 package to be installed.\n" + "You can install this with:\n" + " $ pip install jinja2\n" + ) class _TemplateResponse(Response): @@ -75,7 +78,6 @@ def __init__( context_processors: typing.Optional[ typing.List[typing.Callable[[Request], typing.Dict[str, typing.Any]]] ] = None, - **env_options: typing.Any, ) -> None: ... @@ -93,34 +95,29 @@ def __init__( def __init__( self, directory: typing.Union[ - str, PathLike, typing.Sequence[typing.Union[str, PathLike]], None + str, PathLike[str], typing.Sequence[typing.Union[str, PathLike[str]]], None ] = None, *, context_processors: typing.Optional[ typing.List[typing.Callable[[Request], typing.Dict[str, typing.Any]]] ] = None, env: typing.Optional["jinja2.Environment"] = None, - **env_options: typing.Any, ) -> None: - if env_options: - warnings.warn( - "Extra environment options are deprecated. Use a preconfigured jinja2.Environment instead.", # noqa: E501 - DeprecationWarning, - ) - assert jinja2 is not None, "jinja2 must be installed to use Jinja2Templates" - assert directory or env, "either 'directory' or 'env' arguments must be passed" self.context_processors = context_processors or [] if directory is not None: - self.env = self._create_env(directory, **env_options) + self.env = self._create_env(directory) elif env is not None: self.env = env + else: + raise RuntimeError( + "Either 'directory' or 'env' must be passed to Jinja2Templates" + ) def _create_env( self, directory: typing.Union[ - str, PathLike, typing.Sequence[typing.Union[str, PathLike]] + str, PathLike[str], typing.Sequence[typing.Union[str, PathLike[str]]] ], - **env_options: typing.Any, ) -> "jinja2.Environment": @pass_context # TODO: Make `__name` a positional-only argument when we drop Python 3.7 @@ -130,10 +127,7 @@ def url_for(context: dict, __name: str, **path_params: typing.Any) -> URL: return request.url_for(__name, **path_params) loader = jinja2.FileSystemLoader(directory) - env_options.setdefault("loader", loader) - env_options.setdefault("autoescape", True) - - env = jinja2.Environment(**env_options) + env = jinja2.Environment(loader=loader, autoescape=True) env.globals["url_for"] = url_for return env diff --git a/tests/test_templates.py b/tests/test_templates.py index 1f1909f4b..7971284c9 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -129,7 +129,8 @@ async def page_b(request): def test_templates_require_directory_or_environment(): with pytest.raises( - AssertionError, match="either 'directory' or 'env' arguments must be passed" + RuntimeError, + match="Either 'directory' or 'env' must be passed to Jinja2Templates", ): Jinja2Templates() # type: ignore[call-overload] @@ -153,8 +154,3 @@ def test_templates_with_environment(tmpdir): templates = Jinja2Templates(env=env) template = templates.get_template("index.html") assert template.render({}) == "Hello" - - -def test_templates_with_environment_options_emit_warning(tmpdir): - with pytest.warns(DeprecationWarning): - Jinja2Templates(str(tmpdir), autoescape=True) From 3243be655207acf0d81d57a53fe63e73a6406eec Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 19 Jun 2023 09:04:14 +0200 Subject: [PATCH 13/14] Remove deprecation warning filters --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 03c250a46..db5b92151 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,12 +78,9 @@ xfail_strict = true filterwarnings = [ # Turn warnings that aren't filtered into exceptions "error", - "ignore: run_until_first_complete is deprecated and will be removed in a future version.:DeprecationWarning", - "ignore: starlette.middleware.wsgi is deprecated and will be removed in a future release.*:DeprecationWarning", "ignore: Async generator 'starlette.requests.Request.stream' was garbage collected before it had been exhausted.*:ResourceWarning", "ignore: path is deprecated.*:DeprecationWarning:certifi", "ignore: Use 'content=<...>' to upload raw bytes/text content.:DeprecationWarning", - "ignore: The `allow_redirects` argument is deprecated. Use `follow_redirects` instead.:DeprecationWarning", "ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning", "ignore: You seem to already have a custom sys.excepthook handler installed. I'll skip installing Trio's custom handler, but this means MultiErrors will not show full tracebacks.:RuntimeWarning", ] From 93bc1e647e4a01bed9b7840cbb839fc4644ddf97 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 19 Jun 2023 09:32:29 +0200 Subject: [PATCH 14/14] Remove parameters to PathLike --- starlette/templating.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/starlette/templating.py b/starlette/templating.py index 9b29186ae..d0e50e049 100644 --- a/starlette/templating.py +++ b/starlette/templating.py @@ -21,7 +21,7 @@ except ModuleNotFoundError: # pragma: nocover raise RuntimeError( "Jinja2Templates requires the jinja2 package to be installed.\n" - "You can install this with:\n" + "You can install it with:\n" " $ pip install jinja2\n" ) @@ -95,7 +95,7 @@ def __init__( def __init__( self, directory: typing.Union[ - str, PathLike[str], typing.Sequence[typing.Union[str, PathLike[str]]], None + str, PathLike, typing.Sequence[typing.Union[str, PathLike]], None ] = None, *, context_processors: typing.Optional[ @@ -116,7 +116,7 @@ def __init__( def _create_env( self, directory: typing.Union[ - str, PathLike[str], typing.Sequence[typing.Union[str, PathLike[str]]] + str, PathLike, typing.Sequence[typing.Union[str, PathLike]] ], ) -> "jinja2.Environment": @pass_context