from __future__ import annotations
from inspect import isclass
from typing import TYPE_CHECKING, Generic, TypeVar
from polyfactory.exceptions import MissingDependencyException
from polyfactory.factories.base import BaseFactory
from polyfactory.field_meta import FieldMeta, Null
if TYPE_CHECKING:
from typing import Any, TypeGuard
try:
import attrs
from attr._make import Factory
from attrs import AttrsInstance
except ImportError as ex:
msg = "attrs is not installed"
raise MissingDependencyException(msg) from ex
T = TypeVar("T", bound=AttrsInstance)
[docs]class AttrsFactory(Generic[T], BaseFactory[T]):
"""Base factory for attrs classes."""
__model__: type[T]
__is_base_factory__ = True
[docs] @classmethod
def is_supported_type(cls, value: Any) -> TypeGuard[type[T]]:
return isclass(value) and hasattr(value, "__attrs_attrs__")
[docs] @classmethod
def get_model_fields(cls) -> list[FieldMeta]:
field_metas: list[FieldMeta] = []
none_type = type(None)
cls.resolve_types(cls.__model__)
fields = attrs.fields(cls.__model__)
for field in fields:
if not field.init:
continue
annotation = none_type if field.type is None else field.type
default = field.default
if isinstance(default, Factory):
# The default value is not currently being used when generating
# the field values. When that is implemented, this would need
# to be handled differently since the `default.factory` could
# take a `self` argument.
default_value = default.factory
elif default is None:
default_value = Null
else:
default_value = default
field_metas.append(
FieldMeta.from_type(
annotation=annotation,
name=field.alias,
default=default_value,
random=cls.__random__,
),
)
return field_metas
[docs] @classmethod
def resolve_types(cls, model: type[T], **kwargs: Any) -> None:
"""Resolve any strings and forward annotations in type annotations.
:param model: The model to resolve the type annotations for.
:param kwargs: Any parameters that need to be passed to `attrs.resolve_types`.
"""
attrs.resolve_types(model, **kwargs)