Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/examples/configuration/test_example_12.py
Original file line number Diff line number Diff line change
@@ -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"
18 changes: 18 additions & 0 deletions docs/usage/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------

Expand Down
29 changes: 28 additions & 1 deletion polyfactory/factories/pydantic_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -386,6 +404,7 @@ class PaymentFactory(ModelFactory[Payment]):
__config_keys__ = (
*BaseFactory.__config_keys__,
"__use_examples__",
"__by_name__",
)

@classmethod
Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand Down
72 changes: 72 additions & 0 deletions tests/test_pydantic_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading