mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 06:46:14 +00:00
Chore: Code formatting (#594)
## About A few cosmetic adjustments aka. code formatting. Also validate the outcome on CI/GHA. Feel free to improve now or later at your disposal. ## Details The updates are based on using the most recent versions of pyproject-fmt and ruff. Specifically, spots marked with `noqa` might need further love, also at your disposal. --------- Co-authored-by: Kenneth Reitz <me@kennethreitz.org>
This commit is contained in:
@@ -8,10 +8,8 @@ import responder
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
# Startup: initialize resources
|
||||
print("Starting up...")
|
||||
yield
|
||||
# Shutdown: clean up resources
|
||||
print("Shutting down...")
|
||||
|
||||
|
||||
api = responder.API(lifespan=lifespan)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Complete REST API example with Pydantic validation.
|
||||
# https://responder.kennethreitz.org/tutorial-rest.html
|
||||
import responder
|
||||
from pydantic import BaseModel
|
||||
|
||||
import responder
|
||||
|
||||
|
||||
class BookIn(BaseModel):
|
||||
title: str
|
||||
@@ -35,8 +36,13 @@ def list_books(req, resp):
|
||||
resp.media = list(books_db.values())
|
||||
|
||||
|
||||
@api.route("/books", methods=["POST"], check_existing=False,
|
||||
request_model=BookIn, response_model=BookOut)
|
||||
@api.route(
|
||||
"/books",
|
||||
methods=["POST"],
|
||||
check_existing=False,
|
||||
request_model=BookIn,
|
||||
response_model=BookOut,
|
||||
)
|
||||
async def create_book(req, resp):
|
||||
global next_id
|
||||
data = await req.media()
|
||||
|
||||
@@ -35,7 +35,7 @@ def index(req, resp):
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
@api.route("/chat", websocket=True)
|
||||
@@ -47,7 +47,7 @@ async def chat(ws):
|
||||
message = await ws.receive_text()
|
||||
for client in connected:
|
||||
await client.send_text(message)
|
||||
except Exception:
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
finally:
|
||||
connected.discard(ws)
|
||||
|
||||
+51
-70
@@ -8,7 +8,7 @@ requires = [
|
||||
name = "responder"
|
||||
description = "A familiar HTTP Service Framework for Python."
|
||||
readme = "README.md"
|
||||
license = {text = "Apache 2.0"}
|
||||
license = { text = "Apache 2.0" }
|
||||
authors = [
|
||||
{ name = "Kenneth Reitz", email = "me@kennethreitz.org" },
|
||||
]
|
||||
@@ -20,7 +20,7 @@ classifiers = [
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
@@ -31,10 +31,10 @@ classifiers = [
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dynamic = [ "version" ]
|
||||
dependencies = [
|
||||
"a2wsgi",
|
||||
"apispec>=1.0.0",
|
||||
"apispec>=1",
|
||||
"chardet",
|
||||
"docopt-ng",
|
||||
"graphene>=3",
|
||||
@@ -44,17 +44,15 @@ dependencies = [
|
||||
"pueblo[sfa-full]>=0.0.11",
|
||||
"pydantic>=2",
|
||||
"python-multipart",
|
||||
"starlette[full]>=1.0",
|
||||
"starlette[full]>=1",
|
||||
"uvicorn[standard]",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
develop = [
|
||||
optional-dependencies.develop = [
|
||||
"pyproject-fmt",
|
||||
"ruff",
|
||||
"validate-pyproject",
|
||||
]
|
||||
docs = [
|
||||
optional-dependencies.docs = [
|
||||
"alabaster<1.1",
|
||||
"myst-parser",
|
||||
"sphinx>=5,<9",
|
||||
@@ -62,8 +60,8 @@ docs = [
|
||||
"sphinx-copybutton",
|
||||
"sphinx-design-elements",
|
||||
]
|
||||
release = ["build", "twine"]
|
||||
test = [
|
||||
optional-dependencies.release = [ "build", "twine" ]
|
||||
optional-dependencies.test = [
|
||||
"flask",
|
||||
"mypy",
|
||||
"pytest",
|
||||
@@ -71,32 +69,22 @@ test = [
|
||||
"pytest-mock",
|
||||
"pytest-rerunfailures",
|
||||
]
|
||||
urls.Documentation = "https://responder.kennethreitz.org"
|
||||
urls.Homepage = "https://github.com/kennethreitz/responder"
|
||||
urls.Issues = "https://github.com/kennethreitz/responder/issues"
|
||||
urls.Repository = "https://github.com/kennethreitz/responder"
|
||||
scripts.responder = "responder.ext.cli:cli"
|
||||
|
||||
[project.scripts]
|
||||
responder = "responder.ext.cli:cli"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/kennethreitz/responder"
|
||||
Documentation = "https://responder.kennethreitz.org"
|
||||
Repository = "https://github.com/kennethreitz/responder"
|
||||
Issues = "https://github.com/kennethreitz/responder/issues"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "responder.__version__.__version__"}
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
responder = ["py.typed", "ext/openapi/docs/*.html"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
exclude = ["tests"]
|
||||
[tool.setuptools]
|
||||
dynamic.version = { attr = "responder.__version__.__version__" }
|
||||
package-data.responder = [ "py.typed", "ext/openapi/docs/*.html" ]
|
||||
packages.find.exclude = [ "tests" ]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 90
|
||||
|
||||
extend-exclude = [
|
||||
"docs/source/conf.py",
|
||||
]
|
||||
|
||||
lint.select = [
|
||||
# Builtins
|
||||
"A",
|
||||
@@ -124,59 +112,20 @@ lint.select = [
|
||||
# flake8-2020
|
||||
"YTT",
|
||||
]
|
||||
|
||||
lint.extend-ignore = [
|
||||
"S101", # Allow use of `assert`.
|
||||
]
|
||||
|
||||
lint.per-file-ignores."responder/util/cmd.py" = [ "A005" ] # Module shadows a Python standard-library module
|
||||
|
||||
lint.per-file-ignores."tests/*" = [
|
||||
"ERA001", # Found commented-out code.
|
||||
"S101", # Allow use of `assert`, and `print`.
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = """
|
||||
-rfEXs -p pytester --strict-markers --verbosity=3
|
||||
--cov --cov-report=term-missing --cov-report=xml
|
||||
"""
|
||||
filterwarnings = [
|
||||
"error::UserWarning",
|
||||
]
|
||||
log_level = "DEBUG"
|
||||
log_cli_level = "DEBUG"
|
||||
log_format = "%(asctime)-15s [%(name)-36s] %(levelname)-8s: %(message)s"
|
||||
minversion = "2.0"
|
||||
testpaths = [
|
||||
"responder",
|
||||
"tests",
|
||||
]
|
||||
markers = [
|
||||
]
|
||||
xfail_strict = true
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = false
|
||||
omit = [
|
||||
"*.html",
|
||||
"tests/*",
|
||||
]
|
||||
|
||||
[tool.coverage.report]
|
||||
fail_under = 0
|
||||
show_missing = true
|
||||
exclude_lines = [
|
||||
"# pragma: no cover",
|
||||
"raise NotImplemented",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
packages = [
|
||||
"responder",
|
||||
]
|
||||
exclude = [
|
||||
]
|
||||
exclude = []
|
||||
check_untyped_defs = true
|
||||
explicit_package_bases = true
|
||||
ignore_missing_imports = true
|
||||
@@ -184,3 +133,35 @@ implicit_optional = true
|
||||
install_types = true
|
||||
namespace_packages = true
|
||||
non_interactive = true
|
||||
|
||||
[tool.pytest]
|
||||
ini_options.addopts = """
|
||||
-rfEXs -p pytester --strict-markers --verbosity=3
|
||||
--cov --cov-report=term-missing --cov-report=xml
|
||||
"""
|
||||
ini_options.filterwarnings = [
|
||||
"error::UserWarning",
|
||||
]
|
||||
ini_options.log_level = "DEBUG"
|
||||
ini_options.log_cli_level = "DEBUG"
|
||||
ini_options.log_format = "%(asctime)-15s [%(name)-36s] %(levelname)-8s: %(message)s"
|
||||
ini_options.minversion = "2.0"
|
||||
ini_options.testpaths = [
|
||||
"responder",
|
||||
"tests",
|
||||
]
|
||||
ini_options.markers = []
|
||||
ini_options.xfail_strict = true
|
||||
|
||||
[tool.coverage]
|
||||
run.branch = false
|
||||
run.omit = [
|
||||
"*.html",
|
||||
"tests/*",
|
||||
]
|
||||
report.exclude_lines = [
|
||||
"# pragma: no cover",
|
||||
"raise NotImplemented",
|
||||
]
|
||||
report.fail_under = 0
|
||||
report.show_missing = true
|
||||
|
||||
+2
-4
@@ -161,9 +161,7 @@ class API:
|
||||
import uuid as _uuid
|
||||
|
||||
def _add_request_id(req, resp):
|
||||
rid = req.headers.get(
|
||||
"X-Request-ID", str(_uuid.uuid4())
|
||||
)
|
||||
rid = req.headers.get("X-Request-ID", str(_uuid.uuid4()))
|
||||
resp.headers["X-Request-ID"] = rid
|
||||
|
||||
self.router.after_request(_add_request_id)
|
||||
@@ -562,7 +560,7 @@ class API:
|
||||
"""Run the application. Shorthand for :meth:`serve` that inherits the ``debug`` setting.
|
||||
|
||||
:param kwargs: Keyword arguments passed through to :meth:`serve`.
|
||||
"""
|
||||
""" # noqa: E501
|
||||
if "debug" not in kwargs:
|
||||
kwargs.update({"debug": self.debug})
|
||||
self.serve(**kwargs)
|
||||
|
||||
@@ -129,7 +129,9 @@ class OpenAPISchema:
|
||||
op["requestBody"] = {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": f"#/components/schemas/{model_name}"}
|
||||
"schema": {
|
||||
"$ref": f"#/components/schemas/{model_name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,7 @@ class RateLimiter:
|
||||
def _cleanup(self, key):
|
||||
now = time.time()
|
||||
cutoff = now - self.period
|
||||
self._buckets[key] = [
|
||||
t for t in self._buckets[key] if t > cutoff
|
||||
]
|
||||
self._buckets[key] = [t for t in self._buckets[key] if t > cutoff]
|
||||
|
||||
def check(self, req, resp):
|
||||
"""Check rate limit. Sets 429 status if exceeded."""
|
||||
|
||||
+1
-1
@@ -192,7 +192,7 @@ class Route(BaseRoute):
|
||||
try:
|
||||
validated = resp_model(**response.media)
|
||||
response.media = validated.model_dump()
|
||||
except Exception:
|
||||
except Exception: # noqa: S110
|
||||
pass # Don't break the response if serialization fails
|
||||
|
||||
# Run after-request hooks
|
||||
|
||||
+14
-11
@@ -4,14 +4,14 @@ import time
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient as StarletteTestClient
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
import responder
|
||||
from responder.background import BackgroundQueue
|
||||
from responder.models import CaseInsensitiveDict, QueryDict, Response
|
||||
from responder.models import QueryDict
|
||||
from responder.routes import Route, WebSocketRoute
|
||||
from responder.templates import Templates
|
||||
|
||||
|
||||
# --- api.py coverage ---
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ def test_background_task_exception(capsys):
|
||||
raise ValueError("task failed")
|
||||
|
||||
future = failing_task()
|
||||
future.result # wait for completion
|
||||
future.result # wait for completion # noqa: B018
|
||||
time.sleep(0.2) # let the done callback fire
|
||||
|
||||
captured = capsys.readouterr()
|
||||
@@ -302,7 +302,7 @@ def test_yaml_content_negotiation(api):
|
||||
def test_websocket_404(api):
|
||||
"""Lines 308-310: WebSocket to unknown route gets closed."""
|
||||
client = StarletteTestClient(api)
|
||||
with pytest.raises(Exception):
|
||||
with pytest.raises(WebSocketDisconnect):
|
||||
with client.websocket_connect("ws://;/nonexistent"):
|
||||
pass
|
||||
|
||||
@@ -325,9 +325,7 @@ def test_websocket_route_params():
|
||||
pass
|
||||
|
||||
route = WebSocketRoute("/ws/{room_id:int}", handler)
|
||||
matches, scope = route.matches(
|
||||
{"type": "websocket", "path": "/ws/42"}
|
||||
)
|
||||
matches, scope = route.matches({"type": "websocket", "path": "/ws/42"})
|
||||
assert matches is True
|
||||
assert scope["path_params"] == {"room_id": 42}
|
||||
|
||||
@@ -591,7 +589,10 @@ def test_pydantic_schema():
|
||||
from pydantic import BaseModel
|
||||
|
||||
api = responder.API(
|
||||
title="Test", version="1.0", openapi="3.0.2", allowed_hosts=[";"],
|
||||
title="Test",
|
||||
version="1.0",
|
||||
openapi="3.0.2",
|
||||
allowed_hosts=[";"],
|
||||
)
|
||||
|
||||
@api.schema("Pet")
|
||||
@@ -611,7 +612,10 @@ def test_pydantic_request_response_models():
|
||||
from pydantic import BaseModel
|
||||
|
||||
api = responder.API(
|
||||
title="Test", version="1.0", openapi="3.0.2", allowed_hosts=[";"],
|
||||
title="Test",
|
||||
version="1.0",
|
||||
openapi="3.0.2",
|
||||
allowed_hosts=[";"],
|
||||
)
|
||||
|
||||
class ItemIn(BaseModel):
|
||||
@@ -623,8 +627,7 @@ def test_pydantic_request_response_models():
|
||||
name: str
|
||||
price: float
|
||||
|
||||
@api.route("/items", methods=["POST"],
|
||||
request_model=ItemIn, response_model=ItemOut)
|
||||
@api.route("/items", methods=["POST"], request_model=ItemIn, response_model=ItemOut)
|
||||
async def create(req, resp):
|
||||
data = await req.media()
|
||||
resp.media = {"id": 1, **data}
|
||||
|
||||
+24
-10
@@ -109,10 +109,13 @@ def test_graphql_error_response(api, schema):
|
||||
def test_graphql_variables_json(api, schema):
|
||||
"""Variables passed via JSON body."""
|
||||
api.add_route("/", GraphQLView(schema=schema, api=api))
|
||||
r = api.requests.post("http://;/", json={
|
||||
r = api.requests.post(
|
||||
"http://;/",
|
||||
json={
|
||||
"query": "query Hello($name: String!) { hello(name: $name) }",
|
||||
"variables": {"name": "Alice"},
|
||||
})
|
||||
},
|
||||
)
|
||||
assert r.json() == {"data": {"hello": "Hello Alice"}}
|
||||
|
||||
|
||||
@@ -121,7 +124,8 @@ def test_graphql_variables_query_param(api, schema):
|
||||
api.add_route("/", GraphQLView(schema=schema, api=api))
|
||||
variables = json.dumps({"name": "Bob"})
|
||||
r = api.requests.get(
|
||||
f"http://;/?query=query Hello($name: String!) {{ hello(name: $name) }}&variables={variables}",
|
||||
f"http://;/?query=query Hello($name: String!) "
|
||||
f"{{ hello(name: $name) }}&variables={variables}",
|
||||
headers={"Accept": "json"},
|
||||
)
|
||||
assert r.json() == {"data": {"hello": "Hello Bob"}}
|
||||
@@ -134,10 +138,13 @@ def test_graphql_operation_name_json(api, multi_op_schema):
|
||||
query SayHello { hello }
|
||||
query SayGoodbye { goodbye }
|
||||
"""
|
||||
r = api.requests.post("http://;/", json={
|
||||
r = api.requests.post(
|
||||
"http://;/",
|
||||
json={
|
||||
"query": query,
|
||||
"operationName": "SayHello",
|
||||
})
|
||||
},
|
||||
)
|
||||
data = r.json()
|
||||
assert data["data"]["hello"] == "Hello stranger"
|
||||
|
||||
@@ -157,9 +164,12 @@ def test_graphql_operation_name_query_param(api, multi_op_schema):
|
||||
def test_graphql_mutation(api, mutation_schema):
|
||||
"""Mutations work via JSON body."""
|
||||
api.add_route("/", GraphQLView(schema=mutation_schema, api=api))
|
||||
r = api.requests.post("http://;/", json={
|
||||
r = api.requests.post(
|
||||
"http://;/",
|
||||
json={
|
||||
"query": 'mutation { createUser(name: "Eve") { ok name } }',
|
||||
})
|
||||
},
|
||||
)
|
||||
data = r.json()
|
||||
assert data["data"]["createUser"]["ok"] is True
|
||||
assert data["data"]["createUser"]["name"] == "Eve"
|
||||
@@ -168,10 +178,14 @@ def test_graphql_mutation(api, mutation_schema):
|
||||
def test_graphql_mutation_with_variables(api, mutation_schema):
|
||||
"""Mutations with variables."""
|
||||
api.add_route("/", GraphQLView(schema=mutation_schema, api=api))
|
||||
r = api.requests.post("http://;/", json={
|
||||
"query": "mutation CreateUser($name: String!) { createUser(name: $name) { ok name } }",
|
||||
r = api.requests.post(
|
||||
"http://;/",
|
||||
json={
|
||||
"query": "mutation CreateUser($name: String!) "
|
||||
"{ createUser(name: $name) { ok name } }",
|
||||
"variables": {"name": "Frank"},
|
||||
})
|
||||
},
|
||||
)
|
||||
data = r.json()
|
||||
assert data["data"]["createUser"]["ok"] is True
|
||||
assert data["data"]["createUser"]["name"] == "Frank"
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
"""Tests for new features: validation, SSE, after_request, route groups, etc."""
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient as StarletteTestClient
|
||||
|
||||
import responder
|
||||
from responder.ext.ratelimit import RateLimiter
|
||||
|
||||
|
||||
# --- Pydantic auto-validation ---
|
||||
|
||||
|
||||
@@ -42,7 +39,9 @@ def test_pydantic_request_validation():
|
||||
assert "errors" in r.json()
|
||||
|
||||
# Invalid request — wrong type
|
||||
r = api.requests.post("http://;/items", json={"name": "widget", "price": "not_a_number"})
|
||||
r = api.requests.post(
|
||||
"http://;/items", json={"name": "widget", "price": "not_a_number"}
|
||||
)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
@@ -50,8 +49,7 @@ def test_pydantic_response_serialization():
|
||||
"""Auto-serialize response through response_model."""
|
||||
api = responder.API(allowed_hosts=[";"])
|
||||
|
||||
@api.route("/items", methods=["POST"],
|
||||
request_model=ItemIn, response_model=ItemOut)
|
||||
@api.route("/items", methods=["POST"], request_model=ItemIn, response_model=ItemOut)
|
||||
async def create(req, resp):
|
||||
data = await req.media()
|
||||
# Include an extra field that should be stripped by the model
|
||||
@@ -257,7 +255,7 @@ def test_rate_limiter():
|
||||
def view(req, resp):
|
||||
resp.text = "ok"
|
||||
|
||||
for i in range(3):
|
||||
for _i in range(3):
|
||||
r = api.requests.get("http://;/")
|
||||
assert r.status_code == 200
|
||||
assert "X-RateLimit-Remaining" in r.headers
|
||||
|
||||
Reference in New Issue
Block a user