File: context.py

package info (click to toggle)
python-django-stubs 5.2.9-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,832 kB
  • sloc: python: 5,185; makefile: 15; sh: 8
file content (525 lines) | stat: -rw-r--r-- 23,203 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
import os
import sys
from collections import defaultdict
from collections.abc import Iterable, Iterator, Mapping, Sequence
from contextlib import contextmanager
from functools import cached_property
from typing import TYPE_CHECKING, Any, Literal, Union

from django.core.exceptions import FieldDoesNotExist, FieldError
from django.db import models
from django.db.models.base import Model
from django.db.models.expressions import Expression
from django.db.models.fields import AutoField, CharField, Field
from django.db.models.fields.related import ForeignKey, RelatedField
from django.db.models.fields.reverse_related import ForeignObjectRel
from django.db.models.lookups import Exact
from django.db.models.sql.query import Query
from mypy.checker import TypeChecker
from mypy.nodes import TypeInfo
from mypy.plugin import MethodContext
from mypy.typeanal import make_optional_type
from mypy.types import AnyType, Instance, ProperType, TypeOfAny, UnionType, get_proper_type
from mypy.types import Type as MypyType

from mypy_django_plugin.exceptions import UnregisteredModelError
from mypy_django_plugin.lib import fullnames, helpers

# This import fails when `psycopg2` is not installed, avoid crashing the plugin.
try:
    from django.contrib.postgres.fields import ArrayField
except ImportError:

    class ArrayField:  # type: ignore[no-redef]
        pass


if TYPE_CHECKING:
    from django.apps.registry import Apps
    from django.conf import LazySettings
    from django.contrib.contenttypes.fields import GenericForeignKey


@contextmanager
def temp_environ() -> Iterator[None]:
    """Allow the ability to set os.environ temporarily"""
    environ = dict(os.environ)
    try:
        yield
    finally:
        os.environ.clear()
        os.environ.update(environ)


def initialize_django(settings_module: str) -> tuple["Apps", "LazySettings"]:
    with temp_environ():
        os.environ["DJANGO_SETTINGS_MODULE"] = settings_module

        # add current directory to sys.path
        sys.path.append(os.getcwd())

        from django.apps import apps
        from django.conf import settings

        apps.get_swappable_settings_name.cache_clear()  # type: ignore[attr-defined]
        apps.clear_cache()

        if not settings.configured:
            settings._setup()  # type: ignore[misc]
        apps.populate(settings.INSTALLED_APPS)

    assert apps.apps_ready, "Apps are not ready"
    assert settings.configured, "Settings are not configured"

    return apps, settings


class LookupsAreUnsupported(Exception):
    pass


def get_field_type_from_model_type_info(info: TypeInfo | None, field_name: str) -> Instance | None:
    if info is None:
        return None
    field_node = info.get(field_name)
    if field_node is None:
        return None
    field_type = get_proper_type(field_node.type)
    if not isinstance(field_type, Instance):
        return None
    # Field declares a set and a get type arg. Fallback to `None` when we can't find any args
    if len(field_type.args) != 2:
        return None
    return field_type


def _get_field_set_type_from_model_type_info(info: TypeInfo | None, field_name: str) -> MypyType | None:
    field_type = get_field_type_from_model_type_info(info, field_name)
    if field_type is not None:
        return field_type.args[0]
    return None


def _get_field_get_type_from_model_type_info(info: TypeInfo | None, field_name: str) -> MypyType | None:
    field_type = get_field_type_from_model_type_info(info, field_name)
    if field_type is not None:
        return field_type.args[1]
    return None


class DjangoContext:
    def __init__(self, django_settings_module: str) -> None:
        self.django_settings_module = django_settings_module

        apps, settings = initialize_django(self.django_settings_module)
        self.apps_registry = apps
        self.settings = settings

    @cached_property
    def model_modules(self) -> dict[str, dict[str, type[Model]]]:
        """All modules that contain Django models."""
        modules: dict[str, dict[str, type[Model]]] = defaultdict(dict)
        for concrete_model_cls in self.apps_registry.get_models(include_auto_created=True, include_swapped=True):
            modules[concrete_model_cls.__module__][concrete_model_cls.__name__] = concrete_model_cls
            # collect abstract=True models
            for model_cls in concrete_model_cls.mro()[1:]:
                if issubclass(model_cls, Model) and hasattr(model_cls, "_meta") and model_cls._meta.abstract:
                    modules[model_cls.__module__][model_cls.__name__] = model_cls
        return modules

    def get_model_class_by_fullname(self, fullname: str) -> type[Model] | None:
        """Returns None if Model is abstract"""
        module, _, model_cls_name = fullname.rpartition(".")
        return self.model_modules.get(module, {}).get(model_cls_name)

    def get_model_fields(self, model_cls: type[Model]) -> Iterator["Field[Any, Any]"]:
        for field in model_cls._meta.get_fields():
            if isinstance(field, Field):
                yield field

    def get_model_foreign_keys(self, model_cls: type[Model]) -> Iterator["ForeignKey[Any, Any]"]:
        for field in model_cls._meta.get_fields():
            if isinstance(field, ForeignKey):
                yield field

    def get_model_related_fields(self, model_cls: type[Model]) -> Iterator["RelatedField[Any, Any]"]:
        """Get model forward relations"""
        for field in model_cls._meta.get_fields():
            if isinstance(field, RelatedField):
                yield field

    def get_model_relations(self, model_cls: type[Model]) -> Iterator[ForeignObjectRel]:
        """Get model reverse relations"""
        for field in model_cls._meta.get_fields():
            if isinstance(field, ForeignObjectRel):
                yield field

    def get_field_lookup_exact_type(
        self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel]
    ) -> MypyType:
        if isinstance(field, RelatedField | ForeignObjectRel):
            related_model_cls = self.get_field_related_model_cls(field)
            rel_model_info = helpers.lookup_class_typeinfo(api, related_model_cls)
            if rel_model_info is None:
                return AnyType(TypeOfAny.explicit)

            primary_key_field = self.get_primary_key_field(related_model_cls)
            primary_key_type = self.get_field_get_type(api, rel_model_info, primary_key_field, method="init")

            model_and_primary_key_type = UnionType.make_union([Instance(rel_model_info, []), primary_key_type])
            return make_optional_type(model_and_primary_key_type)

        field_info = helpers.lookup_class_typeinfo(api, field.__class__)
        if field_info is None:
            return AnyType(TypeOfAny.explicit)
        return helpers.get_private_descriptor_type(field_info, "_pyi_lookup_exact_type", is_nullable=field.null)

    def get_related_target_field(
        self, related_model_cls: type[Model], field: "ForeignKey[Any, Any]"
    ) -> "Field[Any, Any] | None":
        # ForeignKey only supports one `to_fields` item (ForeignObject supports many)
        assert len(field.to_fields) == 1
        to_field_name = field.to_fields[0]
        if to_field_name:
            rel_field = related_model_cls._meta.get_field(to_field_name)
            if not isinstance(rel_field, Field):
                return None  # Not supported
            return rel_field
        return self.get_primary_key_field(related_model_cls)

    def get_primary_key_field(self, model_cls: type[Model]) -> "Field[Any, Any]":
        for field in model_cls._meta.get_fields():
            if isinstance(field, Field):
                if field.primary_key:
                    return field
        raise ValueError("No primary key defined")

    def get_expected_types(self, api: TypeChecker, model_cls: type[Model], *, method: str) -> dict[str, MypyType]:
        contenttypes_in_apps = self.apps_registry.is_installed("django.contrib.contenttypes")

        expected_types = {}
        # add pk if not abstract=True
        if not model_cls._meta.abstract:
            primary_key_field = self.get_primary_key_field(model_cls)
            field_set_type = self.get_field_set_type(api, primary_key_field, method=method)
            expected_types["pk"] = field_set_type

        model_info = helpers.lookup_class_typeinfo(api, model_cls)
        for field in model_cls._meta.get_fields():
            if isinstance(field, Field):
                field_name = field.attname
                # Can not determine target_field for recursive relationship when model is abstract
                if field.related_model == "self" and model_cls._meta.abstract:
                    continue
                # Try to retrieve set type from a model's TypeInfo object and fallback to retrieving it manually
                # from django-stubs own declaration. This is to align with the setter types declared for
                # assignment.
                field_set_type = _get_field_set_type_from_model_type_info(
                    model_info, field_name
                ) or self.get_field_set_type(api, field, method=method)
                expected_types[field_name] = field_set_type

                if isinstance(field, ForeignKey):
                    field_name = field.name
                    foreign_key_info = helpers.lookup_class_typeinfo(api, field.__class__)
                    if foreign_key_info is None:
                        # maybe there's no type annotation for the field
                        expected_types[field_name] = AnyType(TypeOfAny.unannotated)
                        continue

                    try:
                        related_model = self.get_field_related_model_cls(field)
                    except UnregisteredModelError:
                        # Recognise the field but don't say anything about its type..
                        expected_types[field_name] = AnyType(TypeOfAny.from_error)
                        continue

                    if related_model._meta.proxy_for_model is not None:
                        related_model = related_model._meta.proxy_for_model

                    related_model_info = helpers.lookup_class_typeinfo(api, related_model)
                    if related_model_info is None:
                        expected_types[field_name] = AnyType(TypeOfAny.unannotated)
                        continue

                    is_nullable = self.get_field_nullability(field, method)
                    foreign_key_set_type = helpers.get_private_descriptor_type(
                        foreign_key_info, "_pyi_private_set_type", is_nullable=is_nullable
                    )
                    model_set_type = helpers.convert_any_to_type(foreign_key_set_type, Instance(related_model_info, []))

                    expected_types[field_name] = model_set_type

            elif contenttypes_in_apps:
                from django.contrib.contenttypes.fields import GenericForeignKey

                if isinstance(field, GenericForeignKey):
                    # it's generic, so cannot set specific model
                    field_name = field.name
                    gfk_info = helpers.lookup_class_typeinfo(api, field.__class__)
                    if gfk_info is None:
                        gfk_set_type: MypyType = AnyType(TypeOfAny.unannotated)
                    else:
                        gfk_set_type = helpers.get_private_descriptor_type(
                            gfk_info, "_pyi_private_set_type", is_nullable=True
                        )
                    expected_types[field_name] = gfk_set_type

        return expected_types

    @cached_property
    def all_registered_model_classes(self) -> set[type[models.Model]]:
        model_classes = self.apps_registry.get_models()

        all_model_bases = set()
        for model_cls in model_classes:
            for base_cls in model_cls.mro():
                if issubclass(base_cls, models.Model):
                    all_model_bases.add(base_cls)

        return all_model_bases

    @cached_property
    def model_class_fullnames_by_label(self) -> Mapping[str, str]:
        return {
            klass._meta.label: helpers.get_class_fullname(klass)
            for klass in self.all_registered_model_classes
            if klass is not models.Model
        }

    def get_field_nullability(self, field: Union["Field[Any, Any]", ForeignObjectRel], method: str | None) -> bool:
        if method in ("values", "values_list"):
            return field.null

        nullable = field.null
        if not nullable and isinstance(field, CharField) and field.blank:
            return True
        if method == "__init__":
            if (isinstance(field, Field) and field.primary_key) or isinstance(field, ForeignKey):
                return True
        if method == "create":
            if isinstance(field, AutoField):
                return True
        if isinstance(field, Field) and field.has_default():
            return True
        return nullable

    def get_field_set_type(
        self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel], *, method: str
    ) -> MypyType:
        """Get a type of __set__ for this specific Django field."""
        target_field = field
        if isinstance(field, ForeignKey):
            try:
                # We gotta be careful for exceptions when we're triggering '__get__'.
                # Related model could very well be unresolvable
                target_field = field.target_field
            except ValueError:
                return AnyType(TypeOfAny.from_error)

        field_info = helpers.lookup_class_typeinfo(api, target_field.__class__)
        if field_info is None:
            return AnyType(TypeOfAny.from_error)

        field_set_type = helpers.get_private_descriptor_type(
            field_info, "_pyi_private_set_type", is_nullable=self.get_field_nullability(field, method)
        )
        if isinstance(target_field, ArrayField):
            argument_field_type = self.get_field_set_type(api, target_field.base_field, method=method)
            field_set_type = helpers.convert_any_to_type(field_set_type, argument_field_type)
        return field_set_type

    def get_field_get_type(
        self,
        api: TypeChecker,
        model_info: TypeInfo | None,
        field: Union["Field[Any, Any]", ForeignObjectRel],
        *,
        method: str,
    ) -> MypyType:
        """Get a type of __get__ for this specific Django field."""
        if isinstance(field, Field):
            get_type = _get_field_get_type_from_model_type_info(model_info, field.attname)
            if get_type is not None:
                return get_type

        field_info = helpers.lookup_class_typeinfo(api, field.__class__)
        if field_info is None:
            return AnyType(TypeOfAny.unannotated)

        is_nullable = self.get_field_nullability(field, method)
        if isinstance(field, RelatedField):
            related_model_cls = self.get_field_related_model_cls(field)
            rel_model_info = helpers.lookup_class_typeinfo(api, related_model_cls)

            if method in ("values", "values_list"):
                primary_key_field = self.get_primary_key_field(related_model_cls)
                return self.get_field_get_type(api, rel_model_info, primary_key_field, method=method)

            model_info = helpers.lookup_class_typeinfo(api, related_model_cls)
            if model_info is None:
                return AnyType(TypeOfAny.unannotated)

            return Instance(model_info, [])
        return helpers.get_private_descriptor_type(field_info, "_pyi_private_get_type", is_nullable=is_nullable)

    def get_field_related_model_cls(self, field: Union["RelatedField[Any, Any]", ForeignObjectRel]) -> type[Model]:
        if isinstance(field, RelatedField):
            related_model_cls = field.remote_field.model
        else:
            related_model_cls = field.field.model

        if related_model_cls is None:
            raise UnregisteredModelError

        if isinstance(related_model_cls, str):
            if related_model_cls == "self":  # type: ignore[unreachable]
                # same model
                related_model_cls = field.model
            elif "." not in related_model_cls:
                # same file model
                related_model_fullname = f"{field.model.__module__}.{related_model_cls}"
                related_model_cls = self.get_model_class_by_fullname(related_model_fullname)
                if related_model_cls is None:
                    raise UnregisteredModelError
            else:
                try:
                    related_model_cls = self.apps_registry.get_model(related_model_cls)
                except LookupError as e:
                    raise UnregisteredModelError from e

        return related_model_cls

    def _resolve_field_from_parts(
        self, field_parts: Iterable[str], model_cls: type[Model]
    ) -> tuple[Union["Field[Any, Any]", ForeignObjectRel], type[Model]]:
        currently_observed_model = model_cls
        field: Field[Any, Any] | ForeignObjectRel | GenericForeignKey | None = None
        for field_part in field_parts:
            if field_part == "pk":
                field = self.get_primary_key_field(currently_observed_model)
                continue

            field = currently_observed_model._meta.get_field(field_part)
            if isinstance(field, RelatedField):
                currently_observed_model = self.get_field_related_model_cls(field)
                model_name = currently_observed_model._meta.model_name
                if model_name is not None and field_part == (model_name + "_id"):
                    field = self.get_primary_key_field(currently_observed_model)

            if isinstance(field, ForeignObjectRel):
                currently_observed_model = self.get_field_related_model_cls(field)

        # Guaranteed by `query.solve_lookup_type` before.
        assert isinstance(field, Field | ForeignObjectRel)
        return field, currently_observed_model

    def solve_lookup_type(
        self, model_cls: type[Model], lookup: str
    ) -> tuple[Sequence[str], Sequence[str], Expression | Literal[False]] | None:
        query = Query(model_cls)
        if (lookup == "pk" or lookup.startswith("pk__")) and query.get_meta().pk is None:
            # Primary key lookup when no primary key field is found, model is presumably
            # abstract and we can't say anything about 'pk'.
            return None
        try:
            return query.solve_lookup_type(lookup)
        # This occurs when the following conditions are met:
        # - model_cls._meta.abstract = True
        # - part of the lookup is a foreign key defined on model_cls where the 'to' argument is a string
        # On abstract models the 'to' parameter is only coalesced into the actual class whenever a subclass of it is
        # instantiated, therefore it is never swapped out for abstract base classes.
        except AttributeError:
            pass
        query_parts = lookup.split("__")
        try:
            field = query.get_meta().get_field(query_parts[0])
        except FieldDoesNotExist:
            return None

        if len(query_parts) == 1:
            return [], [query_parts[0]], False

        if not isinstance(field, RelatedField | ForeignObjectRel):
            return None

        related_model = self.get_field_related_model_cls(field)
        sub_query = Query(related_model).solve_lookup_type("__".join(query_parts[1:]))
        entire_query_parts = [query_parts[0], *sub_query[1]]
        return sub_query[0], entire_query_parts, sub_query[2]

    def resolve_lookup_into_field(
        self, model_cls: type[Model], lookup: str
    ) -> tuple[Union["Field[Any, Any]", ForeignObjectRel, None], type[Model]]:
        solved_lookup = self.solve_lookup_type(model_cls, lookup)
        if solved_lookup is None:
            return None, model_cls
        lookup_parts, field_parts, _ = solved_lookup
        if lookup_parts:
            raise LookupsAreUnsupported()
        return self._resolve_field_from_parts(field_parts, model_cls)

    def resolve_lookup_expected_type(
        self, ctx: MethodContext, model_cls: type[Model], lookup: str, model_instance: Instance
    ) -> MypyType:
        try:
            solved_lookup = self.solve_lookup_type(model_cls, lookup)
        except FieldError as exc:
            if helpers.is_annotated_model(model_instance.type) and model_instance.extra_attrs:
                # If the field comes from .annotate(), we assume Any for it
                # and allow chaining any lookups.
                lookup_base_field, *_ = lookup.split("__")
                if lookup_base_field in model_instance.extra_attrs.attrs:
                    return model_instance.extra_attrs.attrs[lookup_base_field]

            msg = exc.args[0]
            if model_instance.extra_attrs:
                msg = ", ".join((msg, *model_instance.extra_attrs.attrs.keys()))
            ctx.api.fail(msg, ctx.context)
            return AnyType(TypeOfAny.from_error)

        if solved_lookup is None:
            return AnyType(TypeOfAny.implementation_artifact)
        lookup_parts, field_parts, is_expression = solved_lookup
        if is_expression:
            return AnyType(TypeOfAny.explicit)

        field, _ = self._resolve_field_from_parts(field_parts, model_cls)

        lookup_cls = None
        if lookup_parts:
            lookup = lookup_parts[-1]
            lookup_cls = field.get_lookup(lookup)
            if lookup_cls is None:
                # unknown lookup
                return AnyType(TypeOfAny.explicit)

        if lookup_cls is None or isinstance(lookup_cls, Exact):
            return self.get_field_lookup_exact_type(helpers.get_typechecker_api(ctx), field)

        assert lookup_cls is not None

        lookup_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), lookup_cls)
        if lookup_info is None:
            return AnyType(TypeOfAny.explicit)

        for lookup_base in helpers.iter_bases(lookup_info):
            if lookup_base.args and isinstance((lookup_type := get_proper_type(lookup_base.args[0])), Instance):
                # if it's Field, consider lookup_type a __get__ of current field
                if lookup_type.type.fullname == fullnames.FIELD_FULLNAME:
                    field_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), field.__class__)
                    if field_info is None:
                        return AnyType(TypeOfAny.explicit)
                    lookup_type = get_proper_type(
                        helpers.get_private_descriptor_type(field_info, "_pyi_private_get_type", is_nullable=field.null)
                    )
                return lookup_type

        return AnyType(TypeOfAny.explicit)

    def resolve_f_expression_type(self, f_expression_type: Instance) -> ProperType:
        return AnyType(TypeOfAny.explicit)

    @cached_property
    def is_contrib_auth_installed(self) -> bool:
        return "django.contrib.auth" in self.settings.INSTALLED_APPS