Repository: 0b01001001/spectree (language: python)

Issue:
Support pydantic v1 and v2 with shims
This super-unpolished draft PR is mainly an exploration to support both v1 and v2 pydantic based on the discussion from #319 and how some other libraries (notably [FastAPI](https://github.com/tiangolo/fastapi/blob/c165be380f87b7b079f98c5134cccef0c9d67e4d/fastapi/_compat.py#L46-L494) and [app-model](https://github.com/pyapp-kit/app-model/blob/main/src/app_model/_pydantic_compat.py)) in the ecosystem are handling the pydantic v2 upgrade.

Some cons with this approach:
- Messy and feels hacky
- Complex maintainability

With that in mind, this could still serve as a decent temporary solution to provide flexibility. For long-term, I think all the internals will need to be rehashed to support pydantic v1/v2 and other validator libraries like msgspec in a more structured way.

Quick overview of the changes:
- `_pydantic.py` holds all the shims and compat things for pydantic v1 and v2
- All the internal code imports from `_pydantic` and follows v2 pydantic API when possible (for v1, that works either through the shims or the aliases). When that is not possible, there are if/else clauses to separate v1 and v2 convention as necessary (mainly inside the models and some test logic)
- Testing setup to run unit and snapshot tests with pydantic v1 and v2
- Passes linting/tests locally on 3.11.2, untested on older versions



Discussion / hints:

Fix patch:
diff --git a/Makefile b/Makefile
index bbd6c91d..0d82fdaa 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
 .DEFAULT_GOAL:=install
 
 SOURCE_FILES=spectree tests examples
+MYPY_SOURCE_FILES=spectree tests # temporary
 
 install:
 	pip install -U -e .[email,quart,flask,falcon,starlette,dev]
@@ -16,6 +17,8 @@ import_test:
 test: import_test
 	pip install -U -e .[email,flask,quart,falcon,starlette]
 	pytest tests -vv -rs
+	pip install pydantic==1.10.11
+	pytest tests -vv -rs
 
 doc:
 	cd docs && make html
@@ -43,6 +46,6 @@ lint:
 	isort --check --diff --project=spectree ${SOURCE_FILES}
 	black --check --diff ${SOURCE_FILES}
 	flake8 ${SOURCE_FILES} --count --show-source --statistics --ignore=D203,E203,W503 --max-line-length=88 --max-complexity=15
-	mypy --install-types --non-interactive ${SOURCE_FILES}
+	mypy --install-types --non-interactive ${MYPY_SOURCE_FILES}
 
 .PHONY: test doc
diff --git a/pyproject.toml b/pyproject.toml
index b258af2a..fcfb1cc2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,7 +22,7 @@ classifiers = [
     "Topic :: Software Development :: Libraries :: Python Modules",
 ]
 dependencies = [
-    "pydantic>=1.2,<2",
+    "pydantic>=1.2,<3",
 ]
 
 [project.optional-dependencies]
@@ -37,7 +37,7 @@ dev = [
     "syrupy>=4.0",
 ]
 email = [
-    "pydantic[email]>=1.2,<2",
+    "pydantic[email]>=1.2,<3",
 ]
 falcon = [
     "falcon>=3.0.0",
diff --git a/setup.py b/setup.py
index f42a2b44..de1217cf 100644
--- a/setup.py
+++ b/setup.py
@@ -6,10 +6,10 @@
 setup(
     name="spectree",
     install_requires=[
-        "pydantic>=1.2,<2",
+        "pydantic>=1.2,<3",
     ],
     extras_require={
-        "email": ["pydantic[email]>=1.2,<2"],
+        "email": ["pydantic[email]>=1.2,<3"],
         "flask": ["flask"],
         "quart": ["quart"],
         "falcon": ["falcon>=3.0.0"],
diff --git a/spectree/_pydantic.py b/spectree/_pydantic.py
new file mode 100644
index 00000000..70559fe7
--- /dev/null
+++ b/spectree/_pydantic.py
@@ -0,0 +1,37 @@
+from pydantic.version import VERSION as PYDANTIC_VERSION
+
+PYDANTIC2 = PYDANTIC_VERSION.startswith("2")
+
+__all__ = [
+    "BaseModel",
+    "ValidationError",
+    "Field",
+    "root_validator",
+    "AnyUrl",
+    "BaseSettings",
+    "EmailStr",
+    "validator",
+]
+
+if PYDANTIC2:
+    from pydantic.v1 import (
+        AnyUrl,
+        BaseModel,
+        BaseSettings,
+        EmailStr,
+        Field,
+        ValidationError,
+        root_validator,
+        validator,
+    )
+else:
+    from pydantic import (  # type: ignore[no-redef,assignment]
+        AnyUrl,
+        BaseModel,
+        BaseSettings,
+        EmailStr,
+        Field,
+        ValidationError,
+        root_validator,
+        validator,
+    )
diff --git a/spectree/_types.py b/spectree/_types.py
index 79e93208..b572fad5 100644
--- a/spectree/_types.py
+++ b/spectree/_types.py
@@ -11,9 +11,10 @@
     Union,
 )
 
-from pydantic import BaseModel
 from typing_extensions import Protocol
 
+from ._pydantic import BaseModel
+
 ModelType = Type[BaseModel]
 OptionalModelType = Optional[ModelType]
 NamingStrategy = Callable[[ModelType], str]
diff --git a/spectree/config.py b/spectree/config.py
index 0e0bfd06..2a9a680c 100644
--- a/spectree/config.py
+++ b/spectree/config.py
@@ -2,8 +2,7 @@
 from enum import Enum
 from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Union
 
-from pydantic import AnyUrl, BaseModel, BaseSettings, EmailStr, root_validator
-
+from ._pydantic import AnyUrl, BaseModel, BaseSettings, EmailStr, root_validator
 from .models import SecurityScheme, Server
 from .page import DEFAULT_PAGE_TEMPLATES
 
diff --git a/spectree/models.py b/spectree/models.py
index e49dae5a..1b07ef9e 100644
--- a/spectree/models.py
+++ b/spectree/models.py
@@ -2,7 +2,7 @@
 from enum import Enum
 from typing import Any, Dict, Optional, Sequence
 
-from pydantic import BaseModel, Field, root_validator, validator
+from ._pydantic import BaseModel, Field, root_validator, validator
 
 # OpenAPI names validation regexp
 OpenAPI_NAME_RE = re.compile(r"^[A-Za-z0-9-._]+")
diff --git a/spectree/plugins/falcon_plugin.py b/spectree/plugins/falcon_plugin.py
index 892661eb..07b5d13f 100644
--- a/spectree/plugins/falcon_plugin.py
+++ b/spectree/plugins/falcon_plugin.py
@@ -5,8 +5,8 @@
 
 from falcon import HTTP_400, HTTP_415, HTTPError
 from falcon.routing.compiled import _FIELD_PATTERN as FALCON_FIELD_PATTERN
-from pydantic import ValidationError
 
+from .._pydantic import ValidationError
 from .._types import ModelType
 from ..response import Response
 from .base import BasePlugin
diff --git a/spectree/plugins/flask_plugin.py b/spectree/plugins/flask_plugin.py
index 95a9ca88..0604a5ab 100644
--- a/spectree/plugins/flask_plugin.py
+++ b/spectree/plugins/flask_plugin.py
@@ -1,9 +1,9 @@
 from typing import Any, Callable, Mapping, Optional, Tuple, get_type_hints
 
 from flask import Blueprint, abort, current_app, jsonify, make_response, request
-from pydantic import BaseModel, ValidationError
 from werkzeug.routing import parse_converter_args
 
+from .._pydantic import BaseModel, ValidationError
 from .._types import ModelType
 from ..response import Response
 from ..utils import get_multidict_items, werkzeug_parse_rule
diff --git a/spectree/plugins/quart_plugin.py b/spectree/plugins/quart_plugin.py
index 28c9fb16..de891c2e 100644
--- a/spectree/plugins/quart_plugin.py
+++ b/spectree/plugins/quart_plugin.py
@@ -1,10 +1,10 @@
 import inspect
 from typing import Any, Callable, Mapping, Optional, Tuple, get_type_hints
 
-from pydantic import BaseModel, ValidationError
 from quart import Blueprint, abort, current_app, jsonify, make_response, request
 from werkzeug.routing import parse_converter_args
 
+from .._pydantic import BaseModel, ValidationError
 from .._types import ModelType
 from ..response import Response
 from ..utils import get_multidict_items, werkzeug_parse_rule
diff --git a/spectree/plugins/starlette_plugin.py b/spectree/plugins/starlette_plugin.py
index 5dabdc26..938bad75 100644
--- a/spectree/plugins/starlette_plugin.py
+++ b/spectree/plugins/starlette_plugin.py
@@ -4,12 +4,12 @@
 from json import JSONDecodeError
 from typing import Any, Callable, Optional, get_type_hints
 
-from pydantic import ValidationError
 from starlette.convertors import CONVERTOR_TYPES
 from starlette.requests import Request
 from starlette.responses import HTMLResponse, JSONResponse
 from starlette.routing import compile_path
 
+from .._pydantic import ValidationError
 from .._types import ModelType
 from ..response import Response
 from .base import BasePlugin, Context
diff --git a/spectree/response.py b/spectree/response.py
index 15716b2a..6b8e69e5 100644
--- a/spectree/response.py
+++ b/spectree/response.py
@@ -1,8 +1,7 @@
 from http import HTTPStatus
 from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
 
-from pydantic import BaseModel
-
+from ._pydantic import BaseModel
 from ._types import ModelType, NamingStrategy, OptionalModelType
 from .utils import gen_list_model, get_model_key, parse_code
 
diff --git a/spectree/utils.py b/spectree/utils.py
index 52af7c74..cc803f05 100644
--- a/spectree/utils.py
+++ b/spectree/utils.py
@@ -16,8 +16,7 @@
     Union,
 )
 
-from pydantic import BaseModel, ValidationError
-
+from ._pydantic import BaseModel, ValidationError
 from ._types import ModelType, MultiDict, NamingStrategy, NestedNamingStrategy
 
 # parse HTTP status code to get the code


Test patch:
diff --git a/tests/common.py b/tests/common.py
index 7af87452..ec375a3e 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -1,9 +1,8 @@
 from enum import Enum, IntEnum
 from typing import Dict, List
 
-from pydantic import BaseModel, Field, root_validator
-
 from spectree import BaseFile, ExternalDocs, SecurityScheme, SecuritySchemeData, Tag
+from spectree._pydantic import BaseModel, Field, root_validator
 from spectree.utils import hash_module_path
 
 api_tag = Tag(
diff --git a/tests/test_config.py b/tests/test_config.py
index c4716542..bc1f4e8e 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -2,9 +2,9 @@
 from typing import Type
 
 import pytest
-from pydantic import ValidationError
 
 from spectree import SecurityScheme
+from spectree._pydantic import ValidationError
 from spectree.config import Configuration, EmailFieldType
 
 from .common import SECURITY_SCHEMAS, WRONG_SECURITY_SCHEMAS_DATA
diff --git a/tests/test_response.py b/tests/test_response.py
index 103ee3f0..1ba509c2 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -1,8 +1,8 @@
 from typing import List, get_type_hints
 
 import pytest
-from pydantic import BaseModel
 
+from spectree._pydantic import BaseModel
 from spectree.models import ValidationError
 from spectree.response import DEFAULT_CODE_DESC, Response
 from spectree.utils import gen_list_model
diff --git a/tests/test_spec.py b/tests/test_spec.py
index 98ebace2..3e802a0b 100644
--- a/tests/test_spec.py
+++ b/tests/test_spec.py
@@ -1,10 +1,10 @@
 import pytest
 from falcon import App as FalconApp
 from flask import Flask
-from pydantic import BaseModel
 from starlette.applications import Starlette
 
 from spectree import Response
+from spectree._pydantic import BaseModel
 from spectree.config import Configuration
 from spectree.models import Server, ValidationError
 from spectree.plugins.flask_plugin import FlaskPlugin