diff --git a/docs/deprecation.md b/docs/deprecation.md new file mode 100644 index 0000000000..3edd0941a4 --- /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 22ecca4005..b91d70309c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,3 +1,31 @@ +## 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. + +### Deprecated + +* Deprecated `BaseHTTPMiddleware`. Please refer to [Pure ASGI Middleware](https://www.starlette.io/middleware/#pure-asgi-middleware) + for recommended approach. +* Deprecated `Router.on_event` decorator. Please refer to [Registering events](https://www.starlette.io/events/#registering-events) + for recommended approach. +* Deprecated `Router.websocket_route` decorator. Please refer to [WebSocket Routing](https://www.starlette.io/routing/#websocket-routing) + for recommended approach. +* Deprecated `Router.route` decorator. Please refer to [HTTP Routing](https://www.starlette.io/routing/#http-routing) for recommended approach. +* Deprecated `Router.host` method. Please refer to [Host-based routing](https://www.starlette.io/routing/#host-based-routing) + for recommended approach. +* Deprecated `Router.mount` method. Please refer to [Submounting routes](https://www.starlette.io/routing/#submounting-routes) + for recommended approach. + ## 0.21.0 September 26, 2022 diff --git a/mkdocs.yml b/mkdocs.yml index 6b1d6cc466..f73fe0b16b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,6 +45,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: @@ -53,7 +54,7 @@ markdown_extensions: - pymdownx.highlight - pymdownx.superfences - pymdownx.tabbed: - alternate_style: true + alternate_style: true extra_javascript: - 'js/chat.js' diff --git a/setup.cfg b/setup.cfg index 23cf32cc03..1e36eae3e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,8 +28,6 @@ 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 diff --git a/starlette/__init__.py b/starlette/__init__.py index 6a726d853b..5becc17c04 100644 --- a/starlette/__init__.py +++ b/starlette/__init__.py @@ -1 +1 @@ -__version__ = "0.21.0" +__version__ = "1.0.0" diff --git a/starlette/concurrency.py b/starlette/concurrency.py index 5c76cb3df7..2905aa5701 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 87da735915..d64c75abf8 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 586c9870dc..826bcb3c85 100644 --- a/starlette/middleware/base.py +++ b/starlette/middleware/base.py @@ -1,4 +1,5 @@ import typing +import warnings import anyio @@ -12,6 +13,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 BaseHTTPMiddleware: def __init__( diff --git a/starlette/middleware/wsgi.py b/starlette/middleware/wsgi.py deleted file mode 100644 index 9dbd065284..0000000000 --- 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 479d4ae653..54c8f9a7e2 100644 --- a/starlette/routing.py +++ b/starlette/routing.py @@ -38,21 +38,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`, @@ -742,36 +727,24 @@ def __eq__(self, other: typing.Any) -> bool: def mount( self, path: str, app: ASGIApp, name: typing.Optional[str] = None ) -> None: # pragma: nocover - """ - We no longer document this API, and its usage is discouraged. - Instead you should use the following approach: - - routes = [ - Mount(path, ...), - ... - ] - - app = Starlette(routes=routes) - """ - + warnings.warn( + "The 'mount' method is now deprecated, and will be removed in version " + "2.0.0. Refer to https://www.starlette.io/routing/#submounting-routes " + "for recommended approach.", + DeprecationWarning, + ) 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 - """ - We no longer document this API, and its usage is discouraged. - Instead you should use the following approach: - - routes = [ - Host(path, ...), - ... - ] - - app = Starlette(routes=routes) - """ - + warnings.warn( + "The 'host' method is deprecated, and will be removed in version 2.0.0." + "Refer to https://www.starlette.io/routing/#host-based-routing for the " + "recommended approach.", + DeprecationWarning, + ) route = Host(host, app=app, name=name) self.routes.append(route) @@ -805,17 +778,12 @@ def route( name: typing.Optional[str] = None, include_in_schema: bool = True, ) -> typing.Callable: # pragma: nocover - """ - 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 2.0.0." + "Refer to https://www.starlette.io/routing/#http-routing for the " + "recommended approach.", + DeprecationWarning, + ) def decorator(func: typing.Callable) -> typing.Callable: self.add_route( @@ -832,17 +800,13 @@ def decorator(func: typing.Callable) -> typing.Callable: def websocket_route( self, path: str, name: typing.Optional[str] = None ) -> typing.Callable: # pragma: nocover - """ - 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 2.0.0. Refer to " + "https://www.starlette.io/routing/#websocket-routing for the recommended " + "approach.", + DeprecationWarning, + ) def decorator(func: typing.Callable) -> typing.Callable: self.add_websocket_route(path, func, name=name) @@ -861,6 +825,13 @@ def add_event_handler( self.on_shutdown.append(func) def on_event(self, event_type: str) -> typing.Callable: # pragma: nocover + warnings.warn( + "The `on_event` decorator is deprecated, and will be removed in version " + "2.0.0. Refer to https://www.starlette.io/events/#registering-events for " + "recommended approach.", + DeprecationWarning, + ) + def decorator(func: typing.Callable) -> typing.Callable: self.add_event_handler(event_type, func) return func diff --git a/starlette/status.py b/starlette/status.py index 1689328a4e..8d28007742 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 bcb4cd6ff2..0000000000 --- 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 22b9da0e83..6ac1b39b2c 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 05583a430b..4f69f3add7 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 @@ -147,16 +145,3 @@ class CustomWebSocketException(WebSocketException): repr(CustomWebSocketException(1013, reason="Something custom")) == "CustomWebSocketException(code=1013, reason='Something custom')" ) - - -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