Browse Source

Fix inconsistent processing of model docstring formfeed char

pull/6039/head
Max McLennan 2 years ago
parent
commit
a4ea039a5c
  1. 3
      fastapi/utils.py
  2. 192
      tests/test_get_model_definitions_formfeed_escape.py

3
fastapi/utils.py

@ -47,9 +47,10 @@ def get_model_definitions(
)
definitions.update(m_definitions)
model_name = model_name_map[model]
definitions[model_name] = m_schema
for m_schema in definitions.values():
if "description" in m_schema:
m_schema["description"] = m_schema["description"].split("\f")[0]
definitions[model_name] = m_schema
return definitions

192
tests/test_get_model_definitions_formfeed_escape.py

@ -0,0 +1,192 @@
from typing import Any, Iterator, Set, Type
import fastapi.openapi.utils
import fastapi.utils
import pydantic.schema
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
class Address(BaseModel):
"""
This is a public description of an Address
\f
You can't see this part of the docstring, it's private!
"""
line_1: str
city: str
state_province: str
class Facility(BaseModel):
id: str
address: Address
app = FastAPI()
client = TestClient(app)
@app.get("/facilities/{facility_id}")
def get_facility(facility_id: str) -> Facility:
...
openapi_schema = {
"components": {
"schemas": {
"Address": {
# NOTE: the description of this model shows only the public-facing text, before the `\f` in docstring
"description": "This is a public " "description of an Address\n",
"properties": {
"city": {"title": "City", "type": "string"},
"line_1": {"title": "Line " "1", "type": "string"},
"state_province": {"title": "State " "Province", "type": "string"},
},
"required": ["line_1", "city", "state_province"],
"title": "Address",
"type": "object",
},
"Facility": {
"properties": {
"address": {"$ref": "#/components/schemas/Address"},
"id": {"title": "Id", "type": "string"},
},
"required": ["id", "address"],
"title": "Facility",
"type": "object",
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": {"$ref": "#/components/schemas/ValidationError"},
"title": "Detail",
"type": "array",
}
},
"title": "HTTPValidationError",
"type": "object",
},
"ValidationError": {
"properties": {
"loc": {
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
"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": "FastAPI", "version": "0.1.0"},
"openapi": "3.0.2",
"paths": {
"/facilities/{facility_id}": {
"get": {
"operationId": "get_facility_facilities__facility_id__get",
"parameters": [
{
"in": "path",
"name": "facility_id",
"required": True,
"schema": {"title": "Facility " "Id", "type": "string"},
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Facility"}
}
},
"description": "Successful " "Response",
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation " "Error",
},
},
"summary": "Get Facility",
}
}
},
}
def test_openapi_schema():
"""
Sanity check to ensure our app's openapi schema renders as we expect
"""
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
class SortedTypeSet(Set[Type[Any]]):
"""
Set of Types whose `__iter__()` method yields results sorted by the type names
"""
def __init__(self, seq: Set[Type[Any]], *, sort_reversed: bool):
"""
:param seq: Initial members of this set
:param sort_reversed: If true, reverse-order the sorting by type name during iteration
"""
super().__init__(seq)
self.sort_reversed = sort_reversed
def __iter__(self) -> Iterator[Type[Any]]:
members_sorted = sorted(
super().__iter__(),
key=lambda type_: type_.__name__,
reverse=self.sort_reversed,
)
for member in members_sorted:
yield member
def test_model_description_escaped_with_formfeed():
"""
Ensure that openapi model descriptions that originate from Pydantic docstrings always truncate the docstring to text
that falls before the formfeed (\f) character. This feature was introduced in (https://github.com/tiangolo/fastapi/pull/3032).
When originally introduced, there was a possibility that the truncation may be ignored depending on the order in which
the models got processed. This created non-deterministic errors, since Pydantic model processing uses unordered sets
and model ordering may differ from one invocation to the next.
This test verifies that (\f) escape of docstrings works in all possible orderings of our two Pydantic model classes.
"""
flat_models = fastapi.openapi.utils.get_flat_models_from_routes(app.routes)
model_name_map = pydantic.schema.get_model_name_map(flat_models)
expected_address_description = "This is a public description of an Address\n"
models_when_sorted_asc = fastapi.utils.get_model_definitions(
flat_models=SortedTypeSet(flat_models, sort_reversed=False),
model_name_map=model_name_map,
)
assert (
models_when_sorted_asc["Address"]["description"] == expected_address_description
)
models_when_sorted_desc = fastapi.utils.get_model_definitions(
flat_models=SortedTypeSet(flat_models, sort_reversed=True),
model_name_map=model_name_map,
)
assert (
models_when_sorted_desc["Address"]["description"]
== expected_address_description
)
Loading…
Cancel
Save