From 97b441faac66e32f1b14e1a9f4e174fac005f9e3 Mon Sep 17 00:00:00 2001 From: Andrew Truong Date: Sat, 15 Nov 2025 12:04:23 +0000 Subject: [PATCH 1/2] feat: support pydantic by_name --- .../examples/configuration/test_example_12.py | 27 +++++++ docs/usage/configuration.rst | 18 +++++ polyfactory/factories/pydantic_factory.py | 32 ++++++++- tests/test_pydantic_factory.py | 72 +++++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 docs/examples/configuration/test_example_12.py 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..2414cb8e 100644 --- a/polyfactory/factories/pydantic_factory.py +++ b/polyfactory/factories/pydantic_factory.py @@ -108,6 +108,7 @@ class PydanticBuildContext(BaseBuildContext): factory_use_construct: bool + by_name: NotRequired[bool] class PydanticConstraints(Constraints): @@ -377,6 +378,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 +405,7 @@ class PaymentFactory(ModelFactory[Payment]): __config_keys__ = ( *BaseFactory.__config_keys__, "__use_examples__", + "__by_name__", ) @classmethod @@ -512,6 +532,7 @@ def build( kwargs["_build_context"] = PydanticBuildContext( seen_models=set(), factory_use_construct=factory_use_construct, + by_name=cls.__by_name__, ) processed_kwargs = cls.process_kwargs(**kwargs) @@ -545,12 +566,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 _build_context.get("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 +587,9 @@ 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, + by_name=cls.__by_name__, ) 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") From 5dde9a87f16a8ebaa13a861287d3a916d2c3d222 Mon Sep 17 00:00:00 2001 From: Andrew Truong Date: Sat, 15 Nov 2025 13:53:01 +0000 Subject: [PATCH 2/2] refactor: remove from build context --- polyfactory/factories/pydantic_factory.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/polyfactory/factories/pydantic_factory.py b/polyfactory/factories/pydantic_factory.py index 2414cb8e..0a85cf27 100644 --- a/polyfactory/factories/pydantic_factory.py +++ b/polyfactory/factories/pydantic_factory.py @@ -108,7 +108,6 @@ class PydanticBuildContext(BaseBuildContext): factory_use_construct: bool - by_name: NotRequired[bool] class PydanticConstraints(Constraints): @@ -532,7 +531,6 @@ def build( kwargs["_build_context"] = PydanticBuildContext( seen_models=set(), factory_use_construct=factory_use_construct, - by_name=cls.__by_name__, ) processed_kwargs = cls.process_kwargs(**kwargs) @@ -568,7 +566,7 @@ def _create_model(cls, _build_context: PydanticBuildContext, **kwargs: Any) -> T return cls.__model__.model_construct(**kwargs) # Use model_validate with by_name for Pydantic v2 models when requested - if _build_context.get("by_name") and _is_pydantic_v2_model(cls.__model__): + 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) @@ -589,7 +587,6 @@ def coverage(cls, factory_use_construct: bool = False, **kwargs: Any) -> abc.Ite kwargs["_build_context"] = PydanticBuildContext( seen_models=set(), factory_use_construct=factory_use_construct, - by_name=cls.__by_name__, ) for data in cls.process_kwargs_coverage(**kwargs):