openrewrite / openrewrite/rewrite
Python: class decorated with an Any-returning decorator loses its type and its bases
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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