Skip to content

Commit aeb435c

Browse files
authored
Disable monkeypatches, add dependencies via new hook (typeddjango#60)
* code cleanups, disable monkeypatches, move to add_additional_deps * disable incremental mode for tests * add pip-wheel-metadata * move some code from get_base_hook to get_attribute_hook to reduce dependencies * simplify values/values_list tests and code * disable cache for some tests failing with incremental mode * enable incremental mode for tests typechecking * pin mypy version * fix tests * lint * fix internal crashes
1 parent 13d1901 commit aeb435c

24 files changed

+818
-624
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ out/
77
.mypy_cache/
88
django-sources
99
build/
10-
dist/
10+
dist/
11+
pip-wheel-metadata/

django-stubs/conf/__init__.pyi

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ from typing import Any
22

33
from django.utils.functional import LazyObject
44

5+
# explicit dependency on standard settings to make it loaded
6+
from . import global_settings
7+
58
ENVIRONMENT_VARIABLE: str = ...
69

710
# required for plugin to be able to distinguish this specific instance of LazySettings from others
8-
class _DjangoConfLazyObject(LazyObject): ...
11+
class _DjangoConfLazyObject(LazyObject):
12+
def __getattr__(self, item: Any) -> Any: ...
913

1014
class LazySettings(_DjangoConfLazyObject):
1115
configured: bool

django-stubs/conf/global_settings.pyi

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@ by the DJANGO_SETTINGS_MODULE environment variable.
55

66
# This is defined here as a do-nothing function because we can't import
77
# django.utils.translation -- that module depends on the settings.
8-
from typing import Any, Dict, List, Optional, Pattern, Tuple, Protocol, Union, Callable, TYPE_CHECKING, Sequence
8+
from typing import Any, Dict, List, Optional, Pattern, Protocol, Sequence, Tuple, Union
99

1010
####################
1111
# CORE #
1212
####################
13-
if TYPE_CHECKING:
14-
from django.db.models.base import Model
15-
1613
DEBUG: bool = ...
1714

1815
# Whether the framework should propagate raw exceptions rather than catching
@@ -153,7 +150,7 @@ FORCE_SCRIPT_NAME = None
153150
# ]
154151
DISALLOWED_USER_AGENTS: List[Pattern] = ...
155152

156-
ABSOLUTE_URL_OVERRIDES: Dict[str, Callable[[Model], str]] = ...
153+
ABSOLUTE_URL_OVERRIDES: Dict[str, Any] = ...
157154

158155
# List of compiled regular expression objects representing URLs that need not
159156
# be reported by BrokenLinkEmailsMiddleware. Here are a few examples:

mypy_django_plugin/helpers.py

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
from mypy.mro import calculate_mro
66
from mypy.nodes import (
7-
AssignmentStmt, ClassDef, Expression, ImportedName, Lvalue, MypyFile, NameExpr, SymbolNode, TypeInfo,
8-
SymbolTable, SymbolTableNode, Block, GDEF, MDEF, Var)
9-
from mypy.plugin import FunctionContext, MethodContext
7+
GDEF, MDEF, AssignmentStmt, Block, CallExpr, ClassDef, Expression, ImportedName, Lvalue, MypyFile, NameExpr,
8+
SymbolNode, SymbolTable, SymbolTableNode, TypeInfo, Var,
9+
)
10+
from mypy.plugin import CheckerPluginInterface, FunctionContext, MethodContext
1011
from mypy.types import (
11-
AnyType, Instance, NoneTyp, Type, TypeOfAny, TypeVarType, UnionType,
12-
TupleType, TypedDictType)
12+
AnyType, Instance, NoneTyp, TupleType, Type, TypedDictType, TypeOfAny, TypeVarType, UnionType,
13+
)
1314

1415
if typing.TYPE_CHECKING:
1516
from mypy.checker import TypeChecker
@@ -216,6 +217,7 @@ def extract_field_setter_type(tp: Instance) -> Optional[Type]:
216217

217218

218219
def extract_field_getter_type(tp: Type) -> Optional[Type]:
220+
""" Extract return type of __get__ of subclass of Field"""
219221
if not isinstance(tp, Instance):
220222
return None
221223
if tp.type.has_base(FIELD_FULLNAME):
@@ -226,13 +228,12 @@ def extract_field_getter_type(tp: Type) -> Optional[Type]:
226228
return None
227229

228230

229-
def get_django_metadata(model: TypeInfo) -> Dict[str, typing.Any]:
230-
return model.metadata.setdefault('django', {})
231+
def get_django_metadata(model_info: TypeInfo) -> Dict[str, typing.Any]:
232+
return model_info.metadata.setdefault('django', {})
231233

232234

233235
def get_related_field_primary_key_names(base_model: TypeInfo) -> typing.List[str]:
234-
django_metadata = get_django_metadata(base_model)
235-
return django_metadata.setdefault('related_field_primary_keys', [])
236+
return get_django_metadata(base_model).setdefault('related_field_primary_keys', [])
236237

237238

238239
def get_fields_metadata(model: TypeInfo) -> Dict[str, typing.Any]:
@@ -243,6 +244,10 @@ def get_lookups_metadata(model: TypeInfo) -> Dict[str, typing.Any]:
243244
return get_django_metadata(model).setdefault('lookups', {})
244245

245246

247+
def get_related_managers_metadata(model: TypeInfo) -> Dict[str, typing.Any]:
248+
return get_django_metadata(model).setdefault('related_managers', {})
249+
250+
246251
def extract_explicit_set_type_of_model_primary_key(model: TypeInfo) -> Optional[Type]:
247252
"""
248253
If field with primary_key=True is set on the model, extract its __set__ type.
@@ -310,7 +315,7 @@ def is_field_nullable(model: TypeInfo, field_name: str) -> bool:
310315
return get_fields_metadata(model).get(field_name, {}).get('null', False)
311316

312317

313-
def is_foreign_key(t: Type) -> bool:
318+
def is_foreign_key_like(t: Type) -> bool:
314319
if not isinstance(t, Instance):
315320
return False
316321
return has_any_of_bases(t.type, (FOREIGN_KEY_FULLNAME, ONETOONE_FIELD_FULLNAME))
@@ -366,13 +371,14 @@ def make_named_tuple(api: 'TypeChecker', fields: 'OrderedDict[str, Type]', name:
366371
return TupleType(list(fields.values()), fallback=fallback)
367372

368373

369-
def make_typeddict(api: 'TypeChecker', fields: 'OrderedDict[str, Type]', required_keys: typing.Set[str]) -> Type:
374+
def make_typeddict(api: CheckerPluginInterface, fields: 'OrderedDict[str, Type]',
375+
required_keys: typing.Set[str]) -> TypedDictType:
370376
object_type = api.named_generic_type('mypy_extensions._TypedDict', [])
371377
typed_dict_type = TypedDictType(fields, required_keys=required_keys, fallback=object_type)
372378
return typed_dict_type
373379

374380

375-
def make_tuple(api: 'TypeChecker', fields: typing.List[Type]) -> Type:
381+
def make_tuple(api: 'TypeChecker', fields: typing.List[Type]) -> TupleType:
376382
implicit_any = AnyType(TypeOfAny.special_form)
377383
fallback = api.named_generic_type('builtins.tuple', [implicit_any])
378384
return TupleType(fields, fallback=fallback)
@@ -386,3 +392,52 @@ def get_private_descriptor_type(type_info: TypeInfo, private_field_name: str, is
386392
descriptor_type = make_optional(descriptor_type)
387393
return descriptor_type
388394
return AnyType(TypeOfAny.unannotated)
395+
396+
397+
def iter_over_classdefs(module_file: MypyFile) -> typing.Iterator[ClassDef]:
398+
for defn in module_file.defs:
399+
if isinstance(defn, ClassDef):
400+
yield defn
401+
402+
403+
def iter_call_assignments(klass: ClassDef) -> typing.Iterator[typing.Tuple[Lvalue, CallExpr]]:
404+
for lvalue, rvalue in iter_over_assignments(klass):
405+
if isinstance(rvalue, CallExpr):
406+
yield lvalue, rvalue
407+
408+
409+
def get_related_manager_type_from_metadata(model_info: TypeInfo, related_manager_name: str,
410+
api: CheckerPluginInterface) -> Optional[Instance]:
411+
related_manager_metadata = get_related_managers_metadata(model_info)
412+
if not related_manager_metadata:
413+
return None
414+
415+
if related_manager_name not in related_manager_metadata:
416+
return None
417+
418+
manager_class_name = related_manager_metadata[related_manager_name]['manager']
419+
of = related_manager_metadata[related_manager_name]['of']
420+
of_types = []
421+
for of_type_name in of:
422+
if of_type_name == 'any':
423+
of_types.append(AnyType(TypeOfAny.implementation_artifact))
424+
else:
425+
try:
426+
of_type = api.named_generic_type(of_type_name, [])
427+
except AssertionError:
428+
# Internal error: attempted lookup of unknown name
429+
of_type = AnyType(TypeOfAny.implementation_artifact)
430+
431+
of_types.append(of_type)
432+
433+
return api.named_generic_type(manager_class_name, of_types)
434+
435+
436+
def get_primary_key_field_name(model_info: TypeInfo) -> Optional[str]:
437+
for base in model_info.mro:
438+
fields = get_fields_metadata(base)
439+
for field_name, field_props in fields.items():
440+
is_primary_key = field_props.get('primary_key', False)
441+
if is_primary_key:
442+
return field_name
443+
return None

mypy_django_plugin/lookups.py

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import dataclasses
2-
from typing import Union, List
1+
from typing import List, Union
32

3+
import dataclasses
44
from mypy.nodes import TypeInfo
55
from mypy.plugin import CheckerPluginInterface
6-
from mypy.types import Type, Instance
6+
from mypy.types import Instance, Type
77

88
from mypy_django_plugin import helpers
99

@@ -57,20 +57,24 @@ def resolve_lookup(api: CheckerPluginInterface, model_type_info: TypeInfo, looku
5757
return nodes
5858

5959

60+
def resolve_model_pk_lookup(api: CheckerPluginInterface, model_type_info: TypeInfo) -> LookupNode:
61+
# Primary keys are special-cased
62+
primary_key_type = helpers.extract_primary_key_type_for_get(model_type_info)
63+
if primary_key_type:
64+
return FieldNode(primary_key_type)
65+
else:
66+
# No PK, use the get type for AutoField as PK type.
67+
autofield_info = api.lookup_typeinfo('django.db.models.fields.AutoField')
68+
pk_type = helpers.get_private_descriptor_type(autofield_info, '_pyi_private_get_type',
69+
is_nullable=False)
70+
return FieldNode(pk_type)
71+
72+
6073
def resolve_model_lookup(api: CheckerPluginInterface, model_type_info: TypeInfo,
6174
lookup: str) -> LookupNode:
6275
"""Resolve a lookup on the given model."""
6376
if lookup == 'pk':
64-
# Primary keys are special-cased
65-
primary_key_type = helpers.extract_primary_key_type_for_get(model_type_info)
66-
if primary_key_type:
67-
return FieldNode(primary_key_type)
68-
else:
69-
# No PK, use the get type for AutoField as PK type.
70-
autofield_info = api.lookup_typeinfo('django.db.models.fields.AutoField')
71-
pk_type = helpers.get_private_descriptor_type(autofield_info, '_pyi_private_get_type',
72-
is_nullable=False)
73-
return FieldNode(pk_type)
77+
return resolve_model_pk_lookup(api, model_type_info)
7478

7579
field_name = get_actual_field_name_for_lookup_field(lookup, model_type_info)
7680

@@ -82,7 +86,7 @@ def resolve_model_lookup(api: CheckerPluginInterface, model_type_info: TypeInfo,
8286
if field_name.endswith('_id'):
8387
field_name_without_id = field_name.rstrip('_id')
8488
foreign_key_field = model_type_info.get(field_name_without_id)
85-
if foreign_key_field is not None and helpers.is_foreign_key(foreign_key_field.type):
89+
if foreign_key_field is not None and helpers.is_foreign_key_like(foreign_key_field.type):
8690
# Hack: If field ends with '_id' and there is a model field without the '_id' suffix, then use that field.
8791
field_node = foreign_key_field
8892
field_name = field_name_without_id
@@ -92,10 +96,23 @@ def resolve_model_lookup(api: CheckerPluginInterface, model_type_info: TypeInfo,
9296
raise LookupException(
9397
f'When resolving lookup "{lookup}", could not determine type for {model_type_info.name()}.{field_name}')
9498

95-
if helpers.is_foreign_key(field_node_type):
99+
if field_node_type.type.fullname() == 'builtins.object':
100+
# could be related manager
101+
related_manager_type = helpers.get_related_manager_type_from_metadata(model_type_info, field_name, api)
102+
if related_manager_type:
103+
model_arg = related_manager_type.args[0]
104+
if not isinstance(model_arg, Instance):
105+
raise LookupException(
106+
f'When resolving lookup "{lookup}", could not determine type '
107+
f'for {model_type_info.name()}.{field_name}')
108+
109+
return RelatedModelNode(typ=model_arg, is_nullable=False)
110+
111+
if helpers.is_foreign_key_like(field_node_type):
96112
field_type = helpers.extract_field_getter_type(field_node_type)
97113
is_nullable = helpers.is_optional(field_type)
98114
if is_nullable:
115+
# type is always non-optional
99116
field_type = helpers.make_required(field_type)
100117

101118
if isinstance(field_type, Instance):
@@ -104,24 +121,16 @@ def resolve_model_lookup(api: CheckerPluginInterface, model_type_info: TypeInfo,
104121
raise LookupException(f"Not an instance for field {field_type} lookup {lookup}")
105122

106123
field_type = helpers.extract_field_getter_type(field_node_type)
107-
108124
if field_type:
109125
return FieldNode(typ=field_type)
110-
else:
111-
# Not a Field
112-
if field_name == 'id':
113-
# If no 'id' field was fouond, use an int
114-
return FieldNode(api.named_generic_type('builtins.int', []))
115-
116-
related_manager_arg = None
117-
if field_node_type.type.has_base(helpers.RELATED_MANAGER_CLASS_FULLNAME):
118-
related_manager_arg = field_node_type.args[0]
119-
120-
if related_manager_arg is not None:
121-
# Reverse relation
122-
return RelatedModelNode(typ=related_manager_arg, is_nullable=True)
123-
raise LookupException(
124-
f'When resolving lookup "{lookup}", could not determine type for {model_type_info.name()}.{field_name}')
126+
127+
# Not a Field
128+
if field_name == 'id':
129+
# If no 'id' field was found, use an int
130+
return FieldNode(api.named_generic_type('builtins.int', []))
131+
132+
raise LookupException(
133+
f'When resolving lookup {lookup!r}, could not determine type for {model_type_info.name()}.{field_name}')
125134

126135

127136
def get_actual_field_name_for_lookup_field(lookup: str, model_type_info: TypeInfo) -> str:

0 commit comments

Comments
 (0)