Skip to content

Commit

Permalink
Merge branch 'master' into fix/regex-get_route_path
Browse files Browse the repository at this point in the history
  • Loading branch information
Kludex committed Aug 19, 2024
2 parents 7d74b3b + c78c9aa commit fa6d9ac
Show file tree
Hide file tree
Showing 47 changed files with 291 additions and 277 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:

strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: "actions/checkout@v4"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Python 3.8+
$ pip3 install starlette
```

You'll also want to install an ASGI server, such as [uvicorn](http://www.uvicorn.org/), [daphne](https://github.com/django/daphne/), or [hypercorn](https://pgjones.gitlab.io/hypercorn/).
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
$ pip3 install uvicorn
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Python 3.8+
$ pip3 install starlette
```

You'll also want to install an ASGI server, such as [uvicorn](http://www.uvicorn.org/), [daphne](https://github.com/django/daphne/), or [hypercorn](https://pgjones.gitlab.io/hypercorn/).
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
$ pip3 install uvicorn
Expand Down
35 changes: 35 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
## 0.38.2

July 27, 2024

#### Fixed

* Not assume all routines have `__name__` on `routing.get_name()` [#2648](https://github.com/encode/starlette/pull/2648).

## 0.38.1

July 23, 2024

#### Removed

* Revert "Add support for ASGI pathsend extension" [#2649](https://github.com/encode/starlette/pull/2649).

## 0.38.0

July 20, 2024

#### Added

* Allow use of `memoryview` in `StreamingResponse` and `Response` [#2576](https://github.com/encode/starlette/pull/2576)
and [#2577](https://github.com/encode/starlette/pull/2577).
* Send 404 instead of 500 when filename requested is too long on `StaticFiles` [#2583](https://github.com/encode/starlette/pull/2583).

#### Changed

* Fail fast on invalid `Jinja2Template` instantiation parameters [#2568](https://github.com/encode/starlette/pull/2568).
* Check endpoint handler is async only once [#2536](https://github.com/encode/starlette/pull/2536).

#### Fixed

* Add proper synchronization to `WebSocketTestSession` [#2597](https://github.com/encode/starlette/pull/2597).

## 0.37.2

March 5, 2024
Expand Down
20 changes: 20 additions & 0 deletions docs/third-party-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ Simple APISpec integration for Starlette.
Document your REST API built with Starlette by declaring OpenAPI (Swagger)
schemas in YAML format in your endpoint's docstrings.

### Starlette Compress

<a href="https://github.com/Zaczero/starlette-compress" target="_blank">GitHub</a>

Starlette-Compress is a fast and simple middleware for compressing responses in Starlette.
It adds ZStd, Brotli, and GZip compression support with sensible default configuration.

### Starlette Context

<a href="https://github.com/tomwojcik/starlette-context" target="_blank">GitHub</a>
Expand Down Expand Up @@ -148,6 +155,12 @@ Built with [Tabler](https://tabler.io/) and [Datatables](https://datatables.net/
to quickly generate fully customizable admin interface for your models. You can export your data to many formats (*CSV*, *PDF*,
*Excel*, etc), filter your data with complex query including `AND` and `OR` conditions, upload files, ...

### Vellox

<a href="https://github.com/junah201/vellox" target="_blank">GitHub</a>

Serverless ASGI adapter for GCP Cloud Functions.

## Starlette Bridge

<a href="https://github.com/tarsil/starlette-bridge" target="_blank">GitHub</a> |
Expand Down Expand Up @@ -247,3 +260,10 @@ Allows mounting [package resources](https://docs.python.org/3/library/importlib.
<a href="https://docs.sentry.io/platforms/python/guides/starlette/" target="_blank">Documentation</a>

Sentry is a software error detection tool. It offers actionable insights for resolving performance issues and errors, allowing users to diagnose, fix, and optimize Python debugging. Additionally, it integrates seamlessly with Starlette for Python application development. Sentry's capabilities include error tracking, performance insights, contextual information, and alerts/notifications.

### Shiny

<a href="https://github.com/posit-dev/py-shiny" target="_blank">GitHub</a> |
<a href="https://shiny.posit.co/py/" target="_blank">Documentation</a>

Leveraging Starlette and asyncio, Shiny allows developers to create effortless Python web applications using the power of reactive programming. Shiny eliminates the hassle of manual state management, automatically determining the best execution path for your app at runtime while simultaneously minimizing re-rendering. This means that Shiny can support everything from the simplest dashboard to full-featured web apps.
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Internet :: WWW/HTTP",
]
dependencies = [
Expand Down Expand Up @@ -51,6 +52,7 @@ path = "starlette/__init__.py"

[tool.ruff.lint]
select = ["E", "F", "I", "FA", "UP"]
ignore = ["UP031"]

[tool.ruff.lint.isort]
combine-as-imports = true
Expand All @@ -65,7 +67,7 @@ module = "starlette.testclient.*"
implicit_optional = true

[tool.pytest.ini_options]
addopts = "-rxXs --strict-config --strict-markers"
addopts = "-rXs --strict-config --strict-markers"
xfail_strict = true
filterwarnings = [
# Turn warnings that aren't filtered into exceptions
Expand All @@ -78,6 +80,8 @@ filterwarnings = [
"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",
# TODO: This warning appeared when we bumped anyio to 4.4.0.
"ignore: Unclosed .MemoryObject(Send|Receive)Stream.:ResourceWarning",
]

[tool.coverage.run]
Expand Down
26 changes: 14 additions & 12 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@
-e .[full]

# Testing
coverage==7.4.3
importlib-metadata==7.0.1
mypy==1.8.0
ruff==0.1.15
typing_extensions==4.10.0
coverage==7.6.0
importlib-metadata==8.2.0
mypy==1.11.1
ruff==0.5.5
typing_extensions==4.12.2
types-contextvars==2.4.7.3
types-PyYAML==6.0.12.12
types-PyYAML==6.0.12.20240724
types-dataclasses==0.6.6
pytest==8.0.2
trio==0.24.0
pytest==8.3.2
trio==0.26.1
# TODO: Remove when trio is updated.
attrs==23.2.0

# Documentation
mkdocs==1.5.3
mkdocs-material==9.5.12
mkdocs==1.6.0
mkdocs-material==9.5.30
mkautodoc==0.2.0

# Packaging
build==1.1.1
twine==5.0.0
build==1.2.1
twine==5.1.1
2 changes: 1 addition & 1 deletion scripts/lint
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ export SOURCE_FILES="starlette tests"
set -x

${PREFIX}ruff format $SOURCE_FILES
${PREFIX}ruff --fix $SOURCE_FILES
${PREFIX}ruff check --fix $SOURCE_FILES
2 changes: 1 addition & 1 deletion starlette/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.37.2"
__version__ = "0.38.2"
12 changes: 4 additions & 8 deletions starlette/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,11 @@


@typing.overload
def is_async_callable(obj: AwaitableCallable[T]) -> TypeGuard[AwaitableCallable[T]]:
...
def is_async_callable(obj: AwaitableCallable[T]) -> TypeGuard[AwaitableCallable[T]]: ...


@typing.overload
def is_async_callable(obj: typing.Any) -> TypeGuard[AwaitableCallable[typing.Any]]:
...
def is_async_callable(obj: typing.Any) -> TypeGuard[AwaitableCallable[typing.Any]]: ...


def is_async_callable(obj: typing.Any) -> typing.Any:
Expand All @@ -49,13 +47,11 @@ def is_async_callable(obj: typing.Any) -> typing.Any:

class AwaitableOrContextManager(
typing.Awaitable[T_co], typing.AsyncContextManager[T_co], typing.Protocol[T_co]
):
...
): ...


class SupportsAsyncClose(typing.Protocol):
async def close(self) -> None:
... # pragma: no cover
async def close(self) -> None: ... # pragma: no cover


SupportsAsyncCloseType = typing.TypeVar(
Expand Down
17 changes: 7 additions & 10 deletions starlette/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,29 +68,26 @@ def __init__(
self.file_values = self._read_file(env_file)

@typing.overload
def __call__(self, key: str, *, default: None) -> str | None:
...
def __call__(self, key: str, *, default: None) -> str | None: ...

@typing.overload
def __call__(self, key: str, cast: type[T], default: T = ...) -> T:
...
def __call__(self, key: str, cast: type[T], default: T = ...) -> T: ...

@typing.overload
def __call__(self, key: str, cast: type[str] = ..., default: str = ...) -> str:
...
def __call__(self, key: str, cast: type[str] = ..., default: str = ...) -> str: ...

@typing.overload
def __call__(
self,
key: str,
cast: typing.Callable[[typing.Any], T] = ...,
default: typing.Any = ...,
) -> T:
...
) -> T: ...

@typing.overload
def __call__(self, key: str, cast: type[str] = ..., default: T = ...) -> T | str:
...
def __call__(
self, key: str, cast: type[str] = ..., default: T = ...
) -> T | str: ...

def __call__(
self,
Expand Down
10 changes: 6 additions & 4 deletions starlette/middleware/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@


class _MiddlewareClass(Protocol[P]):
def __init__(self, app: ASGIApp, *args: P.args, **kwargs: P.kwargs) -> None:
... # pragma: no cover
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
async def __call__(
self, scope: Scope, receive: Receive, send: Send
) -> None: ... # pragma: no cover


class Middleware:
Expand Down
11 changes: 7 additions & 4 deletions starlette/middleware/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import html
import inspect
import sys
import traceback
import typing

Expand Down Expand Up @@ -237,11 +238,13 @@ def generate_html(self, exc: Exception, limit: int = 7) -> str:
exc_html += self.generate_frame_html(frame, is_collapsed)
is_collapsed = True

if sys.version_info >= (3, 13): # pragma: no cover
exc_type_str = traceback_obj.exc_type_str
else: # pragma: no cover
exc_type_str = traceback_obj.exc_type.__name__

# escape error class and text
error = (
f"{html.escape(traceback_obj.exc_type.__name__)}: "
f"{html.escape(str(traceback_obj))}"
)
error = f"{html.escape(exc_type_str)}: {html.escape(str(traceback_obj))}"

return TEMPLATE.format(styles=STYLES, js=JS, error=error, exc_html=exc_html)

Expand Down
3 changes: 2 additions & 1 deletion starlette/middleware/gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ def __init__(self, app: ASGIApp, minimum_size: int, compresslevel: int = 9) -> N

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
self.send = send
await self.app(scope, receive, self.send_with_gzip)
with self.gzip_buffer, self.gzip_file:
await self.app(scope, receive, self.send_with_gzip)

async def send_with_gzip(self, message: Message) -> None:
message_type = message["type"]
Expand Down
16 changes: 6 additions & 10 deletions starlette/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ def __init__(
self.body = self.render(content)
self.init_headers(headers)

def render(self, content: typing.Any) -> bytes:
def render(self, content: typing.Any) -> bytes | memoryview:
if content is None:
return b""
if isinstance(content, bytes):
if isinstance(content, (bytes, memoryview)):
return content
return content.encode(self.charset) # type: ignore

Expand Down Expand Up @@ -94,7 +94,7 @@ def set_cookie(
value: str = "",
max_age: int | None = None,
expires: datetime | str | int | None = None,
path: str = "/",
path: str | None = "/",
domain: str | None = None,
secure: bool = False,
httponly: bool = False,
Expand Down Expand Up @@ -299,12 +299,10 @@ def __init__(
if self.filename is not None:
content_disposition_filename = quote(self.filename)
if content_disposition_filename != self.filename:
content_disposition = "{}; filename*=utf-8''{}".format(
content_disposition_type, content_disposition_filename
)
content_disposition = f"{content_disposition_type}; filename*=utf-8''{content_disposition_filename}" # noqa: E501
else:
content_disposition = '{}; filename="{}"'.format(
content_disposition_type, self.filename
content_disposition = (
f'{content_disposition_type}; filename="{self.filename}"'
)
self.headers.setdefault("content-disposition", content_disposition)
self.stat_result = stat_result
Expand Down Expand Up @@ -341,8 +339,6 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
)
if scope["method"].upper() == "HEAD":
await send({"type": "http.response.body", "body": b"", "more_body": False})
elif "extensions" in scope and "http.response.pathsend" in scope["extensions"]:
await send({"type": "http.response.pathsend", "path": str(self.path)})
else:
async with await anyio.open_file(self.path, mode="rb") as file:
more_body = True
Expand Down
4 changes: 1 addition & 3 deletions starlette/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None:


def get_name(endpoint: typing.Callable[..., typing.Any]) -> str:
if inspect.isroutine(endpoint) or inspect.isclass(endpoint):
return endpoint.__name__
return endpoint.__class__.__name__
return getattr(endpoint, "__name__", endpoint.__class__.__name__)


def replace_params(
Expand Down
9 changes: 7 additions & 2 deletions starlette/staticfiles.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import errno
import importlib.util
import os
import stat
Expand Down Expand Up @@ -124,8 +125,12 @@ async def get_response(self, path: str, scope: Scope) -> Response:
)
except PermissionError:
raise HTTPException(status_code=401)
except OSError:
raise
except OSError as exc:
# Filename is too long, so it can't be a valid static file.
if exc.errno == errno.ENAMETOOLONG:
raise HTTPException(status_code=404)

raise exc

if stat_result and stat.S_ISREG(stat_result.st_mode):
# We have a static file to serve.
Expand Down
1 change: 1 addition & 0 deletions starlette/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
And RFC 2324 - https://tools.ietf.org/html/rfc2324
"""

from __future__ import annotations

import warnings
Expand Down
Loading

0 comments on commit fa6d9ac

Please sign in to comment.