From 009a3bab62c4e3f1479914b08a3291cfe86e5e9e Mon Sep 17 00:00:00 2001 From: Logan Connolly Date: Sun, 24 Nov 2024 16:00:32 +0100 Subject: [PATCH 1/7] docs: fix typo in applications example (#2763) --- docs/applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/applications.md b/docs/applications.md index b90c20f32..a3be21f7f 100644 --- a/docs/applications.md +++ b/docs/applications.md @@ -27,7 +27,7 @@ async def websocket_endpoint(websocket): await websocket.send_text('Hello, websocket!') await websocket.close() -@asyncontextmanager +@asynccontextmanager async def lifespan(app): print('Startup') yield From 13d0c1fbcdb6c6f861155beed352112843e57717 Mon Sep 17 00:00:00 2001 From: Eugene Toder Date: Tue, 26 Nov 2024 02:28:34 -0500 Subject: [PATCH 2/7] Make _MiddlewareFactory compatible with Callable (#2768) --- starlette/middleware/__init__.py | 2 +- tests/test_applications.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/starlette/middleware/__init__.py b/starlette/middleware/__init__.py index 8e0a54edb..5164e49e0 100644 --- a/starlette/middleware/__init__.py +++ b/starlette/middleware/__init__.py @@ -14,7 +14,7 @@ class _MiddlewareFactory(Protocol[P]): - def __call__(self, app: ASGIApp, *args: P.args, **kwargs: P.kwargs) -> ASGIApp: ... # pragma: no cover + def __call__(self, app: ASGIApp, /, *args: P.args, **kwargs: P.kwargs) -> ASGIApp: ... # pragma: no cover class Middleware: diff --git a/tests/test_applications.py b/tests/test_applications.py index 29c011a29..db2a6050b 100644 --- a/tests/test_applications.py +++ b/tests/test_applications.py @@ -3,7 +3,7 @@ import os from contextlib import asynccontextmanager from pathlib import Path -from typing import AsyncGenerator, AsyncIterator, Generator +from typing import AsyncGenerator, AsyncIterator, Callable, Generator import anyio.from_thread import pytest @@ -567,9 +567,12 @@ async def _app(scope: Scope, receive: Receive, send: Send) -> None: return _app + def get_middleware_factory() -> Callable[[ASGIApp, str], ASGIApp]: + return _middleware_factory + app = Starlette() app.add_middleware(_middleware_factory, arg="foo") - app.add_middleware(_middleware_factory, arg="bar") + app.add_middleware(get_middleware_factory(), "bar") with test_client_factory(app): pass From 0ba83959e609bbd460966f092287df1bbd564cc6 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 30 Nov 2024 10:52:32 +0100 Subject: [PATCH 3/7] bump-python-multipart --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a532e4628..551a5a3ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ full = [ "itsdangerous", "jinja2", - "python-multipart>=0.0.7", + "python-multipart>=0.0.18", "pyyaml", "httpx>=0.22.0", ] From 5ccbc62175eece867b498115724eb8d3fa27acb0 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 30 Nov 2024 10:53:55 +0100 Subject: [PATCH 4/7] Pin httpx in `full` extra (#2773) --- pyproject.toml | 2 +- tests/middleware/test_wsgi.py | 2 +- tests/test_requests.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 551a5a3ad..f27646083 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ full = [ "jinja2", "python-multipart>=0.0.18", "pyyaml", - "httpx>=0.22.0", + "httpx>=0.27.0,<0.29.0", ] [project.urls] diff --git a/tests/middleware/test_wsgi.py b/tests/middleware/test_wsgi.py index 58696bb65..e4ac66ab4 100644 --- a/tests/middleware/test_wsgi.py +++ b/tests/middleware/test_wsgi.py @@ -77,7 +77,7 @@ def test_wsgi_post(test_client_factory: TestClientFactory) -> None: client = test_client_factory(app) response = client.post("/", json={"example": 123}) assert response.status_code == 200 - assert response.text == '{"example": 123}' + assert response.text == '{"example":123}' def test_wsgi_exception(test_client_factory: TestClientFactory) -> None: diff --git a/tests/test_requests.py b/tests/test_requests.py index f0494e751..665dceb87 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -91,7 +91,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: assert response.json() == {"body": ""} response = client.post("/", json={"a": "123"}) - assert response.json() == {"body": '{"a": "123"}'} + assert response.json() == {"body": '{"a":"123"}'} response = client.post("/", data="abc") # type: ignore assert response.json() == {"body": "abc"} @@ -112,7 +112,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: assert response.json() == {"body": ""} response = client.post("/", json={"a": "123"}) - assert response.json() == {"body": '{"a": "123"}'} + assert response.json() == {"body": '{"a":"123"}'} response = client.post("/", data="abc") # type: ignore assert response.json() == {"body": "abc"} From 35dae138a0edd581f11255865304fdc914e763e1 Mon Sep 17 00:00:00 2001 From: Sean Chen <2367058391@qq.com> Date: Sat, 30 Nov 2024 17:58:59 +0800 Subject: [PATCH 5/7] test: add tests in `test_routing` (#2676) Co-authored-by: Marcelo Trylesinski --- tests/test_routing.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_routing.py b/tests/test_routing.py index fb2bfeec1..6abdc564c 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -390,6 +390,14 @@ def test_reverse_mount_urls() -> None: assert mounted.url_path_for("users:user", subpath="test", username="tom") == "/test/users/tom" assert mounted.url_path_for("users", subpath="test", path="/tom") == "/test/users/tom" + mounted = Router([Mount("/users", ok, name="users")]) + with pytest.raises(NoMatchFound): + mounted.url_path_for("users", path="/a", foo="bar") + + mounted = Router([Mount("/users", ok, name="users")]) + with pytest.raises(NoMatchFound): + mounted.url_path_for("users") + def test_mount_at_root(test_client_factory: TestClientFactory) -> None: mounted = Router([Mount("/", ok, name="users")]) @@ -479,6 +487,8 @@ def test_host_reverse_urls() -> None: mixed_hosts_app.url_path_for("port:homepage").make_absolute_url("https://whatever") == "https://port.example.org:3600/" ) + with pytest.raises(NoMatchFound): + mixed_hosts_app.url_path_for("api", path="whatever", foo="bar") async def subdomain_app(scope: Scope, receive: Receive, send: Send) -> None: From b68a142a356ede730083347f254e1eae8b5c803e Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 30 Nov 2024 12:41:25 +0100 Subject: [PATCH 6/7] Replace `mkautodoc` by `mkdocstrings` (#2776) --- docs/applications.md | 12 ++++++--- mkdocs.yml | 25 +++++++++++++++++- requirements.txt | 3 ++- starlette/applications.py | 55 +++++++++++++++++++-------------------- 4 files changed, 61 insertions(+), 34 deletions(-) diff --git a/docs/applications.md b/docs/applications.md index a3be21f7f..695f38011 100644 --- a/docs/applications.md +++ b/docs/applications.md @@ -45,10 +45,14 @@ routes = [ app = Starlette(debug=True, routes=routes, lifespan=lifespan) ``` -### Instantiating the application - -::: starlette.applications.Starlette - :docstring: +??? abstract "API Reference" + ::: starlette.applications.Starlette + options: + parameter_headings: false + show_root_heading: true + heading_level: 3 + filters: + - "__init__" ### Storing state on the app instance diff --git a/mkdocs.yml b/mkdocs.yml index 83d245e04..55bf1c224 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,9 +55,32 @@ nav: - Contributing: "contributing.md" markdown_extensions: - - mkautodoc - admonition - pymdownx.highlight - pymdownx.superfences + - pymdownx.details - pymdownx.tabbed: alternate_style: true + +watch: + - starlette + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_section_style: list + show_root_toc_entry: false + members_order: source + separate_signature: true + filters: ["!^_"] + docstring_options: + ignore_init_summary: true + merge_init_into_class: true + parameter_headings: true + show_signature_annotations: true + signature_crossrefs: true + import: + - url: https://docs.python.org/3/objects.inv diff --git a/requirements.txt b/requirements.txt index 1dc7bc7c7..edec1987b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,8 @@ trio==0.27.0 # Documentation mkdocs==1.6.1 mkdocs-material==9.5.43 -mkautodoc==0.2.0 +mkdocstrings-python<1.12.0; python_version < "3.9" +mkdocstrings-python==1.12.2; python_version >= "3.9" # Packaging build==1.2.2.post1 diff --git a/starlette/applications.py b/starlette/applications.py index aae38f588..0a717bb3a 100644 --- a/starlette/applications.py +++ b/starlette/applications.py @@ -25,34 +25,7 @@ class Starlette: - """ - Creates an application instance. - - **Parameters:** - - * **debug** - Boolean indicating if debug tracebacks should be returned on errors. - * **routes** - A list of routes to serve incoming HTTP and WebSocket requests. - * **middleware** - A list of middleware to run for every request. A starlette - application will always automatically include two middleware classes. - `ServerErrorMiddleware` is added as the very outermost middleware, to handle - any uncaught errors occurring anywhere in the entire stack. - `ExceptionMiddleware` is added as the very innermost middleware, to deal - with handled exception cases occurring in the routing or endpoints. - * **exception_handlers** - A mapping of either integer status codes, - or exception class types onto callables which handle the exceptions. - Exception handler callables should be of the form - `handler(request, exc) -> response` and may be either standard functions, or - async functions. - * **on_startup** - A list of callables to run on application startup. - Startup handler callables do not take any arguments, and may be either - standard functions, or async functions. - * **on_shutdown** - A list of callables to run on application shutdown. - Shutdown handler callables do not take any arguments, and may be either - standard functions, or async functions. - * **lifespan** - A lifespan context function, which can be used to perform - startup and shutdown tasks. This is a newer style that replaces the - `on_startup` and `on_shutdown` handlers. Use one or the other, not both. - """ + """Creates an Starlette application.""" def __init__( self: AppType, @@ -64,6 +37,32 @@ def __init__( on_shutdown: typing.Sequence[typing.Callable[[], typing.Any]] | None = None, lifespan: Lifespan[AppType] | None = None, ) -> None: + """Initializes the application. + + Parameters: + debug: Boolean indicating if debug tracebacks should be returned on errors. + routes: A list of routes to serve incoming HTTP and WebSocket requests. + middleware: A list of middleware to run for every request. A starlette + application will always automatically include two middleware classes. + `ServerErrorMiddleware` is added as the very outermost middleware, to handle + any uncaught errors occurring anywhere in the entire stack. + `ExceptionMiddleware` is added as the very innermost middleware, to deal + with handled exception cases occurring in the routing or endpoints. + exception_handlers: A mapping of either integer status codes, + or exception class types onto callables which handle the exceptions. + Exception handler callables should be of the form + `handler(request, exc) -> response` and may be either standard functions, or + async functions. + on_startup: A list of callables to run on application startup. + Startup handler callables do not take any arguments, and may be either + standard functions, or async functions. + on_shutdown: A list of callables to run on application shutdown. + Shutdown handler callables do not take any arguments, and may be either + standard functions, or async functions. + lifespan: A lifespan context function, which can be used to perform + startup and shutdown tasks. This is a newer style that replaces the + `on_startup` and `on_shutdown` handlers. Use one or the other, not both. + """ # The lifespan context function is a newer style that replaces # on_startup / on_shutdown handlers. Use one or the other, not both. assert lifespan is None or ( From eee4cdcb9a4e787d53df88ccbee2861a0bb9b0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B6=B5=E6=9B=A6?= Date: Mon, 2 Dec 2024 00:46:16 +0800 Subject: [PATCH 7/7] Fix: Resolve the issue where the directory itself being a symlink does not work. (#2711) --- starlette/staticfiles.py | 2 +- tests/test_staticfiles.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/starlette/staticfiles.py b/starlette/staticfiles.py index 7498c3011..746e740e0 100644 --- a/starlette/staticfiles.py +++ b/starlette/staticfiles.py @@ -155,7 +155,7 @@ def lookup_path(self, path: str) -> tuple[str, os.stat_result | None]: full_path = os.path.abspath(joined_path) else: full_path = os.path.realpath(joined_path) - directory = os.path.realpath(directory) + directory = os.path.realpath(directory) if os.path.commonpath([full_path, directory]) != directory: # Don't allow misbehaving clients to break out of the static files # directory. diff --git a/tests/test_staticfiles.py b/tests/test_staticfiles.py index 8beb3cd87..8f7423593 100644 --- a/tests/test_staticfiles.py +++ b/tests/test_staticfiles.py @@ -559,3 +559,23 @@ def test_staticfiles_avoids_path_traversal(tmp_path: Path) -> None: assert exc_info.value.status_code == 404 assert exc_info.value.detail == "Not Found" + + +def test_staticfiles_self_symlinks(tmpdir: Path, test_client_factory: TestClientFactory) -> None: + statics_path = os.path.join(tmpdir, "statics") + os.mkdir(statics_path) + + source_file_path = os.path.join(statics_path, "index.html") + with open(source_file_path, "w") as file: + file.write("

Hello

") + + statics_symlink_path = os.path.join(tmpdir, "statics_symlink") + os.symlink(statics_path, statics_symlink_path) + + app = StaticFiles(directory=statics_symlink_path, follow_symlink=True) + client = test_client_factory(app) + + response = client.get("/index.html") + assert response.url == "http://testserver/index.html" + assert response.status_code == 200 + assert response.text == "

Hello

"