Repository: litestar-org/polyfactory (python)

Issue:
Bug: Annotations for nested types are ignored
### Description

When a pydantic model contains a simple annotated attribute, the restrictions from the annotation are used for generating the value.

Example:
```python
Dimension = Annotated[int, Field(gt=1000, lt=2000)]

class Item(BaseModel):
    x: Dimension

class ItemFactory(ModelFactory):
    __model__ = Item

print(ItemFactory.build())
```
```
>>> x=1456
```

However, if I change the Item model to 

```python
class Item(BaseModel):
    dimensions: list[Dimension]

```
I get the following error saying that polyfactory generated the value 8569
```
>>>
pydantic_core._pydantic_core.ValidationError: 1 validation error for Item
dimensions.0
  Input should be less than 2000 [type=less_than, input_value=8569, input_type=int]
    For further information visit https://errors.pydantic.dev/2.4/v/less_than
```




### URL to code causing the issue

_No response_

### MCVE

```python
from polyfactory.factories.pydantic_factory import ModelFactory
from typing import Annotated
from pydantic import BaseModel, Field

Dimension = Annotated[int, Field(gt=1000, lt=2000)]

class Item(BaseModel):
    dimensions: list[Dimension]


class ItemFactory(ModelFactory):
    __model__ = Item


print(ItemFactory.build())
```


### Steps to reproduce

```bash
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
```


### Screenshots

"In the format of: `![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)`"


### Logs

_No response_

### Release Version

2.11.0

### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh Litestar dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/polyfactory/issues/434">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/polyfactory/issues/434/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/polyfactory/issues/434/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->


Pull request:
fix: correctly handle collections with constrained items  
<!--
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/)
-->

### Pull Request Checklist

- [x] New code has 100% test coverage
- [ ] (If applicable) The prose documentation has been updated to reflect the changes introduced by this PR
- [ ] (If applicable) The reference documentation has been updated to reflect the changes introduced by this PR
- [x] Pre-Commit Checks were ran and passed
- [x] Tests were ran and passed

### Description
<!--
Please describe your pull request for new release changelog purposes
-->

This PR ensures that the constraints for subitems in collection types are parsed correctly.

### Close Issue(s)
<!--
Please add in issue numbers this pull request will close, if applicable
Examples: Fixes #4321 or Closes #1234
-->

- Fixes #434.


Patch:
diff --git a/polyfactory/factories/pydantic_factory.py b/polyfactory/factories/pydantic_factory.py
index 9c16710..bb03581 100644
--- a/polyfactory/factories/pydantic_factory.py
+++ b/polyfactory/factories/pydantic_factory.py
@@ -39,7 +39,7 @@ with suppress(ImportError):
 
 if TYPE_CHECKING:
     from random import Random
-    from typing import Callable
+    from typing import Callable, Sequence
 
     from typing_extensions import NotRequired, TypeGuard
 
@@ -284,6 +284,19 @@ class PydanticFieldMeta(FieldMeta):
             constraints=cast("PydanticConstraints", {k: v for k, v in constraints.items() if v is not None}) or None,
         )
 
+    if VERSION.startswith("2"):
+
+        @classmethod
+        def get_constraints_metadata(cls, annotation: Any) -> Sequence[Any]:
+            metadata = []
+            for m in super().get_constraints_metadata(annotation):
+                if isinstance(m, FieldInfo):
+                    metadata.extend(m.metadata)
+                else:
+                    metadata.append(m)
+
+            return metadata
+
 
 class ModelFactory(Generic[T], BaseFactory[T]):
     """Base factory for pydantic models"""
diff --git a/polyfactory/field_meta.py b/polyfactory/field_meta.py
index 245ada3..4b99f8b 100644
--- a/polyfactory/field_meta.py
+++ b/polyfactory/field_meta.py
@@ -8,14 +8,21 @@ from typing_extensions import get_args, get_origin
 from polyfactory.collection_extender import CollectionExtender
 from polyfactory.constants import DEFAULT_RANDOM, TYPE_MAPPING
 from polyfactory.utils.deprecation import check_for_deprecated_parameters
-from polyfactory.utils.helpers import normalize_annotation, unwrap_annotated, unwrap_args, unwrap_new_type
+from polyfactory.utils.helpers import (
+    get_annotation_metadata,
+    normalize_annotation,
+    unwrap_annotated,
+    unwrap_args,
+    unwrap_new_type,
+)
 from polyfactory.utils.predicates import is_annotated, is_any_annotated
 
 if TYPE_CHECKING:
     import datetime
+    from decimal import Decimal
     from random import Random
+    from typing import Sequence
 
-    from _pydecimal import Decimal
     from typing_extensions import NotRequired, Self
 
 
@@ -134,7 +141,7 @@ class FieldMeta:
         field_type = normalize_annotation(annotation, random=random)
 
         if not constraints and is_annotated(annotation):
-            _, metadata = unwrap_annotated(annotation, random=random)
+            metadata = cls.get_constraints_metadata(annotation)
             constraints = cls.parse_constraints(metadata)
 
         if not is_any_annotated(annotation):
@@ -156,7 +163,7 @@ class FieldMeta:
             number_of_args = 1
             extended_type_args = CollectionExtender.extend_type_args(field.annotation, field.type_args, number_of_args)
             field.children = [
-                FieldMeta.from_type(
+                cls.from_type(
                     annotation=unwrap_new_type(arg),
                     random=random,
                 )
@@ -165,7 +172,7 @@ class FieldMeta:
         return field
 
     @classmethod
-    def parse_constraints(cls, metadata: list[Any]) -> "Constraints":
+    def parse_constraints(cls, metadata: Sequence[Any]) -> "Constraints":
         constraints = {}
 
         for value in metadata:
@@ -215,3 +222,15 @@ class FieldMeta:
                     },
                 )
         return cast("Constraints", constraints)
+
+    @classmethod
+    def get_constraints_metadata(cls, annotation: Any) -> Sequence[Any]:
+        """Get the metadatas of the constraints from the given annotation.
+
+        :param annotation: A type annotation.
+        :param random: An instance of random.Random.
+
+        :returns: A list of the metadata in the annotation.
+        """
+
+        return get_annotation_metadata(annotation)
diff --git a/polyfactory/utils/helpers.py b/polyfactory/utils/helpers.py
index da34a8b..2eb2eca 100644
--- a/polyfactory/utils/helpers.py
+++ b/polyfactory/utils/helpers.py
@@ -21,6 +21,7 @@ from polyfactory.utils.predicates import (
 
 if TYPE_CHECKING:
     from random import Random
+    from typing import Sequence
 
 
 def unwrap_new_type(annotation: Any) -> Any:
@@ -162,6 +163,17 @@ def normalize_annotation(annotation: Any, random: Random) -> Any:
     return origin
 
 
+def get_annotation_metadata(annotation: Any) -> Sequence[Any]:
+    """Get the metadata in the annotation.
+
+    :param annotation: A type annotation.
+
+    :returns: The metadata.
+    """
+
+    return get_args(annotation)[1:]
+
+
 def get_collection_type(annotation: Any) -> type[list | tuple | set | frozenset | dict]:
     """Get the collection type from the annotation.
 


Test patch:
diff --git a/tests/test_msgspec_factory.py b/tests/test_msgspec_factory.py
index b4f13f9..02ebe60 100644
--- a/tests/test_msgspec_factory.py
+++ b/tests/test_msgspec_factory.py
@@ -7,7 +7,7 @@ from uuid import UUID
 
 import msgspec
 import pytest
-from msgspec import Struct, structs
+from msgspec import Meta, Struct, structs
 from typing_extensions import Annotated
 
 from polyfactory.exceptions import ParameterException
@@ -215,3 +215,37 @@ def test_inheritence_with_generics() -> None:
 
     validated_bar = msgspec.convert(bar_dict, type=Bar)
     assert validated_bar == bar
+
+
+def test_sequence_with_constrained_item_types() -> None:
+    ConstrainedInt = Annotated[int, Meta(ge=100, le=200)]
+
+    class Foo(Struct):
+        list_field: List[ConstrainedInt]
+        tuple_field: Tuple[ConstrainedInt]
+        variable_tuple_field: Tuple[ConstrainedInt, ...]
+        set_field: Set[ConstrainedInt]
+
+    class FooFactory(MsgspecFactory[Foo]):
+        __model__ = Foo
+
+    foo = FooFactory.build()
+    validated_foo = msgspec.convert(structs.asdict(foo), Foo)
+
+    assert validated_foo == foo
+
+
+def test_mapping_with_constrained_item_types() -> None:
+    ConstrainedInt = Annotated[int, Meta(ge=100, le=200)]
+    ConstrainedStr = Annotated[str, Meta(min_length=1, max_length=3)]
+
+    class Foo(Struct):
+        dict_field = Dict[ConstrainedStr, ConstrainedInt]
+
+    class FooFactory(MsgspecFactory[Foo]):
+        __model__ = Foo
+
+    foo = FooFactory.build()
+    validated_foo = msgspec.convert(structs.asdict(foo), Foo)
+
+    assert validated_foo == foo
diff --git a/tests/test_pydantic_factory.py b/tests/test_pydantic_factory.py
index 1a72f90..9dc8fdc 100644
--- a/tests/test_pydantic_factory.py
+++ b/tests/test_pydantic_factory.py
@@ -1,8 +1,9 @@
 import sys
-from typing import Optional
+from typing import Dict, List, Optional, Set, Tuple
 
 import pytest
 from pydantic import VERSION, BaseModel, Field, Json
+from typing_extensions import Annotated
 
 from polyfactory.factories.pydantic_factory import ModelFactory
 
@@ -78,3 +79,31 @@ def test_nested_json_type() -> None:
         __model__ = B
 
     assert isinstance(BFactory.build(), B)
+
+
+def test_sequence_with_annotated_item_types() -> None:
+    ConstrainedInt = Annotated[int, Field(ge=100, le=200)]
+
+    class Foo(BaseModel):
+        list_field: List[ConstrainedInt]
+        tuple_field: Tuple[ConstrainedInt]
+        variable_tuple_field: Tuple[ConstrainedInt, ...]
+        set_field: Set[ConstrainedInt]
+
+    class FooFactory(ModelFactory[Foo]):
+        __model__ = Foo
+
+    assert FooFactory.build()
+
+
+def test_mapping_with_annotated_item_types() -> None:
+    ConstrainedInt = Annotated[int, Field(ge=100, le=200)]
+    ConstrainedStr = Annotated[str, Field(min_length=1, max_length=3)]
+
+    class Foo(BaseModel):
+        dict_field: Dict[ConstrainedStr, ConstrainedInt]
+
+    class FooFactory(ModelFactory[Foo]):
+        __model__ = Foo
+
+    assert FooFactory.build()