Repository: litestar-org/polyfactory
Pull request #564 (2024-07-08 07:10:21)

fix: Nested model in collection build context
## Description

`ModelFactory._create_model` not handling _build_context = None


#563



github-actions[bot]: Documentation preview will be available shortly at https://litestar-org.github.io/polyfactory-docs-preview/564
github-actions[bot]: Documentation preview will be available shortly at https://litestar-org.github.io/polyfactory-docs-preview/564

Patch:
diff --git a/polyfactory/factories/base.py b/polyfactory/factories/base.py
index c8ef09c..6860b67 100644
--- a/polyfactory/factories/base.py
+++ b/polyfactory/factories/base.py
@@ -845,7 +845,7 @@ class BaseFactory(ABC, Generic[T]):
                     field_meta,
                 )
 
-                yield handle_collection_type_coverage(child_meta, origin, cls)
+                yield handle_collection_type_coverage(child_meta, origin, cls, build_context=build_context)
 
             elif is_any(unwrapped_annotation) or isinstance(unwrapped_annotation, TypeVar):
                 yield create_random_string(cls.__random__, min_length=1, max_length=10)
diff --git a/polyfactory/factories/pydantic_factory.py b/polyfactory/factories/pydantic_factory.py
index 5e287fc..d773ee9 100644
--- a/polyfactory/factories/pydantic_factory.py
+++ b/polyfactory/factories/pydantic_factory.py
@@ -501,7 +501,7 @@ class ModelFactory(Generic[T], BaseFactory[T]):
         :returns: An instance of type T.
 
         """
-        if _build_context.get("factory_use_construct"):
+        if cls._get_build_context(_build_context).get("factory_use_construct"):
             if _is_pydantic_v1_model(cls.__model__):
                 return cls.__model__.construct(**kwargs)  # type: ignore[return-value]
             return cls.__model__.model_construct(**kwargs)  # type: ignore[return-value]
diff --git a/polyfactory/value_generators/complex_types.py b/polyfactory/value_generators/complex_types.py
index 2ef2e63..c68bc3c 100644
--- a/polyfactory/value_generators/complex_types.py
+++ b/polyfactory/value_generators/complex_types.py
@@ -92,6 +92,7 @@ def handle_collection_type_coverage(
     field_meta: FieldMeta,
     container_type: type,
     factory: type[BaseFactory[Any]],
+    build_context: BuildContext | None = None,
 ) -> Any:
     """Handle coverage generation of container types recursively.
 
@@ -110,31 +111,34 @@ def handle_collection_type_coverage(
             Iterable[Tuple[FieldMeta, FieldMeta]],
             zip(field_meta.children[::2], field_meta.children[1::2]),
         ):
-            key = CoverageContainer(factory.get_field_value_coverage(key_field_meta))
-            value = CoverageContainer(factory.get_field_value_coverage(value_field_meta))
+            key = CoverageContainer(factory.get_field_value_coverage(key_field_meta, build_context=build_context))
+            value = CoverageContainer(factory.get_field_value_coverage(value_field_meta, build_context=build_context))
             container[key] = value
         return container
 
     if issubclass(container_type, MutableSequence):
         container_instance = container_type()
         for subfield_meta in field_meta.children:
-            container_instance.extend(factory.get_field_value_coverage(subfield_meta))
+            container_instance.extend(factory.get_field_value_coverage(subfield_meta, build_context=build_context))
 
         return container_instance
 
     if issubclass(container_type, Set):
         set_instance = container_type()
         for subfield_meta in field_meta.children:
-            set_instance = set_instance.union(factory.get_field_value_coverage(subfield_meta))
+            set_instance = set_instance.union(
+                factory.get_field_value_coverage(subfield_meta, build_context=build_context)
+            )
 
         return set_instance
 
     if issubclass(container_type, AbstractSet):
-        return container.union(handle_collection_type_coverage(field_meta, set, factory))
+        return container.union(handle_collection_type_coverage(field_meta, set, factory, build_context=build_context))
 
     if issubclass(container_type, tuple):
         return container_type(
-            CoverageContainer(factory.get_field_value_coverage(subfield_meta)) for subfield_meta in field_meta.children
+            CoverageContainer(factory.get_field_value_coverage(subfield_meta, build_context=build_context))
+            for subfield_meta in field_meta.children
         )
 
     msg = f"Unsupported container type: {container_type}"


Test patch:
diff --git a/tests/test_pydantic_factory.py b/tests/test_pydantic_factory.py
index 9f35430..bc668da 100644
--- a/tests/test_pydantic_factory.py
+++ b/tests/test_pydantic_factory.py
@@ -207,6 +207,45 @@ def test_use_default_with_non_callable_default() -> None:
     assert foo.default_field == 10
 
 
+def test_factory_nested_model_collection_coverage() -> None:
+    class Nested(BaseModel):
+        foo: int
+
+    class CollectionModel(BaseModel):
+        collection: List[Nested]
+
+    class CollectionModelFactory(ModelFactory[CollectionModel]):
+        __model__ = CollectionModel
+
+    instances = list(CollectionModelFactory.coverage())
+    assert len(instances) == 1
+    instance = instances[0]
+    assert len(instance.collection) == 1
+    assert isinstance(instance.collection[0], Nested)
+
+
+def test_factory_nested_model_collection_construct_coverage() -> None:
+    class Nested(BaseModel):
+        foo: int
+
+        @validator("foo")
+        @classmethod
+        def always_invalid(cls, v: int) -> None:
+            raise ValueError("invalid by validator")
+
+    class CollectionModel(BaseModel):
+        collection: List[Nested]
+
+    class CollectionModelFactory(ModelFactory[CollectionModel]):
+        __model__ = CollectionModel
+
+    instances = list(CollectionModelFactory.coverage(factory_use_construct=True))
+    assert len(instances) == 1
+    instance = instances[0]
+    assert len(instance.collection) == 1
+    assert isinstance(instance.collection[0], Nested)
+
+
 def test_factory_use_construct() -> None:
     # factory should pass values without validation
     invalid_age = "non_valid_age"