microsoft / pyright

Static Type Checker for Python
Other
13.12k stars 1.4k forks source link

class-decorator errors but equivalent function call succeeds #8681

Closed randolf-scholz closed 1 month ago

randolf-scholz commented 1 month ago

In the example below, pyright complains that the decorator is untyped and claims that PointA lacks the __dataclass_fields__ attribute. Yet, when the decorator is applied as a function to the equivalent class PointB, no such diagnostics are produced, so presumably either one must be incorrect. (mypy playground with --strict shows no errors)

Code sample in pyright playground

from typing import Protocol, ClassVar, Any
from dataclasses import dataclass, Field

class Dataclass(Protocol):
    __dataclass_fields__: ClassVar[dict[str, Field[Any]]]
    """The fields of the dataclass."""

def dataclass_decorator[D: Dataclass](cls: type[D]) -> type[D]:
    # for example, add a custom pretty printing based on the fields.
    return cls

# using the decorator as a decorator
@dataclass_decorator  # ❌ raises reportArgumentType, reportUntypedClassDecorator
@dataclass
class PointA:
    x: int
    y: int

@dataclass
class PointB:
    x: int
    y: int

# using the decorator as a function
Point = dataclass_decorator(PointB) # ✅ no error 

Maybe related to https://github.com/microsoft/pyright/issues/4339? The typeshed source contains a comment that such a Dataclass Protocol might produce unexpected results in pyright.

erictraut commented 1 month ago

Pyright is working as intended here, so I don't consider this a bug.

You've created a circular reference here that cannot be resolved.

The issue is that dataclasses can (and do) contain fields that refer to the dataclass type itself. For example, PointA in your example, could contain a field defined as a: "PointA". In this case, "PointA" refers to the decorated PointA — i.e. the version of the class that has all of its class decorators applied. For that reason, pyright processes class decorations on the class before processing any of the fields, whose types affect synthesized dataclass methods. That means __dataclass_fields__ has not yet been synthesized at the point where your dataclass_decorator call is evaluated for PointA.

In the case of PointB, you have broken the circularity by applying the decorator independently. This is the approach I recommend if your dataclass class decorator depends on the presence of synthesized methods and fields.