Skip to content

Commit

Permalink
Merge pull request #7 from eadwinCode/pydantic_v2_support
Browse files Browse the repository at this point in the history
Pydantic v2 and v1 support
  • Loading branch information
eadwinCode committed Sep 5, 2023
2 parents aebd890 + 8a37832 commit 8dd3411
Show file tree
Hide file tree
Showing 25 changed files with 2,297 additions and 842 deletions.
16 changes: 16 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[report]
exclude_lines =
pragma: no cover
def __repr__
def __str__
if self.debug:
if IS_PYDANTIC_V1
if TYPE_CHECKING:
if t.TYPE_CHECKING:
if settings.DEBUG
raise AssertionError
raise NotImplementedError
if 0:
if __name__ == .__main__.:
class .*\bProtocol\):
@(abc\.)?abstractmethod
14 changes: 10 additions & 4 deletions .github/workflows/test_full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11']
django-version: ['<3.0', '<3.1', '<3.2', '<3.3', '<4.1', '<4.2']
pydantic-version: [ "pydantic-v1", "pydantic-v2" ]
fail-fast: false

steps:
- uses: actions/checkout@v2
Expand All @@ -21,9 +23,13 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install core
run: pip install "Django${{ matrix.django-version }}" "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0" pydantic[email]
- name: Install tests
run: pip install pytest pytest-django
run: pip install "Django${{ matrix.django-version }}" pytest pytest-django
- name: Install Pydantic v1
if: matrix.pydantic-version == 'pydantic-v1'
run: pip install "pydantic>=1.10.0,<2.0.0" pydantic[email]
- name: Install Pydantic v2
if: matrix.pydantic-version == 'pydantic-v2'
run: pip install "pydantic>=2.0.2,<3.0.0" pydantic[email]
- name: Test
run: pytest
codestyle:
Expand All @@ -34,7 +40,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.9
python-version: 3.8
- name: Install Flit
run: pip install flit
- name: Install Dependencies
Expand Down
46 changes: 46 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-merge-conflict
- repo: https://github.com/asottile/yesqa
rev: v1.3.0
hooks:
- id: yesqa
- repo: local
hooks:
- id: code_formatting
args: []
name: Code Formatting
entry: "make fmt"
types: [python]
language_version: python3
language: python
- id: code_linting
args: [ ]
name: Code Linting
entry: "make lint"
types: [ python ]
language_version: python3
language: python
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: end-of-file-fixer
exclude: >-
^examples/[^/]*\.svg$
- id: requirements-txt-fixer
- id: trailing-whitespace
types: [python]
- id: check-case-conflict
- id: check-json
- id: check-xml
- id: check-executables-have-shebangs
- id: check-toml
- id: check-xml
- id: check-yaml
- id: debug-statements
- id: check-added-large-files
- id: check-symlinks
- id: debug-statements
exclude: ^tests/
15 changes: 6 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,21 @@ clean: ## Removing cached python compiled files
find . -name \*~ | xargs rm -fv
find . -name __pycache__ | xargs rm -rfv

install: ## Install dependencies
install:clean ## Install dependencies
flit install --deps develop --symlink
pre-commit install -f

lint: ## Run code linters
make clean
lint:fmt ## Run code linters
black --check ninja_schema tests
ruff check ninja_schema tests
mypy ninja_schema

fmt format: ## Run code formatters
make clean
fmt format:clean ## Run code formatters
black ninja_schema tests
ruff check --fix ninja_schema tests

test: ## Run tests
make clean
test:clean ## Run tests
pytest .

test-cov: ## Run tests with coverage
make clean
test-cov:clean ## Run tests with coverage
pytest --cov=ninja_schema --cov-report term-missing tests
22 changes: 0 additions & 22 deletions mypy.ini

This file was deleted.

40 changes: 35 additions & 5 deletions ninja_schema/orm/factory.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import typing
from typing import TYPE_CHECKING, List, Optional, Type, Union, cast

from django.db.models import Model

from ninja_schema.pydanticutils import IS_PYDANTIC_V1

from ..errors import ConfigError
from ..types import DictStrAny
from .schema_registry import SchemaRegister
Expand Down Expand Up @@ -36,7 +39,7 @@ def create_schema(
depth: int = 0,
fields: Optional[List[str]] = None,
exclude: Optional[List[str]] = None,
skip_registry: bool = False
skip_registry: bool = False,
) -> Union[Type["ModelSchema"], Type["Schema"], None]:
from .model_schema import ModelSchema

Expand All @@ -57,12 +60,39 @@ def create_schema(
"depth": depth,
"registry": registry,
}
model_config = cls.get_model_config(**model_config_kwargs) # type: ignore

attrs = {"Config": model_config}
cls.get_model_config(**model_config_kwargs) # type: ignore
new_schema = (
cls._get_schema_v1(name, model_config_kwargs, ModelSchema)
if IS_PYDANTIC_V1
else cls._get_schema_v2(name, model_config_kwargs, ModelSchema)
)

new_schema = type(name, (ModelSchema,), attrs)
new_schema = cast(Type[ModelSchema], new_schema)
if not skip_registry:
registry.register_model(model, new_schema)
return new_schema

@classmethod
def _get_schema_v1(
cls, name: str, model_config_kwargs: typing.Dict, model_type: typing.Type
) -> Union[Type["ModelSchema"], Type["Schema"], None]:
model_config = cls.get_model_config(**model_config_kwargs)

attrs = {"Config": model_config}

new_schema = type(name, (model_type,), attrs)
new_schema = cast(Type["ModelSchema"], new_schema)
return new_schema

@classmethod
def _get_schema_v2(
cls, name: str, model_config_kwargs: typing.Dict, model_type: typing.Type
) -> Union[Type["ModelSchema"], Type["Schema"]]:
model_config = cls.get_model_config(**model_config_kwargs)

new_schema_string = f"""class {name}(model_type):
class Config(model_config):
pass """

exec(new_schema_string, locals())
return locals().get(name) # type:ignore[return-value]
49 changes: 41 additions & 8 deletions ninja_schema/orm/getters.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
from typing import Any
import typing as t

import pydantic
from django.db.models import Manager, QuerySet
from django.db.models.fields.files import FieldFile
from pydantic.utils import GetterDict

pydantic_version = list(map(int, pydantic.VERSION.split(".")))[:2]
assert pydantic_version >= [1, 6], "Pydantic 1.6+ required"
from ..pydanticutils import IS_PYDANTIC_V1

__all__ = [
"DjangoGetter",
]


class DjangoGetter(GetterDict):
def get(self, key: Any, default: Any = None) -> Any:
result = super().get(key, default)

class DjangoGetterMixin:
def _convert_result(self, result: t.Any) -> t.Any:
if isinstance(result, Manager):
return list(result.all())

Expand All @@ -29,3 +25,40 @@ def get(self, key: Any, default: Any = None) -> Any:
return result.url

return result


if IS_PYDANTIC_V1:
from pydantic.utils import GetterDict

pydantic_version = list(map(int, pydantic.VERSION.split(".")))[:2]
assert pydantic_version >= [1, 6], "Pydantic 1.6+ required"

class DjangoGetter(GetterDict, DjangoGetterMixin):
def get(self, key: t.Any, default: t.Any = None) -> t.Any:
result = super().get(key, default)
return self._convert_result(result)

else:

class DjangoGetter(DjangoGetterMixin): # type:ignore[no-redef]
__slots__ = ("_obj", "_schema_cls", "_context")

def __init__(self, obj: t.Any, schema_cls: t.Any, context: t.Any = None):
self._obj = obj
self._schema_cls = schema_cls
self._context = context

def __getattr__(self, key: str) -> t.Any:
# if key.startswith("__pydantic"):
# return getattr(self._obj, key)
if isinstance(self._obj, dict):
if key not in self._obj:
raise AttributeError(key)
value = self._obj[key]
else:
try:
value = getattr(self._obj, key)
except AttributeError as e:
raise AttributeError(key) from e

return self._convert_result(value)
58 changes: 51 additions & 7 deletions ninja_schema/orm/mixins.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,60 @@
from typing import Callable, Type
import typing as t
import warnings

from django.db.models import Model as DjangoModel

from ..pydanticutils import IS_PYDANTIC_V1
from ..types import DictStrAny
from .getters import DjangoGetter


class SchemaMixins:
dict: Callable

class BaseMixins:
def apply_to_model(
self, model_instance: Type[DjangoModel], **kwargs: DictStrAny
) -> Type[DjangoModel]:
for attr, value in self.dict(**kwargs).items():
self, model_instance: t.Type[DjangoModel], **kwargs: DictStrAny
) -> t.Type[DjangoModel]:
for attr, value in self.dict(**kwargs).items(): # type:ignore[attr-defined]
setattr(model_instance, attr, value)
return model_instance


if not IS_PYDANTIC_V1:
from pydantic import BaseModel, model_validator
from pydantic.json_schema import GenerateJsonSchema
from pydantic_core.core_schema import ValidationInfo

class BaseMixinsV2(BaseMixins):
@model_validator(mode="before")
def _run_root_validator(cls, values: t.Any, info: ValidationInfo) -> t.Any:
values = DjangoGetter(values, cls, info.context)
return values

@classmethod
def from_orm(cls, obj: t.Any, **options: t.Any) -> BaseModel:
return cls.model_validate( # type:ignore[attr-defined,no-any-return]
obj, **options
)

def dict(self, *a: t.Any, **kw: t.Any) -> DictStrAny:
# Backward compatibility with pydantic 1.x
return self.model_dump(*a, **kw) # type:ignore[attr-defined,no-any-return]

@classmethod
def json_schema(cls) -> DictStrAny:
return cls.model_json_schema( # type:ignore[attr-defined,no-any-return]
schema_generator=GenerateJsonSchema
)

@classmethod
def schema(cls) -> DictStrAny:
warnings.warn(
".schema() is deprecated, use .json_schema() instead",
DeprecationWarning,
stacklevel=2,
)
return cls.json_schema()

BaseMixins = BaseMixinsV2 # type:ignore[misc]


class SchemaMixins(BaseMixins):
pass
Loading

0 comments on commit 8dd3411

Please sign in to comment.