Skip to content

Python: class decorated with an Any-returning decorator loses its type and its bases #8772

Open

Description

@jkschneider

What happens

A class decorated with a decorator whose return annotation is Any (or that is untyped) comes out of the parser with an unknown class type and no attributed bases. The same class without the decorator, or with a decorator typed (cls: T) -> T, attributes normally.

Minimal project (django, djangorestframework provisioned so APIView resolves):

from typing import Any, TypeVar
from rest_framework.views import APIView

T = TypeVar("T")

def any_deco(cls: Any = None) -> Any:
    return cls

def typed_deco(cls: T) -> T:
    return cls

class PlainView(APIView):
    def get(self, request): ...

@any_deco
class AnyDecoView(APIView):
    def get(self, request): ...

@typed_deco
class TypedDecoView(APIView):
    def get(self, request): ...

class Base(APIView):
    pass

@any_deco
class AnyDecoChild(Base):
    def post(self, request): ...

org.openrewrite.java.search.FindClassHierarchy over the resulting LST:

className superclass
probe.views.PlainView rest_framework.views.APIView
<unknown>
probe.views.TypedDecoView rest_framework.views.APIView
probe.views.Base rest_framework.views.APIView
<unknown>

The two <unknown> rows are AnyDecoView and AnyDecoChild. Any recipe that asks TypeUtils.isAssignableTo("rest_framework.views.APIView", classDecl.getImplements().get(0).getType()) gets false for them.

Why it matters

This is the dominant shape in real Django code bases. In getsentry/sentry, 192 of 208 endpoint classes under src/sentry/api/endpoints are decorated with @cell_silo_endpoint / @region_silo_endpoint / @control_silo_endpoint, and every one of those decorators is declared (decorated_obj: Any = None, ...) -> Any in src/sentry/api/base.py. Over that package, the class hierarchy table lists 990 class declarations, of which 315 are decorated, and the decorated ones all come back as <unknown> with no supertypes. A recipe looking for APIView subclasses finds the 16 undecorated base classes and misses the endpoints.

Where it likely is

visit_ClassDef in _parser_visitor.py sets the declaration's type from self._type_mapping.type(node) and each base from __convert_type(base). ty infers the bound name of a decorated class as the decorator's return type, so when that is Any both the class type and, apparently, the base expressions' types are lost. The class definition itself still has a class literal type in ty regardless of what the decorator returns; attributing the ClassDef (and its bases) from the definition rather than from the bound symbol would keep AnyDecoView typed as probe.views.AnyDecoView with APIView as its supertype, the way the undecorated declaration is.

Repro built with rewrite-python from the openrewrite PyPI package 8.91.4 and ty-types 0.0.78.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions