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

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

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

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.

Description

bug parser python

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.

Dominant language
Java
Stars
3.9k
Forks
599
Avg merge
9h 38m
Merged PRs (30d)
557

Getting set up

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.

More from openrewrite/rewrite

All issues in openrewrite/rewrite

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.