Repository: litestar-org/polyfactory (python)

Issue:
Bug: Recursive model in a dictionary fails to generate
### Description

I saw that recursive type got introduced recently, they seem to work fine under the tested scenarios, however I had an issue with the code sample below where the recursivity occurs within a dictionary.

### MCVE

```python
from polyfactory.factories.pydantic_factory import ModelFactory
from pydantic import BaseModel
from __future__ import annotations

class Recursive(BaseModel):
    nested: dict[str,Recursive]
factory = ModelFactory.create_factory(Recursive)
instance = factory.build()
```

The code above throws the following error:
```
ValidationError: 1 validation error for Recursive
next.ZMmfLLwXbzrHxjwdxFXt
  Input should be a valid dictionary or instance of Recursive [type=model_type, input_value=<class 'polyfactory.field_meta.Null'>, input_type=type]
    For further information visit https://errors.pydantic.dev/2.9/v/model_type
```

### Release Version

2.16.2

### Platform

- [ ] Linux
- [X] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/polyfactory/issues/585">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/polyfactory/issues/585/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/polyfactory/issues/585/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->


Pull request:
fix: handle recursive collections
<!--
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/)
-->
## Description

-

<!--
Please add in issue numbers this pull request will close, if applicable
Examples: Fixes #4321 or Closes #1234

Ensure you are using a supported keyword to properly link an issue:
https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
-->
## Closes

Fixes #585 


Patch:
diff --git a/polyfactory/value_generators/complex_types.py b/polyfactory/value_generators/complex_types.py
index beef1a3..d828026 100644
--- a/polyfactory/value_generators/complex_types.py
+++ b/polyfactory/value_generators/complex_types.py
@@ -34,7 +34,10 @@ def handle_collection_type(
         container_type = INSTANTIABLE_TYPE_MAPPING[container_type]  # type: ignore[assignment]
 
     container = container_type()
-    if not field_meta.children:
+    if field_meta.children is None or any(
+        child_meta.annotation in factory._get_build_context(build_context)["seen_models"]
+        for child_meta in field_meta.children
+    ):
         return container
 
     if issubclass(container_type, MutableMapping) or is_typeddict(container_type):
diff --git a/polyfactory/value_generators/constrained_collections.py b/polyfactory/value_generators/constrained_collections.py
index 0cbaba1..580b7d1 100644
--- a/polyfactory/value_generators/constrained_collections.py
+++ b/polyfactory/value_generators/constrained_collections.py
@@ -1,18 +1,18 @@
 from __future__ import annotations
 
 from enum import EnumMeta
-from typing import TYPE_CHECKING, Any, Callable, List, Literal, Mapping, TypeVar, cast
+from typing import TYPE_CHECKING, Any, Callable, Literal, Mapping, TypeVar
 
 from polyfactory.exceptions import ParameterException
-from polyfactory.field_meta import FieldMeta
 
 if TYPE_CHECKING:
     from polyfactory.factories.base import BaseFactory, BuildContext
+    from polyfactory.field_meta import FieldMeta
 
 T = TypeVar("T", list, set, frozenset)
 
 
-def handle_constrained_collection(
+def handle_constrained_collection(  # noqa: C901
     collection_type: Callable[..., T],
     factory: type[BaseFactory[Any]],
     field_meta: FieldMeta,
@@ -37,6 +37,10 @@ def handle_constrained_collection(
 
     :returns: A collection value.
     """
+    build_context = factory._get_build_context(build_context)
+    if field_meta.annotation in build_context["seen_models"]:
+        return collection_type()
+
     min_items = abs(min_items if min_items is not None else (max_items or 0))
     max_items = abs(max_items if max_items is not None else min_items + 1)
 
@@ -99,6 +103,12 @@ def handle_constrained_mapping(
 
     :returns: A mapping instance.
     """
+    build_context = factory._get_build_context(build_context)
+    if field_meta.children is None or any(
+        child_meta.annotation in build_context["seen_models"] for child_meta in field_meta.children
+    ):
+        return {}
+
     min_items = abs(min_items if min_items is not None else (max_items or 0))
     max_items = abs(max_items if max_items is not None else min_items + 1)
 
@@ -110,7 +120,7 @@ def handle_constrained_mapping(
 
     collection: dict[Any, Any] = {}
 
-    children = cast(List[FieldMeta], field_meta.children)
+    children = field_meta.children
     key_field_meta = children[0]
     value_field_meta = children[1]
     while len(collection) < length:


Test patch:
diff --git a/tests/test_recursive_models.py b/tests/test_recursive_models.py
index ac4c1c3..0b2a2f9 100644
--- a/tests/test_recursive_models.py
+++ b/tests/test_recursive_models.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 from dataclasses import dataclass, field
-from typing import List, Optional, Union
+from typing import Any, Dict, List, Optional, Union
 
 import pytest
 
@@ -47,6 +47,8 @@ class PydanticNode(BaseModel):
     optional_union_child: Union[PydanticNode, None]  # noqa: UP007
     optional_child: Optional[PydanticNode]  # noqa: UP007
     child: PydanticNode = Field(default=_Sentinel)  # type: ignore[assignment]
+    recursive_key: Dict[PydanticNode, Any]  # noqa: UP006
+    recursive_value: Dict[str, PydanticNode]  # noqa: UP006
 
 
 @pytest.mark.parametrize("factory_use_construct", (True, False))
@@ -59,6 +61,8 @@ def test_recursive_pydantic_models(factory_use_construct: bool) -> None:
     assert result.optional_union_child is None
     assert result.optional_child is None
     assert result.list_child == []
+    assert result.recursive_key == {}
+    assert result.recursive_value == {}
 
 
 @dataclass