diff --git a/docs/applications.md b/docs/applications.md index 6fb74f19f..b90c20f32 100644 --- a/docs/applications.md +++ b/docs/applications.md @@ -3,6 +3,8 @@ Starlette includes an application class `Starlette` that nicely ties together al its other functionality. ```python +from contextlib import asynccontextmanager + from starlette.applications import Starlette from starlette.responses import PlainTextResponse from starlette.routing import Route, Mount, WebSocketRoute @@ -25,8 +27,11 @@ async def websocket_endpoint(websocket): await websocket.send_text('Hello, websocket!') await websocket.close() -def startup(): - print('Ready to go') +@asyncontextmanager +async def lifespan(app): + print('Startup') + yield + print('Shutdown') routes = [ @@ -37,7 +42,7 @@ routes = [ Mount('/static', StaticFiles(directory="static")), ] -app = Starlette(debug=True, routes=routes, on_startup=[startup]) +app = Starlette(debug=True, routes=routes, lifespan=lifespan) ``` ### Instantiating the application diff --git a/docs/index.md b/docs/index.md index cc243b43c..faf1c7c55 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,3 @@ ---- -hide: navigation ---- -
@@ -52,18 +48,18 @@ It is production-ready, and gives you the following:
## Installation
```shell
-$ pip install starlette
+pip install starlette
```
You'll also want to install an ASGI server, such as [uvicorn](https://www.uvicorn.org/), [daphne](https://github.com/django/daphne/), or [hypercorn](https://hypercorn.readthedocs.io/en/latest/).
```shell
-$ pip install uvicorn
+pip install uvicorn
```
## Example
-```python title="example.py"
+```python title="main.py"
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
@@ -81,11 +77,9 @@ app = Starlette(debug=True, routes=[
Then run the application...
```shell
-$ uvicorn example:app
+uvicorn main:app
```
-For a more complete example, [see here](https://github.com/encode/starlette-example).
-
## Dependencies
Starlette only requires `anyio`, and the following dependencies are optional:
@@ -103,7 +97,7 @@ You can install all of these with `pip install starlette[full]`.
Starlette is designed to be used either as a complete framework, or as
an ASGI toolkit. You can use any of its components independently.
-```python
+```python title="main.py"
from starlette.responses import PlainTextResponse
@@ -113,10 +107,10 @@ async def app(scope, receive, send):
await response(scope, receive, send)
```
-Run the `app` application in `example.py`:
+Run the `app` application in `main.py`:
```shell
-$ uvicorn example:app
+$ uvicorn main:app
INFO: Started server process [11509]
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 1b56b342a..c484234f3 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -1,8 +1,15 @@
---
-hide: navigation
toc_depth: 2
---
+## 0.41.3 (November 18, 2024)
+
+#### Fixed
+
+* Exclude the query parameters from the `scope[raw_path]` on the `TestClient` [#2716](https://github.com/encode/starlette/pull/2716).
+* Replace `dict` by `Mapping` on `HTTPException.headers` [#2749](https://github.com/encode/starlette/pull/2749).
+* Correct middleware argument passing and improve factory pattern [#2752](https://github.com/encode/starlette/2752).
+
## 0.41.2 (October 27, 2024)
#### Fixed
diff --git a/mkdocs.yml b/mkdocs.yml
index ad503317d..83d245e04 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -21,7 +21,6 @@ theme:
repo: fontawesome/brands/github
features:
- content.code.copy
- - navigation.tabs
- toc.follow
repo_name: encode/starlette
@@ -29,7 +28,7 @@ repo_url: https://github.com/encode/starlette
edit_uri: edit/master/docs/
nav:
- - Home: "index.md"
+ - Introduction: "index.md"
- Features:
- Applications: "applications.md"
- Requests: "requests.md"
diff --git a/starlette/__init__.py b/starlette/__init__.py
index b0416f28d..ef838c985 100644
--- a/starlette/__init__.py
+++ b/starlette/__init__.py
@@ -1 +1 @@
-__version__ = "0.41.2"
+__version__ = "0.41.3"
diff --git a/starlette/applications.py b/starlette/applications.py
index 0feae72e4..aae38f588 100644
--- a/starlette/applications.py
+++ b/starlette/applications.py
@@ -10,7 +10,7 @@
from typing_extensions import ParamSpec
from starlette.datastructures import State, URLPath
-from starlette.middleware import Middleware, _MiddlewareClass
+from starlette.middleware import Middleware, _MiddlewareFactory
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.exceptions import ExceptionMiddleware
@@ -96,7 +96,7 @@ def build_middleware_stack(self) -> ASGIApp:
app = self.router
for cls, args, kwargs in reversed(middleware):
- app = cls(app=app, *args, **kwargs)
+ app = cls(app, *args, **kwargs)
return app
@property
@@ -123,7 +123,7 @@ def host(self, host: str, app: ASGIApp, name: str | None = None) -> None:
def add_middleware(
self,
- middleware_class: type[_MiddlewareClass[P]],
+ middleware_class: _MiddlewareFactory[P],
*args: P.args,
**kwargs: P.kwargs,
) -> None:
diff --git a/starlette/exceptions.py b/starlette/exceptions.py
index bd3352eb0..c48f0838a 100644
--- a/starlette/exceptions.py
+++ b/starlette/exceptions.py
@@ -12,7 +12,7 @@ def __init__(
self,
status_code: int,
detail: str | None = None,
- headers: dict[str, str] | None = None,
+ headers: typing.Mapping[str, str] | None = None,
) -> None:
if detail is None:
detail = http.HTTPStatus(status_code).phrase
diff --git a/starlette/middleware/__init__.py b/starlette/middleware/__init__.py
index 8566aac08..8e0a54edb 100644
--- a/starlette/middleware/__init__.py
+++ b/starlette/middleware/__init__.py
@@ -8,21 +8,19 @@
else: # pragma: no cover
from typing_extensions import ParamSpec
-from starlette.types import ASGIApp, Receive, Scope, Send
+from starlette.types import ASGIApp
P = ParamSpec("P")
-class _MiddlewareClass(Protocol[P]):
- def __init__(self, app: ASGIApp, *args: P.args, **kwargs: P.kwargs) -> None: ... # pragma: no cover
-
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ... # pragma: no cover
+class _MiddlewareFactory(Protocol[P]):
+ def __call__(self, app: ASGIApp, *args: P.args, **kwargs: P.kwargs) -> ASGIApp: ... # pragma: no cover
class Middleware:
def __init__(
self,
- cls: type[_MiddlewareClass[P]],
+ cls: _MiddlewareFactory[P],
*args: P.args,
**kwargs: P.kwargs,
) -> None:
@@ -38,5 +36,6 @@ def __repr__(self) -> str:
class_name = self.__class__.__name__
args_strings = [f"{value!r}" for value in self.args]
option_strings = [f"{key}={value!r}" for key, value in self.kwargs.items()]
- args_repr = ", ".join([self.cls.__name__] + args_strings + option_strings)
+ name = getattr(self.cls, "__name__", "")
+ args_repr = ", ".join([name] + args_strings + option_strings)
return f"{class_name}({args_repr})"
diff --git a/starlette/routing.py b/starlette/routing.py
index 1504ef50a..3b3c52968 100644
--- a/starlette/routing.py
+++ b/starlette/routing.py
@@ -236,7 +236,7 @@ def __init__(
if middleware is not None:
for cls, args, kwargs in reversed(middleware):
- self.app = cls(app=self.app, *args, **kwargs)
+ self.app = cls(self.app, *args, **kwargs)
if methods is None:
self.methods = None
@@ -328,7 +328,7 @@ def __init__(
if middleware is not None:
for cls, args, kwargs in reversed(middleware):
- self.app = cls(app=self.app, *args, **kwargs)
+ self.app = cls(self.app, *args, **kwargs)
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
@@ -388,7 +388,7 @@ def __init__(
self.app = self._base_app
if middleware is not None:
for cls, args, kwargs in reversed(middleware):
- self.app = cls(app=self.app, *args, **kwargs)
+ self.app = cls(self.app, *args, **kwargs)
self.name = name
self.path_regex, self.path_format, self.param_convertors = compile_path(self.path + "/{path:path}")
diff --git a/starlette/testclient.py b/starlette/testclient.py
index 5143c4c57..645ca1090 100644
--- a/starlette/testclient.py
+++ b/starlette/testclient.py
@@ -281,7 +281,7 @@ def handle_request(self, request: httpx.Request) -> httpx.Response:
scope = {
"type": "websocket",
"path": unquote(path),
- "raw_path": raw_path,
+ "raw_path": raw_path.split(b"?", 1)[0],
"root_path": self.root_path,
"scheme": scheme,
"query_string": query.encode(),
@@ -300,7 +300,7 @@ def handle_request(self, request: httpx.Request) -> httpx.Response:
"http_version": "1.1",
"method": request.method,
"path": unquote(path),
- "raw_path": raw_path,
+ "raw_path": raw_path.split(b"?", 1)[0],
"root_path": self.root_path,
"scheme": scheme,
"query_string": query.encode(),
diff --git a/tests/middleware/test_base.py b/tests/middleware/test_base.py
index 041cc7ce2..fa0cba479 100644
--- a/tests/middleware/test_base.py
+++ b/tests/middleware/test_base.py
@@ -10,7 +10,7 @@
from starlette.applications import Starlette
from starlette.background import BackgroundTask
-from starlette.middleware import Middleware, _MiddlewareClass
+from starlette.middleware import Middleware, _MiddlewareFactory
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import ClientDisconnect, Request
from starlette.responses import PlainTextResponse, Response, StreamingResponse
@@ -232,7 +232,7 @@ async def dispatch(
)
def test_contextvars(
test_client_factory: TestClientFactory,
- middleware_cls: type[_MiddlewareClass[Any]],
+ middleware_cls: _MiddlewareFactory[Any],
) -> None:
# this has to be an async endpoint because Starlette calls run_in_threadpool
# on sync endpoints which has it's own set of peculiarities w.r.t propagating
diff --git a/tests/test_applications.py b/tests/test_applications.py
index 056044438..29c011a29 100644
--- a/tests/test_applications.py
+++ b/tests/test_applications.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import os
from contextlib import asynccontextmanager
from pathlib import Path
@@ -533,6 +535,48 @@ def get_app() -> ASGIApp:
assert SimpleInitializableMiddleware.counter == 2
+def test_middleware_args(test_client_factory: TestClientFactory) -> None:
+ calls: list[str] = []
+
+ class MiddlewareWithArgs:
+ def __init__(self, app: ASGIApp, arg: str) -> None:
+ self.app = app
+ self.arg = arg
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ calls.append(self.arg)
+ await self.app(scope, receive, send)
+
+ app = Starlette()
+ app.add_middleware(MiddlewareWithArgs, "foo")
+ app.add_middleware(MiddlewareWithArgs, "bar")
+
+ with test_client_factory(app):
+ pass
+
+ assert calls == ["bar", "foo"]
+
+
+def test_middleware_factory(test_client_factory: TestClientFactory) -> None:
+ calls: list[str] = []
+
+ def _middleware_factory(app: ASGIApp, arg: str) -> ASGIApp:
+ async def _app(scope: Scope, receive: Receive, send: Send) -> None:
+ calls.append(arg)
+ await app(scope, receive, send)
+
+ return _app
+
+ app = Starlette()
+ app.add_middleware(_middleware_factory, arg="foo")
+ app.add_middleware(_middleware_factory, arg="bar")
+
+ with test_client_factory(app):
+ pass
+
+ assert calls == ["bar", "foo"]
+
+
def test_lifespan_app_subclass() -> None:
# This test exists to make sure that subclasses of Starlette
# (like FastAPI) are compatible with the types hints for Lifespan
diff --git a/tests/test_testclient.py b/tests/test_testclient.py
index 92f16d336..68593a9ac 100644
--- a/tests/test_testclient.py
+++ b/tests/test_testclient.py
@@ -378,3 +378,27 @@ def homepage(request: Request) -> Response:
client = test_client_factory(app, base_url="http://testserver/api/v1/")
response = client.get("/bar")
assert response.text == "/api/v1/bar"
+
+
+def test_raw_path_with_querystring(test_client_factory: TestClientFactory) -> None:
+ async def app(scope: Scope, receive: Receive, send: Send) -> None:
+ response = Response(scope.get("raw_path"))
+ await response(scope, receive, send)
+
+ client = test_client_factory(app)
+ response = client.get("/hello-world", params={"foo": "bar"})
+ assert response.content == b"/hello-world"
+
+
+def test_websocket_raw_path_without_params(test_client_factory: TestClientFactory) -> None:
+ async def app(scope: Scope, receive: Receive, send: Send) -> None:
+ websocket = WebSocket(scope, receive=receive, send=send)
+ await websocket.accept()
+ raw_path = scope.get("raw_path")
+ assert raw_path is not None
+ await websocket.send_bytes(raw_path)
+
+ client = test_client_factory(app)
+ with client.websocket_connect("/hello-world", params={"foo": "bar"}) as websocket:
+ data = websocket.receive_bytes()
+ assert data == b"/hello-world"