Repository: litestar-org/polyfactory (python)

Issue:
Enhancement: issue an error when defining attributes that are not part of the model
### Summary

Defining a field on a factory (e.g: with PostGenerated) that is not defined on the target model will result into the field being completely ignored.

When making an error, e.g: a typo in the field name on the factory, it's hard to know what's going on.

I'd like an error to be thrown telling me that I tried to define a field that doesn't exist on the model.

### Basic Example

```
from dataclasses import dataclass

from polyfactory import PostGenerated
from polyfactory.factories import DataclassFactory


@dataclass
class Pet:
    name: str
    person_id: str


@dataclass
class Person:
    name: str


class PetFactory(DataclassFactory[Pet]):
    __model__ = Pet


def add_pets(name, values, *args, **defaults):
    return [PetFactory.build()]


class PersonFactory(DataclassFactory[Person]):
    __model__ = Person

    pets = PostGenerated(add_pets)


person = PersonFactory.build()

print(person)
``` 

This will output the following and silently ignore `pets`:

> Person(name='lEoXzCHKCZmVcFmEeoqP')


### Drawbacks and Impact

Improved developer experience.

### Unresolved questions

_No response_

Pull request:
feat: check if fields declared on the factory belong to the model
<!--
By submitting this pull request, you agree to:
- follow [Litestar's Code of Conduct](https://github.com/litestar-org/.github/blob/main/CODE_OF_CONDUCT.md)
- follow [Litestar's contribution guidelines](https://github.com/litestar-org/.github/blob/main/CONTRIBUTING.md)
- follow the [PSFs's Code of Conduct](https://www.python.org/psf/conduct/)
-->

### Pull Request Checklist

- [x] New code has 100% test coverage
- [x] (If applicable) The prose documentation has been updated to reflect the changes introduced by this PR
- [ ] (If applicable) The reference documentation has been updated to reflect the changes introduced by this PR
- [x] Pre-Commit Checks were ran and passed
- [x] Tests were ran and passed

### Description
<!--
Please describe your pull request for new release changelog purposes
-->
This change will detect whenever a field is defined on the factory that doesn't exist on the model.

- If the field is using an explicit construct (like `Use`, `PostGenerated`, ...) it will raise an exception
- in other case it will only output a warning
- classmethods / methods / functions and fields starting with `__`  are not checked because they can't be easily differentiated


### Close Issue(s)
Fixes #400
<!--
Please add in issue numbers this pull request will close, if applicable
Examples:  or Closes #1234
-->

- 


Patch:
diff --git a/docs/usage/configuration.rst b/docs/usage/configuration.rst
index cf37b73..d38d53c 100644
--- a/docs/usage/configuration.rst
+++ b/docs/usage/configuration.rst
@@ -101,3 +101,15 @@ By setting to `False`, then optional types will always be treated as the wrapped
 .. literalinclude:: /examples/configuration/test_example_7.py
     :caption: Disable Allow None Optionals
     :language: python
+
+Check Factory Fields
+--------------------
+When `__check_model__` is set to `True`, declaring fields on the factory that don't exist on the model will trigger an exception.
+
+This is only true when fields are declared with ``Use``, ``PostGenerated``, ``Ignore`` and ``Require``.
+Any other field definition will not be checked.
+
+
+.. literalinclude:: /examples/configuration/test_example_8.py
+    :caption: Enable Check Factory Fields
+    :language: python
diff --git a/docs/usage/declaring_factories.rst b/docs/usage/declaring_factories.rst
index 37402c9..6057152 100644
--- a/docs/usage/declaring_factories.rst
+++ b/docs/usage/declaring_factories.rst
@@ -38,6 +38,7 @@ Or for attrs models:
     Validators are not currently supported - neither the built in validators that come
     with `attrs` nor custom validators.
 
+
 Imperative Factory Creation
 ---------------------------
 
diff --git a/polyfactory/factories/base.py b/polyfactory/factories/base.py
index eeee424..aecb9cf 100644
--- a/polyfactory/factories/base.py
+++ b/polyfactory/factories/base.py
@@ -102,6 +102,11 @@ class BaseFactory(ABC, Generic[T]):
     The model for the factory.
     This attribute is required for non-base factories and an exception will be raised if its not set.
     """
+    __check_model__: bool = False
+    """
+    Flag dictating whether to check if fields defined on the factory exists on the model or not.
+    If 'True', checks will be done against Use, PostGenerated, Ignore, Require constructs fields only.
+    """
     __allow_none_optionals__: ClassVar[bool] = True
     """
     Flag dictating whether to allow 'None' for optional values.
@@ -184,7 +189,7 @@ class BaseFactory(ABC, Generic[T]):
             if not cls.is_supported_type(model):
                 for factory in BaseFactory._base_factories:
                     if factory.is_supported_type(model):
-                        msg = f"{cls.__name__} does not support {model.__name__}, but this type is support by the {factory.__name__} base factory class. To resolve this error, subclass the factory from {factory.__name__} instead of {cls.__name__}"
+                        msg = f"{cls.__name__} does not support {model.__name__}, but this type is supported by the {factory.__name__} base factory class. To resolve this error, subclass the factory from {factory.__name__} instead of {cls.__name__}"
                         raise ConfigurationException(
                             msg,
                         )
@@ -192,6 +197,8 @@ class BaseFactory(ABC, Generic[T]):
                     raise ConfigurationException(
                         msg,
                     )
+            if cls.__check_model__:
+                cls._check_declared_fields_exist_in_model()
         else:
             BaseFactory._base_factories.append(cls)
 
@@ -665,6 +672,38 @@ class BaseFactory(ABC, Generic[T]):
         """
         raise NotImplementedError
 
+    @classmethod
+    def get_factory_fields(cls) -> list[tuple[str, Any]]:
+        """Retrieve a list of fields from the factory.
+
+        Trying to be smart about what should be considered a field on the model,
+        ignoring dunder methods and some parent class attributes.
+
+        :returns: A list of tuples made of field name and field definition
+        """
+        factory_fields = cls.__dict__.items()
+        return [
+            (field_name, field_value)
+            for field_name, field_value in factory_fields
+            if not (field_name.startswith("__") or field_name == "_abc_impl")
+        ]
+
+    @classmethod
+    def _check_declared_fields_exist_in_model(cls) -> None:
+        model_fields_names = {field_meta.name for field_meta in cls.get_model_fields()}
+        factory_fields = cls.get_factory_fields()
+
+        for field_name, field_value in factory_fields:
+            if field_name in model_fields_names:
+                continue
+
+            error_message = (
+                f"{field_name} is declared on the factory {cls.__name__}"
+                f" but it is not part of the model {cls.__model__.__name__}"
+            )
+            if isinstance(field_value, (Use, PostGenerated, Ignore, Require)):
+                raise ConfigurationException(error_message)
+
     @classmethod
     def process_kwargs(cls, **kwargs: Any) -> dict[str, Any]:
         """Process the given kwargs and generate values for the factory's model.


Test patch:
diff --git a/docs/examples/configuration/test_example_8.py b/docs/examples/configuration/test_example_8.py
new file mode 100644
index 0000000..3a440b2
--- /dev/null
+++ b/docs/examples/configuration/test_example_8.py
@@ -0,0 +1,24 @@
+from dataclasses import dataclass
+from uuid import UUID
+
+import pytest
+
+from polyfactory import ConfigurationException, PostGenerated
+from polyfactory.factories.dataclass_factory import DataclassFactory
+
+
+@dataclass
+class Person:
+    id: UUID
+
+
+def test_check_factory_fields() -> None:
+    with pytest.raises(
+        ConfigurationException,
+        match="unknown_field is declared on the factory PersonFactory but it is not part of the model Person",
+    ):
+
+        class PersonFactory(DataclassFactory[Person]):
+            __model__ = Person
+            __check_model__ = True
+            unknown_field = PostGenerated(lambda: "foo")
diff --git a/tests/test_factory_fields.py b/tests/test_factory_fields.py
index 935ffe8..b6d0151 100644
--- a/tests/test_factory_fields.py
+++ b/tests/test_factory_fields.py
@@ -1,12 +1,12 @@
 import random
 from datetime import datetime, timedelta
-from typing import Any, Optional
+from typing import Any, Optional, Union
 
 import pytest
 from pydantic import BaseModel
 
 from polyfactory.decorators import post_generated
-from polyfactory.exceptions import MissingBuildKwargException
+from polyfactory.exceptions import ConfigurationException, MissingBuildKwargException
 from polyfactory.factories.pydantic_factory import ModelFactory
 from polyfactory.fields import Ignore, PostGenerated, Require, Use
 
@@ -161,3 +161,43 @@ def test_post_generation_classmethod() -> None:
         assert result.caption == "this was really long for me"
     else:
         assert result.caption == "just this"
+
+
+@pytest.mark.parametrize(
+    "factory_field",
+    [
+        Use(lambda: "foo"),
+        PostGenerated(lambda: "foo"),
+        Require(),
+        Ignore(),
+    ],
+)
+def test_non_existing_model_fields_does_not_raise_by_default(
+    factory_field: Union[Use, PostGenerated, Require, Ignore],
+) -> None:
+    class NoFieldModel(BaseModel):
+        pass
+
+    ModelFactory.create_factory(NoFieldModel, bases=None, unknown_field=factory_field)
+
+
+@pytest.mark.parametrize(
+    "factory_field",
+    [
+        Use(lambda: "foo"),
+        PostGenerated(lambda: "foo"),
+        Require(),
+        Ignore(),
+    ],
+)
+def test_non_existing_model_fields_raises_with__check__model__(
+    factory_field: Union[Use, PostGenerated, Require, Ignore],
+) -> None:
+    class NoFieldModel(BaseModel):
+        pass
+
+    with pytest.raises(
+        ConfigurationException,
+        match="unknown_field is declared on the factory NoFieldModelFactory but it is not part of the model NoFieldModel",
+    ):
+        ModelFactory.create_factory(NoFieldModel, bases=None, __check_model__=True, unknown_field=factory_field)