Skip to content

Commit 717be59

Browse files
authored
Reorganize code a bit, add current directory to sys.path (#198)
* reorganize code a bit * add current directory to sys.path * remove PYTHONPATH mention from the docs * linting
1 parent b939bc9 commit 717be59

File tree

11 files changed

+277
-228
lines changed

11 files changed

+277
-228
lines changed

README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,6 @@ django_settings_module = mysettings
5252

5353
Where `mysettings` is a value of `DJANGO_SETTINGS_MODULE` (with or without quotes)
5454

55-
You might also need to explicitly tweak your `PYTHONPATH` the very same way `django` does it internally in case you have troubles with mypy / django plugin not finding your settings module. Try adding the root path of your project to your `PYTHONPATH` environment variable like so:
56-
57-
```bash
58-
PYTHONPATH=${PYTHONPATH}:${PWD}
59-
```
60-
6155
Current implementation uses Django runtime to extract models information, so it will crash, if your installed apps `models.py` is not correct. For this same reason, you cannot use `reveal_type` inside global scope of any Python file that will be executed for `django.setup()`.
6256

6357
In other words, if your `manage.py runserver` crashes, mypy will crash too.

mypy_django_plugin/django/context.py

Lines changed: 153 additions & 159 deletions
Large diffs are not rendered by default.

mypy_django_plugin/lib/fullnames.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@
3737
MIGRATION_CLASS_FULLNAME = 'django.db.migrations.migration.Migration'
3838
OPTIONS_CLASS_FULLNAME = 'django.db.models.options.Options'
3939
HTTPREQUEST_CLASS_FULLNAME = 'django.http.request.HttpRequest'
40+
41+
F_EXPRESSION_FULLNAME = 'django.db.models.expressions.F'

mypy_django_plugin/lib/helpers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,11 @@ def get_typechecker_api(ctx: Union[AttributeContext, MethodContext, FunctionCont
284284
def is_model_subclass_info(info: TypeInfo, django_context: 'DjangoContext') -> bool:
285285
return (info.fullname() in django_context.model_base_classes
286286
or info.has_base(fullnames.MODEL_CLASS_FULLNAME))
287+
288+
289+
def check_types_compatible(ctx: Union[FunctionContext, MethodContext],
290+
*, expected_type: MypyType, actual_type: MypyType, error_message: str) -> None:
291+
api = get_typechecker_api(ctx)
292+
api.check_subtype(actual_type, expected_type,
293+
ctx.context, error_message,
294+
'got', 'expected')

mypy_django_plugin/main.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
)
1212
from mypy.types import Type as MypyType
1313

14+
import mypy_django_plugin.transformers.orm_lookups
1415
from mypy_django_plugin.django.context import DjangoContext
1516
from mypy_django_plugin.lib import fullnames, helpers
1617
from mypy_django_plugin.transformers import (
@@ -148,13 +149,13 @@ def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
148149
# forward relations
149150
for field in self.django_context.get_model_fields(model_class):
150151
if isinstance(field, RelatedField):
151-
related_model_cls = self.django_context.fields_context.get_related_model_cls(field)
152+
related_model_cls = self.django_context.get_field_related_model_cls(field)
152153
related_model_module = related_model_cls.__module__
153154
if related_model_module != file.fullname():
154155
deps.add(self._new_dependency(related_model_module))
155156
# reverse relations
156157
for relation in model_class._meta.related_objects:
157-
related_model_cls = self.django_context.fields_context.get_related_model_cls(relation)
158+
related_model_cls = self.django_context.get_field_related_model_cls(relation)
158159
related_model_module = related_model_cls.__module__
159160
if related_model_module != file.fullname():
160161
deps.add(self._new_dependency(related_model_module))
@@ -210,7 +211,8 @@ def get_method_hook(self, fullname: str
210211
if class_fullname in manager_classes and method_name == 'create':
211212
return partial(init_create.redefine_and_typecheck_model_create, django_context=self.django_context)
212213
if class_fullname in manager_classes and method_name in {'filter', 'get', 'exclude'}:
213-
return partial(init_create.typecheck_queryset_filter, django_context=self.django_context)
214+
return partial(mypy_django_plugin.transformers.orm_lookups.typecheck_queryset_filter,
215+
django_context=self.django_context)
214216
return None
215217

216218
def get_base_class_hook(self, fullname: str

mypy_django_plugin/transformers/fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context
4444

4545
assert isinstance(current_field, RelatedField)
4646

47-
related_model_cls = django_context.fields_context.get_related_model_cls(current_field)
47+
related_model_cls = django_context.get_field_related_model_cls(current_field)
4848

4949
related_model = related_model_cls
5050
related_model_to_set = related_model_cls

mypy_django_plugin/transformers/init_create.py

Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from mypy.types import Type as MypyType
77

88
from mypy_django_plugin.django.context import DjangoContext
9-
from mypy_django_plugin.lib import fullnames, helpers
9+
from mypy_django_plugin.lib import helpers
1010

1111

1212
def get_actual_types(ctx: Union[MethodContext, FunctionContext],
@@ -30,12 +30,6 @@ def get_actual_types(ctx: Union[MethodContext, FunctionContext],
3030
return actual_types
3131

3232

33-
def check_types_compatible(ctx, *, expected_type, actual_type, error_message):
34-
ctx.api.check_subtype(actual_type, expected_type,
35-
ctx.context, error_message,
36-
'got', 'expected')
37-
38-
3933
def typecheck_model_method(ctx: Union[FunctionContext, MethodContext], django_context: DjangoContext,
4034
model_cls: Type[Model], method: str) -> MypyType:
4135
typechecker_api = helpers.get_typechecker_api(ctx)
@@ -48,11 +42,11 @@ def typecheck_model_method(ctx: Union[FunctionContext, MethodContext], django_co
4842
model_cls.__name__),
4943
ctx.context)
5044
continue
51-
check_types_compatible(ctx,
52-
expected_type=expected_types[actual_name],
53-
actual_type=actual_type,
54-
error_message='Incompatible type for "{}" of "{}"'.format(actual_name,
55-
model_cls.__name__))
45+
helpers.check_types_compatible(ctx,
46+
expected_type=expected_types[actual_name],
47+
actual_type=actual_type,
48+
error_message='Incompatible type for "{}" of "{}"'.format(actual_name,
49+
model_cls.__name__))
5650

5751
return ctx.default_return_type
5852

@@ -79,40 +73,3 @@ def redefine_and_typecheck_model_create(ctx: MethodContext, django_context: Djan
7973
return ctx.default_return_type
8074

8175
return typecheck_model_method(ctx, django_context, model_cls, 'create')
82-
83-
84-
def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
85-
lookup_kwargs = ctx.arg_names[1]
86-
provided_lookup_types = ctx.arg_types[1]
87-
88-
assert isinstance(ctx.type, Instance)
89-
90-
if not ctx.type.args or not isinstance(ctx.type.args[0], Instance):
91-
return ctx.default_return_type
92-
93-
model_cls_fullname = ctx.type.args[0].type.fullname()
94-
model_cls = django_context.get_model_class_by_fullname(model_cls_fullname)
95-
if model_cls is None:
96-
return ctx.default_return_type
97-
98-
for lookup_kwarg, provided_type in zip(lookup_kwargs, provided_lookup_types):
99-
if lookup_kwarg is None:
100-
continue
101-
# Combinables are not supported yet
102-
if (isinstance(provided_type, Instance)
103-
and provided_type.type.has_base('django.db.models.expressions.Combinable')):
104-
continue
105-
106-
lookup_type = django_context.lookups_context.resolve_lookup_expected_type(ctx, model_cls, lookup_kwarg)
107-
# Managers as provided_type is not supported yet
108-
if (isinstance(provided_type, Instance)
109-
and helpers.has_any_of_bases(provided_type.type, (fullnames.MANAGER_CLASS_FULLNAME,
110-
fullnames.QUERYSET_CLASS_FULLNAME))):
111-
return ctx.default_return_type
112-
113-
check_types_compatible(ctx,
114-
expected_type=lookup_type,
115-
actual_type=provided_type,
116-
error_message=f'Incompatible type for lookup {lookup_kwarg!r}:')
117-
118-
return ctx.default_return_type

mypy_django_plugin/transformers/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ class AddRelatedModelsId(ModelClassInitializer):
9999
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
100100
for field in model_cls._meta.get_fields():
101101
if isinstance(field, ForeignKey):
102-
related_model_cls = self.django_context.fields_context.get_related_model_cls(field)
102+
related_model_cls = self.django_context.get_field_related_model_cls(field)
103103
rel_primary_key_field = self.django_context.get_primary_key_field(related_model_cls)
104104
field_info = self.lookup_class_typeinfo_or_incomplete_defn_error(rel_primary_key_field.__class__)
105-
is_nullable = self.django_context.fields_context.get_field_nullability(field, None)
105+
is_nullable = self.django_context.get_field_nullability(field, None)
106106
set_type, get_type = get_field_descriptor_types(field_info, is_nullable)
107107
self.add_new_node_to_model_class(field.attname,
108108
Instance(field_info, [set_type, get_type]))
@@ -162,7 +162,7 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
162162
# no reverse accessor
163163
continue
164164

165-
related_model_cls = self.django_context.fields_context.get_related_model_cls(relation)
165+
related_model_cls = self.django_context.get_field_related_model_cls(relation)
166166
related_model_info = self.lookup_class_typeinfo_or_incomplete_defn_error(related_model_cls)
167167

168168
if isinstance(relation, OneToOneRel):
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from mypy.plugin import MethodContext
2+
from mypy.types import AnyType, Instance
3+
from mypy.types import Type as MypyType
4+
from mypy.types import TypeOfAny
5+
6+
from mypy_django_plugin.django.context import DjangoContext
7+
from mypy_django_plugin.lib import fullnames, helpers
8+
9+
10+
def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
11+
lookup_kwargs = ctx.arg_names[1]
12+
provided_lookup_types = ctx.arg_types[1]
13+
14+
assert isinstance(ctx.type, Instance)
15+
16+
if not ctx.type.args or not isinstance(ctx.type.args[0], Instance):
17+
return ctx.default_return_type
18+
19+
model_cls_fullname = ctx.type.args[0].type.fullname()
20+
model_cls = django_context.get_model_class_by_fullname(model_cls_fullname)
21+
if model_cls is None:
22+
return ctx.default_return_type
23+
24+
for lookup_kwarg, provided_type in zip(lookup_kwargs, provided_lookup_types):
25+
if lookup_kwarg is None:
26+
continue
27+
if (isinstance(provided_type, Instance)
28+
and provided_type.type.has_base('django.db.models.expressions.Combinable')):
29+
provided_type = resolve_combinable_type(provided_type, django_context)
30+
31+
lookup_type = django_context.resolve_lookup_expected_type(ctx, model_cls, lookup_kwarg)
32+
# Managers as provided_type is not supported yet
33+
if (isinstance(provided_type, Instance)
34+
and helpers.has_any_of_bases(provided_type.type, (fullnames.MANAGER_CLASS_FULLNAME,
35+
fullnames.QUERYSET_CLASS_FULLNAME))):
36+
return ctx.default_return_type
37+
38+
helpers.check_types_compatible(ctx,
39+
expected_type=lookup_type,
40+
actual_type=provided_type,
41+
error_message=f'Incompatible type for lookup {lookup_kwarg!r}:')
42+
43+
return ctx.default_return_type
44+
45+
46+
def resolve_combinable_type(combinable_type: Instance, django_context: DjangoContext) -> MypyType:
47+
if combinable_type.type.fullname() != fullnames.F_EXPRESSION_FULLNAME:
48+
# Combinables aside from F expressions are unsupported
49+
return AnyType(TypeOfAny.explicit)
50+
51+
return django_context.resolve_f_expression_type(combinable_type)

mypy_django_plugin/transformers/querysets.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,19 @@ def determine_proper_manager_type(ctx: FunctionContext) -> MypyType:
4040
def get_field_type_from_lookup(ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model],
4141
*, method: str, lookup: str) -> Optional[MypyType]:
4242
try:
43-
lookup_field = django_context.lookups_context.resolve_lookup_info_field(model_cls, lookup)
43+
lookup_field = django_context.resolve_lookup_into_field(model_cls, lookup)
4444
except FieldError as exc:
4545
ctx.api.fail(exc.args[0], ctx.context)
4646
return None
4747
except LookupsAreUnsupported:
4848
return AnyType(TypeOfAny.explicit)
4949

5050
if isinstance(lookup_field, RelatedField) and lookup_field.column == lookup:
51-
related_model_cls = django_context.fields_context.get_related_model_cls(lookup_field)
51+
related_model_cls = django_context.get_field_related_model_cls(lookup_field)
5252
lookup_field = django_context.get_primary_key_field(related_model_cls)
5353

54-
field_get_type = django_context.fields_context.get_field_get_type(helpers.get_typechecker_api(ctx),
55-
lookup_field, method=method)
54+
field_get_type = django_context.get_field_get_type(helpers.get_typechecker_api(ctx),
55+
lookup_field, method=method)
5656
return field_get_type
5757

5858

@@ -73,8 +73,8 @@ def get_values_list_row_type(ctx: MethodContext, django_context: DjangoContext,
7373
elif named:
7474
column_types: 'OrderedDict[str, MypyType]' = OrderedDict()
7575
for field in django_context.get_model_fields(model_cls):
76-
column_type = django_context.fields_context.get_field_get_type(typechecker_api, field,
77-
method='values_list')
76+
column_type = django_context.get_field_get_type(typechecker_api, field,
77+
method='values_list')
7878
column_types[field.attname] = column_type
7979
return helpers.make_oneoff_named_tuple(typechecker_api, 'Row', column_types)
8080
else:

0 commit comments

Comments
 (0)