Repository: litestar-org/polyfactory (python)

Issue:
Bug: Required fields in TypedDicts throw exceptions
### Description

Hello, thanks for this super useful library! I was trying out polyfactory with some TypedDicts and it throws an exception. Not sure if this should be classified as a bug or a feature request, but wanted to bring it to your attention in any case.

### URL to code causing the issue

_No response_

### MCVE

```python
from typing_extensions import TypedDict, Required
from polyfactory.factories import TypedDictFactory

class Foo(TypedDict, total=False):
    name: Required[str]
    bar: str

class FooFactory(TypedDictFactory[Foo]):
    __model__ = Foo
```


### Steps to reproduce

```bash
Run `FooFactory.build()`, and I get the exception: `TypeError: Required accepts only a single type. Got (<class 'str'>,).`
```


### Screenshots

_No response_

### Logs

_No response_

### Release Version

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


Pull request:
fix: handle Required and NonRequired annotations
<!--
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
-->

- properly handle `Required` and `NonRequired` annotations

### Close Issue(s)
<!--
Please add in issue numbers this pull request will close, if applicable
Examples: Fixes #4321 or Closes #1234
-->

- Fixes #421 


Patch:
diff --git a/polyfactory/factories/typed_dict_factory.py b/polyfactory/factories/typed_dict_factory.py
index 80eb78d..2a3ea1b 100644
--- a/polyfactory/factories/typed_dict_factory.py
+++ b/polyfactory/factories/typed_dict_factory.py
@@ -1,8 +1,16 @@
 from __future__ import annotations
 
-from typing import Any, Generic, TypeVar
-
-from typing_extensions import TypeGuard, _TypedDictMeta, get_type_hints, is_typeddict  # type: ignore[attr-defined]
+from typing import Any, Generic, TypeVar, get_args
+
+from typing_extensions import (  # type: ignore[attr-defined]
+    NotRequired,
+    Required,
+    TypeGuard,
+    _TypedDictMeta,  # pyright: ignore[reportGeneralTypeIssues]
+    get_origin,
+    get_type_hints,
+    is_typeddict,
+)
 
 from polyfactory.constants import DEFAULT_RANDOM
 from polyfactory.factories.base import BaseFactory
@@ -35,13 +43,19 @@ class TypedDictFactory(Generic[TypedDictT], BaseFactory[TypedDictT]):
         """
         model_type_hints = get_type_hints(cls.__model__, include_extras=True)
 
-        fields_meta: list["FieldMeta"] = [
-            FieldMeta.from_type(
-                annotation=annotation,
-                random=DEFAULT_RANDOM,
-                name=field_name,
-                default=getattr(cls.__model__, field_name, Null),
+        field_metas: list[FieldMeta] = []
+        for field_name, annotation in model_type_hints.items():
+            origin = get_origin(annotation)
+            if origin in (Required, NotRequired):
+                annotation = get_args(annotation)[0]  # noqa: PLW2901
+
+            field_metas.append(
+                FieldMeta.from_type(
+                    annotation=annotation,
+                    random=DEFAULT_RANDOM,
+                    name=field_name,
+                    default=getattr(cls.__model__, field_name, Null),
+                ),
             )
-            for field_name, annotation in model_type_hints.items()
-        ]
-        return fields_meta
+
+        return field_metas


Test patch:
diff --git a/tests/constraints/test_int_constraints.py b/tests/constraints/test_int_constraints.py
index 7a963d8..67edb30 100644
--- a/tests/constraints/test_int_constraints.py
+++ b/tests/constraints/test_int_constraints.py
@@ -1,7 +1,7 @@
 from random import Random
 
 import pytest
-from hypothesis import assume, given
+from hypothesis import HealthCheck, assume, given, settings
 from hypothesis.strategies import integers
 
 from polyfactory.exceptions import ParameterException
@@ -55,6 +55,7 @@ def test_handle_constrained_int_handles_lt(maximum: int) -> None:
     assert result < maximum
 
 
+@settings(suppress_health_check=(HealthCheck.filter_too_much,))
 @given(
     integers(min_value=1, max_value=10),
     integers(min_value=1, max_value=10),
diff --git a/tests/test_typeddict_factory.py b/tests/test_typeddict_factory.py
index 9d3a313..c9a3267 100644
--- a/tests/test_typeddict_factory.py
+++ b/tests/test_typeddict_factory.py
@@ -1,7 +1,8 @@
 from typing import Dict, List, Optional
 
+from annotated_types import Ge
 from pydantic import BaseModel
-from typing_extensions import TypedDict
+from typing_extensions import Annotated, NotRequired, Required, TypedDict
 
 from polyfactory.factories import TypedDictFactory
 from polyfactory.factories.pydantic_factory import ModelFactory
@@ -44,3 +45,25 @@ def test_factory_model_with_typeddict_attribute_value() -> None:
     assert result.td["name"]
     assert result.td["list_field"][0]
     assert type(result.td["int_field"]) in (type(None), int)
+
+
+def test_typeddict_with_required_and_non_required_fields() -> None:
+    class TypedDictModel(TypedDict):
+        id: Required[int]
+        name: NotRequired[str]
+        annotated: Required[Annotated[int, Ge(100)]]
+        list_field: List[Dict[str, int]]
+        optional_int: Required[Optional[int]]
+
+    class TypedDictModelFactory(TypedDictFactory[TypedDictModel]):
+        __model__ = TypedDictModel
+
+    result = TypedDictModelFactory.build()
+
+    assert isinstance(result["id"], int)
+    assert isinstance(result["annotated"], int)
+    assert result["annotated"] >= 100
+    assert isinstance(result["list_field"], list)
+    assert isinstance(result["optional_int"], (type(None), int))
+    assert "name" in result
+    assert isinstance(result["name"], str)