openrewrite / openrewrite/rewrite

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

Open
#8,772 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
3.7k
Forks
571
Avg merge
13h 12m
Merged PRs (30d)
261

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in _parser_visitor.py at visit_ClassDef, then inspect how self._type_mapping.type(node) and __convert_type(base) handle decorated class definitions. Use the minimal Django/DRF reproduction and the class-hierarchy results to verify the change. Done means Any-returning or untyped decorators preserve the class declaration type and its APIView or other base types.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
devtools, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.