Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: make it compatible with pydantic v1 & v2 #388

Merged
merged 1 commit into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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