Repository: strawberry-graphql/strawberry

Issue:
Ability to set additional fields in `to_pydantic` for `@strawberry.experimental.pydantic`
<!--- Provide a general summary of the changes you want in the title above. -->

I've been using the experimental pydantic convertors (@strawberry.experimental.pydantic.). When the model is an input - I usually have two models, StrawberryInput and ServiceInput where the ServiceInput almost always has just one additional field (`customer_id`) since we get that from the context. I should be able to do `.to_pydantic(customer_id=customer_id)`.

Discussed in [Discord](https://discord.com/channels/689806334337482765/773519351423827978/989937871450411080).

<!--- This template is entirely optional and can be removed, but is here to help both you and us. -->
<!--- Anything on lines wrapped in comments like these will not show up in the final text. -->

## Feature Request Type

- [ ] Core functionality
- [ ] Alteration (enhancement/optimization) of existing feature(s)
- [ ] New behavior

## Description

<!-- A few sentences describing what it is. -->

Allow to_pydantic to take in **kwargs to pass to pydantic constructor
<!--- Provide a general summary of the changes you want in the title above. -->

<!--- This template is entirely optional and can be removed, but is here to help both you and us. -->
<!--- Anything on lines wrapped in comments like these will not show up in the final text. -->

## Feature Request Type

- [ ] Core functionality
- [x ] Alteration (enhancement/optimization) of existing feature(s)
- [ ] New behavior

## Description

Currently this goes boom because the pydantic model will be missing a required field `passsword`

```python
from pydantic import BaseModel
import strawberry


class MyModel(BaseModel):
    email: str
    password: str


@strawberry.experimental.pydantic.input(model=MyModel)
class MyModelStrawberry:
    email: strawberry.auto


MyModelStrawberry(email="").to_pydantic()

```

We add the kwargs param in to_pydantic
```python
  def to_pydantic(self, **kwargs) -> PydanticModel:
      ...
```
and pass it to the constructor

Also since we can tell when the strawberry type doesn't have all the fields required for pydantic at mypy type check time. we can enhacne the mypy plugin to error when `to_pydantic` is called with a missing required field. I have a draft mr for that which i will submit soon


Discussion:

Resolving patch:
diff --git a/strawberry/experimental/pydantic/conversion_types.py b/strawberry/experimental/pydantic/conversion_types.py
--- a/strawberry/experimental/pydantic/conversion_types.py
+++ b/strawberry/experimental/pydantic/conversion_types.py
@@ -24,7 +24,7 @@ def from_pydantic(
     ) -> StrawberryTypeFromPydantic[PydanticModel]:
         ...
 
-    def to_pydantic(self) -> PydanticModel:
+    def to_pydantic(self, **kwargs) -> PydanticModel:
         ...
 
     @property
diff --git a/strawberry/experimental/pydantic/object_type.py b/strawberry/experimental/pydantic/object_type.py
--- a/strawberry/experimental/pydantic/object_type.py
+++ b/strawberry/experimental/pydantic/object_type.py
@@ -245,13 +245,14 @@ def from_pydantic_default(
                 cls=cls, model_instance=instance, extra=extra
             )
 
-        def to_pydantic_default(self) -> PydanticModel:
+        def to_pydantic_default(self, **kwargs) -> PydanticModel:
             instance_kwargs = {
                 f.name: convert_strawberry_class_to_pydantic_model(
                     getattr(self, f.name)
                 )
                 for f in dataclasses.fields(self)
             }
+            instance_kwargs.update(kwargs)
             return model(**instance_kwargs)
 
         if not has_custom_from_pydantic:
diff --git a/strawberry/ext/mypy_plugin.py b/strawberry/ext/mypy_plugin.py
--- a/strawberry/ext/mypy_plugin.py
+++ b/strawberry/ext/mypy_plugin.py
@@ -1,8 +1,10 @@
 from decimal import Decimal
 from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union, cast
 
+from pydantic.mypy import METADATA_KEY as PYDANTIC_METADATA_KEY, PydanticModelField
 from typing_extensions import Final
 
+import mypy
 from mypy.nodes import (
     ARG_OPT,
     ARG_POS,
@@ -388,13 +390,55 @@ def strawberry_pydantic_class_callback(ctx: ClassDefContext) -> None:
         ]
         add_method(ctx, "__init__", init_args, NoneType())
 
-        model_type = _get_type_for_expr(model_expression, ctx.api)
+        model_type = cast(
+            mypy.types.Instance, _get_type_for_expr(model_expression, ctx.api)
+        )
+
+        # these are the fields that the user added to the strawberry type
+        new_strawberry_fields: Set[str] = set()
+
+        # TODO: think about inheritance for strawberry?
+        for stmt in ctx.cls.defs.body:
+            if isinstance(stmt, AssignmentStmt):
+                lhs = cast(NameExpr, stmt.lvalues[0])
+                new_strawberry_fields.add(lhs.name)
+
+        # grab the fields created from the pydantic plugin
+        pydantic_fields: Set[PydanticModelField] = set()
+        try:
+            for name, data in model_type.type.metadata[PYDANTIC_METADATA_KEY][
+                "fields"
+            ].items():
+                field = PydanticModelField.deserialize(ctx.cls.info, data)
+                pydantic_fields.add(field)
+        except KeyError:
+            # this will happen if the user didn't add the pydantic plugin
+            # AND is using the pydantic conversion decorator
+            ctx.api.fail(
+                "Pydantic plugin not installed,"
+                " please add pydantic.mypy your mypy.ini plugins",
+                ctx.reason,
+            )
+
+        missing_pydantic_fields: Set[PydanticModelField] = set(
+            f for f in pydantic_fields if f.name not in new_strawberry_fields
+        )
 
         # Add to_pydantic
+        # TODO: Only add this if not manually defined
         add_method(
             ctx,
             "to_pydantic",
-            args=[],
+            args=[
+                f.to_argument(
+                    # TODO: use_alias should depend on config?
+                    info=model_type.type,
+                    typed=True,
+                    force_optional=False,
+                    use_alias=True,
+                )
+                for f in missing_pydantic_fields
+            ],
             return_type=model_type,
         )
 


Test patch:
diff --git a/tests/experimental/pydantic/test_conversion.py b/tests/experimental/pydantic/test_conversion.py
--- a/tests/experimental/pydantic/test_conversion.py
+++ b/tests/experimental/pydantic/test_conversion.py
@@ -5,7 +5,7 @@
 
 import pytest
 
-from pydantic import BaseConfig, BaseModel, Field
+from pydantic import BaseConfig, BaseModel, Field, ValidationError
 from pydantic.fields import ModelField
 from pydantic.typing import NoArgAnyCallable
 
@@ -1156,3 +1156,37 @@ class UserInput:
     assert user.age == 1
     assert user.password is None
     assert user.work["Monday"].hours == 1
+
+
+def test_can_add_missing_arguments_to_pydantic():
+    class User(BaseModel):
+        age: int
+        password: str
+
+    @strawberry.experimental.pydantic.type(User)
+    class UserInput:
+        age: strawberry.auto
+
+    data = UserInput(age=1)
+    user = data.to_pydantic(password="hunter2")
+
+    assert user.age == 1
+    assert user.password == "hunter2"
+
+
+def test_raise_missing_arguments_to_pydantic():
+    class User(BaseModel):
+        age: int
+        password: str
+
+    @strawberry.experimental.pydantic.type(User)
+    class UserInput:
+        age: strawberry.auto
+
+    data = UserInput(age=1)
+
+    with pytest.raises(
+        ValidationError,
+        match=("1 validation error for User"),
+    ):
+        data.to_pydantic()
diff --git a/tests/mypy/test_pydantic.decorators.yml b/tests/mypy/test_pydantic.decorators.yml
--- a/tests/mypy/test_pydantic.decorators.yml
+++ b/tests/mypy/test_pydantic.decorators.yml
@@ -147,3 +147,60 @@
     reveal_type(UserStrawberry.from_pydantic(UserPydantic(age=123)).to_pydantic())
   out: |
      main:11: note: Revealed type is "main.UserPydantic"
+
+- case: test_to_pydantic_kwargs
+  main: |
+    from pydantic import BaseModel
+    import strawberry
+
+
+    class MyModel(BaseModel):
+        email: str
+        password: str
+
+
+    @strawberry.experimental.pydantic.input(model=MyModel)
+    class MyModelStrawberry:
+        email: strawberry.auto
+
+
+    MyModelStrawberry(email="").to_pydantic()
+  out: |
+     main:15: error: Missing named argument "password" for "to_pydantic" of "MyModelStrawberry"  [call-arg]
+- case: test_to_pydantic_kwargs_with_defaults
+  main: |
+    from pydantic import BaseModel
+    import strawberry
+
+
+    class MyModel(BaseModel):
+        email: str
+        password: str = "hunter2"
+
+
+    @strawberry.experimental.pydantic.input(model=MyModel)
+    class MyModelStrawberry:
+        email: strawberry.auto
+
+
+    MyModelStrawberry(email="").to_pydantic()
+  out: |
+- case: test_to_pydantic_kwargs_with_optional_default
+  main: |
+    from pydantic import BaseModel
+    from typing import Optional
+    import strawberry
+
+
+    class MyModel(BaseModel):
+        email: str
+        password: Optional[str] # pydantic makes this implicit = None default
+
+
+    @strawberry.experimental.pydantic.input(model=MyModel)
+    class MyModelStrawberry:
+        email: strawberry.auto
+
+
+    MyModelStrawberry(email="").to_pydantic()
+  out: |