demberto
demberto

Reputation: 540

Checking if a field is a dataclass doesn't work when annotations are imported

from __future__ import annotations
from dataclasses import dataclass, is_dataclass, field, fields

@dataclass
class A:
    a: int | None = None
    b: int | None = None

@dataclass
class B:
    a_obj: A = field(default_factory=A)
    x: int = 0
    y: int = 0

for f in fields(B):
    print(is_dataclass(f.type))  # False for all

Is NOT using annotations the only way out of this, I would have to change a lot of the type annotations to use Optional[T] instead of T | None

Upvotes: 1

Views: 566

Answers (2)

Wizard.Ritvik
Wizard.Ritvik

Reputation: 11690

One option is to use an approach with cached class properties; this can also help to work around cases when class A is defined after B, for instance.

from __future__ import annotations

from dataclasses import dataclass, is_dataclass, field, fields, Field
from functools import cache
from typing import get_type_hints


def cached_class_attr(f):
    return classmethod(property(cache(f)))


@dataclass
class B:
    # using lambda because we can't use `A` directly, as it's not defined yet
    a_obj: A = field(default_factory=lambda: A())
    x: int = 0
    y: int = 0

    @cached_class_attr
    def __true_annotations__(cls):
        return get_type_hints(cls)

    @cached_class_attr
    def __fields__(cls):
        ann_dict: dict = cls.__true_annotations__
        cls_fields: tuple[Field, ...] = fields(cls)

        for f in cls_fields:
            f.type = ann_dict[f.name]

        return cls_fields


@dataclass
class A:
    a: int | None = None
    b: int | None = None


for name, tp in B.__true_annotations__.items():
    print(name, is_dataclass(tp))

Output:

a_obj True
x False
y False

Upvotes: 1

jsbueno
jsbueno

Reputation: 110631

I checked the code of dataclasses in Python 3.10 and there is no prevision for it to work with stringfied annotations, in the way that happens when one use from __future__ import annotations.

That is because the idea of stringifying annotations is ok just when using them for static type hinting. Dataclasses and newer Python libs, such as pydantic actually make use of annotations for runtime purposes - that has been the cause of PEP 563, which describes this stringfying effect, not to have been promoted to the default behavior in Python 3.10 - and the contending PEP 649 being put forward.

In this case, you can "re-hydrate" the annotations before passing the class to the dataclass decorator, since it does not do that. -- put this as an intermediate decorator and it should work:

def hydrate(cls):
    cls.__annotations__ = typing.get_type_hints(cls)
    return cls

and then:

from __future__ import annotations

...

@dataclass
@hydrate
class B:
    a_obj: A = field(default_factory=A)
    x: int = 0
    y: int = 0


Upvotes: 1

Related Questions