Skip to content

Commit

Permalink
Merge branch 'master' into use-etag-from-headers-for-if-range
Browse files Browse the repository at this point in the history
  • Loading branch information
Kludex authored Dec 3, 2024
2 parents 83a1bef + eee4cdc commit 88c27e2
Show file tree
Hide file tree
Showing 12 changed files with 104 additions and 44 deletions.
14 changes: 9 additions & 5 deletions docs/applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
25 changes: 24 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ dependencies = [
full = [
"itsdangerous",
"jinja2",
"python-multipart>=0.0.7",
"python-multipart>=0.0.18",
"pyyaml",
"httpx>=0.22.0",
"httpx>=0.27.0,<0.29.0",
]

[project.urls]
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 27 additions & 28 deletions starlette/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion starlette/middleware/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion starlette/staticfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion tests/middleware/test_wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 5 additions & 2 deletions tests/test_applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand All @@ -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"}
Expand Down
10 changes: 10 additions & 0 deletions tests/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")])
Expand Down Expand Up @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions tests/test_staticfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<h1>Hello</h1>")

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 == "<h1>Hello</h1>"

0 comments on commit 88c27e2

Please sign in to comment.