Reparametrize managers without explicit type parameters (#1169)

* Reparametrize managers without explicit type parameters

This extracts the reparametrization logic from #1030 in addition to
removing the codepath that copied methods from querysets to managers.
That code path seems to not be needed with this change.

* Use typevars from parent instead of base

* Use typevars from parent manager instead of base manager

This removes warnings when subclassing from something other than the
base manager class, where the typevar has been restricted.

* Remove unused imports

* Fix failed test

* Only reparametrize if generics are omitted

* Fix docstring

* Add test with disallow_any_generics=True

* Add an FAQ section and document disallow_any_generics behaviour
This commit is contained in:
Sigurd Ljødal
2022-10-03 19:36:45 +02:00
committed by GitHub
parent 946274bed8
commit dde0f2f876
10 changed files with 196 additions and 164 deletions
+57 -1
View File
@@ -15,7 +15,7 @@ from mypy.nodes import (
TypeInfo,
Var,
)
from mypy.plugin import AttributeContext, DynamicClassDefContext
from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext
from mypy.semanal import SemanticAnalyzer
from mypy.semanal_shared import has_placeholder
from mypy.types import AnyType, CallableType, Instance, ProperType
@@ -466,3 +466,59 @@ def create_new_manager_class_from_as_manager_method(ctx: DynamicClassDefContext)
# Note that the generated manager type is always inserted at module level
SymbolTableNode(GDEF, new_manager_info, plugin_generated=True),
)
def reparametrize_any_manager_hook(ctx: ClassDefContext) -> None:
"""
Add implicit generics to manager classes that are defined without generic.
Eg.
class MyManager(models.Manager): ...
is interpreted as:
_T = TypeVar('_T', covariant=True)
class MyManager(models.Manager[_T]): ...
Note that this does not happen if mypy is run with disallow_any_generics = True,
as not specifying the generic type is then considered an error.
"""
manager = ctx.api.lookup_fully_qualified_or_none(ctx.cls.fullname)
if manager is None or manager.node is None:
return
assert isinstance(manager.node, TypeInfo)
if manager.node.type_vars:
# We've already been here
return
parent_manager = next(
(base for base in manager.node.bases if base.type.has_base(fullnames.BASE_MANAGER_CLASS_FULLNAME)),
None,
)
if parent_manager is None:
return
is_missing_params = (
len(parent_manager.args) == 1
and isinstance(parent_manager.args[0], AnyType)
and parent_manager.args[0].type_of_any is TypeOfAny.from_omitted_generics
)
if not is_missing_params:
return
type_vars = tuple(parent_manager.type.defn.type_vars)
# If we end up with placeholders we need to defer so the placeholders are
# resolved in a future iteration
if any(has_placeholder(type_var) for type_var in type_vars):
if not ctx.api.final_iteration:
ctx.api.defer()
else:
return
parent_manager.args = type_vars
manager.node.defn.type_vars = list(type_vars)
manager.node.add_type_vars()
+5 -60
View File
@@ -5,7 +5,7 @@ from django.db.models.fields import DateField, DateTimeField, Field
from django.db.models.fields.related import ForeignKey
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
from mypy.checker import TypeChecker
from mypy.nodes import ARG_STAR2, Argument, AssignmentStmt, CallExpr, Context, FuncDef, NameExpr, TypeInfo, Var
from mypy.nodes import ARG_STAR2, Argument, AssignmentStmt, CallExpr, Context, NameExpr, TypeInfo, Var
from mypy.plugin import AnalyzeTypeContext, AttributeContext, CheckerPluginInterface, ClassDefContext
from mypy.plugins import common
from mypy.semanal import SemanticAnalyzer
@@ -282,45 +282,6 @@ class AddManagers(ModelClassInitializer):
def is_any_parametrized_manager(self, typ: Instance) -> bool:
return typ.type.fullname in fullnames.MANAGER_CLASSES and isinstance(typ.args[0], AnyType)
def create_new_model_parametrized_manager(self, name: str, base_manager_info: TypeInfo) -> Instance:
bases = []
for original_base in base_manager_info.bases:
if self.is_any_parametrized_manager(original_base):
original_base = helpers.reparametrize_instance(original_base, [Instance(self.model_classdef.info, [])])
bases.append(original_base)
# TODO: This adds the manager to the module, even if we end up
# deferring. That can be avoided by not adding it to the module first,
# but rather waiting until we know we won't defer
new_manager_info = self.add_new_class_for_current_module(name, bases)
# copy fields to a new manager
custom_manager_type = Instance(new_manager_info, [Instance(self.model_classdef.info, [])])
for name, sym in base_manager_info.names.items():
# replace self type with new class, if copying method
if isinstance(sym.node, FuncDef):
copied_method = helpers.copy_method_to_another_class(
api=self.api,
cls=new_manager_info.defn,
self_type=custom_manager_type,
new_method_name=name,
method_node=sym.node,
original_module_name=base_manager_info.module_name,
)
if not copied_method and not self.api.final_iteration:
raise helpers.IncompleteDefnException()
continue
new_sym = sym.copy()
if isinstance(new_sym.node, Var):
new_var = Var(name, type=sym.type)
new_var.info = new_manager_info
new_var._fullname = new_manager_info.fullname + "." + name
new_sym.node = new_var
new_manager_info.names[name] = new_sym
return custom_manager_type
def lookup_manager(self, fullname: str, manager: "Manager[Any]") -> Optional[TypeInfo]:
manager_info = self.lookup_typeinfo(fullname)
if manager_info is None:
@@ -354,7 +315,8 @@ class AddManagers(ModelClassInitializer):
# Manager is already typed -> do nothing unless it's a dynamically generated manager
self.reparametrize_dynamically_created_manager(manager_name, manager_info)
continue
elif manager_info is None:
if manager_info is None:
# We couldn't find a manager type, see if we should create one
manager_info = self.create_manager_from_from_queryset(manager_name)
@@ -362,25 +324,8 @@ class AddManagers(ModelClassInitializer):
incomplete_manager_defs.add(manager_name)
continue
if manager_name not in self.model_classdef.info.names or self.is_manager_dynamically_generated(
manager_info
):
manager_type = Instance(manager_info, [Instance(self.model_classdef.info, [])])
self.add_new_node_to_model_class(manager_name, manager_type)
elif self.has_any_parametrized_manager_as_base(manager_info):
# Ending up here could for instance be due to having a custom _Manager_
# that is not built from a custom QuerySet. Another example is a
# related manager.
manager_class_name = manager.__class__.__name__
custom_model_manager_name = manager.model.__name__ + "_" + manager_class_name
try:
manager_type = self.create_new_model_parametrized_manager(
custom_model_manager_name, base_manager_info=manager_info
)
except helpers.IncompleteDefnException:
continue
self.add_new_node_to_model_class(manager_name, manager_type)
manager_type = Instance(manager_info, [Instance(self.model_classdef.info, [])])
self.add_new_node_to_model_class(manager_name, manager_type)
if incomplete_manager_defs:
if not self.api.final_iteration: