Repository: litestar-org/polyfactory (python)

Issue:
Enhancement: pydantic: allow to use examples attribute of the field
### Summary

Pydantic allows to specify example values. It is very good for fields like city or country or currency. It gives very realistic outputs then. 

Then we can use random.choice to choose one of these examples while allowing it to be anything else. This may be an optional feature enabled in the factory for users that want it.

### Basic Example

```
class Payment(BaseModel):
    amount: int = Field(0)
    currency: str = Field(None, examples=['USD', 'EUR', 'INR'])

class PaymentFactory(ModelFactory(Payment)):
    __use_examples__ = True  # default would be false for backwards compatibility
```
```
>>> payment = PaymentFactory.build()
>>> payment
Payment(amount=120, currency="EUR")
```



### Drawbacks and Impact

I see no impact. As it will be an optional feature, it will be backwards compatible.

### Unresolved questions

No questions.

Pull request:
feat: Issue 678 use pydantic field examples
<!--
By submitting this pull request, you agree to:
- follow [Litestar's Code of Conduct](https://github.com/litestar-org/.github/blob/main/CODE_OF_CONDUCT.md)
- follow [Litestar's contribution guidelines](https://github.com/litestar-org/.github/blob/main/CONTRIBUTING.md)
- follow the [PSFs's Code of Conduct](https://www.python.org/psf/conduct/)
-->
## Description
trying to implement the enhancement I asked for. If you define ``__allow_examples__`` dunder attribute as True in your factory, it will use a random value from the pydantic class's .examples attribute (see https://docs.pydantic.dev/latest/concepts/fields/#customizing-json-schema). The default value is False, so it is backwards compatible.

This feature is supported only in Pydantic V2, but for Pydantic V1 it will just not try to access this attribute (it is not present there), so it is backwards compatible. 

<!--
Please add in issue numbers this pull request will close, if applicable

Ensure you are using a supported keyword to properly link an issue:
https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
-->
## Closes
Fixes #678 


Patch:
diff --git a/docs/usage/configuration.rst b/docs/usage/configuration.rst
index 00866c7..e9b4f91 100644
--- a/docs/usage/configuration.rst
+++ b/docs/usage/configuration.rst
@@ -130,3 +130,16 @@ By default, ``__use_defaults__`` is set to ``False.`` If you need more fine grai
 .. literalinclude:: /examples/configuration/test_example_9.py
     :caption: Use Default Values
     :language: python
+
+
+Use Example Values (Pydantic >= V2)
+-----------------------------------
+
+If ``__use_examples__`` is set to ``True``, then a random value from examples attribute will be used for a given field,
+provided there's a non-empty list of examples defined for that field.
+
+By default, ``__use_examples__`` is set to ``False.``
+
+.. literalinclude:: /examples/configuration/test_example_10.py
+    :caption: Use Examples Values
+    :language: python
diff --git a/polyfactory/factories/pydantic_factory.py b/polyfactory/factories/pydantic_factory.py
index 8d438e1..e0b9a75 100644
--- a/polyfactory/factories/pydantic_factory.py
+++ b/polyfactory/factories/pydantic_factory.py
@@ -124,6 +124,7 @@ class PydanticFieldMeta(FieldMeta):
         default: Any = ...,
         children: list[FieldMeta] | None = None,
         constraints: PydanticConstraints | None = None,
+        examples: list[Any] | None = None,
     ) -> None:
         super().__init__(
             name=name,
@@ -133,6 +134,7 @@ class PydanticFieldMeta(FieldMeta):
             children=children,
             constraints=constraints,
         )
+        self.examples = examples
 
     @classmethod
     def from_field_info(
@@ -212,13 +214,15 @@ class PydanticFieldMeta(FieldMeta):
             if is_json:
                 constraints["json"] = True
 
-        return PydanticFieldMeta.from_type(
+        result = PydanticFieldMeta.from_type(
             annotation=annotation,
             children=children,
             constraints=cast("Constraints", {k: v for k, v in constraints.items() if v is not None}) or None,
             default=default_value,
             name=name,
         )
+        result.examples = field_info.examples
+        return result
 
     @classmethod
     def from_model_field(  # pragma: no cover
@@ -336,12 +340,15 @@ class PydanticFieldMeta(FieldMeta):
                 for arg in fields_to_iterate
             )
 
+        examples = None
+
         return PydanticFieldMeta(
             name=name,
             annotation=annotation,  # pyright: ignore[reportArgumentType]
             children=children or None,
             default=default_value,
             constraints=cast("PydanticConstraints", {k: v for k, v in constraints.items() if v is not None}) or None,
+            examples=examples,
         )
 
     if not _IS_PYDANTIC_V1:
@@ -363,6 +370,28 @@ class ModelFactory(Generic[T], BaseFactory[T]):
 
     __forward_ref_resolution_type_mapping__: ClassVar[Mapping[str, type]] = {}
     __is_base_factory__ = True
+    __use_examples__: ClassVar[bool] = False  # for backwards compatibility
+    """
+    Flag indicating whether to use a random example, if provided (Pydantic >=V2)
+
+    Example code::
+
+        class Payment(BaseModel):
+            amount: int = Field(0)
+            currency: str = Field(examples=['USD', 'EUR', 'INR'])
+
+        class PaymentFactory(ModelFactory[Payment]):
+            __use_examples__ = True
+
+    >>> payment = PaymentFactory.build()
+    >>> payment
+    Payment(amount=120, currency="EUR")
+    """
+
+    __config_keys__ = (
+        *BaseFactory.__config_keys__,
+        "__use_examples__",
+    )
 
     def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
         super().__init_subclass__(*args, **kwargs)
@@ -425,6 +454,7 @@ class ModelFactory(Generic[T], BaseFactory[T]):
         build_context: BuildContext | None = None,
     ) -> Any:
         constraints = cast("PydanticConstraints", field_meta.constraints)
+
         if constraints.pop("json", None):
             value = cls.get_field_value(
                 field_meta, field_build_parameters=field_build_parameters, build_context=build_context
@@ -435,6 +465,34 @@ class ModelFactory(Generic[T], BaseFactory[T]):
             annotation, field_meta, field_build_parameters=field_build_parameters, build_context=build_context
         )
 
+    @classmethod
+    def get_field_value(
+        cls,
+        field_meta: FieldMeta,
+        field_build_parameters: Any | None = None,
+        build_context: BuildContext | None = None,
+    ) -> Any:
+        """Return a value from examples if exists, else random value.
+
+        :param field_meta: FieldMeta instance.
+        :param field_build_parameters: Any build parameters passed to the factory as kwarg values.
+        :param build_context: BuildContext data for current build.
+
+        :returns: An arbitrary value.
+
+        """
+        result: Any
+
+        field_meta = cast("PydanticFieldMeta", field_meta)
+
+        if cls.__use_examples__ and field_meta.examples:
+            result = cls.__random__.choice(field_meta.examples)
+        else:
+            result = super().get_field_value(
+                field_meta=field_meta, field_build_parameters=field_build_parameters, build_context=build_context
+            )
+        return result
+
     @classmethod
     def build(
         cls,


Test patch:
diff --git a/docs/examples/configuration/test_example_10.py b/docs/examples/configuration/test_example_10.py
new file mode 100644
index 0000000..06f861f
--- /dev/null
+++ b/docs/examples/configuration/test_example_10.py
@@ -0,0 +1,17 @@
+from pydantic import BaseModel, Field
+
+from polyfactory.factories.pydantic_factory import ModelFactory
+
+
+class Payment(BaseModel):
+    amount: int = Field(0)
+    currency: str = Field(examples=["USD", "EUR", "INR"])
+
+
+class PaymentFactory(ModelFactory[Payment]):
+    __use_examples__ = True
+
+
+def test_use_examples() -> None:
+    instance = PaymentFactory.build()
+    assert instance.currency in ["USD", "EUR", "INR"]
diff --git a/tests/constraints/test_decimal_constraints.py b/tests/constraints/test_decimal_constraints.py
index 64dd62b..5438551 100644
--- a/tests/constraints/test_decimal_constraints.py
+++ b/tests/constraints/test_decimal_constraints.py
@@ -392,7 +392,7 @@ def test_handle_decimal_length() -> None:
 
     # here decimal places should determine max length
     max_digits = 10
-    decimal_places = 5
+    decimal_places: int | None = 5
 
     result = handle_decimal_length(decimal, decimal_places, max_digits)
     assert isinstance(result, Decimal)
diff --git a/tests/constraints/test_frozen_set_constraints.py b/tests/constraints/test_frozen_set_constraints.py
index 0b35aea..32547db 100644
--- a/tests/constraints/test_frozen_set_constraints.py
+++ b/tests/constraints/test_frozen_set_constraints.py
@@ -6,8 +6,7 @@ from hypothesis import given
 from hypothesis.strategies import integers
 
 from polyfactory.exceptions import ParameterException
-from polyfactory.factories.pydantic_factory import ModelFactory
-from polyfactory.field_meta import FieldMeta
+from polyfactory.factories.pydantic_factory import ModelFactory, PydanticFieldMeta
 from polyfactory.value_generators.constrained_collections import (
     handle_constrained_collection,
 )
@@ -22,7 +21,7 @@ def test_handle_constrained_set_with_min_items_and_max_items(min_items: int, max
         result = handle_constrained_collection(
             collection_type=frozenset,
             factory=ModelFactory,
-            field_meta=FieldMeta(name="test", annotation=frozenset),
+            field_meta=PydanticFieldMeta(name="test", annotation=frozenset),
             item_type=str,
             max_items=max_items,
             min_items=min_items,
@@ -34,7 +33,7 @@ def test_handle_constrained_set_with_min_items_and_max_items(min_items: int, max
             handle_constrained_collection(
                 collection_type=frozenset,
                 factory=ModelFactory,
-                field_meta=FieldMeta(name="test", annotation=frozenset),
+                field_meta=PydanticFieldMeta(name="test", annotation=frozenset),
                 item_type=str,
                 max_items=max_items,
                 min_items=min_items,
@@ -50,7 +49,7 @@ def test_handle_constrained_set_with_max_items(
     result = handle_constrained_collection(
         collection_type=frozenset,
         factory=ModelFactory,
-        field_meta=FieldMeta(name="test", annotation=frozenset),
+        field_meta=PydanticFieldMeta(name="test", annotation=frozenset),
         item_type=str,
         max_items=max_items,
     )
@@ -66,7 +65,7 @@ def test_handle_constrained_set_with_min_items(
     result = handle_constrained_collection(
         collection_type=frozenset,
         factory=ModelFactory,
-        field_meta=FieldMeta(name="test", annotation=frozenset),
+        field_meta=PydanticFieldMeta(name="test", annotation=frozenset),
         item_type=str,
         min_items=min_items,
     )
@@ -79,7 +78,7 @@ def test_handle_constrained_set_with_different_types(t_type: Any) -> None:
         result = handle_constrained_collection(
             collection_type=frozenset,
             factory=ModelFactory,
-            field_meta=FieldMeta(name="test", annotation=frozenset),
+            field_meta=PydanticFieldMeta(name="test", annotation=frozenset),
             item_type=t_type,
         )
         assert len(result) >= 0
diff --git a/tests/constraints/test_list_constraints.py b/tests/constraints/test_list_constraints.py
index ccc0d86..e0d1a26 100644
--- a/tests/constraints/test_list_constraints.py
+++ b/tests/constraints/test_list_constraints.py
@@ -8,8 +8,7 @@ from hypothesis.strategies import integers
 from pydantic import VERSION
 
 from polyfactory.exceptions import ParameterException
-from polyfactory.factories.pydantic_factory import ModelFactory
-from polyfactory.field_meta import FieldMeta
+from polyfactory.factories.pydantic_factory import ModelFactory, PydanticFieldMeta
 from polyfactory.value_generators.constrained_collections import (
     handle_constrained_collection,
 )
@@ -24,7 +23,7 @@ def test_handle_constrained_list_with_min_items_and_max_items(min_items: int, ma
         result = handle_constrained_collection(
             collection_type=list,
             factory=ModelFactory,
-            field_meta=FieldMeta(name="test", annotation=list),
+            field_meta=PydanticFieldMeta(name="test", annotation=list),
             item_type=str,
             max_items=max_items,
             min_items=min_items,
@@ -36,7 +35,7 @@ def test_handle_constrained_list_with_min_items_and_max_items(min_items: int, ma
             handle_constrained_collection(
                 collection_type=list,
                 factory=ModelFactory,
-                field_meta=FieldMeta(name="test", annotation=list),
+                field_meta=PydanticFieldMeta(name="test", annotation=list),
                 item_type=str,
                 max_items=max_items,
                 min_items=min_items,
@@ -52,7 +51,7 @@ def test_handle_constrained_list_with_max_items(
     result = handle_constrained_collection(
         collection_type=list,
         factory=ModelFactory,
-        field_meta=FieldMeta(name="test", annotation=list),
+        field_meta=PydanticFieldMeta(name="test", annotation=list),
         item_type=str,
         max_items=max_items,
     )
@@ -68,7 +67,7 @@ def test_handle_constrained_list_with_min_items(
     result = handle_constrained_collection(
         collection_type=list,
         factory=ModelFactory,
-        field_meta=FieldMeta.from_type(List[str], name="test"),
+        field_meta=PydanticFieldMeta.from_type(List[str], name="test"),
         item_type=str,
         min_items=min_items,
     )
@@ -81,7 +80,7 @@ def test_handle_constrained_list_with_min_items(
 )
 @pytest.mark.parametrize("t_type", tuple(ModelFactory.get_provider_map()))
 def test_handle_constrained_list_with_different_types(t_type: Any) -> None:
-    field_meta = FieldMeta.from_type(List[t_type], name="test")
+    field_meta = PydanticFieldMeta.from_type(List[t_type], name="test")
     result = handle_constrained_collection(
         collection_type=list,
         factory=ModelFactory,
@@ -92,7 +91,7 @@ def test_handle_constrained_list_with_different_types(t_type: Any) -> None:
 
 
 def test_handle_unique_items() -> None:
-    field_meta = FieldMeta.from_type(List[str], name="test", constraints={"unique_items": True})
+    field_meta = PydanticFieldMeta.from_type(List[str], name="test", constraints={"unique_items": True})
     result = handle_constrained_collection(
         collection_type=list,
         factory=ModelFactory,
diff --git a/tests/constraints/test_mapping_constraints.py b/tests/constraints/test_mapping_constraints.py
index 405df99..eb20a49 100644
--- a/tests/constraints/test_mapping_constraints.py
+++ b/tests/constraints/test_mapping_constraints.py
@@ -3,8 +3,7 @@ from hypothesis import given
 from hypothesis.strategies import integers
 
 from polyfactory.exceptions import ParameterException
-from polyfactory.factories.pydantic_factory import ModelFactory
-from polyfactory.field_meta import FieldMeta
+from polyfactory.factories.pydantic_factory import ModelFactory, PydanticFieldMeta
 from polyfactory.value_generators.constrained_collections import (
     handle_constrained_mapping,
 )
@@ -15,9 +14,9 @@ from polyfactory.value_generators.constrained_collections import (
     integers(min_value=0, max_value=10),
 )
 def test_handle_constrained_mapping_with_min_items_and_max_items(min_items: int, max_items: int) -> None:
-    key_field = FieldMeta(name="key", annotation=str)
-    value_field = FieldMeta(name="value", annotation=int)
-    field_meta = FieldMeta(name="test", annotation=dict, children=[key_field, value_field])
+    key_field = PydanticFieldMeta(name="key", annotation=str)
+    value_field = PydanticFieldMeta(name="value", annotation=int)
+    field_meta = PydanticFieldMeta(name="test", annotation=dict, children=[key_field, value_field])
 
     if max_items >= min_items:
         result = handle_constrained_mapping(
@@ -47,9 +46,9 @@ def test_handle_constrained_mapping_with_constrained_key_and_value() -> None:
     min_length = 5
     max_length = 10
 
-    key_field = FieldMeta(name="key", annotation=str, constraints={"min_length": key_min_length})
-    value_field = FieldMeta(name="value", annotation=int, constraints={"gt": value_gt})
-    field_meta = FieldMeta(name="test", annotation=dict, children=[key_field, value_field])
+    key_field = PydanticFieldMeta(name="key", annotation=str, constraints={"min_length": key_min_length})
+    value_field = PydanticFieldMeta(name="value", annotation=int, constraints={"gt": value_gt})
+    field_meta = PydanticFieldMeta(name="test", annotation=dict, children=[key_field, value_field])
 
     result = handle_constrained_mapping(
         factory=ModelFactory,
diff --git a/tests/constraints/test_set_constraints.py b/tests/constraints/test_set_constraints.py
index b84cf44..b7e8e1c 100644
--- a/tests/constraints/test_set_constraints.py
+++ b/tests/constraints/test_set_constraints.py
@@ -6,8 +6,7 @@ from hypothesis import given
 from hypothesis.strategies import integers
 
 from polyfactory.exceptions import ParameterException
-from polyfactory.factories.pydantic_factory import ModelFactory
-from polyfactory.field_meta import FieldMeta
+from polyfactory.factories.pydantic_factory import ModelFactory, PydanticFieldMeta
 from polyfactory.value_generators.constrained_collections import (
     handle_constrained_collection,
 )
@@ -22,7 +21,7 @@ def test_handle_constrained_set_with_min_items_and_max_items(min_items: int, max
         result = handle_constrained_collection(
             collection_type=list,
             factory=ModelFactory,
-            field_meta=FieldMeta(name="test", annotation=set),
+            field_meta=PydanticFieldMeta(name="test", annotation=set),
             item_type=str,
             max_items=max_items,
             min_items=min_items,
@@ -34,7 +33,7 @@ def test_handle_constrained_set_with_min_items_and_max_items(min_items: int, max
             handle_constrained_collection(
                 collection_type=list,
                 factory=ModelFactory,
-                field_meta=FieldMeta(name="test", annotation=set),
+                field_meta=PydanticFieldMeta(name="test", annotation=set),
                 item_type=str,
                 max_items=max_items,
                 min_items=min_items,
@@ -50,7 +49,7 @@ def test_handle_constrained_set_with_max_items(
     result = handle_constrained_collection(
         collection_type=list,
         factory=ModelFactory,
-        field_meta=FieldMeta(name="test", annotation=set),
+        field_meta=PydanticFieldMeta(name="test", annotation=set),
         item_type=str,
         max_items=max_items,
     )
@@ -66,7 +65,7 @@ def test_handle_constrained_set_with_min_items(
     result = handle_constrained_collection(
         collection_type=list,
         factory=ModelFactory,
-        field_meta=FieldMeta(name="test", annotation=set),
+        field_meta=PydanticFieldMeta(name="test", annotation=set),
         item_type=str,
         min_items=min_items,
     )
@@ -79,7 +78,7 @@ def test_handle_constrained_set_with_different_types(t_type: Any) -> None:
         result = handle_constrained_collection(
             collection_type=list,
             factory=ModelFactory,
-            field_meta=FieldMeta(name="test", annotation=set),
+            field_meta=PydanticFieldMeta(name="test", annotation=set),
             item_type=t_type,
         )
         assert len(result) >= 0
diff --git a/tests/test_pydantic_factory.py b/tests/test_pydantic_factory.py
index c26168d..5612cf5 100644
--- a/tests/test_pydantic_factory.py
+++ b/tests/test_pydantic_factory.py
@@ -1047,3 +1047,56 @@ def test_skip_validation() -> None:
 
     AFactory = ModelFactory.create_factory(A)
     assert AFactory.build()
+
+
+@pytest.mark.skipif(_IS_PYDANTIC_V1, reason="Pydantic 1 doesn't support examples")
+def test_use_examples_not_defined() -> None:
+    class Payment(BaseModel):
+        amount: int = Field(0)
+        currency: str = Field(examples=["USD", "EUR", "INR"])
+
+    class PaymentFactory(ModelFactory[Payment]): ...
+
+    instance = PaymentFactory.build()
+    # it cannot fit the listed items, because faker uses longer strings
+
+    assert instance.currency not in ["USD", "EUR", "INR"]
+
+
+@pytest.mark.skipif(_IS_PYDANTIC_V1, reason="Pydantic 1 doesn't support examples")
+def test_use_examples_true() -> None:
+    class Payment(BaseModel):
+        amount: int = Field(0)
+        currency: str = Field(examples=["USD", "EUR", "INR"])
+
+    class PaymentFactory(ModelFactory[Payment]):
+        __use_examples__ = True
+
+    instance = PaymentFactory.build()
+    assert instance.currency in ["USD", "EUR", "INR"]
+
+
+@pytest.mark.skipif(_IS_PYDANTIC_V1, reason="Pydantic 1 doesn't support examples")
+def test_use_examples_false() -> None:
+    class Payment(BaseModel):
+        amount: int = Field(0)
+        currency: str = Field(examples=["USD", "EUR", "INR"])
+
+    class PaymentFactory(ModelFactory[Payment]):
+        __use_examples__ = False
+
+    instance = PaymentFactory.build()
+    assert instance.currency not in ["USD", "EUR", "INR"]
+
+
+@pytest.mark.skipif(_IS_PYDANTIC_V1, reason="Pydantic 1 doesn't support examples")
+def test_use_examples_value_override() -> None:
+    class Payment(BaseModel):
+        amount: int = Field(0)
+        currency: str = Field(examples=["USD", "EUR", "INR"])
+
+    class PaymentFactory(ModelFactory[Payment]):
+        __use_examples__ = True
+
+    instance = PaymentFactory.build(currency="DKK")
+    assert instance.currency == "DKK"