1
0
Fork 0

Adding upstream version 0.5.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-04 22:22:32 +02:00
parent 303fa6e9d8
commit 97e6d74bac
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
110 changed files with 12006 additions and 0 deletions

0
tests/v3_0/__init__.py Normal file
View file

View file

@ -0,0 +1,93 @@
from typing import Any
from openapi_pydantic.compat import PYDANTIC_V2
from openapi_pydantic.v3.v3_0 import (
XML,
Callback,
Components,
Contact,
Discriminator,
Encoding,
Example,
ExternalDocumentation,
Header,
Info,
License,
Link,
MediaType,
OAuthFlow,
OAuthFlows,
OpenAPI,
Operation,
Parameter,
PathItem,
Paths,
Reference,
RequestBody,
Response,
Responses,
Schema,
SecurityRequirement,
SecurityScheme,
Server,
ServerVariable,
Tag,
)
def test_config_example() -> None:
all_types = [
OpenAPI,
Info,
Contact,
License,
Server,
ServerVariable,
Components,
Paths,
PathItem,
Operation,
ExternalDocumentation,
Parameter,
RequestBody,
MediaType,
Encoding,
Responses,
Response,
Callback,
Example,
Link,
Header,
Tag,
Reference,
Schema,
Discriminator,
XML,
SecurityScheme,
OAuthFlows,
OAuthFlow,
SecurityRequirement,
]
for schema_type in all_types:
_assert_config_examples(schema_type)
def _assert_config_examples(schema_type: Any) -> None:
if PYDANTIC_V2:
if not hasattr(schema_type, "model_config"):
return
extra = schema_type.model_config.get("json_schema_extra")
if extra is not None:
examples = extra["examples"]
for example_dict in examples:
obj = schema_type.model_validate(example_dict)
assert obj.model_fields_set
else:
Config = getattr(schema_type, "Config", None)
schema_extra = getattr(Config, "schema_extra", None)
if schema_extra is not None:
examples = schema_extra["examples"]
for example_dict in examples:
obj = schema_type(**example_dict)
assert obj.__fields_set__

View file

@ -0,0 +1,34 @@
import pytest
from pydantic import ValidationError
from openapi_pydantic.v3.v3_0 import Schema
@pytest.mark.parametrize(
"datatype",
(
"string",
"number",
"integer",
"boolean",
"array",
"object",
),
)
def test_good_types_parse_and_equate(datatype: str) -> None:
assert Schema(type=datatype).type == datatype
def test_bad_types_raise_validation_errors() -> None:
with pytest.raises(ValidationError):
Schema(type="invalid")
with pytest.raises(ValidationError):
Schema(anyOf=[{"type": "invalid"}])
with pytest.raises(ValidationError):
Schema(
properties={
"a": Schema(type="invalid"),
},
)

View file

@ -0,0 +1,117 @@
# mypy: ignore-errors
from typing import Optional
import pytest
from openapi_pydantic.compat import PYDANTIC_V2
from openapi_pydantic.v3.v3_0 import (
Info,
MediaType,
OpenAPI,
Operation,
PathItem,
RequestBody,
Response,
Schema,
)
from openapi_pydantic.v3.v3_0.util import (
PydanticSchema,
construct_open_api_with_schema_class,
)
@pytest.mark.skipif(not PYDANTIC_V2, reason="computed fields require Pydantic V2")
def test_optional_and_computed_fields() -> None:
api = construct_sample_api()
result = construct_open_api_with_schema_class(api)
assert result.components is not None
assert result.components.schemas is not None
req_schema = result.components.schemas["SampleRequest"]
assert isinstance(req_schema, Schema)
assert req_schema.properties is not None
assert req_schema.required is not None
resp_schema = result.components.schemas["SampleResponse"]
assert isinstance(resp_schema, Schema)
assert resp_schema.properties is not None
assert resp_schema.required is not None
# When validating:
# - required fields are still required
# - optional fields are still optional
# - computed fields don't exist
assert "req" in req_schema.properties
assert "opt" in req_schema.properties
assert "comp" not in req_schema.properties
assert set(req_schema.required) == {"req"}
# When serializing:
# - required fields are still required
# - optional fields are still optional
# (except when json_schema_serialization_defaults_required is enabled)
# - computed fields are required
assert "req" in resp_schema.properties
assert "opt" in resp_schema.properties
assert "comp" in resp_schema.properties
assert set(resp_schema.required) == {"req", "comp"}
def construct_sample_api() -> OpenAPI:
from typing import TYPE_CHECKING, Callable
from pydantic import BaseModel
if TYPE_CHECKING:
def computed_field(x: Callable) -> Callable: ...
else:
from pydantic import computed_field
class SampleModel(BaseModel):
req: bool
opt: Optional[bool] = None
@computed_field # type: ignore
@property
def comp(self) -> bool:
return True
class SampleRequest(SampleModel):
model_config = {"json_schema_mode": "validation"}
class SampleResponse(SampleModel):
model_config = {"json_schema_mode": "serialization"}
return OpenAPI(
info=Info(
title="Sample API",
version="v0.0.1",
),
paths={
"/callme": PathItem(
post=Operation(
requestBody=RequestBody(
content={
"application/json": MediaType(
schema=PydanticSchema(schema_class=SampleRequest)
)
}
),
responses={
"200": Response(
description="resp",
content={
"application/json": MediaType(
schema=PydanticSchema(schema_class=SampleResponse)
)
},
)
},
)
)
},
)

330
tests/v3_0/test_util.py Normal file
View file

@ -0,0 +1,330 @@
import logging
from typing import Callable, Generic, Literal, TypeVar
import pytest
from pydantic import BaseModel, Field
from openapi_pydantic.compat import PYDANTIC_V2
from openapi_pydantic.v3.v3_0 import (
Info,
MediaType,
OpenAPI,
Operation,
PathItem,
Reference,
RequestBody,
Response,
Schema,
)
from openapi_pydantic.v3.v3_0.util import (
PydanticSchema,
construct_open_api_with_schema_class,
)
def test_construct_open_api_with_schema_class_1() -> None:
open_api = construct_base_open_api_1()
result_open_api_1 = construct_open_api_with_schema_class(open_api)
result_open_api_2 = construct_open_api_with_schema_class(
open_api, [PingRequest, PingResponse]
)
assert result_open_api_1.components == result_open_api_2.components
assert result_open_api_1 == result_open_api_2
dump_json = getattr(result_open_api_1, "model_dump_json" if PYDANTIC_V2 else "json")
open_api_json = dump_json(by_alias=True, exclude_none=True, indent=2)
logging.debug(open_api_json)
def test_construct_open_api_with_schema_class_2() -> None:
open_api_1 = construct_base_open_api_1()
open_api_2 = construct_base_open_api_2()
result_open_api_1 = construct_open_api_with_schema_class(open_api_1)
result_open_api_2 = construct_open_api_with_schema_class(
open_api_2, [PingRequest, PingResponse]
)
assert result_open_api_1 == result_open_api_2
def test_construct_open_api_with_schema_class_3() -> None:
open_api_3 = construct_base_open_api_3()
result_with_alias_1 = construct_open_api_with_schema_class(open_api_3)
assert result_with_alias_1.components is not None
assert result_with_alias_1.components.schemas is not None
schema_with_alias = result_with_alias_1.components.schemas["PongResponse"]
assert isinstance(schema_with_alias, Schema)
assert schema_with_alias.properties is not None
assert "pong_foo" in schema_with_alias.properties
assert "pong_bar" in schema_with_alias.properties
result_with_alias_2 = construct_open_api_with_schema_class(
open_api_3, by_alias=True
)
assert result_with_alias_1 == result_with_alias_2
result_without_alias = construct_open_api_with_schema_class(
open_api_3, by_alias=False
)
assert result_without_alias.components is not None
assert result_without_alias.components.schemas is not None
schema_without_alias = result_without_alias.components.schemas["PongResponse"]
assert isinstance(schema_without_alias, Schema)
assert schema_without_alias.properties is not None
assert "resp_foo" in schema_without_alias.properties
assert "resp_bar" in schema_without_alias.properties
@pytest.mark.skipif(PYDANTIC_V2, reason="generic type for Pydantic V1")
def test_construct_open_api_with_schema_class_4_generic_response_v1() -> None:
DataT = TypeVar("DataT")
from pydantic.v1.generics import GenericModel
class GenericResponse(GenericModel, Generic[DataT]):
msg: str = Field(description="message of the generic response")
data: DataT = Field(description="data value of the generic response")
open_api_4 = construct_base_open_api_4_generic_response(
GenericResponse[PongResponse]
)
result = construct_open_api_with_schema_class(open_api_4)
assert result.components is not None
assert result.components.schemas is not None
assert "GenericResponse_PongResponse_" in result.components.schemas
@pytest.mark.skipif(not PYDANTIC_V2, reason="generic type for Pydantic V2")
def test_construct_open_api_with_schema_class_4_generic_response() -> None:
DataT = TypeVar("DataT")
class GenericResponse(BaseModel, Generic[DataT]):
msg: str = Field(description="message of the generic response")
data: DataT = Field(description="data value of the generic response")
open_api_4 = construct_base_open_api_4_generic_response(
GenericResponse[PongResponse]
)
result = construct_open_api_with_schema_class(open_api_4)
assert result.components is not None
assert result.components.schemas is not None
assert "GenericResponse_PongResponse_" in result.components.schemas
def construct_base_open_api_1() -> OpenAPI:
model_validate: Callable[[dict], OpenAPI] = getattr(
OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj"
)
return model_validate(
{
"info": {"title": "My own API", "version": "v0.0.1"},
"paths": {
"/ping": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": PydanticSchema(schema_class=PingRequest)
}
}
},
"responses": {
"200": {
"description": "pong",
"content": {
"application/json": {
"schema": PydanticSchema(
schema_class=PingResponse
)
}
},
}
},
}
}
},
}
)
def construct_base_open_api_2() -> OpenAPI:
return OpenAPI(
info=Info(title="My own API", version="v0.0.1"),
paths={
"/ping": PathItem(
post=Operation(
requestBody=RequestBody(
content={
"application/json": MediaType(
media_type_schema=Reference(
**{"$ref": "#/components/schemas/PingRequest"}
)
)
}
),
responses={
"200": Response(
description="pong",
content={
"application/json": MediaType(
media_type_schema=Reference(
**{"$ref": "#/components/schemas/PingResponse"}
)
)
},
)
},
)
)
},
)
def construct_base_open_api_3() -> OpenAPI:
return OpenAPI(
info=Info(
title="My own API",
version="v0.0.1",
),
paths={
"/ping": PathItem(
post=Operation(
requestBody=RequestBody(
content={
"application/json": MediaType(
media_type_schema=PydanticSchema(
schema_class=PingRequest
)
)
}
),
responses={
"200": Response(
description="pong",
content={
"application/json": MediaType(
media_type_schema=PydanticSchema(
schema_class=PongResponse
)
)
},
)
},
)
)
},
)
def construct_base_open_api_3_plus() -> OpenAPI:
return OpenAPI(
info=Info(
title="My own API",
version="v0.0.1",
),
paths={
"/ping": PathItem(
post=Operation(
requestBody=RequestBody(
content={
"application/json": MediaType(
media_type_schema=PydanticSchema(
schema_class=PingPlusRequest
)
)
}
),
responses={
"200": Response(
description="pong",
content={
"application/json": MediaType(
media_type_schema=PydanticSchema(
schema_class=PongResponse
)
)
},
)
},
)
)
},
)
def construct_base_open_api_4_generic_response(response_schema: type) -> OpenAPI:
return OpenAPI(
info=Info(
title="My own API",
version="v0.0.1",
),
paths={
"/ping": PathItem(
post=Operation(
requestBody=RequestBody(
content={
"application/json": MediaType(
media_type_schema=PydanticSchema(
schema_class=PingRequest
)
)
}
),
responses={
"200": Response(
description="pong",
content={
"application/json": MediaType(
media_type_schema=PydanticSchema(
schema_class=response_schema
)
)
},
)
},
)
)
},
)
class PingRequest(BaseModel):
"""Ping Request"""
req_foo: str = Field(description="foo value of the request")
req_bar: str = Field(description="bar value of the request")
class PingResponse(BaseModel):
"""Ping response"""
resp_foo: str = Field(description="foo value of the response")
resp_bar: str = Field(description="bar value of the response")
class PongResponse(BaseModel):
"""Pong response"""
resp_foo: str = Field(alias="pong_foo", description="foo value of the response")
resp_bar: str = Field(alias="pong_bar", description="bar value of the response")
class PingPlusRequest(BaseModel):
"""Ping Request with extra"""
req_foo: str
req_bar: str
req_single_choice: Literal["one"]
def test_enum_with_single_choice() -> None:
api_obj = construct_open_api_with_schema_class(construct_base_open_api_3_plus())
model_dump = getattr(api_obj, "model_dump" if PYDANTIC_V2 else "dict")
api = model_dump(by_alias=True, exclude_none=True)
schema = api["components"]["schemas"]["PingPlusRequest"]
prop = schema["properties"]["req_single_choice"]
# OpenAPI 3.0 does not support "const", so make sure the enum is not
# rendered that way.
assert not prop.get("const")
assert prop["enum"] == ["one"]

View file

@ -0,0 +1,97 @@
# mypy: ignore-errors
import sys
from typing import Any, Optional
from openapi_spec_validator import validate
from pydantic import BaseModel
from openapi_pydantic.compat import PYDANTIC_V2
from openapi_pydantic.v3.v3_0 import (
Components,
DataType,
Example,
Header,
Info,
MediaType,
OpenAPI,
Operation,
PathItem,
RequestBody,
Response,
Schema,
)
from openapi_pydantic.v3.v3_0.util import (
PydanticSchema,
construct_open_api_with_schema_class,
)
if sys.version_info < (3, 9):
from typing_extensions import Literal
else:
from typing import Literal
def test_basic_schema() -> None:
class SampleModel(BaseModel):
required: bool
optional: Optional[bool] = None
one_literal_choice: Literal["only_choice"]
multiple_literal_choices: Literal["choice1", "choice2"]
part_api = construct_sample_api(SampleModel)
api = construct_open_api_with_schema_class(part_api)
assert api.components is not None
assert api.components.schemas is not None
if PYDANTIC_V2:
json_api: Any = api.model_dump(mode="json", by_alias=True, exclude_none=True)
else:
json_api: Any = api.dict(by_alias=True, exclude_none=True)
validate(json_api)
def construct_sample_api(SampleModel) -> OpenAPI:
class SampleRequest(SampleModel):
model_config = {"json_schema_mode": "validation"}
class SampleResponse(SampleModel):
model_config = {"json_schema_mode": "serialization"}
return OpenAPI(
info=Info(
title="Sample API",
version="v0.0.1",
),
paths={
"/callme": PathItem(
post=Operation(
requestBody=RequestBody(
content={
"application/json": MediaType(
schema=PydanticSchema(schema_class=SampleRequest)
)
}
),
responses={
"200": Response(
description="resp",
headers={
"WWW-Authenticate": Header(
description="Indicate how to authenticate",
schema=Schema(type=DataType.STRING),
)
},
content={
"application/json": MediaType(
schema=PydanticSchema(schema_class=SampleResponse)
)
},
)
},
)
)
},
components=Components(examples={"thing-example": Example(value="thing1")}),
)