mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-27 08:10:57 -05:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
417a3ab140 | ||
|
|
a3235ed8de | ||
|
|
38495fffa5 | ||
|
|
b77a43bcac | ||
|
|
483eb73b26 | ||
|
|
51a928d3f5 | ||
|
|
e71636e381 | ||
|
|
f7f17fcfd6 |
@@ -1,5 +1,16 @@
|
||||
## Latest changes
|
||||
|
||||
## 0.36.0
|
||||
|
||||
* Fix implementation for `skip_defaults` when returning a Pydantic model. PR [#422](https://github.com/tiangolo/fastapi/pull/422) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Fix OpenAPI generation when using the same dependency in multiple places for the same *path operation*. PR [#417](https://github.com/tiangolo/fastapi/pull/417) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Allow having empty paths in *path operations* used with `include_router` and a `prefix`.
|
||||
* This allows having a router for `/cats` and all its *path operations*, while having one of them for `/cats`.
|
||||
* Now it doesn't have to be only `/cats/` (with a trailing slash).
|
||||
* To use it, declare the path in the *path operation* as the empty string (`""`).
|
||||
* PR [#415](https://github.com/tiangolo/fastapi/pull/415) by [@vitalik](https://github.com/vitalik).
|
||||
* Fix mypy error after merging PR #415. PR [#462](https://github.com/tiangolo/fastapi/pull/462).
|
||||
|
||||
## 0.35.0
|
||||
|
||||
* Fix typo in routing `assert`. PR [#419](https://github.com/tiangolo/fastapi/pull/419) by [@pablogamboa](https://github.com/pablogamboa).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.35.0"
|
||||
__version__ = "0.36.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
@@ -108,7 +108,16 @@ def get_sub_dependant(
|
||||
return sub_dependant
|
||||
|
||||
|
||||
def get_flat_dependant(dependant: Dependant) -> Dependant:
|
||||
CacheKey = Tuple[Optional[Callable], Tuple[str, ...]]
|
||||
|
||||
|
||||
def get_flat_dependant(
|
||||
dependant: Dependant, *, skip_repeats: bool = False, visited: List[CacheKey] = None
|
||||
) -> Dependant:
|
||||
if visited is None:
|
||||
visited = []
|
||||
visited.append(dependant.cache_key)
|
||||
|
||||
flat_dependant = Dependant(
|
||||
path_params=dependant.path_params.copy(),
|
||||
query_params=dependant.query_params.copy(),
|
||||
@@ -120,7 +129,11 @@ def get_flat_dependant(dependant: Dependant) -> Dependant:
|
||||
path=dependant.path,
|
||||
)
|
||||
for sub_dependant in dependant.dependencies:
|
||||
flat_sub = get_flat_dependant(sub_dependant)
|
||||
if skip_repeats and sub_dependant.cache_key in visited:
|
||||
continue
|
||||
flat_sub = get_flat_dependant(
|
||||
sub_dependant, skip_repeats=skip_repeats, visited=visited
|
||||
)
|
||||
flat_dependant.path_params.extend(flat_sub.path_params)
|
||||
flat_dependant.query_params.extend(flat_sub.query_params)
|
||||
flat_dependant.header_params.extend(flat_sub.header_params)
|
||||
|
||||
@@ -45,7 +45,7 @@ validation_error_response_definition = {
|
||||
|
||||
|
||||
def get_openapi_params(dependant: Dependant) -> List[Field]:
|
||||
flat_dependant = get_flat_dependant(dependant)
|
||||
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
|
||||
return (
|
||||
flat_dependant.path_params
|
||||
+ flat_dependant.query_params
|
||||
@@ -150,7 +150,7 @@ def get_openapi_path(
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(route=route, method=method)
|
||||
parameters: List[Dict] = []
|
||||
flat_dependant = get_flat_dependant(route.dependant)
|
||||
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
|
||||
security_definitions, operation_security = get_openapi_security_definitions(
|
||||
flat_dependant=flat_dependant
|
||||
)
|
||||
|
||||
@@ -52,6 +52,8 @@ def serialize_response(
|
||||
errors.extend(errors_)
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
if skip_defaults and isinstance(response, BaseModel):
|
||||
value = response.dict(skip_defaults=skip_defaults)
|
||||
return jsonable_encoder(
|
||||
value,
|
||||
include=include,
|
||||
@@ -201,7 +203,6 @@ class APIRoute(routing.Route):
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
dependency_overrides_provider: Any = None,
|
||||
) -> None:
|
||||
assert path.startswith("/"), "Routed paths must always start with '/'"
|
||||
self.path = path
|
||||
self.endpoint = endpoint
|
||||
self.name = get_name(endpoint) if name is None else name
|
||||
@@ -448,6 +449,14 @@ class APIRouter(routing.Router):
|
||||
assert not prefix.endswith(
|
||||
"/"
|
||||
), "A path prefix must not end with '/', as the routes will start with '/'"
|
||||
else:
|
||||
for r in router.routes:
|
||||
path = getattr(r, "path")
|
||||
name = getattr(r, "name", "unknown")
|
||||
if path is not None and not path:
|
||||
raise Exception(
|
||||
f"Prefix and path cannot be both empty (path operation: {name})"
|
||||
)
|
||||
if responses is None:
|
||||
responses = {}
|
||||
for route in router.routes:
|
||||
|
||||
33
tests/test_empty_router.py
Normal file
33
tests/test_empty_router.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_empty():
|
||||
return ["OK"]
|
||||
|
||||
|
||||
app.include_router(router, prefix="/prefix")
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_use_empty():
|
||||
with client:
|
||||
response = client.get("/prefix")
|
||||
assert response.json() == ["OK"]
|
||||
|
||||
response = client.get("/prefix/")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_include_empty():
|
||||
# if both include and router.path are empty - it should raise exception
|
||||
with pytest.raises(Exception):
|
||||
app.include_router(router)
|
||||
103
tests/test_repeated_dependency_schema.py
Normal file
103
tests/test_repeated_dependency_schema.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from fastapi import Depends, FastAPI, Header
|
||||
from starlette.status import HTTP_200_OK
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
def get_header(*, someheader: str = Header(...)):
|
||||
return someheader
|
||||
|
||||
|
||||
def get_something_else(*, someheader: str = Depends(get_header)):
|
||||
return f"{someheader}123"
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def get_deps(dep1: str = Depends(get_header), dep2: str = Depends(get_something_else)):
|
||||
return {"dep1": dep1, "dep2": dep2}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
schema = {
|
||||
"components": {
|
||||
"schemas": {
|
||||
"HTTPValidationError": {
|
||||
"properties": {
|
||||
"detail": {
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
}
|
||||
},
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
"items": {"type": "string"},
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error " "Type", "type": "string"},
|
||||
},
|
||||
"required": ["loc", "msg", "type"],
|
||||
"title": "ValidationError",
|
||||
"type": "object",
|
||||
},
|
||||
}
|
||||
},
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"openapi": "3.0.2",
|
||||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"operationId": "get_deps__get",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "header",
|
||||
"name": "someheader",
|
||||
"required": True,
|
||||
"schema": {"title": "Someheader", "type": "string"},
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
"description": "Successful " "Response",
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Validation " "Error",
|
||||
},
|
||||
},
|
||||
"summary": "Get Deps",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == HTTP_200_OK
|
||||
actual_schema = response.json()
|
||||
assert actual_schema == schema
|
||||
assert (
|
||||
len(actual_schema["paths"]["/"]["get"]["parameters"]) == 1
|
||||
) # primary goal of this test
|
||||
|
||||
|
||||
def test_response():
|
||||
response = client.get("/", headers={"someheader": "hello"})
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {"dep1": "hello", "dep2": "hello123"}
|
||||
29
tests/test_skip_defaults.py
Normal file
29
tests/test_skip_defaults.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class SubModel(BaseModel):
|
||||
a: Optional[str] = "foo"
|
||||
|
||||
|
||||
class Model(BaseModel):
|
||||
x: Optional[int]
|
||||
sub: SubModel
|
||||
|
||||
|
||||
@app.get("/", response_model=Model, response_model_skip_defaults=True)
|
||||
def get() -> Model:
|
||||
return Model(sub={})
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_return_defaults():
|
||||
response = client.get("/")
|
||||
assert response.json() == {"sub": {}}
|
||||
Reference in New Issue
Block a user