mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 06:46:14 +00:00
0cbcaf9c4f
## Summary
Comprehensive post-v3.0.0 modernization: new features, bug fixes,
dependency cleanup, docs, and test coverage.
**New features:**
- **HTTP method filtering** — `@api.route("/data", methods=["GET"])`
- **Lifespan context manager** — modern async startup/shutdown
- **`api.exception_handler()`** — custom error handling per exception
type
- **`api.graphql()`** — one-liner GraphQL setup
- **`resp.file()`** — serve files from disk with auto content-type
- **before_request short-circuit** — set status code to skip route
handler
- **`req.path_params`** / **`req.client`** / **`req.is_json`** — new
request properties
- **`uuid`** and **`path`** route convertors
- **PEP 561 `py.typed`** marker
**Bug fixes:**
- Fix multipart parser losing headers
- Fix `url_for()` with typed params (`{id:int}`)
- Fix `resp.body` encoding crash on bytes content
- Fix Python 3.9 type syntax (`from __future__ import annotations`)
- Fix broken session test and no-op file upload test
- Fix helloworld example 404 on root path
**Dependencies:**
- Flattened — `pip install responder` gets everything
- Core: just starlette + uvicorn (down from 10 deps)
**Docs & README:**
- All new features documented in tour
- Modernized README features list
- Deployment guide: Docker, cloud, uvicorn
- Removed Pipenv, extras, stale references throughout
**Tests & quality:**
- 117 tests (up from 92), 91% coverage, 0 warnings
- CaseInsensitiveDict, GraphQL edge cases, staticfiles tests
- Ruff clean, all `tmpdir` → `tmp_path`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
135 lines
3.9 KiB
Python
135 lines
3.9 KiB
Python
"""
|
|
Utility functions for testing server components.
|
|
|
|
This module provides functions for managing test server instances,
|
|
including port allocation and server readiness checking.
|
|
"""
|
|
|
|
import errno
|
|
import logging
|
|
import socket
|
|
import time
|
|
import typing as t
|
|
from copy import copy
|
|
from functools import lru_cache
|
|
from urllib.request import urlopen
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def random_port() -> int:
|
|
"""
|
|
Return a random available port by binding to port 0.
|
|
|
|
Returns:
|
|
int: An available port number that can be used for testing.
|
|
"""
|
|
sock = socket.socket()
|
|
try:
|
|
sock.bind(("", 0))
|
|
return sock.getsockname()[1]
|
|
finally:
|
|
sock.close()
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
def transient_socket_error_numbers() -> t.List[int]:
|
|
"""
|
|
A list of TCP socket error numbers to ignore in `wait_server_tcp`.
|
|
|
|
On Windows, Winsock error codes are the Unix error code + 10000.
|
|
|
|
Returns:
|
|
List[int]: A list containing both Unix and Windows-specific error codes.
|
|
For each Unix error code 'x', includes both 'x' and 'x + 10000'.
|
|
"""
|
|
error_numbers = [
|
|
errno.EAGAIN,
|
|
errno.ECONNABORTED,
|
|
errno.ECONNREFUSED,
|
|
errno.ETIMEDOUT,
|
|
errno.EWOULDBLOCK,
|
|
]
|
|
error_numbers_effective = copy(error_numbers)
|
|
error_numbers_effective.extend(error_number + 10000 for error_number in error_numbers)
|
|
return error_numbers_effective
|
|
|
|
|
|
def wait_server_tcp(
|
|
port: int,
|
|
host: str = "127.0.0.1",
|
|
timeout: int = 10,
|
|
delay: float = 0.1,
|
|
) -> None:
|
|
"""
|
|
Wait for server to be ready by attempting TCP connections.
|
|
|
|
Args:
|
|
port: The port number to connect to
|
|
host: The host to connect to (default: "127.0.0.1")
|
|
timeout: Maximum time to wait in seconds (default: 10)
|
|
delay: Delay between attempts in seconds (default: 0.1)
|
|
|
|
Raises:
|
|
RuntimeError: If server is not ready within timeout period
|
|
"""
|
|
endpoint = f"tcp://{host}:{port}/"
|
|
logger.debug(f"Waiting for endpoint: {endpoint}")
|
|
start_time = time.time()
|
|
while time.time() - start_time < timeout:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.settimeout(delay / 2) # Set socket timeout
|
|
error_number = sock.connect_ex((host, port))
|
|
if error_number == 0:
|
|
break
|
|
|
|
# Expected errors when server is not ready.
|
|
if error_number in transient_socket_error_numbers():
|
|
pass
|
|
|
|
# Unexpected error.
|
|
else:
|
|
raise RuntimeError(
|
|
f"Unexpected error while connecting to {endpoint}: {error_number}"
|
|
)
|
|
time.sleep(delay)
|
|
else:
|
|
raise RuntimeError(
|
|
f"Server at {endpoint} failed to start within {timeout} seconds"
|
|
)
|
|
|
|
|
|
def wait_server_http(
|
|
port: int,
|
|
host: str = "127.0.0.1",
|
|
protocol: str = "http",
|
|
attempts: int = 20,
|
|
delay: float = 0.1,
|
|
) -> None:
|
|
"""
|
|
Wait for server to be ready by attempting to connect to it.
|
|
|
|
Args:
|
|
port: The port number to connect to
|
|
host: The host to connect to (default: "127.0.0.1")
|
|
protocol: The protocol to use (default: "http")
|
|
attempts: Number of connection attempts (default: 20)
|
|
delay: Delay per attempt in seconds (default: 0.1)
|
|
|
|
Raises:
|
|
RuntimeError: If server is not ready after all attempts
|
|
"""
|
|
url = f"{protocol}://{host}:{port}/"
|
|
for attempt in range(1, attempts + 1):
|
|
try:
|
|
urlopen(url, timeout=delay / 2) # noqa: S310
|
|
break
|
|
except OSError:
|
|
if attempt < attempts: # Don't sleep on last attempt
|
|
time.sleep(delay)
|
|
else:
|
|
raise RuntimeError(
|
|
f"Server at {url} failed to respond after {attempts} attempts "
|
|
f"(total wait time: {attempts * delay:.1f}s)"
|
|
)
|