diff --git a/docs/examples/configuration/test_example_12.py b/docs/examples/configuration/test_example_12.py new file mode 100644 index 00000000..56cacfaf --- /dev/null +++ b/docs/examples/configuration/test_example_12.py @@ -0,0 +1,27 @@ +from pydantic import AliasPath, BaseModel, Field + +from polyfactory.factories.pydantic_factory import ModelFactory + + +class User(BaseModel): + username: str = Field(..., validation_alias="user_name") + email: str = Field(..., validation_alias=AliasPath("contact", "email")) # type: ignore[pydantic-alias] + + +class UserFactory(ModelFactory[User]): + __by_name__ = True + + # Set factory defaults using field names + username = "john_doe" + + +def test_by_name() -> None: + # Factory uses model_validate with by_name=True + instance = UserFactory.build() + assert instance.username == "john_doe" + assert isinstance(instance.email, str) + + # Can override factory defaults + instance2 = UserFactory.build(username="jane_doe", email="jane@example.com") + assert instance2.username == "jane_doe" + assert instance2.email == "jane@example.com" diff --git a/docs/usage/configuration.rst b/docs/usage/configuration.rst index 9419025f..3c8d18e8 100644 --- a/docs/usage/configuration.rst +++ b/docs/usage/configuration.rst @@ -148,6 +148,24 @@ By default, ``__use_examples__`` is set to ``False.`` :language: python +Validation Aliases (Pydantic >= V2) +------------------------------------ + +If ``__by_name__`` is set to ``True``, then the factory will use ``model_validate()`` with the ``by_name`` parameter +when creating model instances. This is useful when working with models that use validation aliases, such as +``validation_alias`` or ``AliasPath``, as it allows the factory to handle these aliases automatically without +requiring additional model configuration. + +By default, ``__by_name__`` is set to ``False.`` + +.. literalinclude:: /examples/configuration/test_example_12.py + :caption: Validation Aliases with by_name + :language: python + +.. note:: + This feature is only available for Pydantic V2 models. For Pydantic V1 models, this setting has no effect. + + Forward References ------------------ diff --git a/polyfactory/factories/pydantic_factory.py b/polyfactory/factories/pydantic_factory.py index de8fe08c..0a85cf27 100644 --- a/polyfactory/factories/pydantic_factory.py +++ b/polyfactory/factories/pydantic_factory.py @@ -377,6 +377,24 @@ class PaymentFactory(ModelFactory[Payment]): >>> payment Payment(amount=120, currency="EUR") """ + __by_name__: ClassVar[bool] = False + """ + Flag indicating whether to use model_validate with by_name parameter (Pydantic V2 only) + + This helps handle validation aliases automatically without requiring users to modify their model configurations. + + Example code:: + + class MyModel(BaseModel): + field_a: str = Field(..., validation_alias="special_field_a") + + class MyFactory(ModelFactory[MyModel]): + __by_name__ = True + + >>> instance = MyFactory.build(field_a="test") + >>> instance.field_a + "test" + """ if not _IS_PYDANTIC_V1: __forward_references__: ClassVar[dict[str, Any]] = { # Resolve to str to avoid recursive issues @@ -386,6 +404,7 @@ class PaymentFactory(ModelFactory[Payment]): __config_keys__ = ( *BaseFactory.__config_keys__, "__use_examples__", + "__by_name__", ) @classmethod @@ -545,12 +564,19 @@ def _create_model(cls, _build_context: PydanticBuildContext, **kwargs: Any) -> T if _is_pydantic_v1_model(cls.__model__): return cls.__model__.construct(**kwargs) # type: ignore[return-value] return cls.__model__.model_construct(**kwargs) + + # Use model_validate with by_name for Pydantic v2 models when requested + if cls.__by_name__ and _is_pydantic_v2_model(cls.__model__): + return cls.__model__.model_validate(kwargs, by_name=True) # type: ignore[return-value] + return cls.__model__(**kwargs) @classmethod def coverage(cls, factory_use_construct: bool = False, **kwargs: Any) -> abc.Iterator[T]: """Build a batch of the factory's Meta.model with full coverage of the sub-types of the model. + :param factory_use_construct: A boolean that determines whether validations will be made when instantiating the + model. This is supported only for pydantic models. :param kwargs: Any kwargs. If field_meta names are set in kwargs, their values will be used. :returns: A iterator of instances of type T. @@ -559,7 +585,8 @@ def coverage(cls, factory_use_construct: bool = False, **kwargs: Any) -> abc.Ite if "_build_context" not in kwargs: kwargs["_build_context"] = PydanticBuildContext( - seen_models=set(), factory_use_construct=factory_use_construct + seen_models=set(), + factory_use_construct=factory_use_construct, ) for data in cls.process_kwargs_coverage(**kwargs): diff --git a/tests/test_pydantic_factory.py b/tests/test_pydantic_factory.py index e6e2b433..32985d85 100644 --- a/tests/test_pydantic_factory.py +++ b/tests/test_pydantic_factory.py @@ -494,6 +494,78 @@ class MyFactory(ModelFactory): assert instance.aliased_field == "some" +@pytest.mark.skipif(IS_PYDANTIC_V1, reason="pydantic 2 only test") +def test_build_instance_with_by_name_class_variable() -> None: + """Test that __by_name__ class variable enables by_name for model validation.""" + from pydantic import AliasPath + + class MyModel(BaseModel): + field_a: str = Field(..., validation_alias="special_field_a") + field_b: int = Field(..., validation_alias=AliasPath("nested", "field_b")) # type: ignore[pydantic-alias] + + class MyFactory(ModelFactory): + __model__ = MyModel + __by_name__ = True + + # With __by_name__ = True, the factory uses model_validate with by_name + instance = MyFactory.build() + assert isinstance(instance.field_a, str) + assert isinstance(instance.field_b, int) + + # Can pass field names directly when __by_name__ is True + instance2 = MyFactory.build(field_a="test", field_b=42) + assert instance2.field_a == "test" + assert instance2.field_b == 42 + + +@pytest.mark.skipif(IS_PYDANTIC_V1, reason="pydantic 2 only test") +def test_build_instance_with_by_name_and_alias_path() -> None: + """Test that __by_name__ class variable works with AliasPath validation aliases.""" + from pydantic import AliasPath + + class NestedModel(BaseModel): + value: str = Field(..., validation_alias=AliasPath("b", "a")) # type: ignore[pydantic-alias] + + class MyFactory(ModelFactory): + __model__ = NestedModel + __by_name__ = True + + # Build with __by_name__ = True to handle the validation alias correctly + instance = MyFactory.build() + assert isinstance(instance.value, str) + + +@pytest.mark.skipif(IS_PYDANTIC_V1, reason="pydantic 2 only test") +def test_build_instance_with_by_name_and_factory_field_values() -> None: + """Test that __by_name__ class variable works with factory field value overrides.""" + from pydantic import AliasPath + + class MyModel(BaseModel): + field_a: str = Field(..., validation_alias="special_field_a") + field_b: int = Field(..., validation_alias=AliasPath("nested", "field_b")) # type: ignore[pydantic-alias] + field_c: str = Field(default="default_c") + + class MyFactory(ModelFactory): + __model__ = MyModel + __by_name__ = True + + # Set default values on the factory + field_a = "factory_default_a" + field_c = "factory_default_c" + + # Build using factory defaults + instance = MyFactory.build() + assert instance.field_a == "factory_default_a" + assert isinstance(instance.field_b, int) + assert instance.field_c == "factory_default_c" + + # Override factory defaults + instance2 = MyFactory.build(field_a="override_a", field_b=99) + assert instance2.field_a == "override_a" + assert instance2.field_b == 99 + assert instance2.field_c == "factory_default_c" + + def test_build_instance_by_field_name_with_allow_population_by_field_name_flag() -> None: class MyModel(BaseModel): aliased_field: str = Field(..., alias="special_field")