Repository: litestar-org/polyfactory
Pull request #452 (2023-12-08 14:12:59)

feat: Optional `__model__` type
<!--
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
- [ ] (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
-->
For now you have to specify model type twice: both for a generic type and explicitly for a `__model__` field:
```python
@dataclass
class Person:
    name: str

class PersonFactory(DataclassFactory[Person]):  # << Person type
    __model__ = Person  # << Person type again 🤔 
```

This PR adds support for omitting `__model__` field specification:
```python
@dataclass
class Person:
    name: str

class PersonFactory(DataclassFactory[Person]):
    ... 
```

After PR is merged, next PR will be documentation fixes related.

### Close Issue(s)
<!--
Please add in issue numbers this pull request will close, if applicable
Examples: Fixes #4321 or Closes #1234
-->

- #445 


github-actions[bot]: Documentation preview will be available shortly at https://litestar-org.github.io/polyfactory-docs-preview/452
github-actions[bot]: Documentation preview will be available shortly at https://litestar-org.github.io/polyfactory-docs-preview/452
github-actions[bot]: Documentation preview will be available shortly at https://litestar-org.github.io/polyfactory-docs-preview/452
Mityuha: > This looks great! Just a few points regarding the tests and the documentation.
> 
> 1. I think it'd be good to document this behavior as part of this PR itself.
> 2. In the tests, I don't think it's necessary to test that the `build` method works correctly. Instead, a check to see that the `__model__` has been set properly is what should be tested since that's the behavior we're concerned about in these tests.
> 3. I think the tests can all be moved into one file, and the test for the happy case (i.e. the model is correctly available) can be tested by parameterizing. Something similar to what you've done in `test_model_inference_error.py`.

Thank you for review. All changes within change request were done. 
Mityuha: > @Mityuha sorry about this, but I wasn't clear in my previous review regarding the documentation. I think we would need to explicitly document this behavior somewhere. That is state explicitly the following:
> 
> * users may skip providing the `__model__` IF its type hinted in the generics
> * users can provide just the `__model__` and not provide the type in the generics
> * if a `__model__` is provided, then that will be used over whatever the value is given as the generic model type

@guacs OK, not a problem. Where you think is it better to document such behaviour? I would suggest documenting it
- in the `Usage Guide`/`Declaring Factories` section. Maybe inside like a `info` block
- optionally in README. Maybe in the `Example` section with the reference to `Declaring Factories` section
- optionally in the main documentation page with the reference like in README

Why do I suggest noting this behaviour inside README/index? It's a proven fact that really small number of users read something besides README/index page. So any `info` block in main pages could maybe attract user's attention. 
(Or maybe contrariwise we want to hide some implementation details by burying this behaviour in the depths of documentation).

What do you think?
guacs: > > @Mityuha sorry about this, but I wasn't clear in my previous review regarding the documentation. I think we would need to explicitly document this behavior somewhere. That is state explicitly the following:
> > 
> > * users may skip providing the `__model__` IF its type hinted in the generics
> > * users can provide just the `__model__` and not provide the type in the generics
> > * if a `__model__` is provided, then that will be used over whatever the value is given as the generic model type
> 
> @guacs OK, not a problem. Where you think is it better to document such behaviour? I would suggest documenting it
> 
> * in the `Usage Guide`/`Declaring Factories` section. Maybe inside like a `info` block
> * optionally in README. Maybe in the `Example` section with the reference to `Declaring Factories` section
> * optionally in the main documentation page with the reference like in README
> 
> Why do I suggest noting this behaviour inside README/index? It's a proven fact that really small number of users read something besides README/index page. So any `info` block in main pages could maybe attract user's attention. (Or maybe contrariwise we want to hide some implementation details by burying this behaviour in the depths of documentation).
> 
> What do you think?

This is a good question. Here's what I'm thinking.

- Keep the example in the `README.md` and the `README/index` as it is now (i.e. with the explicit definition of `__model__` as well as the model being specified as the generic argument. The reason I'm thinking of doing it this way is because I don't think documenting all these other scenarios in the first page itself feels right.
- Document the behavior in `Declaring Factories` in `Usage` section.

Thoughts, @Mityuha? I'd like your opinion as well, @JacobCoffee. 
Mityuha: > Thoughts, @Mityuha? I'd like your opinion as well, @JacobCoffee.

What I do think is that maybe we want to make life easier by omitting explicit `__model__` definition (that's what my issue is about). From this point of view it would IMHO be better to place example _without_ explicit `__model__` definition at least for some reasons:
1. New library users will start using the new simple syntax without any problem. So this syntax raises fewer questions making the library more attractive. (Also documenting this new syntax in README/Index pages encourages using it. And it appears to be what we want to achieve).
2. The new syntax does NOT affect users who already use the library. By the way, if user comes back to documentation, there is a chance that the new syntax will be noticed. And here is pp.3
3. Also, I don't think that anyone will be happy with the transition from implicit `__model__` syntax to explicit `__model__`. But the opposite is true. 
4. Placing the new syntax in the depths of documentation makes these changes (new syntax) pretty useless IMHO. It will be like a sacred knowledge
5. IMHO the implicit `__model__` syntax should have been considered as a target syntax because of its simplicity 

>  I don't think documenting all these other scenarios in the first page itself feels right

Me neither. I didn't offer to document all scenarios in the first page. But! I think the appropriate link to all possible scenarios might be useful. But I'm not sure

guacs: > Also documenting this new syntax in README/Index pages encourages using it

@Mityuha you're right. I think this is the better option. So, I'm thinking this is how we can do it:
- keep the example with the new syntax on the readme etc
- in the `Declaring Factories` section, document the whole thing

> I think the appropriate link to all possible scenarios might be useful

I'm not sure if the link to the docs regarding the `__model__` inference etc, is needed in the README itself. The README is just meant for a very quick introduction to the library.

github-actions[bot]: Documentation preview will be available shortly at https://litestar-org.github.io/polyfactory-docs-preview/452
Mityuha: > > Also documenting this new syntax in README/Index pages encourages using it
> 
> @Mityuha you're right. I think this is the better option. So, I'm thinking this is how we can do it:
> 
> * keep the example with the new syntax on the readme etc
> * in the `Declaring Factories` section, document the whole thing
> 
> > I think the appropriate link to all possible scenarios might be useful
> 
> I'm not sure if the link to the docs regarding the `__model__` inference etc, is needed in the README itself. The README is just meant for a very quick introduction to the library.

@guacs @JacobCoffee Guys, please, check out the new note block in the `Declaring Factories` section. 
Eventually, it turned out quite the way I wanted. I mean regarding explanation structure. But honestly speaking I thought that sphinx tables would look more attractive =)
Anyway, waiting for your comments.
github-actions[bot]: Documentation preview will be available shortly at https://litestar-org.github.io/polyfactory-docs-preview/452

Patch:
diff --git a/README.md b/README.md
index 32d6e06..12e1f63 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ class Person:
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
+    ...
 
 
 def test_is_person() -> None:
diff --git a/docs/PYPI_README.md b/docs/PYPI_README.md
index b9530e7..ac3ce48 100644
--- a/docs/PYPI_README.md
+++ b/docs/PYPI_README.md
@@ -44,7 +44,7 @@ class Person:
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
+    ...
 
 
 def test_is_person() -> None:
diff --git a/docs/usage/declaring_factories.rst b/docs/usage/declaring_factories.rst
index 2bfab8c..a1404c3 100644
--- a/docs/usage/declaring_factories.rst
+++ b/docs/usage/declaring_factories.rst
@@ -8,6 +8,24 @@ base factories.
     :caption: Declaring a factory for a dataclass
     :language: python
 
+You can also specify the model type by only specifying the factory generic type parameter.
+
+.. code-block:: python
+
+    @dataclass
+    class Person:
+        name: str
+        age: float
+        height: float
+        weight: float
+
+    class PersonFactory(DataclassFactory[Person]):
+        ...
+
+.. note::
+   The syntax with the ``__model__`` class attribute omitting
+   is only available since version 2.13.0.
+
 The same applies to the other factories exported by this library, for example:
 
 .. literalinclude:: /examples/declaring_factories/test_example_2.py
diff --git a/polyfactory/factories/base.py b/polyfactory/factories/base.py
index 9febf9b..242b73c 100644
--- a/polyfactory/factories/base.py
+++ b/polyfactory/factories/base.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import typing
 from abc import ABC, abstractmethod
 from collections import Counter, abc, deque
 from contextlib import suppress
@@ -36,6 +35,7 @@ from typing import (
     ClassVar,
     Collection,
     Generic,
+    Iterable,
     Mapping,
     Sequence,
     Type,
@@ -45,7 +45,7 @@ from typing import (
 from uuid import UUID
 
 from faker import Faker
-from typing_extensions import get_args
+from typing_extensions import get_args, get_origin, get_original_bases
 
 from polyfactory.constants import (
     DEFAULT_RANDOM,
@@ -110,7 +110,7 @@ class BaseFactory(ABC, Generic[T]):
     __model__: type[T]
     """
     The model for the factory.
-    This attribute is required for non-base factories and an exception will be raised if its not set.
+    This attribute is required for non-base factories and an exception will be raised if it's not set. Can be automatically inferred from the factory generic argument.
     """
     __check_model__: bool = False
     """
@@ -190,12 +190,13 @@ class BaseFactory(ABC, Generic[T]):
             )
 
         if "__is_base_factory__" not in cls.__dict__ or not cls.__is_base_factory__:
-            model = getattr(cls, "__model__", None)
+            model: type[T] | None = getattr(cls, "__model__", None) or cls._infer_model_type()
             if not model:
                 msg = f"required configuration attribute '__model__' is not set on {cls.__name__}"
                 raise ConfigurationException(
                     msg,
                 )
+            cls.__model__ = model
             if not cls.is_supported_type(model):
                 for factory in BaseFactory._base_factories:
                     if factory.is_supported_type(model):
@@ -219,6 +220,28 @@ class BaseFactory(ABC, Generic[T]):
         if cls.__set_as_default_factory_for_type__:
             BaseFactory._factory_type_mapping[cls.__model__] = cls
 
+    @classmethod
+    def _infer_model_type(cls: type[F]) -> type[T] | None:
+        """Return model type inferred from class declaration.
+        class Foo(ModelFactory[MyModel]):  # <<< MyModel
+            ...
+
+        If more than one base class and/or generic arguments specified return None.
+
+        :returns: Inferred model type or None
+        """
+
+        factory_bases: Iterable[type[BaseFactory[T]]] = (
+            b for b in get_original_bases(cls) if get_origin(b) and issubclass(get_origin(b), BaseFactory)
+        )
+        generic_args: Sequence[type[T]] = [
+            arg for factory_base in factory_bases for arg in get_args(factory_base) if not isinstance(arg, TypeVar)
+        ]
+        if len(generic_args) != 1:
+            return None
+
+        return generic_args[0]
+
     @classmethod
     def _get_sync_persistence(cls) -> SyncPersistenceProtocol[T]:
         """Return a SyncPersistenceHandler if defined for the factory, otherwise raises a ConfigurationException.
@@ -678,7 +701,7 @@ class BaseFactory(ABC, Generic[T]):
         cls,
         field_meta: FieldMeta,
         field_build_parameters: Any | None = None,
-    ) -> typing.Iterable[Any]:
+    ) -> Iterable[Any]:
         """Return a field value on the subclass if existing, otherwise returns a mock value.
 
         :param field_meta: FieldMeta instance.


Test patch:
diff --git a/docs/examples/configuration/test_example_1.py b/docs/examples/configuration/test_example_1.py
index 6eef5da..0136ec9 100644
--- a/docs/examples/configuration/test_example_1.py
+++ b/docs/examples/configuration/test_example_1.py
@@ -12,7 +12,6 @@ class Person:
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
     __random_seed__ = 1
 
     @classmethod
diff --git a/docs/examples/configuration/test_example_2.py b/docs/examples/configuration/test_example_2.py
index 85676ec..38c4533 100644
--- a/docs/examples/configuration/test_example_2.py
+++ b/docs/examples/configuration/test_example_2.py
@@ -13,7 +13,6 @@ class Person:
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
     __random__ = Random(10)
 
     @classmethod
diff --git a/docs/examples/configuration/test_example_3.py b/docs/examples/configuration/test_example_3.py
index 1b55e5b..b5a07ac 100644
--- a/docs/examples/configuration/test_example_3.py
+++ b/docs/examples/configuration/test_example_3.py
@@ -14,7 +14,6 @@ class Person:
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
     __faker__ = Faker(locale="es_ES")
 
     __random_seed__ = 10
diff --git a/docs/examples/configuration/test_example_4.py b/docs/examples/configuration/test_example_4.py
index d52cf17..ac5e578 100644
--- a/docs/examples/configuration/test_example_4.py
+++ b/docs/examples/configuration/test_example_4.py
@@ -50,7 +50,6 @@ class AsyncPersistenceHandler(AsyncPersistenceProtocol[Person]):
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
     __sync_persistence__ = SyncPersistenceHandler
     __async_persistence__ = AsyncPersistenceHandler
 
diff --git a/docs/examples/configuration/test_example_5.py b/docs/examples/configuration/test_example_5.py
index db63d9e..e6b8874 100644
--- a/docs/examples/configuration/test_example_5.py
+++ b/docs/examples/configuration/test_example_5.py
@@ -32,14 +32,13 @@ class Person:
 
 
 class PetFactory(DataclassFactory[Pet]):
-    __model__ = Pet
     __set_as_default_factory_for_type__ = True
 
     name = Use(DataclassFactory.__random__.choice, ["Roxy", "Spammy", "Moshe"])
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
+    ...
 
 
 def test_default_pet_factory() -> None:
diff --git a/docs/examples/configuration/test_example_6.py b/docs/examples/configuration/test_example_6.py
index a84aa60..b962ba0 100644
--- a/docs/examples/configuration/test_example_6.py
+++ b/docs/examples/configuration/test_example_6.py
@@ -10,8 +10,6 @@ class Owner:
 
 
 class OwnerFactory(DataclassFactory[Owner]):
-    __model__ = Owner
-
     __randomize_collection_length__ = True
     __min_collection_length__ = 2
     __max_collection_length__ = 5
diff --git a/docs/examples/configuration/test_example_7.py b/docs/examples/configuration/test_example_7.py
index db521b9..22a29d9 100644
--- a/docs/examples/configuration/test_example_7.py
+++ b/docs/examples/configuration/test_example_7.py
@@ -12,7 +12,6 @@ class Person:
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
     __allow_none_optionals__ = False
 
 
diff --git a/docs/examples/configuration/test_example_8.py b/docs/examples/configuration/test_example_8.py
index 3a440b2..0b607c6 100644
--- a/docs/examples/configuration/test_example_8.py
+++ b/docs/examples/configuration/test_example_8.py
@@ -19,6 +19,5 @@ def test_check_factory_fields() -> None:
     ):
 
         class PersonFactory(DataclassFactory[Person]):
-            __model__ = Person
             __check_model__ = True
             unknown_field = PostGenerated(lambda: "foo")
diff --git a/docs/examples/declaring_factories/test_example_2.py b/docs/examples/declaring_factories/test_example_2.py
index f5ce247..6ebf804 100644
--- a/docs/examples/declaring_factories/test_example_2.py
+++ b/docs/examples/declaring_factories/test_example_2.py
@@ -11,7 +11,7 @@ class Person(TypedDict):
 
 
 class PersonFactory(TypedDictFactory[Person]):
-    __model__ = Person
+    ...
 
 
 def test_is_person() -> None:
diff --git a/docs/examples/declaring_factories/test_example_3.py b/docs/examples/declaring_factories/test_example_3.py
index 1dcb28d..4dafe47 100644
--- a/docs/examples/declaring_factories/test_example_3.py
+++ b/docs/examples/declaring_factories/test_example_3.py
@@ -11,7 +11,7 @@ class Person(BaseModel):
 
 
 class PersonFactory(ModelFactory[Person]):
-    __model__ = Person
+    ...
 
 
 def test_is_person() -> None:
diff --git a/docs/examples/declaring_factories/test_example_4.py b/docs/examples/declaring_factories/test_example_4.py
index f862f71..b192348 100644
--- a/docs/examples/declaring_factories/test_example_4.py
+++ b/docs/examples/declaring_factories/test_example_4.py
@@ -12,7 +12,7 @@ class Person:
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
+    ...
 
 
 def test_is_person() -> None:
diff --git a/docs/examples/declaring_factories/test_example_5.py b/docs/examples/declaring_factories/test_example_5.py
index 24d80ed..f64de77 100644
--- a/docs/examples/declaring_factories/test_example_5.py
+++ b/docs/examples/declaring_factories/test_example_5.py
@@ -31,7 +31,7 @@ class Person:
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
+    ...
 
 
 def test_dynamic_factory_generation() -> None:
diff --git a/docs/examples/declaring_factories/test_example_7.py b/docs/examples/declaring_factories/test_example_7.py
index c48bbe1..d17aee8 100644
--- a/docs/examples/declaring_factories/test_example_7.py
+++ b/docs/examples/declaring_factories/test_example_7.py
@@ -20,7 +20,7 @@ class Person:
 
 
 class PersonFactory(AttrsFactory[Person]):
-    __model__ = Person
+    ...
 
 
 def test_person_factory() -> None:
diff --git a/docs/examples/decorators/test_example_1.py b/docs/examples/decorators/test_example_1.py
index 924498c..b64f90e 100644
--- a/docs/examples/decorators/test_example_1.py
+++ b/docs/examples/decorators/test_example_1.py
@@ -13,8 +13,6 @@ class DatetimeRange:
 
 
 class DatetimeRangeFactory(DataclassFactory[DatetimeRange]):
-    __model__ = DatetimeRange
-
     @post_generated
     @classmethod
     def to_dt(cls, from_dt: datetime) -> datetime:
diff --git a/docs/examples/fields/test_example_1.py b/docs/examples/fields/test_example_1.py
index 9e3ae79..5d8926a 100644
--- a/docs/examples/fields/test_example_1.py
+++ b/docs/examples/fields/test_example_1.py
@@ -34,8 +34,6 @@ pet_instance = Pet(name="Roxy", sound="woof woof", species=Species.DOG)
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
-
     pets = [pet_instance]
 
 
diff --git a/docs/examples/fields/test_example_2.py b/docs/examples/fields/test_example_2.py
index 5763936..d827524 100644
--- a/docs/examples/fields/test_example_2.py
+++ b/docs/examples/fields/test_example_2.py
@@ -32,15 +32,11 @@ class Person:
 
 
 class PetFactory(DataclassFactory[Pet]):
-    __model__ = Pet
-
     name = Use(DataclassFactory.__random__.choice, ["Ralph", "Roxy"])
     species = Use(DataclassFactory.__random__.choice, list(Species))
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
-
     pets = Use(PetFactory.batch, size=2)
 
 
diff --git a/docs/examples/fields/test_example_3.py b/docs/examples/fields/test_example_3.py
index 81a2bc1..b312df4 100644
--- a/docs/examples/fields/test_example_3.py
+++ b/docs/examples/fields/test_example_3.py
@@ -32,15 +32,11 @@ class Person:
 
 
 class PetFactory(DataclassFactory[Pet]):
-    __model__ = Pet
-
     name = lambda: DataclassFactory.__random__.choice(["Ralph", "Roxy"])
     species = lambda: DataclassFactory.__random__.choice(list(Species))
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
-
     pets = Use(PetFactory.batch, size=2)
 
 
diff --git a/docs/examples/fields/test_example_4.py b/docs/examples/fields/test_example_4.py
index 84ed2ae..31ede62 100644
--- a/docs/examples/fields/test_example_4.py
+++ b/docs/examples/fields/test_example_4.py
@@ -32,8 +32,6 @@ class Person:
 
 
 class PetFactory(DataclassFactory[Pet]):
-    __model__ = Pet
-
     @classmethod
     def name(cls) -> str:
         return cls.__random__.choice(["Ralph", "Roxy"])
@@ -44,8 +42,6 @@ class PetFactory(DataclassFactory[Pet]):
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
-
     pets = Use(PetFactory.batch, size=2)
 
 
diff --git a/docs/examples/fields/test_example_5.py b/docs/examples/fields/test_example_5.py
index 4435b9e..fbe9b40 100644
--- a/docs/examples/fields/test_example_5.py
+++ b/docs/examples/fields/test_example_5.py
@@ -10,8 +10,6 @@ class Person(TypedDict):
 
 
 class PersonFactory(TypedDictFactory[Person]):
-    __model__ = Person
-
     id = Ignore()
 
 
diff --git a/docs/examples/fields/test_example_6.py b/docs/examples/fields/test_example_6.py
index 893466b..0c6a5f8 100644
--- a/docs/examples/fields/test_example_6.py
+++ b/docs/examples/fields/test_example_6.py
@@ -13,8 +13,6 @@ class Person(TypedDict):
 
 
 class PersonFactory(TypedDictFactory[Person]):
-    __model__ = Person
-
     id = Require()
 
 
diff --git a/docs/examples/fields/test_example_7.py b/docs/examples/fields/test_example_7.py
index 86debbe..c99f884 100644
--- a/docs/examples/fields/test_example_7.py
+++ b/docs/examples/fields/test_example_7.py
@@ -18,8 +18,6 @@ class DatetimeRange:
 
 
 class DatetimeRangeFactory(DataclassFactory[DatetimeRange]):
-    __model__ = DatetimeRange
-
     to_dt = PostGenerated(add_timedelta)
 
 
diff --git a/docs/examples/fields/test_example_8.py b/docs/examples/fields/test_example_8.py
index d079e6a..b204ade 100644
--- a/docs/examples/fields/test_example_8.py
+++ b/docs/examples/fields/test_example_8.py
@@ -31,14 +31,10 @@ class Person:
 
 
 class PetFactory(DataclassFactory[Pet]):
-    __model__ = Pet
-
     name = lambda: DataclassFactory.__random__.choice(["Ralph", "Roxy"])
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
-
     pet = PetFactory
 
 
diff --git a/docs/examples/fixtures/test_example_1.py b/docs/examples/fixtures/test_example_1.py
index c5f7b51..663493a 100644
--- a/docs/examples/fixtures/test_example_1.py
+++ b/docs/examples/fixtures/test_example_1.py
@@ -19,7 +19,7 @@ class Person:
 
 @register_fixture
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
+    ...
 
 
 def test_person_factory(person_factory: PersonFactory) -> None:
diff --git a/docs/examples/fixtures/test_example_2.py b/docs/examples/fixtures/test_example_2.py
index ee26d81..58864fd 100644
--- a/docs/examples/fixtures/test_example_2.py
+++ b/docs/examples/fixtures/test_example_2.py
@@ -18,7 +18,7 @@ class Person:
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
+    ...
 
 
 person_factory_fixture = register_fixture(PersonFactory)
diff --git a/docs/examples/fixtures/test_example_3.py b/docs/examples/fixtures/test_example_3.py
index f5cd325..bebd96d 100644
--- a/docs/examples/fixtures/test_example_3.py
+++ b/docs/examples/fixtures/test_example_3.py
@@ -18,7 +18,7 @@ class Person:
 
 
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
+    ...
 
 
 person_factory_fixture = register_fixture(PersonFactory, name="aliased_person_factory")
diff --git a/docs/examples/fixtures/test_example_4.py b/docs/examples/fixtures/test_example_4.py
index 431cc08..d6803f2 100644
--- a/docs/examples/fixtures/test_example_4.py
+++ b/docs/examples/fixtures/test_example_4.py
@@ -26,12 +26,10 @@ class ClassRoom:
 
 @register_fixture
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
+    ...
 
 
 class ClassRoomFactory(DataclassFactory[ClassRoom]):
-    __model__ = ClassRoom
-
     teacher = Fixture(PersonFactory, name="Ludmilla Newman")
     pupils = Fixture(PersonFactory, size=20)
 
diff --git a/docs/examples/handling_custom_types/test_example_1.py b/docs/examples/handling_custom_types/test_example_1.py
index 6f21076..5f94ac2 100644
--- a/docs/examples/handling_custom_types/test_example_1.py
+++ b/docs/examples/handling_custom_types/test_example_1.py
@@ -26,8 +26,6 @@ class Person:
 # by default the factory class cannot handle unknown types,
 # so we need to override the provider map to add it:
 class PersonFactory(DataclassFactory[Person]):
-    __model__ = Person
-
     @classmethod
     def get_provider_map(cls) -> Dict[Type, Any]:
         providers_map = super().get_provider_map()
diff --git a/docs/examples/handling_custom_types/test_example_2.py b/docs/examples/handling_custom_types/test_example_2.py
index 9cd954c..5e35fcc 100644
--- a/docs/examples/handling_custom_types/test_example_2.py
+++ b/docs/examples/handling_custom_types/test_example_2.py
@@ -42,7 +42,7 @@ class Person:
 
 # we use our CustomDataclassFactory as a base for the PersonFactory
 class PersonFactory(CustomDataclassFactory[Person]):
-    __model__ = Person
+    ...
 
 
 def test_custom_dataclass_base_factory() -> None:
diff --git a/docs/examples/library_factories/sqlalchemy_factory/test_example_1.py b/docs/examples/library_factories/sqlalchemy_factory/test_example_1.py
index 9b2a304..292c10d 100644
--- a/docs/examples/library_factories/sqlalchemy_factory/test_example_1.py
+++ b/docs/examples/library_factories/sqlalchemy_factory/test_example_1.py
@@ -15,7 +15,7 @@ class Author(Base):
 
 
 class AuthorFactory(SQLAlchemyFactory[Author]):
-    __model__ = Author
+    ...
 
 
 def test_sqla_factory() -> None:
diff --git a/docs/examples/library_factories/sqlalchemy_factory/test_example_2.py b/docs/examples/library_factories/sqlalchemy_factory/test_example_2.py
index 9dc55f0..80a5146 100644
--- a/docs/examples/library_factories/sqlalchemy_factory/test_example_2.py
+++ b/docs/examples/library_factories/sqlalchemy_factory/test_example_2.py
@@ -27,11 +27,10 @@ class Book(Base):
 
 
 class AuthorFactory(SQLAlchemyFactory[Author]):
-    __model__ = Author
+    ...
 
 
 class AuthorFactoryWithRelationship(SQLAlchemyFactory[Author]):
-    __model__ = Author
     __set_relationships__ = True
 
 
diff --git a/docs/examples/library_factories/sqlalchemy_factory/test_example_3.py b/docs/examples/library_factories/sqlalchemy_factory/test_example_3.py
index a958c87..d51985d 100644
--- a/docs/examples/library_factories/sqlalchemy_factory/test_example_3.py
+++ b/docs/examples/library_factories/sqlalchemy_factory/test_example_3.py
@@ -27,7 +27,6 @@ class Book(Base):
 
 
 class AuthorFactory(SQLAlchemyFactory[Author]):
-    __model__ = Author
     __set_relationships__ = True
 
 
diff --git a/docs/examples/model_coverage/test_example_1.py b/docs/examples/model_coverage/test_example_1.py
index ced96f4..a832b6c 100644
--- a/docs/examples/model_coverage/test_example_1.py
+++ b/docs/examples/model_coverage/test_example_1.py
@@ -24,7 +24,7 @@ class Profile:
 
 
 class ProfileFactory(DataclassFactory[Profile]):
-    __model__ = Profile
+    ...
 
 
 def test_profile_coverage() -> None:
diff --git a/docs/examples/model_coverage/test_example_2.py b/docs/examples/model_coverage/test_example_2.py
index bf67f95..cd8787b 100644
--- a/docs/examples/model_coverage/test_example_2.py
+++ b/docs/examples/model_coverage/test_example_2.py
@@ -29,7 +29,7 @@ class SocialGroup:
 
 
 class SocialGroupFactory(DataclassFactory[SocialGroup]):
-    __model__ = SocialGroup
+    ...
 
 
 def test_social_group_coverage() -> None:
diff --git a/tests/test_optional_model_field_inference.py b/tests/test_optional_model_field_inference.py
new file mode 100644
index 0000000..3aeefda
--- /dev/null
+++ b/tests/test_optional_model_field_inference.py
@@ -0,0 +1,161 @@
+from typing import Any, Dict, Generic, Type, TypedDict, TypeVar
+
+import pytest
+from attrs import define
+from msgspec import Struct
+from pydantic import BaseModel
+from pydantic.generics import GenericModel
+from sqlalchemy import Column, Integer
+from sqlalchemy.orm.decl_api import DeclarativeMeta, registry
+
+from polyfactory import ConfigurationException
+from polyfactory.factories import TypedDictFactory
+from polyfactory.factories.attrs_factory import AttrsFactory
+from polyfactory.factories.base import BaseFactory
+from polyfactory.factories.msgspec_factory import MsgspecFactory
+from polyfactory.factories.pydantic_factory import ModelFactory
+from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory
+
+try:
+    from odmantic import Model
+
+    from polyfactory.factories.odmantic_odm_factory import OdmanticModelFactory
+except ImportError:
+    Model, OdmanticModelFactory = None, None  # type: ignore
+
+try:
+    from beanie import Document
+
+    from polyfactory.factories.beanie_odm_factory import BeanieDocumentFactory
+except ImportError:
+    BeanieDocumentFactory = None  # type: ignore
+    Document = None  # type: ignore
+
+
+@define
+class AttrsBase:
+    bool_field: bool
+
+
+class ModelBase(BaseModel):
+    dict_field: Dict[str, int]
+
+
+class MsgspecBase(Struct):
+    int_field: int
+
+
+class Base(metaclass=DeclarativeMeta):
+    __abstract__ = True
+
+    registry = registry()
+
+
+class SQLAlchemyBase(Base):
+    __tablename__ = "model"
+
+    id: Any = Column(Integer(), primary_key=True)
+
+
+class TypedDictBase(TypedDict):
+    name: str
+
+
+@pytest.mark.parametrize(
+    "base_factory, generic_arg",
+    [
+        (AttrsFactory, AttrsBase),
+        (ModelFactory, ModelBase),
+        (MsgspecFactory, MsgspecBase),
+        (SQLAlchemyFactory, SQLAlchemyBase),
+        (TypedDictFactory, TypedDictBase),
+    ],
+)
+def test_modeL_inference_ok(base_factory: Type[BaseFactory], generic_arg: Type[Any]) -> None:
+    class Foo(base_factory[generic_arg]):  # type: ignore
+        ...
+
+    assert getattr(Foo, "__model__") is generic_arg
+
+
+@pytest.mark.skipif(Model is None, reason="Odmantic import error")
+def test_odmantic_model_inference_ok() -> None:
+    class OdmanticModelBase(Model):  # type: ignore
+        name: str
+
+    class Foo(OdmanticModelFactory[OdmanticModelBase]):
+        ...
+
+    assert getattr(Foo, "__model__") is OdmanticModelBase
+
+
+@pytest.mark.skipif(Document is None, reason="Beanie import error")
+def test_beanie_model_inference_ok() -> None:
+    class BeanieBase(Document):
+        name: str
+
+    class Foo(BeanieDocumentFactory[BeanieBase]):
+        ...
+
+    assert getattr(Foo, "__model__") is BeanieBase
+
+
+@pytest.mark.parametrize(
+    "base_factory",
+    [
+        AttrsFactory,
+        ModelFactory,
+        MsgspecFactory,
+        SQLAlchemyFactory,
+        TypedDictFactory,
+    ],
+)
+def test_model_without_generic_type_inference_error(base_factory: Type[BaseFactory]) -> None:
+    with pytest.raises(ConfigurationException):
+
+        class Foo(base_factory):  # type: ignore
+            ...
+
+
+@pytest.mark.parametrize(
+    "base_factory",
+    [
+        AttrsFactory,
+        ModelFactory,
+        MsgspecFactory,
+        SQLAlchemyFactory,
+        TypedDictFactory,
+    ],
+)
+def test_model_type_error(base_factory: Type[BaseFactory]) -> None:
+    with pytest.raises(ConfigurationException):
+
+        class Foo(base_factory[int]):  # type: ignore
+            ...
+
+
+def test_model_multiple_inheritance_cannot_infer_error() -> None:
+    class PFoo(BaseModel):
+        val: int
+
+    class TDFoo(TypedDict):
+        val: str
+
+    with pytest.raises(ConfigurationException):
+
+        class Foo(ModelFactory[PFoo], TypedDictFactory[TDFoo]):  # type: ignore
+            ...
+
+
+def test_generic_model_is_not_an_error() -> None:
+    T = TypeVar("T")
+    P = TypeVar("P")
+
+    class Foo(GenericModel, Generic[T, P]):  # type: ignore[misc]
+        val1: T
+        val2: P
+
+    class FooFactory(ModelFactory[Foo[str, int]]):
+        ...
+
+    assert getattr(FooFactory, "__model__") is Foo[str, int]