Skip to content

Call dynamic class hook on generic classes #16052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3205,6 +3205,13 @@ def apply_dynamic_class_hook(self, s: AssignmentStmt) -> None:
if isinstance(callee_expr, RefExpr) and callee_expr.fullname:
method_name = call.callee.name
fname = callee_expr.fullname + "." + method_name
elif (
isinstance(callee_expr, IndexExpr)
and isinstance(callee_expr.base, RefExpr)
and isinstance(callee_expr.analyzed, TypeApplication)
):
method_name = call.callee.name
fname = callee_expr.base.fullname + "." + method_name
elif isinstance(callee_expr, CallExpr):
# check if chain call
call = callee_expr
Expand Down
12 changes: 11 additions & 1 deletion test-data/unit/check-custom-plugin.test
Original file line number Diff line number Diff line change
Expand Up @@ -684,12 +684,16 @@ plugins=<ROOT>/test-data/unit/plugins/dyn_class.py
[case testDynamicClassHookFromClassMethod]
# flags: --config-file tmp/mypy.ini

from mod import QuerySet, Manager
from mod import QuerySet, Manager, GenericQuerySet

MyManager = Manager.from_queryset(QuerySet)
ManagerFromGenericQuerySet = GenericQuerySet[int].as_manager()

reveal_type(MyManager()) # N: Revealed type is "__main__.MyManager"
reveal_type(MyManager().attr) # N: Revealed type is "builtins.str"
reveal_type(ManagerFromGenericQuerySet()) # N: Revealed type is "__main__.ManagerFromGenericQuerySet"
reveal_type(ManagerFromGenericQuerySet().attr) # N: Revealed type is "builtins.int"
queryset: GenericQuerySet[int] = ManagerFromGenericQuerySet()

def func(manager: MyManager) -> None:
reveal_type(manager) # N: Revealed type is "__main__.MyManager"
Expand All @@ -704,6 +708,12 @@ class QuerySet:
class Manager:
@classmethod
def from_queryset(cls, queryset_cls: Type[QuerySet]): ...
T = TypeVar("T")
class GenericQuerySet(Generic[T]):
attr: T

@classmethod
def as_manager(cls): ...

[builtins fixtures/classmethod.pyi]
[file mypy.ini]
Expand Down
40 changes: 39 additions & 1 deletion test-data/unit/plugins/dyn_class_from_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@

from typing import Callable

from mypy.nodes import GDEF, Block, ClassDef, RefExpr, SymbolTable, SymbolTableNode, TypeInfo
from mypy.nodes import (
GDEF,
Block,
ClassDef,
IndexExpr,
MemberExpr,
NameExpr,
RefExpr,
SymbolTable,
SymbolTableNode,
TypeApplication,
TypeInfo,
)
from mypy.plugin import DynamicClassDefContext, Plugin
from mypy.types import Instance

Expand All @@ -13,6 +25,8 @@ def get_dynamic_class_hook(
) -> Callable[[DynamicClassDefContext], None] | None:
if "from_queryset" in fullname:
return add_info_hook
if "as_manager" in fullname:
return as_manager_hook
return None


Expand All @@ -34,5 +48,29 @@ def add_info_hook(ctx: DynamicClassDefContext) -> None:
ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, info))


def as_manager_hook(ctx: DynamicClassDefContext) -> None:
class_def = ClassDef(ctx.name, Block([]))
class_def.fullname = ctx.api.qualified_name(ctx.name)

info = TypeInfo(SymbolTable(), class_def, ctx.api.cur_mod_id)
class_def.info = info
assert isinstance(ctx.call.callee, MemberExpr)
assert isinstance(ctx.call.callee.expr, IndexExpr)
assert isinstance(ctx.call.callee.expr.analyzed, TypeApplication)
assert isinstance(ctx.call.callee.expr.analyzed.expr, NameExpr)

queryset_type_fullname = ctx.call.callee.expr.analyzed.expr.fullname
queryset_node = ctx.api.lookup_fully_qualified_or_none(queryset_type_fullname)
assert queryset_node is not None
queryset_info = queryset_node.node
assert isinstance(queryset_info, TypeInfo)
parameter_type = ctx.call.callee.expr.analyzed.types[0]

obj = ctx.api.named_type("builtins.object")
info.mro = [info, queryset_info, obj.type]
info.bases = [Instance(queryset_info, [parameter_type])]
ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, info))


def plugin(version: str) -> type[DynPlugin]:
return DynPlugin