diff --git a/.gitignore b/.gitignore index bff8fa258..25fbf0b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ venv*/ .python-version build/ dist/ +.idea/ diff --git a/requirements.txt b/requirements.txt index 88d6fb011..b8aaaaaa5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ types-PyYAML==6.0.12.20241230 types-dataclasses==0.6.6 pytest==8.3.4 trio==0.28.0 +blockbuster==1.5.13 # Documentation black==24.10.0 diff --git a/starlette/datastructures.py b/starlette/datastructures.py index 90a7296a0..6acf30940 100644 --- a/starlette/datastructures.py +++ b/starlette/datastructures.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys import typing from shlex import shlex from urllib.parse import SplitResult, parse_qsl, urlencode, urlsplit @@ -438,7 +439,7 @@ async def write(self, data: bytes) -> None: if self.size is not None: self.size += len(data) - if self._in_memory: + if self._in_memory and self.file.tell() + len(data) <= getattr(self.file, "_max_size", sys.maxsize): self.file.write(data) else: await run_in_threadpool(self.file.write, data) diff --git a/starlette/middleware/errors.py b/starlette/middleware/errors.py index 76ad776be..6ca0495f8 100644 --- a/starlette/middleware/errors.py +++ b/starlette/middleware/errors.py @@ -6,6 +6,8 @@ import traceback import typing +import anyio + from starlette._utils import is_async_callable from starlette.concurrency import run_in_threadpool from starlette.requests import Request @@ -167,7 +169,7 @@ async def _send(message: Message) -> None: request = Request(scope) if self.debug: # In debug mode, return traceback responses. - response = self.debug_response(request, exc) + response = await anyio.to_thread.run_sync(self.debug_response, request, exc) elif self.handler is None: # Use our default 500 error handler. response = self.error_response(request, exc) diff --git a/starlette/testclient.py b/starlette/testclient.py index fb2ad6e35..804743775 100644 --- a/starlette/testclient.py +++ b/starlette/testclient.py @@ -288,7 +288,7 @@ async def receive() -> Message: await response_complete.wait() return {"type": "http.disconnect"} - body = request.read() + body = await anyio.to_thread.run_sync(request.read) if isinstance(body, str): body_bytes: bytes = body.encode("utf-8") # pragma: no cover elif body is None: diff --git a/tests/conftest.py b/tests/conftest.py index 4db3ae018..e2b5b90a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,23 @@ from __future__ import annotations import functools +from collections.abc import Iterator from typing import Any, Literal import pytest +from blockbuster import blockbuster_ctx from starlette.testclient import TestClient from tests.types import TestClientFactory +@pytest.fixture(autouse=True) +def blockbuster() -> Iterator[None]: + with blockbuster_ctx("starlette") as bb: + bb.functions["os.stat"].can_block_in("/mimetypes.py", "init") + yield + + @pytest.fixture def test_client_factory( anyio_backend_name: Literal["asyncio", "trio"], diff --git a/tests/middleware/test_base.py b/tests/middleware/test_base.py index 7232cfd18..6c94c7578 100644 --- a/tests/middleware/test_base.py +++ b/tests/middleware/test_base.py @@ -468,7 +468,7 @@ async def cancel_on_disconnect( # A timeout is set for 0.1 second in order to ensure that # we never deadlock the test run in an infinite loop - with anyio.move_on_after(0.1): + with anyio.move_on_after(0.2): while True: await send( { diff --git a/tests/test_templates.py b/tests/test_templates.py index 6b2080c17..d8eae97fc 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -23,7 +23,7 @@ def test_templates(tmpdir: Path, test_client_factory: TestClientFactory) -> None with open(path, "w") as file: file.write("Hello, world") - async def homepage(request: Request) -> Response: + def homepage(request: Request) -> Response: return templates.TemplateResponse(request, "index.html") app = Starlette(debug=True, routes=[Route("/", endpoint=homepage)]) @@ -40,7 +40,7 @@ def test_calls_context_processors(tmp_path: Path, test_client_factory: TestClien path = tmp_path / "index.html" path.write_text("Hello {{ username }}") - async def homepage(request: Request) -> Response: + def homepage(request: Request) -> Response: return templates.TemplateResponse(request, "index.html") def hello_world_processor(request: Request) -> dict[str, str]: @@ -69,7 +69,7 @@ def test_template_with_middleware(tmpdir: Path, test_client_factory: TestClientF with open(path, "w") as file: file.write("Hello, world") - async def homepage(request: Request) -> Response: + def homepage(request: Request) -> Response: return templates.TemplateResponse(request, "index.html") class CustomMiddleware(BaseHTTPMiddleware): @@ -96,7 +96,7 @@ def test_templates_with_directories(tmp_path: Path, test_client_factory: TestCli template_a = dir_a / "template_a.html" template_a.write_text(" a") - async def page_a(request: Request) -> Response: + def page_a(request: Request) -> Response: return templates.TemplateResponse(request, "template_a.html") dir_b = tmp_path.resolve() / "b" @@ -104,7 +104,7 @@ async def page_a(request: Request) -> Response: template_b = dir_b / "template_b.html" template_b.write_text(" b") - async def page_b(request: Request) -> Response: + def page_b(request: Request) -> Response: return templates.TemplateResponse(request, "template_b.html") app = Starlette( @@ -150,7 +150,7 @@ def test_templates_with_environment(tmpdir: Path, test_client_factory: TestClien with open(path, "w") as file: file.write("Hello, world") - async def homepage(request: Request) -> Response: + def homepage(request: Request) -> Response: return templates.TemplateResponse(request, "index.html") env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(tmpdir)))