Skip to content

Commit

Permalink
test: make it compatible with pydantic v1 & v2 (#388)
Browse files Browse the repository at this point in the history
Signed-off-by: Keming <kemingy94@gmail.com>
  • Loading branch information
kemingy authored Nov 25, 2024
1 parent a4e65c2 commit 493b560
Show file tree
Hide file tree
Showing 26 changed files with 322 additions and 749 deletions.
16 changes: 10 additions & 6 deletions examples/falcon_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,21 @@ def on_post(self, req, resp, form: File):
resp.media = {"filename": file.filename, "type": file.type}


if __name__ == "__main__":
"""
cmd:
http :8000/ping
http ':8000/api/zh/en?text=hi' uid=neo limit=1 vip=true
"""
def create_app():
app = falcon.App()
app.add_route("/ping", Ping())
app.add_route("/api/{source}/{target}", Classification())
app.add_route("/api/file_upload", FileUpload())
spec.register(app)
return app


if __name__ == "__main__":
"""
cmd:
http :8000/ping
http ':8000/api/zh/en?text=hi' uid=neo limit=1 vip=true
"""
app = create_app()
httpd = simple_server.make_server("localhost", 8000, app)
httpd.serve_forever()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "spectree"
version = "1.4.0"
version = "1.4.1"
dynamic = []
description = "generate OpenAPI document and validate request&response with Python annotations."
readme = "README.md"
Expand Down
60 changes: 43 additions & 17 deletions spectree/_pydantic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any, Protocol, runtime_checkable
from enum import Enum
from typing import Any, Protocol, Type, runtime_checkable

from pydantic.version import VERSION as PYDANTIC_VERSION

Expand All @@ -9,43 +10,63 @@
__all__ = [
"AnyUrl",
"BaseModel",
"BaseSettings",
"EmailStr",
"Field",
"InternalBaseModel",
"InternalField",
"InternalValidationError",
"ValidationError",
"generate_root_model",
"is_base_model",
"is_base_model_instance",
"is_pydantic_model",
"is_root_model",
"is_root_model_instance",
"root_validator",
"serialize_model_instance",
"validator",
]


class UNSET_TYPE(Enum):
NODEFAULT = "NO_DEFAULT"


NODEFAULT = UNSET_TYPE.NODEFAULT

if PYDANTIC2:
from pydantic.v1 import (
AnyUrl,
BaseModel,
BaseSettings,
EmailStr,
Field,
ValidationError,
root_validator,
validator,
)
from pydantic import BaseModel, Field, RootModel, ValidationError
from pydantic.v1 import AnyUrl, root_validator, validator
from pydantic.v1 import BaseModel as InternalBaseModel
from pydantic.v1 import Field as InternalField
from pydantic.v1 import ValidationError as InternalValidationError
from pydantic_core import core_schema # noqa

else:
from pydantic import ( # type: ignore[no-redef,assignment]
AnyUrl,
BaseModel,
BaseSettings,
EmailStr,
Field,
ValidationError,
root_validator,
validator,
)

InternalBaseModel = BaseModel # type: ignore
InternalValidationError = ValidationError # type: ignore
InternalField = Field # type: ignore


def generate_root_model(root_type, name="GeneratedRootModel") -> Type:
if PYDANTIC2:
return type(name, (RootModel[root_type],), {})
return type(
name,
(BaseModel,),
{
"__annotations__": {ROOT_FIELD: root_type},
},
)


@runtime_checkable
class PydanticModelProtocol(Protocol):
Expand Down Expand Up @@ -124,7 +145,7 @@ def is_pydantic_model(t: Any) -> bool:
def is_base_model(t: Any) -> bool:
"""Check whether a type is a Pydantic BaseModel"""
try:
return issubclass(t, BaseModel)
return is_pydantic_model(t)
except TypeError:
return False

Expand Down Expand Up @@ -154,7 +175,12 @@ def is_partial_base_model_instance(instance: Any) -> bool:

def is_root_model(t: Any) -> bool:
"""Check whether a type is a Pydantic RootModel."""
return is_base_model(t) and ROOT_FIELD in t.__fields__
pydantic_v1_root = is_base_model(t) and ROOT_FIELD in t.__fields__
pydantic_v2_root = is_base_model(t) and any(
f"{m.__module__}.{m.__name__}" == "pydantic.root_model.RootModel"
for m in t.mro()
)
return pydantic_v1_root or pydantic_v2_root


def is_root_model_instance(value: Any):
Expand Down
11 changes: 11 additions & 0 deletions spectree/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ def __iter__(self) -> Iterator[str]:
pass


class MultiDictStarlette(Protocol):
def __iter__(self) -> Iterator[str]:
pass

def getlist(self, key: Any) -> List[Any]:
pass

def __getitem__(self, key: Any) -> Any:
pass


class FunctionDecorator(Protocol):
resp: Any
tags: Sequence[Any]
Expand Down
24 changes: 7 additions & 17 deletions spectree/config.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import warnings
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Union
from typing import Any, Dict, List, Mapping, Optional, Union

from ._pydantic import AnyUrl, BaseModel, BaseSettings, EmailStr, root_validator
from ._pydantic import AnyUrl, InternalBaseModel, root_validator
from .models import SecurityScheme, Server
from .page import DEFAULT_PAGE_TEMPLATES

# Fall back to a str field if email-validator is not installed.
if TYPE_CHECKING:
EmailFieldType = str
else:
try:
EmailStr.validate("a@b.com")
EmailFieldType = EmailStr
except ImportError:
EmailFieldType = str


class ModeEnum(str, Enum):
"""the mode of the SpecTree validator"""
Expand All @@ -28,18 +18,18 @@ class ModeEnum(str, Enum):
greedy = "greedy"


class Contact(BaseModel):
class Contact(InternalBaseModel):
"""contact information"""

#: name of the contact
name: str
#: contact url
url: Optional[AnyUrl] = None
#: contact email address
email: Optional[EmailFieldType] = None
email: Optional[str] = None


class License(BaseModel):
class License(InternalBaseModel):
"""license information"""

#: name of the license
Expand All @@ -48,7 +38,7 @@ class License(BaseModel):
url: Optional[AnyUrl] = None


class Configuration(BaseSettings):
class Configuration(InternalBaseModel):
# OpenAPI configurations
#: title of the service
title: str = "Service API Document"
Expand Down Expand Up @@ -106,8 +96,8 @@ class Configuration(BaseSettings):
#: OAuth2 use PKCE with authorization code grant
use_pkce_with_authorization_code_grant: bool = False

# Pydantic v1 config
class Config:
env_prefix = "spectree_"
validate_assignment = True

@root_validator(pre=True)
Expand Down
Loading

0 comments on commit 493b560

Please sign in to comment.