Repository: litestar-org/polyfactory (python)

Issue:
Bug: forward referenced nested models don't work with `DataclassFactory`
### Description

The MCVE results in the following error: `NameError: name 'Bar' is not defined`

### URL to code causing the issue

_No response_

### MCVE

```python
from __future__ import annotations

from dataclasses import dataclass
from polyfactory.factories import DataclassFactory


@dataclass
class Foo:
    bar: Bar


@dataclass
class Bar:
    ...


class FooFactory(DataclassFactory[Foo]):
    __model__ = Foo

foo = FooFactory.build()
```


### Steps to reproduce

_No response_

### Screenshots

_No response_

### Logs

_No response_

### Release Version

2.9.0

### Platform

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

<!-- POLAR PLEDGE BADGE START -->
---

## Funding
* If you would like to see an issue prioritized, make a pledge towards it!
* We receive the pledge once the issue is completed & verified

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


Pull request:
fix: properly resolve dataclass forward references
[//]: # "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)"

### Pull Request Checklist

- [ ] 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
- [ ] Pre-Commit Checks were ran and passed
- [ ] Tests were ran and passed

### Description
[//]: # "Please describe your pull request for new release changelog purposes"

This PR reverts #371 because the dataclasses `fields` API does not properly resolve the forward annotations as explained in this [issue](https://github.com/python/cpython/issues/80824). For getting the resolved types, we have to use `get_type_hints` though that doesn't work well with models like `MappedAsDataclass` of `sqlalchemy`. For those cases, it's probably better for users to subclass the current `DataclassFactory` implementation and handle them accordingly.

### Close Issue(s)
[//]: # "Please add in issue numbers this pull request will close, if applicable"
[//]: # "Examples: Fixes #4321 or Closes #1234"

- Fixes #382  


Patch:
diff --git a/polyfactory/factories/dataclass_factory.py b/polyfactory/factories/dataclass_factory.py
index c034f84..fd68522 100644
--- a/polyfactory/factories/dataclass_factory.py
+++ b/polyfactory/factories/dataclass_factory.py
@@ -1,14 +1,12 @@
 from __future__ import annotations
 
 from dataclasses import MISSING, fields, is_dataclass
-from typing import TYPE_CHECKING, Any, ForwardRef, Generic
+from typing import Any, Generic
+
+from typing_extensions import TypeGuard, get_type_hints
 
 from polyfactory.factories.base import BaseFactory, T
 from polyfactory.field_meta import FieldMeta, Null
-from polyfactory.utils.helpers import evaluate_forwardref
-
-if TYPE_CHECKING:
-    from typing_extensions import TypeGuard
 
 
 class DataclassFactory(Generic[T], BaseFactory[T]):
@@ -35,6 +33,8 @@ class DataclassFactory(Generic[T], BaseFactory[T]):
         """
         fields_meta: list["FieldMeta"] = []
 
+        model_type_hints = get_type_hints(cls.__model__, include_extras=True)
+
         for field in fields(cls.__model__):  # type: ignore[arg-type]
             if field.default_factory and field.default_factory is not MISSING:
                 default_value = field.default_factory()
@@ -43,16 +43,9 @@ class DataclassFactory(Generic[T], BaseFactory[T]):
             else:
                 default_value = Null
 
-            if isinstance(field.type, ForwardRef):
-                annotation = evaluate_forwardref(field.type)  # type: ignore[unreachable]
-            elif isinstance(field.type, str):
-                annotation = evaluate_forwardref(ForwardRef(field.type))  # type: ignore[unreachable]
-            else:
-                annotation = field.type
-
             fields_meta.append(
                 FieldMeta.from_type(
-                    annotation=annotation,
+                    annotation=model_type_hints[field.name],
                     name=field.name,
                     default=default_value,
                     random=cls.__random__,
diff --git a/polyfactory/utils/helpers.py b/polyfactory/utils/helpers.py
index 9467a8d..ad0c376 100644
--- a/polyfactory/utils/helpers.py
+++ b/polyfactory/utils/helpers.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 import sys
-from typing import TYPE_CHECKING, Any, ForwardRef
+from typing import TYPE_CHECKING, Any
 
 from typing_extensions import get_args, get_origin
 
@@ -130,17 +130,3 @@ def normalize_annotation(annotation: Any, random: Random) -> Any:
         return origin[args] if origin is not type else annotation
 
     return origin
-
-
-def evaluate_forwardref(ref: ForwardRef) -> Any:
-    """Evaluate ForwardRef to get type annotation
-
-    :param ref: A ForwardRef object
-
-    :returns: A type annotation
-
-    """
-    if sys.version_info < (3, 9):
-        return ref._evaluate(globals(), locals())
-
-    return ref._evaluate(globals(), locals(), frozenset())


Test patch:
diff --git a/tests/test_dataclass_factory.py b/tests/test_dataclass_factory.py
index 2cb0cb7..5e86fcd 100644
--- a/tests/test_dataclass_factory.py
+++ b/tests/test_dataclass_factory.py
@@ -55,27 +55,6 @@ def test_factory_pydantic_dc() -> None:
     assert result.constrained_field >= 100
 
 
-def test_factory_sqlalchemy_dc() -> None:
-    from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column
-
-    class SqlAlchemyDCBase(MappedAsDataclass, DeclarativeBase):
-        pass
-
-    class SqlAlchemyDC(SqlAlchemyDCBase):
-        __tablename__ = "foo"
-
-        id: Mapped[int] = mapped_column(primary_key=True)
-        name: Mapped[str]
-
-    class SqlAlchemyDCFactory(DataclassFactory[SqlAlchemyDC]):
-        __model__ = SqlAlchemyDC
-
-    result = SqlAlchemyDCFactory.build()
-
-    assert isinstance(result.id, int)
-    assert isinstance(result.name, str)
-
-
 def test_vanilla_dc_with_embedded_model() -> None:
     @vanilla_dataclass
     class VanillaDC:
@@ -189,34 +168,33 @@ class example:
     assert MyFactory.process_kwargs() == {"foo": ANY}
 
 
-def test_sqlalchemy_dc_factory_with_future_annotations(create_module: Callable[[str], ModuleType]) -> None:
+def test_dataclass_with_forward_reference(create_module: Callable[[str], ModuleType]) -> None:
     module = create_module(
         """
 from __future__ import annotations
 
-from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column
+from dataclasses import dataclass
+from polyfactory.factories import DataclassFactory
 
-class SqlAlchemyDCBase(MappedAsDataclass, DeclarativeBase):  # type: ignore
-    pass
 
-class SqlAlchemyDC(SqlAlchemyDCBase):
-    __tablename__ = "foo"
+@dataclass
+class Foo:
+    bar: Bar
 
-    id: Mapped[int] = mapped_column(primary_key=True)
-    name: Mapped[str]
 
+@dataclass
+class Bar:
+    ...
 """
     )
 
-    SqlAlchemyDC: type = module.SqlAlchemyDC
-    assert SqlAlchemyDC.__annotations__ == {"id": "Mapped[int]", "name": "Mapped[str]"}
+    Foo: type = module.Foo
 
-    class SqlAlchemyDCFactory(DataclassFactory[SqlAlchemyDC]):  # type:ignore[valid-type]
-        __model__ = SqlAlchemyDC
+    class FooFactory(DataclassFactory[Foo]):  # type:ignore[valid-type]
+        __model__ = Foo
 
-    result = SqlAlchemyDCFactory.build()
-    assert isinstance(result.id, int)
-    assert isinstance(result.name, str)
+    foo = FooFactory.build()
+    assert isinstance(foo, Foo)
 
 
 def test_variable_length_tuple_generation__many_type_args() -> None: