diff --git a/ormar/models/helpers/pydantic.py b/ormar/models/helpers/pydantic.py index 1423176..03ab736 100644 --- a/ormar/models/helpers/pydantic.py +++ b/ormar/models/helpers/pydantic.py @@ -50,9 +50,10 @@ def get_pydantic_field(field_name: str, model: Type["Model"]) -> "ModelField": :return: newly created pydantic field :rtype: pydantic.ModelField """ + type_ = model.Meta.model_fields[field_name].__type__ return ModelField( name=field_name, - type_=model.Meta.model_fields[field_name].__type__, # type: ignore + type_=type_, # type: ignore model_config=model.__config__, required=not model.Meta.model_fields[field_name].nullable, class_validators={}, diff --git a/ormar/models/helpers/relations.py b/ormar/models/helpers/relations.py index 68bf893..c6d1ba4 100644 --- a/ormar/models/helpers/relations.py +++ b/ormar/models/helpers/relations.py @@ -101,6 +101,7 @@ def register_reverse_model_fields(model_field: "ForeignKeyField") -> None: :type model_field: relation Field """ related_name = model_field.get_related_name() + # TODO: Reverse relations does not register pydantic fields? if model_field.is_multi: model_field.to.Meta.model_fields[related_name] = ManyToMany( # type: ignore model_field.owner, diff --git a/ormar/models/mixins/pydantic_mixin.py b/ormar/models/mixins/pydantic_mixin.py index ce63f05..1ecdd89 100644 --- a/ormar/models/mixins/pydantic_mixin.py +++ b/ormar/models/mixins/pydantic_mixin.py @@ -1,3 +1,4 @@ +import copy import string from random import choices from typing import ( @@ -82,7 +83,9 @@ class PydanticMixin(RelationMixin): (pydantic.BaseModel,), {"__annotations__": fields_dict, **defaults}, ) - return cast(Type[pydantic.BaseModel], model) + model = cast(Type[pydantic.BaseModel], model) + cls._copy_field_validators(model=model) + return model @classmethod def _determine_pydantic_field_type( @@ -111,3 +114,33 @@ class PydanticMixin(RelationMixin): if target is not None and field.nullable: target = Optional[target] return target + + @classmethod + def _copy_field_validators(cls, model: Type[pydantic.BaseModel]) -> None: + """ + Copy field validators from ormar model to generated pydantic model. + """ + for field_name, field in model.__fields__.items(): + if ( + field_name not in cls.__fields__ + or cls.Meta.model_fields[field_name].is_relation + ): + continue + validators = cls.__fields__[field_name].validators + already_attached = [ + validator.__wrapped__ for validator in field.validators # type: ignore + ] + validators_to_copy = [ + validator + for validator in validators + if validator.__wrapped__ not in already_attached # type: ignore + ] + field.validators.extend(copy.deepcopy(validators_to_copy)) + class_validators = cls.__fields__[field_name].class_validators + field.class_validators.update(copy.deepcopy(class_validators)) + field.pre_validators = copy.deepcopy( + cls.__fields__[field_name].pre_validators + ) + field.post_validators = copy.deepcopy( + cls.__fields__[field_name].post_validators + ) diff --git a/tests/test_fastapi/test_excludes_with_get_pydantic.py b/tests/test_fastapi/test_excludes_with_get_pydantic.py index d9e9de4..b18976a 100644 --- a/tests/test_fastapi/test_excludes_with_get_pydantic.py +++ b/tests/test_fastapi/test_excludes_with_get_pydantic.py @@ -4,11 +4,8 @@ from fastapi import FastAPI from starlette.testclient import TestClient from tests.settings import DATABASE_URL -from tests.test_inheritance_and_pydantic_generation.test_geting_the_pydantic_models import ( +from tests.test_inheritance_and_pydantic_generation.test_geting_pydantic_models import ( Category, - Item, - MutualA, - MutualB, SelfRef, database, metadata, @@ -53,7 +50,9 @@ app.post("/categories/", response_model=Category)(create_category) response_model=SelfRef.get_pydantic(exclude={"parent", "children__name"}), ) async def create_selfref( - selfref: SelfRef.get_pydantic(exclude={"children__name"}), # type: ignore + selfref: SelfRef.get_pydantic( # type: ignore + exclude={"children__name"} # noqa: F821 + ), ): selfr = SelfRef(**selfref.dict()) await selfr.save() diff --git a/tests/test_inheritance_and_pydantic_generation/test_geting_the_pydantic_models.py b/tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py similarity index 100% rename from tests/test_inheritance_and_pydantic_generation/test_geting_the_pydantic_models.py rename to tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py diff --git a/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py b/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py index dc533d9..270d14e 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py +++ b/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py @@ -5,7 +5,6 @@ import pydantic import pytest import sqlalchemy from pydantic import ValidationError -from pydantic.class_validators import make_generic_validator import ormar @@ -44,26 +43,14 @@ class ModelExample(ormar.Model): raise ValueError("must contain a space") return v - -def validate_str_field(cls, v): - if " " not in v: - raise ValueError("must contain a space") - return v - - -def validate_choices(cls, v): - if v not in list(EnumExample): - raise ValueError(f"{v} is not in allowed choices: {list(EnumExample)}") - return v + @pydantic.validator("str_field") + def validate_str_field2(cls, v): + if " " not in v: + raise ValueError("must contain a space") + return v ModelExampleCreate = ModelExample.get_pydantic(exclude={"id"}) -ModelExampleCreate.__fields__["str_field"].validators.append( - make_generic_validator(validate_str_field) -) -ModelExampleCreate.__fields__["enum_field"].validators.append( - make_generic_validator(validate_choices) -) def test_ormar_validator():