Skip to content

Commit 36395d9

Browse files
stardust85Michel Samia
andauthored
feat: Issue 678 use pydantic field examples (#679)
Co-authored-by: Michel Samia <[email protected]>
1 parent 18d8579 commit 36395d9

File tree

9 files changed

+169
-32
lines changed

9 files changed

+169
-32
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from pydantic import BaseModel, Field
2+
3+
from polyfactory.factories.pydantic_factory import ModelFactory
4+
5+
6+
class Payment(BaseModel):
7+
amount: int = Field(0)
8+
currency: str = Field(examples=["USD", "EUR", "INR"])
9+
10+
11+
class PaymentFactory(ModelFactory[Payment]):
12+
__use_examples__ = True
13+
14+
15+
def test_use_examples() -> None:
16+
instance = PaymentFactory.build()
17+
assert instance.currency in ["USD", "EUR", "INR"]

docs/usage/configuration.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,16 @@ By default, ``__use_defaults__`` is set to ``False.`` If you need more fine grai
130130
.. literalinclude:: /examples/configuration/test_example_9.py
131131
:caption: Use Default Values
132132
:language: python
133+
134+
135+
Use Example Values (Pydantic >= V2)
136+
-----------------------------------
137+
138+
If ``__use_examples__`` is set to ``True``, then a random value from examples attribute will be used for a given field,
139+
provided there's a non-empty list of examples defined for that field.
140+
141+
By default, ``__use_examples__`` is set to ``False.``
142+
143+
.. literalinclude:: /examples/configuration/test_example_10.py
144+
:caption: Use Examples Values
145+
:language: python

polyfactory/factories/pydantic_factory.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def __init__(
124124
default: Any = ...,
125125
children: list[FieldMeta] | None = None,
126126
constraints: PydanticConstraints | None = None,
127+
examples: list[Any] | None = None,
127128
) -> None:
128129
super().__init__(
129130
name=name,
@@ -133,6 +134,7 @@ def __init__(
133134
children=children,
134135
constraints=constraints,
135136
)
137+
self.examples = examples
136138

137139
@classmethod
138140
def from_field_info(
@@ -212,13 +214,15 @@ def from_field_info(
212214
if is_json:
213215
constraints["json"] = True
214216

215-
return PydanticFieldMeta.from_type(
217+
result = PydanticFieldMeta.from_type(
216218
annotation=annotation,
217219
children=children,
218220
constraints=cast("Constraints", {k: v for k, v in constraints.items() if v is not None}) or None,
219221
default=default_value,
220222
name=name,
221223
)
224+
result.examples = field_info.examples
225+
return result
222226

223227
@classmethod
224228
def from_model_field( # pragma: no cover
@@ -336,12 +340,15 @@ def from_model_field( # pragma: no cover
336340
for arg in fields_to_iterate
337341
)
338342

343+
examples = None
344+
339345
return PydanticFieldMeta(
340346
name=name,
341347
annotation=annotation, # pyright: ignore[reportArgumentType]
342348
children=children or None,
343349
default=default_value,
344350
constraints=cast("PydanticConstraints", {k: v for k, v in constraints.items() if v is not None}) or None,
351+
examples=examples,
345352
)
346353

347354
if not _IS_PYDANTIC_V1:
@@ -363,6 +370,28 @@ class ModelFactory(Generic[T], BaseFactory[T]):
363370

364371
__forward_ref_resolution_type_mapping__: ClassVar[Mapping[str, type]] = {}
365372
__is_base_factory__ = True
373+
__use_examples__: ClassVar[bool] = False # for backwards compatibility
374+
"""
375+
Flag indicating whether to use a random example, if provided (Pydantic >=V2)
376+
377+
Example code::
378+
379+
class Payment(BaseModel):
380+
amount: int = Field(0)
381+
currency: str = Field(examples=['USD', 'EUR', 'INR'])
382+
383+
class PaymentFactory(ModelFactory[Payment]):
384+
__use_examples__ = True
385+
386+
>>> payment = PaymentFactory.build()
387+
>>> payment
388+
Payment(amount=120, currency="EUR")
389+
"""
390+
391+
__config_keys__ = (
392+
*BaseFactory.__config_keys__,
393+
"__use_examples__",
394+
)
366395

367396
def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
368397
super().__init_subclass__(*args, **kwargs)
@@ -425,6 +454,7 @@ def get_constrained_field_value(
425454
build_context: BuildContext | None = None,
426455
) -> Any:
427456
constraints = cast("PydanticConstraints", field_meta.constraints)
457+
428458
if constraints.pop("json", None):
429459
value = cls.get_field_value(
430460
field_meta, field_build_parameters=field_build_parameters, build_context=build_context
@@ -435,6 +465,34 @@ def get_constrained_field_value(
435465
annotation, field_meta, field_build_parameters=field_build_parameters, build_context=build_context
436466
)
437467

468+
@classmethod
469+
def get_field_value(
470+
cls,
471+
field_meta: FieldMeta,
472+
field_build_parameters: Any | None = None,
473+
build_context: BuildContext | None = None,
474+
) -> Any:
475+
"""Return a value from examples if exists, else random value.
476+
477+
:param field_meta: FieldMeta instance.
478+
:param field_build_parameters: Any build parameters passed to the factory as kwarg values.
479+
:param build_context: BuildContext data for current build.
480+
481+
:returns: An arbitrary value.
482+
483+
"""
484+
result: Any
485+
486+
field_meta = cast("PydanticFieldMeta", field_meta)
487+
488+
if cls.__use_examples__ and field_meta.examples:
489+
result = cls.__random__.choice(field_meta.examples)
490+
else:
491+
result = super().get_field_value(
492+
field_meta=field_meta, field_build_parameters=field_build_parameters, build_context=build_context
493+
)
494+
return result
495+
438496
@classmethod
439497
def build(
440498
cls,

tests/constraints/test_decimal_constraints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ def test_handle_decimal_length() -> None:
392392

393393
# here decimal places should determine max length
394394
max_digits = 10
395-
decimal_places = 5
395+
decimal_places: int | None = 5
396396

397397
result = handle_decimal_length(decimal, decimal_places, max_digits)
398398
assert isinstance(result, Decimal)

tests/constraints/test_frozen_set_constraints.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
from hypothesis.strategies import integers
77

88
from polyfactory.exceptions import ParameterException
9-
from polyfactory.factories.pydantic_factory import ModelFactory
10-
from polyfactory.field_meta import FieldMeta
9+
from polyfactory.factories.pydantic_factory import ModelFactory, PydanticFieldMeta
1110
from polyfactory.value_generators.constrained_collections import (
1211
handle_constrained_collection,
1312
)
@@ -22,7 +21,7 @@ def test_handle_constrained_set_with_min_items_and_max_items(min_items: int, max
2221
result = handle_constrained_collection(
2322
collection_type=frozenset,
2423
factory=ModelFactory,
25-
field_meta=FieldMeta(name="test", annotation=frozenset),
24+
field_meta=PydanticFieldMeta(name="test", annotation=frozenset),
2625
item_type=str,
2726
max_items=max_items,
2827
min_items=min_items,
@@ -34,7 +33,7 @@ def test_handle_constrained_set_with_min_items_and_max_items(min_items: int, max
3433
handle_constrained_collection(
3534
collection_type=frozenset,
3635
factory=ModelFactory,
37-
field_meta=FieldMeta(name="test", annotation=frozenset),
36+
field_meta=PydanticFieldMeta(name="test", annotation=frozenset),
3837
item_type=str,
3938
max_items=max_items,
4039
min_items=min_items,
@@ -50,7 +49,7 @@ def test_handle_constrained_set_with_max_items(
5049
result = handle_constrained_collection(
5150
collection_type=frozenset,
5251
factory=ModelFactory,
53-
field_meta=FieldMeta(name="test", annotation=frozenset),
52+
field_meta=PydanticFieldMeta(name="test", annotation=frozenset),
5453
item_type=str,
5554
max_items=max_items,
5655
)
@@ -66,7 +65,7 @@ def test_handle_constrained_set_with_min_items(
6665
result = handle_constrained_collection(
6766
collection_type=frozenset,
6867
factory=ModelFactory,
69-
field_meta=FieldMeta(name="test", annotation=frozenset),
68+
field_meta=PydanticFieldMeta(name="test", annotation=frozenset),
7069
item_type=str,
7170
min_items=min_items,
7271
)
@@ -79,7 +78,7 @@ def test_handle_constrained_set_with_different_types(t_type: Any) -> None:
7978
result = handle_constrained_collection(
8079
collection_type=frozenset,
8180
factory=ModelFactory,
82-
field_meta=FieldMeta(name="test", annotation=frozenset),
81+
field_meta=PydanticFieldMeta(name="test", annotation=frozenset),
8382
item_type=t_type,
8483
)
8584
assert len(result) >= 0

tests/constraints/test_list_constraints.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
from pydantic import VERSION
99

1010
from polyfactory.exceptions import ParameterException
11-
from polyfactory.factories.pydantic_factory import ModelFactory
12-
from polyfactory.field_meta import FieldMeta
11+
from polyfactory.factories.pydantic_factory import ModelFactory, PydanticFieldMeta
1312
from polyfactory.value_generators.constrained_collections import (
1413
handle_constrained_collection,
1514
)
@@ -24,7 +23,7 @@ def test_handle_constrained_list_with_min_items_and_max_items(min_items: int, ma
2423
result = handle_constrained_collection(
2524
collection_type=list,
2625
factory=ModelFactory,
27-
field_meta=FieldMeta(name="test", annotation=list),
26+
field_meta=PydanticFieldMeta(name="test", annotation=list),
2827
item_type=str,
2928
max_items=max_items,
3029
min_items=min_items,
@@ -36,7 +35,7 @@ def test_handle_constrained_list_with_min_items_and_max_items(min_items: int, ma
3635
handle_constrained_collection(
3736
collection_type=list,
3837
factory=ModelFactory,
39-
field_meta=FieldMeta(name="test", annotation=list),
38+
field_meta=PydanticFieldMeta(name="test", annotation=list),
4039
item_type=str,
4140
max_items=max_items,
4241
min_items=min_items,
@@ -52,7 +51,7 @@ def test_handle_constrained_list_with_max_items(
5251
result = handle_constrained_collection(
5352
collection_type=list,
5453
factory=ModelFactory,
55-
field_meta=FieldMeta(name="test", annotation=list),
54+
field_meta=PydanticFieldMeta(name="test", annotation=list),
5655
item_type=str,
5756
max_items=max_items,
5857
)
@@ -68,7 +67,7 @@ def test_handle_constrained_list_with_min_items(
6867
result = handle_constrained_collection(
6968
collection_type=list,
7069
factory=ModelFactory,
71-
field_meta=FieldMeta.from_type(List[str], name="test"),
70+
field_meta=PydanticFieldMeta.from_type(List[str], name="test"),
7271
item_type=str,
7372
min_items=min_items,
7473
)
@@ -81,7 +80,7 @@ def test_handle_constrained_list_with_min_items(
8180
)
8281
@pytest.mark.parametrize("t_type", tuple(ModelFactory.get_provider_map()))
8382
def test_handle_constrained_list_with_different_types(t_type: Any) -> None:
84-
field_meta = FieldMeta.from_type(List[t_type], name="test")
83+
field_meta = PydanticFieldMeta.from_type(List[t_type], name="test")
8584
result = handle_constrained_collection(
8685
collection_type=list,
8786
factory=ModelFactory,
@@ -92,7 +91,7 @@ def test_handle_constrained_list_with_different_types(t_type: Any) -> None:
9291

9392

9493
def test_handle_unique_items() -> None:
95-
field_meta = FieldMeta.from_type(List[str], name="test", constraints={"unique_items": True})
94+
field_meta = PydanticFieldMeta.from_type(List[str], name="test", constraints={"unique_items": True})
9695
result = handle_constrained_collection(
9796
collection_type=list,
9897
factory=ModelFactory,

tests/constraints/test_mapping_constraints.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
from hypothesis.strategies import integers
44

55
from polyfactory.exceptions import ParameterException
6-
from polyfactory.factories.pydantic_factory import ModelFactory
7-
from polyfactory.field_meta import FieldMeta
6+
from polyfactory.factories.pydantic_factory import ModelFactory, PydanticFieldMeta
87
from polyfactory.value_generators.constrained_collections import (
98
handle_constrained_mapping,
109
)
@@ -15,9 +14,9 @@
1514
integers(min_value=0, max_value=10),
1615
)
1716
def test_handle_constrained_mapping_with_min_items_and_max_items(min_items: int, max_items: int) -> None:
18-
key_field = FieldMeta(name="key", annotation=str)
19-
value_field = FieldMeta(name="value", annotation=int)
20-
field_meta = FieldMeta(name="test", annotation=dict, children=[key_field, value_field])
17+
key_field = PydanticFieldMeta(name="key", annotation=str)
18+
value_field = PydanticFieldMeta(name="value", annotation=int)
19+
field_meta = PydanticFieldMeta(name="test", annotation=dict, children=[key_field, value_field])
2120

2221
if max_items >= min_items:
2322
result = handle_constrained_mapping(
@@ -47,9 +46,9 @@ def test_handle_constrained_mapping_with_constrained_key_and_value() -> None:
4746
min_length = 5
4847
max_length = 10
4948

50-
key_field = FieldMeta(name="key", annotation=str, constraints={"min_length": key_min_length})
51-
value_field = FieldMeta(name="value", annotation=int, constraints={"gt": value_gt})
52-
field_meta = FieldMeta(name="test", annotation=dict, children=[key_field, value_field])
49+
key_field = PydanticFieldMeta(name="key", annotation=str, constraints={"min_length": key_min_length})
50+
value_field = PydanticFieldMeta(name="value", annotation=int, constraints={"gt": value_gt})
51+
field_meta = PydanticFieldMeta(name="test", annotation=dict, children=[key_field, value_field])
5352

5453
result = handle_constrained_mapping(
5554
factory=ModelFactory,

tests/constraints/test_set_constraints.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
from hypothesis.strategies import integers
77

88
from polyfactory.exceptions import ParameterException
9-
from polyfactory.factories.pydantic_factory import ModelFactory
10-
from polyfactory.field_meta import FieldMeta
9+
from polyfactory.factories.pydantic_factory import ModelFactory, PydanticFieldMeta
1110
from polyfactory.value_generators.constrained_collections import (
1211
handle_constrained_collection,
1312
)
@@ -22,7 +21,7 @@ def test_handle_constrained_set_with_min_items_and_max_items(min_items: int, max
2221
result = handle_constrained_collection(
2322
collection_type=list,
2423
factory=ModelFactory,
25-
field_meta=FieldMeta(name="test", annotation=set),
24+
field_meta=PydanticFieldMeta(name="test", annotation=set),
2625
item_type=str,
2726
max_items=max_items,
2827
min_items=min_items,
@@ -34,7 +33,7 @@ def test_handle_constrained_set_with_min_items_and_max_items(min_items: int, max
3433
handle_constrained_collection(
3534
collection_type=list,
3635
factory=ModelFactory,
37-
field_meta=FieldMeta(name="test", annotation=set),
36+
field_meta=PydanticFieldMeta(name="test", annotation=set),
3837
item_type=str,
3938
max_items=max_items,
4039
min_items=min_items,
@@ -50,7 +49,7 @@ def test_handle_constrained_set_with_max_items(
5049
result = handle_constrained_collection(
5150
collection_type=list,
5251
factory=ModelFactory,
53-
field_meta=FieldMeta(name="test", annotation=set),
52+
field_meta=PydanticFieldMeta(name="test", annotation=set),
5453
item_type=str,
5554
max_items=max_items,
5655
)
@@ -66,7 +65,7 @@ def test_handle_constrained_set_with_min_items(
6665
result = handle_constrained_collection(
6766
collection_type=list,
6867
factory=ModelFactory,
69-
field_meta=FieldMeta(name="test", annotation=set),
68+
field_meta=PydanticFieldMeta(name="test", annotation=set),
7069
item_type=str,
7170
min_items=min_items,
7271
)
@@ -79,7 +78,7 @@ def test_handle_constrained_set_with_different_types(t_type: Any) -> None:
7978
result = handle_constrained_collection(
8079
collection_type=list,
8180
factory=ModelFactory,
82-
field_meta=FieldMeta(name="test", annotation=set),
81+
field_meta=PydanticFieldMeta(name="test", annotation=set),
8382
item_type=t_type,
8483
)
8584
assert len(result) >= 0

0 commit comments

Comments
 (0)