Skip to content

Commit cea601f

Browse files
committed
feat: support pydantic by_name
1 parent 6ab574a commit cea601f

File tree

4 files changed

+148
-1
lines changed

4 files changed

+148
-1
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from pydantic import AliasPath, BaseModel, Field
2+
3+
from polyfactory.factories.pydantic_factory import ModelFactory
4+
5+
6+
class User(BaseModel):
7+
username: str = Field(..., validation_alias="user_name")
8+
email: str = Field(..., validation_alias=AliasPath("contact", "email")) # type: ignore[pydantic-alias]
9+
10+
11+
class UserFactory(ModelFactory[User]):
12+
__by_name__ = True
13+
14+
# Set factory defaults using field names
15+
username = "john_doe"
16+
17+
18+
def test_by_name() -> None:
19+
# Factory uses model_validate with by_name=True
20+
instance = UserFactory.build()
21+
assert instance.username == "john_doe"
22+
assert isinstance(instance.email, str)
23+
24+
# Can override factory defaults
25+
instance2 = UserFactory.build(username="jane_doe", email="[email protected]")
26+
assert instance2.username == "jane_doe"
27+
assert instance2.email == "[email protected]"

docs/usage/configuration.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,24 @@ By default, ``__use_examples__`` is set to ``False.``
148148
:language: python
149149

150150

151+
Validation Aliases (Pydantic >= V2)
152+
------------------------------------
153+
154+
If ``__by_name__`` is set to ``True``, then the factory will use ``model_validate()`` with the ``by_name`` parameter
155+
when creating model instances. This is useful when working with models that use validation aliases, such as
156+
``validation_alias`` or ``AliasPath``, as it allows the factory to handle these aliases automatically without
157+
requiring additional model configuration.
158+
159+
By default, ``__by_name__`` is set to ``False.``
160+
161+
.. literalinclude:: /examples/configuration/test_example_12.py
162+
:caption: Validation Aliases with by_name
163+
:language: python
164+
165+
.. note::
166+
This feature is only available for Pydantic V2 models. For Pydantic V1 models, this setting has no effect.
167+
168+
151169
Forward References
152170
------------------
153171

polyfactory/factories/pydantic_factory.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105

106106
class PydanticBuildContext(BaseBuildContext):
107107
factory_use_construct: bool
108+
by_name: NotRequired[bool]
108109

109110

110111
class PydanticConstraints(Constraints):
@@ -370,6 +371,24 @@ class PaymentFactory(ModelFactory[Payment]):
370371
>>> payment
371372
Payment(amount=120, currency="EUR")
372373
"""
374+
__by_name__: ClassVar[bool] = False
375+
"""
376+
Flag indicating whether to use model_validate with by_name parameter (Pydantic V2 only)
377+
378+
This helps handle validation aliases automatically without requiring users to modify their model configurations.
379+
380+
Example code::
381+
382+
class MyModel(BaseModel):
383+
field_a: str = Field(..., validation_alias="special_field_a")
384+
385+
class MyFactory(ModelFactory[MyModel]):
386+
__by_name__ = True
387+
388+
>>> instance = MyFactory.build(field_a="test")
389+
>>> instance.field_a
390+
"test"
391+
"""
373392
if not _IS_PYDANTIC_V1:
374393
__forward_references__: ClassVar[dict[str, Any]] = {
375394
# Resolve to str to avoid recursive issues
@@ -379,6 +398,7 @@ class PaymentFactory(ModelFactory[Payment]):
379398
__config_keys__ = (
380399
*BaseFactory.__config_keys__,
381400
"__use_examples__",
401+
"__by_name__",
382402
)
383403

384404
@classmethod
@@ -505,6 +525,7 @@ def build(
505525
kwargs["_build_context"] = PydanticBuildContext(
506526
seen_models=set(),
507527
factory_use_construct=factory_use_construct,
528+
by_name=cls.__by_name__,
508529
)
509530

510531
processed_kwargs = cls.process_kwargs(**kwargs)
@@ -538,12 +559,19 @@ def _create_model(cls, _build_context: PydanticBuildContext, **kwargs: Any) -> T
538559
if _is_pydantic_v1_model(cls.__model__):
539560
return cls.__model__.construct(**kwargs) # type: ignore[return-value]
540561
return cls.__model__.model_construct(**kwargs)
562+
563+
# Use model_validate with by_name for Pydantic v2 models when requested
564+
if _build_context.get("by_name") and _is_pydantic_v2_model(cls.__model__):
565+
return cls.__model__.model_validate(kwargs, by_name=True) # type: ignore[return-value]
566+
541567
return cls.__model__(**kwargs)
542568

543569
@classmethod
544570
def coverage(cls, factory_use_construct: bool = False, **kwargs: Any) -> abc.Iterator[T]:
545571
"""Build a batch of the factory's Meta.model with full coverage of the sub-types of the model.
546572
573+
:param factory_use_construct: A boolean that determines whether validations will be made when instantiating the
574+
model. This is supported only for pydantic models.
547575
:param kwargs: Any kwargs. If field_meta names are set in kwargs, their values will be used.
548576
549577
:returns: A iterator of instances of type T.
@@ -552,7 +580,9 @@ def coverage(cls, factory_use_construct: bool = False, **kwargs: Any) -> abc.Ite
552580

553581
if "_build_context" not in kwargs:
554582
kwargs["_build_context"] = PydanticBuildContext(
555-
seen_models=set(), factory_use_construct=factory_use_construct
583+
seen_models=set(),
584+
factory_use_construct=factory_use_construct,
585+
by_name=cls.__by_name__,
556586
)
557587

558588
for data in cls.process_kwargs_coverage(**kwargs):

tests/test_pydantic_factory.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,78 @@ class MyFactory(ModelFactory):
494494
assert instance.aliased_field == "some"
495495

496496

497+
@pytest.mark.skipif(IS_PYDANTIC_V1, reason="pydantic 2 only test")
498+
def test_build_instance_with_by_name_class_variable() -> None:
499+
"""Test that __by_name__ class variable enables by_name for model validation."""
500+
from pydantic import AliasPath
501+
502+
class MyModel(BaseModel):
503+
field_a: str = Field(..., validation_alias="special_field_a")
504+
field_b: int = Field(..., validation_alias=AliasPath("nested", "field_b")) # type: ignore[pydantic-alias]
505+
506+
class MyFactory(ModelFactory):
507+
__model__ = MyModel
508+
__by_name__ = True
509+
510+
# With __by_name__ = True, the factory uses model_validate with by_name
511+
instance = MyFactory.build()
512+
assert isinstance(instance.field_a, str)
513+
assert isinstance(instance.field_b, int)
514+
515+
# Can pass field names directly when __by_name__ is True
516+
instance2 = MyFactory.build(field_a="test", field_b=42)
517+
assert instance2.field_a == "test"
518+
assert instance2.field_b == 42
519+
520+
521+
@pytest.mark.skipif(IS_PYDANTIC_V1, reason="pydantic 2 only test")
522+
def test_build_instance_with_by_name_and_alias_path() -> None:
523+
"""Test that __by_name__ class variable works with AliasPath validation aliases."""
524+
from pydantic import AliasPath
525+
526+
class NestedModel(BaseModel):
527+
value: str = Field(..., validation_alias=AliasPath("b", "a")) # type: ignore[pydantic-alias]
528+
529+
class MyFactory(ModelFactory):
530+
__model__ = NestedModel
531+
__by_name__ = True
532+
533+
# Build with __by_name__ = True to handle the validation alias correctly
534+
instance = MyFactory.build()
535+
assert isinstance(instance.value, str)
536+
537+
538+
@pytest.mark.skipif(IS_PYDANTIC_V1, reason="pydantic 2 only test")
539+
def test_build_instance_with_by_name_and_factory_field_values() -> None:
540+
"""Test that __by_name__ class variable works with factory field value overrides."""
541+
from pydantic import AliasPath
542+
543+
class MyModel(BaseModel):
544+
field_a: str = Field(..., validation_alias="special_field_a")
545+
field_b: int = Field(..., validation_alias=AliasPath("nested", "field_b")) # type: ignore[pydantic-alias]
546+
field_c: str = Field(default="default_c")
547+
548+
class MyFactory(ModelFactory):
549+
__model__ = MyModel
550+
__by_name__ = True
551+
552+
# Set default values on the factory
553+
field_a = "factory_default_a"
554+
field_c = "factory_default_c"
555+
556+
# Build using factory defaults
557+
instance = MyFactory.build()
558+
assert instance.field_a == "factory_default_a"
559+
assert isinstance(instance.field_b, int)
560+
assert instance.field_c == "factory_default_c"
561+
562+
# Override factory defaults
563+
instance2 = MyFactory.build(field_a="override_a", field_b=99)
564+
assert instance2.field_a == "override_a"
565+
assert instance2.field_b == 99
566+
assert instance2.field_c == "factory_default_c"
567+
568+
497569
def test_build_instance_by_field_name_with_allow_population_by_field_name_flag() -> None:
498570
class MyModel(BaseModel):
499571
aliased_field: str = Field(..., alias="special_field")

0 commit comments

Comments
 (0)