Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Add support for pydantic v2 via pydantic.v1 compat module #16332

Merged
merged 7 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions changelog.d/16332.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for pydantic v2 in addition to pydantic v1. Contributed by Maxwell G (@gotmax23).
193 changes: 146 additions & 47 deletions poetry.lock
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated with poetry update pydantic so other dependencies weren't touched here.

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,11 @@ cryptography = ">=3.4.7"
ijson = ">=3.1.4"
matrix-common = "^1.3.0"
# We need packaging.requirements.Requirement, added in 16.1.
packaging = ">=16.1"
# This is the most recent version of Pydantic with available on common distros.
# We are currently incompatible with >=2.0.0: (https://github.com/matrix-org/synapse/issues/15858)
pydantic = "^1.7.4"
# We need packaging.verison.Version(...).major added in 20.0.
packaging = ">=20.0"
# We support pydantic v1 and pydantic v2 via the pydantic.v1 compat module.
# See https://github.com/matrix-org/synapse/issues/15858
pydantic = ">=1.7.4, <3"

# This is for building the rust components during "poetry install", which
# currently ignores the `build-system.requires` directive (c.f.
Expand Down Expand Up @@ -321,6 +322,8 @@ all = [
isort = ">=5.10.1"
black = ">=22.7.0"
ruff = "0.0.290"
# Type checking only works with the pydantic.v1 compat module from pydantic v2
pydantic = "^2"
gotmax23 marked this conversation as resolved.
Show resolved Hide resolved

# Typechecking
lxml-stubs = ">=0.4.0"
Expand Down
98 changes: 82 additions & 16 deletions scripts-dev/check_pydantic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,41 @@
import traceback
import unittest.mock
from contextlib import contextmanager
from typing import Any, Callable, Dict, Generator, List, Set, Type, TypeVar
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Generator,
List,
Set,
Type,
TypeVar,
)

from parameterized import parameterized
from pydantic import BaseModel as PydanticBaseModel, conbytes, confloat, conint, constr
from pydantic.typing import get_args

from synapse._pydantic_compat import HAS_PYDANTIC_V2

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import (
BaseModel as PydanticBaseModel,
conbytes,
confloat,
conint,
constr,
)
from pydantic.v1.typing import get_args
else:
from pydantic import (
BaseModel as PydanticBaseModel,
conbytes,
confloat,
conint,
constr,
)
from pydantic.typing import get_args

from typing_extensions import ParamSpec

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -251,7 +281,10 @@ def test_expression_without_strict_raises(self) -> None:
with monkeypatch_pydantic(), self.assertRaises(ModelCheckerException):
run_test_snippet(
"""
from pydantic import constr
try:
from pydantic.v1 import constr
except ImportError:
from pydantic import constr
constr()
"""
)
Expand All @@ -269,7 +302,10 @@ def test_wildcard_import_raises(self) -> None:
with monkeypatch_pydantic(), self.assertRaises(ModelCheckerException):
run_test_snippet(
"""
from pydantic import *
try:
from pydantic.v1 import *
except ImportError:
from pydantic import *
constr()
"""
)
Expand All @@ -278,7 +314,10 @@ def test_alternative_import_raises(self) -> None:
with monkeypatch_pydantic(), self.assertRaises(ModelCheckerException):
run_test_snippet(
"""
from pydantic.types import constr
try:
from pydantic.v1.types import constr
except ImportError:
from pydantic.types import constr
constr()
"""
)
Expand All @@ -287,16 +326,22 @@ def test_alternative_import_attribute_raises(self) -> None:
with monkeypatch_pydantic(), self.assertRaises(ModelCheckerException):
run_test_snippet(
"""
import pydantic.types
pydantic.types.constr()
try:
from pydantic.v1 import types as pydantic_types
except ImportError:
from pydantic import types as pydantic_types
pydantic_types.constr()
"""
)

def test_kwarg_but_no_strict_raises(self) -> None:
with monkeypatch_pydantic(), self.assertRaises(ModelCheckerException):
run_test_snippet(
"""
from pydantic import constr
try:
from pydantic.v1 import constr
except ImportError:
from pydantic import constr
constr(min_length=10)
"""
)
Expand All @@ -305,7 +350,10 @@ def test_kwarg_strict_False_raises(self) -> None:
with monkeypatch_pydantic(), self.assertRaises(ModelCheckerException):
run_test_snippet(
"""
from pydantic import constr
try:
from pydantic.v1 import constr
except ImportError:
from pydantic import constr
constr(strict=False)
"""
)
Expand All @@ -314,7 +362,10 @@ def test_kwarg_strict_True_doesnt_raise(self) -> None:
with monkeypatch_pydantic():
run_test_snippet(
"""
from pydantic import constr
try:
from pydantic.v1 import constr
except ImportError:
from pydantic import constr
constr(strict=True)
"""
)
Expand All @@ -323,7 +374,10 @@ def test_annotation_without_strict_raises(self) -> None:
with monkeypatch_pydantic(), self.assertRaises(ModelCheckerException):
run_test_snippet(
"""
from pydantic import constr
try:
from pydantic.v1 import constr
except ImportError:
from pydantic import constr
x: constr()
"""
)
Expand All @@ -332,7 +386,10 @@ def test_field_annotation_without_strict_raises(self) -> None:
with monkeypatch_pydantic(), self.assertRaises(ModelCheckerException):
run_test_snippet(
"""
from pydantic import BaseModel, conint
try:
from pydantic.v1 import BaseModel, conint
except ImportError:
from pydantic import BaseModel, conint
class C:
x: conint()
"""
Expand Down Expand Up @@ -361,7 +418,10 @@ def test_field_holding_unwanted_type_raises(self, annotation: str) -> None:
run_test_snippet(
f"""
from typing import *
from pydantic import *
try:
from pydantic.v1 import *
except ImportError:
from pydantic import *
class C(BaseModel):
f: {annotation}
"""
Expand All @@ -388,7 +448,10 @@ def test_field_holding_accepted_type_doesnt_raise(self, annotation: str) -> None
run_test_snippet(
f"""
from typing import *
from pydantic import *
try:
from pydantic.v1 import *
except ImportError:
from pydantic import *
class C(BaseModel):
f: {annotation}
"""
Expand All @@ -398,7 +461,10 @@ def test_field_holding_str_raises_with_alternative_import(self) -> None:
with monkeypatch_pydantic(), self.assertRaises(ModelCheckerException):
run_test_snippet(
"""
from pydantic.main import BaseModel
try:
from pydantic.v1.main import BaseModel
except ImportError:
from pydantic.main import BaseModel
class C(BaseModel):
f: str
"""
Expand Down
26 changes: 26 additions & 0 deletions synapse/_pydantic_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2023 Maxwell G <maxwell@gtmx.me>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from packaging.version import Version
clokep marked this conversation as resolved.
Show resolved Hide resolved

try:
from pydantic import __version__ as pydantic_version
except ImportError:
import importlib.metadata

pydantic_version = importlib.metadata.version("pydantic")

HAS_PYDANTIC_V2: bool = Version(pydantic_version).major == 2

__all__ = ("HAS_PYDANTIC_V2",)
10 changes: 8 additions & 2 deletions synapse/config/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, Dict, Type, TypeVar
from typing import TYPE_CHECKING, Any, Dict, Type, TypeVar

import jsonschema
from pydantic import BaseModel, ValidationError, parse_obj_as

from synapse._pydantic_compat import HAS_PYDANTIC_V2

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import BaseModel, ValidationError, parse_obj_as
else:
from pydantic import BaseModel, ValidationError, parse_obj_as
clokep marked this conversation as resolved.
Show resolved Hide resolved

from synapse.config._base import ConfigError
from synapse.types import JsonDict, StrSequence
Expand Down
10 changes: 8 additions & 2 deletions synapse/config/workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@

import argparse
import logging
from typing import Any, Dict, List, Optional, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

import attr
from pydantic import BaseModel, Extra, StrictBool, StrictInt, StrictStr

from synapse._pydantic_compat import HAS_PYDANTIC_V2

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import BaseModel, Extra, StrictBool, StrictInt, StrictStr
else:
from pydantic import BaseModel, Extra, StrictBool, StrictInt, StrictStr

from synapse.config._base import (
Config,
Expand Down
10 changes: 8 additions & 2 deletions synapse/events/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import collections.abc
from typing import List, Type, Union, cast
from typing import TYPE_CHECKING, List, Type, Union, cast

import jsonschema
from pydantic import Field, StrictBool, StrictStr

from synapse._pydantic_compat import HAS_PYDANTIC_V2

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import Field, StrictBool, StrictStr
else:
from pydantic import Field, StrictBool, StrictStr

from synapse.api.constants import (
MAX_ALIAS_LENGTH,
Expand Down
11 changes: 9 additions & 2 deletions synapse/http/servlet.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@
overload,
)

from pydantic import BaseModel, MissingError, PydanticValueError, ValidationError
from pydantic.error_wrappers import ErrorWrapper
from synapse._pydantic_compat import HAS_PYDANTIC_V2

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import BaseModel, MissingError, PydanticValueError, ValidationError
from pydantic.v1.error_wrappers import ErrorWrapper
else:
from pydantic import BaseModel, MissingError, PydanticValueError, ValidationError
from pydantic.error_wrappers import ErrorWrapper

from typing_extensions import Literal

from twisted.web.server import Request
Expand Down
7 changes: 6 additions & 1 deletion synapse/rest/client/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
from typing import TYPE_CHECKING, List, Optional, Tuple
from urllib.parse import urlparse

from pydantic import StrictBool, StrictStr, constr
from synapse._pydantic_compat import HAS_PYDANTIC_V2

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import StrictBool, StrictStr, constr
else:
from pydantic import StrictBool, StrictStr, constr
from typing_extensions import Literal

from twisted.web.server import Request
Expand Down
7 changes: 6 additions & 1 deletion synapse/rest/client/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
from http import HTTPStatus
from typing import TYPE_CHECKING, List, Optional, Tuple

from pydantic import Extra, StrictStr
from synapse._pydantic_compat import HAS_PYDANTIC_V2

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import Extra, StrictStr
else:
from pydantic import Extra, StrictStr

from synapse.api import errors
from synapse.api.errors import NotFoundError, SynapseError, UnrecognizedRequestError
Expand Down
8 changes: 7 additions & 1 deletion synapse/rest/client/directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
import logging
from typing import TYPE_CHECKING, List, Optional, Tuple

from pydantic import StrictStr
from synapse._pydantic_compat import HAS_PYDANTIC_V2

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import StrictStr
else:
from pydantic import StrictStr

from typing_extensions import Literal

from twisted.web.server import Request
Expand Down
7 changes: 6 additions & 1 deletion synapse/rest/client/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
# limitations under the License.
from typing import TYPE_CHECKING, Dict, Optional

from pydantic import Extra, StrictInt, StrictStr, constr, validator
from synapse._pydantic_compat import HAS_PYDANTIC_V2

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import Extra, StrictInt, StrictStr, constr, validator
else:
from pydantic import Extra, StrictInt, StrictStr, constr, validator

from synapse.rest.models import RequestBodyModel
from synapse.util.threepids import validate_email
Expand Down
8 changes: 7 additions & 1 deletion synapse/rest/key/v2/remote_key_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@
import re
from typing import TYPE_CHECKING, Dict, Mapping, Optional, Set, Tuple

from pydantic import Extra, StrictInt, StrictStr
from synapse._pydantic_compat import HAS_PYDANTIC_V2

if TYPE_CHECKING or HAS_PYDANTIC_V2:
from pydantic.v1 import Extra, StrictInt, StrictStr
else:
from pydantic import StrictInt, StrictStr, Extra

from signedjson.sign import sign_json

from twisted.web.server import Request
Expand Down
Loading
Loading