From b4109e72718d1b4caf3b347856b1a4e5495ee1fb Mon Sep 17 00:00:00 2001 From: James Chua Date: Fri, 7 Jan 2022 19:59:54 +0800 Subject: [PATCH 1/9] fix(pydantic): Check for AutoFieldsNotInBaseModelError when converting from pydantic models. --- RELEASE.MD | 18 ++++++ .../experimental/pydantic/exceptions.py | 12 +++- .../experimental/pydantic/object_type.py | 5 ++ strawberry/experimental/pydantic/utils.py | 20 +++++- .../pydantic/schema/test_basic.py | 6 +- .../experimental/pydantic/test_conversion.py | 62 +++++++++++++++++++ 6 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 RELEASE.MD diff --git a/RELEASE.MD b/RELEASE.MD new file mode 100644 index 0000000000..3e257ccf3e --- /dev/null +++ b/RELEASE.MD @@ -0,0 +1,18 @@ +Release type: patch + + This release checks for AutoFieldsNotInBaseModelError when converting from pydantic models. + It is raised when strawberry.auto is used, but the pydantic model does not have +the particular field defined. + +```{python} +class User(BaseModel): + age: int + +@strawberry.experimental.pydantic.type(User) +class UserType: + age: strawberry.auto + password: strawberry.auto +``` + +Previously no errors would be raised, and the password field would not appear on graphql schema. +Such mistakes could be common during refactoring. Now, AutoFieldsNotInBaseModelError is raised. diff --git a/strawberry/experimental/pydantic/exceptions.py b/strawberry/experimental/pydantic/exceptions.py index 3e7145b835..f112807be0 100644 --- a/strawberry/experimental/pydantic/exceptions.py +++ b/strawberry/experimental/pydantic/exceptions.py @@ -1,4 +1,4 @@ -from typing import Any, Type +from typing import Any, List, Type from pydantic import BaseModel from pydantic.typing import NoArgAnyCallable @@ -35,3 +35,13 @@ def __init__(self, default: Any, default_factory: NoArgAnyCallable): ) super().__init__(message) + + +class AutoFieldsNotInBaseModelError(Exception): + def __init__(self, fields: List[str], cls_name: str, model: Type[BaseModel]): + message = ( + f"{cls_name} defines {fields} with strawberry.auto. " + f"Field(s) not present in {model.__name__} BaseModel." + ) + + super().__init__(message) diff --git a/strawberry/experimental/pydantic/object_type.py b/strawberry/experimental/pydantic/object_type.py index 2bb92c7fdf..993edca1a5 100644 --- a/strawberry/experimental/pydantic/object_type.py +++ b/strawberry/experimental/pydantic/object_type.py @@ -18,6 +18,7 @@ from strawberry.experimental.pydantic.fields import get_basic_type from strawberry.experimental.pydantic.utils import ( DataclassCreationFields, + ensure_all_auto_fields_in_pydantic, get_default_factory_for_field, get_private_fields, sort_creation_fields, @@ -111,6 +112,10 @@ def wrap(cls): if not fields_set: raise MissingFieldsListError(cls) + ensure_all_auto_fields_in_pydantic( + model=model, auto_fields=fields_set, cls_name=cls.__name__ + ) + all_model_fields: List[DataclassCreationFields] = [ DataclassCreationFields( name=field_name, diff --git a/strawberry/experimental/pydantic/utils.py b/strawberry/experimental/pydantic/utils.py index 790837d0ad..8ad9a63fd5 100644 --- a/strawberry/experimental/pydantic/utils.py +++ b/strawberry/experimental/pydantic/utils.py @@ -1,12 +1,14 @@ import dataclasses -from typing import Any, List, NamedTuple, Tuple, Type, Union, cast +from typing import Any, List, NamedTuple, NoReturn, Set, Tuple, Type, Union, cast +from pydantic import BaseModel from pydantic.fields import ModelField from pydantic.typing import NoArgAnyCallable from pydantic.utils import smart_deepcopy from strawberry.arguments import UNSET, _Unset, is_unset # type: ignore from strawberry.experimental.pydantic.exceptions import ( + AutoFieldsNotInBaseModelError, BothDefaultAndDefaultFactoryDefinedError, UnregisteredTypeException, ) @@ -118,3 +120,19 @@ def get_default_factory_for_field(field: ModelField) -> Union[NoArgAnyCallable, return lambda: None return UNSET + + +def ensure_all_auto_fields_in_pydantic( + model: Type[BaseModel], auto_fields: Set[str], cls_name: str +) -> Union[NoReturn, None]: + # Raise error if user defined a strawberry.auto field not present in the model + pydantic_fields = set(field_name for field_name, _ in model.__fields__.items()) + undefined = [ + field_name for field_name in auto_fields if field_name not in pydantic_fields + ] + if len(undefined) > 0: + raise AutoFieldsNotInBaseModelError( + fields=undefined, cls_name=cls_name, model=model + ) + else: + return None diff --git a/tests/experimental/pydantic/schema/test_basic.py b/tests/experimental/pydantic/schema/test_basic.py index eef6c84f3f..847654fb55 100644 --- a/tests/experimental/pydantic/schema/test_basic.py +++ b/tests/experimental/pydantic/schema/test_basic.py @@ -332,7 +332,7 @@ class BranchAType: class BranchBType: pass - @strawberry.experimental.pydantic.type(User, fields=["age", "union_field"]) + @strawberry.experimental.pydantic.type(User, fields=["union_field"]) class UserType: pass @@ -370,7 +370,7 @@ class BranchAType: class BranchBType: pass - @strawberry.experimental.pydantic.type(User, fields=["age", "union_field"]) + @strawberry.experimental.pydantic.type(User, fields=["union_field"]) class UserType: pass @@ -446,7 +446,7 @@ class BranchAType(BaseType): class BranchBType(BaseType): pass - @strawberry.experimental.pydantic.type(User, fields=["age", "interface_field"]) + @strawberry.experimental.pydantic.type(User, fields=["interface_field"]) class UserType: pass diff --git a/tests/experimental/pydantic/test_conversion.py b/tests/experimental/pydantic/test_conversion.py index 7c0460b095..2dfbe664f6 100644 --- a/tests/experimental/pydantic/test_conversion.py +++ b/tests/experimental/pydantic/test_conversion.py @@ -1,3 +1,4 @@ +import re from enum import Enum from typing import Any, List, Optional, Union, cast @@ -10,6 +11,7 @@ import strawberry from strawberry.arguments import UNSET from strawberry.experimental.pydantic.exceptions import ( + AutoFieldsNotInBaseModelError, BothDefaultAndDefaultFactoryDefinedError, ) from strawberry.experimental.pydantic.utils import ( @@ -55,6 +57,66 @@ class UserType: assert user.password == "abc" +def test_cannot_convert_pydantic_type_to_strawberry_missing_field(): + class User(BaseModel): + age: int + + with pytest.raises( + AutoFieldsNotInBaseModelError, + match=re.escape( + "UserType defines ['password'] with strawberry.auto." + " Field(s) not present in User BaseModel." + ), + ): + + @strawberry.experimental.pydantic.type(User) + class UserType: + age: strawberry.auto + password: strawberry.auto + + +def test_cannot_convert_pydantic_type_to_strawberry_property_auto(): + class User(BaseModel): + age: int + + @property + def password(self) -> str: + return "hunter2" + + with pytest.raises( + AutoFieldsNotInBaseModelError, + match=re.escape( + "UserType defines ['password'] with strawberry.auto." + " Field(s) not present in User BaseModel." + ), + ): + + @strawberry.experimental.pydantic.type(User) + class UserType: + age: strawberry.auto + password: strawberry.auto + + +def test_can_convert_pydantic_type_to_strawberry_property(): + class User(BaseModel): + age: int + + @property + def password(self) -> str: + return "hunter2" + + @strawberry.experimental.pydantic.type(User) + class UserType: + age: strawberry.auto + password: str + + origin_user = User(age=1) + user = UserType.from_pydantic(origin_user) + + assert user.age == 1 + assert user.password == "hunter2" + + def test_can_convert_alias_pydantic_field_to_strawberry(): class UserModel(BaseModel): age_: int = Field(..., alias="age") From 6de86dfe986a6f80ec1adafd2dc5637fb9b4ad6d Mon Sep 17 00:00:00 2001 From: James Chua Date: Fri, 7 Jan 2022 20:04:58 +0800 Subject: [PATCH 2/9] fix(pydantic): add comment on test --- tests/experimental/pydantic/test_conversion.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/experimental/pydantic/test_conversion.py b/tests/experimental/pydantic/test_conversion.py index 2dfbe664f6..1d6c5496ca 100644 --- a/tests/experimental/pydantic/test_conversion.py +++ b/tests/experimental/pydantic/test_conversion.py @@ -76,6 +76,8 @@ class UserType: def test_cannot_convert_pydantic_type_to_strawberry_property_auto(): + # auto inferring type of a property is not supported + class User(BaseModel): age: int From bbf70e0ce4d68c6802a45bc8248dc75dcaf25bd2 Mon Sep 17 00:00:00 2001 From: James Chua <30519287+thejaminator@users.noreply.github.com> Date: Fri, 7 Jan 2022 20:22:41 +0800 Subject: [PATCH 3/9] Update RELEASE.MD Co-authored-by: Patrick Arminio --- RELEASE.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.MD b/RELEASE.MD index 3e257ccf3e..2c25eabdc3 100644 --- a/RELEASE.MD +++ b/RELEASE.MD @@ -4,7 +4,7 @@ Release type: patch It is raised when strawberry.auto is used, but the pydantic model does not have the particular field defined. -```{python} +```python class User(BaseModel): age: int From 24e4df3dff5b43f3d93d83c01f9dc11bfc928239 Mon Sep 17 00:00:00 2001 From: James Chua Date: Fri, 7 Jan 2022 20:24:09 +0800 Subject: [PATCH 4/9] fix(pydantic): fix RELEASE.md name --- RELEASE.MD => RELEASE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename RELEASE.MD => RELEASE.md (100%) diff --git a/RELEASE.MD b/RELEASE.md similarity index 100% rename from RELEASE.MD rename to RELEASE.md From f8d70101639944ea1bd62797afd0095588d1c1e4 Mon Sep 17 00:00:00 2001 From: James Chua Date: Fri, 7 Jan 2022 20:26:41 +0800 Subject: [PATCH 5/9] fix(pydantic): Simplify ensure_all_auto_fields_in_pydantic --- strawberry/experimental/pydantic/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/strawberry/experimental/pydantic/utils.py b/strawberry/experimental/pydantic/utils.py index 8ad9a63fd5..fa7b01ad14 100644 --- a/strawberry/experimental/pydantic/utils.py +++ b/strawberry/experimental/pydantic/utils.py @@ -126,9 +126,8 @@ def ensure_all_auto_fields_in_pydantic( model: Type[BaseModel], auto_fields: Set[str], cls_name: str ) -> Union[NoReturn, None]: # Raise error if user defined a strawberry.auto field not present in the model - pydantic_fields = set(field_name for field_name, _ in model.__fields__.items()) undefined = [ - field_name for field_name in auto_fields if field_name not in pydantic_fields + field_name for field_name in auto_fields if field_name not in model.__fields__ ] if len(undefined) > 0: raise AutoFieldsNotInBaseModelError( From fd9f095f3ac581c97ea49a82813b9e17798449b7 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Fri, 7 Jan 2022 14:01:23 +0100 Subject: [PATCH 6/9] Update strawberry/experimental/pydantic/utils.py --- strawberry/experimental/pydantic/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/strawberry/experimental/pydantic/utils.py b/strawberry/experimental/pydantic/utils.py index fa7b01ad14..68b5977539 100644 --- a/strawberry/experimental/pydantic/utils.py +++ b/strawberry/experimental/pydantic/utils.py @@ -126,10 +126,9 @@ def ensure_all_auto_fields_in_pydantic( model: Type[BaseModel], auto_fields: Set[str], cls_name: str ) -> Union[NoReturn, None]: # Raise error if user defined a strawberry.auto field not present in the model - undefined = [ - field_name for field_name in auto_fields if field_name not in model.__fields__ - ] - if len(undefined) > 0: + non_existing_fields = auto_fields - model.__fields__.keys() + + if len(non_existing_fields) > 0: raise AutoFieldsNotInBaseModelError( fields=undefined, cls_name=cls_name, model=model ) From 044624255bab4cc044ec45555f792c00fefa3f2a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Jan 2022 13:01:57 +0000 Subject: [PATCH 7/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- strawberry/experimental/pydantic/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strawberry/experimental/pydantic/utils.py b/strawberry/experimental/pydantic/utils.py index 68b5977539..b5de76015a 100644 --- a/strawberry/experimental/pydantic/utils.py +++ b/strawberry/experimental/pydantic/utils.py @@ -127,7 +127,7 @@ def ensure_all_auto_fields_in_pydantic( ) -> Union[NoReturn, None]: # Raise error if user defined a strawberry.auto field not present in the model non_existing_fields = auto_fields - model.__fields__.keys() - + if len(non_existing_fields) > 0: raise AutoFieldsNotInBaseModelError( fields=undefined, cls_name=cls_name, model=model From 0bcc522906885ba992cbd3aa9aba6053f76f5113 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Fri, 7 Jan 2022 14:03:33 +0100 Subject: [PATCH 8/9] Fix typo --- strawberry/experimental/pydantic/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strawberry/experimental/pydantic/utils.py b/strawberry/experimental/pydantic/utils.py index b5de76015a..1217ac360f 100644 --- a/strawberry/experimental/pydantic/utils.py +++ b/strawberry/experimental/pydantic/utils.py @@ -130,7 +130,7 @@ def ensure_all_auto_fields_in_pydantic( if len(non_existing_fields) > 0: raise AutoFieldsNotInBaseModelError( - fields=undefined, cls_name=cls_name, model=model + fields=non_existing_fields, cls_name=cls_name, model=model ) else: return None From a3e13792c67d81131824fd01218e890aaa1fa653 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Fri, 7 Jan 2022 13:25:15 +0000 Subject: [PATCH 9/9] Fix type --- strawberry/experimental/pydantic/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strawberry/experimental/pydantic/utils.py b/strawberry/experimental/pydantic/utils.py index 1217ac360f..1ae4259c83 100644 --- a/strawberry/experimental/pydantic/utils.py +++ b/strawberry/experimental/pydantic/utils.py @@ -126,9 +126,9 @@ def ensure_all_auto_fields_in_pydantic( model: Type[BaseModel], auto_fields: Set[str], cls_name: str ) -> Union[NoReturn, None]: # Raise error if user defined a strawberry.auto field not present in the model - non_existing_fields = auto_fields - model.__fields__.keys() + non_existing_fields = list(auto_fields - model.__fields__.keys()) - if len(non_existing_fields) > 0: + if non_existing_fields: raise AutoFieldsNotInBaseModelError( fields=non_existing_fields, cls_name=cls_name, model=model )