diff --git a/.gitignore b/.gitignore index 0e255dff4..5f938fd3d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ out/ /django .idea/ .mypy_cache/ -django-sources build/ dist/ -pip-wheel-metadata/ \ No newline at end of file +pip-wheel-metadata/ +.pytest_cache/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..4a85142da --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "django-sources"] + path = django-sources + url = https://github.com/django/django.git + branch = stable/2.2.x diff --git a/.travis.yml b/.travis.yml index 4ea2261ee..795cae6d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,15 +10,17 @@ jobs: set -e pytest - - name: Run plugin test suite with python 3.6 + - name: Typecheck Django test suite with python 3.7 + python: 3.7 + script: 'python ./scripts/typecheck_tests.py' + + - name: Typecheck Django test suite with python 3.6 python: 3.6 - script: | - set -e - pytest + script: 'python ./scripts/typecheck_tests.py' - - name: Typecheck Django test suite + - name: Mypy for plugin code python: 3.7 - script: 'python ./scripts/typecheck_tests.py' + script: 'mypy ./mypy_django_plugin' - name: Lint with black python: 3.7 @@ -36,13 +38,4 @@ before_install: | # Upgrade pip, setuptools, and wheel pip install -U pip setuptools wheel install: | - pip install -r ./dev-requirements.txt - pip install -r ./scripts/typecheck-tests-requirements.txt - -#deploy: -# provider: pypi -# user: "mkurnikov" -# password: -# secure: 0E+hkaIdtpEtyL1KZeglunZ5/PKjouFfa8ljakAwoig7VNUL+2sO/bTyg38wRQl0NvzDzEHSMEt1bzg4Tq7b7Zp6nLuewG/w7mGLzqaOlTySiPEfRsg8s6uO2KrTn7g9VhlXH6UtyTXoQdMt6aE8+bt/GmEesanS57NB2mhwmylFgQwlJFu4LfIv/+aGmc4eLeGI2Qhvs9QYf7qvYlLQldgFh8mAckQEEvaBg35sf+puypZgf4nkx1k/dfG9wnFWZU8PJ41LbMw/Wj+k/9NpF8ePwiAr0fvRMErZd8nvoiWjQQjhzgrLVHhXEP5pTHh3zjDuGFMWyKuBhC6WLsG4qOQz/HvxeYvNI+jaTp15BgxtefG/pCNDUl/8GlCde7xVt7xzEcYNJSRaZPY2oofEFSd9qDnr4kqmyCXpNsaHRHvkL61bFjXUcfOsMMYvQCC6N2Jjb7S97RbnDdkOZO/lnFhVANT2rigsaXlSlWyN6f7ApxDNvu6Ehu5yrx6IjlPZJ0sI9vvY3IoS6Fik7w9E6zjNVjbmUn1D4MKFP4v5ppNASOqYcZeLd42j8rjEp0gIc3ccz9aUIT9q8VqSXSdUbqA6SVwvHXIVPxJMXj0bqWBG1iKs0cPBuzRVpRrwkENWCSWElDAewM1qFEnK0LppyoYFbqoQ8F5FG0+re7QttKQ= -# on: -# tags: true \ No newline at end of file + pip install -r ./dev-requirements.txt \ No newline at end of file diff --git a/README.md b/README.md index 1292fe3d5..d3dbb4007 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ Could be run on earlier versions of Django, but expect some missing imports warn pip install django-stubs ``` +### WARNING: All configuration from pre-1.0.0 versions is dropped, use one below. + +### WARNING: 1.0.0 breaks `dmypy`, if you need it, stay on the 0.12.x series. + To make mypy aware of the plugin, you need to add ``` @@ -27,27 +31,33 @@ plugins = in your `mypy.ini` file. +Plugin requires Django settings module (what you put into `DJANGO_SETTINGS_MODULE` variable) to be specified inside `mypy.ini` file. +``` +[mypy] +strict_optional = True -## Configuration - -In order to specify config file, set `MYPY_DJANGO_CONFIG` environment variable with path to the config file. Default is `./mypy_django.ini` - -Config file format (.ini): +; this one is new +[mypy.plugins.django-stubs] +django_settings_module = mysettings ``` -[mypy_django_plugin] +where `mysettings` is a value of `DJANGO_SETTINGS_MODULE` (with or without quotes) + +New 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()`. -# specify settings module to use for django.conf.settings, this setting -# could also be specified with DJANGO_SETTINGS_MODULE environment variable -# (it also takes priority over config file) -django_settings = mysettings.local +In other words, if your `manage.py runserver` crashes, mypy will crash too. -# if True, all unknown settings in django.conf.settings will fallback to Any, -# specify it if your settings are loaded dynamically to avoid false positives -ignore_missing_settings = True +## Notes -# if True, unknown attributes on Model instances won't produce errors -ignore_missing_model_attributes = True +Implementation monkey-patches Django to add `__class_getitem__` to the `Manager` class. If you'd use Python3.7 and do that too in your code, you can make things like ``` +class MyUserManager(models.Manager['MyUser']): + pass +class MyUser(models.Model): + objects = UserManager() +``` +work, which should make a error messages a bit better. + +Otherwise, custom type will be created in mypy, named `MyUser__MyUserManager`, which will rewrite base manager as `models.Manager[User]` to make methods like `get_queryset()` and others return properly typed `QuerySet`. ## To get help diff --git a/dev-requirements.txt b/dev-requirements.txt index 24d30a5ad..b62d198c6 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ black -pytest-mypy-plugins +pytest-mypy-plugins==1.0.3 flake8 isort==4.3.4 -e . diff --git a/django-sources b/django-sources new file mode 160000 index 000000000..4d6449e12 --- /dev/null +++ b/django-sources @@ -0,0 +1 @@ +Subproject commit 4d6449e1258c88b6e4e1ccbb5e84b210371598d2 diff --git a/django-stubs/apps/registry.pyi b/django-stubs/apps/registry.pyi index 7208ec19e..69381ad2c 100644 --- a/django-stubs/apps/registry.pyi +++ b/django-stubs/apps/registry.pyi @@ -1,6 +1,6 @@ -import collections import threading -from typing import Any, Callable, List, Optional, Tuple, Type, Union, Iterable, DefaultDict +from collections import OrderedDict +from typing import Any, Callable, List, Optional, Tuple, Type, Union, Iterable, DefaultDict, Dict from django.db.migrations.state import AppConfigStub from django.db.models.base import Model @@ -8,23 +8,24 @@ from django.db.models.base import Model from .config import AppConfig class Apps: - all_models: collections.defaultdict = ... - app_configs: collections.OrderedDict = ... + all_models: "Dict[str, OrderedDict[str, Type[Model]]]" = ... + app_configs: "OrderedDict[str, AppConfig]" = ... stored_app_configs: List[Any] = ... apps_ready: bool = ... ready_event: threading.Event = ... loading: bool = ... _pending_operations: DefaultDict[Tuple[str, str], List] - def __init__(self, installed_apps: Optional[Union[List[AppConfigStub], List[str], Tuple]] = ...) -> None: ... models_ready: bool = ... ready: bool = ... + def __init__(self, installed_apps: Optional[Union[List[AppConfigStub], List[str], Tuple]] = ...) -> None: ... def populate(self, installed_apps: Union[List[AppConfigStub], List[str], Tuple] = ...) -> None: ... def check_apps_ready(self) -> None: ... def check_models_ready(self) -> None: ... def get_app_configs(self) -> Iterable[AppConfig]: ... def get_app_config(self, app_label: str) -> AppConfig: ... - def get_models(self, include_auto_created: bool = ..., include_swapped: bool = ...) -> List[Type[Model]]: ... - def get_model(self, app_label: str, model_name: Optional[str] = ..., require_ready: bool = ...) -> Type[Model]: ... + # it's not possible to support it in plugin properly now + def get_models(self, include_auto_created: bool = ..., include_swapped: bool = ...) -> List[Type[Any]]: ... + def get_model(self, app_label: str, model_name: Optional[str] = ..., require_ready: bool = ...) -> Type[Any]: ... def register_model(self, app_label: str, model: Type[Model]) -> None: ... def is_installed(self, app_name: str) -> bool: ... def get_containing_app_config(self, object_name: str) -> Optional[AppConfig]: ... diff --git a/django-stubs/contrib/auth/base_user.pyi b/django-stubs/contrib/auth/base_user.pyi index f9179d2a7..1cd79b020 100644 --- a/django-stubs/contrib/auth/base_user.pyi +++ b/django-stubs/contrib/auth/base_user.pyi @@ -1,12 +1,16 @@ -from typing import Any, Optional, Tuple, List, overload +from typing import Any, Optional, Tuple, List, overload, TypeVar + +from django.db.models.base import Model from django.db import models -class BaseUserManager(models.Manager): +_T = TypeVar("_T", bound=Model) + +class BaseUserManager(models.Manager[_T]): @classmethod def normalize_email(cls, email: Optional[str]) -> str: ... def make_random_password(self, length: int = ..., allowed_chars: str = ...) -> str: ... - def get_by_natural_key(self, username: Optional[str]) -> AbstractBaseUser: ... + def get_by_natural_key(self, username: Optional[str]) -> _T: ... class AbstractBaseUser(models.Model): password: models.CharField = ... diff --git a/django-stubs/contrib/auth/models.pyi b/django-stubs/contrib/auth/models.pyi index b7c0f095a..bbaeb89a1 100644 --- a/django-stubs/contrib/auth/models.pyi +++ b/django-stubs/contrib/auth/models.pyi @@ -1,10 +1,11 @@ -from typing import Any, Collection, Optional, Set, Tuple, Type, Union +from typing import Any, Collection, Optional, Set, Tuple, Type, TypeVar from django.contrib.auth.base_user import AbstractBaseUser as AbstractBaseUser, BaseUserManager as BaseUserManager +from django.contrib.auth.validators import UnicodeUsernameValidator from django.contrib.contenttypes.models import ContentType +from django.db.models.base import Model from django.db.models.manager import EmptyManager -from django.contrib.auth.validators import UnicodeUsernameValidator from django.db import models def update_last_login(sender: Type[AbstractBaseUser], user: AbstractBaseUser, **kwargs: Any) -> None: ... @@ -27,13 +28,15 @@ class Group(models.Model): permissions: models.ManyToManyField = models.ManyToManyField(Permission) def natural_key(self): ... -class UserManager(BaseUserManager): +_T = TypeVar("_T", bound=Model) + +class UserManager(BaseUserManager[_T]): def create_user( self, username: str, email: Optional[str] = ..., password: Optional[str] = ..., **extra_fields: Any - ) -> AbstractUser: ... + ) -> _T: ... def create_superuser( self, username: str, email: Optional[str], password: Optional[str], **extra_fields: Any - ) -> AbstractBaseUser: ... + ) -> _T: ... class PermissionsMixin(models.Model): is_superuser: models.BooleanField = ... diff --git a/django-stubs/contrib/contenttypes/fields.pyi b/django-stubs/contrib/contenttypes/fields.pyi index 1b80e2e17..0eba58382 100644 --- a/django-stubs/contrib/contenttypes/fields.pyi +++ b/django-stubs/contrib/contenttypes/fields.pyi @@ -7,6 +7,7 @@ from django.db.models.fields.related import ForeignObject from django.db.models.fields.related_descriptors import ReverseManyToOneDescriptor from django.db.models.fields.reverse_related import ForeignObjectRel +from django.db.models.expressions import Combinable from django.db.models.fields import Field, PositiveIntegerField from django.db.models.fields.mixins import FieldCacheMixin from django.db.models.query import QuerySet @@ -14,6 +15,10 @@ from django.db.models.query_utils import FilteredRelation, PathInfo from django.db.models.sql.where import WhereNode class GenericForeignKey(FieldCacheMixin): + # django-stubs implementation only fields + _pyi_private_set_type: Union[Any, Combinable] + _pyi_private_get_type: Any + # attributes auto_created: bool = ... concrete: bool = ... editable: bool = ... @@ -44,36 +49,21 @@ class GenericForeignKey(FieldCacheMixin): def get_prefetch_queryset( self, instances: Union[List[Model], QuerySet], queryset: Optional[QuerySet] = ... ) -> Tuple[List[Model], Callable, Callable, bool, str, bool]: ... - def __get__( - self, instance: Optional[Model], cls: Type[Model] = ... - ) -> Optional[Union[GenericForeignKey, Model]]: ... - def __set__(self, instance: Model, value: Optional[Model]) -> None: ... + def __get__(self, instance: Optional[Model], cls: Type[Model] = ...) -> Optional[Any]: ... + def __set__(self, instance: Model, value: Optional[Any]) -> None: ... class GenericRel(ForeignObjectRel): field: GenericRelation - limit_choices_to: Optional[Union[Dict[str, Any], Callable[[], Any]]] - model: Type[Model] - multiple: bool - on_delete: Callable - parent_link: bool - related_name: str - related_query_name: None - symmetrical: bool def __init__( self, field: GenericRelation, to: Union[Type[Model], str], - related_name: None = ..., + related_name: Optional[str] = ..., related_query_name: Optional[str] = ..., limit_choices_to: Optional[Union[Dict[str, Any], Callable[[], Any]]] = ..., ) -> None: ... class GenericRelation(ForeignObject): - auto_created: bool = ... - many_to_many: bool = ... - many_to_one: bool = ... - one_to_many: bool = ... - one_to_one: bool = ... rel_class: Any = ... mti_inherited: bool = ... object_id_field_name: Any = ... @@ -90,23 +80,16 @@ class GenericRelation(ForeignObject): limit_choices_to: Optional[Union[Dict[str, Any], Callable[[], Any]]] = ..., **kwargs: Any ) -> None: ... - def check(self, **kwargs: Any) -> List[Error]: ... def resolve_related_fields(self) -> List[Tuple[PositiveIntegerField, Field]]: ... def get_path_info(self, filtered_relation: Optional[FilteredRelation] = ...) -> List[PathInfo]: ... def get_reverse_path_info(self, filtered_relation: None = ...) -> List[PathInfo]: ... def value_to_string(self, obj: Model) -> str: ... - model: Any = ... - def set_attributes_from_rel(self) -> None: ... - def get_internal_type(self) -> str: ... def get_content_type(self) -> ContentType: ... def get_extra_restriction( self, where_class: Type[WhereNode], alias: Optional[str], remote_alias: str ) -> WhereNode: ... def bulk_related_objects(self, objs: List[Model], using: str = ...) -> QuerySet: ... -class ReverseGenericManyToOneDescriptor(ReverseManyToOneDescriptor): - field: GenericRelation - rel: GenericRel - def related_manager_cls(self): ... +class ReverseGenericManyToOneDescriptor(ReverseManyToOneDescriptor): ... def create_generic_related_manager(superclass: Any, rel: Any): ... diff --git a/django-stubs/contrib/postgres/fields/jsonb.pyi b/django-stubs/contrib/postgres/fields/jsonb.pyi index f071e7417..48f00984a 100644 --- a/django-stubs/contrib/postgres/fields/jsonb.pyi +++ b/django-stubs/contrib/postgres/fields/jsonb.pyi @@ -5,7 +5,7 @@ from django.db.models import Field from django.db.models.lookups import Transform from .mixins import CheckFieldDefaultMixin -class JsonAdapter(object): +class JsonAdapter: encoder: Any = ... def __init__(self, adapted: Any, dumps: Optional[Any] = ..., encoder: Optional[Any] = ...) -> None: ... def dumps(self, obj: Any): ... @@ -22,12 +22,7 @@ class JSONField(CheckFieldDefaultMixin, Field): encoder: Optional[Type[JSONEncoder]] = ..., **kwargs: Any ) -> None: ... - def db_type(self, connection: Any): ... - def get_transform(self, name: Any): ... - def get_prep_value(self, value: Any): ... - def validate(self, value: Any, model_instance: Any) -> None: ... def value_to_string(self, obj: Any): ... - def formfield(self, **kwargs: Any): ... class KeyTransform(Transform): operator: str = ... diff --git a/django-stubs/contrib/sites/managers.pyi b/django-stubs/contrib/sites/managers.pyi index 8dc2fedb5..45ae26d4b 100644 --- a/django-stubs/contrib/sites/managers.pyi +++ b/django-stubs/contrib/sites/managers.pyi @@ -1,15 +1,6 @@ -from typing import Any, List, Optional - -from django.core.checks.messages import Error -from django.db.models.query import QuerySet +from typing import Optional from django.db import models class CurrentSiteManager(models.Manager): - creation_counter: int - model: None - name: None - use_in_migrations: bool = ... def __init__(self, field_name: Optional[str] = ...) -> None: ... - def check(self, **kwargs: Any) -> List[Error]: ... - def get_queryset(self) -> QuerySet: ... diff --git a/django-stubs/core/management/__init__.pyi b/django-stubs/core/management/__init__.pyi index 92d994e67..2b5bef273 100644 --- a/django-stubs/core/management/__init__.pyi +++ b/django-stubs/core/management/__init__.pyi @@ -1,11 +1,11 @@ -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Tuple, Union -from django.core.management.base import BaseCommand as BaseCommand, CommandError as CommandError +from .base import BaseCommand as BaseCommand, CommandError as CommandError def find_commands(management_dir: str) -> List[str]: ... def load_command_class(app_name: str, name: str) -> BaseCommand: ... def get_commands() -> Dict[str, str]: ... -def call_command(command_name: Union[Tuple[str], BaseCommand, str], *args: Any, **options: Any) -> Optional[str]: ... +def call_command(command_name: Union[Tuple[str], BaseCommand, str], *args: Any, **options: Any) -> str: ... class ManagementUtility: argv: List[str] = ... diff --git a/django-stubs/core/serializers/__init__.pyi b/django-stubs/core/serializers/__init__.pyi index d9eab4012..35305be2a 100644 --- a/django-stubs/core/serializers/__init__.pyi +++ b/django-stubs/core/serializers/__init__.pyi @@ -1,24 +1,23 @@ -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union, Iterable +from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Type, Union -from django.apps.config import AppConfig from django.db.models.base import Model -from django.db.models.query import QuerySet from .base import ( - Serializer as Serializer, - Deserializer as Deserializer, - SerializerDoesNotExist as SerializerDoesNotExist, - SerializationError as SerializationError, DeserializationError as DeserializationError, + DeserializedObject, + Deserializer as Deserializer, M2MDeserializationError as M2MDeserializationError, + SerializationError as SerializationError, + Serializer as Serializer, + SerializerDoesNotExist as SerializerDoesNotExist, ) BUILTIN_SERIALIZERS: Any class BadSerializer: internal_use_only: bool = ... - exception: ModuleNotFoundError = ... - def __init__(self, exception: ImportError) -> None: ... + exception: BaseException = ... + def __init__(self, exception: BaseException) -> None: ... def __call__(self, *args: Any, **kwargs: Any) -> Any: ... def register_serializer(format: str, serializer_module: str, serializers: Optional[Dict[str, Any]] = ...) -> None: ... @@ -27,10 +26,6 @@ def get_serializer(format: str) -> Union[Type[Serializer], BadSerializer]: ... def get_serializer_formats() -> List[str]: ... def get_public_serializer_formats() -> List[str]: ... def get_deserializer(format: str) -> Union[Callable, Type[Deserializer]]: ... -def serialize( - format: str, queryset: Union[Iterator[Any], List[Model], QuerySet], **options: Any -) -> Optional[Union[bytes, str]]: ... -def deserialize(format: str, stream_or_string: Any, **options: Any) -> Union[Iterator[Any], Deserializer]: ... -def sort_dependencies( - app_list: Union[Iterable[Tuple[AppConfig, None]], Iterable[Tuple[str, Iterable[Type[Model]]]]] -) -> List[Type[Model]]: ... +def serialize(format: str, queryset: Iterable[Model], **options: Any) -> Any: ... +def deserialize(format: str, stream_or_string: Any, **options: Any) -> Iterator[DeserializedObject]: ... +def sort_dependencies(app_list: Iterable[Any]) -> List[Type[Model]]: ... diff --git a/django-stubs/core/serializers/base.pyi b/django-stubs/core/serializers/base.pyi index 1057130f5..46e2e53f0 100644 --- a/django-stubs/core/serializers/base.pyi +++ b/django-stubs/core/serializers/base.pyi @@ -1,13 +1,13 @@ -from collections import OrderedDict from datetime import date from io import BufferedReader, StringIO, TextIOWrapper -from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union +from typing import Any, Dict, Iterable, List, Mapping, Optional, Type, Union from uuid import UUID from django.core.management.base import OutputWrapper from django.db.models.base import Model from django.db.models.fields.related import ForeignKey, ManyToManyField -from django.db.models.query import QuerySet + +from django.db.models.fields import Field class SerializerDoesNotExist(KeyError): ... class SerializationError(Exception): ... @@ -43,7 +43,7 @@ class Serializer: first: bool = ... def serialize( self, - queryset: Union[Iterator[Any], List[Model], QuerySet], + queryset: Iterable[Model], *, stream: Optional[Any] = ..., fields: Optional[Any] = ..., @@ -52,7 +52,7 @@ class Serializer: progress_output: Optional[Any] = ..., object_count: int = ..., **options: Any - ) -> Optional[Union[List[OrderedDict], bytes, str]]: ... + ) -> Any: ... def start_serialization(self) -> None: ... def end_serialization(self) -> None: ... def start_object(self, obj: Any) -> None: ... @@ -70,15 +70,18 @@ class Deserializer: def __next__(self) -> None: ... class DeserializedObject: - object: Model = ... - m2m_data: Dict[Any, Any] = ... - def __init__(self, obj: Model, m2m_data: Optional[Dict[str, List[int]]] = ...) -> None: ... + object: Any = ... + m2m_data: Dict[str, List[int]] = ... + deferred_fields: Mapping[Field, Any] + def __init__( + self, + obj: Model, + m2m_data: Optional[Dict[str, List[int]]] = ..., + deferred_fields: Optional[Mapping[Field, Any]] = ..., + ) -> None: ... def save(self, save_m2m: bool = ..., using: Optional[str] = ..., **kwargs: Any) -> None: ... + def save_deferred_fields(self, using: Optional[str] = ...) -> None: ... def build_instance(Model: Type[Model], data: Dict[str, Optional[Union[date, int, str, UUID]]], db: str) -> Model: ... -def deserialize_m2m_values( - field: ManyToManyField, field_value: Union[List[List[str]], List[int]], using: str -) -> List[int]: ... -def deserialize_fk_value( - field: ForeignKey, field_value: Optional[Union[List[str], Tuple[str], int, str]], using: str -) -> Optional[Union[int, str, UUID]]: ... +def deserialize_m2m_values(field: ManyToManyField, field_value: Any, using: str) -> List[Any]: ... +def deserialize_fk_value(field: ForeignKey, field_value: Any, using: str) -> Any: ... diff --git a/django-stubs/db/models/base.pyi b/django-stubs/db/models/base.pyi index 6c9aa4561..28e627a05 100644 --- a/django-stubs/db/models/base.pyi +++ b/django-stubs/db/models/base.pyi @@ -1,19 +1,22 @@ -from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, TypeVar, Union +from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, TypeVar, Union, ClassVar, Type from django.db.models.manager import Manager from django.core.checks.messages import CheckMessage -class ModelBase(type): ... +from django.db.models.options import Options _Self = TypeVar("_Self", bound="Model") +class ModelBase(type): ... + class Model(metaclass=ModelBase): class DoesNotExist(Exception): ... class MultipleObjectsReturned(Exception): ... class Meta: ... - _meta: Any _default_manager: Manager[Model] + _meta: Options[Any] + objects: Manager[Any] pk: Any = ... def __init__(self: _Self, *args, **kwargs) -> None: ... def delete(self, using: Any = ..., keep_parents: bool = ...) -> Tuple[int, Dict[str, int]]: ... diff --git a/django-stubs/db/models/expressions.pyi b/django-stubs/db/models/expressions.pyi index 0dbcf0b83..6579b881c 100644 --- a/django-stubs/db/models/expressions.pyi +++ b/django-stubs/db/models/expressions.pyi @@ -52,7 +52,6 @@ class BaseExpression: is_summary: bool = ... filterable: bool = ... window_compatible: bool = ... - output_field: Any def __init__(self, output_field: Optional[_OutputField] = ...) -> None: ... def get_db_converters(self, connection: Any) -> List[Callable]: ... def get_source_expressions(self) -> List[Any]: ... diff --git a/django-stubs/db/models/fields/__init__.pyi b/django-stubs/db/models/fields/__init__.pyi index c10dc5cf7..8d599e685 100644 --- a/django-stubs/db/models/fields/__init__.pyi +++ b/django-stubs/db/models/fields/__init__.pyi @@ -1,11 +1,27 @@ import decimal import uuid from datetime import date, datetime, time, timedelta -from typing import Any, Callable, Dict, Generic, Iterable, Optional, Tuple, Type, TypeVar, Union, Sequence +from typing import ( + Any, + Callable, + Dict, + Generic, + Iterable, + Optional, + Tuple, + Type, + TypeVar, + Union, + Sequence, + List, + overload, +) + +from django.core import checks from django.db.models import Model from django.core.exceptions import FieldDoesNotExist as FieldDoesNotExist -from django.db.models.expressions import Combinable +from django.db.models.expressions import Combinable, Col from django.db.models.query_utils import RegisterLookupMixin from django.forms import Field as FormField, Widget @@ -18,6 +34,7 @@ _FieldChoices = Iterable[Union[_Choice, _ChoiceNamedGroup]] _ValidatorCallable = Callable[..., None] _ErrorMessagesToOverride = Dict[str, Any] +_T = TypeVar("_T", bound="Field") # __set__ value type _ST = TypeVar("_ST") # __get__ return type @@ -30,14 +47,21 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]): widget: Widget help_text: str db_table: str + attname: str + auto_created: bool + primary_key: bool remote_field: Field - max_length: Optional[int] + max_length: int model: Type[Model] name: str + verbose_name: str blank: bool = ... null: bool = ... editable: bool = ... choices: Optional[_FieldChoices] = ... + db_column: Optional[str] + column: str + error_messages: _ErrorMessagesToOverride def __init__( self, verbose_name: Optional[Union[str, bytes]] = ..., @@ -63,7 +87,15 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]): error_messages: Optional[_ErrorMessagesToOverride] = ..., ): ... def __set__(self, instance, value: _ST) -> None: ... - def __get__(self, instance, owner) -> _GT: ... + # class access + @overload + def __get__(self: _T, instance: None, owner) -> _T: ... + # Model instance access + @overload + def __get__(self, instance: Model, owner) -> _GT: ... + # non-Model instances + @overload + def __get__(self: _T, instance, owner) -> _T: ... def deconstruct(self) -> Any: ... def set_attributes_from_name(self, name: str) -> None: ... def db_type(self, connection: Any) -> str: ... @@ -71,6 +103,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]): def get_prep_value(self, value: Any) -> Any: ... def get_internal_type(self) -> str: ... def formfield(self, **kwargs) -> FormField: ... + def save_form_data(self, instance: Model, data: Any) -> None: ... def contribute_to_class(self, cls: Type[Model], name: str, private_only: bool = ...) -> None: ... def to_python(self, value: Any) -> Any: ... def clean(self, value: Any, model_instance: Optional[Model]) -> Any: ... @@ -81,7 +114,17 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]): limit_choices_to: Optional[Any] = ..., ordering: Sequence[str] = ..., ) -> Sequence[Union[_Choice, _ChoiceNamedGroup]]: ... + def has_default(self) -> bool: ... def get_default(self) -> Any: ... + def check(self, **kwargs: Any) -> List[checks.Error]: ... + @property + def validators(self) -> List[_ValidatorCallable]: ... + def validate(self, value: Any, model_instance: Model) -> None: ... + def run_validators(self, value: Any) -> None: ... + def get_col(self, alias: str, output_field: Optional[Field] = ...) -> Col: ... + @property + def cached_col(self) -> Col: ... + def value_from_object(self, obj: Model) -> _GT: ... class IntegerField(Field[_ST, _GT]): _pyi_private_set_type: Union[float, int, str, Combinable] @@ -102,6 +145,9 @@ class FloatField(Field[_ST, _GT]): class DecimalField(Field[_ST, _GT]): _pyi_private_set_type: Union[str, float, decimal.Decimal, Combinable] _pyi_private_get_type: decimal.Decimal + # attributes + max_digits: int = ... + decimal_places: int = ... def __init__( self, verbose_name: Optional[Union[str, bytes]] = ..., diff --git a/django-stubs/db/models/fields/related.pyi b/django-stubs/db/models/fields/related.pyi index 044fb96f7..6192cd366 100644 --- a/django-stubs/db/models/fields/related.pyi +++ b/django-stubs/db/models/fields/related.pyi @@ -1,39 +1,25 @@ -from typing import ( - Type, - Union, - TypeVar, - Any, - Generic, - List, - Optional, - Dict, - Callable, - Tuple, - Sequence, - TYPE_CHECKING, - Iterable, -) +from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type, TypeVar, Union from uuid import UUID -from django.db import models -from django.db.models import Field, Model, QuerySet +from django.db.models.expressions import Combinable from django.db.models.fields.mixins import FieldCacheMixin +from django.db.models.query_utils import PathInfo, Q + +from django.db import models +from django.db.models import Field, Model from django.db.models.fields.related_descriptors import ( - ReverseManyToOneDescriptor as ReverseManyToOneDescriptor, - ReverseOneToOneDescriptor as ReverseOneToOneDescriptor, - ForwardManyToOneDescriptor as ForwardManyToOneDescriptor, ForwardOneToOneDescriptor as ForwardOneToOneDescriptor, + ForwardManyToOneDescriptor as ForwardManyToOneDescriptor, ManyToManyDescriptor as ManyToManyDescriptor, + ReverseOneToOneDescriptor as ReverseOneToOneDescriptor, + ReverseManyToOneDescriptor as ReverseManyToOneDescriptor, ) from django.db.models.fields.reverse_related import ( ForeignObjectRel as ForeignObjectRel, - ManyToManyRel as ManyToManyRel, - ManyToOneRel as ManyToOneRel, OneToOneRel as OneToOneRel, + ManyToOneRel as ManyToOneRel, + ManyToManyRel as ManyToManyRel, ) -from django.db.models.query_utils import PathInfo, Q - -from django.db.models.expressions import Combinable if TYPE_CHECKING: from django.db.models.manager import RelatedManager @@ -59,15 +45,13 @@ class RelatedField(FieldCacheMixin, Field[_ST, _GT]): one_to_one: bool = ... many_to_many: bool = ... many_to_one: bool = ... - def related_model(self) -> Union[Type[Model], str]: ... - def check(self, **kwargs: Any) -> List[Any]: ... opts: Any = ... + @property + def related_model(self) -> Type[Model]: ... def get_forward_related_filter(self, obj: Model) -> Dict[str, Union[int, UUID]]: ... def get_reverse_related_filter(self, obj: Model) -> Q: ... @property def swappable_setting(self) -> Optional[str]: ... - name: Any = ... - verbose_name: Any = ... def set_attributes_from_rel(self) -> None: ... def do_related_class(self, other: Type[Model], cls: Type[Model]) -> None: ... def get_limit_choices_to(self) -> Dict[str, int]: ... @@ -187,7 +171,7 @@ class ManyToManyField(RelatedField[_ST, _GT]): rel_class: Any = ... description: Any = ... has_null_arg: Any = ... - swappable: Any = ... + swappable: bool = ... def __init__( self, to: Union[Type[_T], str], @@ -222,19 +206,15 @@ class ManyToManyField(RelatedField[_ST, _GT]): validators: Iterable[_ValidatorCallable] = ..., error_messages: Optional[_ErrorMessagesToOverride] = ..., ) -> None: ... - def check(self, **kwargs: Any) -> List[Any]: ... + def __get__(self, instance, owner) -> _GT: ... # type: ignore def get_path_info(self, filtered_relation: None = ...) -> List[PathInfo]: ... def get_reverse_path_info(self, filtered_relation: None = ...) -> List[PathInfo]: ... - m2m_db_table: Any = ... - m2m_column_name: Any = ... - m2m_reverse_name: Any = ... - m2m_field_name: Any = ... - m2m_reverse_field_name: Any = ... - m2m_target_field_name: Any = ... - m2m_reverse_target_field_name: Any = ... def contribute_to_related_class(self, cls: Type[Model], related: RelatedField) -> None: ... - def set_attributes_from_rel(self) -> None: ... - def value_from_object(self, obj: Model) -> List[Model]: ... - def save_form_data(self, instance: Model, data: QuerySet) -> None: ... + def m2m_db_table(self) -> str: ... + def m2m_column_name(self) -> str: ... + def m2m_reverse_name(self) -> str: ... + def m2m_reverse_field_name(self) -> str: ... + def m2m_target_field_name(self) -> str: ... + def m2m_reverse_target_field_name(self) -> str: ... def create_many_to_many_intermediary_model(field: Type[Field], klass: Type[Model]) -> Type[Model]: ... diff --git a/django-stubs/db/models/fields/reverse_related.pyi b/django-stubs/db/models/fields/reverse_related.pyi index c156bb28f..c220bee9e 100644 --- a/django-stubs/db/models/fields/reverse_related.pyi +++ b/django-stubs/db/models/fields/reverse_related.pyi @@ -10,13 +10,10 @@ from django.db.models.sql.where import WhereNode from .mixins import FieldCacheMixin class ForeignObjectRel(FieldCacheMixin): - hidden: bool many_to_many: bool many_to_one: bool - name: str one_to_many: bool one_to_one: bool - related_model: Type[Model] auto_created: bool = ... concrete: bool = ... editable: bool = ... @@ -43,6 +40,12 @@ class ForeignObjectRel(FieldCacheMixin): on_delete: Optional[Callable] = ..., ) -> None: ... @property + def hidden(self) -> bool: ... + @property + def name(self) -> str: ... + @property + def related_model(self) -> Type[Model]: ... + @property def remote_field(self) -> RelatedField: ... @property def target_field(self) -> AutoField: ... @@ -63,22 +66,6 @@ class ForeignObjectRel(FieldCacheMixin): def get_path_info(self, filtered_relation: Optional[FilteredRelation] = ...) -> List[PathInfo]: ... class ManyToOneRel(ForeignObjectRel): - field: RelatedField - hidden: bool - limit_choices_to: Any - many_to_many: bool - many_to_one: bool - model: Union[Type[Model], str] - multiple: bool - name: str - on_delete: Callable - one_to_many: bool - one_to_one: bool - parent_link: bool - related_model: Type[Model] - related_name: Optional[str] - related_query_name: Optional[str] - symmetrical: bool def __init__( self, field: ForeignKey, @@ -91,25 +78,8 @@ class ManyToOneRel(ForeignObjectRel): on_delete: Callable = ..., ) -> None: ... def get_related_field(self) -> Field: ... - def set_field_name(self) -> None: ... class OneToOneRel(ManyToOneRel): - field_name: Optional[str] - hidden: bool - limit_choices_to: Dict[str, str] - many_to_many: bool - many_to_one: bool - model: Union[Type[Model], str] - name: str - on_delete: Callable - one_to_many: bool - one_to_one: bool - parent_link: bool - related_model: Type[Model] - related_name: Optional[str] - related_query_name: Optional[str] - symmetrical: bool - multiple: bool = ... def __init__( self, field: OneToOneField, @@ -123,14 +93,8 @@ class OneToOneRel(ManyToOneRel): ) -> None: ... class ManyToManyRel(ForeignObjectRel): - field_name: None - multiple: bool - name: str - parent_link: bool - related_model: Type[Model] through: Optional[Union[Type[Model], str]] = ... through_fields: Optional[Tuple[str, str]] = ... - symmetrical: bool = ... db_constraint: bool = ... def __init__( self, diff --git a/django-stubs/db/models/lookups.pyi b/django-stubs/db/models/lookups.pyi index 5d0a6a327..2a7529a48 100644 --- a/django-stubs/db/models/lookups.pyi +++ b/django-stubs/db/models/lookups.pyi @@ -1,6 +1,6 @@ from collections import OrderedDict from datetime import datetime -from typing import Any, Dict, List, Optional, Tuple, Type, Union, Iterable +from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union, Mapping from django.db.backends.sqlite3.base import DatabaseWrapper from django.db.models.expressions import Combinable, Expression, Func @@ -10,22 +10,21 @@ from django.db.models.sql.query import Query from django.utils.datastructures import OrderedSet from django.utils.safestring import SafeText -from django.db.models import lookups -from django.db.models.fields import TextField, related_lookups, Field +from django.db.models.fields import TextField, related_lookups class Lookup: - lookup_name: Any = ... + lookup_name: str = ... prepare_rhs: bool = ... can_use_none_as_rhs: bool = ... + lhs: Any = ... rhs: Any = ... - bilateral_transforms: Any = ... + bilateral_transforms: List[Type[Transform]] = ... def __init__(self, lhs: Union[Expression, TextField, related_lookups.MultiColSource], rhs: Any) -> None: ... def apply_bilateral_transforms(self, value: Expression) -> Transform: ... def batch_process_rhs( self, compiler: SQLCompiler, connection: DatabaseWrapper, rhs: Optional[OrderedSet] = ... ) -> Tuple[List[str], List[str]]: ... def get_source_expressions(self) -> List[Expression]: ... - lhs: Any = ... def set_source_expressions(self, new_exprs: List[Expression]) -> None: ... def get_prep_lookup(self) -> Any: ... def get_db_prep_lookup(self, value: Union[int, str], connection: DatabaseWrapper) -> Tuple[str, List[SafeText]]: ... @@ -36,20 +35,16 @@ class Lookup: self, compiler: SQLCompiler, connection: DatabaseWrapper ) -> Tuple[str, Union[List[Union[int, str]], Tuple[int, int]]]: ... def rhs_is_direct_value(self) -> bool: ... - def relabeled_clone( - self, relabels: Union[Dict[Optional[str], str], OrderedDict] - ) -> Union[BuiltinLookup, FieldGetDbPrepValueMixin]: ... + def relabeled_clone(self, relabels: Mapping[str, str]) -> Union[BuiltinLookup, FieldGetDbPrepValueMixin]: ... def get_group_by_cols(self) -> List[Expression]: ... - def as_sql(self, compiler: Any, connection: Any) -> None: ... + def as_sql(self, compiler: Any, connection: Any) -> Any: ... def contains_aggregate(self) -> bool: ... def contains_over_clause(self) -> bool: ... @property - def is_summary(self): ... + def is_summary(self) -> bool: ... class Transform(RegisterLookupMixin, Func): bilateral: bool = ... - arity: int = ... - output_field: Field @property def lhs(self) -> Expression: ... def get_bilateral_transforms(self) -> List[Type[Transform]]: ... @@ -61,122 +56,51 @@ class FieldGetDbPrepValueMixin: get_db_prep_lookup_value_is_iterable: bool = ... class FieldGetDbPrepValueIterableMixin(FieldGetDbPrepValueMixin): - get_db_prep_lookup_value_is_iterable: bool = ... def get_prep_lookup(self) -> Iterable[Any]: ... def resolve_expression_parameter( - self, compiler: SQLCompiler, connection: DatabaseWrapper, sql: str, param: Optional[Union[Combinable, int, str]] - ) -> Tuple[str, List[None]]: ... - -class Exact(FieldGetDbPrepValueMixin, BuiltinLookup): - bilateral_transforms: List[Type[lookups.Transform]] - lookup_name: str = ... - -class IExact(BuiltinLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... - -class GreaterThan(FieldGetDbPrepValueMixin, BuiltinLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... - -class GreaterThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... - -class LessThan(FieldGetDbPrepValueMixin, BuiltinLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... + self, compiler: SQLCompiler, connection: DatabaseWrapper, sql: str, param: Any + ) -> Any: ... -class LessThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): - bilateral_transforms: List[Type[lookups.Transform]] - lookup_name: str = ... +class Exact(FieldGetDbPrepValueMixin, BuiltinLookup): ... +class IExact(BuiltinLookup): ... +class GreaterThan(FieldGetDbPrepValueMixin, BuiltinLookup): ... +class GreaterThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): ... +class LessThan(FieldGetDbPrepValueMixin, BuiltinLookup): ... +class LessThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): ... class IntegerFieldFloatRounding: rhs: Any = ... - def get_prep_lookup(self) -> Union[Combinable, Query, int]: ... + def get_prep_lookup(self) -> Any: ... class IntegerGreaterThanOrEqual(IntegerFieldFloatRounding, GreaterThanOrEqual): ... class IntegerLessThan(IntegerFieldFloatRounding, LessThan): ... class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup): - bilateral_transforms: List[Type[lookups.Transform]] - lookup_name: str = ... - def get_rhs_op(self, connection: DatabaseWrapper, rhs: str) -> str: ... def split_parameter_list_as_sql(self, compiler: Any, connection: Any): ... class PatternLookup(BuiltinLookup): param_pattern: str = ... - prepare_rhs: bool = ... - def get_rhs_op(self, connection: DatabaseWrapper, rhs: str) -> str: ... - -class Contains(PatternLookup): - bilateral_transforms: List[Type[lookups.Transform]] - lookup_name: str = ... - -class IContains(Contains): - bilateral_transforms: List[Any] - lookup_name: str = ... - -class StartsWith(PatternLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... - param_pattern: str = ... - -class IStartsWith(StartsWith): - bilateral_transforms: List[Any] - lookup_name: str = ... - -class EndsWith(PatternLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... - param_pattern: str = ... - -class IEndsWith(EndsWith): - bilateral_transforms: List[Any] - lookup_name: str = ... - -class Range(FieldGetDbPrepValueIterableMixin, BuiltinLookup): - bilateral_transforms: List[Type[lookups.Transform]] - lookup_name: str = ... - -class IsNull(BuiltinLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... - prepare_rhs: bool = ... -class Regex(BuiltinLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... - prepare_rhs: bool = ... - -class IRegex(Regex): - bilateral_transforms: List[Any] - lookup_name: str = ... +class Contains(PatternLookup): ... +class IContains(Contains): ... +class StartsWith(PatternLookup): ... +class IStartsWith(StartsWith): ... +class EndsWith(PatternLookup): ... +class IEndsWith(EndsWith): ... +class Range(FieldGetDbPrepValueIterableMixin, BuiltinLookup): ... +class IsNull(BuiltinLookup): ... +class Regex(BuiltinLookup): ... +class IRegex(Regex): ... class YearLookup(Lookup): def year_lookup_bounds(self, connection: DatabaseWrapper, year: int) -> List[str]: ... class YearComparisonLookup(YearLookup): - bilateral_transforms: List[Any] def get_rhs_op(self, connection: DatabaseWrapper, rhs: str) -> str: ... def get_bound(self, start: datetime, finish: datetime) -> Any: ... -class YearExact(YearLookup, Exact): - bilateral_transforms: List[Any] - lookup_name: str = ... - -class YearGt(YearComparisonLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... - -class YearGte(YearComparisonLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... - -class YearLt(YearComparisonLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... - -class YearLte(YearComparisonLookup): - bilateral_transforms: List[Any] - lookup_name: str = ... +class YearExact(YearLookup, Exact): ... +class YearGt(YearComparisonLookup): ... +class YearGte(YearComparisonLookup): ... +class YearLt(YearComparisonLookup): ... +class YearLte(YearComparisonLookup): ... diff --git a/django-stubs/db/models/manager.pyi b/django-stubs/db/models/manager.pyi index e128c0fa3..0dfc2a167 100644 --- a/django-stubs/db/models/manager.pyi +++ b/django-stubs/db/models/manager.pyi @@ -9,9 +9,9 @@ class BaseManager(QuerySet[_T, _T]): creation_counter: int = ... auto_created: bool = ... use_in_migrations: bool = ... - def __new__(cls: Type[BaseManager], *args: Any, **kwargs: Any) -> BaseManager: ... - model: Optional[Any] = ... - name: Optional[Any] = ... + name: str = ... + model: Type[Model] = ... + db: str def __init__(self) -> None: ... def deconstruct(self) -> Tuple[bool, str, None, Tuple, Dict[str, int]]: ... def check(self, **kwargs: Any) -> List[Any]: ... @@ -35,8 +35,4 @@ class ManagerDescriptor: def __get__(self, instance: Optional[Model], cls: Type[Model] = ...) -> Manager: ... class EmptyManager(Manager): - creation_counter: int - name: None - model: Optional[Type[Model]] = ... def __init__(self, model: Type[Model]) -> None: ... - def get_queryset(self) -> QuerySet: ... diff --git a/django-stubs/db/models/options.pyi b/django-stubs/db/models/options.pyi index 1b58182b7..47458d8dd 100644 --- a/django-stubs/db/models/options.pyi +++ b/django-stubs/db/models/options.pyi @@ -1,5 +1,5 @@ import collections -from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, Type, Union +from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, Type, Union, TypeVar, Generic, Sequence from django.apps.config import AppConfig from django.apps.registry import Apps @@ -29,14 +29,14 @@ def make_immutable_fields_list( name: str, data: Union[Iterator[Any], List[Union[ArrayField, CIText]], List[Union[Field, FieldCacheMixin]]] ) -> ImmutableList: ... -class Options: +_M = TypeVar("_M", bound=Model) + +class Options(Generic[_M]): base_manager: Manager concrete_fields: ImmutableList default_manager: Manager fields: ImmutableList local_concrete_fields: ImmutableList - managers: ImmutableList - managers_map: Dict[str, Manager] related_objects: ImmutableList FORWARD_PROPERTIES: Any = ... REVERSE_PROPERTIES: Any = ... @@ -45,22 +45,22 @@ class Options: local_many_to_many: List[ManyToManyField] = ... private_fields: List[Any] = ... local_managers: List[Manager] = ... - base_manager_name: None = ... - default_manager_name: None = ... + base_manager_name: Optional[str] = ... + default_manager_name: Optional[str] = ... model_name: Optional[str] = ... verbose_name: Optional[str] = ... verbose_name_plural: Optional[str] = ... db_table: str = ... - ordering: List[str] = ... + ordering: Optional[List[str]] = ... indexes: List[Any] = ... unique_together: Union[List[Any], Tuple] = ... index_together: Union[List[Any], Tuple] = ... select_on_save: bool = ... - default_permissions: Tuple[str, str, str, str] = ... + default_permissions: Sequence[str] = ... permissions: List[Any] = ... object_name: Optional[str] = ... app_label: str = ... - get_latest_by: None = ... + get_latest_by: Optional[Sequence[str]] = ... order_with_respect_to: None = ... db_tablespace: str = ... required_db_features: List[Any] = ... @@ -78,7 +78,9 @@ class Options: auto_created: bool = ... related_fkey_lookups: List[Any] = ... apps: Apps = ... - default_related_name: None = ... + default_related_name: Optional[str] = ... + model: Type[Model] = ... + original_attrs: Dict[str, Any] = ... def __init__(self, meta: Optional[type], app_label: Optional[str] = ...) -> None: ... @property def label(self) -> str: ... @@ -88,8 +90,6 @@ class Options: def app_config(self) -> AppConfig: ... @property def installed(self): ... - model: Type[Model] = ... - original_attrs: Dict[str, Union[List[str], Apps, str]] = ... def contribute_to_class(self, cls: Type[Model], name: str) -> None: ... def add_manager(self, manager: Manager) -> None: ... def add_field(self, field: Union[GenericForeignKey, Field], private: bool = ...) -> None: ... @@ -100,12 +100,20 @@ class Options: def verbose_name_raw(self) -> Any: ... @property def swapped(self) -> Optional[str]: ... - def many_to_many(self) -> ImmutableList: ... - def fields_map(self) -> Dict[str, ForeignObjectRel]: ... + @property + def many_to_many(self) -> List[ManyToManyField]: ... + @property + def fields_map(self) -> Dict[str, Union[Field, ForeignObjectRel]]: ... + @property + def managers(self) -> List[Manager]: ... + @property + def managers_map(self) -> Dict[str, Manager]: ... def get_field(self, field_name: Union[Callable, str]) -> Field: ... def get_base_chain(self, model: Type[Model]) -> List[Type[Model]]: ... def get_parent_list(self) -> List[Type[Model]]: ... def get_ancestor_link(self, ancestor: Type[Model]) -> Optional[OneToOneField]: ... def get_path_to_parent(self, parent: Type[Model]) -> List[PathInfo]: ... def get_path_from_parent(self, parent: Type[Model]) -> List[PathInfo]: ... - def get_fields(self, include_parents: bool = ..., include_hidden: bool = ...) -> ImmutableList: ... + def get_fields( + self, include_parents: bool = ..., include_hidden: bool = ... + ) -> List[Union[Field, ForeignObjectRel]]: ... diff --git a/django-stubs/db/models/query.pyi b/django-stubs/db/models/query.pyi index e53adda84..53876843e 100644 --- a/django-stubs/db/models/query.pyi +++ b/django-stubs/db/models/query.pyi @@ -78,7 +78,7 @@ class QuerySet(Generic[_T, _Row], Collection[_Row], Sized): def iterator(self, chunk_size: int = ...) -> Iterator[_Row]: ... def aggregate(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: ... def get(self, *args: Any, **kwargs: Any) -> _Row: ... - def create(self, **kwargs: Any) -> _T: ... + def create(self, *args: Any, **kwargs: Any) -> _T: ... def bulk_create( self, objs: Iterable[Model], batch_size: Optional[int] = ..., ignore_conflicts: bool = ... ) -> List[_T]: ... @@ -88,16 +88,20 @@ class QuerySet(Generic[_T, _Row], Collection[_Row], Sized): ) -> Tuple[_T, bool]: ... def earliest(self, *fields: Any, field_name: Optional[Any] = ...) -> _Row: ... def latest(self, *fields: Any, field_name: Optional[Any] = ...) -> _Row: ... - def first(self) -> Optional[_Row]: ... - def last(self) -> Optional[_Row]: ... + # technically it's Optional[_Row], but it creates a lot of false-positives (same for last()) + def first(self) -> _Row: ... + def last(self) -> _Row: ... def in_bulk(self, id_list: Iterable[Any] = ..., *, field_name: str = ...) -> Dict[Any, _T]: ... def delete(self) -> Tuple[int, Dict[str, int]]: ... def update(self, **kwargs: Any) -> int: ... - def _update(self, values: Any) -> Optional[Any]: ... def exists(self) -> bool: ... def explain(self, *, format: Optional[Any] = ..., **options: Any) -> str: ... def raw( - self, raw_query: str, params: Any = ..., translations: Optional[Dict[str, str]] = ..., using: None = ... + self, + raw_query: str, + params: Any = ..., + translations: Optional[Dict[str, str]] = ..., + using: Optional[str] = ..., ) -> RawQuerySet: ... # The type of values may be overridden to be more specific in the mypy plugin, depending on the fields param def values(self, *fields: Union[str, Combinable], **expressions: Any) -> QuerySet[_T, Dict[str, Any]]: ... @@ -121,9 +125,11 @@ class QuerySet(Generic[_T, _Row], Collection[_Row], Sized): def select_for_update(self, nowait: bool = ..., skip_locked: bool = ..., of: Tuple = ...) -> QuerySet[_T, _Row]: ... def select_related(self, *fields: Any) -> QuerySet[_T, _Row]: ... def prefetch_related(self, *lookups: Any) -> QuerySet[_T, _Row]: ... - def annotate(self, *args: Any, **kwargs: Any) -> QuerySet[_T, _Row]: ... + # TODO: return type + def annotate(self, *args: Any, **kwargs: Any) -> QuerySet[Any, Any]: ... def order_by(self, *field_names: Any) -> QuerySet[_T, _Row]: ... def distinct(self, *field_names: Any) -> QuerySet[_T, _Row]: ... + # extra() return type won't be supported any time soon def extra( self, select: Optional[Dict[str, Any]] = ..., @@ -132,7 +138,7 @@ class QuerySet(Generic[_T, _Row], Collection[_Row], Sized): tables: Optional[List[str]] = ..., order_by: Optional[Sequence[str]] = ..., select_params: Optional[Sequence[Any]] = ..., - ) -> QuerySet[_T, _Row]: ... + ) -> QuerySet[Any, Any]: ... def reverse(self) -> QuerySet[_T, _Row]: ... def defer(self, *fields: Any) -> QuerySet[_T, _Row]: ... def only(self, *fields: Any) -> QuerySet[_T, _Row]: ... diff --git a/django-stubs/db/models/query_utils.pyi b/django-stubs/db/models/query_utils.pyi index afc30c830..141083c62 100644 --- a/django-stubs/db/models/query_utils.pyi +++ b/django-stubs/db/models/query_utils.pyi @@ -20,7 +20,7 @@ class QueryWrapper: contains_aggregate: bool = ... data: Tuple[str, List[Any]] = ... def __init__(self, sql: str, params: List[Any]) -> None: ... - def as_sql(self, compiler: SQLCompiler = ..., connection: Any = ...) -> Tuple[str, List[Any]]: ... + def as_sql(self, compiler: SQLCompiler = ..., connection: Any = ...) -> Any: ... class Q(tree.Node): children: Union[List[Dict[str, str]], List[Tuple[str, Any]], List[Q]] @@ -84,4 +84,4 @@ class FilteredRelation: def __init__(self, relation_name: str, *, condition: Any = ...) -> None: ... def clone(self) -> FilteredRelation: ... def resolve_expression(self, *args: Any, **kwargs: Any) -> None: ... - def as_sql(self, compiler: SQLCompiler, connection: Any) -> Tuple[str, List[Union[int, str]]]: ... + def as_sql(self, compiler: SQLCompiler, connection: Any) -> Any: ... diff --git a/django-stubs/db/models/sql/query.pyi b/django-stubs/db/models/sql/query.pyi index c8e279d6b..148450d13 100644 --- a/django-stubs/db/models/sql/query.pyi +++ b/django-stubs/db/models/sql/query.pyi @@ -2,8 +2,8 @@ import collections from collections import OrderedDict, namedtuple from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union -from django.db.models.lookups import Lookup -from django.db.models.query_utils import PathInfo +from django.db.models.lookups import Lookup, Transform +from django.db.models.query_utils import PathInfo, RegisterLookupMixin from django.db.models.sql.compiler import SQLCompiler from django.db.models.sql.datastructures import BaseTable from django.db.models.sql.where import WhereNode @@ -69,6 +69,8 @@ class Query: explain_query: bool = ... explain_format: Optional[str] = ... explain_options: Dict[str, int] = ... + high_mark: Optional[int] = ... + low_mark: int = ... def __init__(self, model: Optional[Type[Model]], where: Type[WhereNode] = ...) -> None: ... @property def extra(self) -> OrderedDict: ... @@ -98,8 +100,9 @@ class Query: def get_initial_alias(self) -> str: ... def count_active_tables(self) -> int: ... def resolve_expression(self, query: Query, *args: Any, **kwargs: Any) -> Query: ... - def as_sql(self, compiler: SQLCompiler, connection: Any) -> Tuple[str, Tuple]: ... + def as_sql(self, compiler: SQLCompiler, connection: Any) -> Any: ... def resolve_lookup_value(self, value: Any, can_reuse: Optional[Set[str]], allow_joins: bool) -> Any: ... + def solve_lookup_type(self, lookup: str) -> Tuple[Sequence[str], Sequence[str], bool]: ... def build_filter( self, filter_expr: Union[Dict[str, str], Tuple[str, Tuple[int, int]]], @@ -140,8 +143,6 @@ class Query: ) -> Tuple[WhereNode, Tuple]: ... def set_empty(self) -> None: ... def is_empty(self) -> bool: ... - high_mark: Optional[int] = ... - low_mark: int = ... def set_limits(self, low: Optional[int] = ..., high: Optional[int] = ...) -> None: ... def clear_limits(self) -> None: ... def has_limit_one(self) -> bool: ... @@ -177,7 +178,10 @@ class Query: def set_values(self, fields: Union[List[str], Tuple]) -> None: ... def trim_start(self, names_with_path: List[Tuple[str, List[PathInfo]]]) -> Tuple[str, bool]: ... def is_nullable(self, field: Field) -> bool: ... - def build_lookup(self, lookups: Sequence[str], lhs: Query, rhs: Optional[Query]) -> Lookup: ... + def build_lookup( + self, lookups: Sequence[str], lhs: Union[RegisterLookupMixin, Query], rhs: Optional[Query] + ) -> Lookup: ... + def try_transform(self, lhs: Union[RegisterLookupMixin, Query], name: str) -> Transform: ... class JoinPromoter: connector: str = ... diff --git a/django-stubs/db/models/sql/subqueries.pyi b/django-stubs/db/models/sql/subqueries.pyi index 35f2b0ab1..ba3d7e82a 100644 --- a/django-stubs/db/models/sql/subqueries.pyi +++ b/django-stubs/db/models/sql/subqueries.pyi @@ -1,104 +1,25 @@ -import collections -import uuid -from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Type, Union +from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union from django.db.models.base import Model from django.db.models.expressions import Case from django.db.models.query import QuerySet -from django.db.models.sql.datastructures import BaseTable from django.db.models.sql.query import Query from django.db.models.sql.where import WhereNode from django.db.models.fields import Field class DeleteQuery(Query): - alias_refcount: Dict[str, int] - annotation_select_mask: None - base_table: str - combinator: None - combinator_all: bool - combined_queries: Tuple - default_cols: bool - default_ordering: bool - deferred_loading: Tuple[frozenset, bool] - distinct: bool - distinct_fields: Tuple - explain_format: None - explain_options: Dict[Any, Any] - explain_query: bool - external_aliases: Set[Any] - extra_order_by: Tuple - extra_select_mask: None - extra_tables: Tuple - filter_is_sticky: bool - group_by: None - high_mark: None - low_mark: int - max_depth: int - model: Type[Model] - order_by: Tuple select: Tuple - select_for_update: bool - select_for_update_nowait: bool - select_for_update_of: Tuple - select_for_update_skip_locked: bool - select_related: bool - standard_ordering: bool - subq_aliases: frozenset - subquery: bool - table_map: Dict[str, List[str]] - used_aliases: Set[str] - values_select: Tuple where_class: Type[WhereNode] - compiler: str = ... where: WhereNode = ... def do_query(self, table: str, where: WhereNode, using: str) -> int: ... def delete_batch(self, pk_list: Union[List[int], List[str]], using: str) -> int: ... def delete_qs(self, query: QuerySet, using: str) -> int: ... class UpdateQuery(Query): - alias_refcount: Dict[str, int] - annotation_select_mask: Optional[Set[Any]] - base_table: str - combinator: None - combinator_all: bool - combined_queries: Tuple - default_cols: bool - default_ordering: bool - deferred_loading: Tuple[frozenset, bool] - distinct: bool - distinct_fields: Tuple - explain_format: None - explain_options: Dict[Any, Any] - explain_query: bool - external_aliases: Set[Any] - extra_order_by: Tuple - extra_select_mask: Optional[Set[Any]] - extra_tables: Tuple - filter_is_sticky: bool - group_by: Optional[bool] - high_mark: None - low_mark: int - max_depth: int - model: Type[Model] - order_by: Tuple select: Tuple - select_for_update: bool - select_for_update_nowait: bool - select_for_update_of: Tuple - select_for_update_skip_locked: bool - select_related: bool - standard_ordering: bool - subq_aliases: frozenset - subquery: bool - table_map: Dict[str, List[str]] - used_aliases: Set[str] - values: List[Tuple[Field, Optional[Type[Model]], Union[Case, uuid.UUID]]] - values_select: Tuple where_class: Type[WhereNode] - compiler: str = ... def __init__(self, *args: Any, **kwargs: Any) -> None: ... - def clone(self) -> UpdateQuery: ... where: WhereNode = ... def update_batch(self, pk_list: List[int], values: Dict[str, Optional[int]], using: str) -> None: ... def add_update_values(self, values: Dict[str, Any]) -> None: ... @@ -107,87 +28,18 @@ class UpdateQuery(Query): def get_related_updates(self) -> List[UpdateQuery]: ... class InsertQuery(Query): - alias_refcount: Dict[str, int] - annotation_select_mask: None - combinator: None - combinator_all: bool - combined_queries: Tuple - default_cols: bool - default_ordering: bool - deferred_loading: Tuple[frozenset, bool] - distinct: bool - distinct_fields: Tuple - explain_format: None - explain_options: Dict[Any, Any] - explain_query: bool - external_aliases: Set[Any] - extra_order_by: Tuple - extra_select_mask: None - extra_tables: Tuple - filter_is_sticky: bool - group_by: None - high_mark: None - low_mark: int - max_depth: int - model: Type[Model] - order_by: Tuple select: Tuple - select_for_update: bool - select_for_update_nowait: bool - select_for_update_of: Tuple - select_for_update_skip_locked: bool - select_related: bool - standard_ordering: bool - subquery: bool - table_map: Dict[str, List[str]] - used_aliases: Set[Any] - values_select: Tuple where: WhereNode where_class: Type[WhereNode] - compiler: str = ... fields: Iterable[Field] = ... objs: List[Model] = ... - def __init__(self, *args: Any, **kwargs: Any) -> None: ... raw: bool = ... + def __init__(self, *args: Any, **kwargs: Any) -> None: ... def insert_values(self, fields: Iterable[Field], objs: List[Model], raw: bool = ...) -> None: ... class AggregateQuery(Query): - alias_refcount: Dict[Any, Any] - annotation_select_mask: None - combinator: None - combinator_all: bool - combined_queries: Tuple - default_cols: bool - default_ordering: bool - deferred_loading: Tuple[frozenset, bool] - distinct: bool - distinct_fields: Tuple - explain_format: None - explain_options: Dict[Any, Any] - explain_query: bool - external_aliases: Set[Any] - extra_order_by: Tuple - extra_select_mask: None - extra_tables: Tuple - filter_is_sticky: bool - group_by: None - high_mark: None - low_mark: int - max_depth: int - model: Type[Model] - order_by: Tuple select: Tuple - select_for_update: bool - select_for_update_nowait: bool - select_for_update_of: Tuple - select_for_update_skip_locked: bool - select_related: bool - standard_ordering: bool sub_params: Tuple - table_map: Dict[Any, Any] - used_aliases: Set[Any] - values_select: Tuple where: WhereNode where_class: Type[WhereNode] - compiler: str = ... def add_subquery(self, query: Query, using: str) -> None: ... diff --git a/django-stubs/db/models/sql/where.pyi b/django-stubs/db/models/sql/where.pyi index 417956b2c..dfab304d5 100644 --- a/django-stubs/db/models/sql/where.pyi +++ b/django-stubs/db/models/sql/where.pyi @@ -18,7 +18,7 @@ class WhereNode(tree.Node): resolved: bool = ... conditional: bool = ... def split_having(self, negated: bool = ...) -> Tuple[Optional[WhereNode], Optional[WhereNode]]: ... - def as_sql(self, compiler: SQLCompiler, connection: Any) -> Tuple[str, List[Union[int, str]]]: ... + def as_sql(self, compiler: SQLCompiler, connection: Any) -> Any: ... def get_group_by_cols(self) -> List[Expression]: ... def relabel_aliases(self, change_map: Union[Dict[Optional[str], str], OrderedDict]) -> None: ... def clone(self) -> WhereNode: ... diff --git a/django-stubs/forms/fields.pyi b/django-stubs/forms/fields.pyi index 46999e8ef..06aefa9b5 100644 --- a/django-stubs/forms/fields.pyi +++ b/django-stubs/forms/fields.pyi @@ -100,8 +100,7 @@ class IntegerField(Field): label_suffix: Optional[Any] = ..., ) -> None: ... -class FloatField(IntegerField): - def validate(self, value: Optional[float]) -> None: ... +class FloatField(IntegerField): ... class DecimalField(IntegerField): decimal_places: Optional[int] @@ -125,7 +124,6 @@ class DecimalField(IntegerField): disabled: bool = ..., label_suffix: Optional[Any] = ..., ) -> None: ... - def validate(self, value: Optional[Decimal]) -> None: ... class BaseTemporalField(Field): input_formats: Any = ... diff --git a/django-stubs/http/request.pyi b/django-stubs/http/request.pyi index 035727adf..eae05c3c0 100644 --- a/django-stubs/http/request.pyi +++ b/django-stubs/http/request.pyi @@ -17,8 +17,10 @@ from typing import ( ) from django.contrib.sessions.backends.base import SessionBase +from django.db.models.base import Model from django.utils.datastructures import CaseInsensitiveMapping, ImmutableList, MultiValueDict +from django.contrib.auth.base_user import AbstractBaseUser from django.core.files import uploadedfile, uploadhandler from django.urls import ResolverMatch @@ -49,6 +51,7 @@ class HttpRequest(BytesIO): resolver_match: ResolverMatch = ... content_type: Optional[str] = ... content_params: Optional[Dict[str, str]] = ... + user: AbstractBaseUser session: SessionBase encoding: Optional[str] = ... upload_handlers: UploadHandlerList = ... diff --git a/django-stubs/middleware/csrf.pyi b/django-stubs/middleware/csrf.pyi index abfde511a..98193211c 100644 --- a/django-stubs/middleware/csrf.pyi +++ b/django-stubs/middleware/csrf.pyi @@ -22,7 +22,7 @@ def rotate_token(request: HttpRequest) -> None: ... class CsrfViewMiddleware(MiddlewareMixin): def process_request(self, request: HttpRequest) -> None: ... def process_view( - self, request: HttpRequest, callback: Callable, callback_args: Tuple, callback_kwargs: Dict[str, Any] + self, request: HttpRequest, callback: Optional[Callable], callback_args: Tuple, callback_kwargs: Dict[str, Any] ) -> Optional[HttpResponseForbidden]: ... def process_response(self, request: HttpRequest, response: HttpResponseBase) -> HttpResponseBase: ... diff --git a/django-stubs/template/response.pyi b/django-stubs/template/response.pyi index e68bd8df6..69aee5d7c 100644 --- a/django-stubs/template/response.pyi +++ b/django-stubs/template/response.pyi @@ -14,6 +14,7 @@ from django.http import HttpResponse class ContentNotRenderedError(Exception): ... class SimpleTemplateResponse(HttpResponse): + content: Any = ... closed: bool cookies: SimpleCookie status_code: int @@ -35,15 +36,10 @@ class SimpleTemplateResponse(HttpResponse): @property def rendered_content(self) -> str: ... def add_post_render_callback(self, callback: Callable) -> None: ... - content: Any = ... def render(self) -> SimpleTemplateResponse: ... @property def is_rendered(self) -> bool: ... def __iter__(self) -> Any: ... - @property - def content(self): ... - @content.setter - def content(self, value: Any) -> None: ... class TemplateResponse(SimpleTemplateResponse): client: Client diff --git a/django-stubs/utils/translation/__init__.pyi b/django-stubs/utils/translation/__init__.pyi index cc514d541..882dbb6ab 100644 --- a/django-stubs/utils/translation/__init__.pyi +++ b/django-stubs/utils/translation/__init__.pyi @@ -39,10 +39,10 @@ ungettext = ngettext def pgettext(context: str, message: str) -> str: ... def npgettext(context: str, singular: str, plural: str, number: int) -> str: ... -gettext_lazy: Any +gettext_lazy: Callable[[str], str] -ugettext_lazy: Any -pgettext_lazy: Any +ugettext_lazy: Callable[[str], str] +pgettext_lazy: Callable[[str], str] def ngettext_lazy(singular: Any, plural: Any, number: Optional[Any] = ...): ... diff --git a/mypy.ini b/mypy.ini index 0900f01f0..f8b1844b6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,4 +1 @@ [mypy] - -[mypy-mypy_django_plugin.monkeypatch.*] -ignore_errors = True \ No newline at end of file diff --git a/mypy_django_plugin/config.py b/mypy_django_plugin/config.py deleted file mode 100644 index c23df20ae..000000000 --- a/mypy_django_plugin/config.py +++ /dev/null @@ -1,31 +0,0 @@ -from configparser import ConfigParser -from typing import Optional - -from dataclasses import dataclass - - -@dataclass -class Config: - django_settings_module: Optional[str] = None - ignore_missing_settings: bool = False - ignore_missing_model_attributes: bool = False - - @classmethod - def from_config_file(cls, fpath: str) -> 'Config': - ini_config = ConfigParser() - ini_config.read(fpath) - if not ini_config.has_section('mypy_django_plugin'): - raise ValueError('Invalid config file: no [mypy_django_plugin] section') - - django_settings = ini_config.get('mypy_django_plugin', 'django_settings', - fallback=None) - if django_settings: - django_settings = django_settings.strip() - - return Config(django_settings_module=django_settings, - ignore_missing_settings=bool(ini_config.get('mypy_django_plugin', - 'ignore_missing_settings', - fallback=False)), - ignore_missing_model_attributes=bool(ini_config.get('mypy_django_plugin', - 'ignore_missing_model_attributes', - fallback=False))) diff --git a/mypy_django_plugin/django/__init__.py b/mypy_django_plugin/django/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py new file mode 100644 index 000000000..778caf43a --- /dev/null +++ b/mypy_django_plugin/django/context.py @@ -0,0 +1,252 @@ +import os +from collections import defaultdict +from contextlib import contextmanager +from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Tuple, Type + +from django.contrib.postgres.fields import ArrayField +from django.core.exceptions import FieldError +from django.db.models.base import Model +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.sql.query import Query +from django.utils.functional import cached_property +from mypy.checker import TypeChecker +from mypy.types import AnyType, Instance +from mypy.types import Type as MypyType +from mypy.types import TypeOfAny + +from mypy_django_plugin.lib import helpers + +if TYPE_CHECKING: + from django.apps.registry import Apps # noqa: F401 + from django.conf import LazySettings # noqa: F401 + + +@contextmanager +def temp_environ(): + """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 + + def noop_class_getitem(cls, key): + return cls + + from django.db import models + + models.QuerySet.__class_getitem__ = classmethod(noop_class_getitem) # type: ignore + models.Manager.__class_getitem__ = classmethod(noop_class_getitem) # type: ignore + + from django.conf import settings + from django.apps import apps + + apps.get_models.cache_clear() # type: ignore + apps.get_swappable_settings_name.cache_clear() # type: ignore + + if not settings.configured: + settings._setup() + + apps.populate(settings.INSTALLED_APPS) + + assert apps.apps_ready + assert settings.configured + + return apps, settings + + +class DjangoFieldsContext: + def __init__(self, django_context: 'DjangoContext') -> None: + self.django_context = django_context + + def get_attname(self, field: Field) -> str: + attname = field.attname + return attname + + def get_field_nullability(self, field: Field, method: Optional[str]) -> bool: + nullable = field.null + if not nullable and isinstance(field, CharField) and field.blank: + return True + if method == '__init__': + if field.primary_key or isinstance(field, ForeignKey): + return True + if method == 'create': + if isinstance(field, AutoField): + return True + if field.has_default(): + return True + return nullable + + def get_field_set_type(self, api: TypeChecker, field: Field, *, method: str) -> MypyType: + """ Get a type of __set__ for this specific Django field. """ + target_field = field + if isinstance(field, ForeignKey): + target_field = field.target_field + + 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, field: Field, *, method: str) -> MypyType: + """ Get a type of __get__ for this specific Django field. """ + 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): + if method == 'values': + primary_key_field = self.django_context.get_primary_key_field(field.related_model) + return self.get_field_get_type(api, primary_key_field, method=method) + + model_info = helpers.lookup_class_typeinfo(api, field.related_model) + if model_info is None: + return AnyType(TypeOfAny.unannotated) + + return Instance(model_info, []) + else: + return helpers.get_private_descriptor_type(field_info, '_pyi_private_get_type', + is_nullable=is_nullable) + + +class DjangoLookupsContext: + def __init__(self, django_context: 'DjangoContext'): + self.django_context = django_context + + def resolve_lookup(self, model_cls: Type[Model], lookup: str) -> Field: + query = Query(model_cls) + lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup) + if lookup_parts: + raise FieldError('Lookups not supported yet') + + currently_observed_model = model_cls + current_field = None + for field_part in field_parts: + if field_part == 'pk': + return self.django_context.get_primary_key_field(currently_observed_model) + + current_field = currently_observed_model._meta.get_field(field_part) + if isinstance(current_field, ForeignObjectRel): + currently_observed_model = current_field.related_model + current_field = self.django_context.get_primary_key_field(currently_observed_model) + else: + if isinstance(current_field, RelatedField): + currently_observed_model = current_field.related_model + + # if it is None, solve_lookup_type() will fail earlier + assert current_field is not None + return current_field + + +class DjangoContext: + def __init__(self, django_settings_module: str) -> None: + self.fields_context = DjangoFieldsContext(self) + self.lookups_context = DjangoLookupsContext(self) + + 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, List[Type[Model]]]: + """ All modules that contain Django models. """ + if self.apps_registry is None: + return {} + + modules: Dict[str, List[Type[Model]]] = defaultdict(list) + for model_cls in self.apps_registry.get_models(): + modules[model_cls.__module__].append(model_cls) + return modules + + def get_model_class_by_fullname(self, fullname: str) -> Optional[Type[Model]]: + # Returns None if Model is abstract + module, _, model_cls_name = fullname.rpartition('.') + for model_cls in self.model_modules.get(module, []): + if model_cls.__name__ == model_cls_name: + return model_cls + return None + + def get_model_fields(self, model_cls: Type[Model]) -> Iterator[Field]: + for field in model_cls._meta.get_fields(): + if isinstance(field, Field): + yield field + + def get_model_relations(self, model_cls: Type[Model]) -> Iterator[ForeignObjectRel]: + for field in model_cls._meta.get_fields(): + if isinstance(field, ForeignObjectRel): + yield field + + def get_primary_key_field(self, model_cls: Type[Model]) -> Field: + 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]: + from django.contrib.contenttypes.fields import GenericForeignKey + + expected_types = {} + # add pk + primary_key_field = self.get_primary_key_field(model_cls) + field_set_type = self.fields_context.get_field_set_type(api, primary_key_field, method=method) + expected_types['pk'] = field_set_type + + for field in model_cls._meta.get_fields(): + if isinstance(field, Field): + field_name = field.attname + field_set_type = self.fields_context.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 + + related_model = field.related_model + if related_model._meta.proxy_for_model: + related_model = field.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.fields_context.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 isinstance(field, GenericForeignKey): + # it's generic, so cannot set specific model + field_name = field.name + gfk_info = helpers.lookup_class_typeinfo(api, field.__class__) + 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 diff --git a/mypy_django_plugin/helpers.py b/mypy_django_plugin/helpers.py deleted file mode 100644 index abdc0861b..000000000 --- a/mypy_django_plugin/helpers.py +++ /dev/null @@ -1,443 +0,0 @@ -import typing -from collections import OrderedDict -from typing import Dict, Optional, cast - -from mypy.mro import calculate_mro -from mypy.nodes import ( - GDEF, MDEF, AssignmentStmt, Block, CallExpr, ClassDef, Expression, ImportedName, Lvalue, MypyFile, NameExpr, - SymbolNode, SymbolTable, SymbolTableNode, TypeInfo, Var, -) -from mypy.plugin import CheckerPluginInterface, FunctionContext, MethodContext -from mypy.types import ( - AnyType, Instance, NoneTyp, TupleType, Type, TypedDictType, TypeOfAny, TypeVarType, UnionType, -) - -if typing.TYPE_CHECKING: - from mypy.checker import TypeChecker - -MODEL_CLASS_FULLNAME = 'django.db.models.base.Model' -FIELD_FULLNAME = 'django.db.models.fields.Field' -CHAR_FIELD_FULLNAME = 'django.db.models.fields.CharField' -ARRAY_FIELD_FULLNAME = 'django.contrib.postgres.fields.array.ArrayField' -AUTO_FIELD_FULLNAME = 'django.db.models.fields.AutoField' -GENERIC_FOREIGN_KEY_FULLNAME = 'django.contrib.contenttypes.fields.GenericForeignKey' -FOREIGN_KEY_FULLNAME = 'django.db.models.fields.related.ForeignKey' -ONETOONE_FIELD_FULLNAME = 'django.db.models.fields.related.OneToOneField' -MANYTOMANY_FIELD_FULLNAME = 'django.db.models.fields.related.ManyToManyField' -DUMMY_SETTINGS_BASE_CLASS = 'django.conf._DjangoConfLazyObject' - -QUERYSET_CLASS_FULLNAME = 'django.db.models.query.QuerySet' -BASE_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.BaseManager' -MANAGER_CLASS_FULLNAME = 'django.db.models.manager.Manager' -RELATED_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.RelatedManager' - -BASEFORM_CLASS_FULLNAME = 'django.forms.forms.BaseForm' -FORM_CLASS_FULLNAME = 'django.forms.forms.Form' -MODELFORM_CLASS_FULLNAME = 'django.forms.models.ModelForm' - -FORM_MIXIN_CLASS_FULLNAME = 'django.views.generic.edit.FormMixin' - -MANAGER_CLASSES = { - MANAGER_CLASS_FULLNAME, - RELATED_MANAGER_CLASS_FULLNAME, - BASE_MANAGER_CLASS_FULLNAME, - QUERYSET_CLASS_FULLNAME -} - - -def get_models_file(app_name: str, all_modules: typing.Dict[str, MypyFile]) -> Optional[MypyFile]: - models_module = '.'.join([app_name, 'models']) - return all_modules.get(models_module) - - -def get_model_fullname(app_name: str, model_name: str, - all_modules: Dict[str, MypyFile]) -> Optional[str]: - models_file = get_models_file(app_name, all_modules) - if models_file is None: - # not imported so far, not supported - return None - sym = models_file.names.get(model_name) - if not sym: - return None - - if isinstance(sym.node, TypeInfo): - return sym.node.fullname() - elif isinstance(sym.node, ImportedName): - return sym.node.target_fullname - else: - return None - - -class SameFileModel(Exception): - def __init__(self, model_cls_name: str): - self.model_cls_name = model_cls_name - - -class SelfReference(ValueError): - pass - - -def get_model_fullname_from_string(model_string: str, - all_modules: Dict[str, MypyFile]) -> Optional[str]: - if model_string == 'self': - raise SelfReference() - - if '.' not in model_string: - raise SameFileModel(model_string) - - app_name, model_name = model_string.split('.') - return get_model_fullname(app_name, model_name, all_modules) - - -def lookup_fully_qualified_generic(name: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolNode]: - if '.' not in name: - return None - module, cls_name = name.rsplit('.', 1) - - module_file = all_modules.get(module) - if module_file is None: - return None - sym = module_file.names.get(cls_name) - if sym is None: - return None - return sym.node - - -def parse_bool(expr: Expression) -> Optional[bool]: - if isinstance(expr, NameExpr): - if expr.fullname == 'builtins.True': - return True - if expr.fullname == 'builtins.False': - return False - return None - - -def reparametrize_instance(instance: Instance, new_args: typing.List[Type]) -> Instance: - return Instance(instance.type, args=new_args, - line=instance.line, column=instance.column) - - -def fill_typevars_with_any(instance: Instance) -> Instance: - return reparametrize_instance(instance, [AnyType(TypeOfAny.unannotated)]) - - -def extract_typevar_value(tp: Instance, typevar_name: str) -> Type: - if typevar_name in {'_T', '_T_co'}: - if '_T' in tp.type.type_vars: - return tp.args[tp.type.type_vars.index('_T')] - if '_T_co' in tp.type.type_vars: - return tp.args[tp.type.type_vars.index('_T_co')] - return tp.args[tp.type.type_vars.index(typevar_name)] - - -def fill_typevars(tp: Instance, type_to_fill: Instance) -> Instance: - typevar_values: typing.List[Type] = [] - for typevar_arg in type_to_fill.args: - if isinstance(typevar_arg, TypeVarType): - typevar_values.append(extract_typevar_value(tp, typevar_arg.name)) - return Instance(type_to_fill.type, typevar_values) - - -def get_argument_by_name(ctx: typing.Union[FunctionContext, MethodContext], name: str) -> Optional[Expression]: - """Return the expression for the specific argument. - - This helper should only be used with non-star arguments. - """ - if name not in ctx.callee_arg_names: - return None - idx = ctx.callee_arg_names.index(name) - args = ctx.args[idx] - if len(args) != 1: - # Either an error or no value passed. - return None - return args[0] - - -def get_argument_type_by_name(ctx: typing.Union[FunctionContext, MethodContext], name: str) -> Optional[Type]: - """Return the type for the specific argument. - - This helper should only be used with non-star arguments. - """ - if name not in ctx.callee_arg_names: - return None - idx = ctx.callee_arg_names.index(name) - arg_types = ctx.arg_types[idx] - if len(arg_types) != 1: - # Either an error or no value passed. - return None - return arg_types[0] - - -def get_setting_expr(api: 'TypeChecker', setting_name: str) -> Optional[Expression]: - try: - settings_sym = api.modules['django.conf'].names['settings'] - except KeyError: - return None - - settings_type: TypeInfo = settings_sym.type.type - auth_user_model_sym = settings_type.get(setting_name) - if not auth_user_model_sym: - return None - - module, _, name = auth_user_model_sym.fullname.rpartition('.') - if module not in api.modules: - return None - - module_file = api.modules.get(module) - for name_expr, value_expr in iter_over_assignments(module_file): - if isinstance(name_expr, NameExpr) and name_expr.name == setting_name: - return value_expr - return None - - -def iter_over_assignments(class_or_module: typing.Union[ClassDef, MypyFile] - ) -> typing.Iterator[typing.Tuple[Lvalue, Expression]]: - if isinstance(class_or_module, ClassDef): - statements = class_or_module.defs.body - else: - statements = class_or_module.defs - - for stmt in statements: - if not isinstance(stmt, AssignmentStmt): - continue - if len(stmt.lvalues) > 1: - # not supported yet - continue - yield stmt.lvalues[0], stmt.rvalue - - -def extract_field_setter_type(tp: Instance) -> Optional[Type]: - """ Extract __set__ value of a field. """ - if tp.type.has_base(FIELD_FULLNAME): - return tp.args[0] - # GenericForeignKey - if tp.type.has_base(GENERIC_FOREIGN_KEY_FULLNAME): - return AnyType(TypeOfAny.special_form) - return None - - -def extract_field_getter_type(tp: Type) -> Optional[Type]: - """ Extract return type of __get__ of subclass of Field""" - if not isinstance(tp, Instance): - return None - if tp.type.has_base(FIELD_FULLNAME): - return tp.args[1] - # GenericForeignKey - if tp.type.has_base(GENERIC_FOREIGN_KEY_FULLNAME): - return AnyType(TypeOfAny.special_form) - return None - - -def get_django_metadata(model_info: TypeInfo) -> Dict[str, typing.Any]: - return model_info.metadata.setdefault('django', {}) - - -def get_related_field_primary_key_names(base_model: TypeInfo) -> typing.List[str]: - return get_django_metadata(base_model).setdefault('related_field_primary_keys', []) - - -def get_fields_metadata(model: TypeInfo) -> Dict[str, typing.Any]: - return get_django_metadata(model).setdefault('fields', {}) - - -def get_lookups_metadata(model: TypeInfo) -> Dict[str, typing.Any]: - return get_django_metadata(model).setdefault('lookups', {}) - - -def get_related_managers_metadata(model: TypeInfo) -> Dict[str, typing.Any]: - return get_django_metadata(model).setdefault('related_managers', {}) - - -def extract_explicit_set_type_of_model_primary_key(model: TypeInfo) -> Optional[Type]: - """ - If field with primary_key=True is set on the model, extract its __set__ type. - """ - for field_name, props in get_fields_metadata(model).items(): - is_primary_key = props.get('primary_key', False) - if is_primary_key: - return extract_field_setter_type(model.names[field_name].type) - return None - - -def extract_primary_key_type_for_get(model: TypeInfo) -> Optional[Type]: - for field_name, props in get_fields_metadata(model).items(): - is_primary_key = props.get('primary_key', False) - if is_primary_key: - return extract_field_getter_type(model.names[field_name].type) - return None - - -def make_optional(typ: Type): - return UnionType.make_union([typ, NoneTyp()]) - - -def make_required(typ: Type) -> Type: - if not isinstance(typ, UnionType): - return typ - items = [item for item in typ.items if not isinstance(item, NoneTyp)] - # will reduce to Instance, if only one item - return UnionType.make_union(items) - - -def is_optional(typ: Type) -> bool: - if not isinstance(typ, UnionType): - return False - - return any([isinstance(item, NoneTyp) for item in typ.items]) - - -def has_any_of_bases(info: TypeInfo, bases: typing.Sequence[str]) -> bool: - for base_fullname in bases: - if info.has_base(base_fullname): - return True - return False - - -def is_none_expr(expr: Expression) -> bool: - return isinstance(expr, NameExpr) and expr.fullname == 'builtins.None' - - -def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo]: - metaclass_sym = info.names.get('Meta') - if metaclass_sym is not None and isinstance(metaclass_sym.node, TypeInfo): - return metaclass_sym.node - return None - - -def get_assigned_value_for_class(type_info: TypeInfo, name: str) -> Optional[Expression]: - for lvalue, rvalue in iter_over_assignments(type_info.defn): - if isinstance(lvalue, NameExpr) and lvalue.name == name: - return rvalue - return None - - -def is_field_nullable(model: TypeInfo, field_name: str) -> bool: - return get_fields_metadata(model).get(field_name, {}).get('null', False) - - -def is_foreign_key_like(t: Type) -> bool: - if not isinstance(t, Instance): - return False - return has_any_of_bases(t.type, (FOREIGN_KEY_FULLNAME, ONETOONE_FIELD_FULLNAME)) - - -def build_class_with_annotated_fields(api: 'TypeChecker', base: Type, fields: 'OrderedDict[str, Type]', - name: str) -> Instance: - """Build an Instance with `name` that contains the specified `fields` as attributes and extends `base`.""" - # Credit: This code is largely copied/modified from TypeChecker.intersect_instance_callable and - # NamedTupleAnalyzer.build_namedtuple_typeinfo - from mypy.checker import gen_unique_name - - cur_module = cast(MypyFile, api.scope.stack[0]) - gen_name = gen_unique_name(name, cur_module.names) - - cdef = ClassDef(name, Block([])) - cdef.fullname = cur_module.fullname() + '.' + gen_name - info = TypeInfo(SymbolTable(), cdef, cur_module.fullname()) - cdef.info = info - info.bases = [base] - - def add_field(var: Var, is_initialized_in_class: bool = False, - is_property: bool = False) -> None: - var.info = info - var.is_initialized_in_class = is_initialized_in_class - var.is_property = is_property - var._fullname = '%s.%s' % (info.fullname(), var.name()) - info.names[var.name()] = SymbolTableNode(MDEF, var) - - vars = [Var(item, typ) for item, typ in fields.items()] - for var in vars: - add_field(var, is_property=True) - - calculate_mro(info) - info.calculate_metaclass_type() - - cur_module.names[gen_name] = SymbolTableNode(GDEF, info, plugin_generated=True) - return Instance(info, []) - - -def make_named_tuple(api: 'TypeChecker', fields: 'OrderedDict[str, Type]', name: str) -> Type: - if not fields: - # No fields specified, so fallback to a subclass of NamedTuple that allows - # __getattr__ / __setattr__ for any attribute name. - fallback = api.named_generic_type('django._NamedTupleAnyAttr', []) - else: - fallback = build_class_with_annotated_fields( - api=api, - base=api.named_generic_type('typing.NamedTuple', []), - fields=fields, - name=name - ) - return TupleType(list(fields.values()), fallback=fallback) - - -def make_typeddict(api: CheckerPluginInterface, fields: 'OrderedDict[str, Type]', - required_keys: typing.Set[str]) -> TypedDictType: - object_type = api.named_generic_type('mypy_extensions._TypedDict', []) - typed_dict_type = TypedDictType(fields, required_keys=required_keys, fallback=object_type) - return typed_dict_type - - -def make_tuple(api: 'TypeChecker', fields: typing.List[Type]) -> TupleType: - implicit_any = AnyType(TypeOfAny.special_form) - fallback = api.named_generic_type('builtins.tuple', [implicit_any]) - return TupleType(fields, fallback=fallback) - - -def get_private_descriptor_type(type_info: TypeInfo, private_field_name: str, is_nullable: bool) -> Type: - node = type_info.get(private_field_name).node - if isinstance(node, Var): - descriptor_type = node.type - if is_nullable: - descriptor_type = make_optional(descriptor_type) - return descriptor_type - return AnyType(TypeOfAny.unannotated) - - -def iter_over_classdefs(module_file: MypyFile) -> typing.Iterator[ClassDef]: - for defn in module_file.defs: - if isinstance(defn, ClassDef): - yield defn - - -def iter_call_assignments(klass: ClassDef) -> typing.Iterator[typing.Tuple[Lvalue, CallExpr]]: - for lvalue, rvalue in iter_over_assignments(klass): - if isinstance(rvalue, CallExpr): - yield lvalue, rvalue - - -def get_related_manager_type_from_metadata(model_info: TypeInfo, related_manager_name: str, - api: CheckerPluginInterface) -> Optional[Instance]: - related_manager_metadata = get_related_managers_metadata(model_info) - if not related_manager_metadata: - return None - - if related_manager_name not in related_manager_metadata: - return None - - manager_class_name = related_manager_metadata[related_manager_name]['manager'] - of = related_manager_metadata[related_manager_name]['of'] - of_types = [] - for of_type_name in of: - if of_type_name == 'any': - of_types.append(AnyType(TypeOfAny.implementation_artifact)) - else: - try: - of_type = api.named_generic_type(of_type_name, []) - except AssertionError: - # Internal error: attempted lookup of unknown name - of_type = AnyType(TypeOfAny.implementation_artifact) - - of_types.append(of_type) - - return api.named_generic_type(manager_class_name, of_types) - - -def get_primary_key_field_name(model_info: TypeInfo) -> Optional[str]: - for base in model_info.mro: - fields = get_fields_metadata(base) - for field_name, field_props in fields.items(): - is_primary_key = field_props.get('primary_key', False) - if is_primary_key: - return field_name - return None diff --git a/mypy_django_plugin/lib/__init__.py b/mypy_django_plugin/lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mypy_django_plugin/lib/fullnames.py b/mypy_django_plugin/lib/fullnames.py new file mode 100644 index 000000000..15e5ce8fc --- /dev/null +++ b/mypy_django_plugin/lib/fullnames.py @@ -0,0 +1,39 @@ + +MODEL_CLASS_FULLNAME = 'django.db.models.base.Model' +FIELD_FULLNAME = 'django.db.models.fields.Field' +CHAR_FIELD_FULLNAME = 'django.db.models.fields.CharField' +ARRAY_FIELD_FULLNAME = 'django.contrib.postgres.fields.array.ArrayField' +AUTO_FIELD_FULLNAME = 'django.db.models.fields.AutoField' +GENERIC_FOREIGN_KEY_FULLNAME = 'django.contrib.contenttypes.fields.GenericForeignKey' +FOREIGN_KEY_FULLNAME = 'django.db.models.fields.related.ForeignKey' +ONETOONE_FIELD_FULLNAME = 'django.db.models.fields.related.OneToOneField' +MANYTOMANY_FIELD_FULLNAME = 'django.db.models.fields.related.ManyToManyField' +DUMMY_SETTINGS_BASE_CLASS = 'django.conf._DjangoConfLazyObject' + +QUERYSET_CLASS_FULLNAME = 'django.db.models.query.QuerySet' +BASE_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.BaseManager' +MANAGER_CLASS_FULLNAME = 'django.db.models.manager.Manager' +RELATED_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.RelatedManager' + +BASEFORM_CLASS_FULLNAME = 'django.forms.forms.BaseForm' +FORM_CLASS_FULLNAME = 'django.forms.forms.Form' +MODELFORM_CLASS_FULLNAME = 'django.forms.models.ModelForm' + +FORM_MIXIN_CLASS_FULLNAME = 'django.views.generic.edit.FormMixin' + +MANAGER_CLASSES = { + MANAGER_CLASS_FULLNAME, + RELATED_MANAGER_CLASS_FULLNAME, + BASE_MANAGER_CLASS_FULLNAME, + # QUERYSET_CLASS_FULLNAME +} + +RELATED_FIELDS_CLASSES = { + FOREIGN_KEY_FULLNAME, + ONETOONE_FIELD_FULLNAME, + MANYTOMANY_FIELD_FULLNAME +} + +MIGRATION_CLASS_FULLNAME = 'django.db.migrations.migration.Migration' +OPTIONS_CLASS_FULLNAME = 'django.db.models.options.Options' +HTTPREQUEST_CLASS_FULLNAME = 'django.http.request.HttpRequest' diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py new file mode 100644 index 000000000..3555e5ac9 --- /dev/null +++ b/mypy_django_plugin/lib/helpers.py @@ -0,0 +1,253 @@ +from collections import OrderedDict +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union, cast + +from mypy import checker +from mypy.checker import TypeChecker +from mypy.mro import calculate_mro +from mypy.nodes import ( + GDEF, MDEF, Block, ClassDef, Expression, MemberExpr, MypyFile, NameExpr, StrExpr, SymbolNode, SymbolTable, + SymbolTableNode, TypeInfo, Var, +) +from mypy.plugin import ( + AttributeContext, CheckerPluginInterface, FunctionContext, MethodContext, +) +from mypy.types import AnyType, Instance, NoneTyp, TupleType +from mypy.types import Type as MypyType +from mypy.types import TypedDictType, TypeOfAny, UnionType + +if TYPE_CHECKING: + from mypy_django_plugin.django.context import DjangoContext + + +def get_django_metadata(model_info: TypeInfo) -> Dict[str, Any]: + return model_info.metadata.setdefault('django', {}) + + +class IncompleteDefnException(Exception): + pass + + +def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolTableNode]: + if '.' not in fullname: + return None + module, cls_name = fullname.rsplit('.', 1) + + module_file = all_modules.get(module) + if module_file is None: + return None + sym = module_file.names.get(cls_name) + if sym is None: + return None + return sym + + +def lookup_fully_qualified_generic(name: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolNode]: + sym = lookup_fully_qualified_sym(name, all_modules) + if sym is None: + return None + return sym.node + + +def lookup_fully_qualified_typeinfo(api: TypeChecker, fullname: str) -> Optional[TypeInfo]: + node = lookup_fully_qualified_generic(fullname, api.modules) + if not isinstance(node, TypeInfo): + return None + return node + + +def lookup_class_typeinfo(api: TypeChecker, klass: type) -> Optional[TypeInfo]: + fullname = get_class_fullname(klass) + field_info = lookup_fully_qualified_typeinfo(api, fullname) + return field_info + + +def reparametrize_instance(instance: Instance, new_args: List[MypyType]) -> Instance: + return Instance(instance.type, args=new_args, + line=instance.line, column=instance.column) + + +def get_class_fullname(klass: type) -> str: + return klass.__module__ + '.' + klass.__qualname__ + + +def get_call_argument_by_name(ctx: Union[FunctionContext, MethodContext], name: str) -> Optional[Expression]: + """ + Return the expression for the specific argument. + This helper should only be used with non-star arguments. + """ + if name not in ctx.callee_arg_names: + return None + idx = ctx.callee_arg_names.index(name) + args = ctx.args[idx] + if len(args) != 1: + # Either an error or no value passed. + return None + return args[0] + + +def get_call_argument_type_by_name(ctx: Union[FunctionContext, MethodContext], name: str) -> Optional[MypyType]: + """Return the type for the specific argument. + + This helper should only be used with non-star arguments. + """ + if name not in ctx.callee_arg_names: + return None + idx = ctx.callee_arg_names.index(name) + arg_types = ctx.arg_types[idx] + if len(arg_types) != 1: + # Either an error or no value passed. + return None + return arg_types[0] + + +def make_optional(typ: MypyType) -> MypyType: + return UnionType.make_union([typ, NoneTyp()]) + + +def parse_bool(expr: Expression) -> Optional[bool]: + if isinstance(expr, NameExpr): + if expr.fullname == 'builtins.True': + return True + if expr.fullname == 'builtins.False': + return False + return None + + +def has_any_of_bases(info: TypeInfo, bases: Set[str]) -> bool: + for base_fullname in bases: + if info.has_base(base_fullname): + return True + return False + + +def get_private_descriptor_type(type_info: TypeInfo, private_field_name: str, is_nullable: bool) -> MypyType: + """ Return declared type of type_info's private_field_name (used for private Field attributes)""" + sym = type_info.get(private_field_name) + if sym is None: + return AnyType(TypeOfAny.unannotated) + + node = sym.node + if isinstance(node, Var): + descriptor_type = node.type + if descriptor_type is None: + return AnyType(TypeOfAny.unannotated) + + if is_nullable: + descriptor_type = make_optional(descriptor_type) + return descriptor_type + return AnyType(TypeOfAny.unannotated) + + +def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo]: + metaclass_sym = info.names.get('Meta') + if metaclass_sym is not None and isinstance(metaclass_sym.node, TypeInfo): + return metaclass_sym.node + return None + + +def add_new_class_for_module(module: MypyFile, name: str, bases: List[Instance], + fields: 'OrderedDict[str, MypyType]') -> TypeInfo: + new_class_unique_name = checker.gen_unique_name(name, module.names) + + # make new class expression + classdef = ClassDef(new_class_unique_name, Block([])) + classdef.fullname = module.fullname() + '.' + new_class_unique_name + + # make new TypeInfo + new_typeinfo = TypeInfo(SymbolTable(), classdef, module.fullname()) + new_typeinfo.bases = bases + calculate_mro(new_typeinfo) + new_typeinfo.calculate_metaclass_type() + + def add_field_to_new_typeinfo(var: Var, is_initialized_in_class: bool = False, + is_property: bool = False) -> None: + var.info = new_typeinfo + var.is_initialized_in_class = is_initialized_in_class + var.is_property = is_property + var._fullname = new_typeinfo.fullname() + '.' + var.name() + new_typeinfo.names[var.name()] = SymbolTableNode(MDEF, var) + + # add fields + var_items = [Var(item, typ) for item, typ in fields.items()] + for var_item in var_items: + add_field_to_new_typeinfo(var_item, is_property=True) + + classdef.info = new_typeinfo + module.names[new_class_unique_name] = SymbolTableNode(GDEF, new_typeinfo, plugin_generated=True) + return new_typeinfo + + +def get_current_module(api: TypeChecker) -> MypyFile: + current_module = None + for item in reversed(api.scope.stack): + if isinstance(item, MypyFile): + current_module = item + break + assert current_module is not None + return current_module + + +def make_oneoff_named_tuple(api: TypeChecker, name: str, fields: 'OrderedDict[str, MypyType]') -> TupleType: + current_module = get_current_module(api) + namedtuple_info = add_new_class_for_module(current_module, name, + bases=[api.named_generic_type('typing.NamedTuple', [])], + fields=fields) + return TupleType(list(fields.values()), fallback=Instance(namedtuple_info, [])) + + +def make_tuple(api: 'TypeChecker', fields: List[MypyType]) -> TupleType: + # fallback for tuples is any builtins.tuple instance + fallback = api.named_generic_type('builtins.tuple', + [AnyType(TypeOfAny.special_form)]) + return TupleType(fields, fallback=fallback) + + +def convert_any_to_type(typ: MypyType, referred_to_type: MypyType) -> MypyType: + if isinstance(typ, UnionType): + converted_items = [] + for item in typ.items: + converted_items.append(convert_any_to_type(item, referred_to_type)) + return UnionType.make_union(converted_items, + line=typ.line, column=typ.column) + if isinstance(typ, Instance): + args = [] + for default_arg in typ.args: + if isinstance(default_arg, AnyType): + args.append(referred_to_type) + else: + args.append(default_arg) + return reparametrize_instance(typ, args) + + if isinstance(typ, AnyType): + return referred_to_type + + return typ + + +def make_typeddict(api: CheckerPluginInterface, fields: 'OrderedDict[str, MypyType]', + required_keys: Set[str]) -> TypedDictType: + object_type = api.named_generic_type('mypy_extensions._TypedDict', []) + typed_dict_type = TypedDictType(fields, required_keys=required_keys, fallback=object_type) + return typed_dict_type + + +def resolve_string_attribute_value(attr_expr: Expression, ctx: Union[FunctionContext, MethodContext], + django_context: 'DjangoContext') -> Optional[str]: + if isinstance(attr_expr, StrExpr): + return attr_expr.value + + # support extracting from settings, in general case it's unresolvable yet + if isinstance(attr_expr, MemberExpr): + member_name = attr_expr.name + if isinstance(attr_expr.expr, NameExpr) and attr_expr.expr.fullname == 'django.conf.settings': + if hasattr(django_context.settings, member_name): + return getattr(django_context.settings, member_name) + + ctx.api.fail(f'Expression of type {type(attr_expr).__name__!r} is not supported', ctx.context) + return None + + +def get_typechecker_api(ctx: Union[AttributeContext, MethodContext, FunctionContext]) -> TypeChecker: + if not isinstance(ctx.api, TypeChecker): + raise ValueError('Not a TypeChecker') + return cast(TypeChecker, ctx.api) diff --git a/mypy_django_plugin/lookups.py b/mypy_django_plugin/lookups.py deleted file mode 100644 index a8eb54a10..000000000 --- a/mypy_django_plugin/lookups.py +++ /dev/null @@ -1,159 +0,0 @@ -from typing import List, Union - -import dataclasses -from mypy.nodes import TypeInfo -from mypy.plugin import CheckerPluginInterface -from mypy.types import Instance, Type - -from mypy_django_plugin import helpers - - -@dataclasses.dataclass -class RelatedModelNode: - typ: Instance - is_nullable: bool - - -@dataclasses.dataclass -class FieldNode: - typ: Type - - -LookupNode = Union[RelatedModelNode, FieldNode] - - -class LookupException(Exception): - pass - - -def resolve_lookup(api: CheckerPluginInterface, model_type_info: TypeInfo, lookup: str) -> List[LookupNode]: - """Resolve a lookup str to a list of LookupNodes. - - Each node represents a part of the lookup (separated by "__"), in order. - Each node is the Model or Field that was resolved. - - Raises LookupException if there were any issues resolving the lookup. - """ - lookup_parts = lookup.split("__") - - nodes = [] - while lookup_parts: - lookup_part = lookup_parts.pop(0) - - if not nodes: - current_node = None - else: - current_node = nodes[-1] - - if current_node is None: - new_node = resolve_model_lookup(api, model_type_info, lookup_part) - elif isinstance(current_node, RelatedModelNode): - new_node = resolve_model_lookup(api, current_node.typ.type, lookup_part) - elif isinstance(current_node, FieldNode): - raise LookupException(f"Field lookups not yet supported for lookup {lookup}") - else: - raise LookupException(f"Unsupported node type: {type(current_node)}") - nodes.append(new_node) - return nodes - - -def resolve_model_pk_lookup(api: CheckerPluginInterface, model_type_info: TypeInfo) -> LookupNode: - # Primary keys are special-cased - primary_key_type = helpers.extract_primary_key_type_for_get(model_type_info) - if primary_key_type: - return FieldNode(primary_key_type) - else: - # No PK, use the get type for AutoField as PK type. - autofield_info = api.lookup_typeinfo('django.db.models.fields.AutoField') - pk_type = helpers.get_private_descriptor_type(autofield_info, '_pyi_private_get_type', - is_nullable=False) - return FieldNode(pk_type) - - -def resolve_model_lookup(api: CheckerPluginInterface, model_type_info: TypeInfo, - lookup: str) -> LookupNode: - """Resolve a lookup on the given model.""" - if lookup == 'pk': - return resolve_model_pk_lookup(api, model_type_info) - - field_name = get_actual_field_name_for_lookup_field(lookup, model_type_info) - - field_node = model_type_info.get(field_name) - if not field_node: - raise LookupException( - f'When resolving lookup "{lookup}", field "{field_name}" was not found in model {model_type_info.name()}') - - if field_name.endswith('_id'): - field_name_without_id = field_name.rstrip('_id') - foreign_key_field = model_type_info.get(field_name_without_id) - if foreign_key_field is not None and helpers.is_foreign_key_like(foreign_key_field.type): - # Hack: If field ends with '_id' and there is a model field without the '_id' suffix, then use that field. - field_node = foreign_key_field - field_name = field_name_without_id - - field_node_type = field_node.type - if field_node_type is None or not isinstance(field_node_type, Instance): - raise LookupException( - f'When resolving lookup "{lookup}", could not determine type for {model_type_info.name()}.{field_name}') - - if field_node_type.type.fullname() == 'builtins.object': - # could be related manager - related_manager_type = helpers.get_related_manager_type_from_metadata(model_type_info, field_name, api) - if related_manager_type: - model_arg = related_manager_type.args[0] - if not isinstance(model_arg, Instance): - raise LookupException( - f'When resolving lookup "{lookup}", could not determine type ' - f'for {model_type_info.name()}.{field_name}') - - return RelatedModelNode(typ=model_arg, is_nullable=False) - - if helpers.is_foreign_key_like(field_node_type): - field_type = helpers.extract_field_getter_type(field_node_type) - is_nullable = helpers.is_optional(field_type) - if is_nullable: - # type is always non-optional - field_type = helpers.make_required(field_type) - - if isinstance(field_type, Instance): - return RelatedModelNode(typ=field_type, is_nullable=is_nullable) - else: - raise LookupException(f"Not an instance for field {field_type} lookup {lookup}") - - field_type = helpers.extract_field_getter_type(field_node_type) - if field_type: - return FieldNode(typ=field_type) - - # Not a Field - if field_name == 'id': - # If no 'id' field was found, use an int - return FieldNode(api.named_generic_type('builtins.int', [])) - - raise LookupException( - f'When resolving lookup {lookup!r}, could not determine type for {model_type_info.name()}.{field_name}') - - -def get_actual_field_name_for_lookup_field(lookup: str, model_type_info: TypeInfo) -> str: - """Attempt to find out the real field name if this lookup is a related_query_name (for reverse relations). - - If it's not, return the original lookup. - """ - lookups_metadata = helpers.get_lookups_metadata(model_type_info) - lookup_metadata = lookups_metadata.get(lookup) - if lookup_metadata is None: - # If not found on current model, look in all bases for their lookup metadata - for base in model_type_info.mro: - lookups_metadata = helpers.get_lookups_metadata(base) - lookup_metadata = lookups_metadata.get(lookup) - if lookup_metadata: - break - if not lookup_metadata: - lookup_metadata = {} - related_name = lookup_metadata.get('related_query_name_target', None) - if related_name: - # If the lookup is a related lookup, then look at the field specified by related_name. - # This is to support if related_query_name is set and differs from. - field_name = related_name - else: - field_name = lookup - return field_name diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index f31b853a0..dbd83429b 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -1,299 +1,247 @@ -import os +import configparser from functools import partial -from typing import Callable, Dict, List, Optional, Tuple, cast +from typing import Callable, Dict, List, Optional, Tuple -from mypy.nodes import MypyFile, NameExpr, TypeInfo +from django.db.models.fields.related import RelatedField +from mypy.errors import Errors +from mypy.nodes import MypyFile, TypeInfo from mypy.options import Options from mypy.plugin import ( AnalyzeTypeContext, AttributeContext, ClassDefContext, FunctionContext, MethodContext, Plugin, ) -from mypy.types import AnyType, Instance, Type, TypeOfAny +from mypy.types import Type as MypyType -from mypy_django_plugin import helpers -from mypy_django_plugin.config import Config -from mypy_django_plugin.transformers import fields, init_create -from mypy_django_plugin.transformers.forms import ( - extract_proper_type_for_get_form, extract_proper_type_for_get_form_class, make_meta_nested_class_inherit_from_any, -) -from mypy_django_plugin.transformers.migrations import ( - determine_model_cls_from_string_for_migrations, +from mypy_django_plugin.django.context import DjangoContext +from mypy_django_plugin.lib import fullnames, helpers +from mypy_django_plugin.transformers import ( + fields, forms, init_create, meta, querysets, request, settings, ) from mypy_django_plugin.transformers.models import process_model_class -from mypy_django_plugin.transformers.queryset import ( - extract_proper_type_for_queryset_values, extract_proper_type_queryset_values_list, - set_first_generic_param_as_default_for_second, -) -from mypy_django_plugin.transformers.related import ( - determine_type_of_related_manager, extract_and_return_primary_key_of_bound_related_field_parameter, -) -from mypy_django_plugin.transformers.settings import ( - get_type_of_setting, return_user_model_hook, -) -def transform_model_class(ctx: ClassDefContext, ignore_missing_model_attributes: bool) -> None: - try: - sym = ctx.api.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME) - except KeyError: - # models.Model is not loaded, skip metadata model write - pass - else: - if sym is not None and isinstance(sym.node, TypeInfo): - helpers.get_django_metadata(sym.node)['model_bases'][ctx.cls.fullname] = 1 +def transform_model_class(ctx: ClassDefContext, + django_context: DjangoContext) -> None: + sym = ctx.api.lookup_fully_qualified_or_none(fullnames.MODEL_CLASS_FULLNAME) - process_model_class(ctx, ignore_missing_model_attributes) - - -def transform_manager_class(ctx: ClassDefContext) -> None: - sym = ctx.api.lookup_fully_qualified_or_none(helpers.MANAGER_CLASS_FULLNAME) if sym is not None and isinstance(sym.node, TypeInfo): - helpers.get_django_metadata(sym.node)['manager_bases'][ctx.cls.fullname] = 1 + helpers.get_django_metadata(sym.node)['model_bases'][ctx.cls.fullname] = 1 + else: + if not ctx.api.final_iteration: + ctx.api.defer() + return + + process_model_class(ctx, django_context) def transform_form_class(ctx: ClassDefContext) -> None: - sym = ctx.api.lookup_fully_qualified_or_none(helpers.BASEFORM_CLASS_FULLNAME) + sym = ctx.api.lookup_fully_qualified_or_none(fullnames.BASEFORM_CLASS_FULLNAME) if sym is not None and isinstance(sym.node, TypeInfo): helpers.get_django_metadata(sym.node)['baseform_bases'][ctx.cls.fullname] = 1 - make_meta_nested_class_inherit_from_any(ctx) - - -def determine_proper_manager_type(ctx: FunctionContext) -> Type: - from mypy.checker import TypeChecker - - api = cast(TypeChecker, ctx.api) - ret = ctx.default_return_type - if not api.tscope.classes: - # not in class - return ret - outer_model_info = api.tscope.classes[0] - if not outer_model_info.has_base(helpers.MODEL_CLASS_FULLNAME): - return ret - if not isinstance(ret, Instance): - return ret - - has_manager_base = False - for i, base in enumerate(ret.type.bases): - if base.type.fullname() in {helpers.MANAGER_CLASS_FULLNAME, - helpers.RELATED_MANAGER_CLASS_FULLNAME, - helpers.BASE_MANAGER_CLASS_FULLNAME}: - has_manager_base = True - break - - if has_manager_base: - # Fill in the manager's type argument from the outer model - new_type_args = [Instance(outer_model_info, [])] - return helpers.reparametrize_instance(ret, new_type_args) - else: - return ret - - -def return_type_for_id_field(ctx: AttributeContext) -> Type: - if not isinstance(ctx.type, Instance): - return AnyType(TypeOfAny.from_error) + forms.make_meta_nested_class_inherit_from_any(ctx) - model_info = ctx.type.type # type: TypeInfo - primary_key_field_name = helpers.get_primary_key_field_name(model_info) - if not primary_key_field_name: - # no field with primary_key=True, just return id as int - return ctx.api.named_generic_type('builtins.int', []) - if primary_key_field_name != 'id': - # there's field with primary_key=True, but it's name is not 'id', fail - ctx.api.fail("Default primary key 'id' is not defined", ctx.context) - return AnyType(TypeOfAny.from_error) +def add_new_manager_base(ctx: ClassDefContext) -> None: + sym = ctx.api.lookup_fully_qualified_or_none(fullnames.MANAGER_CLASS_FULLNAME) + if sym is not None and isinstance(sym.node, TypeInfo): + helpers.get_django_metadata(sym.node)['manager_bases'][ctx.cls.fullname] = 1 - primary_key_sym = model_info.get(primary_key_field_name) - if primary_key_sym and isinstance(primary_key_sym.type, Instance): - pass - # try to parse field type out of primary key field - field_type = helpers.extract_field_getter_type(primary_key_sym.type) - if field_type: - return field_type +def extract_django_settings_module(config_file_path: Optional[str]) -> str: + errors = Errors() + if config_file_path is None: + errors.report(0, None, "'django_settings_module' is not set: no mypy config file specified") + errors.raise_error() - return primary_key_sym.type + parser = configparser.ConfigParser() + parser.read(config_file_path) # type: ignore + if not parser.has_section('mypy.plugins.django-stubs'): + errors.report(0, None, "'django_settings_module' is not set: no section [mypy.plugins.django-stubs]", + file=config_file_path) + errors.raise_error() + if not parser.has_option('mypy.plugins.django-stubs', 'django_settings_module'): + errors.report(0, None, "'django_settings_module' is not set: setting is not provided", + file=config_file_path) + errors.raise_error() -def transform_form_view(ctx: ClassDefContext) -> None: - form_class_value = helpers.get_assigned_value_for_class(ctx.cls.info, 'form_class') - if isinstance(form_class_value, NameExpr): - helpers.get_django_metadata(ctx.cls.info)['form_class'] = form_class_value.fullname + django_settings_module = parser.get('mypy.plugins.django-stubs', 'django_settings_module').strip('\'"') + return django_settings_module -class DjangoPlugin(Plugin): +class NewSemanalDjangoPlugin(Plugin): def __init__(self, options: Options) -> None: super().__init__(options) + django_settings_module = extract_django_settings_module(options.config_file) + self.django_context = DjangoContext(django_settings_module) - config_fpath = os.environ.get('MYPY_DJANGO_CONFIG', 'mypy_django.ini') - if config_fpath and os.path.exists(config_fpath): - self.config = Config.from_config_file(config_fpath) - self.django_settings_module = self.config.django_settings_module - else: - self.config = Config() - self.django_settings_module = None - - if 'DJANGO_SETTINGS_MODULE' in os.environ: - self.django_settings_module = os.environ['DJANGO_SETTINGS_MODULE'] - - def _get_current_model_bases(self) -> Dict[str, int]: - model_sym = self.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME) + def _get_current_queryset_bases(self) -> Dict[str, int]: + model_sym = self.lookup_fully_qualified(fullnames.QUERYSET_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): return (helpers.get_django_metadata(model_sym.node) - .setdefault('model_bases', {helpers.MODEL_CLASS_FULLNAME: 1})) + .setdefault('queryset_bases', {fullnames.QUERYSET_CLASS_FULLNAME: 1})) else: return {} def _get_current_manager_bases(self) -> Dict[str, int]: - model_sym = self.lookup_fully_qualified(helpers.MANAGER_CLASS_FULLNAME) + model_sym = self.lookup_fully_qualified(fullnames.MANAGER_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): return (helpers.get_django_metadata(model_sym.node) - .setdefault('manager_bases', {helpers.MANAGER_CLASS_FULLNAME: 1})) + .setdefault('manager_bases', {fullnames.MANAGER_CLASS_FULLNAME: 1})) else: return {} - def _get_current_form_bases(self) -> Dict[str, int]: - model_sym = self.lookup_fully_qualified(helpers.BASEFORM_CLASS_FULLNAME) + def _get_current_model_bases(self) -> Dict[str, int]: + model_sym = self.lookup_fully_qualified(fullnames.MODEL_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): - return (helpers.get_django_metadata(model_sym.node) - .setdefault('baseform_bases', {helpers.BASEFORM_CLASS_FULLNAME: 1, - helpers.FORM_CLASS_FULLNAME: 1, - helpers.MODELFORM_CLASS_FULLNAME: 1})) + return helpers.get_django_metadata(model_sym.node).setdefault('model_bases', + {fullnames.MODEL_CLASS_FULLNAME: 1}) else: return {} - def _get_current_queryset_bases(self) -> Dict[str, int]: - model_sym = self.lookup_fully_qualified(helpers.QUERYSET_CLASS_FULLNAME) + def _get_current_form_bases(self) -> Dict[str, int]: + model_sym = self.lookup_fully_qualified(fullnames.BASEFORM_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): return (helpers.get_django_metadata(model_sym.node) - .setdefault('queryset_bases', {helpers.QUERYSET_CLASS_FULLNAME: 1})) + .setdefault('baseform_bases', {fullnames.BASEFORM_CLASS_FULLNAME: 1, + fullnames.FORM_CLASS_FULLNAME: 1, + fullnames.MODELFORM_CLASS_FULLNAME: 1})) else: return {} - def _get_settings_modules_in_order_of_priority(self) -> List[str]: - settings_modules = [] - if self.django_settings_module: - settings_modules.append(self.django_settings_module) - - settings_modules.append('django.conf.global_settings') - return settings_modules - def _get_typeinfo_or_none(self, class_name: str) -> Optional[TypeInfo]: sym = self.lookup_fully_qualified(class_name) if sym is not None and isinstance(sym.node, TypeInfo): return sym.node return None - def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]: - if file.fullname() == 'django.conf' and self.django_settings_module: - return [(10, self.django_settings_module, -1)] - - if file.fullname() == 'django.db.models.query': - return [(10, 'mypy_extensions', -1)] + def _new_dependency(self, module: str) -> Tuple[int, str, int]: + return 10, module, -1 - return [] + def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]: + # for settings + if file.fullname() == 'django.conf' and self.django_context.django_settings_module: + return [self._new_dependency(self.django_context.django_settings_module)] + + # for values / values_list + if file.fullname() == 'django.db.models': + return [self._new_dependency('mypy_extensions'), self._new_dependency('typing')] + + # for `get_user_model()` + if self.django_context.settings: + if (file.fullname() == 'django.contrib.auth' + or file.fullname() in {'django.http', 'django.http.request'}): + auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL + try: + auth_user_module = self.django_context.apps_registry.get_model(auth_user_model_name).__module__ + except LookupError: + # get_user_model() model app is not installed + return [] + return [self._new_dependency(auth_user_module)] + + # ensure that all mentioned to='someapp.SomeModel' are loaded with corresponding related Fields + defined_model_classes = self.django_context.model_modules.get(file.fullname()) + if not defined_model_classes: + return [] + deps = set() + for model_class in defined_model_classes: + # forward relations + for field in self.django_context.get_model_fields(model_class): + if isinstance(field, RelatedField): + related_model_module = field.related_model.__module__ + if related_model_module != file.fullname(): + deps.add(self._new_dependency(related_model_module)) + # reverse relations + for relation in model_class._meta.related_objects: + related_model_module = relation.related_model.__module__ + if related_model_module != file.fullname(): + deps.add(self._new_dependency(related_model_module)) + return list(deps) def get_function_hook(self, fullname: str - ) -> Optional[Callable[[FunctionContext], Type]]: + ) -> Optional[Callable[[FunctionContext], MypyType]]: if fullname == 'django.contrib.auth.get_user_model': - return partial(return_user_model_hook, - settings_modules=self._get_settings_modules_in_order_of_priority()) + return partial(settings.get_user_model_hook, django_context=self.django_context) manager_bases = self._get_current_manager_bases() if fullname in manager_bases: - return determine_proper_manager_type + return querysets.determine_proper_manager_type info = self._get_typeinfo_or_none(fullname) if info: - if info.has_base(helpers.FIELD_FULLNAME): - return fields.adjust_return_type_of_field_instantiation + if info.has_base(fullnames.FIELD_FULLNAME): + return partial(fields.transform_into_proper_return_type, django_context=self.django_context) - if helpers.get_django_metadata(info).get('generated_init'): - return init_create.redefine_and_typecheck_model_init + if info.has_base(fullnames.MODEL_CLASS_FULLNAME): + return partial(init_create.redefine_and_typecheck_model_init, django_context=self.django_context) + return None def get_method_hook(self, fullname: str - ) -> Optional[Callable[[MethodContext], Type]]: - class_name, _, method_name = fullname.rpartition('.') - + ) -> Optional[Callable[[MethodContext], MypyType]]: + class_fullname, _, method_name = fullname.rpartition('.') if method_name == 'get_form_class': - info = self._get_typeinfo_or_none(class_name) - if info and info.has_base(helpers.FORM_MIXIN_CLASS_FULLNAME): - return extract_proper_type_for_get_form_class + info = self._get_typeinfo_or_none(class_fullname) + if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME): + return forms.extract_proper_type_for_get_form_class if method_name == 'get_form': - info = self._get_typeinfo_or_none(class_name) - if info and info.has_base(helpers.FORM_MIXIN_CLASS_FULLNAME): - return extract_proper_type_for_get_form + info = self._get_typeinfo_or_none(class_fullname) + if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME): + return forms.extract_proper_type_for_get_form if method_name == 'values': - model_info = self._get_typeinfo_or_none(class_name) - if model_info and model_info.has_base(helpers.QUERYSET_CLASS_FULLNAME): - return extract_proper_type_for_queryset_values + info = self._get_typeinfo_or_none(class_fullname) + if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME): + return partial(querysets.extract_proper_type_queryset_values, django_context=self.django_context) if method_name == 'values_list': - model_info = self._get_typeinfo_or_none(class_name) - if model_info and model_info.has_base(helpers.QUERYSET_CLASS_FULLNAME): - return extract_proper_type_queryset_values_list + info = self._get_typeinfo_or_none(class_fullname) + if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME): + return partial(querysets.extract_proper_type_queryset_values_list, django_context=self.django_context) - if fullname in {'django.apps.registry.Apps.get_model', - 'django.db.migrations.state.StateApps.get_model'}: - return determine_model_cls_from_string_for_migrations + if method_name == 'get_field': + info = self._get_typeinfo_or_none(class_fullname) + if info and info.has_base(fullnames.OPTIONS_CLASS_FULLNAME): + return partial(meta.return_proper_field_type_from_get_field, django_context=self.django_context) manager_classes = self._get_current_manager_bases() - class_fullname, _, method_name = fullname.rpartition('.') if class_fullname in manager_classes and method_name == 'create': - return init_create.redefine_and_typecheck_model_create + return partial(init_create.redefine_and_typecheck_model_create, django_context=self.django_context) return None def get_base_class_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: if fullname in self._get_current_model_bases(): - return partial(transform_model_class, - ignore_missing_model_attributes=self.config.ignore_missing_model_attributes) + return partial(transform_model_class, django_context=self.django_context) if fullname in self._get_current_manager_bases(): - return transform_manager_class + return add_new_manager_base if fullname in self._get_current_form_bases(): return transform_form_class - - info = self._get_typeinfo_or_none(fullname) - if info and info.has_base(helpers.FORM_MIXIN_CLASS_FULLNAME): - return transform_form_view - return None def get_attribute_hook(self, fullname: str - ) -> Optional[Callable[[AttributeContext], Type]]: + ) -> Optional[Callable[[AttributeContext], MypyType]]: class_name, _, attr_name = fullname.rpartition('.') - if class_name == helpers.DUMMY_SETTINGS_BASE_CLASS: - return partial(get_type_of_setting, - setting_name=attr_name, - settings_modules=self._get_settings_modules_in_order_of_priority(), - ignore_missing_settings=self.config.ignore_missing_settings) - - if class_name in self._get_current_model_bases(): - if attr_name == 'id': - return return_type_for_id_field - - model_info = self._get_typeinfo_or_none(class_name) - if model_info: - related_managers = helpers.get_related_managers_metadata(model_info) - if attr_name in related_managers: - return partial(determine_type_of_related_manager, - related_manager_name=attr_name) - - if attr_name.endswith('_id'): - return extract_and_return_primary_key_of_bound_related_field_parameter + if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS: + return partial(settings.get_type_of_settings_attribute, + django_context=self.django_context) - def get_type_analyze_hook(self, fullname: str - ) -> Optional[Callable[[AnalyzeTypeContext], Type]]: - queryset_bases = self._get_current_queryset_bases() - if fullname in queryset_bases: - return partial(set_first_generic_param_as_default_for_second, fullname) + info = self._get_typeinfo_or_none(class_name) + if info and info.has_base(fullnames.HTTPREQUEST_CLASS_FULLNAME) and attr_name == 'user': + return partial(request.set_auth_user_model_as_type_for_request_user, django_context=self.django_context) + return None + def get_type_analyze_hook(self, fullname: str + ) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]: + info = self._get_typeinfo_or_none(fullname) + if (info + and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME) + and not info.has_base(fullnames.BASE_MANAGER_CLASS_FULLNAME)): + return partial(querysets.set_first_generic_param_as_default_for_second, fullname=fullname) return None def plugin(version): - return DjangoPlugin + return NewSemanalDjangoPlugin diff --git a/mypy_django_plugin/transformers/fields.py b/mypy_django_plugin/transformers/fields.py index 85d2df7da..b0793d77d 100644 --- a/mypy_django_plugin/transformers/fields.py +++ b/mypy_django_plugin/transformers/fields.py @@ -1,188 +1,127 @@ -from typing import Optional, cast +from typing import Optional, Tuple, cast -from mypy.checker import TypeChecker -from mypy.nodes import ListExpr, NameExpr, StrExpr, TupleExpr, TypeInfo +from django.db.models.fields import Field +from django.db.models.fields.related import RelatedField +from mypy.nodes import AssignmentStmt, NameExpr, TypeInfo from mypy.plugin import FunctionContext -from mypy.types import ( - AnyType, CallableType, Instance, TupleType, Type, UnionType, -) +from mypy.types import AnyType, Instance +from mypy.types import Type as MypyType +from mypy.types import TypeOfAny -from mypy_django_plugin import helpers +from mypy_django_plugin.django.context import DjangoContext +from mypy_django_plugin.lib import fullnames, helpers -def extract_referred_to_type(ctx: FunctionContext) -> Optional[Instance]: - api = cast(TypeChecker, ctx.api) - if 'to' not in ctx.callee_arg_names: - api.msg.fail(f'to= parameter must be set for {ctx.context.callee.fullname}', - context=ctx.context) +def _get_current_field_from_assignment(ctx: FunctionContext, django_context: DjangoContext) -> Optional[Field]: + outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class() + if (outer_model_info is None + or not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME)): return None - arg_type = ctx.arg_types[ctx.callee_arg_names.index('to')][0] - if not isinstance(arg_type, CallableType): - to_arg_expr = ctx.args[ctx.callee_arg_names.index('to')][0] - if not isinstance(to_arg_expr, StrExpr): - # not string, not supported - return None - try: - model_fullname = helpers.get_model_fullname_from_string(to_arg_expr.value, - all_modules=api.modules) - except helpers.SelfReference: - model_fullname = api.tscope.classes[-1].fullname() - - except helpers.SameFileModel as exc: - model_fullname = api.tscope.classes[-1].module_name + '.' + exc.model_cls_name - - if model_fullname is None: - return None - model_info = helpers.lookup_fully_qualified_generic(model_fullname, - all_modules=api.modules) - if model_info is None or not isinstance(model_info, TypeInfo): - return None - return Instance(model_info, []) - - referred_to_type = arg_type.ret_type - if not isinstance(referred_to_type, Instance): + field_name = None + for stmt in outer_model_info.defn.defs.body: + if isinstance(stmt, AssignmentStmt): + if stmt.rvalue == ctx.context: + if not isinstance(stmt.lvalues[0], NameExpr): + return None + field_name = stmt.lvalues[0].name + break + if field_name is None: return None - if not referred_to_type.type.has_base(helpers.MODEL_CLASS_FULLNAME): - ctx.api.msg.fail(f'to= parameter value must be ' - f'a subclass of {helpers.MODEL_CLASS_FULLNAME}', - context=ctx.context) + + model_cls = django_context.get_model_class_by_fullname(outer_model_info.fullname()) + if model_cls is None: return None - return referred_to_type + current_field = model_cls._meta.get_field(field_name) + return current_field -def convert_any_to_type(typ: Type, referred_to_type: Type) -> Type: - if isinstance(typ, UnionType): - converted_items = [] - for item in typ.items: - converted_items.append(convert_any_to_type(item, referred_to_type)) - return UnionType.make_union(converted_items, - line=typ.line, column=typ.column) - if isinstance(typ, Instance): - args = [] - for default_arg in typ.args: - if isinstance(default_arg, AnyType): - args.append(referred_to_type) - else: - args.append(default_arg) - return helpers.reparametrize_instance(typ, args) +def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context: DjangoContext) -> MypyType: + current_field = _get_current_field_from_assignment(ctx, django_context) + if current_field is None: + return AnyType(TypeOfAny.from_error) - if isinstance(typ, AnyType): - return referred_to_type + assert isinstance(current_field, RelatedField) - return typ + related_model = related_model_to_set = current_field.related_model + if related_model_to_set._meta.proxy_for_model: + related_model_to_set = related_model._meta.proxy_for_model + typechecker_api = helpers.get_typechecker_api(ctx) -def fill_descriptor_types_for_related_field(ctx: FunctionContext) -> Type: - default_return_type = set_descriptor_types_for_field(ctx) - referred_to_type = extract_referred_to_type(ctx) - if referred_to_type is None: - return default_return_type + related_model_info = helpers.lookup_class_typeinfo(typechecker_api, related_model) + if related_model_info is None: + # maybe no type stub + related_model_type = AnyType(TypeOfAny.unannotated) + else: + related_model_type = Instance(related_model_info, []) # type: ignore + + related_model_to_set_info = helpers.lookup_class_typeinfo(typechecker_api, related_model_to_set) + if related_model_to_set_info is None: + # maybe no type stub + related_model_to_set_type = AnyType(TypeOfAny.unannotated) + else: + related_model_to_set_type = Instance(related_model_to_set_info, []) # type: ignore + default_related_field_type = set_descriptor_types_for_field(ctx) # replace Any with referred_to_type - args = [] - for default_arg in default_return_type.args: - args.append(convert_any_to_type(default_arg, referred_to_type)) + args = [ + helpers.convert_any_to_type(default_related_field_type.args[0], related_model_to_set_type), + helpers.convert_any_to_type(default_related_field_type.args[1], related_model_type), + ] + return helpers.reparametrize_instance(default_related_field_type, new_args=args) + - return helpers.reparametrize_instance(ctx.default_return_type, new_args=args) +def get_field_descriptor_types(field_info: TypeInfo, is_nullable: bool) -> Tuple[MypyType, MypyType]: + set_type = helpers.get_private_descriptor_type(field_info, '_pyi_private_set_type', + is_nullable=is_nullable) + get_type = helpers.get_private_descriptor_type(field_info, '_pyi_private_get_type', + is_nullable=is_nullable) + return set_type, get_type def set_descriptor_types_for_field(ctx: FunctionContext) -> Instance: default_return_type = cast(Instance, ctx.default_return_type) - is_nullable = helpers.parse_bool(helpers.get_argument_by_name(ctx, 'null')) - set_type = helpers.get_private_descriptor_type(default_return_type.type, '_pyi_private_set_type', - is_nullable=is_nullable) - get_type = helpers.get_private_descriptor_type(default_return_type.type, '_pyi_private_get_type', - is_nullable=is_nullable) + + is_nullable = False + null_expr = helpers.get_call_argument_by_name(ctx, 'null') + if null_expr is not None: + is_nullable = helpers.parse_bool(null_expr) or False + + set_type, get_type = get_field_descriptor_types(default_return_type.type, is_nullable) return helpers.reparametrize_instance(default_return_type, [set_type, get_type]) -def determine_type_of_array_field(ctx: FunctionContext) -> Type: +def determine_type_of_array_field(ctx: FunctionContext, django_context: DjangoContext) -> MypyType: default_return_type = set_descriptor_types_for_field(ctx) - base_field_arg_type = helpers.get_argument_type_by_name(ctx, 'base_field') + base_field_arg_type = helpers.get_call_argument_type_by_name(ctx, 'base_field') if not base_field_arg_type or not isinstance(base_field_arg_type, Instance): return default_return_type base_type = base_field_arg_type.args[1] # extract __get__ type args = [] for default_arg in default_return_type.args: - args.append(convert_any_to_type(default_arg, base_type)) + args.append(helpers.convert_any_to_type(default_arg, base_type)) return helpers.reparametrize_instance(default_return_type, args) -def transform_into_proper_return_type(ctx: FunctionContext) -> Type: +def transform_into_proper_return_type(ctx: FunctionContext, django_context: DjangoContext) -> MypyType: default_return_type = ctx.default_return_type - if not isinstance(default_return_type, Instance): - return default_return_type + assert isinstance(default_return_type, Instance) - if helpers.has_any_of_bases(default_return_type.type, (helpers.FOREIGN_KEY_FULLNAME, - helpers.ONETOONE_FIELD_FULLNAME, - helpers.MANYTOMANY_FIELD_FULLNAME)): - return fill_descriptor_types_for_related_field(ctx) - - if default_return_type.type.has_base(helpers.ARRAY_FIELD_FULLNAME): - return determine_type_of_array_field(ctx) - - return set_descriptor_types_for_field(ctx) + outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class() + if not outer_model_info or not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME): + # not inside models.Model class + return ctx.default_return_type + assert isinstance(outer_model_info, TypeInfo) + if helpers.has_any_of_bases(default_return_type.type, fullnames.RELATED_FIELDS_CLASSES): + return fill_descriptor_types_for_related_field(ctx, django_context) -def adjust_return_type_of_field_instantiation(ctx: FunctionContext) -> Type: - record_field_properties_into_outer_model_class(ctx) - return transform_into_proper_return_type(ctx) + if default_return_type.type.has_base(fullnames.ARRAY_FIELD_FULLNAME): + return determine_type_of_array_field(ctx, django_context) - -def record_field_properties_into_outer_model_class(ctx: FunctionContext) -> None: - api = cast(TypeChecker, ctx.api) - outer_model = api.scope.active_class() - if outer_model is None or not outer_model.has_base(helpers.MODEL_CLASS_FULLNAME): - # outside models.Model class, undetermined - return - - field_name = None - for name_expr, stmt in helpers.iter_over_assignments(outer_model.defn): - if stmt == ctx.context and isinstance(name_expr, NameExpr): - field_name = name_expr.name - break - if field_name is None: - return - - fields_metadata = helpers.get_fields_metadata(outer_model) - - # primary key - is_primary_key = False - primary_key_arg = helpers.get_argument_by_name(ctx, 'primary_key') - if primary_key_arg: - is_primary_key = helpers.parse_bool(primary_key_arg) - fields_metadata[field_name] = {'primary_key': is_primary_key} - - # choices - choices_arg = helpers.get_argument_by_name(ctx, 'choices') - if choices_arg and isinstance(choices_arg, (TupleExpr, ListExpr)): - # iterable of 2 element tuples of two kinds - _, analyzed_choices = api.analyze_iterable_item_type(choices_arg) - if isinstance(analyzed_choices, TupleType): - first_element_type = analyzed_choices.items[0] - if isinstance(first_element_type, Instance): - fields_metadata[field_name]['choices'] = first_element_type.type.fullname() - - # nullability - null_arg = helpers.get_argument_by_name(ctx, 'null') - is_nullable = False - if null_arg: - is_nullable = helpers.parse_bool(null_arg) - fields_metadata[field_name]['null'] = is_nullable - - # is_blankable - blank_arg = helpers.get_argument_by_name(ctx, 'blank') - is_blankable = False - if blank_arg: - is_blankable = helpers.parse_bool(blank_arg) - fields_metadata[field_name]['blank'] = is_blankable - - # default - default_arg = helpers.get_argument_by_name(ctx, 'default') - if default_arg and not helpers.is_none_expr(default_arg): - fields_metadata[field_name]['default_specified'] = True + return set_descriptor_types_for_field(ctx) diff --git a/mypy_django_plugin/transformers/forms.py b/mypy_django_plugin/transformers/forms.py index 2afc519da..7bd0e1116 100644 --- a/mypy_django_plugin/transformers/forms.py +++ b/mypy_django_plugin/transformers/forms.py @@ -1,29 +1,36 @@ +from typing import Optional + from mypy.plugin import ClassDefContext, MethodContext -from mypy.types import CallableType, Instance, NoneTyp, Type, TypeType +from mypy.types import CallableType, Instance, NoneTyp +from mypy.types import Type as MypyType +from mypy.types import TypeType -from mypy_django_plugin import helpers +from mypy_django_plugin.lib import helpers def make_meta_nested_class_inherit_from_any(ctx: ClassDefContext) -> None: meta_node = helpers.get_nested_meta_node_for_current_class(ctx.cls.info) if meta_node is None: - return None - meta_node.fallback_to_any = True + if not ctx.api.final_iteration: + ctx.api.defer() + else: + meta_node.fallback_to_any = True + + +def get_specified_form_class(object_type: Instance) -> Optional[TypeType]: + form_class_sym = object_type.type.get('form_class') + if form_class_sym and isinstance(form_class_sym.type, CallableType): + return TypeType(form_class_sym.type.ret_type) + return None -def extract_proper_type_for_get_form(ctx: MethodContext) -> Type: +def extract_proper_type_for_get_form(ctx: MethodContext) -> MypyType: object_type = ctx.type - if not isinstance(object_type, Instance): - return ctx.default_return_type + assert isinstance(object_type, Instance) - form_class_type = helpers.get_argument_type_by_name(ctx, 'form_class') + form_class_type = helpers.get_call_argument_type_by_name(ctx, 'form_class') if form_class_type is None or isinstance(form_class_type, NoneTyp): - # extract from specified form_class in metadata - form_class_fullname = helpers.get_django_metadata(object_type.type).get('form_class', None) - if not form_class_fullname: - return ctx.default_return_type - - return ctx.api.named_generic_type(form_class_fullname, []) + form_class_type = get_specified_form_class(object_type) if isinstance(form_class_type, TypeType) and isinstance(form_class_type.item, Instance): return form_class_type.item @@ -34,13 +41,12 @@ def extract_proper_type_for_get_form(ctx: MethodContext) -> Type: return ctx.default_return_type -def extract_proper_type_for_get_form_class(ctx: MethodContext) -> Type: +def extract_proper_type_for_get_form_class(ctx: MethodContext) -> MypyType: object_type = ctx.type - if not isinstance(object_type, Instance): - return ctx.default_return_type + assert isinstance(object_type, Instance) - form_class_fullname = helpers.get_django_metadata(object_type.type).get('form_class', None) - if not form_class_fullname: + form_class_type = get_specified_form_class(object_type) + if form_class_type is None: return ctx.default_return_type - return TypeType(ctx.api.named_generic_type(form_class_fullname, [])) + return form_class_type diff --git a/mypy_django_plugin/transformers/init_create.py b/mypy_django_plugin/transformers/init_create.py index 4e4f97dfa..d1432349e 100644 --- a/mypy_django_plugin/transformers/init_create.py +++ b/mypy_django_plugin/transformers/init_create.py @@ -1,194 +1,75 @@ -from typing import Dict, Optional, Set, cast +from typing import List, Tuple, Type, Union -from mypy.checker import TypeChecker -from mypy.nodes import TypeInfo, Var +from django.db.models.base import Model from mypy.plugin import FunctionContext, MethodContext -from mypy.types import AnyType, Instance, Type, TypeOfAny +from mypy.types import Instance +from mypy.types import Type as MypyType -from mypy_django_plugin import helpers +from mypy_django_plugin.django.context import DjangoContext +from mypy_django_plugin.lib import helpers -def extract_base_pointer_args(model: TypeInfo) -> Set[str]: - pointer_args: Set[str] = set() - for base in model.bases: - if base.type.has_base(helpers.MODEL_CLASS_FULLNAME): - parent_name = base.type.name().lower() - pointer_args.add(f'{parent_name}_ptr') - pointer_args.add(f'{parent_name}_ptr_id') - return pointer_args +def get_actual_types(ctx: Union[MethodContext, FunctionContext], + expected_keys: List[str]) -> List[Tuple[str, MypyType]]: + actual_types = [] + # positionals + for pos, (actual_name, actual_type) in enumerate(zip(ctx.arg_names[0], ctx.arg_types[0])): + if actual_name is None: + if ctx.callee_arg_names[0] == 'kwargs': + # unpacked dict as kwargs is not supported + continue + actual_name = expected_keys[pos] + actual_types.append((actual_name, actual_type)) + # kwargs + if len(ctx.callee_arg_names) > 1: + for actual_name, actual_type in zip(ctx.arg_names[1], ctx.arg_types[1]): + if actual_name is None: + # unpacked dict as kwargs is not supported + continue + actual_types.append((actual_name, actual_type)) + return actual_types -def redefine_and_typecheck_model_init(ctx: FunctionContext) -> Type: - assert isinstance(ctx.default_return_type, Instance) +def typecheck_model_method(ctx: Union[FunctionContext, MethodContext], django_context: DjangoContext, + model_cls: Type[Model], method: str) -> MypyType: + typechecker_api = helpers.get_typechecker_api(ctx) + expected_types = django_context.get_expected_types(typechecker_api, model_cls, method=method) + expected_keys = [key for key in expected_types.keys() if key != 'pk'] - api = cast(TypeChecker, ctx.api) - model: TypeInfo = ctx.default_return_type.type - - expected_types = extract_expected_types(ctx, model, is_init=True) - - # order is preserved, can be used for positionals - positional_names = list(expected_types.keys()) - positional_names.remove('pk') - - visited_positionals = set() - # check positionals - for i, (_, actual_pos_type) in enumerate(zip(ctx.arg_names[0], ctx.arg_types[0])): - actual_pos_name = positional_names[i] - api.check_subtype(actual_pos_type, expected_types[actual_pos_name], - ctx.context, - 'Incompatible type for "{}" of "{}"'.format(actual_pos_name, - model.name()), - 'got', 'expected') - visited_positionals.add(actual_pos_name) - - # extract name of base models for _ptr - base_pointer_args = extract_base_pointer_args(model) - - # check kwargs - for i, (actual_name, actual_type) in enumerate(zip(ctx.arg_names[1], ctx.arg_types[1])): - if actual_name in base_pointer_args: - # parent_ptr args are not supported - continue - if actual_name in visited_positionals: - continue - if actual_name is None: - # unpacked dict as kwargs is not supported - continue + for actual_name, actual_type in get_actual_types(ctx, expected_keys): if actual_name not in expected_types: ctx.api.fail('Unexpected attribute "{}" for model "{}"'.format(actual_name, - model.name()), + model_cls.__name__), ctx.context) continue - api.check_subtype(actual_type, expected_types[actual_name], - ctx.context, - 'Incompatible type for "{}" of "{}"'.format(actual_name, - model.name()), - 'got', 'expected') - return ctx.default_return_type - - -def redefine_and_typecheck_model_create(ctx: MethodContext) -> Type: - api = cast(TypeChecker, ctx.api) - if isinstance(ctx.type, Instance) and len(ctx.type.args) > 0: - model_generic_arg = ctx.type.args[0] - else: - model_generic_arg = ctx.default_return_type - - if isinstance(model_generic_arg, AnyType): - return ctx.default_return_type - - model: TypeInfo = model_generic_arg.type - - # extract name of base models for _ptr - base_pointer_args = extract_base_pointer_args(model) - expected_types = extract_expected_types(ctx, model) - - for actual_name, actual_type in zip(ctx.arg_names[0], ctx.arg_types[0]): - if actual_name in base_pointer_args: - # parent_ptr args are not supported - continue - if actual_name is None: - # unpacked dict as kwargs is not supported - continue - if actual_name not in expected_types: - api.fail('Unexpected attribute "{}" for model "{}"'.format(actual_name, - model.name()), - ctx.context) - continue - api.check_subtype(actual_type, expected_types[actual_name], - ctx.context, - 'Incompatible type for "{}" of "{}"'.format(actual_name, - model.name()), - 'got', 'expected') + typechecker_api.check_subtype(actual_type, expected_types[actual_name], + ctx.context, + 'Incompatible type for "{}" of "{}"'.format(actual_name, + model_cls.__name__), + 'got', 'expected') return ctx.default_return_type -def extract_choices_type(model: TypeInfo, field_name: str) -> Optional[str]: - field_metadata = helpers.get_fields_metadata(model).get(field_name, {}) - if 'choices' in field_metadata: - return field_metadata['choices'] - return None +def redefine_and_typecheck_model_init(ctx: FunctionContext, django_context: DjangoContext) -> MypyType: + assert isinstance(ctx.default_return_type, Instance) + model_fullname = ctx.default_return_type.type.fullname() + model_cls = django_context.get_model_class_by_fullname(model_fullname) + if model_cls is None: + return ctx.default_return_type -def extract_expected_types(ctx: FunctionContext, model: TypeInfo, - is_init: bool = False) -> Dict[str, Type]: - api = cast(TypeChecker, ctx.api) + return typecheck_model_method(ctx, django_context, model_cls, '__init__') - expected_types: Dict[str, Type] = {} - primary_key_type = helpers.extract_explicit_set_type_of_model_primary_key(model) - if not primary_key_type: - # no explicit primary key, set pk to Any and add id - primary_key_type = AnyType(TypeOfAny.special_form) - if is_init: - expected_types['id'] = helpers.make_optional(ctx.api.named_generic_type('builtins.int', [])) - else: - expected_types['id'] = ctx.api.named_generic_type('builtins.int', []) - expected_types['pk'] = primary_key_type +def redefine_and_typecheck_model_create(ctx: MethodContext, django_context: DjangoContext) -> MypyType: + if not isinstance(ctx.default_return_type, Instance): + # only work with ctx.default_return_type = model Instance + return ctx.default_return_type - for base in model.mro: - # extract all fields for all models in MRO - for name, sym in base.names.items(): - # do not redefine special attrs - if name in {'_meta', 'pk'}: - continue + model_fullname = ctx.default_return_type.type.fullname() + model_cls = django_context.get_model_class_by_fullname(model_fullname) + if model_cls is None: + return ctx.default_return_type - if isinstance(sym.node, Var): - typ = sym.node.type - if typ is None or isinstance(typ, AnyType): - # types are not ready, fallback to Any - expected_types[name] = AnyType(TypeOfAny.from_unimported_type) - expected_types[name + '_id'] = AnyType(TypeOfAny.from_unimported_type) - - elif isinstance(typ, Instance): - field_type = helpers.extract_field_setter_type(typ) - if field_type is None: - continue - - if helpers.has_any_of_bases(typ.type, (helpers.FOREIGN_KEY_FULLNAME, - helpers.ONETOONE_FIELD_FULLNAME)): - related_primary_key_type = AnyType(TypeOfAny.implementation_artifact) - # in case it's optional, we need Instance type - referred_to_model = typ.args[1] - is_nullable = helpers.is_optional(referred_to_model) - if is_nullable: - referred_to_model = helpers.make_required(typ.args[1]) - - if isinstance(referred_to_model, Instance) and referred_to_model.type.has_base( - helpers.MODEL_CLASS_FULLNAME): - pk_type = helpers.extract_explicit_set_type_of_model_primary_key(referred_to_model.type) - if not pk_type: - # extract set type of AutoField - autofield_info = api.lookup_typeinfo('django.db.models.fields.AutoField') - pk_type = helpers.get_private_descriptor_type(autofield_info, '_pyi_private_set_type', - is_nullable=is_nullable) - related_primary_key_type = pk_type - - if is_init: - related_primary_key_type = helpers.make_optional(related_primary_key_type) - - expected_types[name + '_id'] = related_primary_key_type - - field_metadata = helpers.get_fields_metadata(model).get(name, {}) - if field_type: - # related fields could be None in __init__ (but should be specified before save()) - if helpers.has_any_of_bases(typ.type, (helpers.FOREIGN_KEY_FULLNAME, - helpers.ONETOONE_FIELD_FULLNAME)) and is_init: - field_type = helpers.make_optional(field_type) - - # if primary_key=True and default specified - elif field_metadata.get('primary_key', False) and field_metadata.get('default_specified', - False): - field_type = helpers.make_optional(field_type) - - # if CharField(blank=True,...) and not nullable, then field can be None in __init__ - elif ( - helpers.has_any_of_bases(typ.type, (helpers.CHAR_FIELD_FULLNAME,)) and is_init and - field_metadata.get('blank', False) and not field_metadata.get('null', False) - ): - field_type = helpers.make_optional(field_type) - - expected_types[name] = field_type - - return expected_types + return typecheck_model_method(ctx, django_context, model_cls, 'create') diff --git a/mypy_django_plugin/transformers/meta.py b/mypy_django_plugin/transformers/meta.py new file mode 100644 index 000000000..007d8e48d --- /dev/null +++ b/mypy_django_plugin/transformers/meta.py @@ -0,0 +1,46 @@ +from django.core.exceptions import FieldDoesNotExist +from mypy.plugin import MethodContext +from mypy.types import AnyType, Instance +from mypy.types import Type as MypyType +from mypy.types import TypeOfAny + +from mypy_django_plugin.django.context import DjangoContext +from mypy_django_plugin.lib import fullnames, helpers + + +def _get_field_instance(ctx: MethodContext, field_fullname: str) -> MypyType: + field_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx), + field_fullname) + if field_info is None: + return AnyType(TypeOfAny.unannotated) + return Instance(field_info, [AnyType(TypeOfAny.explicit), AnyType(TypeOfAny.explicit)]) + + +def return_proper_field_type_from_get_field(ctx: MethodContext, django_context: DjangoContext) -> MypyType: + # Options instance + assert isinstance(ctx.type, Instance) + + model_type = ctx.type.args[0] + if not isinstance(model_type, Instance): + return _get_field_instance(ctx, fullnames.FIELD_FULLNAME) + + model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname()) + if model_cls is None: + return _get_field_instance(ctx, fullnames.FIELD_FULLNAME) + + field_name_expr = helpers.get_call_argument_by_name(ctx, 'field_name') + if field_name_expr is None: + return _get_field_instance(ctx, fullnames.FIELD_FULLNAME) + + field_name = helpers.resolve_string_attribute_value(field_name_expr, ctx, django_context) + if field_name is None: + return _get_field_instance(ctx, fullnames.FIELD_FULLNAME) + + try: + field = model_cls._meta.get_field(field_name) + except FieldDoesNotExist as exc: + ctx.api.fail(exc.args[0], ctx.context) + return AnyType(TypeOfAny.from_error) + + field_fullname = helpers.get_class_fullname(field.__class__) + return _get_field_instance(ctx, field_fullname) diff --git a/mypy_django_plugin/transformers/migrations.py b/mypy_django_plugin/transformers/migrations.py deleted file mode 100644 index b6baad8ae..000000000 --- a/mypy_django_plugin/transformers/migrations.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Optional, cast - -from mypy.checker import TypeChecker -from mypy.nodes import Expression, StrExpr, TypeInfo -from mypy.plugin import MethodContext -from mypy.types import Instance, Type, TypeType - -from mypy_django_plugin import helpers - - -def get_string_value_from_expr(expr: Expression) -> Optional[str]: - if isinstance(expr, StrExpr): - return expr.value - # TODO: somehow figure out other cases - return None - - -def determine_model_cls_from_string_for_migrations(ctx: MethodContext) -> Type: - app_label_expr = ctx.args[ctx.callee_arg_names.index('app_label')][0] - app_label = get_string_value_from_expr(app_label_expr) - if app_label is None: - return ctx.default_return_type - - if 'model_name' not in ctx.callee_arg_names: - return ctx.default_return_type - - model_name_expr_tuple = ctx.args[ctx.callee_arg_names.index('model_name')] - if not model_name_expr_tuple: - return ctx.default_return_type - - model_name = get_string_value_from_expr(model_name_expr_tuple[0]) - if model_name is None: - return ctx.default_return_type - - api = cast(TypeChecker, ctx.api) - model_fullname = helpers.get_model_fullname(app_label, model_name, all_modules=api.modules) - - if model_fullname is None: - return ctx.default_return_type - model_info = helpers.lookup_fully_qualified_generic(model_fullname, - all_modules=api.modules) - if model_info is None or not isinstance(model_info, TypeInfo): - return ctx.default_return_type - return TypeType(Instance(model_info, [])) diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index 099855b61..9318b0c9c 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -1,74 +1,81 @@ -from abc import ABCMeta, abstractmethod -from typing import Any, Dict, Iterator, List, Optional, Tuple, cast - -import dataclasses +from collections import OrderedDict +from typing import Type, cast + +from django.db.models.base import Model +from django.db.models.fields import DateField, DateTimeField +from django.db.models.fields.related import ForeignKey +from django.db.models.fields.reverse_related import ( + ManyToManyRel, ManyToOneRel, OneToOneRel, +) +from mypy.newsemanal.semanal import NewSemanticAnalyzer from mypy.nodes import ( - ARG_POS, ARG_STAR, ARG_STAR2, MDEF, Argument, CallExpr, ClassDef, Expression, IndexExpr, MemberExpr, MypyFile, - NameExpr, StrExpr, SymbolTableNode, TypeInfo, Var, + ARG_STAR2, MDEF, Argument, SymbolTableNode, TypeInfo, Var, ) from mypy.plugin import ClassDefContext -from mypy.plugins.common import add_method -from mypy.semanal import SemanticAnalyzerPass2 -from mypy.types import AnyType, Instance, NoneTyp, TypeOfAny - -from mypy_django_plugin import helpers - - -@dataclasses.dataclass -class ModelClassInitializer(metaclass=ABCMeta): - api: SemanticAnalyzerPass2 - model_classdef: ClassDef - - @classmethod - def from_ctx(cls, ctx: ClassDefContext): - return cls(api=cast(SemanticAnalyzerPass2, ctx.api), model_classdef=ctx.cls) - - def get_meta_attribute(self, name: str) -> Optional[Expression]: - meta_node = helpers.get_nested_meta_node_for_current_class(self.model_classdef.info) - if meta_node is None: - return None - - return helpers.get_assigned_value_for_class(meta_node, name) - - def is_abstract_model(self) -> bool: - is_abstract_expr = self.get_meta_attribute('abstract') - if is_abstract_expr is None: - return False - return self.api.parse_bool(is_abstract_expr) - - def add_new_node_to_model_class(self, name: str, typ: Instance) -> None: +from mypy.plugins import common +from mypy.types import AnyType, Instance, TypeOfAny + +from mypy_django_plugin.django.context import DjangoContext +from mypy_django_plugin.lib import fullnames, helpers +from mypy_django_plugin.transformers import fields +from mypy_django_plugin.transformers.fields import get_field_descriptor_types + + +class ModelClassInitializer: + def __init__(self, ctx: ClassDefContext, django_context: DjangoContext): + self.api = cast(NewSemanticAnalyzer, ctx.api) + self.model_classdef = ctx.cls + self.django_context = django_context + self.ctx = ctx + + def lookup_typeinfo_or_incomplete_defn_error(self, fullname: str) -> TypeInfo: + sym = self.api.lookup_fully_qualified_or_none(fullname) + if sym is None or not isinstance(sym.node, TypeInfo): + raise helpers.IncompleteDefnException(f'No {fullname!r} found') + return sym.node + + def lookup_class_typeinfo_or_incomplete_defn_error(self, klass: type) -> TypeInfo: + fullname = helpers.get_class_fullname(klass) + field_info = self.lookup_typeinfo_or_incomplete_defn_error(fullname) + return field_info + + def create_new_var(self, name: str, typ: Instance) -> Var: # type=: type of the variable itself var = Var(name=name, type=typ) # var.info: type of the object variable is bound to var.info = self.model_classdef.info var._fullname = self.model_classdef.info.fullname() + '.' + name - var.is_inferred = True var.is_initialized_in_class = True + var.is_inferred = True + return var + + def add_new_node_to_model_class(self, name: str, typ: Instance) -> None: + var = self.create_new_var(name, typ) self.model_classdef.info.names[name] = SymbolTableNode(MDEF, var, plugin_generated=True) - @abstractmethod def run(self) -> None: - raise NotImplementedError() + model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname) + if model_cls is None: + return + self.run_with_model_cls(model_cls) - -def iter_over_one_to_n_related_fields(klass: ClassDef) -> Iterator[Tuple[NameExpr, CallExpr]]: - for lvalue, rvalue in helpers.iter_call_assignments(klass): - if (isinstance(lvalue, NameExpr) - and isinstance(rvalue.callee, MemberExpr)): - if rvalue.callee.fullname in {helpers.FOREIGN_KEY_FULLNAME, - helpers.ONETOONE_FIELD_FULLNAME}: - yield lvalue, rvalue - - -class SetIdAttrsForRelatedFields(ModelClassInitializer): - def run(self) -> None: - for lvalue, rvalue in iter_over_one_to_n_related_fields(self.model_classdef): - node_name = lvalue.name + '_id' - self.add_new_node_to_model_class(name=node_name, - typ=self.api.builtin_type('builtins.int')) + def run_with_model_cls(self, model_cls): + pass class InjectAnyAsBaseForNestedMeta(ModelClassInitializer): + """ + Replaces + class MyModel(models.Model): + class Meta: + pass + with + class MyModel(models.Model): + class Meta(Any): + pass + to get around incompatible Meta inner classes for different models. + """ + def run(self) -> None: meta_node = helpers.get_nested_meta_node_for_current_class(self.model_classdef.info) if meta_node is None: @@ -76,228 +83,147 @@ def run(self) -> None: meta_node.fallback_to_any = True -class AddDefaultObjectsManager(ModelClassInitializer): - def add_new_manager(self, name: str, manager_type: Optional[Instance]) -> None: - if manager_type is None: - return None - self.add_new_node_to_model_class(name, manager_type) - - def add_private_default_manager(self, manager_type: Optional[Instance]) -> None: - if manager_type is None: - return None - self.add_new_node_to_model_class('_default_manager', manager_type) - - def get_existing_managers(self) -> List[Tuple[str, TypeInfo]]: - managers = [] - for base in self.model_classdef.info.mro: - for name_expr, member_expr in helpers.iter_call_assignments(base.defn): - manager_name = name_expr.name - callee_expr = member_expr.callee - if isinstance(callee_expr, IndexExpr): - callee_expr = callee_expr.analyzed.expr - if isinstance(callee_expr, (MemberExpr, NameExpr)) \ - and isinstance(callee_expr.node, TypeInfo) \ - and callee_expr.node.has_base(helpers.BASE_MANAGER_CLASS_FULLNAME): - managers.append((manager_name, callee_expr.node)) - return managers - - def run(self) -> None: - existing_managers = self.get_existing_managers() - if existing_managers: - first_manager_type = None - for manager_name, manager_type_info in existing_managers: - manager_type = Instance(manager_type_info, args=[Instance(self.model_classdef.info, [])]) - self.add_new_manager(name=manager_name, manager_type=manager_type) - if first_manager_type is None: - first_manager_type = manager_type - else: - if self.is_abstract_model(): - # abstract models do not need 'objects' queryset - return None - - first_manager_type = self.api.named_type_or_none(helpers.MANAGER_CLASS_FULLNAME, - args=[Instance(self.model_classdef.info, [])]) - self.add_new_manager('objects', manager_type=first_manager_type) - - if self.is_abstract_model(): - return None - default_manager_name_expr = self.get_meta_attribute('default_manager_name') - if isinstance(default_manager_name_expr, StrExpr): - self.add_private_default_manager(self.model_classdef.info.get(default_manager_name_expr.value).type) - else: - self.add_private_default_manager(first_manager_type) - - -class AddIdAttributeIfPrimaryKeyTrueIsNotSet(ModelClassInitializer): - def run(self) -> None: - if self.is_abstract_model(): - # no need for .id attr - return None - - for _, rvalue in helpers.iter_call_assignments(self.model_classdef): - if ('primary_key' in rvalue.arg_names - and self.api.parse_bool(rvalue.args[rvalue.arg_names.index('primary_key')])): - break - else: - self.add_new_node_to_model_class('id', self.api.builtin_type('builtins.object')) - - -class AddRelatedManagers(ModelClassInitializer): - def add_related_manager_variable(self, manager_name: str, related_field_type_data: Dict[str, Any]) -> None: - # add dummy related manager for use later - self.add_new_node_to_model_class(manager_name, self.api.builtin_type('builtins.object')) - - # save name in metadata for use in get_attribute_hook later - related_managers_metadata = helpers.get_related_managers_metadata(self.model_classdef.info) - related_managers_metadata[manager_name] = related_field_type_data - - def run(self) -> None: - for module_name, module_file in self.api.modules.items(): - for model_defn in helpers.iter_over_classdefs(module_file): - for lvalue, rvalue in helpers.iter_call_assignments(model_defn): - if is_related_field(rvalue, module_file): - try: - referenced_model_fullname = extract_ref_to_fullname(rvalue, - module_file=module_file, - all_modules=self.api.modules) - except helpers.SelfReference: - referenced_model_fullname = model_defn.fullname - - except helpers.SameFileModel as exc: - referenced_model_fullname = module_name + '.' + exc.model_cls_name - - if self.model_classdef.fullname == referenced_model_fullname: - related_name = model_defn.name.lower() + '_set' - if 'related_name' in rvalue.arg_names: - related_name_expr = rvalue.args[rvalue.arg_names.index('related_name')] - if not isinstance(related_name_expr, StrExpr): - continue - related_name = related_name_expr.value - if related_name == '+': - # No backwards relation is desired - continue - - if 'related_query_name' in rvalue.arg_names: - related_query_name_expr = rvalue.args[rvalue.arg_names.index('related_query_name')] - if not isinstance(related_query_name_expr, StrExpr): - related_query_name = None - else: - related_query_name = related_query_name_expr.value - # TODO: Handle defaulting to model name if related_name is not set - else: - # No related_query_name specified, default to related_name - related_query_name = related_name - - # field_type_data = get_related_field_type(rvalue, self.api, defn.info) - # if typ is None: - # continue - - # TODO: recursively serialize types, or just https://github.com/python/mypy/issues/6506 - # as long as Model is not a Generic, one level depth is fine - if rvalue.callee.name in {'ForeignKey', 'ManyToManyField'}: - field_type_data = { - 'manager': helpers.RELATED_MANAGER_CLASS_FULLNAME, - 'of': [model_defn.info.fullname()] - } - # return api.named_type_or_none(helpers.RELATED_MANAGER_CLASS_FULLNAME, - # args=[Instance(related_model_typ, [])]) - else: - field_type_data = { - 'manager': model_defn.info.fullname(), - 'of': [] - } - - self.add_related_manager_variable(related_name, related_field_type_data=field_type_data) - - if related_query_name is not None: - # Only create related_query_name if it is a string literal - helpers.get_lookups_metadata(self.model_classdef.info)[related_query_name] = { - 'related_query_name_target': related_name - } - - -def get_related_field_type(rvalue: CallExpr, related_model_typ: TypeInfo) -> Dict[str, Any]: - if rvalue.callee.name in {'ForeignKey', 'ManyToManyField'}: - return { - 'manager': helpers.RELATED_MANAGER_CLASS_FULLNAME, - 'of': [related_model_typ.fullname()] - } - # return api.named_type_or_none(helpers.RELATED_MANAGER_CLASS_FULLNAME, - # args=[Instance(related_model_typ, [])]) - else: - return { - 'manager': related_model_typ.fullname(), - 'of': [] - } - # return Instance(related_model_typ, []) - - -def is_related_field(expr: CallExpr, module_file: MypyFile) -> bool: - if isinstance(expr.callee, MemberExpr) and isinstance(expr.callee.expr, NameExpr): - module = module_file.names.get(expr.callee.expr.name) - if module \ - and module.fullname == 'django.db.models' \ - and expr.callee.name in {'ForeignKey', - 'OneToOneField', - 'ManyToManyField'}: - return True - return False - - -def extract_ref_to_fullname(rvalue_expr: CallExpr, - module_file: MypyFile, all_modules: Dict[str, MypyFile]) -> Optional[str]: - if 'to' in rvalue_expr.arg_names: - to_expr = rvalue_expr.args[rvalue_expr.arg_names.index('to')] - else: - to_expr = rvalue_expr.args[0] - if isinstance(to_expr, NameExpr): - return module_file.names[to_expr.name].fullname - elif isinstance(to_expr, StrExpr): - typ_fullname = helpers.get_model_fullname_from_string(to_expr.value, all_modules) - if typ_fullname is None: - return None - return typ_fullname - return None - - -def add_dummy_init_method(ctx: ClassDefContext) -> None: - any = AnyType(TypeOfAny.special_form) - - pos_arg = Argument(variable=Var('args', any), - type_annotation=any, initializer=None, kind=ARG_STAR) - kw_arg = Argument(variable=Var('kwargs', any), - type_annotation=any, initializer=None, kind=ARG_STAR2) - - add_method(ctx, '__init__', [pos_arg, kw_arg], NoneTyp()) - - # mark as model class - ctx.cls.info.metadata.setdefault('django', {})['generated_init'] = True - - -def add_get_set_attr_fallback_to_any(ctx: ClassDefContext): - any = AnyType(TypeOfAny.special_form) - - name_arg = Argument(variable=Var('name', any), - type_annotation=any, initializer=None, kind=ARG_POS) - add_method(ctx, '__getattr__', [name_arg], any) - - value_arg = Argument(variable=Var('value', any), - type_annotation=any, initializer=None, kind=ARG_POS) - add_method(ctx, '__setattr__', [name_arg, value_arg], any) - - -def process_model_class(ctx: ClassDefContext, ignore_unknown_attributes: bool) -> None: +class AddDefaultPrimaryKey(ModelClassInitializer): + def run_with_model_cls(self, model_cls: Type[Model]) -> None: + auto_field = model_cls._meta.auto_field + if auto_field and not self.model_classdef.info.has_readable_member(auto_field.attname): + # autogenerated field + auto_field_fullname = helpers.get_class_fullname(auto_field.__class__) + auto_field_info = self.lookup_typeinfo_or_incomplete_defn_error(auto_field_fullname) + + set_type, get_type = fields.get_field_descriptor_types(auto_field_info, is_nullable=False) + self.add_new_node_to_model_class(auto_field.attname, Instance(auto_field_info, + [set_type, get_type])) + + +class AddRelatedModelsId(ModelClassInitializer): + def run_with_model_cls(self, model_cls: Type[Model]) -> None: + for field in model_cls._meta.get_fields(): + if isinstance(field, ForeignKey): + rel_primary_key_field = self.django_context.get_primary_key_field(field.related_model) + field_info = self.lookup_class_typeinfo_or_incomplete_defn_error(rel_primary_key_field.__class__) + is_nullable = self.django_context.fields_context.get_field_nullability(field, None) + set_type, get_type = get_field_descriptor_types(field_info, is_nullable) + self.add_new_node_to_model_class(field.attname, + Instance(field_info, [set_type, get_type])) + + +class AddManagers(ModelClassInitializer): + def _is_manager_any(self, typ: Instance) -> bool: + return typ.type.fullname() == fullnames.MANAGER_CLASS_FULLNAME and type(typ.args[0]) == AnyType + + def run_with_model_cls(self, model_cls: Type[Model]) -> None: + for manager_name, manager in model_cls._meta.managers_map.items(): + manager_fullname = helpers.get_class_fullname(manager.__class__) + manager_info = self.lookup_typeinfo_or_incomplete_defn_error(manager_fullname) + + if manager_name not in self.model_classdef.info.names: + manager_type = Instance(manager_info, [Instance(self.model_classdef.info, [])]) + self.add_new_node_to_model_class(manager_name, manager_type) + else: + # create new MODELNAME_MANAGERCLASSNAME class that represents manager parametrized with current model + has_manager_any_base = any(self._is_manager_any(base) for base in manager_info.bases) + if has_manager_any_base: + custom_model_manager_name = manager.model.__name__ + '_' + manager.__class__.__name__ + bases = [] + for original_base in manager_info.bases: + if self._is_manager_any(original_base): + if original_base.type is None: + if not self.api.final_iteration: + self.api.defer() + original_base = helpers.reparametrize_instance(original_base, + [Instance(self.model_classdef.info, [])]) + bases.append(original_base) + current_module = self.api.modules[self.model_classdef.info.module_name] + custom_manager_info = helpers.add_new_class_for_module(current_module, + custom_model_manager_name, + bases=bases, + fields=OrderedDict()) + custom_manager_type = Instance(custom_manager_info, [Instance(self.model_classdef.info, [])]) + self.add_new_node_to_model_class(manager_name, custom_manager_type) + + # add _default_manager + if '_default_manager' not in self.model_classdef.info.names: + default_manager_fullname = helpers.get_class_fullname(model_cls._meta.default_manager.__class__) + default_manager_info = self.lookup_typeinfo_or_incomplete_defn_error(default_manager_fullname) + default_manager = Instance(default_manager_info, [Instance(self.model_classdef.info, [])]) + self.add_new_node_to_model_class('_default_manager', default_manager) + + # add related managers + for relation in self.django_context.get_model_relations(model_cls): + attname = relation.get_accessor_name() + if attname is None: + # no reverse accessor + continue + + related_model_info = self.lookup_class_typeinfo_or_incomplete_defn_error(relation.related_model) + + if isinstance(relation, OneToOneRel): + self.add_new_node_to_model_class(attname, Instance(related_model_info, [])) + continue + + if isinstance(relation, (ManyToOneRel, ManyToManyRel)): + manager_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.RELATED_MANAGER_CLASS_FULLNAME) + self.add_new_node_to_model_class(attname, + Instance(manager_info, [Instance(related_model_info, [])])) + continue + + +class AddExtraFieldMethods(ModelClassInitializer): + def run_with_model_cls(self, model_cls: Type[Model]) -> None: + # get_FOO_display for choices + for field in self.django_context.get_model_fields(model_cls): + if field.choices: + info = self.lookup_typeinfo_or_incomplete_defn_error('builtins.str') + return_type = Instance(info, []) + common.add_method(self.ctx, + name='get_{}_display'.format(field.attname), + args=[], + return_type=return_type) + + # get_next_by, get_previous_by for Date, DateTime + for field in self.django_context.get_model_fields(model_cls): + if isinstance(field, (DateField, DateTimeField)) and not field.null: + return_type = Instance(self.model_classdef.info, []) + common.add_method(self.ctx, + name='get_next_by_{}'.format(field.attname), + args=[Argument(Var('kwargs', AnyType(TypeOfAny.explicit)), + AnyType(TypeOfAny.explicit), + initializer=None, + kind=ARG_STAR2)], + return_type=return_type) + common.add_method(self.ctx, + name='get_previous_by_{}'.format(field.attname), + args=[Argument(Var('kwargs', AnyType(TypeOfAny.explicit)), + AnyType(TypeOfAny.explicit), + initializer=None, + kind=ARG_STAR2)], + return_type=return_type) + + +class AddMetaOptionsAttribute(ModelClassInitializer): + def run_with_model_cls(self, model_cls: Type[Model]) -> None: + if '_meta' not in self.model_classdef.info.names: + options_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.OPTIONS_CLASS_FULLNAME) + self.add_new_node_to_model_class('_meta', + Instance(options_info, [ + Instance(self.model_classdef.info, []) + ])) + + +def process_model_class(ctx: ClassDefContext, + django_context: DjangoContext) -> None: initializers = [ InjectAnyAsBaseForNestedMeta, - AddDefaultObjectsManager, - AddIdAttributeIfPrimaryKeyTrueIsNotSet, - SetIdAttrsForRelatedFields, - AddRelatedManagers, + AddDefaultPrimaryKey, + AddRelatedModelsId, + AddManagers, + AddExtraFieldMethods, + AddMetaOptionsAttribute ] for initializer_cls in initializers: - initializer_cls.from_ctx(ctx).run() - - add_dummy_init_method(ctx) - - if ignore_unknown_attributes: - add_get_set_attr_fallback_to_any(ctx) + try: + initializer_cls(ctx, django_context).run() + except helpers.IncompleteDefnException: + if not ctx.api.final_iteration: + ctx.api.defer() diff --git a/mypy_django_plugin/transformers/queryset.py b/mypy_django_plugin/transformers/queryset.py deleted file mode 100644 index d3b0e3378..000000000 --- a/mypy_django_plugin/transformers/queryset.py +++ /dev/null @@ -1,209 +0,0 @@ -from collections import OrderedDict -from typing import List, Optional, cast - -from mypy.checker import TypeChecker -from mypy.nodes import StrExpr, TypeInfo -from mypy.plugin import ( - AnalyzeTypeContext, CheckerPluginInterface, MethodContext, -) -from mypy.types import AnyType, Instance, Type, TypeOfAny - -from mypy_django_plugin import helpers -from mypy_django_plugin.lookups import ( - LookupException, RelatedModelNode, resolve_lookup, -) - - -def get_queryset_model_arg(ret_type: Instance) -> Type: - if ret_type.args: - return ret_type.args[0] - else: - return AnyType(TypeOfAny.implementation_artifact) - - -def extract_proper_type_for_queryset_values(ctx: MethodContext) -> Type: - object_type = ctx.type - if not isinstance(object_type, Instance): - return ctx.default_return_type - - fields_arg_expr = ctx.args[ctx.callee_arg_names.index('fields')] - if len(fields_arg_expr) == 0: - # values_list/values with no args is not yet supported, so default to Any types for field types - # It should in the future include all model fields, "extra" fields and "annotated" fields - return ctx.default_return_type - - model_arg = get_queryset_model_arg(ctx.default_return_type) - if isinstance(model_arg, Instance): - model_type_info = model_arg.type - else: - model_type_info = None - - column_types: OrderedDict[str, Type] = OrderedDict() - - # parse *fields - for field_expr in fields_arg_expr: - if isinstance(field_expr, StrExpr): - field_name = field_expr.value - # Default to any type - column_types[field_name] = AnyType(TypeOfAny.implementation_artifact) - - if model_type_info: - resolved_lookup_type = resolve_values_lookup(ctx.api, model_type_info, field_name) - if resolved_lookup_type is not None: - column_types[field_name] = resolved_lookup_type - else: - return ctx.default_return_type - - # parse **expressions - expression_arg_names = ctx.arg_names[ctx.callee_arg_names.index('expressions')] - for expression_name in expression_arg_names: - # Arbitrary additional annotation expressions are supported, but they all have type Any for now - column_types[expression_name] = AnyType(TypeOfAny.implementation_artifact) - - row_arg = helpers.make_typeddict(ctx.api, fields=column_types, - required_keys=set()) - return helpers.reparametrize_instance(ctx.default_return_type, [model_arg, row_arg]) - - -def extract_proper_type_queryset_values_list(ctx: MethodContext) -> Type: - object_type = ctx.type - if not isinstance(object_type, Instance): - return ctx.default_return_type - - ret = ctx.default_return_type - - model_arg = get_queryset_model_arg(ctx.default_return_type) - # model_arg: Union[AnyType, Type] = ret.args[0] if len(ret.args) > 0 else any_type - - column_names: List[Optional[str]] = [] - column_types: OrderedDict[str, Type] = OrderedDict() - - fields_arg_expr = ctx.args[ctx.callee_arg_names.index('fields')] - fields_param_is_specified = True - if len(fields_arg_expr) == 0: - # values_list/values with no args is not yet supported, so default to Any types for field types - # It should in the future include all model fields, "extra" fields and "annotated" fields - fields_param_is_specified = False - - if isinstance(model_arg, Instance): - model_type_info = model_arg.type - else: - model_type_info = None - - any_type = AnyType(TypeOfAny.implementation_artifact) - - # Figure out each field name passed to fields - only_strings_as_fields_expressions = True - for field_expr in fields_arg_expr: - if isinstance(field_expr, StrExpr): - field_name = field_expr.value - column_names.append(field_name) - # Default to any type - column_types[field_name] = any_type - - if model_type_info: - resolved_lookup_type = resolve_values_lookup(ctx.api, model_type_info, field_name) - if resolved_lookup_type is not None: - column_types[field_name] = resolved_lookup_type - else: - # Dynamic field names are partially supported for values_list, but not values - column_names.append(None) - only_strings_as_fields_expressions = False - - flat = helpers.parse_bool(helpers.get_argument_by_name(ctx, 'flat')) - named = helpers.parse_bool(helpers.get_argument_by_name(ctx, 'named')) - - api = cast(TypeChecker, ctx.api) - if named and flat: - api.fail("'flat' and 'named' can't be used together.", ctx.context) - return ret - - elif named: - # named=True, flat=False -> List[NamedTuple] - if fields_param_is_specified and only_strings_as_fields_expressions: - row_arg = helpers.make_named_tuple(api, fields=column_types, name="Row") - else: - # fallback to catch-all NamedTuple - row_arg = helpers.make_named_tuple(api, fields=OrderedDict(), name="Row") - - elif flat: - # named=False, flat=True -> List of elements - if len(ctx.args[0]) > 1: - api.fail("'flat' is not valid when values_list is called with more than one field.", - ctx.context) - return ctx.default_return_type - - if fields_param_is_specified and only_strings_as_fields_expressions: - # Grab first element - row_arg = column_types[column_names[0]] - else: - row_arg = any_type - - else: - # named=False, flat=False -> List[Tuple] - if fields_param_is_specified: - args = [ - # Fallback to Any if the column name is unknown (e.g. dynamic) - column_types.get(column_name, any_type) if column_name is not None else any_type - for column_name in column_names - ] - else: - args = [any_type] - row_arg = helpers.make_tuple(api, fields=args) - - new_type_args = [model_arg, row_arg] - return helpers.reparametrize_instance(ret, new_type_args) - - -def resolve_values_lookup(api: CheckerPluginInterface, model_type_info: TypeInfo, lookup: str) -> Optional[Type]: - """Resolves a values/values_list lookup if possible, to a Type.""" - try: - nodes = resolve_lookup(api, model_type_info, lookup) - except LookupException: - nodes = [] - - if not nodes: - return None - - make_optional = False - - for node in nodes: - if isinstance(node, RelatedModelNode) and node.is_nullable: - # All lookups following a relation which is nullable should be optional - make_optional = True - - node = nodes[-1] - - node_type = node.typ - if isinstance(node, RelatedModelNode): - # Related models used in values/values_list get resolved to the primary key of the related model. - # So, we lookup the pk of that model. - pk_lookup_nodes = resolve_lookup(api, node_type.type, "pk") - if not pk_lookup_nodes: - return None - node_type = pk_lookup_nodes[0].typ - if make_optional: - return helpers.make_optional(node_type) - else: - return node_type - - -def set_first_generic_param_as_default_for_second(fullname: str, ctx: AnalyzeTypeContext) -> Type: - if not ctx.type.args: - try: - return ctx.api.named_type(fullname, [AnyType(TypeOfAny.explicit), - AnyType(TypeOfAny.explicit)]) - except KeyError: - # really should never happen - return AnyType(TypeOfAny.explicit) - - args = ctx.type.args - if len(args) == 1: - args = [args[0], args[0]] - - analyzed_args = [ctx.api.analyze_type(arg) for arg in args] - try: - return ctx.api.named_type(fullname, analyzed_args) - except KeyError: - # really should never happen - return AnyType(TypeOfAny.explicit) diff --git a/mypy_django_plugin/transformers/querysets.py b/mypy_django_plugin/transformers/querysets.py new file mode 100644 index 000000000..7145d39a3 --- /dev/null +++ b/mypy_django_plugin/transformers/querysets.py @@ -0,0 +1,191 @@ +from collections import OrderedDict +from typing import List, Optional, Sequence, Type, Union, cast + +from django.core.exceptions import FieldError +from django.db.models.base import Model +from mypy.newsemanal.typeanal import TypeAnalyser +from mypy.nodes import Expression, NameExpr, TypeInfo +from mypy.plugin import AnalyzeTypeContext, FunctionContext, MethodContext +from mypy.types import AnyType, Instance +from mypy.types import Type as MypyType +from mypy.types import TypeOfAny + +from mypy_django_plugin.django.context import DjangoContext +from mypy_django_plugin.lib import fullnames, helpers + + +def determine_proper_manager_type(ctx: FunctionContext) -> MypyType: + default_return_type = ctx.default_return_type + assert isinstance(default_return_type, Instance) + + outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class() + if (outer_model_info is None + or not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME)): + return default_return_type + + return helpers.reparametrize_instance(default_return_type, [Instance(outer_model_info, [])]) + + +def get_field_type_from_lookup(ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model], + *, method: str, lookup: str) -> Optional[MypyType]: + try: + lookup_field = django_context.lookups_context.resolve_lookup(model_cls, lookup) + except FieldError as exc: + ctx.api.fail(exc.args[0], ctx.context) + return None + + field_get_type = django_context.fields_context.get_field_get_type(helpers.get_typechecker_api(ctx), + lookup_field, method=method) + return field_get_type + + +def get_values_list_row_type(ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model], + flat: bool, named: bool) -> MypyType: + field_lookups = resolve_field_lookups(ctx.args[0], ctx, django_context) + if field_lookups is None: + return AnyType(TypeOfAny.from_error) + + typechecker_api = helpers.get_typechecker_api(ctx) + if len(field_lookups) == 0: + if flat: + primary_key_field = django_context.get_primary_key_field(model_cls) + lookup_type = get_field_type_from_lookup(ctx, django_context, model_cls, + lookup=primary_key_field.attname, method='values_list') + assert lookup_type is not None + return lookup_type + elif named: + column_types: 'OrderedDict[str, MypyType]' = OrderedDict() + for field in django_context.get_model_fields(model_cls): + column_type = django_context.fields_context.get_field_get_type(typechecker_api, field, + method='values_list') + column_types[field.attname] = column_type + return helpers.make_oneoff_named_tuple(typechecker_api, 'Row', column_types) + else: + # flat=False, named=False, all fields + field_lookups = [] + for field in django_context.get_model_fields(model_cls): + field_lookups.append(field.attname) + + if len(field_lookups) > 1 and flat: + typechecker_api.fail("'flat' is not valid when 'values_list' is called with more than one field", ctx.context) + return AnyType(TypeOfAny.from_error) + + column_types = OrderedDict() + for field_lookup in field_lookups: + lookup_field_type = get_field_type_from_lookup(ctx, django_context, model_cls, + lookup=field_lookup, method='values_list') + if lookup_field_type is None: + return AnyType(TypeOfAny.from_error) + column_types[field_lookup] = lookup_field_type + + if flat: + assert len(column_types) == 1 + row_type = next(iter(column_types.values())) + elif named: + row_type = helpers.make_oneoff_named_tuple(typechecker_api, 'Row', column_types) + else: + row_type = helpers.make_tuple(typechecker_api, list(column_types.values())) + + return row_type + + +def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context: DjangoContext) -> MypyType: + # called on the Instance, returns QuerySet of something + assert isinstance(ctx.type, Instance) + assert isinstance(ctx.default_return_type, Instance) + + # bail if queryset of Any or other non-instances + if not isinstance(ctx.type.args[0], Instance): + return AnyType(TypeOfAny.from_omitted_generics) + + model_type = ctx.type.args[0] + model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname()) + if model_cls is None: + return ctx.default_return_type + + flat_expr = helpers.get_call_argument_by_name(ctx, 'flat') + if flat_expr is not None and isinstance(flat_expr, NameExpr): + flat = helpers.parse_bool(flat_expr) + else: + flat = False + + named_expr = helpers.get_call_argument_by_name(ctx, 'named') + if named_expr is not None and isinstance(named_expr, NameExpr): + named = helpers.parse_bool(named_expr) + else: + named = False + + if flat and named: + ctx.api.fail("'flat' and 'named' can't be used together", ctx.context) + return helpers.reparametrize_instance(ctx.default_return_type, [model_type, AnyType(TypeOfAny.from_error)]) + + # account for possible None + flat = flat or False + named = named or False + + row_type = get_values_list_row_type(ctx, django_context, model_cls, + flat=flat, named=named) + return helpers.reparametrize_instance(ctx.default_return_type, [model_type, row_type]) + + +def resolve_field_lookups(lookup_exprs: Sequence[Expression], ctx: Union[FunctionContext, MethodContext], + django_context: DjangoContext) -> Optional[List[str]]: + field_lookups = [] + for field_lookup_expr in lookup_exprs: + field_lookup = helpers.resolve_string_attribute_value(field_lookup_expr, ctx, django_context) + if field_lookup is None: + return None + field_lookups.append(field_lookup) + return field_lookups + + +def extract_proper_type_queryset_values(ctx: MethodContext, django_context: DjangoContext) -> MypyType: + # called on QuerySet, return QuerySet of something + assert isinstance(ctx.type, Instance) + assert isinstance(ctx.default_return_type, Instance) + + # if queryset of non-instance type + if not isinstance(ctx.type.args[0], Instance): + return AnyType(TypeOfAny.from_omitted_generics) + + model_type = ctx.type.args[0] + model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname()) + if model_cls is None: + return ctx.default_return_type + + field_lookups = resolve_field_lookups(ctx.args[0], ctx, django_context) + if field_lookups is None: + return AnyType(TypeOfAny.from_error) + + if len(field_lookups) == 0: + for field in django_context.get_model_fields(model_cls): + field_lookups.append(field.attname) + + column_types: 'OrderedDict[str, MypyType]' = OrderedDict() + for field_lookup in field_lookups: + field_lookup_type = get_field_type_from_lookup(ctx, django_context, model_cls, + lookup=field_lookup, method='values') + if field_lookup_type is None: + return helpers.reparametrize_instance(ctx.default_return_type, [model_type, AnyType(TypeOfAny.from_error)]) + + column_types[field_lookup] = field_lookup_type + + row_type = helpers.make_typeddict(ctx.api, column_types, set(column_types.keys())) + return helpers.reparametrize_instance(ctx.default_return_type, [model_type, row_type]) + + +def set_first_generic_param_as_default_for_second(ctx: AnalyzeTypeContext, fullname: str) -> MypyType: + type_analyser_api = cast(TypeAnalyser, ctx.api) + + info = helpers.lookup_fully_qualified_typeinfo(type_analyser_api.api, fullname) # type: ignore + assert isinstance(info, TypeInfo) + + if not ctx.type.args: + return Instance(info, [AnyType(TypeOfAny.explicit), AnyType(TypeOfAny.explicit)]) + + args = ctx.type.args + if len(args) == 1: + args = [args[0], args[0]] + + analyzed_args = [type_analyser_api.analyze_type(arg) for arg in args] + return Instance(info, analyzed_args) diff --git a/mypy_django_plugin/transformers/related.py b/mypy_django_plugin/transformers/related.py deleted file mode 100644 index 46d2e02e6..000000000 --- a/mypy_django_plugin/transformers/related.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Optional, Union - -from mypy.checkmember import AttributeContext -from mypy.nodes import TypeInfo -from mypy.types import AnyType, Instance, Type, TypeOfAny, UnionType - -from mypy_django_plugin import helpers - - -def _extract_referred_to_type_info(typ: Union[UnionType, Instance]) -> Optional[TypeInfo]: - if isinstance(typ, Instance): - return typ.type - else: - # should be Union[TYPE, None] - typ = helpers.make_required(typ) - if isinstance(typ, Instance): - return typ.type - return None - - -def extract_and_return_primary_key_of_bound_related_field_parameter(ctx: AttributeContext) -> Type: - if not isinstance(ctx.default_attr_type, Instance) or not (ctx.default_attr_type.type.fullname() == 'builtins.int'): - return ctx.default_attr_type - - if not isinstance(ctx.type, Instance) or not ctx.type.type.has_base(helpers.MODEL_CLASS_FULLNAME): - return ctx.default_attr_type - - field_name = ctx.context.name.split('_')[0] - sym = ctx.type.type.get(field_name) - if sym and isinstance(sym.type, Instance) and len(sym.type.args) > 0: - referred_to = sym.type.args[1] - if isinstance(referred_to, AnyType): - return AnyType(TypeOfAny.implementation_artifact) - - model_type = _extract_referred_to_type_info(referred_to) - if model_type is None: - return AnyType(TypeOfAny.implementation_artifact) - - primary_key_type = helpers.extract_primary_key_type_for_get(model_type) - if primary_key_type: - return primary_key_type - - is_nullable = helpers.is_field_nullable(ctx.type.type, field_name) - if is_nullable: - return helpers.make_optional(ctx.default_attr_type) - - return ctx.default_attr_type - - -def determine_type_of_related_manager(ctx: AttributeContext, related_manager_name: str) -> Type: - if not isinstance(ctx.type, Instance): - return ctx.default_attr_type - - related_manager_type = helpers.get_related_manager_type_from_metadata(ctx.type.type, - related_manager_name, ctx.api) - if not related_manager_type: - return ctx.default_attr_type - - return related_manager_type diff --git a/mypy_django_plugin/transformers/request.py b/mypy_django_plugin/transformers/request.py new file mode 100644 index 000000000..be584ab42 --- /dev/null +++ b/mypy_django_plugin/transformers/request.py @@ -0,0 +1,16 @@ +from mypy.plugin import AttributeContext +from mypy.types import Instance +from mypy.types import Type as MypyType + +from mypy_django_plugin.django.context import DjangoContext +from mypy_django_plugin.lib import helpers + + +def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_context: DjangoContext) -> MypyType: + auth_user_model = django_context.settings.AUTH_USER_MODEL + model_cls = django_context.apps_registry.get_model(auth_user_model) + model_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), model_cls) + if model_info is None: + return ctx.default_attr_type + + return Instance(model_info, []) diff --git a/mypy_django_plugin/transformers/settings.py b/mypy_django_plugin/transformers/settings.py index 1e72485eb..ba6490b4e 100644 --- a/mypy_django_plugin/transformers/settings.py +++ b/mypy_django_plugin/transformers/settings.py @@ -1,81 +1,50 @@ -from typing import TYPE_CHECKING, List, Optional, cast +from mypy.nodes import MemberExpr +from mypy.plugin import AttributeContext, FunctionContext +from mypy.types import AnyType, Instance +from mypy.types import Type as MypyType +from mypy.types import TypeOfAny, TypeType -from mypy.checkexpr import FunctionContext -from mypy.checkmember import AttributeContext -from mypy.nodes import NameExpr, StrExpr, SymbolTableNode, TypeInfo -from mypy.types import AnyType, Instance, Type, TypeOfAny, TypeType +from mypy_django_plugin.django.context import DjangoContext +from mypy_django_plugin.lib import helpers -from mypy_django_plugin import helpers -if TYPE_CHECKING: - from mypy.checker import TypeChecker +def get_user_model_hook(ctx: FunctionContext, django_context: DjangoContext) -> MypyType: + auth_user_model = django_context.settings.AUTH_USER_MODEL + model_cls = django_context.apps_registry.get_model(auth_user_model) + model_cls_fullname = helpers.get_class_fullname(model_cls) + model_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx), + model_cls_fullname) + if model_info is None: + return AnyType(TypeOfAny.unannotated) -def get_setting_sym(name: str, api: 'TypeChecker', settings_modules: List[str]) -> Optional[SymbolTableNode]: - for settings_mod_name in settings_modules: - if settings_mod_name not in api.modules: - continue - - file = api.modules[settings_mod_name] - sym = file.names.get(name) - if sym is not None: - return sym - - return None - - -def get_type_of_setting(ctx: AttributeContext, setting_name: str, - settings_modules: List[str], ignore_missing_settings: bool) -> Type: - setting_sym = get_setting_sym(setting_name, ctx.api, settings_modules) - if setting_sym: - if setting_sym.type is None: - # TODO: defer till setting_sym.type is not None - return AnyType(TypeOfAny.implementation_artifact) + return TypeType(Instance(model_info, [])) - return setting_sym.type - if not ignore_missing_settings: +def get_type_of_settings_attribute(ctx: AttributeContext, django_context: DjangoContext) -> MypyType: + assert isinstance(ctx.context, MemberExpr) + setting_name = ctx.context.name + if not hasattr(django_context.settings, setting_name): ctx.api.fail(f"'Settings' object has no attribute {setting_name!r}", ctx.context) + return ctx.default_attr_type - return ctx.default_attr_type - + typechecker_api = helpers.get_typechecker_api(ctx) -def return_user_model_hook(ctx: FunctionContext, settings_modules: List[str]) -> Type: - from mypy.checker import TypeChecker + # first look for the setting in the project settings file, then global settings + settings_module = typechecker_api.modules.get(django_context.django_settings_module) + global_settings_module = typechecker_api.modules.get('django.conf.global_settings') + for module in [settings_module, global_settings_module]: + if module is not None: + sym = module.names.get(setting_name) + if sym is not None and sym.type is not None: + return sym.type - api = cast(TypeChecker, ctx.api) + # if by any reason it isn't present there, get type from django settings + value = getattr(django_context.settings, setting_name) + value_fullname = helpers.get_class_fullname(value.__class__) - setting_sym = get_setting_sym('AUTH_USER_MODEL', api, settings_modules) - if setting_sym is None: - return ctx.default_return_type + value_info = helpers.lookup_fully_qualified_typeinfo(typechecker_api, value_fullname) + if value_info is None: + return ctx.default_attr_type - setting_module_name, _, _ = setting_sym.fullname.rpartition('.') - setting_module = api.modules[setting_module_name] - - model_path = None - for name_expr, rvalue_expr in helpers.iter_over_assignments(setting_module): - if isinstance(name_expr, NameExpr) and isinstance(rvalue_expr, StrExpr): - if name_expr.name == 'AUTH_USER_MODEL': - model_path = rvalue_expr.value - break - - if not model_path: - return ctx.default_return_type - - app_label, _, model_class_name = model_path.rpartition('.') - if app_label is None: - return ctx.default_return_type - - model_fullname = helpers.get_model_fullname(app_label, model_class_name, - all_modules=api.modules) - if model_fullname is None: - api.fail(f'"{app_label}.{model_class_name}" model class is not imported so far. Try to import it ' - f'(under if TYPE_CHECKING) at the beginning of the current file', - context=ctx.context) - return ctx.default_return_type - - model_info = helpers.lookup_fully_qualified_generic(model_fullname, - all_modules=api.modules) - if model_info is None or not isinstance(model_info, TypeInfo): - return ctx.default_return_type - return TypeType(Instance(model_info, [])) + return Instance(value_info, []) diff --git a/pyproject.toml b/pyproject.toml index 78fe223df..f1895d9ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [tool.black] line-length = 120 -include = 'django-stubs/.*.pyi$' \ No newline at end of file +include = 'django-stubs/.*\.pyi$' \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index a0afe5454..e918bd2bc 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,6 +2,8 @@ testpaths = ./test-data addopts = --tb=native - --mypy-ini-file=./test-data/plugins.ini -s -v + --cache-clear + --mypy-ini-file=./test-data/plugins.ini + --mypy-extension-hook=scripts.tests_extension_hook.django_plugin_hook diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/django_tests_settings.py b/scripts/django_tests_settings.py new file mode 100644 index 000000000..7c698d838 --- /dev/null +++ b/scripts/django_tests_settings.py @@ -0,0 +1,229 @@ +SECRET_KEY = '1' +SITE_ID = 1 + +INSTALLED_APPS = [ + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sites', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.admin.apps.SimpleAdminConfig', + 'django.contrib.staticfiles', +] + +test_modules = [ + 'absolute_url_overrides', + 'admin_autodiscover', + 'admin_changelist', + 'admin_checks', + 'admin_custom_urls', + 'admin_default_site', + 'admin_docs', + 'admin_filters', + 'admin_inlines', + 'admin_ordering', + 'admin_registration', + 'admin_scripts', + 'admin_utils', + 'admin_views', + 'admin_widgets', + 'aggregation', + 'aggregation_regress', + 'annotations', + 'app_loading', + 'apps', + 'auth_tests', + 'backends', + 'base', + 'bash_completion', + 'basic', + 'builtin_server', + 'bulk_create', + 'cache', + 'check_framework', + 'choices', + 'conditional_processing', + 'constraints', + 'contenttypes_tests', + 'context_processors', + 'csrf_tests', + 'custom_columns', + 'custom_lookups', + 'custom_managers', + 'custom_methods', + 'custom_migration_operations', + 'custom_pk', + 'datatypes', + 'dates', + 'datetimes', + 'db_functions', + 'db_typecasts', + 'db_utils', + 'dbshell', + 'decorators', + 'defer', + 'defer_regress', + 'delete', + 'delete_regress', + 'deprecation', + 'dispatch', + 'distinct_on_fields', + 'empty', + 'expressions', + 'expressions_case', + 'expressions_window', + 'extra_regress', + 'field_deconstruction', + 'field_defaults', + 'field_subclassing', + 'file_storage', + 'file_uploads', + 'files', + 'filtered_relation', + 'fixtures', + 'fixtures_model_package', + 'fixtures_regress', + 'flatpages_tests', + 'force_insert_update', + 'foreign_object', + 'forms_tests', + 'from_db_value', + 'generic_inline_admin', + 'generic_relations', + 'generic_relations_regress', + 'generic_views', + 'get_earliest_or_latest', + 'get_object_or_404', + 'get_or_create', + 'gis_tests', + 'handlers', + 'httpwrappers', + 'humanize_tests', + 'i18n', + 'import_error_package', + 'indexes', + 'inline_formsets', + 'inspectdb', + 'introspection', + 'invalid_models_tests', + 'known_related_objects', + 'logging_tests', + 'lookup', + 'm2m_and_m2o', + 'm2m_intermediary', + 'm2m_multiple', + 'm2m_recursive', + 'm2m_regress', + 'm2m_signals', + 'm2m_through', + 'm2m_through_regress', + 'm2o_recursive', + 'mail', + 'managers_regress', + 'many_to_many', + 'many_to_one', + 'many_to_one_null', + 'max_lengths', + 'messages_tests', + 'middleware', + 'middleware_exceptions', + 'migrate_signals', + 'migration_test_data_persistence', + 'migrations', + 'migrations2', + 'model_fields', + 'model_forms', + 'model_formsets', + 'model_formsets_regress', + 'model_indexes', + 'model_inheritance', + 'model_inheritance_regress', + 'model_meta', + 'model_options', + 'model_package', + 'model_regress', + 'modeladmin', + 'multiple_database', + 'mutually_referential', + 'nested_foreign_keys', + 'no_models', + 'null_fk', + 'null_fk_ordering', + 'null_queries', + 'one_to_one', + 'or_lookups', + 'order_with_respect_to', + 'ordering', + 'pagination', + 'postgres_tests', + 'prefetch_related', + 'project_template', + 'properties', + 'proxy_model_inheritance', + 'proxy_models', + 'queries', + 'queryset_pickle', + 'raw_query', + 'redirects_tests', + 'requests', + 'reserved_names', + 'resolve_url', + 'responses', + 'reverse_lookup', + 'save_delete_hooks', + 'schema', + 'select_for_update', + 'select_related', + 'select_related_onetoone', + 'select_related_regress', + 'serializers', + 'servers', + 'sessions_tests', + 'settings_tests', + 'shell', + 'shortcuts', + 'signals', + 'signed_cookies_tests', + 'signing', + 'sitemaps_tests', + 'sites_framework', + 'sites_tests', + 'staticfiles_tests', + 'str', + 'string_lookup', + 'swappable_models', + 'syndication_tests', + 'template_backends', + 'template_loader', + 'template_tests', + 'test_client', + 'test_client_regress', + 'test_exceptions', + 'test_runner', + 'test_runner_apps', + 'test_utils', + 'timezones', + 'transaction_hooks', + 'transactions', + 'unmanaged_models', + 'update', + 'update_only_fields', + 'urlpatterns', + 'urlpatterns_reverse', + 'user_commands', + 'utils_tests', + 'validation', + 'validators', + 'version', + 'view_tests', + 'wsgi', +] + +invalid_apps = { + 'import_error_package', +} + +for app in invalid_apps: + test_modules.remove(app) + +INSTALLED_APPS += test_modules diff --git a/scripts/enabled_test_modules.py b/scripts/enabled_test_modules.py new file mode 100644 index 000000000..69ad718fd --- /dev/null +++ b/scripts/enabled_test_modules.py @@ -0,0 +1,337 @@ +# Some errors occur for the test suite itself, and cannot be addressed via django-stubs. They should be ignored +# using this constant. +import re + +IGNORED_MODULES = {'schema', 'gis_tests', 'admin_widgets', 'admin_filters', 'migrations', + 'sitemaps_tests', 'staticfiles_tests', 'modeladmin', 'model_forms', + 'generic_views', 'forms_tests', 'flatpages_tests', 'admin_utils', + 'admin_ordering', 'admin_changelist', 'admin_views', 'mail', 'redirects_tests', + 'invalid_models_tests', 'i18n', 'migrate_signals', 'model_formsets', + 'template_tests', 'template_backends', 'test_runner', 'admin_scripts', + 'sites_tests', 'inline_formsets', 'foreign_object', 'cache', 'test_client', 'test_client_regress'} + +MOCK_OBJECTS = ['MockRequest', 'MockCompiler', 'modelz', 'call_count', 'call_args_list', + 'call_args', 'MockUser', 'Xtemplate', 'DummyRequest', 'DummyUser', 'MinimalUser'] +EXTERNAL_MODULES = ['psycopg2', 'PIL', 'selenium', 'oracle', 'mysql', 'sqlite3', 'sqlparse', 'tblib', 'numpy', + 'bcrypt', 'argon2', 'xml.dom'] +IGNORED_ERRORS = { + '__new_common__': [ + *MOCK_OBJECTS, + *EXTERNAL_MODULES, + 'SupportsFloat', + 'Need type annotation for', + 'has no attribute "getvalue"', + 'Cannot assign to a method', + 'Cannot infer type of lambda', + 'already defined (possibly by an import)', + 'already defined on line', + 'Cannot assign to a type', + '"HttpResponse" has no attribute', + '"HttpResponseBase" has no attribute', + # '"HttpRequest" has no attribute', + '"object" has no attribute', + 'defined in the current module', + re.compile(r'"Callable\[(\[(Any(, )?)*((, )?VarArg\(Any\))?((, )?KwArg\(Any\))?\]|\.\.\.), Any\]" ' + r'has no attribute'), + 'has no attribute "deconstruct"', + # private members + re.compile(r'has no attribute ("|\')_[a-zA-Z_]+("|\')'), + "'Settings' object has no attribute", + '**Dict', + re.compile(r"Expression of type '.*' is not supported"), + 'has incompatible type "object"', + 'undefined in superclass', + 'Argument after ** must be a mapping, not "object"', + 'note:', + re.compile(r'Item "None" of "[a-zA-Z_ ,\[\]]+" has no attribute'), + '"Optional[List[_Record]]"', + '"Callable[..., None]" has no attribute', + 'does not return a value', + 'has no attribute "alternatives"', + 'gets multiple values for keyword argument', + '"Handler" has no attribute', + 'Module has no attribute', + "No installed app with label 'missing'", + 'namedtuple', + 'Lookups not supported yet', + 'Argument 1 to "loads" has incompatible type', + # TODO: see test in managers/test_managers.yml + "Cannot determine type of", + 'cache_clear', + 'cache_info', + 'Incompatible types in assignment (expression has type "None", variable has type Module)', + "Module 'django.contrib.messages.storage.fallback' has no attribute 'CookieStorage'", + # TODO: not supported yet + 'GenericRelation', + 'RelatedObjectDoesNotExist', + # Rel's attributes are not accessible from `get_field()` + re.compile(r'"Field\[Any, Any\]" has no attribute ' + r'"(through|field_name|field|get_related_field|related_model|related_name' + r'|get_accessor_name|empty_strings_allowed|many_to_many)"'), + # TODO: multitable inheritance + 'ptr', + 'Incompatible types in assignment (expression has type "Callable[', + 'SimpleLazyObject' + ], + 'apps': [ + 'Incompatible types in assignment (expression has type "str", target has type "type")', + ], + 'auth_tests': [ + '"PasswordValidator" has no attribute "min_length"', + 'AbstractBaseUser', + 'Argument "password_validators" to "password_changed" has incompatible type "Tuple[Validator]"; ' + + 'expected "Optional[Sequence[PasswordValidator]]"', + 'Unsupported right operand type for in ("object")', + 'mock_getpass', + 'Unsupported left operand type for + ("Sequence[str]")', + 'AuthenticationFormWithInactiveUsersOkay', + 'Incompatible types in assignment (expression has type "Dict[str, Any]", variable has type "QueryDict")', + ], + 'basic': [ + 'Unexpected keyword argument "unknown_kwarg" for "refresh_from_db" of "Model"', + 'Unexpected attribute "foo" for model "Article"', + 'has no attribute "touched"' + ], + 'backends': [ + '"DatabaseError" has no attribute "pgcode"' + ], + 'check_framework': [ + 'base class "Model" defined the type as "Callable', + ], + 'constraints': [ + 'Argument "condition" to "UniqueConstraint" has incompatible type "str"; expected "Optional[Q]"' + ], + 'contenttypes_tests': [ + # 'Item "Model" of "Union[GenericForeignKey, Model, None]" has no attribute' + 'base class "BaseOrderWithRespectToTests" defined the type as "None"' + ], + 'custom_lookups': [ + 'in base class "SQLFuncMixin"' + ], + 'custom_pk': [ + '"Employee" has no attribute "id"' + ], + 'custom_managers': [ + 'Unsupported dynamic base class', + '"Book" has no attribute "favorite_avg"', + 'Incompatible types in assignment (expression has type "CharField' + ], + 'csrf_tests': [ + 'Incompatible types in assignment (expression has type "property", ' + + 'base class "HttpRequest" defined the type as "QueryDict")' + ], + 'dates': [ + 'Too few arguments for "dates" of "QuerySet"', + ], + 'defer': [ + 'Too many arguments for "refresh_from_db" of "Model"' + ], + 'dispatch': [ + 'Item "str" of "Union[ValueError, str]" has no attribute "args"' + ], + 'deprecation': [ + '"Manager" has no attribute "old"', + '"Manager" has no attribute "new"' + ], + 'db_functions': [ + '"FloatModel" has no attribute', + ], + 'decorators': [ + '"Type[object]" has no attribute "method"' + ], + 'expressions_case': [ + 'Item "Field[Any, Any]" of "Union[Field[Any, Any], ForeignObjectRel]" has no attribute "is_relation"' + ], + 'expressions_window': [ + 'has incompatible type "str"' + ], + 'file_uploads': [ + '"Iterable[Any]" has no attribute', + '"IO[Any]" has no attribute' + ], + 'file_storage': [ + 'Incompatible types in assignment (expression has type "Callable"' + ], + 'files': [ + 'Incompatible types in assignment (expression has type "IOBase", variable has type "File")', + ], + 'fixtures': [ + 'Incompatible types in assignment (expression has type "int", target has type "Iterable[str]")', + 'Incompatible types in assignment (expression has type "SpyManager[Spy]"' + ], + 'from_db_value': [ + '"Cash" has no attribute' + ], + 'get_object_or_404': [ + 'Argument 1 to "get_object_or_404" has incompatible type "str"; ' + + 'expected "Union[Type[], QuerySet[, ]]"', + 'Argument 1 to "get_list_or_404" has incompatible type "List[Type[Article]]"; ' + + 'expected "Union[Type[], QuerySet[, ]]"', + 'CustomClass' + ], + 'generic_relations_regress': [ + '"Link" has no attribute' + ], + 'httpwrappers': [ + 'Argument 2 to "appendlist" of "QueryDict"', + 'Incompatible types in assignment (expression has type "int", target has type "Union[str, List[str]]")', + 'Argument 1 to "fromkeys" of "QueryDict" has incompatible type "int"' + ], + 'humanize_tests': [ + 'Argument 1 to "append" of "list" has incompatible type "None"; expected "str"' + ], + 'lookup': [ + 'Unexpected keyword argument "headline__startswith" for "in_bulk" of "QuerySet"', + 'is called with more than one field' + ], + 'messages_tests': [ + 'List item 0 has incompatible type "Dict[str, Message]"; expected "Message"' + ], + 'middleware': [ + '"HttpRequest" has no attribute' + ], + 'managers_regress': [ + '"Type[AbstractBase3]" has no attribute "objects"' + ], + 'many_to_one': [ + 'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")', + 'Incompatible type for "parent" of "Child" (got "Child", expected "Union[Parent, Combinable]")' + ], + 'middleware_exceptions': [ + 'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any]"; expected "str"' + ], + 'model_fields': [ + 'Item "Field[Any, Any]" of "Union[Field[Any, Any], ForeignObjectRel]" has no attribute', + 'has no attribute "field"', + 'Incompatible types in assignment (expression has type "Type[Person', + 'base class "IntegerFieldTests"', + 'ImageFieldTestMixin', + 'Incompatible types in assignment (expression has type "FloatModel", variable has type', + ], + 'model_indexes': [ + 'Argument "condition" to "Index" has incompatible type "str"; expected "Optional[Q]"' + ], + 'model_inheritance': [ + 'base class "AbstractBase" defined', + 'base class "AbstractModel" defined', + 'Definition of "name" in base class "ConcreteParent"', + ' Definition of "name" in base class "AbstractParent"', + 'referent_references' + ], + 'model_meta': [ + 'List item 0 has incompatible type "str"; expected "Union[Field[Any, Any], ForeignObjectRel]"' + ], + 'model_regress': [ + 'Incompatible type for "department" of "Worker"', + '"PickledModel" has no attribute', + '"Department" has no attribute "evaluate"', + ], + 'multiple_database': [ + 'Unexpected attribute "extra_arg" for model "Book"' + ], + 'order_with_respect_to': [ + 'BaseOrderWithRespectToTests', + '"Dimension" has no attribute "set_component_order"', + ], + 'one_to_one': [ + 'expression has type "None", variable has type "UndergroundBar"' + ], + 'postgres_tests': [ + 'DummyArrayField', + 'DummyJSONField', + 'Incompatible types in assignment (expression has type "Type[Field[Any, Any]]', + 'Argument "encoder" to "JSONField" has incompatible type "DjangoJSONEncoder";', + re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", ' + r'base class "(UnaccentTest|TrigramTest)" defined the type as "Type\[CharFieldModel\]"\)'), + '("None" and "SearchQuery")', + # TODO: + 'django.contrib.postgres.forms', + 'django.contrib.postgres.aggregates', + ], + 'properties': [ + re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"') + ], + 'prefetch_related': [ + '"Person" has no attribute "houses_lst"', + '"Book" has no attribute "first_authors"', + '"Book" has no attribute "the_authors"', + 'Incompatible types in assignment (expression has type "List[Room]", variable has type "QuerySet[Room, Room]")', + '"Room" has no attribute "main_room_of_attr"', + '"Room" has no attribute "house_attr"' + ], + 'proxy_models': [ + 'Incompatible types in assignment', + 'in base class "User"' + ], + 'queries': [ + 'Incompatible types in assignment (expression has type "None", variable has type "str")', + 'Invalid index type "Optional[str]" for "Dict[str, int]"; expected type "str"', + 'Unsupported operand types for & ("QuerySet[Author, Author]" and "QuerySet[Tag, Tag]")', + 'Unsupported operand types for | ("QuerySet[Author, Author]" and "QuerySet[Tag, Tag]")', + 'ObjectA', + 'ObjectB', + 'ObjectC', + "'flat' and 'named' can't be used together", + ], + 'requests': [ + 'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")' + ], + 'responses': [ + 'Argument 1 to "TextIOWrapper" has incompatible type "HttpResponse"; expected "IO[bytes]"' + ], + 'settings_tests': [ + 'Argument 1 to "Settings" has incompatible type "Optional[str]"; expected "str"' + ], + 'signals': [ + 'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any, Optional[Any], Any]";' + ], + 'syndication_tests': [ + 'List or tuple expected as variable arguments' + ], + 'sessions_tests': [ + 'base class "SessionTestsMixin" defined the type as "None")', + 'Incompatible types in assignment (expression has type "None", variable has type "int")', + '"AbstractBaseSession" has no attribute' + ], + 'select_related_onetoone': [ + 'Incompatible types in assignment (expression has type "Parent2", variable has type "Parent1")', + '"Parent1" has no attribute' + ], + 'servers': [ + re.compile('Argument [0-9] to "WSGIRequestHandler"'), + '"HTTPResponse" has no attribute', + '"type" has no attribute', + '"WSGIRequest" has no attribute "makefile"' + ], + 'serializers': [ + '"SerializersTestBase" defined the type as "None"', + '"Model" has no attribute "data"', + '"Iterable[Any]" has no attribute "content"', + ], + 'transactions': [ + 'Incompatible types in assignment (expression has type "Thread", variable has type "Callable[[], Any]")' + ], + 'urlpatterns': [ + '"object" not callable' + ], + 'urlpatterns_reverse': [ + 'List or tuple expected as variable arguments', + 'No overload variant of "zip" matches argument types "Any", "object"', + 'Argument 1 to "get_callable" has incompatible type "int"' + ], + 'utils_tests': [ + 'Too few arguments for "__init__"', + 'Argument 1 to "activate" has incompatible type "None"; expected "Union[tzinfo, str]"', + 'Incompatible types in assignment (expression has type "None", base class "object" defined the type as', + 'Class', + 'has no attribute "cp"', + 'Argument "name" to "cached_property" has incompatible type "int"; expected "Optional[str]"', + 'has no attribute "sort"', + 'Unsupported target for indexed assignment', + 'defined the type as "None"', + 'Argument 1 to "Path" has incompatible type "Optional[str]"' + ], + 'view_tests': [ + "Module 'django.views.debug' has no attribute 'Path'" + ] +} diff --git a/scripts/mypy.ini b/scripts/mypy.ini index ccc7346ff..f6070bd97 100644 --- a/scripts/mypy.ini +++ b/scripts/mypy.ini @@ -1,6 +1,6 @@ [mypy] strict_optional = True -ignore_missing_imports = False +ignore_missing_imports = True check_untyped_defs = True warn_no_return = False show_traceback = True @@ -9,3 +9,6 @@ incremental = True plugins = mypy_django_plugin.main + +[mypy.plugins.django-stubs] +django_settings_module = 'django_tests_settings' \ No newline at end of file diff --git a/scripts/tests_extension_hook.py b/scripts/tests_extension_hook.py new file mode 100644 index 000000000..f69d03410 --- /dev/null +++ b/scripts/tests_extension_hook.py @@ -0,0 +1,30 @@ +from pytest_mypy.collect import File +from pytest_mypy.item import YamlTestItem + + +def django_plugin_hook(test_item: YamlTestItem) -> None: + custom_settings = test_item.parsed_test_data.get('custom_settings', '') + installed_apps = test_item.parsed_test_data.get('installed_apps', None) + + if installed_apps and custom_settings: + raise ValueError('"installed_apps" and "custom_settings" are not compatible, please use one or the other') + + if installed_apps is not None: + # custom_settings is empty, add INSTALLED_APPS + installed_apps += ['django.contrib.contenttypes'] + installed_apps_as_str = '(' + ','.join([repr(app) for app in installed_apps]) + ',)' + custom_settings += 'INSTALLED_APPS = ' + installed_apps_as_str + + if 'SECRET_KEY' not in custom_settings: + custom_settings = 'SECRET_KEY = "1"\n' + custom_settings + + django_settings_section = "\n[mypy.plugins.django-stubs]\n" \ + "django_settings_module = mysettings" + if not test_item.additional_mypy_config: + test_item.additional_mypy_config = django_settings_section + else: + if '[mypy.plugins.django-stubs]' not in test_item.additional_mypy_config: + test_item.additional_mypy_config += django_settings_section + + mysettings_file = File(path='mysettings.py', content=custom_settings) + test_item.files.append(mysettings_file) diff --git a/scripts/typecheck-tests-requirements.txt b/scripts/typecheck-tests-requirements.txt deleted file mode 100644 index 5396f9cce..000000000 --- a/scripts/typecheck-tests-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -gitpython \ No newline at end of file diff --git a/scripts/typecheck_tests.py b/scripts/typecheck_tests.py index 583d1d2dc..fe09528bb 100644 --- a/scripts/typecheck_tests.py +++ b/scripts/typecheck_tests.py @@ -1,809 +1,23 @@ import itertools -import os -import re import shutil +import subprocess import sys -from contextlib import contextmanager from pathlib import Path from typing import Pattern -from git import Repo -from mypy import build -from mypy.main import process_options +from scripts.enabled_test_modules import IGNORED_ERRORS, IGNORED_MODULES PROJECT_DIRECTORY = Path(__file__).parent.parent -# Django branch to typecheck against -DJANGO_BRANCH = 'stable/2.2.x' -# Specific commit in the Django repository to check against -DJANGO_COMMIT_SHA = '395cf7c37514b642c4bcf30e01fc1a2c4f82b2fe' - -# Some errors occur for the test suite itself, and cannot be addressed via django-stubs. They should be ignored -# using this constant. -MOCK_OBJECTS = ['MockRequest', 'MockCompiler', 'modelz', 'call_count', 'call_args_list', 'call_args', 'MockUser'] -IGNORED_ERRORS = { - '__common__': [ - *MOCK_OBJECTS, - 'LazySettings', - 'NullTranslations', - 'Need type annotation for', - 'Invalid value for a to= parameter', - 'already defined (possibly by an import)', - 'already defined on line', - 'gets multiple values for keyword argument', - 'Cannot assign to a type', - re.compile(r'Cannot assign to class variable "[a-z_]+" via instance'), - # forms <-> models plugin support - # '"Model" has no attribute', - re.compile(r'Cannot determine type of \'(objects|stuff)\''), - # settings - re.compile(r'Module has no attribute "[A-Z_]+"'), - # attributes assigned to test functions - re.compile( - r'"Callable\[(\[(Any(, )?)*((, )?VarArg\(Any\))?((, )?KwArg\(Any\))?\]|\.\.\.), Any\]" has no attribute'), - # assign empty tuple - re.compile(r'Incompatible types in assignment \(expression has type "Tuple\[\]", ' - r'variable has type "Tuple\[[A-Za-z, ]+\]"'), - # assign method to a method - 'Cannot assign to a method', - 'Cannot infer type of lambda', - re.compile(r'Incompatible types in assignment \(expression has type "Callable\[\[(Any(, )?)+\], Any\]", ' - r'variable has type "Callable\['), - # cookies private attribute - 'full_clean" of "Model" does not return a value', - # private members - re.compile(r'has no attribute ("|\')_[a-zA-Z_]+("|\')'), - 'Invalid base class', - 'ValuesIterable', - 'Value of type "Optional[Dict[str, Any]]" is not indexable', - 'Argument 1 to "len" has incompatible type "Optional[List[_Record]]"; expected "Sized"', - 'Argument 1 to "loads" has incompatible type "Union[bytes, str, None]"; ' - + 'expected "Union[str, bytes, bytearray]"', - 'Incompatible types in assignment (expression has type "None", variable has type Module)', - 'note:', - '\'Settings\' object has no attribute', - re.compile(r'"Type\[Model\]" has no attribute "[a-zA-Z_]+"'), - re.compile(r'Item "None" of "[a-zA-Z_ ,\[\]]+" has no attribute'), - 'Xtemplate', - re.compile(r'has no attribute "get_[a-z_]+_display"'), - re.compile(r'has no attribute "get_next_by_[a-z_]+"'), - re.compile(r'has no attribute "get_previous_by_[a-z_]+"'), - re.compile(r'has no attribute "set_[a-z_]+_order"'), - 'psycopg2', - 'PIL', - 'has no attribute "getvalue"', - 'MySQLdb', - 'sqlparse', - 'selenium', - 'oracle', - 'mysql', - 'sqlite3', - 'LogEntry', - '"HttpResponse" has no attribute', - '"HttpResponseBase" has no attribute', - '"object" has no attribute', - '"HttpRequest" has no attribute', - 'xml.dom', - 'numpy', - 'tblib', - 'bcrypt', - 'argon2', - # TODO: values().annotate() - 'TypedDict', - 'namedtuple', - 'has no attribute "deconstruct"', - '**Dict', - 'undefined in superclass', - 'SimpleLazyObject' - ], - 'admin_scripts': [ - 'Incompatible types in assignment (expression has type "Callable[' - ], - 'admin_utils': [ - re.compile(r'Argument [0-9] to "lookup_field" has incompatible type'), - 'MockModelAdmin', - 'Incompatible types in assignment (expression has type "str", variable has type "Callable[..., Any]")', - '"Article" has no attribute "non_field"' - ], - 'admin_views': [ - 'Argument 1 to "FileWrapper" has incompatible type "StringIO"; expected "IO[bytes]"', - 'Incompatible types in assignment', - '"object" not callable', - '"Type[SubscriberAdmin]" has no attribute "overridden"' - ], - 'admin_ordering': [ - '"Band" has no attribute "field"' - ], - 'aggregation': [ - 'Incompatible type for "contact" of "Book" (got "Optional[Author]", expected "Union[Author, Combinable]")', - 'Incompatible type for "publisher" of "Book" (got "Optional[Publisher]", ' - + 'expected "Union[Publisher, Combinable]")', - 'has no attribute' - ], - 'aggregation_regress': [ - 'has no attribute' - ], - 'annotations': [ - 'Incompatible type for "store" of "Employee" (got "Optional[Store]", expected "Union[Store, Combinable]")', - '"Book" has no attribute', - '"Employee" has no attribute', - 'Item "Book" of', - 'Item "Ticket" of' - ], - 'apps': [ - 'Incompatible types in assignment (expression has type "str", target has type "type")', - '"Callable[[bool, bool], List[Type[Model]]]" has no attribute "cache_clear"' - ], - 'auth_tests': [ - '"PasswordValidator" has no attribute "min_length"', - '"validate_password" does not return a value', - '"password_changed" does not return a value', - re.compile(r'"validate" of "([A-Za-z]+)" does not return a value'), - 'Module has no attribute "SessionStore"', - 'AbstractBaseUser', - 'Argument "user" to "password_changed" has incompatible type "object"', - 'Argument "password_validators" to "password_changed" has incompatible type "Tuple[Validator]"; ' - + 'expected "Optional[Sequence[PasswordValidator]]"', - 'Value of type "Optional[List[_Record]]" is not indexable', - '"Model" has no attribute', - 'Incompatible type for "id" of "User"', - re.compile(r'Module has no attribute "(update|revert)_proxy_model_permissions"'), - 'Unsupported right operand type for in ("object")', - 'mock_getpass', - 'Unsupported left operand type for + ("Sequence[str]")', - 'AuthenticationFormWithInactiveUsersOkay', - 'has no attribute "alternatives"', - 'Incompatible types in assignment (expression has type "Dict[str, Any]", variable has type "QueryDict")' - ], - 'basic': [ - 'Unexpected keyword argument "unknown_kwarg" for "refresh_from_db" of "Model"', - 'Unexpected attribute "foo" for model "Article"', - 'has no attribute' - ], - 'backends': [ - '"DatabaseError" has no attribute "pgcode"' - ], - 'check_framework': [ - 'base class "Model" defined the type as "Callable', - 'Cannot determine type of \'check\'' - ], - 'constraints': [ - 'Argument "condition" to "UniqueConstraint" has incompatible type "str"; expected "Optional[Q]"' - ], - 'contenttypes_tests': [ - 'Item "Model" of "Union[GenericForeignKey, Model, None]" has no attribute' - ], - 'custom_lookups': [ - 'in base class "SQLFuncMixin"' - ], - 'custom_pk': [ - '"Employee" has no attribute "id"' - ], - 'custom_managers': [ - '"Type[Book]" has no attribute "objects"' - ], - 'csrf_tests': [ - 'Incompatible types in assignment (expression has type "property", ' + - 'base class "HttpRequest" defined the type as "QueryDict")' - ], - 'dates': [ - 'Too few arguments for "dates" of "QuerySet"', - ], - 'defer': [ - 'Too many arguments for "refresh_from_db" of "Model"' - ], - 'dispatch': [ - 'Argument 1 to "connect" of "Signal" has incompatible type "object"; expected "Callable[..., Any]"', - 'Item "str" of "Union[ValueError, str]" has no attribute "args"' - ], - 'deprecation': [ - '"Manager" has no attribute "old"', - '"Manager" has no attribute "new"' - ], - 'db_functions': [ - 'for **', - 'expected "float"', - 'Incompatible types in assignment (expression has type "Optional[FloatModel]", variable has type "FloatModel")', - re.compile(r'Item .* has no attribute'), - 'Module has no attribute', - 'Module \'datetime\' has no attribute', - '"DTModel" has no attribute', - '"Author" has no attribute', - '"FloatModel" has no attribute', - '"Article" has no attribute' - ], - 'decorators': [ - 'DummyRequest', - 'DummyUser', - '"Type[object]" has no attribute "method"' - ], - 'defer_regress': [ - '"Base" has no attribute "derived"' - ], - 'delete': [ - '"RChild" has no attribute', - '"Child" has no attribute "parent_ptr"' - ], - 'expressions': [ - 'Item "Experiment" of', - 'Item "Time" of', - '"Experiment" has no attribute', - '"Time" has no attribute', - ], - 'expressions_case': [ - '"CaseTestModel" has no attribute "selected"' - ], - 'expressions_window': [ - 'has incompatible type "str"' - ], - 'extra_regress': [ - '"User" has no attribute "alpha"' - ], - 'file_uploads': [ - '"Iterable[Any]" has no attribute', - '"IO[Any]" has no attribute' - ], - 'file_storage': [ - 'Incompatible types in assignment (expression has type "Callable[[], Any]"' - ], - 'files': [ - '"file_move_safe" does not return a value', - 'Incompatible types in assignment (expression has type "IOBase", variable has type "File")', - 'Module has no attribute "open"' - ], - 'fixtures': [ - 'Incompatible types in assignment (expression has type "int", target has type "Iterable[str]")' - ], - 'flatpages_tests': [ - '"Site" has no attribute "add"', - ], - 'forms_tests': [ - 'List item 0 has incompatible type "Jinja2"; expected "DjangoTemplates"', - 'Not enough arguments for format string', - 'Argument after ** must be a mapping, not "object"', - 'expression has type "None", base class "TestFormParent"', - 'variable has type "SongForm"', - '"full_clean" of "BaseForm" does not return a value', - 'No overload variant of "zip" matches argument types "Tuple[str, str, str]", "object"', - 'Incompatible types in assignment (expression has type "GetDateShowHiddenInitial", ' - + 'variable has type "GetDate")', - re.compile(r'Incompatible types in assignment \(expression has type "[a-zA-Z]+Field", ' - r'base class "BaseForm" defined the type as "Dict\[str, Any\]"\)'), - 'List or tuple expected as variable arguments', - 'Argument 1 to "__init__" of "MultiWidget" has incompatible type "List[object]"; ' - + 'expected "Sequence[Union[Widget, Type[Widget]]]"', - 'Argument 1 to "issubclass" has incompatible type "ModelFormMetaclass"; expected "type"', - 'Incompatible types in assignment (expression has type "List[str]", target has type "str")', - 'Incompatible types in assignment (expression has type "TestForm", variable has type "Person")', - 'Incompatible types in assignment (expression has type "Type[Textarea]", ' - + 'base class "Field" defined the type as "Widget")', - 'Incompatible types in assignment (expression has type "SimpleUploadedFile", variable has type "BinaryIO")', - 'has no attribute', - 'Name \'forms.Field\' is not defined' - ], - 'foreign_object': [ - '"Person" has no attribute', - '"SlugPage" has no attribute' - ], - 'from_db_value': [ - '"Cash" has no attribute' - ], - 'get_object_or_404': [ - 'Argument 1 to "get_object_or_404" has incompatible type "str"; ' - + 'expected "Union[Type[], QuerySet[, ]]"', - 'Argument 1 to "get_list_or_404" has incompatible type "List[Type[Article]]"; ' - + 'expected "Union[Type[], QuerySet[, ]]"', - 'CustomClass' - ], - 'generic_relations': [ - 'has no attribute "id"', - 'has no attribute "pk"' - ], - 'generic_relations_regress': [ - '"HasLinkThing" has no attribute', - '"Link" has no attribute' - ], - 'httpwrappers': [ - 'Argument 2 to "appendlist" of "QueryDict"', - 'Incompatible types in assignment (expression has type "int", target has type "Union[str, List[str]]")', - 'Argument 1 to "fromkeys" of "QueryDict" has incompatible type "int"' - ], - 'humanize_tests': [ - 'Argument 1 to "append" of "list" has incompatible type "None"; expected "str"' - ], - 'inline_formsets': [ - 'has no attribute "form"' - ], - 'i18n': [ - 'Incompatible types in assignment (expression has type "I18nForm", variable has type "SelectDateForm")', - 'has incompatible type "object"', - 'Value of type "Optional[List[_Record]]" is not indexable', - '"Command" has no attribute' - ], - 'logging_tests': [ - '"Handler" has no attribute "stream"' - ], - 'lookup': [ - 'Unexpected keyword argument "headline__startswith" for "in_bulk" of "QuerySet"', - '\'flat\' is not valid when values_list is called with more than one field.' - ], - 'm2o_recursive': [ - 'Incompatible type for "id" of "Category" (got "None", expected "int")' - ], - 'many_to_one': [ - 'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")', - 'Incompatible type for "parent" of "Child" (got "Child", expected "Union[Parent, Combinable]")' - ], - 'managers_regress': [ - '"Type[AbstractBase3]" has no attribute "objects"' - ], - 'middleware_exceptions': [ - 'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any]"; expected "str"' - ], - 'model_fields': [ - 'Incompatible types in assignment (expression has type "Type[Person]", variable has type', - 'Unexpected keyword argument "name" for "Person"', - 'Cannot assign multiple types to name "PersonTwoImages" without an explicit "Type[...]" annotation', - re.compile( - r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class "IntegerFieldTests"' - r' defined the type as "Type\[IntegerModel\]"\)'), - re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class' - r' "ImageFieldTestMixin" defined the type as "Type\[PersonWithHeightAndWidth\]"\)'), - 'Incompatible import of "Person"', - 'Incompatible types in assignment (expression has type "FloatModel", variable has type ' - '"Union[float, int, str, Combinable]")', - '"UUIDGrandchild" has no attribute "uuidchild_ptr_id"', - '"Person" has no attribute', - '"Foo" has no attribute', - ], - 'model_formsets': [ - 'has no attribute' - ], - 'model_indexes': [ - 'Argument "condition" to "Index" has incompatible type "str"; expected "Optional[Q]"' - ], - 'model_inheritance_regress': [ - '"Restaurant" has no attribute', - '"ArticleWithAuthor" has no attribute', - '"Person" has no attribute', - '"MessyBachelorParty" has no attribute', - '"Place" has no attribute', - '"ItalianRestaurant" has no attribute' - ], - 'model_meta': [ - '"Field[Any, Any]" has no attribute "many_to_many"' - ], - 'model_regress': [ - re.compile(r'Incompatible type for "[a-z]+" of "Worker" \(got "int", expected'), - '"PickledModel" has no attribute', - '"Department" has no attribute' - ], - 'modeladmin': [ - 'BandAdmin', - 'base class "ModelAdmin" defined the type a', - 'base class "InlineModelAdmin" defined the type a', - 'List item 0 has incompatible type', - 'Incompatible types in assignment (expression has type "None", base class "AdminBase" ' - + 'defined the type as "List[str]")' - ], - 'migrate_signals': [ - 'Value of type "None" is not indexable', - 'Argument 1 to "set" has incompatible type "None"; expected "Iterable[]"' - ], - 'migrations': [ - 'FakeMigration', - 'FakeLoader', - 'Dict entry 0 has incompatible type "Any": "Set[Tuple[Any, ...]]"; expected "Any": "str"', - 'Argument 1 to "RunPython" has incompatible type "str"; expected "Callable[..., Any]"', - 'Argument 1 to "append" of "list" has incompatible type "AddIndex"; expected "CreateModel"', - 'Argument 2 to "register_serializer" of "MigrationWriter" has incompatible type ' - + '"Type[TestModel1]"; expected "Type[BaseSerializer]"', - 'Argument 1 to "append" of "list" has incompatible type "AddConstraint"; expected "CreateModel"' - ], - 'multiple_database': [ - 'Too many arguments for "create" of "QuerySet"', - '"User" has no attribute "userprofile"', - 'Item "GenericForeignKey" of', - 'Item "Model" of' - ], - 'known_related_objects': [ - '"Pool" has no attribute' - ], - 'one_to_one': [ - '"Place" has no attribute', - '"Type[Place]" has no attribute', - '"ManualPrimaryKey" has no attribute' - ], - 'postgres_tests': [ - 'DummyArrayField', - 'DummyJSONField', - 'Cannot assign multiple types to name', - 'Incompatible types in assignment (expression has type "Type[Field[Any, Any]]', - 'Argument "encoder" to "JSONField" has incompatible type "DjangoJSONEncoder"; ' - + 'expected "Optional[Type[JSONEncoder]]"', - 'Incompatible type for "field" of "IntegerArrayModel" (got "None", ' - + 'expected "Union[Sequence[int], Combinable]")', - re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", ' - r'base class "(UnaccentTest|TrigramTest)" defined the type as "Type\[CharFieldModel\]"\)'), - '"Type[PostgreSQLModel]" has no attribute "objects"', - '("None" and "SearchQuery")', - # TODO: - 'django.contrib.postgres.forms', - 'django.contrib.postgres.aggregates', - ], - 'properties': [ - re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"') - ], - 'prefetch_related': [ - 'Incompatible types in assignment (expression has type "List[Room]", variable has type "QuerySet[Room, Room]")', - '"Person" has no attribute', - '"Author" has no attribute', - '"Book" has no attribute', - 'Item "Room" of', - '"AuthorWithAge" has no attribute', - 'has no attribute "read_by"' - ], - 'proxy_model_inheritance': [ - 'Incompatible import of "ProxyModel"' - ], - 'queries': [ - 'Incompatible types in assignment (expression has type "None", variable has type "str")', - 'Invalid index type "Optional[str]" for "Dict[str, int]"; expected type "str"', - 'No overload variant of "values_list" of "QuerySet" matches argument types "str", "bool", "bool"', - 'Unsupported operand types for & ("QuerySet[Author, Author]" and "QuerySet[Tag, Tag]")', - 'Unsupported operand types for | ("QuerySet[Author, Author]" and "QuerySet[Tag, Tag]")', - 'Incompatible types in assignment (expression has type "ObjectB", variable has type "ObjectA")', - 'Incompatible types in assignment (expression has type "ObjectC", variable has type "ObjectA")', - 'Incompatible type for "objectb" of "ObjectC" (got "ObjectA", expected' - ' "Union[ObjectB, Combinable, None, None]")', - '"Note" has no attribute', - '"Ranking" has no attribute', - '"BaseUser" has no attribute', - '"Item" has no attribute', - "'flat' and 'named' can't be used together.", - ], - 'queryset_pickle': [ - 'RelatedObjectDoesNotExist', - '"Type[Event]" has no attribute "happening"' - ], - 'requests': [ - 'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")' - ], - 'responses': [ - 'Argument 1 to "TextIOWrapper" has incompatible type "HttpResponse"; expected "IO[bytes]"' - ], - 'schema': [ - 'Incompatible type for "info" of "Note" (got "None", expected "Union[str, Combinable]")', - 'Incompatible type for "detail_info" of "NoteRename" (got "None", expected "Union[str, Combinable]")', - 'Incompatible type for "year" of "UniqueTest" (got "None", expected "Union[float, int, str, Combinable]")' - ], - 'settings_tests': [ - 'Argument 1 to "Settings" has incompatible type "Optional[str]"; expected "str"' - ], - 'signals': [ - 'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any, Optional[Any], Any]"; ' - + 'expected "Tuple[Any, Any, Any]"' - ], - 'sites_tests': [ - 'Item "RequestSite" of "Union[Site, RequestSite]" has no attribute "id"' - ], - 'syndication_tests': [ - 'List or tuple expected as variable arguments' - ], - 'sessions_tests': [ - 'base class "SessionTestsMixin" defined the type as "None")', - 'Incompatible types in assignment (expression has type "None", variable has type "int")', - '"AbstractBaseSession" has no attribute' - ], - 'select_related': [ - '"Species" has no attribute', - 'Item "object" of "Union[object, Any]" has no attribute "first"' - ], - 'select_related_onetoone': [ - 'Incompatible types in assignment (expression has type "Parent2", variable has type "Parent1")', - 'has no attribute' - ], - 'select_for_update': [ - 'has incompatible type "object"' - ], - 'servers': [ - re.compile('Argument [0-9] to "WSGIRequestHandler"'), - '"HTTPResponse" has no attribute', - '"type" has no attribute', - '"WSGIRequest" has no attribute "makefile"' - ], - 'template_tests': [ - re.compile(r'Argument 1 to "[a-zA-Z_]+" has incompatible type "int"; expected "str"'), - 'TestObject', - 'variable has type "Callable[[Any], Any]', - 'Value of type variable "AnyStr" of "urljoin" cannot be "Optional[str]"', - 'has no attribute "template_debug"', - "Module 'django.template.base' has no attribute 'TemplateSyntaxError'" - ], - 'template_backends': [ - 'Incompatible import of "Jinja2" (imported name has type "Type[Jinja2]", local name has type "object")', - 'TemplateStringsTests', - "Cannot find module named 'DoesNotExist'" - ], - 'test_client': [ - 'Incompatible types in assignment (expression has type "HttpResponse", ' - + 'variable has type "StreamingHttpResponse")' - ], - 'test_client_regress': [ - 'Incompatible types in assignment (expression has type "Dict[, ]", ' - + 'variable has type "SessionBase")', - 'Unsupported left operand type for + ("None")', - ], - 'test_runner': [ - 'Argument "result" to "run" of "TestCase" has incompatible type "RemoteTestResult"; ' - + 'expected "Optional[TestResult]"', - 'has no attribute "id"', - 'MockTestRunner' - ], - 'transactions': [ - 'Incompatible types in assignment (expression has type "Thread", variable has type "Callable[[], Any]")' - ], - 'urlpatterns': [ - '"object" not callable' - ], - 'user_commands': [ - 'Incompatible types in assignment (expression has type "Callable[[Any, KwArg(Any)], Any]", variable has type', - 'Cannot find module named \'user_commands.management.commands\'' - ], - 'utils_tests': [ - 'a_package_name_that_does_not_exist', - 'Too few arguments for "__init__"', - 'Argument 1 to "activate" has incompatible type "None"; expected "Union[tzinfo, str]"', - 'Module has no attribute "content"', - 'Argument 1 to "int_to_base36" has incompatible type "object"; expected "int"', - 'Argument 1 to "base36_to_int" has incompatible type "object"; expected "str"', - 'Incompatible types in assignment (expression has type "None", base class "object" defined the type as', - 'Invalid type "Class"', - 'has no attribute "cp"', - 'Argument "name" to "cached_property" has incompatible type "int"; expected "Optional[str]"', - 'foo', - '"ImmutableList[int]" has no attribute "sort"', - 'has no attribute "cache_clear"', - 'has no attribute "cache_info"', - 'Argument 1 to "Path" has incompatible type "Optional[str]"; expected "Union[str, _PathLike[str]]"', - 'defined the type as "None"' - ], - 'view_tests': [ - '"EmailMessage" has no attribute "alternatives"', - '"Handler" has no attribute "include_html"', - "Module 'django.views.debug' has no attribute 'Path'" - ] -} -# Test folders to typecheck -TESTS_DIRS = [ - 'absolute_url_overrides', - 'admin_autodiscover', - 'admin_changelist', - 'admin_checks', - 'admin_custom_urls', - 'admin_default_site', - 'admin_docs', - # TODO: 'admin_filters', - 'admin_inlines', - 'admin_ordering', - 'admin_registration', - 'admin_scripts', - 'admin_utils', - 'admin_views', - 'admin_widgets', - 'aggregation', - 'aggregation_regress', - 'annotations', - 'app_loading', - 'apps', - 'auth_tests', - 'backends', - 'base', - 'bash_completion', - 'basic', - 'builtin_server', - 'bulk_create', - # TODO: 'cache', - 'check_framework', - 'choices', - 'conditional_processing', - 'constraints', - 'contenttypes_tests', - 'context_processors', - 'csrf_tests', - 'custom_columns', - 'custom_lookups', - 'custom_managers', - 'custom_methods', - 'custom_migration_operations', - 'custom_pk', - 'datatypes', - 'dates', - 'datetimes', - 'db_functions', - 'db_typecasts', - 'db_utils', - 'dbshell', - 'decorators', - 'defer', - 'defer_regress', - 'delete', - 'delete_regress', - 'deprecation', - 'dispatch', - 'distinct_on_fields', - 'empty', - 'expressions', - 'expressions_case', - 'expressions_window', - 'extra_regress', - 'field_deconstruction', - 'field_defaults', - 'field_subclassing', - 'file_storage', - 'file_uploads', - 'files', - 'filtered_relation', - 'fixtures', - 'fixtures_model_package', - 'fixtures_regress', - 'flatpages_tests', - 'force_insert_update', - 'foreign_object', - 'forms_tests', - 'from_db_value', - 'generic_inline_admin', - 'generic_relations', - 'generic_relations_regress', - # TODO: 'generic_views', - 'get_earliest_or_latest', - 'get_object_or_404', - 'get_or_create', - # TODO: 'gis_tests', - 'handlers', - 'httpwrappers', - 'humanize_tests', - 'i18n', - 'import_error_package', - 'indexes', - 'inline_formsets', - 'inspectdb', - 'introspection', - # not practical - # 'invalid_models_tests', - 'known_related_objects', - 'logging_tests', - 'lookup', - 'm2m_and_m2o', - 'm2m_intermediary', - 'm2m_multiple', - 'm2m_recursive', - 'm2m_regress', - 'm2m_signals', - 'm2m_through', - 'm2m_through_regress', - 'm2o_recursive', - # TODO: 'mail', - 'managers_regress', - 'many_to_many', - 'many_to_one', - 'many_to_one_null', - 'max_lengths', - # TODO: 'messages_tests', - 'middleware', - 'middleware_exceptions', - 'migrate_signals', - 'migration_test_data_persistence', - 'migrations', - 'migrations2', - 'model_fields', - # TODO: 'model_forms', - 'model_formsets', - 'model_formsets_regress', - 'model_indexes', - # TODO: 'model_inheritance', - 'model_inheritance_regress', - 'model_meta', - 'model_options', - 'model_package', - 'model_regress', - 'modeladmin', - 'multiple_database', - 'mutually_referential', - 'nested_foreign_keys', - 'no_models', - 'null_fk', - 'null_fk_ordering', - 'null_queries', - 'one_to_one', - 'or_lookups', - 'order_with_respect_to', - 'ordering', - 'pagination', - 'postgres_tests', - 'prefetch_related', - 'project_template', - 'properties', - 'proxy_model_inheritance', - # TODO: 'proxy_models', - 'queries', - 'queryset_pickle', - 'raw_query', - 'redirects_tests', - 'requests', - 'reserved_names', - 'resolve_url', - 'responses', - 'reverse_lookup', - 'save_delete_hooks', - 'schema', - 'select_for_update', - 'select_related', - 'select_related_onetoone', - 'select_related_regress', - # TODO: 'serializers', - 'servers', - 'sessions_tests', - 'settings_tests', - 'shell', - 'shortcuts', - 'signals', - 'signed_cookies_tests', - 'signing', - # TODO: 'sitemaps_tests', - 'sites_framework', - 'sites_tests', - # TODO: 'staticfiles_tests', - 'str', - 'string_lookup', - 'swappable_models', - 'syndication_tests', - 'template_backends', - 'template_loader', - 'template_tests', - 'test_client', - 'test_client_regress', - 'test_exceptions', - 'test_runner', - 'test_runner_apps', - 'test_utils', - 'timezones', - 'transaction_hooks', - 'transactions', - 'unmanaged_models', - 'update', - 'update_only_fields', - 'urlpatterns', - # not annotatable without annotation in test - # 'urlpatterns_reverse', - 'user_commands', - 'utils_tests', - 'validation', - 'validators', - 'version', - 'view_tests', - 'wsgi', -] - - -@contextmanager -def cd(path): - """Context manager to temporarily change working directories""" - if not path: - return - prev_cwd = Path.cwd().as_posix() - if isinstance(path, Path): - path = path.as_posix() - os.chdir(str(path)) - try: - yield - finally: - os.chdir(prev_cwd) +def is_ignored(line: str, test_folder_name: str) -> bool: + if 'runtests' in line: + return True + if test_folder_name in IGNORED_MODULES: + return True -def is_ignored(line: str, test_folder_name: str) -> bool: - for pattern in itertools.chain(IGNORED_ERRORS['__common__'], + for pattern in itertools.chain(IGNORED_ERRORS['__new_common__'], IGNORED_ERRORS.get(test_folder_name, [])): if isinstance(pattern, Pattern): if pattern.search(line): @@ -828,24 +42,6 @@ def replace_with_clickable_location(error: str, abs_test_folder: Path) -> str: return error.replace(raw_path, clickable_location) -def check_with_mypy(abs_path: Path, config_file_path: Path) -> int: - error_happened = False - with cd(abs_path): - sources, options = process_options(['--cache-dir', str(config_file_path.parent / '.mypy_cache'), - '--config-file', str(config_file_path), - str(abs_path)]) - res = build.build(sources, options) - for error_line in res.errors: - if not is_ignored(error_line, abs_path.name): - error_happened = True - print(replace_with_clickable_location(error_line, abs_test_folder=abs_path)) - return int(error_happened) - - -def get_absolute_path_for_test(test_dirname: str): - return (PROJECT_DIRECTORY / tests_root / test_dirname).absolute() - - if __name__ == '__main__': mypy_config_file = (PROJECT_DIRECTORY / 'scripts' / 'mypy.ini').absolute() repo_directory = PROJECT_DIRECTORY / 'django-sources' @@ -853,28 +49,31 @@ def get_absolute_path_for_test(test_dirname: str): tests_root = repo_directory / 'tests' global_rc = 0 - # clone Django repository, if it does not exist - if not repo_directory.exists(): - repo = Repo.clone_from('https://github.com/django/django.git', repo_directory) - else: - repo = Repo(repo_directory) - repo.remotes['origin'].pull(DJANGO_BRANCH) - - repo.git.checkout(DJANGO_COMMIT_SHA) - - if len(sys.argv) > 1: - tests_to_run = sys.argv[1:] - else: - tests_to_run = TESTS_DIRS + # copy django settings to the tests_root directory + shutil.copy(PROJECT_DIRECTORY / 'scripts' / 'django_tests_settings.py', tests_root) try: - for dirname in tests_to_run: - abs_path = get_absolute_path_for_test(dirname) - print(f'Checking {abs_path}') - - rc = check_with_mypy(abs_path, mypy_config_file) - if rc != 0: - global_rc = 1 + mypy_options = ['--cache-dir', str(mypy_config_file.parent / '.mypy_cache'), + '--config-file', str(mypy_config_file)] + mypy_options += [str(tests_root)] + + import distutils.spawn + + mypy_executable = distutils.spawn.find_executable('mypy') + completed = subprocess.run([mypy_executable, *mypy_options], env={'PYTHONPATH': str(tests_root)}, + stdout=subprocess.PIPE, cwd=str(tests_root)) + sorted_lines = sorted(completed.stdout.decode().splitlines()) + for line in sorted_lines: + try: + module_name = line.split('/')[0] + except IndexError: + module_name = 'unknown' + + if not is_ignored(line, module_name): + if line.startswith(module_name): + print(replace_with_clickable_location(line, abs_test_folder=tests_root)) + else: + print(line) sys.exit(global_rc) diff --git a/setup.py b/setup.py index a834f6139..6365c58e4 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,11 @@ import os -import sys from distutils.core import setup +from typing import List from setuptools import find_packages -def find_stub_files(name): +def find_stub_files(name: str) -> List[str]: result = [] for root, dirs, files in os.walk(name): for file in files: @@ -21,27 +21,27 @@ def find_stub_files(name): readme = f.read() dependencies = [ - 'mypy>=0.710,<0.720', - 'typing-extensions' + 'mypy>=0.720,<0.730', + 'typing-extensions', + 'django', + # depends on psycopg2 because of Postgres' ArrayField support + 'psycopg2' ] -if sys.version_info[:2] < (3, 7): - # dataclasses port for 3.6 - dependencies += ['dataclasses'] setup( name="django-stubs", - version="0.13.0", - description='Django mypy stubs', + version="1.0.0", + description='Mypy stubs for Django', long_description=readme, long_description_content_type='text/markdown', license='MIT', - url="https://github.com/mkurnikov/django-stubs", + url="https://github.com/typeddjango/django-stubs", author="Maksim Kurnikov", author_email="maxim.kurnikov@gmail.com", py_modules=[], python_requires='>=3.6', install_requires=dependencies, - packages=['django-stubs', *find_packages()], + packages=['django-stubs', *find_packages(exclude=['scripts'])], package_data={'django-stubs': find_stub_files('django-stubs')}, classifiers=[ 'Development Status :: 3 - Alpha', diff --git a/test-data/typecheck/config.test b/test-data/typecheck/config.test deleted file mode 100644 index 405276d68..000000000 --- a/test-data/typecheck/config.test +++ /dev/null @@ -1,37 +0,0 @@ -[CASE missing_settings_ignored_flag] -[env MYPY_DJANGO_CONFIG=${MYPY_CWD}/mypy_django.ini] -[disable_cache] -from django.conf import settings -reveal_type(settings.NO_SUCH_SETTING) # N: Revealed type is 'Any' - -[file mypy_django.ini] -[[mypy_django_plugin] -ignore_missing_settings = True -[/CASE] - -[CASE django_settings_via_config_file] -[env MYPY_DJANGO_CONFIG=${MYPY_CWD}/mypy_django.ini] -[disable_cache] -from django.conf import settings -reveal_type(settings.MY_SETTING) # N: Revealed type is 'builtins.int' - -[file mypy_django.ini] -[[mypy_django_plugin] -django_settings = mysettings - -[file mysettings.py] -MY_SETTING: int = 1 -[/CASE] - -[CASE mypy_django_ini_in_current_directory_is_a_default] -[disable_cache] -from django.conf import settings -reveal_type(settings.MY_SETTING) # N: Revealed type is 'builtins.int' - -[file mypy_django.ini] -[[mypy_django_plugin] -django_settings = mysettings - -[file mysettings.py] -MY_SETTING: int = 1 -[/CASE] diff --git a/test-data/typecheck/fields.test b/test-data/typecheck/fields.test deleted file mode 100644 index 9af5571b3..000000000 --- a/test-data/typecheck/fields.test +++ /dev/null @@ -1,144 +0,0 @@ -[CASE array_field_descriptor_access] -from django.db import models -from django.contrib.postgres.fields import ArrayField - -class User(models.Model): - array = ArrayField(base_field=models.Field()) - -user = User() -reveal_type(user.array) # N: Revealed type is 'builtins.list*[Any]' -[/CASE] - -[CASE array_field_base_field_parsed_into_generic_typevar] -from django.db import models -from django.contrib.postgres.fields import ArrayField - -class User(models.Model): - members = ArrayField(base_field=models.IntegerField()) - members_as_text = ArrayField(base_field=models.CharField(max_length=255)) - -user = User() -reveal_type(user.members) # N: Revealed type is 'builtins.list*[builtins.int]' -reveal_type(user.members_as_text) # N: Revealed type is 'builtins.list*[builtins.str]' -[/CASE] - -[CASE test_model_fields_classes_present_as_primitives] -from django.db import models - -class User(models.Model): - id = models.AutoField(primary_key=True) - small_int = models.SmallIntegerField() - name = models.CharField(max_length=255) - slug = models.SlugField(max_length=255) - text = models.TextField() - -user = User() -reveal_type(user.id) # N: Revealed type is 'builtins.int' -reveal_type(user.small_int) # N: Revealed type is 'builtins.int*' -reveal_type(user.name) # N: Revealed type is 'builtins.str*' -reveal_type(user.slug) # N: Revealed type is 'builtins.str*' -reveal_type(user.text) # N: Revealed type is 'builtins.str*' -[/CASE] - -[CASE test_model_field_classes_from_existing_locations] -from django.db import models -from django.contrib.postgres import fields as pg_fields -from decimal import Decimal - -class Booking(models.Model): - id = models.AutoField(primary_key=True) - time_range = pg_fields.DateTimeRangeField(null=False) - some_decimal = models.DecimalField(max_digits=10, decimal_places=5) - -booking = Booking() -reveal_type(booking.id) # N: Revealed type is 'builtins.int' -reveal_type(booking.time_range) # N: Revealed type is 'Any' -reveal_type(booking.some_decimal) # N: Revealed type is 'decimal.Decimal*' -[/CASE] - -[CASE test_add_id_field_if_no_primary_key_defined] -from django.db import models - -class User(models.Model): - pass - -reveal_type(User().id) # N: Revealed type is 'builtins.int' -[/CASE] - -[CASE test_do_not_add_id_if_field_with_primary_key_True_defined] -from django.db import models - -class User(models.Model): - my_pk = models.IntegerField(primary_key=True) - -reveal_type(User().my_pk) # N: Revealed type is 'builtins.int*' -reveal_type(User().id) -[out] -main:7: note: Revealed type is 'Any' -main:7: error: "User" has no attribute "id" -[/CASE] - -[CASE test_meta_nested_class_allows_subclassing_in_multiple_inheritance] -from typing import Any -from django.db import models - -class Mixin1(models.Model): - class Meta: - abstract = True - -class Mixin2(models.Model): - class Meta: - abstract = True - -class User(Mixin1, Mixin2): - pass -[/CASE] - -[CASE test_inheritance_from_abstract_model_does_not_fail_if_field_with_id_exists] -from django.db import models -class Abstract(models.Model): - class Meta: - abstract = True -class User(Abstract): - id = models.AutoField(primary_key=True) -[/CASE] - -[CASE test_primary_key_on_optional_queryset_method] -from django.db import models -class User(models.Model): - pass -reveal_type(User.objects.first().id) -[out] -main:4: note: Revealed type is 'Any' -main:4: error: Item "None" of "Optional[User]" has no attribute "id" -[/CASE] - -[CASE standard_it_from_parent_model_could_be_overridden_with_non_integer_field_in_child_model] -from django.db import models -import uuid -class ParentModel(models.Model): - pass -class MyModel(ParentModel): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) -reveal_type(MyModel().id) # N: Revealed type is 'uuid.UUID' -[/CASE] - -[CASE blank_and_null_char_field_allows_none] -from django.db import models -class MyModel(models.Model): - nulltext=models.CharField(max_length=1, blank=True, null=True) -MyModel(nulltext="") -MyModel(nulltext=None) -MyModel().nulltext=None -reveal_type(MyModel().nulltext) # N: Revealed type is 'Union[builtins.str, None]' -[/CASE] - -[CASE blank_and_not_null_charfield_does_not_allow_none] -from django.db import models -class MyModel(models.Model): - notnulltext=models.CharField(max_length=1, blank=True, null=False) -MyModel(notnulltext=None) # Should allow None in constructor -MyModel(notnulltext="") -MyModel().notnulltext = None # E: Incompatible types in assignment (expression has type "None", variable has type "Union[str, int, Combinable]") -reveal_type(MyModel().notnulltext) # N: Revealed type is 'builtins.str*' -[/CASE] diff --git a/test-data/typecheck/fields/test_base.yml b/test-data/typecheck/fields/test_base.yml new file mode 100644 index 000000000..16d98a841 --- /dev/null +++ b/test-data/typecheck/fields/test_base.yml @@ -0,0 +1,134 @@ +- case: test_model_fields_classes_present_as_primitives + main: | + from myapp.models import User + user = User(small_int=1, name='user', slug='user', text='user') + reveal_type(user.id) # N: Revealed type is 'builtins.int*' + reveal_type(user.small_int) # N: Revealed type is 'builtins.int*' + reveal_type(user.name) # N: Revealed type is 'builtins.str*' + reveal_type(user.slug) # N: Revealed type is 'builtins.str*' + reveal_type(user.text) # N: Revealed type is 'builtins.str*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class User(models.Model): + id = models.AutoField(primary_key=True) + small_int = models.SmallIntegerField() + name = models.CharField(max_length=255) + slug = models.SlugField(max_length=255) + text = models.TextField() + +- case: test_model_field_classes_from_existing_locations + main: | + from myapp.models import Booking + booking = Booking() + reveal_type(booking.id) # N: Revealed type is 'builtins.int*' + reveal_type(booking.time_range) # N: Revealed type is 'Any' + reveal_type(booking.some_decimal) # N: Revealed type is 'decimal.Decimal*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from django.contrib.postgres import fields as pg_fields + from decimal import Decimal + + class Booking(models.Model): + id = models.AutoField(primary_key=True) + time_range = pg_fields.DateTimeRangeField(null=False) + some_decimal = models.DecimalField(max_digits=10, decimal_places=5) + +- case: test_add_id_field_if_no_primary_key_defined + disable_cache: true + main: | + from myapp.models import User + reveal_type(User().id) # N: Revealed type is 'builtins.int*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class User(models.Model): + pass + +- case: test_do_not_add_id_if_field_with_primary_key_True_defined + disable_cache: true + main: | + from myapp.models import User + reveal_type(User().my_pk) # N: Revealed type is 'builtins.int*' + reveal_type(User().id) + out: | + main:3: note: Revealed type is 'Any' + main:3: error: "User" has no attribute "id" + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class User(models.Model): + my_pk = models.IntegerField(primary_key=True) + +- case: blank_and_null_char_field_allows_none + main: | + from myapp.models import MyModel + MyModel(nulltext="") + MyModel(nulltext=None) + MyModel().nulltext=None + reveal_type(MyModel().nulltext) # N: Revealed type is 'Union[builtins.str, None]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + nulltext=models.CharField(max_length=1, blank=True, null=True) + +- case: blank_and_not_null_charfield_does_not_allow_none + main: | + from myapp.models import MyModel + MyModel(notnulltext=None) # Should allow None in constructor + MyModel(notnulltext="") + MyModel().notnulltext = None # E: Incompatible types in assignment (expression has type "None", variable has type "Union[str, int, Combinable]") + reveal_type(MyModel().notnulltext) # N: Revealed type is 'builtins.str*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + notnulltext=models.CharField(max_length=1, blank=True, null=False) + +- case: if_field_called_on_class_return_field_itself + main: | + from myapp.models import MyUser + reveal_type(MyUser.name) # N: Revealed type is 'django.db.models.fields.CharField[Union[builtins.str, builtins.int, django.db.models.expressions.Combinable], builtins.str]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + name = models.CharField(max_length=100) + +- case: fields_on_non_model_classes_resolve_to_field_type + main: | + from django.db import models + class MyClass: + myfield: models.IntegerField[int, int] + reveal_type(MyClass.myfield) # N: Revealed type is 'django.db.models.fields.IntegerField[builtins.int, builtins.int]' + reveal_type(MyClass().myfield) # N: Revealed type is 'django.db.models.fields.IntegerField[builtins.int, builtins.int]' diff --git a/test-data/typecheck/fields/test_generic_foreign_key.yml b/test-data/typecheck/fields/test_generic_foreign_key.yml new file mode 100644 index 000000000..d0979a841 --- /dev/null +++ b/test-data/typecheck/fields/test_generic_foreign_key.yml @@ -0,0 +1,21 @@ +- case: generic_foreign_key_could_point_to_any_model_and_is_always_optional + main: | + from myapp.models import Tag, User + myuser = User() + Tag(content_object=None) + Tag(content_object=myuser) + Tag.objects.create(content_object=None) + Tag.objects.create(content_object=myuser) + reveal_type(Tag().content_object) # N: Revealed type is 'Union[Any, None]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from django.contrib.contenttypes import fields + class User(models.Model): + pass + class Tag(models.Model): + content_object = fields.GenericForeignKey() \ No newline at end of file diff --git a/test-data/typecheck/fields/test_nullable.yml b/test-data/typecheck/fields/test_nullable.yml new file mode 100644 index 000000000..e1430016a --- /dev/null +++ b/test-data/typecheck/fields/test_nullable.yml @@ -0,0 +1,70 @@ +- case: nullable_field_with_strict_optional_true + main: | + from myapp.models import MyModel + reveal_type(MyModel().text) # N: Revealed type is 'builtins.str*' + reveal_type(MyModel().text_nullable) # N: Revealed type is 'Union[builtins.str, None]' + MyModel().text = None # E: Incompatible types in assignment (expression has type "None", variable has type "Union[str, int, Combinable]") + MyModel().text_nullable = None + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + text_nullable = models.CharField(max_length=100, null=True) + text = models.CharField(max_length=100) + +- case: nullable_array_field + main: | + from myapp.models import MyModel + reveal_type(MyModel().lst) # N: Revealed type is 'Union[builtins.list[builtins.str], None]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from django.contrib.postgres.fields import ArrayField + + class MyModel(models.Model): + lst = ArrayField(base_field=models.CharField(max_length=100), null=True) + +- case: nullable_foreign_key + main: | + from myapp.models import Publisher, Book + reveal_type(Book().publisher) # N: Revealed type is 'Union[myapp.models.Publisher, None]' + Book().publisher = 11 # E: Incompatible types in assignment (expression has type "int", variable has type "Union[Publisher, Combinable, None]") + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class Publisher(models.Model): + pass + class Book(models.Model): + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, null=True) + +- case: nullable_self_foreign_key + main: | + from myapp.models import Inventory + parent = Inventory() + core = Inventory(parent_id=parent.id) + reveal_type(core.parent_id) # N: Revealed type is 'Union[builtins.int, None]' + reveal_type(core.parent) # N: Revealed type is 'Union[myapp.models.Inventory, None]' + Inventory(parent=None) + Inventory(parent_id=None) + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Inventory(models.Model): + parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True) diff --git a/test-data/typecheck/fields/test_postgres_fields.yml b/test-data/typecheck/fields/test_postgres_fields.yml new file mode 100644 index 000000000..2d2c0022f --- /dev/null +++ b/test-data/typecheck/fields/test_postgres_fields.yml @@ -0,0 +1,35 @@ +- case: array_field_descriptor_access + main: | + from myapp.models import User + user = User(array=[]) + reveal_type(user.array) # N: Revealed type is 'builtins.list*[Any]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from django.contrib.postgres.fields import ArrayField + + class User(models.Model): + array = ArrayField(base_field=models.Field()) + +- case: array_field_base_field_parsed_into_generic_typevar + main: | + from myapp.models import User + user = User() + reveal_type(user.members) # N: Revealed type is 'builtins.list*[builtins.int]' + reveal_type(user.members_as_text) # N: Revealed type is 'builtins.list*[builtins.str]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from django.contrib.postgres.fields import ArrayField + + class User(models.Model): + members = ArrayField(base_field=models.IntegerField()) + members_as_text = ArrayField(base_field=models.CharField(max_length=255)) diff --git a/test-data/typecheck/fields/test_related.yml b/test-data/typecheck/fields/test_related.yml new file mode 100644 index 000000000..3951db34d --- /dev/null +++ b/test-data/typecheck/fields/test_related.yml @@ -0,0 +1,522 @@ +- case: test_foreign_key_field_with_related_name + main: | + from myapp.models import Book, Publisher + book = Book() + reveal_type(book.publisher) # N: Revealed type is 'myapp.models.Publisher*' + publisher = Publisher() + reveal_type(publisher.books) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Book]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Book(models.Model): + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, + related_name='books') + +- case: foreign_key_field_creates_attribute_with_underscore_id + main: | + from myapp.models import Book + book = Book() + reveal_type(book.publisher_id) # N: Revealed type is 'builtins.int*' + reveal_type(book.owner_id) # N: Revealed type is 'builtins.int*' + installed_apps: + - django.contrib.auth + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Book(models.Model): + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) + owner = models.ForeignKey(db_column='model_id', to='auth.User', on_delete=models.CASCADE) + +- case: foreign_key_field_different_order_of_params + main: | + from myapp.models import Book, Publisher + book = Book() + reveal_type(book.publisher) # N: Revealed type is 'myapp.models.Publisher*' + reveal_type(book.publisher2) # N: Revealed type is 'myapp.models.Publisher*' + + publisher = Publisher() + reveal_type(publisher.books) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Book]' + reveal_type(publisher.books2) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Book]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Book(models.Model): + publisher = models.ForeignKey(on_delete=models.CASCADE, to=Publisher, + related_name='books') + publisher2 = models.ForeignKey(to=Publisher, related_name='books2', on_delete=models.CASCADE) + +- case: to_parameter_as_string_with_application_name__model_imported + main: | + from myapp2.models import Book + book = Book() + reveal_type(book.publisher) # N: Revealed type is 'myapp.models.Publisher*' + installed_apps: + - myapp + - myapp2 + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + - path: myapp2/__init__.py + - path: myapp2/models.py + content: | + from django.db import models + class Book(models.Model): + publisher = models.ForeignKey(to='myapp.Publisher', on_delete=models.CASCADE) + +- case: one_to_one_field_no_related_name + main: | + from myapp.models import User, Profile + reveal_type(User().profile) # N: Revealed type is 'myapp.models.Profile' + reveal_type(Profile().user) # N: Revealed type is 'myapp.models.User*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class User(models.Model): + pass + class Profile(models.Model): + user = models.OneToOneField(to=User, on_delete=models) + +- case: test_circular_dependency_in_imports_with_foreign_key + main: | + from myapp import models + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class App(models.Model): + def method(self) -> None: + reveal_type(self.views) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.View]' + reveal_type(self.members) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Member]' + reveal_type(self.sheets) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Sheet]' + reveal_type(self.profile) # N: Revealed type is 'myapp.models.Profile' + class View(models.Model): + app = models.ForeignKey(to=App, related_name='views', on_delete=models.CASCADE) + class Member(models.Model): + app = models.ForeignKey(related_name='members', on_delete=models.CASCADE, to=App) + class Sheet(models.Model): + app = models.ForeignKey(App, related_name='sheets', on_delete=models.CASCADE) + class Profile(models.Model): + app = models.OneToOneField(App, related_name='profile', on_delete=models.CASCADE) + +- case: test_circular_dependency_in_imports_with_string_based + main: | + from myapp.models import View + reveal_type(View().app.views) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.View]' + reveal_type(View().app.unknown) + out: | + main:3: note: Revealed type is 'Any' + main:3: error: "App" has no attribute "unknown" + installed_apps: + - myapp + - myapp2 + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from myapp2.models import App + class View(models.Model): + app = models.ForeignKey(to=App, related_name='views', on_delete=models.CASCADE) + - path: myapp2/__init__.py + - path: myapp2/models.py + content: | + from django.db import models + class App(models.Model): + def method(self) -> None: + reveal_type(self.views) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.View]' + +- case: models_related_managers_work_with_direct_model_inheritance_and_with_inheritance_from_other_model + main: | + from myapp.models import App + reveal_type(App().views) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.View]' + reveal_type(App().views2) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.View2]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class App(models.Model): + pass + class View(models.Model): + app = models.ForeignKey(to=App, on_delete=models.CASCADE, related_name='views') + class View2(View): + app2 = models.ForeignKey(to=App, on_delete=models.CASCADE, related_name='views2') + +- case: models_imported_inside_init_file_foreign_key + main: | + from myapp2.models import View + reveal_type(View().app.views) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp2.models.View]' + installed_apps: + - myapp + - myapp2 + files: + - path: myapp/__init__.py + - path: myapp/models/__init__.py + content: | + from .app import App + - path: myapp/models/app.py + content: | + from django.db import models + class App(models.Model): + pass + - path: myapp2/__init__.py + - path: myapp2/models.py + content: | + from django.db import models + from myapp.models import App + class View(models.Model): + app = models.ForeignKey(to='myapp.App', related_name='views', on_delete=models.CASCADE) + +- case: models_imported_inside_init_file_one_to_one_field + main: | + from myapp2.models import Profile + reveal_type(Profile().user) # N: Revealed type is 'myapp.models.user.User*' + reveal_type(Profile().user.profile) # N: Revealed type is 'myapp2.models.Profile' + installed_apps: + - myapp + - myapp2 + files: + - path: myapp/__init__.py + - path: myapp/models/__init__.py + content: | + from .user import User + - path: myapp/models/user.py + content: | + from django.db import models + class User(models.Model): + pass + - path: myapp2/__init__.py + - path: myapp2/models.py + content: | + from django.db import models + from myapp.models import User + class Profile(models.Model): + user = models.OneToOneField(to='myapp.User', related_name='profile', on_delete=models.CASCADE) + +- case: models_triple_circular_reference + main: | + from myapp.models import App + reveal_type(App().owner) # N: Revealed type is 'myapp.models.user.User*' + reveal_type(App().owner.profile) # N: Revealed type is 'myapp.models.profile.Profile' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models/__init__.py + content: | + from .user import User + from .profile import Profile + from .app import App + - path: myapp/models/user.py + content: | + from django.db import models + class User(models.Model): + pass + - path: myapp/models/profile.py + content: | + from django.db import models + class Profile(models.Model): + user = models.OneToOneField(to='myapp.User', related_name='profile', on_delete=models.CASCADE) + - path: myapp/models/app.py + content: | + from django.db import models + class App(models.Model): + owner = models.ForeignKey(to='myapp.User', on_delete=models.CASCADE, related_name='apps') + +- case: many_to_many_field_converts_to_queryset_of_model_type + main: | + from myapp.models import App, Member + reveal_type(Member().apps) # N: Revealed type is 'django.db.models.manager.RelatedManager*[myapp.models.App]' + reveal_type(App().members) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Member]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class App(models.Model): + pass + class Member(models.Model): + apps = models.ManyToManyField(to=App, related_name='members') + +- case: many_to_many_works_with_string_if_imported + main: | + from myapp.models import Member + reveal_type(Member().apps) # N: Revealed type is 'django.db.models.manager.RelatedManager*[myapp2.models.App]' + installed_apps: + - myapp + - myapp2 + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Member(models.Model): + apps = models.ManyToManyField(to='myapp2.App', related_name='members') + - path: myapp2/__init__.py + - path: myapp2/models.py + content: | + from django.db import models + class App(models.Model): + pass + +- case: foreign_key_with_self + main: | + from myapp.models import User + reveal_type(User().parent) # N: Revealed type is 'myapp.models.User*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class User(models.Model): + parent = models.ForeignKey('self', on_delete=models.CASCADE) + +- case: many_to_many_with_self + main: | + from myapp.models import User + reveal_type(User().friends) # N: Revealed type is 'django.db.models.manager.RelatedManager*[myapp.models.User]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class User(models.Model): + friends = models.ManyToManyField('self') + +- case: recursively_checking_for_base_model_in_to_parameter + main: | + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class BaseModel(models.Model): + pass + class ParkingSpot(BaseModel): + pass + class Booking(BaseModel): + parking_spot = models.ForeignKey(to=ParkingSpot, null=True, on_delete=models.SET_NULL) + +- case: if_no_related_name_is_passed_create_default_related_managers + main: | + from myapp.models import Publisher + reveal_type(Publisher().book_set) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Book]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Book(models.Model): + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) + +- case: underscore_id_attribute_has_set_type_of_primary_key_if_explicit + main: | + import datetime + from myapp.models import Book, Book2 + + reveal_type(Book().publisher_id) # N: Revealed type is 'builtins.str*' + Book(publisher_id=1) + Book(publisher_id='hello') + Book(publisher_id=datetime.datetime.now()) # E: Incompatible type for "publisher_id" of "Book" (got "datetime", expected "Union[str, int, Combinable, None]") + Book.objects.create(publisher_id=1) + Book.objects.create(publisher_id='hello') + + reveal_type(Book2().publisher_id) # N: Revealed type is 'builtins.int*' + Book2(publisher_id=1) + Book2(publisher_id=[]) # E: Incompatible type for "publisher_id" of "Book2" (got "List[Any]", expected "Union[float, int, str, Combinable, None]") + Book2.objects.create(publisher_id=1) + Book2.objects.create(publisher_id=[]) # E: Incompatible type for "publisher_id" of "Book2" (got "List[Any]", expected "Union[float, int, str, Combinable]") + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + import datetime + class Publisher(models.Model): + mypk = models.CharField(max_length=100, primary_key=True) + class Book(models.Model): + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) + + class Publisher2(models.Model): + mypk = models.IntegerField(primary_key=True) + class Book2(models.Model): + publisher = models.ForeignKey(to=Publisher2, on_delete=models.CASCADE) + +- case: if_model_is_defined_as_name_of_the_class_look_for_it_in_the_same_file + main: | + from myapp.models import Book + reveal_type(Book().publisher) # N: Revealed type is 'myapp.models.Publisher*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Book(models.Model): + publisher = models.ForeignKey(to='Publisher', on_delete=models.CASCADE) + +- case: test_foreign_key_field_without_backwards_relation + main: | + from myapp.models import Book, Publisher + book = Book() + reveal_type(book.publisher) # N: Revealed type is 'myapp.models.Publisher*' + + publisher = Publisher() + reveal_type(publisher.books) + reveal_type(publisher.books2) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Book]' + out: | + main:6: error: "Publisher" has no attribute "books"; maybe "books2"? + main:6: note: Revealed type is 'Any' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Book(models.Model): + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, + related_name='+') + publisher2 = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, + related_name='books2') + +- case: to_parameter_could_be_resolved_if_passed_from_settings + main: | + from myapp.models import Book + book = Book() + reveal_type(book.publisher) # N: Revealed type is 'myapp.models.Publisher*' + custom_settings: | + INSTALLED_APPS = ('django.contrib.contenttypes', 'myapp') + BOOK_RELATED_MODEL = 'myapp.Publisher' + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.conf import settings + from django.db import models + + class Publisher(models.Model): + pass + class Book(models.Model): + publisher = models.ForeignKey(to=settings.BOOK_RELATED_MODEL, on_delete=models.CASCADE, + related_name='books') + +- case: foreign_key_with_custom_app_name + main: | + from myapp.models import MyMain + reveal_type(MyMain().user) # N: Revealed type is 'myapp2.models.MyUser*' + installed_apps: + - myapp + - myapp2.apps.MyApp2Config + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyMain(models.Model): + user = models.ForeignKey('myapp2__user.MyUser', on_delete=models.CASCADE) + - path: myapp2/__init__.py + - path: myapp2/models.py + content: | + from django.db import models + class MyUser(models.Model): + pass + - path: myapp2/apps.py + content: | + from django.apps.config import AppConfig + class MyApp2Config(AppConfig): + name = 'myapp2' + label = 'myapp2__user' + + +- case: related_field_to_extracted_from_function + main: | + from myapp.models import Profile + reveal_type(Profile().user) # N: Revealed type is 'myapp.models.User*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class User(models.Model): + pass + def get_user_model_name(): + return 'myapp.User' + class Profile(models.Model): + user = models.ForeignKey(to=get_user_model_name(), on_delete=models.CASCADE) + + +- case: related_manager_name_defined_by_pattern + main: | + from myapp.models import Publisher + reveal_type(Publisher().books) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Book]' + reveal_type(Publisher().articles) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Article]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Entry(models.Model): + class Meta: + abstract = True + publisher = models.ForeignKey(to=Publisher, related_name='%(class)ss', on_delete=models.CASCADE) + class Book(Entry): + pass + class Article(Entry): + pass \ No newline at end of file diff --git a/test-data/typecheck/forms.test b/test-data/typecheck/forms.test deleted file mode 100644 index fea840554..000000000 --- a/test-data/typecheck/forms.test +++ /dev/null @@ -1,41 +0,0 @@ -[CASE no_incompatible_meta_nested_class_false_positive] -from django.db import models -from django import forms - -class Article(models.Model): - pass -class Category(models.Model): - pass -class ArticleForm(forms.ModelForm): - class Meta: - model = Article - fields = '__all__' -class CategoryForm(forms.ModelForm): - class Meta: - model = Category - fields = '__all__' -class CompositeForm(ArticleForm, CategoryForm): - pass -[/CASE] - -[CASE formview_methods_on_forms_return_proper_types] -from typing import Any -from django import forms -from django.views.generic.edit import FormView - -class MyForm(forms.ModelForm): - pass -class MyForm2(forms.ModelForm): - pass - -class MyView(FormView): - form_class = MyForm - - def post(self, request, *args: Any, **kwds: Any): - form_class = self.get_form_class() - reveal_type(form_class) # N: Revealed type is 'Type[main.MyForm]' - reveal_type(self.get_form(None)) # N: Revealed type is 'main.MyForm' - reveal_type(self.get_form()) # N: Revealed type is 'main.MyForm' - reveal_type(self.get_form(form_class)) # N: Revealed type is 'main.MyForm' - reveal_type(self.get_form(MyForm2)) # N: Revealed type is 'main.MyForm2' -[/CASE] diff --git a/test-data/typecheck/import_all.test b/test-data/typecheck/import_all.test deleted file mode 100644 index e81fae8f3..000000000 --- a/test-data/typecheck/import_all.test +++ /dev/null @@ -1,402 +0,0 @@ -[CASE import_all_packages_to_trigger_stubs_check] -import django.apps -import django.apps.config -import django.apps.registry -import django.conf.global_settings -import django.conf.urls -import django.conf.urls.i18n -import django.conf.urls.static -import django.contrib.admin.actions -import django.contrib.admin.apps -import django.contrib.admin.checks -import django.contrib.admin.decorators -import django.contrib.admin.filters -import django.contrib.admin.forms -import django.contrib.admin.helpers -import django.contrib.admin.models -import django.contrib.admin.options -import django.contrib.admin.sites -import django.contrib.admin.templatetags -import django.contrib.admin.templatetags.admin_list -import django.contrib.admin.templatetags.admin_modify -import django.contrib.admin.templatetags.admin_static -import django.contrib.admin.templatetags.admin_urls -import django.contrib.admin.templatetags.base -import django.contrib.admin.templatetags.log -import django.contrib.admin.utils -import django.contrib.admin.views -import django.contrib.admin.views.autocomplete -import django.contrib.admin.views.decorators -import django.contrib.admin.views.main -import django.contrib.admin.widgets -import django.contrib.admindocs -import django.contrib.admindocs.middleware -import django.contrib.admindocs.utils -import django.contrib.admindocs.views -import django.contrib.auth.admin -import django.contrib.auth.apps -import django.contrib.auth.backends -import django.contrib.auth.base_user -import django.contrib.auth.checks -import django.contrib.auth.context_processors -import django.contrib.auth.decorators -import django.contrib.auth.forms -import django.contrib.auth.handlers -import django.contrib.auth.handlers.modwsgi -import django.contrib.auth.hashers -import django.contrib.auth.management.commands -import django.contrib.auth.management.commands.changepassword -import django.contrib.auth.management.commands.createsuperuser -import django.contrib.auth.middleware -import django.contrib.auth.mixins -import django.contrib.auth.models -import django.contrib.auth.password_validation -import django.contrib.auth.signals -import django.contrib.auth.tokens -import django.contrib.auth.validators -import django.contrib.auth.views -import django.contrib.contenttypes.admin -import django.contrib.contenttypes.apps -import django.contrib.contenttypes.checks -import django.contrib.contenttypes.fields -import django.contrib.contenttypes.forms -import django.contrib.contenttypes.management.commands -import django.contrib.contenttypes.management.commands.remove_stale_contenttypes -import django.contrib.contenttypes.models -import django.contrib.contenttypes.views -import django.contrib.flatpages.forms -import django.contrib.flatpages.middleware -import django.contrib.flatpages.models -import django.contrib.flatpages.sitemaps -import django.contrib.flatpages.templatetags -import django.contrib.flatpages.templatetags.flatpages -import django.contrib.flatpages.views -import django.contrib.humanize.templatetags -import django.contrib.humanize.templatetags.humanize -import django.contrib.messages.api -import django.contrib.messages.context_processors -import django.contrib.messages.middleware -import django.contrib.messages.storage -import django.contrib.messages.storage.base -import django.contrib.messages.storage.cookie -import django.contrib.messages.storage.fallback -import django.contrib.messages.storage.session -import django.contrib.messages.utils -import django.contrib.messages.views -import django.contrib.postgres.fields -import django.contrib.postgres.fields.array -import django.contrib.postgres.fields.citext -import django.contrib.postgres.fields.hstore -import django.contrib.postgres.fields.jsonb -import django.contrib.postgres.fields.mixins -import django.contrib.postgres.fields.ranges -import django.contrib.postgres.operations -import django.contrib.redirects -import django.contrib.redirects.middleware -import django.contrib.redirects.models -import django.contrib.sessions.backends -import django.contrib.sessions.backends.base -import django.contrib.sessions.backends.cache -import django.contrib.sessions.backends.cached_db -import django.contrib.sessions.backends.db -import django.contrib.sessions.backends.file -import django.contrib.sessions.backends.signed_cookies -import django.contrib.sessions.base_session -import django.contrib.sessions.management.commands -import django.contrib.sessions.management.commands.clearsessions -import django.contrib.sessions.middleware -import django.contrib.sessions.models -import django.contrib.sessions.serializers -import django.contrib.sitemaps.management.commands -import django.contrib.sitemaps.management.commands.ping_google -import django.contrib.sitemaps.views -import django.contrib.sites -import django.contrib.sites.apps -import django.contrib.sites.management -import django.contrib.sites.managers -import django.contrib.sites.middleware -import django.contrib.sites.models -import django.contrib.sites.requests -import django.contrib.sites.shortcuts -import django.contrib.staticfiles.apps -import django.contrib.staticfiles.checks -import django.contrib.staticfiles.finders -import django.contrib.staticfiles.handlers -import django.contrib.staticfiles.management.commands -import django.contrib.staticfiles.management.commands.collectstatic -import django.contrib.staticfiles.management.commands.findstatic -import django.contrib.staticfiles.management.commands.runserver -import django.contrib.staticfiles.storage -import django.contrib.staticfiles.templatetags -import django.contrib.staticfiles.templatetags.staticfiles -import django.contrib.staticfiles.urls -import django.contrib.staticfiles.utils -import django.contrib.staticfiles.views -import django.contrib.syndication -import django.contrib.syndication.views -import django.core.cache.backends -import django.core.cache.backends.base -import django.core.cache.backends.db -import django.core.cache.backends.dummy -import django.core.cache.backends.filebased -import django.core.cache.backends.locmem -import django.core.cache.utils -import django.core.checks.caches -import django.core.checks.database -import django.core.checks.messages -import django.core.checks.model_checks -import django.core.checks.registry -import django.core.checks.security -import django.core.checks.security.base -import django.core.checks.security.csrf -import django.core.checks.security.sessions -import django.core.checks.templates -import django.core.checks.urls -import django.core.exceptions -import django.core.files -import django.core.files.base -import django.core.files.images -import django.core.files.locks -import django.core.files.move -import django.core.files.storage -import django.core.files.temp -import django.core.files.uploadedfile -import django.core.files.uploadhandler -import django.core.files.utils -import django.core.handlers -import django.core.handlers.base -import django.core.handlers.exception -import django.core.handlers.wsgi -import django.core.mail -import django.core.mail.message -import django.core.mail.utils -import django.core.management -import django.core.management.base -import django.core.management.color -import django.core.management.sql -import django.core.management.templates -import django.core.management.utils -import django.core.paginator -import django.core.serializers -import django.core.serializers.base -import django.core.serializers.json -import django.core.serializers.python -import django.core.servers -import django.core.servers.basehttp -import django.core.signals -import django.core.signing -import django.core.validators -import django.core.wsgi -import django.db.backends.base -import django.db.backends.base.base -import django.db.backends.base.client -import django.db.backends.base.creation -import django.db.backends.base.features -import django.db.backends.base.introspection -import django.db.backends.base.operations -import django.db.backends.base.schema -import django.db.backends.base.validation -import django.db.backends.ddl_references -import django.db.backends.dummy -import django.db.backends.dummy.base -import django.db.backends.mysql -import django.db.backends.mysql.client -import django.db.backends.postgresql -import django.db.backends.postgresql.client -import django.db.backends.sqlite3 -import django.db.backends.sqlite3.base -import django.db.backends.sqlite3.creation -import django.db.backends.sqlite3.features -import django.db.backends.sqlite3.introspection -import django.db.backends.sqlite3.operations -import django.db.backends.sqlite3.schema -import django.db.backends.utils -import django.db.migrations.autodetector -import django.db.migrations.exceptions -import django.db.migrations.executor -import django.db.migrations.graph -import django.db.migrations.loader -import django.db.migrations.migration -import django.db.migrations.operations -import django.db.migrations.operations.base -import django.db.migrations.operations.fields -import django.db.migrations.operations.models -import django.db.migrations.operations.special -import django.db.migrations.operations.utils -import django.db.migrations.optimizer -import django.db.migrations.questioner -import django.db.migrations.recorder -import django.db.migrations.serializer -import django.db.migrations.state -import django.db.migrations.topological_sort -import django.db.migrations.utils -import django.db.migrations.writer -import django.db.models.aggregates -import django.db.models.base -import django.db.models.deletion -import django.db.models.expressions -import django.db.models.fields -import django.db.models.fields.files -import django.db.models.fields.mixins -import django.db.models.fields.proxy -import django.db.models.fields.related -import django.db.models.fields.related_descriptors -import django.db.models.fields.related_lookups -import django.db.models.fields.reverse_related -import django.db.models.functions -import django.db.models.functions.comparison -import django.db.models.functions.datetime -import django.db.models.functions.text -import django.db.models.functions.window -import django.db.models.indexes -import django.db.models.lookups -import django.db.models.manager -import django.db.models.options -import django.db.models.query -import django.db.models.query_utils -import django.db.models.signals -import django.db.models.sql -import django.db.models.sql.compiler -import django.db.models.sql.constants -import django.db.models.sql.datastructures -import django.db.models.sql.query -import django.db.models.sql.subqueries -import django.db.models.sql.where -import django.db.models.utils -import django.db.transaction -import django.db.utils -import django.dispatch -import django.dispatch.dispatcher -import django.forms -import django.forms.boundfield -import django.forms.fields -import django.forms.forms -import django.forms.formsets -import django.forms.models -import django.forms.renderers -import django.forms.utils -import django.forms.widgets -import django.http -import django.http.cookie -import django.http.multipartparser -import django.http.request -import django.http.response -import django.middleware -import django.middleware.cache -import django.middleware.clickjacking -import django.middleware.common -import django.middleware.csrf -import django.middleware.gzip -import django.middleware.http -import django.middleware.locale -import django.middleware.security -import django.shortcuts -import django.template.backends -import django.template.backends.base -import django.template.backends.django -import django.template.backends.dummy -import django.template.backends.jinja2 -import django.template.backends.utils -import django.template.base -import django.template.context -import django.template.context_processors -import django.template.defaultfilters -import django.template.defaulttags -import django.template.engine -import django.template.exceptions -import django.template.library -import django.template.loader -import django.template.loader_tags -import django.template.loaders -import django.template.loaders.app_directories -import django.template.loaders.base -import django.template.loaders.cached -import django.template.loaders.filesystem -import django.template.loaders.locmem -import django.template.response -import django.template.smartif -import django.template.utils -import django.templatetags -import django.templatetags.cache -import django.templatetags.i18n -import django.templatetags.l10n -import django.templatetags.static -import django.templatetags.tz -import django.test -import django.test.client -import django.test.html -import django.test.runner -import django.test.selenium -import django.test.signals -import django.test.testcases -import django.test.utils -import django.urls -import django.urls.base -import django.urls.conf -import django.urls.converters -import django.urls.exceptions -import django.urls.resolvers -import django.urls.utils -import django.utils._os -import django.utils.archive -import django.utils.autoreload -import django.utils.baseconv -import django.utils.cache -import django.utils.crypto -import django.utils.datastructures -import django.utils.dateformat -import django.utils.dateparse -import django.utils.dates -import django.utils.datetime_safe -import django.utils.deconstruct -import django.utils.decorators -import django.utils.deprecation -import django.utils.duration -import django.utils.encoding -import django.utils.feedgenerator -import django.utils.formats -import django.utils.functional -import django.utils.html -import django.utils.http -import django.utils.inspect -import django.utils.ipv6 -import django.utils.itercompat -import django.utils.jslex -import django.utils.log -import django.utils.lorem_ipsum -import django.utils.module_loading -import django.utils.numberformat -import django.utils.regex_helper -import django.utils.safestring -import django.utils.six -import django.utils.termcolors -import django.utils.text -import django.utils.timesince -import django.utils.timezone -import django.utils.translation -import django.utils.translation.template -import django.utils.translation.trans_null -import django.utils.translation.trans_real -import django.utils.tree -import django.utils.version -import django.utils.xmlutils -import django.views.csrf -import django.views.debug -import django.views.decorators -import django.views.decorators.cache -import django.views.decorators.clickjacking -import django.views.decorators.csrf -import django.views.decorators.debug -import django.views.decorators.gzip -import django.views.decorators.http -import django.views.decorators.vary -import django.views.defaults -import django.views.generic -import django.views.generic.base -import django.views.generic.dates -import django.views.generic.detail -import django.views.generic.edit -import django.views.generic.list -import django.views.i18n -import django.views.static -[/CASE] \ No newline at end of file diff --git a/test-data/typecheck/managers.test b/test-data/typecheck/managers.test deleted file mode 100644 index a6a8c9bea..000000000 --- a/test-data/typecheck/managers.test +++ /dev/null @@ -1,201 +0,0 @@ -[CASE test_every_model_has_objects_queryset_available] -from django.db import models -class User(models.Model): - pass -reveal_type(User.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.User]' -reveal_type(User.objects.get()) # N: Revealed type is 'main.User*' - -[CASE every_model_has_its_own_objects_queryset] -from django.db import models -class Parent(models.Model): - pass -class Child(Parent): - pass -reveal_type(Parent.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.Parent]' -reveal_type(Child.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.Child]' -[out] - -[CASE if_manager_is_defined_on_model_do_not_add_objects] -from django.db import models - -class MyModel(models.Model): - authors = models.Manager[MyModel]() -reveal_type(MyModel.authors) # N: Revealed type is 'django.db.models.manager.Manager[main.MyModel]' -MyModel.objects # E: "Type[MyModel]" has no attribute "objects" -[out] - -[CASE test_model_objects_attribute_present_in_case_of_model_cls_passed_as_generic_parameter] -from typing import TypeVar, Generic, Type -from django.db import models - -_T = TypeVar('_T', bound=models.Model) -class Base(Generic[_T]): - def __init__(self, model_cls: Type[_T]): - self.model_cls = model_cls - reveal_type(self.model_cls._default_manager) # N: Revealed type is 'django.db.models.manager.Manager[django.db.models.base.Model]' -class MyModel(models.Model): - pass -base_instance = Base(MyModel) -reveal_type(base_instance.model_cls._default_manager) # N: Revealed type is 'django.db.models.manager.Manager[main.MyModel]' - -class Child(Base[MyModel]): - def method(self) -> None: - reveal_type(self.model_cls._default_manager) # N: Revealed type is 'django.db.models.manager.Manager[main.MyModel]' - -[CASE if_custom_manager_defined_it_is_set_to_default_manager] -from typing import TypeVar -from django.db import models -_T = TypeVar('_T', bound=models.Model) -class CustomManager(models.Manager[_T]): - pass -class MyModel(models.Model): - manager = CustomManager[MyModel]() -reveal_type(MyModel._default_manager) # N: Revealed type is 'main.CustomManager[main.MyModel]' - -[CASE if_default_manager_name_is_passed_set_default_manager_to_it] -from typing import TypeVar -from django.db import models - -_T = TypeVar('_T', bound=models.Model) -class Manager1(models.Manager[_T]): - pass -class Manager2(models.Manager[_T]): - pass -class MyModel(models.Model): - class Meta: - default_manager_name = 'm2' - m1: Manager1[MyModel] - m2: Manager2[MyModel] -reveal_type(MyModel._default_manager) # N: Revealed type is 'main.Manager2[main.MyModel]' - -[CASE test_leave_as_is_if_objects_is_set_and_fill_typevars_with_outer_class] -from django.db import models - -class UserManager(models.Manager[MyUser]): - def get_or_404(self) -> MyUser: - pass - -class MyUser(models.Model): - objects = UserManager() - -reveal_type(MyUser.objects) # N: Revealed type is 'main.UserManager[main.MyUser]' -reveal_type(MyUser.objects.get()) # N: Revealed type is 'main.MyUser*' -reveal_type(MyUser.objects.get_or_404()) # N: Revealed type is 'main.MyUser' - -[CASE model_imported_from_different_file] -from django.db import models -from models.main import Inventory - -class Band(models.Model): - pass -reveal_type(Inventory.objects) # N: Revealed type is 'django.db.models.manager.Manager[models.main.Inventory]' -reveal_type(Band.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.Band]' -[file models/__init__.py] -[file models/main.py] -from django.db import models -class Inventory(models.Model): - pass - -[CASE managers_that_defined_on_other_models_do_not_influence] -from django.db import models - -class AbstractPerson(models.Model): - abstract_persons = models.Manager[AbstractPerson]() -class PublishedBookManager(models.Manager[Book]): - pass -class AnnotatedBookManager(models.Manager[Book]): - pass -class Book(models.Model): - title = models.CharField(max_length=50) - published_objects = PublishedBookManager() - annotated_objects = AnnotatedBookManager() - -reveal_type(AbstractPerson.abstract_persons) # N: Revealed type is 'django.db.models.manager.Manager[main.AbstractPerson]' -reveal_type(Book.published_objects) # N: Revealed type is 'main.PublishedBookManager[main.Book]' -Book.published_objects.create(title='hello') -reveal_type(Book.annotated_objects) # N: Revealed type is 'main.AnnotatedBookManager[main.Book]' -Book.annotated_objects.create(title='hello') -[out] - -[CASE managers_inherited_from_abstract_classes_multiple_inheritance] -from django.db import models -class CustomManager1(models.Manager[AbstractBase1]): - pass -class AbstractBase1(models.Model): - class Meta: - abstract = True - name = models.CharField(max_length=50) - manager1 = CustomManager1() -class CustomManager2(models.Manager[AbstractBase2]): - pass -class AbstractBase2(models.Model): - class Meta: - abstract = True - value = models.CharField(max_length=50) - restricted = CustomManager2() - -class Child(AbstractBase1, AbstractBase2): - pass -[out] - -[CASE managers_from_unrelated_models_dont_interfere] - -from django.db import models - -# Normal scenario where one model has a manager with an annotation of the same type as the model -class UnrelatedModel(models.Model): - objects = models.Manager[UnrelatedModel]() - -class MyModel(models.Model): - pass - -reveal_type(UnrelatedModel.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.UnrelatedModel]' -reveal_type(UnrelatedModel.objects.first()) # N: Revealed type is 'Union[main.UnrelatedModel*, None]' - -reveal_type(MyModel.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.MyModel]' -reveal_type(MyModel.objects.first()) # N: Revealed type is 'Union[main.MyModel*, None]' - -# Possible to specify objects without explicit annotation of models.Manager() -class UnrelatedModel2(models.Model): - objects = models.Manager() - -class MyModel2(models.Model): - pass - -reveal_type(UnrelatedModel2.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.UnrelatedModel2]' -reveal_type(UnrelatedModel2.objects.first()) # N: Revealed type is 'Union[main.UnrelatedModel2*, None]' - -reveal_type(MyModel2.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.MyModel2]' -reveal_type(MyModel2.objects.first()) # N: Revealed type is 'Union[main.MyModel2*, None]' - - -# Inheritance works -class ParentOfMyModel3(models.Model): - objects = models.Manager() - -class MyModel3(ParentOfMyModel3): - pass - - -reveal_type(ParentOfMyModel3.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.ParentOfMyModel3]' -reveal_type(ParentOfMyModel3.objects.first()) # N: Revealed type is 'Union[main.ParentOfMyModel3*, None]' - -reveal_type(MyModel3.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.MyModel3]' -reveal_type(MyModel3.objects.first()) # N: Revealed type is 'Union[main.MyModel3*, None]' - - -# Inheritance works with explicit objects in child -class ParentOfMyModel4(models.Model): - objects = models.Manager() - -class MyModel4(ParentOfMyModel4): - objects = models.Manager[MyModel4]() - -reveal_type(ParentOfMyModel4.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.ParentOfMyModel4]' -reveal_type(ParentOfMyModel4.objects.first()) # N: Revealed type is 'Union[main.ParentOfMyModel4*, None]' - -reveal_type(MyModel4.objects) # N: Revealed type is 'django.db.models.manager.Manager[main.MyModel4]' -reveal_type(MyModel4.objects.first()) # N: Revealed type is 'Union[main.MyModel4*, None]' - - -[out] \ No newline at end of file diff --git a/test-data/typecheck/managers/querysets/test_basic_methods.yml b/test-data/typecheck/managers/querysets/test_basic_methods.yml new file mode 100644 index 000000000..64b4b49e0 --- /dev/null +++ b/test-data/typecheck/managers/querysets/test_basic_methods.yml @@ -0,0 +1,38 @@ +- case: queryset_basic_methods_return_type + main: | + from myapp.models import Blog + + qs = Blog.objects.all() + reveal_type(qs) # N: Revealed type is 'django.db.models.query.QuerySet[myapp.models.Blog*, myapp.models.Blog*]' + reveal_type(qs.get(id=1)) # N: Revealed type is 'myapp.models.Blog*' + reveal_type(iter(qs)) # N: Revealed type is 'typing.Iterator[myapp.models.Blog*]' + reveal_type(qs.iterator()) # N: Revealed type is 'typing.Iterator[myapp.models.Blog*]' + reveal_type(qs.first()) # N: Revealed type is 'myapp.models.Blog*' + reveal_type(qs.earliest()) # N: Revealed type is 'myapp.models.Blog*' + reveal_type(qs[0]) # N: Revealed type is 'myapp.models.Blog*' + reveal_type(qs[:9]) # N: Revealed type is 'django.db.models.query.QuerySet[myapp.models.Blog*, myapp.models.Blog*]' + reveal_type(qs.in_bulk()) # N: Revealed type is 'builtins.dict[Any, myapp.models.Blog*]' + + # .dates / .datetimes + reveal_type(Blog.objects.dates("created_at", "day")) # N: Revealed type is 'django.db.models.query.QuerySet[myapp.models.Blog*, datetime.date]' + reveal_type(Blog.objects.datetimes("created_at", "day")) # N: Revealed type is 'django.db.models.query.QuerySet[myapp.models.Blog*, datetime.datetime]' + + # AND-ing QuerySets + reveal_type(Blog.objects.all() & Blog.objects.all()) # N: Revealed type is 'django.db.models.query.QuerySet[myapp.models.Blog*, myapp.models.Blog*]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class Blog(models.Model): + created_at = models.DateTimeField() + +- case: queryset_could_be_specified_with_one_type + main: | + from typing import Optional + from django.db import models + queryset: models.QuerySet[models.Model] = models.QuerySet() + reveal_type(queryset) # N: Revealed type is 'django.db.models.query.QuerySet[django.db.models.base.Model, django.db.models.base.Model]' diff --git a/test-data/typecheck/managers/querysets/test_values.yml b/test-data/typecheck/managers/querysets/test_values.yml new file mode 100644 index 000000000..420fcd07e --- /dev/null +++ b/test-data/typecheck/managers/querysets/test_values.yml @@ -0,0 +1,109 @@ +- case: queryset_values_method_returns_typeddict + main: | + from myapp.models import Blog + values = Blog.objects.values('num_posts', 'text').get() + reveal_type(values) # N: Revealed type is 'TypedDict({'num_posts': builtins.int, 'text': builtins.str})' + reveal_type(values["num_posts"]) # N: Revealed type is 'builtins.int' + reveal_type(values["text"]) # N: Revealed type is 'builtins.str' + + values_pk = Blog.objects.values('pk').get() + reveal_type(values_pk) # N: Revealed type is 'TypedDict({'pk': builtins.int})' + reveal_type(values_pk["pk"]) # N: Revealed type is 'builtins.int' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Blog(models.Model): + num_posts = models.IntegerField() + text = models.CharField(max_length=100) + +- case: queryset_values_all_values + main: | + from myapp.models import Blog + all_values_dict = Blog.objects.values().get() + reveal_type(all_values_dict) # N: Revealed type is 'TypedDict({'id': builtins.int, 'num_posts': builtins.int, 'text': builtins.str})' + reveal_type(all_values_dict["id"]) # N: Revealed type is 'builtins.int' + reveal_type(all_values_dict["num_posts"]) # N: Revealed type is 'builtins.int' + reveal_type(all_values_dict["text"]) # N: Revealed type is 'builtins.str' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Blog(models.Model): + num_posts = models.IntegerField() + text = models.CharField(max_length=100) + +- case: queryset_foreign_key_object_always_a_primary_key + main: | + from myapp.models import Blog + values1 = Blog.objects.values('publisher').get() + reveal_type(values1) # N: Revealed type is 'TypedDict({'publisher': builtins.int})' + reveal_type(values1['publisher']) # N: Revealed type is 'builtins.int' + + values2 = Blog.objects.values('publisher_id').get() + reveal_type(values2) # N: Revealed type is 'TypedDict({'publisher_id': builtins.int})' + reveal_type(values2["publisher_id"]) # N: Revealed type is 'builtins.int' + + # all values return _id version + all_values = Blog.objects.values().get() + reveal_type(all_values) # N: Revealed type is 'TypedDict({'id': builtins.int, 'publisher_id': builtins.int})' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Blog(models.Model): + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) + +- case: values_with_related_model_fields + main: | + from myapp.models import Entry + values = Entry.objects.values('blog__num_articles', 'blog__publisher__name').get() + reveal_type(values) # N: Revealed type is 'TypedDict({'blog__num_articles': builtins.int, 'blog__publisher__name': builtins.str})' + + pk_values = Entry.objects.values('blog__pk', 'blog__publisher__pk').get() + reveal_type(pk_values) # N: Revealed type is 'TypedDict({'blog__pk': builtins.int, 'blog__publisher__pk': builtins.int})' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + name = models.CharField(max_length=100) + class Blog(models.Model): + num_articles = models.IntegerField() + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) + class Entry(models.Model): + blog = models.ForeignKey(Blog, on_delete=models.CASCADE) + +- case: select_all_related_model_values_for_every_current_value + main: | + from myapp.models import Publisher + related_model_values = Publisher.objects.values('id', 'blog__name').get() + reveal_type(related_model_values) # N: Revealed type is 'TypedDict({'id': builtins.int, 'blog__name': builtins.str})' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Blog(models.Model): + name = models.CharField(max_length=100) + publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) diff --git a/test-data/typecheck/managers/querysets/test_values_list.yml b/test-data/typecheck/managers/querysets/test_values_list.yml new file mode 100644 index 000000000..76d29ff72 --- /dev/null +++ b/test-data/typecheck/managers/querysets/test_values_list.yml @@ -0,0 +1,190 @@ +- case: values_list_simple_field_returns_queryset_of_tuples + main: | + from myapp.models import MyUser + reveal_type(MyUser.objects.values_list('name').get()) # N: Revealed type is 'Tuple[builtins.str]' + reveal_type(MyUser.objects.values_list('id', 'name').get()) # N: Revealed type is 'Tuple[builtins.int, builtins.str]' + + values_tuple = MyUser.objects.values_list('name', 'age').get() + reveal_type(values_tuple[0]) # N: Revealed type is 'builtins.str' + reveal_type(values_tuple[1]) # N: Revealed type is 'builtins.int' + + # no fields specified return all fields + all_values_tuple = MyUser.objects.values_list().get() + reveal_type(all_values_tuple) # N: Revealed type is 'Tuple[builtins.int, builtins.str, builtins.int]' + + # pk as field + pk_values = MyUser.objects.values_list('pk').get() + reveal_type(pk_values) # N: # N: Revealed type is 'Tuple[builtins.int]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + name = models.CharField(max_length=100) + age = models.IntegerField() + +- case: values_list_related_model_fields + main: | + from myapp.models import Post, Blog + values_tuple = Post.objects.values_list('blog', 'blog__num_posts', 'blog__publisher', 'blog__publisher__name').get() + reveal_type(values_tuple[0]) # N: Revealed type is 'myapp.models.Blog' + reveal_type(values_tuple[1]) # N: Revealed type is 'builtins.int' + reveal_type(values_tuple[2]) # N: Revealed type is 'myapp.models.Publisher' + reveal_type(values_tuple[3]) # N: Revealed type is 'builtins.str' + + reverse_fields_list = Blog.objects.values_list('post__text').get() + reveal_type(reverse_fields_list) # N: Revealed type is 'Tuple[builtins.str]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + name = models.CharField(max_length=100) + class Blog(models.Model): + num_posts = models.IntegerField() + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) + class Post(models.Model): + text = models.CharField(max_length=100) + blog = models.ForeignKey(to=Blog, on_delete=models.CASCADE) + +- case: values_list_flat_true + main: | + from myapp.models import MyUser, MyUser2 + reveal_type(MyUser.objects.values_list('name', flat=True).get()) # N: Revealed type is 'builtins.str*' + reveal_type(MyUser.objects.values_list('name', 'age', flat=True).get()) + + # flat=True without specified fields returns primary key values + reveal_type(MyUser.objects.values_list(flat=True)[0]) # N: Revealed type is 'builtins.int*' + reveal_type(MyUser2.objects.values_list(flat=True)[0]) # N: Revealed type is 'builtins.str*' + out: | + main:3: error: 'flat' is not valid when 'values_list' is called with more than one field + main:3: note: Revealed type is 'Any' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + name = models.CharField(max_length=100) + age = models.IntegerField() + class MyUser2(models.Model): + name = models.CharField(max_length=100, primary_key=True) + +- case: values_list_named_true + main: | + from myapp.models import MyUser + values_named_tuple = MyUser.objects.values_list('name', 'age', named=True).get() + reveal_type(values_named_tuple) # N: Revealed type is 'Tuple[builtins.str, builtins.int, fallback=main.Row]' + reveal_type(values_named_tuple.name) # N: Revealed type is 'builtins.str' + reveal_type(values_named_tuple.age) # N: Revealed type is 'builtins.int' + + # no fields specified, returns all fields namedtuple + all_values_named_tuple = MyUser.objects.values_list(named=True).get() + reveal_type(all_values_named_tuple.id) # N: Revealed type is 'builtins.int' + reveal_type(all_values_named_tuple.name) # N: Revealed type is 'builtins.str' + reveal_type(all_values_named_tuple.age) # N: Revealed type is 'builtins.int' + reveal_type(all_values_named_tuple.is_admin) # N: Revealed type is 'builtins.bool' + + # pk as field + pk_values = MyUser.objects.values_list('pk', named=True).get() + reveal_type(pk_values) # N: Revealed type is 'Tuple[builtins.int, fallback=main.Row2]' + reveal_type(pk_values.pk) # N: # N: Revealed type is 'builtins.int' + + # values_list(named=True) inside function + def func() -> None: + from myapp.models import MyUser + reveal_type(MyUser.objects.values_list('name', named=True).get()) # N: Revealed type is 'Tuple[builtins.str, fallback=main.Row3]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + name = models.CharField(max_length=100) + age = models.IntegerField() + is_admin = models.BooleanField() + +- case: values_list_flat_true_named_true_error + main: | + from myapp.models import MyUser + reveal_type(MyUser.objects.values_list('name', flat=True, named=True).get()) + out: | + main:2: error: 'flat' and 'named' can't be used together + main:2: note: Revealed type is 'Any' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + name = models.CharField(max_length=100) + +- case: invalid_lookups + main: | + from myapp.models import Blog + reveal_type(Blog.objects.values_list('unknown').get()) + reveal_type(Blog.objects.values_list('unknown', flat=True).get()) + reveal_type(Blog.objects.values_list('unknown', named=True).get()) + reveal_type(Blog.objects.values_list('publisher__unknown').get()) + out: | + main:2: error: Cannot resolve keyword 'unknown' into field. Choices are: id, publisher, publisher_id + main:2: note: Revealed type is 'Any' + main:3: error: Cannot resolve keyword 'unknown' into field. Choices are: id, publisher, publisher_id + main:3: note: Revealed type is 'Any' + main:4: error: Cannot resolve keyword 'unknown' into field. Choices are: id, publisher, publisher_id + main:4: note: Revealed type is 'Any' + main:5: error: Lookups not supported yet + main:5: note: Revealed type is 'Any' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Blog(models.Model): + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) + +- case: named_true_with_related_model_fields + main: | + from myapp.models import Entry, Blog + values = Entry.objects.values_list('blog__num_articles', 'blog__publisher__name', named=True).get() + reveal_type(values.blog__num_articles) # N: Revealed type is 'builtins.int' + reveal_type(values.blog__publisher__name) # N: Revealed type is 'builtins.str' + + pk_values = Entry.objects.values_list('blog__pk', 'blog__publisher__pk', named=True).get() + reveal_type(pk_values.blog__pk) # N: Revealed type is 'builtins.int' + reveal_type(pk_values.blog__publisher__pk) # N: Revealed type is 'builtins.int' + + # reverse relation + reverse_values = Blog.objects.values_list('entry__text', named=True).get() + reveal_type(reverse_values.entry__text) # N: Revealed type is 'builtins.str' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + name = models.CharField(max_length=100) + class Blog(models.Model): + num_articles = models.IntegerField() + publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) + class Entry(models.Model): + text = models.CharField(max_length=100) + blog = models.ForeignKey(Blog, on_delete=models.CASCADE) diff --git a/test-data/typecheck/managers/test_managers.yml b/test-data/typecheck/managers/test_managers.yml new file mode 100644 index 000000000..0e0e299bd --- /dev/null +++ b/test-data/typecheck/managers/test_managers.yml @@ -0,0 +1,322 @@ +- case: test_every_model_has_objects_queryset_available + main: | + from myapp.models import User + reveal_type(User.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.User]' + reveal_type(User.objects.get()) # N: Revealed type is 'myapp.models.User*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class User(models.Model): + pass + +- case: every_model_has_its_own_objects_queryset + main: | + from myapp.models import Parent, Child + reveal_type(Parent.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.Parent]' + reveal_type(Child.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.Child]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Parent(models.Model): + pass + class Child(Parent): + pass + +- case: test_model_objects_attribute_present_in_case_of_model_cls_passed_as_generic_parameter + main: | + from myapp.models import Base, MyModel + base_instance = Base(MyModel) + reveal_type(base_instance.model_cls._default_manager) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.MyModel]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from typing import TypeVar, Generic, Type + from django.db import models + + _T = TypeVar('_T', bound=models.Model) + class Base(Generic[_T]): + def __init__(self, model_cls: Type[_T]): + self.model_cls = model_cls + reveal_type(self.model_cls._default_manager) # N: Revealed type is 'django.db.models.manager.Manager[django.db.models.base.Model]' + class MyModel(models.Model): + pass + class Child(Base[MyModel]): + def method(self) -> None: + reveal_type(self.model_cls._default_manager) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.MyModel]' + +- case: if_custom_manager_defined_it_is_set_to_default_manager + main: | + from myapp.models import MyModel + reveal_type(MyModel._default_manager) # N: Revealed type is 'myapp.models.CustomManager[myapp.models.MyModel]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from typing import TypeVar + from django.db import models + _T = TypeVar('_T', bound=models.Model) + class CustomManager(models.Manager[_T]): + pass + class MyModel(models.Model): + manager = CustomManager['MyModel']() + +- case: if_default_manager_name_is_passed_set_default_manager_to_it + main: | + from myapp.models import MyModel + reveal_type(MyModel._default_manager) # N: Revealed type is 'myapp.models.Manager2[myapp.models.MyModel]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from typing import TypeVar + from django.db import models + + _T = TypeVar('_T', bound=models.Model) + class Manager1(models.Manager[_T]): + pass + class Manager2(models.Manager[_T]): + pass + class MyModel(models.Model): + class Meta: + default_manager_name = 'm2' + m1 = Manager1['MyModel']() + m2 = Manager2['MyModel']() + +- case: test_leave_as_is_if_objects_is_set_and_fill_typevars_with_outer_class + main: | + from myapp.models import MyUser + reveal_type(MyUser.objects) # N: Revealed type is 'myapp.models.UserManager[myapp.models.MyUser]' + reveal_type(MyUser.objects.get()) # N: Revealed type is 'myapp.models.MyUser*' + reveal_type(MyUser.objects.get_or_404()) # N: Revealed type is 'myapp.models.MyUser' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class UserManager(models.Manager['MyUser']): + def get_or_404(self) -> 'MyUser': + pass + + class MyUser(models.Model): + objects = UserManager() + +- case: model_imported_from_different_file + main: | + from myapp.models import Inventory, Band + reveal_type(Inventory.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.main.Inventory]' + reveal_type(Band.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.Band]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models/__init__.py + content: | + from django.db import models + from .main import Inventory + class Band(models.Model): + pass + - path: myapp/models/main.py + content: | + from django.db import models + class Inventory(models.Model): + pass + +- case: managers_that_defined_on_other_models_do_not_influence + main: | + from myapp.models import AbstractPerson, Book + reveal_type(AbstractPerson.abstract_persons) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.AbstractPerson]' + reveal_type(Book.published_objects) # N: Revealed type is 'myapp.models.PublishedBookManager[myapp.models.Book]' + Book.published_objects.create(title='hello') + reveal_type(Book.annotated_objects) # N: Revealed type is 'myapp.models.AnnotatedBookManager[myapp.models.Book]' + Book.annotated_objects.create(title='hello') + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class AbstractPerson(models.Model): + abstract_persons = models.Manager['AbstractPerson']() + class PublishedBookManager(models.Manager['Book']): + pass + class AnnotatedBookManager(models.Manager['Book']): + pass + class Book(models.Model): + title = models.CharField(max_length=50) + published_objects = PublishedBookManager() + annotated_objects = AnnotatedBookManager() + +- case: managers_inherited_from_abstract_classes_multiple_inheritance + main: | + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class CustomManager1(models.Manager['AbstractBase1']): + pass + class AbstractBase1(models.Model): + class Meta: + abstract = True + name = models.CharField(max_length=50) + manager1 = CustomManager1() + class CustomManager2(models.Manager['AbstractBase2']): + pass + class AbstractBase2(models.Model): + class Meta: + abstract = True + value = models.CharField(max_length=50) + restricted = CustomManager2() + + class Child(AbstractBase1, AbstractBase2): + pass + +- case: model_has_a_manager_of_the_same_type + main: | + from myapp.models import UnrelatedModel, MyModel + reveal_type(UnrelatedModel.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.UnrelatedModel]' + reveal_type(UnrelatedModel.objects.first()) # N: Revealed type is 'myapp.models.UnrelatedModel*' + + reveal_type(MyModel.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.MyModel]' + reveal_type(MyModel.objects.first()) # N: Revealed type is 'myapp.models.MyModel*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class UnrelatedModel(models.Model): + objects = models.Manager['UnrelatedModel']() + + class MyModel(models.Model): + pass + +- case: manager_without_annotation_of_the_model_gets_it_from_outer_one + main: | + from myapp.models import UnrelatedModel2, MyModel2 + reveal_type(UnrelatedModel2.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.UnrelatedModel2]' + reveal_type(UnrelatedModel2.objects.first()) # N: Revealed type is 'myapp.models.UnrelatedModel2*' + + reveal_type(MyModel2.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.MyModel2]' + reveal_type(MyModel2.objects.first()) # N: Revealed type is 'myapp.models.MyModel2*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class UnrelatedModel2(models.Model): + objects = models.Manager() + + class MyModel2(models.Model): + pass + +- case: inherited_manager_has_the_proper_type_of_model + main: | + from myapp.models import ParentOfMyModel3, MyModel3 + reveal_type(ParentOfMyModel3.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.ParentOfMyModel3]' + reveal_type(ParentOfMyModel3.objects.first()) # N: Revealed type is 'myapp.models.ParentOfMyModel3*' + + reveal_type(MyModel3.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.MyModel3]' + reveal_type(MyModel3.objects.first()) # N: Revealed type is 'myapp.models.MyModel3*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class ParentOfMyModel3(models.Model): + objects = models.Manager() + + class MyModel3(ParentOfMyModel3): + pass + +- case: inheritance_with_explicit_type_on_child_manager + main: | + from myapp.models import ParentOfMyModel4, MyModel4 + reveal_type(ParentOfMyModel4.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.ParentOfMyModel4]' + reveal_type(ParentOfMyModel4.objects.first()) # N: Revealed type is 'myapp.models.ParentOfMyModel4*' + + reveal_type(MyModel4.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.MyModel4]' + reveal_type(MyModel4.objects.first()) # N: Revealed type is 'myapp.models.MyModel4*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class ParentOfMyModel4(models.Model): + objects = models.Manager() + + class MyModel4(ParentOfMyModel4): + objects = models.Manager['MyModel4']() + +# TODO: make it work someday +#- case: inheritance_of_two_models_with_custom_objects_manager +# main: | +# from myapp.models import MyBaseUser, MyUser +# reveal_type(MyBaseUser.objects) # N: Revealed type is 'myapp.models.MyBaseManager[myapp.models.MyBaseUser]' +# reveal_type(MyBaseUser.objects.get()) # N: Revealed type is 'myapp.models.MyBaseUser' +# +# reveal_type(MyUser.objects) # N: Revealed type is 'myapp.models.MyManager[myapp.models.MyUser]' +# reveal_type(MyUser.objects.get()) # N: Revealed type is 'myapp.models.MyUser' +# installed_apps: +# - myapp +# files: +# - path: myapp/__init__.py +# - path: myapp/models.py +# content: | +# from django.db import models +# +# class MyBaseManager(models.Manager): +# pass +# class MyBaseUser(models.Model): +# objects = MyBaseManager() +# +# class MyManager(models.Manager): +# pass +# class MyUser(MyBaseUser): +# objects = MyManager() + +- case: custom_manager_returns_proper_model_types + main: | + from myapp.models import User + reveal_type(User.objects.get()) # N: Revealed type is 'myapp.models.User*' + reveal_type(User.objects.select_related()) # N: Revealed type is 'django.db.models.query.QuerySet[myapp.models.User*, myapp.models.User*]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyManager(models.Manager): + pass + class User(models.Model): + objects = MyManager() diff --git a/test-data/typecheck/messages.test b/test-data/typecheck/messages.test deleted file mode 100644 index 9a3646140..000000000 --- a/test-data/typecheck/messages.test +++ /dev/null @@ -1,10 +0,0 @@ -[CASE successmessagemixin_compatible_with_formmixin] -from django.views.generic.edit import FormMixin -from django.contrib.messages.views import SuccessMessageMixin - -class FormFirstView(FormMixin, SuccessMessageMixin): - pass - -class SuccessMessageFirstView(FormMixin, SuccessMessageMixin): - pass -[/CASE] diff --git a/test-data/typecheck/migrations.test b/test-data/typecheck/migrations.test deleted file mode 100644 index 08dd7fabc..000000000 --- a/test-data/typecheck/migrations.test +++ /dev/null @@ -1,48 +0,0 @@ -[CASE registry_apps_get_model] -from django.apps.registry import Apps -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from myapp.models import User -apps = Apps() -model_cls = apps.get_model('myapp', 'User') -reveal_type(model_cls) # N: Revealed type is 'Type[myapp.models.User]' -reveal_type(model_cls.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.User]' - -[file myapp/__init__.py] -[file myapp/models.py] -from django.db import models -class User(models.Model): - pass - -[CASE registry_apps_get_model_passed_as_variables_not_supported] -from django.apps.registry import Apps -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from myapp.models import User -app_name = 'myapp' -model_name = 'User' -apps = Apps() -model_cls = apps.get_model(app_name, model_name) -reveal_type(model_cls) # N: Revealed type is 'Type[django.db.models.base.Model]' - -[file myapp/__init__.py] -[file myapp/models.py] -from django.db import models -class User(models.Model): - pass - -[CASE state_apps_get_model] -from django.db.migrations.state import StateApps -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from myapp.models import User -apps = StateApps([], {}) -model_cls = apps.get_model('myapp', 'User') -reveal_type(model_cls) # N: Revealed type is 'Type[myapp.models.User]' -reveal_type(model_cls.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.User]' - -[file myapp/__init__.py] -[file myapp/models.py] -from django.db import models -class User(models.Model): - pass \ No newline at end of file diff --git a/test-data/typecheck/model.test b/test-data/typecheck/model.test deleted file mode 100644 index 4b95b742d..000000000 --- a/test-data/typecheck/model.test +++ /dev/null @@ -1,60 +0,0 @@ -[CASE test_typechecking_for_model_subclasses] -from django.db import models - -class A(models.Model): - pass -class B(models.Model): - b_attr = 1 - pass -class C(A): - pass - -def service(a: A) -> int: - pass - -b_instance = B() -service(b_instance) # E: Argument 1 to "service" has incompatible type "B"; expected "A" - -a_instance = A() -c_instance = C() -service(a_instance) -service(c_instance) -[/CASE] - - -[CASE fail_if_no_such_attribute_on_model] -from django.db import models - -class B(models.Model): - b_attr = 1 - pass - -b_instance = B() -reveal_type(b_instance.b_attr) # N: Revealed type is 'builtins.int' - -reveal_type(b_instance.non_existent_attribute) -b_instance.non_existent_attribute = 2 -[out] -main:10: note: Revealed type is 'Any' -main:10: error: "B" has no attribute "non_existent_attribute" -main:11: error: "B" has no attribute "non_existent_attribute" -[/CASE] - - -[CASE ignore_missing_attributes_if_setting_is_passed] -from django.db import models - -class B(models.Model): - pass - -b_instance = B() -reveal_type(b_instance.non_existent_attribute) # N: Revealed type is 'Any' -b_instance.non_existent_attribute = 2 - -[env MYPY_DJANGO_CONFIG=${MYPY_CWD}/mypy_django.ini] - -[file mypy_django.ini] -[[mypy_django_plugin] -ignore_missing_model_attributes = True - -[/CASE] \ No newline at end of file diff --git a/test-data/typecheck/model_create.test b/test-data/typecheck/model_create.test deleted file mode 100644 index 5f028f231..000000000 --- a/test-data/typecheck/model_create.test +++ /dev/null @@ -1,64 +0,0 @@ -[CASE default_manager_create_is_typechecked] -from django.db import models - -class User(models.Model): - name = models.CharField(max_length=100) - age = models.IntegerField() - -User.objects.create(name='Max', age=10) -User.objects.create(age=[]) # E: Incompatible type for "age" of "User" (got "List[Any]", expected "Union[float, int, str, Combinable]") -[out] - -[CASE model_recognises_parent_attributes] -from django.db import models - -class Parent(models.Model): - name = models.CharField(max_length=100) -class Child(Parent): - lastname = models.CharField(max_length=100) -Child.objects.create(name='Maxim', lastname='Maxim2') -[out] - -[CASE deep_multiple_inheritance_with_create] -from django.db import models - -class Parent1(models.Model): - name1 = models.CharField(max_length=50) -class Parent2(models.Model): - id2 = models.AutoField(primary_key=True) - name2 = models.CharField(max_length=50) - -class Child1(Parent1, Parent2): - value = models.IntegerField() -class Child4(Child1): - value4 = models.IntegerField() -Child4.objects.create(name1='n1', name2='n2', value=1, value4=4) - -[CASE optional_primary_key_for_create_is_error] -from django.db import models -class MyModel(models.Model): - pass -MyModel.objects.create(id=None) # E: Incompatible type for "id" of "MyModel" (got "None", expected "int") - -[CASE optional_related_model_for_create_is_error] -from django.db import models -class Publisher(models.Model): - pass -class Book(models.Model): - publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) -Book.objects.create(publisher=None) # E: Incompatible type for "publisher" of "Book" (got "None", expected "Union[Publisher, Combinable]") - -[CASE when_default_for_primary_key_is_specified_allow_none_to_be_set] -from django.db import models -def return_int(): - return 0 -class MyModel(models.Model): - id = models.IntegerField(primary_key=True, default=return_int) -MyModel(id=None) -MyModel.objects.create(id=None) - -class MyModel2(models.Model): - id = models.IntegerField(primary_key=True, default=None) -MyModel2(id=None) # E: Incompatible type for "id" of "MyModel2" (got "None", expected "Union[float, int, str, Combinable]") -MyModel2.objects.create(id=None) # E: Incompatible type for "id" of "MyModel2" (got "None", expected "Union[float, int, str, Combinable]") -[out] \ No newline at end of file diff --git a/test-data/typecheck/model_init.test b/test-data/typecheck/model_init.test deleted file mode 100644 index 76e9b14d5..000000000 --- a/test-data/typecheck/model_init.test +++ /dev/null @@ -1,177 +0,0 @@ -[CASE arguments_to_init_unexpected_attributes] -from django.db import models - -class MyUser(models.Model): - pass -user = MyUser(name=1, age=12) -[out] -main:5: error: Unexpected attribute "name" for model "MyUser" -main:5: error: Unexpected attribute "age" for model "MyUser" - -[CASE arguments_to_init_from_class_incompatible_type] -from django.db import models - -class MyUser(models.Model): - name = models.CharField(max_length=100) - age = models.IntegerField() -user = MyUser(name='hello', age=[]) -[out] -main:6: error: Incompatible type for "age" of "MyUser" (got "List[Any]", expected "Union[float, int, str, Combinable]") - -[CASE arguments_to_init_combined_from_base_classes] -from django.db import models - -class BaseUser(models.Model): - name = models.CharField(max_length=100) - age = models.IntegerField() -class ChildUser(BaseUser): - lastname = models.CharField(max_length=100) -user = ChildUser(name='Max', age=12, lastname='Lastname') -[out] - -[CASE fields_from_abstract_user_propagate_to_init] -from django.contrib.auth.models import AbstractUser - -class MyUser(AbstractUser): - pass -user = MyUser(username='maxim', password='password', first_name='Max', last_name='MaxMax') -[out] - -[CASE generic_foreign_key_field_no_typechecking] -from django.db import models -from django.contrib.contenttypes.fields import GenericForeignKey - -class MyUser(models.Model): - content_object = GenericForeignKey() - -user = MyUser(content_object=1) -[out] - -[CASE pk_refers_to_primary_key_and_could_be_passed_to_init] -from django.db import models - -class MyUser1(models.Model): - mypk = models.CharField(primary_key=True) -class MyUser2(models.Model): - name = models.CharField(max_length=100) -user2 = MyUser1(pk='hello') -user3= MyUser2(pk=1) -[out] - -[CASE typechecking_of_pk] -from django.db import models - -class MyUser1(models.Model): - mypk = models.IntegerField(primary_key=True) -user = MyUser1(pk=[]) # E: Incompatible type for "pk" of "MyUser1" (got "List[Any]", expected "Union[float, int, str, Combinable]") -[out] - -[CASE can_set_foreign_key_by_its_primary_key] -from django.db import models - -class Publisher(models.Model): - pass -class PublisherDatetime(models.Model): - dt_pk = models.DateTimeField(primary_key=True) -class Book(models.Model): - publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) - publisher_dt = models.ForeignKey(PublisherDatetime, on_delete=models.CASCADE) - -Book(publisher_id=1) -Book(publisher_id=[]) # E: Incompatible type for "publisher_id" of "Book" (got "List[Any]", expected "Union[Combinable, int, str, None]") -Book(publisher_dt_id=11) # E: Incompatible type for "publisher_dt_id" of "Book" (got "int", expected "Union[str, date, Combinable, None]") -[out] - -[CASE setting_value_to_an_array_of_ints] -from typing import List, Tuple - -from django.db import models -from django.contrib.postgres.fields import ArrayField - -class MyModel(models.Model): - array = ArrayField(base_field=models.IntegerField()) -array_val: Tuple[int, ...] = (1,) -MyModel(array=array_val) -array_val2: List[int] = [1] -MyModel(array=array_val2) -array_val3: List[str] = ['hello'] -MyModel(array=array_val3) # E: Incompatible type for "array" of "MyModel" (got "List[str]", expected "Union[Sequence[int], Combinable]") -[out] - -[CASE if_no_explicit_primary_key_id_can_be_passed] -from django.db import models - -class MyModel(models.Model): - name = models.CharField(max_length=100) -MyModel(id=1, name='maxim') -[out] - -[CASE arguments_can_be_passed_as_positionals] -from django.db import models -class MyModel(models.Model): - pass -MyModel(1) - -class MyModel2(models.Model): - name = models.IntegerField() -MyModel2(1, 12) -MyModel2(1, []) # E: Incompatible type for "name" of "MyModel2" (got "List[Any]", expected "Union[float, int, str, Combinable]") -[out] - -[CASE arguments_passed_as_dictionary_unpacking_are_not_supported] -from django.db import models -class MyModel(models.Model): - name = models.CharField(max_length=100) -MyModel(**{'name': 'hello'}) -[out] - -[CASE pointer_to_parent_model_is_not_supported] -from django.db import models -class Place(models.Model): - pass -class Restaurant(Place): - pass -place = Place() -Restaurant(place_ptr=place) -Restaurant(place_ptr_id=place.id) -[out] - -[CASE charfield_with_integer_choices] -from django.db import models -class MyModel(models.Model): - day = models.CharField(max_length=3, choices=((1, 'Fri'), (2, 'Sat'))) -MyModel(day=1) -[out] - -[CASE if_there_is_no_data_for_base_classes_of_fields_and_ignore_unresolved_attributes_set_to_true_to_not_fail] -from decimal import Decimal -from django.db import models -from fields2 import MoneyField - -class InvoiceRow(models.Model): - base_amount = MoneyField(max_digits=10, decimal_places=2) - vat_rate = models.DecimalField(max_digits=10, decimal_places=2) -InvoiceRow(1, Decimal(0), Decimal(0)) -InvoiceRow(base_amount=Decimal(0), vat_rate=Decimal(0)) -InvoiceRow.objects.create(base_amount=Decimal(0), vat_rate=Decimal(0)) -[out] -main:3: error: Cannot find module named 'fields2' -main:3: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports - -[CASE optional_primary_key_is_allowed_for_init] -from django.db import models -class MyModel(models.Model): - pass -MyModel(id=None) -MyModel(None) -[out] - -[CASE optional_related_model_is_allowed_for_init] -from django.db import models -class Publisher(models.Model): - pass -class Book(models.Model): - publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) -Book(publisher=None) -Book(publisher_id=None) -[out] \ No newline at end of file diff --git a/test-data/typecheck/models/test_create.yml b/test-data/typecheck/models/test_create.yml new file mode 100644 index 000000000..37d46bdf8 --- /dev/null +++ b/test-data/typecheck/models/test_create.yml @@ -0,0 +1,113 @@ +- case: default_manager_create_is_typechecked + main: | + from myapp.models import User + User.objects.create(pk=1, name='Max', age=10) + User.objects.create(age=[]) # E: Incompatible type for "age" of "User" (got "List[Any]", expected "Union[float, int, str, Combinable]") + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class User(models.Model): + name = models.CharField(max_length=100) + age = models.IntegerField() + +- case: model_recognises_parent_attributes + main: | + from myapp.models import Child + Child.objects.create(name='Maxim', lastname='Maxim2') + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class Parent(models.Model): + name = models.CharField(max_length=100) + class Child(Parent): + lastname = models.CharField(max_length=100) + +- case: deep_multiple_inheritance_with_create + main: | + from myapp.models import Child4 + Child4.objects.create(name1='n1', name2='n2', value=1, value4=4) + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class Parent1(models.Model): + name1 = models.CharField(max_length=50) + class Parent2(models.Model): + id2 = models.AutoField(primary_key=True) + name2 = models.CharField(max_length=50) + + class Child1(Parent1, Parent2): + value = models.IntegerField() + class Child4(Child1): + value4 = models.IntegerField() + +- case: optional_id_fields_for_create_is_error_if_not_autofield + main: | + from myapp.models import Publisher, Book + + Book.objects.create(id=None) # E: Incompatible type for "id" of "Book" (got "None", expected "Union[float, int, str, Combinable]") + Book.objects.create(publisher=None) # E: Incompatible type for "publisher" of "Book" (got "None", expected "Union[Publisher, Combinable]") + Book.objects.create(publisher_id=None) # E: Incompatible type for "publisher_id" of "Book" (got "None", expected "Union[Combinable, int, str]") + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class Book(models.Model): + id = models.IntegerField(primary_key=True) + publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) + +- case: none_for_primary_key_is_allowed_if_field_is_autogenerated + main: | + from myapp.models import Book + Book.objects.create(id=None) + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Book(models.Model): + pass + +- case: when_default_for_primary_key_is_specified_allow_none_to_be_set + main: | + from myapp.models import MyModel + MyModel(id=None) + MyModel.objects.create(id=None) + + from myapp.models import MyModel2 + MyModel2(id=None) + MyModel2.objects.create(id=None) # E: Incompatible type for "id" of "MyModel2" (got "None", expected "Union[float, int, str, Combinable]") + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + def return_int(): + return 0 + class MyModel(models.Model): + id = models.IntegerField(primary_key=True, default=return_int) + class MyModel2(models.Model): + id = models.IntegerField(primary_key=True) diff --git a/test-data/typecheck/models/test_extra_methods.yml b/test-data/typecheck/models/test_extra_methods.yml new file mode 100644 index 000000000..846e8ab39 --- /dev/null +++ b/test-data/typecheck/models/test_extra_methods.yml @@ -0,0 +1,56 @@ +- case: if_field_has_choices_set_model_has_get_FIELDNAME_display_method + main: | + from myapp.models import MyUser + user = MyUser(name='user', gender='M') + user.get_name_display() # E: "MyUser" has no attribute "get_name_display"; maybe "get_gender_display"? + reveal_type(user.get_gender_display()) # N: Revealed type is 'builtins.str' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + GENDER_CHOICES = ( + ('M', 'Male'), + ('F', 'Female'), + ) + class MyUser(models.Model): + name = models.CharField(max_length=100) + gender = models.CharField(max_length=100, choices=GENDER_CHOICES) + +- case: date_datetime_fields_have_get_next_by_get_previous_by + main: | + from myapp.models import MyUser + reveal_type(MyUser().get_next_by_date()) # N: Revealed type is 'myapp.models.MyUser' + reveal_type(MyUser().get_next_by_datetime()) # N: Revealed type is 'myapp.models.MyUser' + reveal_type(MyUser().get_previous_by_date()) # N: Revealed type is 'myapp.models.MyUser' + reveal_type(MyUser().get_previous_by_datetime()) # N: Revealed type is 'myapp.models.MyUser' + + # accept arbitrary kwargs + MyUser().get_next_by_date(arg1=1, arg2=2) + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + date = models.DateField() + datetime = models.DateTimeField() + +- case: get_next_by_get_previous_by_absent_if_null_true + main: | + from myapp.models import MyUser + MyUser().get_next_by_date() # E: "MyUser" has no attribute "get_next_by_date" + MyUser().get_previous_by_date() # E: "MyUser" has no attribute "get_previous_by_date" + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + date = models.DateField(null=True) diff --git a/test-data/typecheck/models/test_inheritance.yml b/test-data/typecheck/models/test_inheritance.yml new file mode 100644 index 000000000..d31f16b03 --- /dev/null +++ b/test-data/typecheck/models/test_inheritance.yml @@ -0,0 +1,70 @@ +- case: test_meta_nested_class_allows_subclassing_in_multiple_inheritance + main: | + from typing import Any + from django.db import models + class Mixin1(models.Model): + class Meta: + abstract = True + class Mixin2(models.Model): + class Meta: + abstract = True + class User(Mixin1, Mixin2): + pass + +- case: test_inheritance_from_abstract_model_does_not_fail_if_field_with_id_exists + main: | + from django.db import models + class Abstract(models.Model): + class Meta: + abstract = True + class User(Abstract): + id = models.AutoField(primary_key=True) + +- case: test_typechecking_for_model_subclasses + main: | + from myapp.models import A, B, C + def service(a: A) -> int: + pass + b_instance = B() + service(b_instance) # E: Argument 1 to "service" has incompatible type "B"; expected "A" + a_instance = A() + c_instance = C() + service(a_instance) + service(c_instance) + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class A(models.Model): + pass + class B(models.Model): + b_attr = 1 + pass + class C(A): + pass + +- case: fail_if_no_such_attribute_on_model + main: | + from myapp.models import B + b_instance = B() + reveal_type(b_instance.b_attr) # N: Revealed type is 'builtins.int' + + reveal_type(b_instance.non_existent_attribute) + b_instance.non_existent_attribute = 2 + out: | + main:5: note: Revealed type is 'Any' + main:5: error: "B" has no attribute "non_existent_attribute" + main:6: error: "B" has no attribute "non_existent_attribute" + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class B(models.Model): + b_attr = 1 + pass \ No newline at end of file diff --git a/test-data/typecheck/models/test_init.yml b/test-data/typecheck/models/test_init.yml new file mode 100644 index 000000000..b42ec666e --- /dev/null +++ b/test-data/typecheck/models/test_init.yml @@ -0,0 +1,235 @@ +- case: arguments_to_init_unexpected_attributes + main: | + from myapp.models import MyUser + user = MyUser(name=1, age=12) + out: | + main:2: error: Unexpected attribute "name" for model "MyUser" + main:2: error: Unexpected attribute "age" for model "MyUser" + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class MyUser(models.Model): + pass + +- case: plain_function_which_returns_model + main: | + from myapp.models import MyUser + def func(i: int) -> MyUser: + pass + func("hello") # E: Argument 1 to "func" has incompatible type "str"; expected "int" + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + pass + +- case: arguments_to_init_from_class_incompatible_type + main: | + from myapp.models import MyUser + user = MyUser(name='hello', age=[]) + out: | + main:2: error: Incompatible type for "age" of "MyUser" (got "List[Any]", expected "Union[float, int, str, Combinable]") + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class MyUser(models.Model): + name = models.CharField(max_length=100) + age = models.IntegerField() + +- case: arguments_to_init_combined_from_base_classes + main: | + from myapp.models import BaseUser, ChildUser + user = ChildUser(name='Max', age=12, lastname='Lastname') + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class BaseUser(models.Model): + name = models.CharField(max_length=100) + age = models.IntegerField() + class ChildUser(BaseUser): + lastname = models.CharField(max_length=100) + +- case: fields_from_abstract_user_propagate_to_init + main: | + from myapp.models import MyUser + user = MyUser(name='Maxim') + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class AbstractUser(models.Model): + class Meta: + abstract = True + name = models.CharField(max_length=100) + class MyUser(AbstractUser): + pass + +- case: pk_refers_to_primary_key_and_could_be_passed_to_init + main: | + from myapp.models import MyUser1, MyUser2 + user2 = MyUser1(pk='hello') + user3 = MyUser2(pk=1, name='maxim') + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class MyUser1(models.Model): + mypk = models.CharField(primary_key=True) + class MyUser2(models.Model): + name = models.CharField(max_length=100) + +- case: typechecking_of_pk + main: | + from myapp.models import MyUser1 + user = MyUser1(pk=[]) # E: Incompatible type for "pk" of "MyUser1" (got "List[Any]", expected "Union[float, int, str, Combinable, None]") + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class MyUser1(models.Model): + mypk = models.IntegerField(primary_key=True) + +- case: set_foreign_key_by_its_primary_key + main: | + from datetime import datetime + now = datetime.now() + + from myapp.models import Publisher, PublisherDatetime, Book + Book(publisher_id=1, publisher_dt_id=now) + Book(publisher_id=[], publisher_dt_id=now) # E: Incompatible type for "publisher_id" of "Book" (got "List[Any]", expected "Union[Combinable, int, str, None]") + Book(publisher_id=1, publisher_dt_id=1) # E: Incompatible type for "publisher_dt_id" of "Book" (got "int", expected "Union[str, date, Combinable, None]") + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class Publisher(models.Model): + pass + class PublisherDatetime(models.Model): + dt_pk = models.DateTimeField(primary_key=True) + class Book(models.Model): + publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) + publisher_dt = models.ForeignKey(PublisherDatetime, on_delete=models.CASCADE) + +- case: setting_value_to_an_array_of_ints + main: | + from typing import List, Tuple + from myapp.models import MyModel + array_val: Tuple[int, ...] = (1,) + MyModel(array=array_val) + array_val2: List[int] = [1] + MyModel(array=array_val2) + class NotAValid: + pass + array_val3: List[NotAValid] = [NotAValid()] + MyModel(array=array_val3) # E: Incompatible type for "array" of "MyModel" (got "List[NotAValid]", expected "Union[Sequence[Union[float, int, str, Combinable]], Combinable]") + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from typing import List, Tuple + + from django.db import models + from django.contrib.postgres.fields import ArrayField + + class MyModel(models.Model): + array = ArrayField(base_field=models.IntegerField()) + +- case: if_no_explicit_primary_key_id_can_be_passed + main: | + from myapp.models import MyModel + MyModel(id=1, name='maxim') + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + name = models.CharField(max_length=100) + +- case: arguments_can_be_passed_as_positionals + main: | + from myapp.models import MyModel, MyModel2 + MyModel(1) + MyModel2(1, 12) + MyModel2(1, []) # E: Incompatible type for "name" of "MyModel2" (got "List[Any]", expected "Union[float, int, str, Combinable]") + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + pass + class MyModel2(models.Model): + name = models.IntegerField() + +- case: charfield_with_integer_choices + main: | + from myapp.models import MyModel + MyModel(day=1) + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + day = models.CharField(max_length=3, choices=((1, 'Fri'), (2, 'Sat'))) + +- case: optional_id_fields_allowed_in_init + main: | + from myapp.models import Book, Publisher + Book(id=None) + Book(publisher=None) + Book(publisher_id=None) + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + name = models.CharField(primary_key=True, max_length=100) + class Book(models.Model): + publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) diff --git a/test-data/typecheck/models/test_meta_options.yml b/test-data/typecheck/models/test_meta_options.yml new file mode 100644 index 000000000..ea7e29841 --- /dev/null +++ b/test-data/typecheck/models/test_meta_options.yml @@ -0,0 +1,38 @@ +- case: meta_attribute_has_a_type_of_current_model + main: | + from myapp.models import MyUser + reveal_type(MyUser._meta) # N: Revealed type is 'django.db.models.options.Options[myapp.models.MyUser]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + pass + +- case: get_field_returns_proper_field_type + main: | + from myapp.models import MyUser + reveal_type(MyUser._meta.get_field('base_name')) # N: Revealed type is 'django.db.models.fields.CharField[Any, Any]' + reveal_type(MyUser._meta.get_field('name')) # N: Revealed type is 'django.db.models.fields.CharField[Any, Any]' + reveal_type(MyUser._meta.get_field('age')) # N: Revealed type is 'django.db.models.fields.IntegerField[Any, Any]' + reveal_type(MyUser._meta.get_field('unknown')) + reveal_type(MyUser._meta.get_field('to_user')) # N: Revealed type is 'django.db.models.fields.related.ForeignKey[Any, Any]' + out: | + main:5: note: Revealed type is 'Any' + main:5: error: MyUser has no field named 'unknown' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyBaseUser(models.Model): + base_name = models.CharField(max_length=100) + class MyUser(MyBaseUser): + name = models.CharField(max_length=100) + age = models.IntegerField() + to_user = models.ForeignKey('self', on_delete=models.SET_NULL) diff --git a/test-data/typecheck/models/test_proxy_models.yml b/test-data/typecheck/models/test_proxy_models.yml new file mode 100644 index 000000000..ba46845ab --- /dev/null +++ b/test-data/typecheck/models/test_proxy_models.yml @@ -0,0 +1,21 @@ +- case: foreign_key_to_proxy_model_accepts_first_non_proxy_model + main: | + from myapp.models import Blog, Publisher, PublisherProxy + Blog(publisher=Publisher()) + Blog.objects.create(publisher=Publisher()) + Blog().publisher = Publisher() + reveal_type(Blog().publisher) # N: Revealed type is 'myapp.models.PublisherProxy*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class Publisher(models.Model): + pass + class PublisherProxy(Publisher): + class Meta: + proxy = True + class Blog(models.Model): + publisher = models.ForeignKey(to=PublisherProxy, on_delete=models.CASCADE) diff --git a/test-data/typecheck/nullable_fields.test b/test-data/typecheck/nullable_fields.test deleted file mode 100644 index d6a33be55..000000000 --- a/test-data/typecheck/nullable_fields.test +++ /dev/null @@ -1,42 +0,0 @@ -[CASE nullable_field_with_strict_optional_true] -from django.db import models -class MyModel(models.Model): - text_nullable = models.CharField(max_length=100, null=True) - text = models.CharField(max_length=100) -reveal_type(MyModel().text) # N: Revealed type is 'builtins.str*' -reveal_type(MyModel().text_nullable) # N: Revealed type is 'Union[builtins.str, None]' -MyModel().text = None # E: Incompatible types in assignment (expression has type "None", variable has type "Union[str, int, Combinable]") -MyModel().text_nullable = None -[out] - -[CASE nullable_array_field] -from django.db import models -from django.contrib.postgres.fields import ArrayField - -class MyModel(models.Model): - lst = ArrayField(base_field=models.CharField(max_length=100), null=True) -reveal_type(MyModel().lst) # N: Revealed type is 'Union[builtins.list[builtins.str], None]' -[out] - -[CASE nullable_foreign_key] -from django.db import models - -class Publisher(models.Model): - pass -class Book(models.Model): - publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, null=True) -reveal_type(Book().publisher) # N: Revealed type is 'Union[main.Publisher, None]' -Book().publisher = 11 # E: Incompatible types in assignment (expression has type "int", variable has type "Union[Publisher, Combinable, None]") -[out] - -[CASE nullable_self_foreign_key] -from django.db import models -class Inventory(models.Model): - parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True) -parent = Inventory() -core = Inventory(parent_id=parent.id) -reveal_type(core.parent_id) # N: Revealed type is 'Union[builtins.int, None]' -reveal_type(core.parent) # N: Revealed type is 'Union[main.Inventory, None]' -Inventory(parent=None) -Inventory(parent_id=None) -[out] \ No newline at end of file diff --git a/test-data/typecheck/queryset.test b/test-data/typecheck/queryset.test deleted file mode 100644 index 794893f25..000000000 --- a/test-data/typecheck/queryset.test +++ /dev/null @@ -1,389 +0,0 @@ -[CASE test_queryset_second_argument_filled_automatically] -from django.db import models - -class Blog(models.Model): pass - -# QuerySet where second type argument is not specified shouldn't raise any errors -class BlogQuerySet(models.QuerySet[Blog]): - pass - -blog_qs: models.QuerySet[Blog] -reveal_type(blog_qs) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog, main.Blog]' -[/CASE] - - -[CASE test_queryset_methods] -from django.db import models - -class Blog(models.Model): - created_at = models.DateTimeField() - -qs = Blog.objects.all() -reveal_type(qs) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, main.Blog*]' -reveal_type(qs.get(id=1)) # N: Revealed type is 'main.Blog*' -reveal_type(iter(qs)) # N: Revealed type is 'typing.Iterator[main.Blog*]' -reveal_type(qs.iterator()) # N: Revealed type is 'typing.Iterator[main.Blog*]' -reveal_type(qs.first()) # N: Revealed type is 'Union[main.Blog*, None]' -reveal_type(qs.earliest()) # N: Revealed type is 'main.Blog*' -reveal_type(qs[0]) # N: Revealed type is 'main.Blog*' -reveal_type(qs[:9]) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, main.Blog*]' -reveal_type(qs.in_bulk()) # N: Revealed type is 'builtins.dict[Any, main.Blog*]' - -# .dates / .datetimes -reveal_type(Blog.objects.dates("created_at", "day")) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, datetime.date]' -reveal_type(Blog.objects.datetimes("created_at", "day")) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, datetime.datetime]' -[/CASE] - - -[CASE test_combine_querysets_with_and] -from django.db import models - -class Blog(models.Model): - name = models.CharField(max_length=100) - created_at = models.DateTimeField() - -# When ANDing QuerySets, the left-side's _Row parameter is used -reveal_type(Blog.objects.all() & Blog.objects.values()) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, main.Blog*]' -reveal_type(Blog.objects.values() & Blog.objects.values()) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.dict*[builtins.str, Any]]' -reveal_type(Blog.objects.values_list('id', 'name') & Blog.objects.values()) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, builtins.str]]' -reveal_type(Blog.objects.values_list('id', 'name', named=True) & Blog.objects.values()) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, builtins.str, fallback=main.Row]]' -reveal_type(Blog.objects.values_list('id', flat=True) & Blog.objects.values()) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.int*]' -[/CASE] - - -[CASE test_queryset_values_method] -from django.db import models - -class Blog(models.Model): pass - -values_qs = Blog.objects.values() -reveal_type(values_qs) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.dict[builtins.str, Any]]' -reveal_type(values_qs.all()) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.dict*[builtins.str, Any]]' -reveal_type(values_qs.get(id=1)) # N: Revealed type is 'builtins.dict*[builtins.str, Any]' -reveal_type(iter(values_qs)) # N: Revealed type is 'typing.Iterator[builtins.dict*[builtins.str, Any]]' -reveal_type(values_qs.iterator()) # N: Revealed type is 'typing.Iterator[builtins.dict*[builtins.str, Any]]' -reveal_type(values_qs.first()) # N: Revealed type is 'Union[builtins.dict*[builtins.str, Any], None]' -reveal_type(values_qs.earliest()) # N: Revealed type is 'builtins.dict*[builtins.str, Any]' -reveal_type(values_qs[0]) # N: Revealed type is 'builtins.dict*[builtins.str, Any]' -reveal_type(values_qs[:9]) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.dict*[builtins.str, Any]]' -reveal_type(values_qs.in_bulk()) # N: Revealed type is 'builtins.dict[Any, main.Blog*]' -[/CASE] - - -[CASE test_queryset_values_list_named_false_flat_false] -from django.db import models - -class Blog(models.Model): - name = models.CharField(max_length=100) - -values_list_qs = Blog.objects.values_list('id', 'name') -reveal_type(values_list_qs) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, builtins.str]]' -reveal_type(values_list_qs.all()) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, builtins.str]]' -reveal_type(values_list_qs.get(id=1)) # N: Revealed type is 'Tuple[builtins.int, builtins.str]' -reveal_type(iter(values_list_qs)) # N: Revealed type is 'typing.Iterator[Tuple[builtins.int, builtins.str]]' -reveal_type(values_list_qs.iterator()) # N: Revealed type is 'typing.Iterator[Tuple[builtins.int, builtins.str]]' -reveal_type(values_list_qs.first()) # N: Revealed type is 'Union[Tuple[builtins.int, builtins.str], None]' -reveal_type(values_list_qs.earliest()) # N: Revealed type is 'Tuple[builtins.int, builtins.str]' -reveal_type(values_list_qs[0]) # N: Revealed type is 'Tuple[builtins.int, builtins.str]' -reveal_type(values_list_qs[:9]) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, builtins.str]]' -reveal_type(values_list_qs.in_bulk()) # N: Revealed type is 'builtins.dict[Any, main.Blog*]' -[/CASE] - - -[CASE test_queryset_values_list_named_false_flat_true] -from django.db import models - -class Blog(models.Model): - name = models.CharField(max_length=100) - -flat_values_list_qs = Blog.objects.values_list('id', flat=True) -reveal_type(flat_values_list_qs) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.int]' -reveal_type(flat_values_list_qs.all()) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.int*]' -reveal_type(flat_values_list_qs.get(id=1)) # N: Revealed type is 'builtins.int*' -reveal_type(iter(flat_values_list_qs)) # N: Revealed type is 'typing.Iterator[builtins.int*]' -reveal_type(flat_values_list_qs.iterator()) # N: Revealed type is 'typing.Iterator[builtins.int*]' -reveal_type(flat_values_list_qs.first()) # N: Revealed type is 'Union[builtins.int*, None]' -reveal_type(flat_values_list_qs.earliest()) # N: Revealed type is 'builtins.int*' -reveal_type(flat_values_list_qs[0]) # N: Revealed type is 'builtins.int*' -reveal_type(flat_values_list_qs[:9]) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, builtins.int*]' -reveal_type(flat_values_list_qs.in_bulk()) # N: Revealed type is 'builtins.dict[Any, main.Blog*]' -[/CASE] - - -[CASE test_queryset_values_list_named_true_flat_false] -from django.db import models - -class Blog(models.Model): - name = models.CharField(max_length=100) - -named_values_list_qs = Blog.objects.values_list('id', named=True) -reveal_type(named_values_list_qs) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, fallback=main.Row]]' -reveal_type(named_values_list_qs.all()) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, fallback=main.Row]]' -reveal_type(named_values_list_qs.get(id=1)) # N: Revealed type is 'Tuple[builtins.int, fallback=main.Row]' -reveal_type(iter(named_values_list_qs)) # N: Revealed type is 'typing.Iterator[Tuple[builtins.int, fallback=main.Row]]' -reveal_type(named_values_list_qs.iterator()) # N: Revealed type is 'typing.Iterator[Tuple[builtins.int, fallback=main.Row]]' -reveal_type(named_values_list_qs.first()) # N: Revealed type is 'Union[Tuple[builtins.int, fallback=main.Row], None]' -reveal_type(named_values_list_qs.earliest()) # N: Revealed type is 'Tuple[builtins.int, fallback=main.Row]' -reveal_type(named_values_list_qs[0]) # N: Revealed type is 'Tuple[builtins.int, fallback=main.Row]' -reveal_type(named_values_list_qs[:9]) # N: Revealed type is 'django.db.models.query.QuerySet[main.Blog*, Tuple[builtins.int, fallback=main.Row]]' -reveal_type(named_values_list_qs.in_bulk()) # N: Revealed type is 'builtins.dict[Any, main.Blog*]' -[/CASE] - - -[CASE test_queryset_values_list_flat_true_custom_primary_key_get_element] -from django.db import models - -class Blog(models.Model): - primary_uuid = models.UUIDField(primary_key=True) - -# Blog has a primary key field specified, so no automatic 'id' field is expected to exist -reveal_type(Blog.objects.values_list('id', flat=True).get()) # N: Revealed type is 'Any' - -# Access Blog's pk (which is UUID field) -reveal_type(Blog.objects.values_list('pk', flat=True).get()) # N: Revealed type is 'uuid.UUID*' -[/CASE] - - -[CASE test_queryset_values_list_flat_true_custom_primary_key_related_field] -from django.db import models - -class Blog(models.Model): - primary_uuid = models.UUIDField(primary_key=True) - -class Entry(models.Model): - blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name="entries") - -# Accessing PK of model pointed to by foreign key -reveal_type(Entry.objects.values_list('blog', flat=True).get()) # N: Revealed type is 'uuid.UUID*' -# Alternative way of accessing PK of model pointed to by foreign key -reveal_type(Entry.objects.values_list('blog_id', flat=True).get()) # N: Revealed type is 'uuid.UUID*' -# Yet another (more explicit) way of accessing PK of related model -reveal_type(Entry.objects.values_list('blog__pk', flat=True).get()) # N: Revealed type is 'uuid.UUID*' - -# Blog has a primary key field specified, so no automatic 'id' field is expected to exist -reveal_type(Entry.objects.values_list('blog__id', flat=True).get()) # N: Revealed type is 'Any' -[/CASE] - - -[CASE test_queryset_values_list_error_conditions] -from django.db import models - -class Blog(models.Model): - name = models.CharField(max_length=100) - -# Emulate at type-check time the errors that Django reports -Blog.objects.values_list('id', flat=True, named=True) # E: 'flat' and 'named' can't be used together. -Blog.objects.values_list('id', 'name', flat=True) # E: 'flat' is not valid when values_list is called with more than one field. -[/CASE] - - -[CASE test_queryset_values_list_returns_tuple_of_fields] -from django.db import models - -class Blog(models.Model): - name = models.CharField(max_length=100) - created_at = models.DateTimeField() - -# values_list where parameter types are all known -reveal_type(Blog.objects.values_list('id', 'created_at').get()) # N: Revealed type is 'Tuple[builtins.int, datetime.datetime]' -tup = Blog.objects.values_list('id', 'created_at').get() -reveal_type(tup[0]) # N: Revealed type is 'builtins.int' -reveal_type(tup[1]) # N: Revealed type is 'datetime.datetime' -tup[2] # E: Tuple index out of range - -# values_list returning namedtuple -reveal_type(Blog.objects.values_list('id', 'created_at', named=True).get()) # N: Revealed type is 'Tuple[builtins.int, datetime.datetime, fallback=main.Row]' -[/CASE] - - -[CASE test_queryset_values_list_invalid_lookups_produce_any] -from django.db import models - -class Blog(models.Model): pass -class Entry(models.Model): - blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name="entries") - -# Invalid lookups produce Any type rather than giving errors. -reveal_type(Blog.objects.values_list('id', 'invalid_lookup').get()) # N: Revealed type is 'Tuple[builtins.int, Any]' -reveal_type(Blog.objects.values_list('entries_id', flat=True).get()) # N: Revealed type is 'Any' -reveal_type(Blog.objects.values_list('entries__foo', flat=True).get()) # N: Revealed type is 'Any' -reveal_type(Blog.objects.values_list('+', flat=True).get()) # N: Revealed type is 'Any' -[/CASE] - - -[CASE test_queryset_values_list_basic_inheritance] -from django.db import models - -class Blog(models.Model): - name = models.CharField(max_length=100) - created_at = models.DateTimeField() - -class BlogChild(Blog): - child_field = models.CharField(max_length=100) - -# Basic inheritance -reveal_type(BlogChild.objects.values_list('id', 'created_at', 'child_field').get()) # N: Revealed type is 'Tuple[builtins.int, datetime.datetime, builtins.str]' -[/CASE] - - -[CASE test_query_values_list_flat_true_plain_foreign_key] -from django.db import models - -class Blog(models.Model): pass -class Entry(models.Model): - blog = models.ForeignKey(Blog, on_delete=models.CASCADE) - -# Foreign key -reveal_type(Entry.objects.values_list('blog', flat=True).get()) # N: Revealed type is 'builtins.int*' -reveal_type(Entry.objects.values_list('blog__id', flat=True).get()) # N: Revealed type is 'builtins.int*' -reveal_type(Entry.objects.values_list('blog__pk', flat=True).get()) # N: Revealed type is 'builtins.int*' -reveal_type(Entry.objects.values_list('blog_id', flat=True).get()) # N: Revealed type is 'builtins.int*' -[/CASE] - - -[CASE test_query_values_list_flat_true_custom_primary_key] -from django.db import models - -class Blog(models.Model): - id = models.UUIDField(primary_key=True) - -class Entry(models.Model): - blog = models.ForeignKey(Blog, on_delete=models.CASCADE) - -# Foreign key -reveal_type(Entry.objects.values_list('blog', flat=True).get()) # N: Revealed type is 'uuid.UUID*' -reveal_type(Entry.objects.values_list('blog__id', flat=True).get()) # N: Revealed type is 'uuid.UUID*' -reveal_type(Entry.objects.values_list('blog__pk', flat=True).get()) # N: Revealed type is 'uuid.UUID*' -reveal_type(Entry.objects.values_list('blog_id', flat=True).get()) # N: Revealed type is 'uuid.UUID*' -[/CASE] - - -[CASE test_query_values_list_flat_true_nullable_foreign_key] -from django.db import models - -class Blog(models.Model): pass -class Entry(models.Model): - nullable_blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name="+", null=True) - -# Foreign key (nullable=True) -reveal_type(Entry.objects.values_list('nullable_blog', flat=True).get()) # N: Revealed type is 'Union[builtins.int, None]' -reveal_type(Entry.objects.values_list('nullable_blog_id', flat=True).get()) # N: Revealed type is 'Union[builtins.int, None]' -reveal_type(Entry.objects.values_list('nullable_blog__id', flat=True).get()) # N: Revealed type is 'Union[builtins.int, None]' -reveal_type(Entry.objects.values_list('nullable_blog__pk', flat=True).get()) # N: Revealed type is 'Union[builtins.int, None]' -[/CASE] - - -[CASE test_query_values_list_flat_true_foreign_key_reverse_relation] -from django.db import models - -class Blog(models.Model): pass -class Entry(models.Model): - blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name='entries') - blog_with_related_query_name = models.ForeignKey(Blog, on_delete=models.CASCADE, related_query_name="my_related_query_name") - title = models.CharField(max_length=100) - -# Reverse relation of ForeignKey -reveal_type(Blog.objects.values_list('entries', flat=True).get()) # N: Revealed type is 'builtins.int*' -reveal_type(Blog.objects.values_list('entries__id', flat=True).get()) # N: Revealed type is 'builtins.int*' -reveal_type(Blog.objects.values_list('entries__title', flat=True).get()) # N: Revealed type is 'builtins.str*' - -# Reverse relation of ForeignKey (with related_query_name set) -reveal_type(Blog.objects.values_list('my_related_query_name__id', flat=True).get()) # N: Revealed type is 'builtins.int*' -[/CASE] - - -[CASE test_query_values_list_flat_true_foreign_key_custom_primary_key_reverse_relation] -from django.db import models - -class Blog(models.Model): pass - -class Entry(models.Model): - id = models.UUIDField(primary_key=True) - blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name='entries') - blog_with_related_query_name = models.ForeignKey(Blog, on_delete=models.CASCADE, related_query_name="my_related_query_name") - title = models.CharField(max_length=100) - -# Reverse relation of ForeignKey -reveal_type(Blog.objects.values_list('entries', flat=True).get()) # N: Revealed type is 'uuid.UUID*' -reveal_type(Blog.objects.values_list('entries__id', flat=True).get()) # N: Revealed type is 'uuid.UUID*' - -# Reverse relation of ForeignKey (with related_query_name set) -reveal_type(Blog.objects.values_list('my_related_query_name__id', flat=True).get()) # N: Revealed type is 'uuid.UUID*' -[/CASE] - - -[CASE test_queryset_values_list_and_values_behavior_with_no_fields_specified_and_accessing_unknown_attributes] -from django.db import models - -class Blog(models.Model): - name = models.CharField(max_length=100) - created_at = models.DateTimeField() - -row_named = Blog.objects.values_list('id', 'created_at', named=True).get() -reveal_type(row_named.id) # N: Revealed type is 'builtins.int' -reveal_type(row_named.created_at) # N: Revealed type is 'datetime.datetime' -row_named.non_existent_field # E: "Row" has no attribute "non_existent_field" - - -# When no fields are specified, fallback to Any -row_named_no_fields = Blog.objects.values_list(named=True).get() -reveal_type(row_named_no_fields) # N: Revealed type is 'Tuple[, fallback=django._NamedTupleAnyAttr]' - -# Don't complain about access to any attribute for now -reveal_type(row_named_no_fields.non_existent_field) # N: Revealed type is 'Any' -row_named_no_fields.non_existent_field = 1 - -# It should still behave like a NamedTuple -reveal_type(row_named_no_fields._asdict()) # N: Revealed type is 'builtins.dict[builtins.str, Any]' - - -dict_row = Blog.objects.values('id', 'created_at').get() -reveal_type(dict_row["id"]) # N: Revealed type is 'builtins.int' -reveal_type(dict_row["created_at"]) # N: Revealed type is 'datetime.datetime' -dict_row["non_existent_field"] # E: 'non_existent_field' is not a valid TypedDict key; expected one of ('id', 'created_at') -dict_row.pop('created_at') -dict_row.pop('non_existent_field') # E: 'non_existent_field' is not a valid TypedDict key; expected one of ('id', 'created_at') - -row_dict_no_fields = Blog.objects.values().get() -reveal_type(row_dict_no_fields) # N: Revealed type is 'builtins.dict*[builtins.str, Any]' -reveal_type(row_dict_no_fields["non_existent_field"]) # N: Revealed type is 'Any' - -[CASE values_with_annotate_inside_the_expressions] -from django.db import models -from django.db.models.functions import Lower, Upper - -class Publisher(models.Model): - pass - -class Book(models.Model): - name = models.CharField(max_length=100) - publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, related_name='books') - -reveal_type(Publisher().books.values('name', lower_name=Lower('name'), upper_name=Upper('name'))) # N: Revealed type is 'django.db.models.query.QuerySet[main.Book*, TypedDict({'name'?: builtins.str, 'lower_name'?: Any, 'upper_name'?: Any})]' - - -[CASE values_and_values_list_some_dynamic_fields] -from django.db import models - -class Publisher(models.Model): - pass - -class Book(models.Model): - name = models.CharField(max_length=100) - publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, related_name='books') - -some_dynamic_field = 'publisher' - -# Correct Tuple field types should be filled in when string literal is used, while Any is used for dynamic fields -reveal_type(Publisher().books.values_list('name', some_dynamic_field)) # N: Revealed type is 'django.db.models.query.QuerySet[main.Book*, Tuple[builtins.str, Any]]' - -# Flat with dynamic fields (there is only 1), means of course Any -reveal_type(Publisher().books.values_list(some_dynamic_field, flat=True)) # N: Revealed type is 'django.db.models.query.QuerySet[main.Book*, Any]' - -# A NamedTuple with a fallback to Any could be implemented, but for now that's unsupported, so all -# fields on the NamedTuple are Any for now -reveal_type(Publisher().books.values_list('name', some_dynamic_field, named=True).name) # N: Revealed type is 'Any' - -# A TypedDict with a fallback to Any could be implemented, but for now that's unsupported, -# so an ordinary Dict is used for now. -reveal_type(Publisher().books.values(some_dynamic_field, 'name')) # N: Revealed type is 'django.db.models.query.QuerySet[main.Book*, builtins.dict[builtins.str, Any]]' diff --git a/test-data/typecheck/related_fields.test b/test-data/typecheck/related_fields.test deleted file mode 100644 index 57e2299e1..000000000 --- a/test-data/typecheck/related_fields.test +++ /dev/null @@ -1,318 +0,0 @@ -[CASE test_foreign_key_field_with_related_name] -from django.db import models - -class Publisher(models.Model): - pass - -class Book(models.Model): - publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, - related_name='books') - -book = Book() -reveal_type(book.publisher) # N: Revealed type is 'main.Publisher*' - -publisher = Publisher() -reveal_type(publisher.books) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.Book]' - -[CASE test_foreign_key_field_creates_attribute_with_underscore_id] -from django.db import models - -class Publisher(models.Model): - pass - -class Book(models.Model): - publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) - owner = models.ForeignKey(db_column='model_id', to='db.Unknown', on_delete=models.CASCADE) - -book = Book() -reveal_type(book.publisher_id) # N: Revealed type is 'builtins.int' -reveal_type(book.owner_id) # N: Revealed type is 'Any' - -[CASE test_foreign_key_field_different_order_of_params] -from django.db import models - -class Publisher(models.Model): - pass - -class Book(models.Model): - publisher = models.ForeignKey(on_delete=models.CASCADE, to=Publisher, - related_name='books') - publisher2 = models.ForeignKey(to=Publisher, related_name='books2', on_delete=models.CASCADE) - -book = Book() -reveal_type(book.publisher) # N: Revealed type is 'main.Publisher*' -reveal_type(book.publisher2) # N: Revealed type is 'main.Publisher*' - -publisher = Publisher() -reveal_type(publisher.books) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.Book]' -reveal_type(publisher.books2) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.Book]' - -[CASE test_to_parameter_as_string_with_application_name__model_imported] -from django.db import models -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from myapp.models import Publisher - -class Book(models.Model): - publisher = models.ForeignKey(to='myapp.Publisher', on_delete=models.CASCADE) - -book = Book() -reveal_type(book.publisher) # N: Revealed type is 'myapp.models.Publisher*' - -[file myapp/__init__.py] -[file myapp/models.py] -from django.db import models -class Publisher(models.Model): - pass - -[CASE test_to_parameter_as_string_with_application_name_fallbacks_to_any_if_model_not_present_in_dependency_graph] -from django.db import models - -class Book(models.Model): - publisher = models.ForeignKey(to='myapp.Publisher', on_delete=models.CASCADE) - -book = Book() -reveal_type(book.publisher) # N: Revealed type is 'Any' -reveal_type(book.publisher_id) # N: Revealed type is 'Any' -Book(publisher_id=1) -Book.objects.create(publisher_id=1) - -[file myapp/__init__.py] -[file myapp/models.py] -from django.db import models -class Publisher(models.Model): - pass - -[CASE test_circular_dependency_in_imports_with_foreign_key] -from django.db import models - -class App(models.Model): - def method(self) -> None: - reveal_type(self.views) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.View]' - reveal_type(self.members) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.Member]' - reveal_type(self.sheets) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.Sheet]' - reveal_type(self.profile) # N: Revealed type is 'main.Profile' -class View(models.Model): - app = models.ForeignKey(to=App, related_name='views', on_delete=models.CASCADE) -class Member(models.Model): - app = models.ForeignKey(related_name='members', on_delete=models.CASCADE, to=App) -class Sheet(models.Model): - app = models.ForeignKey(App, related_name='sheets', on_delete=models.CASCADE) -class Profile(models.Model): - app = models.OneToOneField(App, related_name='profile', on_delete=models.CASCADE) - -[CASE test_circular_dependency_in_imports_with_string_based] -from django.db import models -from myapp.models import App -class View(models.Model): - app = models.ForeignKey(to=App, related_name='views', on_delete=models.CASCADE) - -reveal_type(View().app.views) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.View]' -reveal_type(View().app.unknown) -[out] -main:7: note: Revealed type is 'Any' -main:7: error: "App" has no attribute "unknown" - -[file myapp/__init__.py] -[file myapp/models.py] -from django.db import models -class App(models.Model): - def method(self) -> None: - reveal_type(self.views) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.View]' - -[CASE models_related_managers_work_with_direct_model_inheritance_and_with_inheritance_from_other_model] -from django.db.models import Model -from django.db import models - -class App(Model): - pass - -class View(Model): - app = models.ForeignKey(to=App, on_delete=models.CASCADE, related_name='views') - -class View2(View): - app = models.ForeignKey(to=App, on_delete=models.CASCADE, related_name='views2') - -reveal_type(App().views) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.View]' -reveal_type(App().views2) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.View2]' -[out] - -[CASE models_imported_inside_init_file_foreign_key] -[disable_cache] -from django.db import models -from myapp.models import App -class View(models.Model): - app = models.ForeignKey(to='myapp.App', related_name='views', on_delete=models.CASCADE) -reveal_type(View().app.views) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.View]' - -[file myapp/__init__.py] -[file myapp/models/__init__.py] -from .app import App -[file myapp/models/app.py] -from django.db import models -class App(models.Model): - pass -[/CASE] - -[CASE models_imported_inside_init_file_one_to_one_field] -[disable_cache] -from django.db import models -from myapp.models import User -class Profile(models.Model): - user = models.OneToOneField(to='myapp.User', related_name='profile', on_delete=models.CASCADE) -reveal_type(Profile().user.profile) # N: Revealed type is 'main.Profile' - -[file myapp/__init__.py] -[file myapp/models/__init__.py] -from .user import User -[file myapp/models/user.py] -from django.db import models -class User(models.Model): - pass - -[CASE models_triple_circular_reference] -from myapp.models import App -reveal_type(App().owner.profile) # N: Revealed type is 'myapp.models.profile.Profile' - -[file myapp/__init__.py] -[file myapp/models/__init__.py] -from .user import User -from .profile import Profile -from .app import App - -[file myapp/models/user.py] -from django.db import models -class User(models.Model): - pass - -[file myapp/models/profile.py] -from django.db import models -from myapp.models import User -class Profile(models.Model): - user = models.OneToOneField(to='myapp.User', related_name='profile', on_delete=models.CASCADE) - -[file myapp/models/app.py] -from django.db import models -class App(models.Model): - owner = models.ForeignKey(to='myapp.User', on_delete=models.CASCADE, related_name='apps') -[disable_cache] -[/CASE] - -[CASE many_to_many_field_converts_to_queryset_of_model_type] -from django.db import models -class App(models.Model): - pass -class Member(models.Model): - apps = models.ManyToManyField(to=App, related_name='members') -reveal_type(Member().apps) # N: Revealed type is 'django.db.models.manager.RelatedManager*[main.App]' -reveal_type(App().members) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.Member]' -[out] - -[CASE many_to_many_works_with_string_if_imported] -from django.db import models -from myapp.models import App -class Member(models.Model): - apps = models.ManyToManyField(to='myapp.App', related_name='members') -reveal_type(Member().apps) # N: Revealed type is 'django.db.models.manager.RelatedManager*[myapp.models.App]' - -[file myapp/__init__.py] -[file myapp/models.py] -from django.db import models -class App(models.Model): - pass -[out] - -[CASE foreign_key_with_self] -from django.db import models -class User(models.Model): - parent = models.ForeignKey('self', on_delete=models.CASCADE) -reveal_type(User().parent) # N: Revealed type is 'main.User*' -[out] - -[CASE many_to_many_with_self] -from django.db import models -class User(models.Model): - friends = models.ManyToManyField('self') -reveal_type(User().friends) # N: Revealed type is 'django.db.models.manager.RelatedManager*[main.User]' -[out] - -[CASE recursively_checking_for_base_model_in_to_parameter] -from django.db import models - -class BaseModel(models.Model): - pass -class ParkingSpot(BaseModel): - pass -class Booking(BaseModel): - parking_spot = models.ForeignKey(to=ParkingSpot, null=True, on_delete=models.SET_NULL) -[out] - -[CASE if_no_related_name_is_passed_create_default_related_managers] -from django.db import models -class Publisher(models.Model): - pass -class Book(models.Model): - publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) -reveal_type(Publisher().book_set) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.Book]' - -[CASE underscore_id_attribute_has_set_type_of_primary_key_if_explicit] -from django.db import models -import datetime -class Publisher(models.Model): - mypk = models.CharField(max_length=100, primary_key=True) -class Book(models.Model): - publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE) - -reveal_type(Book().publisher_id) # N: Revealed type is 'builtins.str' -Book(publisher_id=1) -Book(publisher_id='hello') -Book(publisher_id=datetime.datetime.now()) # E: Incompatible type for "publisher_id" of "Book" (got "datetime", expected "Union[str, int, Combinable, None]") -Book.objects.create(publisher_id=1) -Book.objects.create(publisher_id='hello') - -class Publisher2(models.Model): - mypk = models.IntegerField(primary_key=True) -class Book2(models.Model): - publisher = models.ForeignKey(to=Publisher2, on_delete=models.CASCADE) - -reveal_type(Book2().publisher_id) # N: Revealed type is 'builtins.int' -Book2(publisher_id=1) -Book2(publisher_id=[]) # E: Incompatible type for "publisher_id" of "Book2" (got "List[Any]", expected "Union[float, int, str, Combinable, None]") -Book2.objects.create(publisher_id=1) -Book2.objects.create(publisher_id=[]) # E: Incompatible type for "publisher_id" of "Book2" (got "List[Any]", expected "Union[float, int, str, Combinable]") -[out] - -[CASE if_model_is_defined_as_name_of_the_class_look_for_it_in_the_same_file] -from django.db import models - -class Book(models.Model): - publisher = models.ForeignKey(to='Publisher', on_delete=models.CASCADE) -class Publisher(models.Model): - pass -reveal_type(Book().publisher) # N: Revealed type is 'main.Publisher*' -[out] - -[CASE test_foreign_key_field_without_backwards_relation] -from django.db import models - -class Publisher(models.Model): - pass - -class Book(models.Model): - publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, - related_name='+') - publisher2 = models.ForeignKey(to=Publisher, on_delete=models.CASCADE, - related_name='books2') - -book = Book() -reveal_type(book.publisher) # N: Revealed type is 'main.Publisher*' - -publisher = Publisher() -reveal_type(publisher.books) -reveal_type(publisher.books2) # N: Revealed type is 'django.db.models.manager.RelatedManager[main.Book]' -[out] -main:16: note: Revealed type is 'Any' -main:16: error: "Publisher" has no attribute "books"; maybe "books2"? -[/CASE] - diff --git a/test-data/typecheck/settings.test b/test-data/typecheck/settings.test deleted file mode 100644 index bfd32b256..000000000 --- a/test-data/typecheck/settings.test +++ /dev/null @@ -1,99 +0,0 @@ -[CASE test_settings_are_parsed_into_django_conf_settings] -[env DJANGO_SETTINGS_MODULE=mysettings] -[disable_cache] -from django.conf import settings - -# standard settings -reveal_type(settings.AUTH_USER_MODEL) # N: Revealed type is 'builtins.str' - -reveal_type(settings.ROOT_DIR) # N: Revealed type is 'builtins.str' -reveal_type(settings.APPS_DIR) # N: Revealed type is 'pathlib.Path' -reveal_type(settings.OBJ) # N: Revealed type is 'django.utils.functional.LazyObject' -reveal_type(settings.NUMBERS) # N: Revealed type is 'builtins.list[builtins.str*]' -reveal_type(settings.DICT) # N: Revealed type is 'builtins.dict[Any, Any]' -[file base.py] -from pathlib import Path -ROOT_DIR = '/etc' -APPS_DIR = Path(ROOT_DIR) -[file mysettings.py] -from base import * -SECRET_KEY = 112233 -NUMBERS = ['one', 'two'] -DICT = {} # type: ignore -from django.utils.functional import LazyObject -OBJ = LazyObject() -[/CASE] - -[CASE test_settings_could_be_defined_in_different_module_and_imported_with_star] -[env DJANGO_SETTINGS_MODULE=mysettings] -[disable_cache] -from django.conf import settings - -reveal_type(settings.ROOT_DIR) # N: Revealed type is 'pathlib.Path' -reveal_type(settings.SETUP) # N: Revealed type is 'Union[builtins.int, None]' -reveal_type(settings.DATABASES) # N: Revealed type is 'builtins.dict[builtins.str*, builtins.str*]' - -reveal_type(settings.LOCAL_SETTING) # N: Revealed type is 'builtins.int' -reveal_type(settings.BASE_SETTING) # N: Revealed type is 'builtins.int' - -[file mysettings.py] -from local import * -from typing import Optional -SETUP: Optional[int] = 3 - -[file local.py] -from base import * -SETUP: int = 3 -DATABASES = {'default': 'mydb'} - -LOCAL_SETTING = 1 - -[file base.py] -from pathlib import Path -from typing import Any -SETUP: Any = None -ROOT_DIR = Path(__file__) - -BASE_SETTING = 1 - -[/CASE] - -[CASE global_settings_are_always_loaded] -from django.conf import settings - -reveal_type(settings.AUTH_USER_MODEL) # N: Revealed type is 'builtins.str' -reveal_type(settings.AUTHENTICATION_BACKENDS) # N: Revealed type is 'typing.Sequence[builtins.str]' -[/CASE] - -[CASE test_circular_dependency_in_settings_works_if_settings_have_annotations] -[env DJANGO_SETTINGS_MODULE=mysettings] -[disable_cache] -from django.conf import settings -class Class: - pass -reveal_type(settings.MYSETTING) # N: Revealed type is 'builtins.int' -reveal_type(settings.REGISTRY) # N: Revealed type is 'Union[main.Class, None]' -reveal_type(settings.LIST) # N: Revealed type is 'builtins.list[builtins.str]' - -[file mysettings.py] -from typing import TYPE_CHECKING, Optional, List - -if TYPE_CHECKING: - from main import Class - -MYSETTING = 1122 -REGISTRY: Optional['Class'] = None -LIST: List[str] = ['1', '2'] -[/CASE] - -[CASE fail_if_there_is_no_setting] -from django.conf import settings -reveal_type(settings.NOT_EXISTING) - -[env DJANGO_SETTINGS_MODULE=mysettings2] -[disable_cache] -[file mysettings2.py] -[out] -main:2: note: Revealed type is 'Any' -main:2: error: 'Settings' object has no attribute 'NOT_EXISTING' -[/CASE] \ No newline at end of file diff --git a/test-data/typecheck/shortcuts.test b/test-data/typecheck/shortcuts.test deleted file mode 100644 index 9296466a0..000000000 --- a/test-data/typecheck/shortcuts.test +++ /dev/null @@ -1,62 +0,0 @@ -[CASE get_object_or_404_returns_proper_types] -from django.shortcuts import get_object_or_404, get_list_or_404 -from django.db import models - -class MyModel(models.Model): - pass -reveal_type(get_object_or_404(MyModel)) # N: Revealed type is 'main.MyModel*' -reveal_type(get_object_or_404(MyModel.objects)) # N: Revealed type is 'main.MyModel*' -reveal_type(get_object_or_404(MyModel.objects.get_queryset())) # N: Revealed type is 'main.MyModel*' - -reveal_type(get_list_or_404(MyModel)) # N: Revealed type is 'builtins.list[main.MyModel*]' -reveal_type(get_list_or_404(MyModel.objects)) # N: Revealed type is 'builtins.list[main.MyModel*]' -reveal_type(get_list_or_404(MyModel.objects.get_queryset())) # N: Revealed type is 'builtins.list[main.MyModel*]' -[/CASE] - -[CASE get_user_model_returns_proper_class] -[env DJANGO_SETTINGS_MODULE=mysettings] -[disable_cache] -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from myapp.models import MyUser -from django.contrib.auth import get_user_model - -UserModel = get_user_model() -reveal_type(UserModel.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.MyUser]' - -[file mysettings.py] -from basic import * -INSTALLED_APPS = ('myapp',) - -[file basic.py] -AUTH_USER_MODEL = 'myapp.MyUser' - -[file myapp/__init__.py] -[file myapp/models.py] -from django.db import models -class MyUser(models.Model): - pass -[/CASE] - -[CASE return_type_model_and_show_error_if_model_not_yet_imported] -[env DJANGO_SETTINGS_MODULE=mysettings] -[disable_cache] -from django.contrib.auth import get_user_model - -UserModel = get_user_model() -reveal_type(UserModel.objects) - -[file mysettings.py] -INSTALLED_APPS = ('myapp',) -AUTH_USER_MODEL = 'myapp.MyUser' - -[file myapp/__init__.py] -[file myapp/models.py] -from django.db import models -class MyUser(models.Model): - pass -[out] -main:3: error: "myapp.MyUser" model class is not imported so far. Try to import it (under if TYPE_CHECKING) at the beginning of the current file -main:4: note: Revealed type is 'Any' -main:4: error: "Type[Model]" has no attribute "objects" -[/CASE] diff --git a/test-data/typecheck/test_config.yml b/test-data/typecheck/test_config.yml new file mode 100644 index 000000000..5d475ba9a --- /dev/null +++ b/test-data/typecheck/test_config.yml @@ -0,0 +1,40 @@ +- case: pyproject_toml_config + main: | + from myapp.models import MyModel + mymodel = MyModel(user_id=1) + reveal_type(mymodel.id) # N: Revealed type is 'builtins.int*' + reveal_type(mymodel.user) # N: Revealed type is 'django.contrib.auth.models.User*' + reveal_type(mymodel.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.MyModel]' + mypy_config: | + [mypy.plugins.django-stubs] + django_settings_module = mysettings + custom_settings: | + SECRET_KEY = '1' + INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth', 'myapp') + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from typing import TYPE_CHECKING + from django.db import models + class MyModel(models.Model): + user = models.ForeignKey('auth.User', on_delete=models.CASCADE) + if TYPE_CHECKING: + reveal_type(MyModel().user) # N: Revealed type is 'django.contrib.auth.models.User*' + +- case: generate_pyproject_toml_and_settings_file_from_installed_apps_key + main: | + from myapp.models import MyModel + mymodel = MyModel(user_id=1) + reveal_type(mymodel.id) # N: Revealed type is 'builtins.int*' + installed_apps: + - django.contrib.auth + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + user = models.ForeignKey('auth.User', on_delete=models.CASCADE) + diff --git a/test-data/typecheck/test_forms.yml b/test-data/typecheck/test_forms.yml new file mode 100644 index 000000000..ed200a0bb --- /dev/null +++ b/test-data/typecheck/test_forms.yml @@ -0,0 +1,58 @@ +- case: no_incompatible_meta_nested_class_false_positive + main: | + from django import forms + from myapp.models import Article, Category + class ArticleForm(forms.ModelForm): + class Meta: + model = Article + fields = '__all__' + class CategoryForm(forms.ModelForm): + class Meta: + model = Category + fields = '__all__' + class CompositeForm(ArticleForm, CategoryForm): + pass + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + from django import forms + + class Article(models.Model): + pass + class Category(models.Model): + pass + +- case: formview_methods_on_forms_return_proper_types + main: | + from typing import Any + from django import forms + from django.views.generic.edit import FormView + + class MyForm(forms.ModelForm): + pass + class MyForm2(forms.ModelForm): + pass + class MyView(FormView): + form_class = MyForm + def post(self, request, *args: Any, **kwds: Any): + form_class = self.get_form_class() + reveal_type(form_class) # N: Revealed type is 'Type[main.MyForm]' + reveal_type(self.get_form(None)) # N: Revealed type is 'main.MyForm' + reveal_type(self.get_form()) # N: Revealed type is 'main.MyForm' + reveal_type(self.get_form(form_class)) # N: Revealed type is 'main.MyForm' + reveal_type(self.get_form(MyForm2)) # N: Revealed type is 'main.MyForm2' + +- case: successmessagemixin_compatible_with_formmixin + main: | + from django.views.generic.edit import FormMixin + from django.contrib.messages.views import SuccessMessageMixin + + class FormFirstView(FormMixin, SuccessMessageMixin): + pass + + class SuccessMessageFirstView(FormMixin, SuccessMessageMixin): + pass diff --git a/test-data/typecheck/test_helpers.yml b/test-data/typecheck/test_helpers.yml new file mode 100644 index 000000000..a2ea0c64d --- /dev/null +++ b/test-data/typecheck/test_helpers.yml @@ -0,0 +1,42 @@ +- case: transaction_atomic_contextmanager + main: | + from django.db import transaction + with transaction.atomic(): + pass + with transaction.atomic(using="mydb"): + pass + with transaction.atomic(using="mydb", savepoint=False): + pass + +- case: transaction_atomic_decorator + main: | + from django.db import transaction + + @transaction.atomic() + def decorated_func(param1: str, param2: int) -> bool: + pass + # Ensure that the function's type is preserved + reveal_type(decorated_func) # N: Revealed type is 'def (param1: builtins.str, param2: builtins.int) -> builtins.bool' + + @transaction.atomic(using="mydb") + def decorated_func_using(param1: str, param2: int) -> bool: + pass + # Ensure that the function's type is preserved + reveal_type(decorated_func_using) # N: Revealed type is 'def (param1: builtins.str, param2: builtins.int) -> builtins.bool' + + class ClassWithAtomicMethod: + # Bare decorator + @transaction.atomic + def atomic_method1(self, abc: int) -> str: + pass + @transaction.atomic(savepoint=True) + def atomic_method2(self): + pass + @transaction.atomic(using="db", savepoint=True) + def atomic_method3(self, myparam: str) -> int: + pass + ClassWithAtomicMethod().atomic_method1("abc") # E: Argument 1 to "atomic_method1" of "ClassWithAtomicMethod" has incompatible type "str"; expected "int" + # Ensure that the method's type is preserved + reveal_type(ClassWithAtomicMethod().atomic_method1) # N: Revealed type is 'def (abc: builtins.int) -> builtins.str' + # Ensure that the method's type is preserved + reveal_type(ClassWithAtomicMethod().atomic_method3) # N: Revealed type is 'def (myparam: builtins.str) -> builtins.int' \ No newline at end of file diff --git a/test-data/typecheck/test_import_all.yml b/test-data/typecheck/test_import_all.yml new file mode 100644 index 000000000..844304475 --- /dev/null +++ b/test-data/typecheck/test_import_all.yml @@ -0,0 +1,402 @@ +- case: import_all_modules + main: | + import django.apps + import django.apps.config + import django.apps.registry + import django.conf.global_settings + import django.conf.urls + import django.conf.urls.i18n + import django.conf.urls.static + import django.contrib.admin.actions + import django.contrib.admin.apps + import django.contrib.admin.checks + import django.contrib.admin.decorators + import django.contrib.admin.filters + import django.contrib.admin.forms + import django.contrib.admin.helpers + import django.contrib.admin.models + import django.contrib.admin.options + import django.contrib.admin.sites + import django.contrib.admin.templatetags + import django.contrib.admin.templatetags.admin_list + import django.contrib.admin.templatetags.admin_modify + import django.contrib.admin.templatetags.admin_static + import django.contrib.admin.templatetags.admin_urls + import django.contrib.admin.templatetags.base + import django.contrib.admin.templatetags.log + import django.contrib.admin.utils + import django.contrib.admin.views + import django.contrib.admin.views.autocomplete + import django.contrib.admin.views.decorators + import django.contrib.admin.views.main + import django.contrib.admin.widgets + import django.contrib.admindocs + import django.contrib.admindocs.middleware + import django.contrib.admindocs.utils + import django.contrib.admindocs.views + import django.contrib.auth.admin + import django.contrib.auth.apps + import django.contrib.auth.backends + import django.contrib.auth.base_user + import django.contrib.auth.checks + import django.contrib.auth.context_processors + import django.contrib.auth.decorators + import django.contrib.auth.forms + import django.contrib.auth.handlers + import django.contrib.auth.handlers.modwsgi + import django.contrib.auth.hashers + import django.contrib.auth.management.commands + import django.contrib.auth.management.commands.changepassword + import django.contrib.auth.management.commands.createsuperuser + import django.contrib.auth.middleware + import django.contrib.auth.mixins + import django.contrib.auth.models + import django.contrib.auth.password_validation + import django.contrib.auth.signals + import django.contrib.auth.tokens + import django.contrib.auth.validators + import django.contrib.auth.views + import django.contrib.contenttypes.admin + import django.contrib.contenttypes.apps + import django.contrib.contenttypes.checks + import django.contrib.contenttypes.fields + import django.contrib.contenttypes.forms + import django.contrib.contenttypes.management.commands + import django.contrib.contenttypes.management.commands.remove_stale_contenttypes + import django.contrib.contenttypes.models + import django.contrib.contenttypes.views + import django.contrib.flatpages.forms + import django.contrib.flatpages.middleware + import django.contrib.flatpages.models + import django.contrib.flatpages.sitemaps + import django.contrib.flatpages.templatetags + import django.contrib.flatpages.templatetags.flatpages + import django.contrib.flatpages.views + import django.contrib.humanize.templatetags + import django.contrib.humanize.templatetags.humanize + import django.contrib.messages.api + import django.contrib.messages.context_processors + import django.contrib.messages.middleware + import django.contrib.messages.storage + import django.contrib.messages.storage.base + import django.contrib.messages.storage.cookie + import django.contrib.messages.storage.fallback + import django.contrib.messages.storage.session + import django.contrib.messages.utils + import django.contrib.messages.views + import django.contrib.postgres.fields + import django.contrib.postgres.fields.array + import django.contrib.postgres.fields.citext + import django.contrib.postgres.fields.hstore + import django.contrib.postgres.fields.jsonb + import django.contrib.postgres.fields.mixins + import django.contrib.postgres.fields.ranges + import django.contrib.postgres.operations + import django.contrib.redirects + import django.contrib.redirects.middleware + import django.contrib.redirects.models + import django.contrib.sessions.backends + import django.contrib.sessions.backends.base + import django.contrib.sessions.backends.cache + import django.contrib.sessions.backends.cached_db + import django.contrib.sessions.backends.db + import django.contrib.sessions.backends.file + import django.contrib.sessions.backends.signed_cookies + import django.contrib.sessions.base_session + import django.contrib.sessions.management.commands + import django.contrib.sessions.management.commands.clearsessions + import django.contrib.sessions.middleware + import django.contrib.sessions.models + import django.contrib.sessions.serializers + import django.contrib.sitemaps.management.commands + import django.contrib.sitemaps.management.commands.ping_google + import django.contrib.sitemaps.views + import django.contrib.sites + import django.contrib.sites.apps + import django.contrib.sites.management + import django.contrib.sites.managers + import django.contrib.sites.middleware + import django.contrib.sites.models + import django.contrib.sites.requests + import django.contrib.sites.shortcuts + import django.contrib.staticfiles.apps + import django.contrib.staticfiles.checks + import django.contrib.staticfiles.finders + import django.contrib.staticfiles.handlers + import django.contrib.staticfiles.management.commands + import django.contrib.staticfiles.management.commands.collectstatic + import django.contrib.staticfiles.management.commands.findstatic + import django.contrib.staticfiles.management.commands.runserver + import django.contrib.staticfiles.storage + import django.contrib.staticfiles.templatetags + import django.contrib.staticfiles.templatetags.staticfiles + import django.contrib.staticfiles.urls + import django.contrib.staticfiles.utils + import django.contrib.staticfiles.views + import django.contrib.syndication + import django.contrib.syndication.views + import django.core.cache.backends + import django.core.cache.backends.base + import django.core.cache.backends.db + import django.core.cache.backends.dummy + import django.core.cache.backends.filebased + import django.core.cache.backends.locmem + import django.core.cache.utils + import django.core.checks.caches + import django.core.checks.database + import django.core.checks.messages + import django.core.checks.model_checks + import django.core.checks.registry + import django.core.checks.security + import django.core.checks.security.base + import django.core.checks.security.csrf + import django.core.checks.security.sessions + import django.core.checks.templates + import django.core.checks.urls + import django.core.exceptions + import django.core.files + import django.core.files.base + import django.core.files.images + import django.core.files.locks + import django.core.files.move + import django.core.files.storage + import django.core.files.temp + import django.core.files.uploadedfile + import django.core.files.uploadhandler + import django.core.files.utils + import django.core.handlers + import django.core.handlers.base + import django.core.handlers.exception + import django.core.handlers.wsgi + import django.core.mail + import django.core.mail.message + import django.core.mail.utils + import django.core.management + import django.core.management.base + import django.core.management.color + import django.core.management.sql + import django.core.management.templates + import django.core.management.utils + import django.core.paginator + import django.core.serializers + import django.core.serializers.base + import django.core.serializers.json + import django.core.serializers.python + import django.core.servers + import django.core.servers.basehttp + import django.core.signals + import django.core.signing + import django.core.validators + import django.core.wsgi + import django.db.backends.base + import django.db.backends.base.base + import django.db.backends.base.client + import django.db.backends.base.creation + import django.db.backends.base.features + import django.db.backends.base.introspection + import django.db.backends.base.operations + import django.db.backends.base.schema + import django.db.backends.base.validation + import django.db.backends.ddl_references + import django.db.backends.dummy + import django.db.backends.dummy.base + import django.db.backends.mysql + import django.db.backends.mysql.client + import django.db.backends.postgresql + import django.db.backends.postgresql.client + import django.db.backends.sqlite3 + import django.db.backends.sqlite3.base + import django.db.backends.sqlite3.creation + import django.db.backends.sqlite3.features + import django.db.backends.sqlite3.introspection + import django.db.backends.sqlite3.operations + import django.db.backends.sqlite3.schema + import django.db.backends.utils + import django.db.migrations.autodetector + import django.db.migrations.exceptions + import django.db.migrations.executor + import django.db.migrations.graph + import django.db.migrations.loader + import django.db.migrations.migration + import django.db.migrations.operations + import django.db.migrations.operations.base + import django.db.migrations.operations.fields + import django.db.migrations.operations.models + import django.db.migrations.operations.special + import django.db.migrations.operations.utils + import django.db.migrations.optimizer + import django.db.migrations.questioner + import django.db.migrations.recorder + import django.db.migrations.serializer + import django.db.migrations.state + import django.db.migrations.topological_sort + import django.db.migrations.utils + import django.db.migrations.writer + import django.db.models.aggregates + import django.db.models.base + import django.db.models.deletion + import django.db.models.expressions + import django.db.models.fields + import django.db.models.fields.files + import django.db.models.fields.mixins + import django.db.models.fields.proxy + import django.db.models.fields.related + import django.db.models.fields.related_descriptors + import django.db.models.fields.related_lookups + import django.db.models.fields.reverse_related + import django.db.models.functions + import django.db.models.functions.comparison + import django.db.models.functions.datetime + import django.db.models.functions.text + import django.db.models.functions.window + import django.db.models.indexes + import django.db.models.lookups + import django.db.models.manager + import django.db.models.options + import django.db.models.query + import django.db.models.query_utils + import django.db.models.signals + import django.db.models.sql + import django.db.models.sql.compiler + import django.db.models.sql.constants + import django.db.models.sql.datastructures + import django.db.models.sql.query + import django.db.models.sql.subqueries + import django.db.models.sql.where + import django.db.models.utils + import django.db.transaction + import django.db.utils + import django.dispatch + import django.dispatch.dispatcher + import django.forms + import django.forms.boundfield + import django.forms.fields + import django.forms.forms + import django.forms.formsets + import django.forms.models + import django.forms.renderers + import django.forms.utils + import django.forms.widgets + import django.http + import django.http.cookie + import django.http.multipartparser + import django.http.request + import django.http.response + import django.middleware + import django.middleware.cache + import django.middleware.clickjacking + import django.middleware.common + import django.middleware.csrf + import django.middleware.gzip + import django.middleware.http + import django.middleware.locale + import django.middleware.security + import django.shortcuts + import django.template.backends + import django.template.backends.base + import django.template.backends.django + import django.template.backends.dummy + import django.template.backends.jinja2 + import django.template.backends.utils + import django.template.base + import django.template.context + import django.template.context_processors + import django.template.defaultfilters + import django.template.defaulttags + import django.template.engine + import django.template.exceptions + import django.template.library + import django.template.loader + import django.template.loader_tags + import django.template.loaders + import django.template.loaders.app_directories + import django.template.loaders.base + import django.template.loaders.cached + import django.template.loaders.filesystem + import django.template.loaders.locmem + import django.template.response + import django.template.smartif + import django.template.utils + import django.templatetags + import django.templatetags.cache + import django.templatetags.i18n + import django.templatetags.l10n + import django.templatetags.static + import django.templatetags.tz + import django.test + import django.test.client + import django.test.html + import django.test.runner + import django.test.selenium + import django.test.signals + import django.test.testcases + import django.test.utils + import django.urls + import django.urls.base + import django.urls.conf + import django.urls.converters + import django.urls.exceptions + import django.urls.resolvers + import django.urls.utils + import django.utils._os + import django.utils.archive + import django.utils.autoreload + import django.utils.baseconv + import django.utils.cache + import django.utils.crypto + import django.utils.datastructures + import django.utils.dateformat + import django.utils.dateparse + import django.utils.dates + import django.utils.datetime_safe + import django.utils.deconstruct + import django.utils.decorators + import django.utils.deprecation + import django.utils.duration + import django.utils.encoding + import django.utils.feedgenerator + import django.utils.formats + import django.utils.functional + import django.utils.html + import django.utils.http + import django.utils.inspect + import django.utils.ipv6 + import django.utils.itercompat + import django.utils.jslex + import django.utils.log + import django.utils.lorem_ipsum + import django.utils.module_loading + import django.utils.numberformat + import django.utils.regex_helper + import django.utils.safestring + import django.utils.six + import django.utils.termcolors + import django.utils.text + import django.utils.timesince + import django.utils.timezone + import django.utils.translation + import django.utils.translation.template + import django.utils.translation.trans_null + import django.utils.translation.trans_real + import django.utils.tree + import django.utils.version + import django.utils.xmlutils + import django.views.csrf + import django.views.debug + import django.views.decorators + import django.views.decorators.cache + import django.views.decorators.clickjacking + import django.views.decorators.csrf + import django.views.decorators.debug + import django.views.decorators.gzip + import django.views.decorators.http + import django.views.decorators.vary + import django.views.defaults + import django.views.generic + import django.views.generic.base + import django.views.generic.dates + import django.views.generic.detail + import django.views.generic.edit + import django.views.generic.list + import django.views.i18n + import django.views.static diff --git a/test-data/typecheck/test_request.yml b/test-data/typecheck/test_request.yml new file mode 100644 index 000000000..1c0850a25 --- /dev/null +++ b/test-data/typecheck/test_request.yml @@ -0,0 +1,17 @@ +- case: request_object_has_user_of_type_auth_user_model + disable_cache: true + main: | + from django.http.request import HttpRequest + reveal_type(HttpRequest().user) # N: Revealed type is 'myapp.models.MyUser' + # check that other fields work ok + reveal_type(HttpRequest().method) # N: Revealed type is 'Union[builtins.str, None]' + custom_settings: | + INSTALLED_APPS = ('django.contrib.contenttypes', 'myapp') + AUTH_USER_MODEL='myapp.MyUser' + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + pass \ No newline at end of file diff --git a/test-data/typecheck/test_settings.yml b/test-data/typecheck/test_settings.yml new file mode 100644 index 000000000..507c319c4 --- /dev/null +++ b/test-data/typecheck/test_settings.yml @@ -0,0 +1,50 @@ +- case: settings_loaded_from_different_files + disable_cache: true + main: | + from django.conf import settings + # standard settings + reveal_type(settings.AUTH_USER_MODEL) # N: Revealed type is 'builtins.str' + reveal_type(settings.ROOT_DIR) # N: Revealed type is 'builtins.str' + reveal_type(settings.APPS_DIR) # N: Revealed type is 'pathlib.Path' + reveal_type(settings.NUMBERS) # N: Revealed type is 'builtins.list[builtins.str*]' + reveal_type(settings.DICT) # N: Revealed type is 'builtins.dict[Any, Any]' + custom_settings: | + from base import * + SECRET_KEY = 112233 + NUMBERS = ['one', 'two'] + DICT = {} # type: ignore + files: + - path: base.py + content: | + from pathlib import Path + ROOT_DIR = '/etc' + APPS_DIR = Path(ROOT_DIR) + +- case: global_settings_are_always_loaded + main: | + from django.conf import settings + reveal_type(settings.AUTH_USER_MODEL) # N: Revealed type is 'builtins.str' + reveal_type(settings.AUTHENTICATION_BACKENDS) # N: Revealed type is 'typing.Sequence[builtins.str]' + installed_apps: [] + +- case: fail_if_there_is_no_setting + main: | + from django.conf import settings + reveal_type(settings.NOT_EXISTING) + out: | + main:2: note: Revealed type is 'Any' + main:2: error: 'Settings' object has no attribute 'NOT_EXISTING' + +- case: override_default_setting_with_different_type_in_the_different_module + custom_settings: | + from settings.basic_settings import * + main: | + from django.conf import settings + reveal_type(settings.MEDIA_ROOT) # N: Revealed type is 'pathlib.Path' + reveal_type(settings.MEDIA_ROOT / 'part') # N: Revealed type is 'pathlib.Path*' + files: + - path: settings/__init__.py + - path: settings/basic_settings.py + content: | + from pathlib import Path + MEDIA_ROOT = Path() diff --git a/test-data/typecheck/test_shortcuts.yml b/test-data/typecheck/test_shortcuts.yml new file mode 100644 index 000000000..e0db4eeb3 --- /dev/null +++ b/test-data/typecheck/test_shortcuts.yml @@ -0,0 +1,38 @@ +- case: get_object_or_404_returns_proper_types + main: | + from django.shortcuts import get_object_or_404, get_list_or_404 + from myapp.models import MyModel + + reveal_type(get_object_or_404(MyModel)) # N: Revealed type is 'myapp.models.MyModel*' + reveal_type(get_object_or_404(MyModel.objects)) # N: Revealed type is 'myapp.models.MyModel*' + reveal_type(get_object_or_404(MyModel.objects.get_queryset())) # N: Revealed type is 'myapp.models.MyModel*' + + reveal_type(get_list_or_404(MyModel)) # N: Revealed type is 'builtins.list[myapp.models.MyModel*]' + reveal_type(get_list_or_404(MyModel.objects)) # N: Revealed type is 'builtins.list[myapp.models.MyModel*]' + reveal_type(get_list_or_404(MyModel.objects.get_queryset())) # N: Revealed type is 'builtins.list[myapp.models.MyModel*]' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyModel(models.Model): + pass + +- case: get_user_model_returns_proper_class + disable_cache: true + main: | + from django.contrib.auth import get_user_model + UserModel = get_user_model() + reveal_type(UserModel.objects) # N: Revealed type is 'django.db.models.manager.Manager[myapp.models.MyUser]' + custom_settings: | + INSTALLED_APPS = ('django.contrib.contenttypes', 'myapp') + AUTH_USER_MODEL = 'myapp.MyUser' + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class MyUser(models.Model): + pass diff --git a/test-data/typecheck/transaction.test b/test-data/typecheck/transaction.test deleted file mode 100644 index 62643e1b4..000000000 --- a/test-data/typecheck/transaction.test +++ /dev/null @@ -1,50 +0,0 @@ -[CASE test_transaction_atomic] - -from django.db import transaction - -with transaction.atomic(): - pass - -with transaction.atomic(using="mydb"): - pass - -with transaction.atomic(using="mydb", savepoint=False): - pass - -@transaction.atomic() -def decorated_func(param1: str, param2: int) -> bool: - pass - -# Ensure that the function's type is preserved -reveal_type(decorated_func) # N: Revealed type is 'def (param1: builtins.str, param2: builtins.int) -> builtins.bool' - -@transaction.atomic(using="mydb") -def decorated_func_using(param1: str, param2: int) -> bool: - pass - -# Ensure that the function's type is preserved -reveal_type(decorated_func_using) # N: Revealed type is 'def (param1: builtins.str, param2: builtins.int) -> builtins.bool' - -class ClassWithAtomicMethod: - # Bare decorator - @transaction.atomic - def atomic_method1(self, abc: int) -> str: - pass - - @transaction.atomic(savepoint=True) - def atomic_method2(self): - pass - - @transaction.atomic(using="db", savepoint=True) - def atomic_method3(self, myparam: str) -> int: - pass - -ClassWithAtomicMethod().atomic_method1("abc") # E: Argument 1 to "atomic_method1" of "ClassWithAtomicMethod" has incompatible type "str"; expected "int" - -# Ensure that the method's type is preserved -reveal_type(ClassWithAtomicMethod().atomic_method1) # N: Revealed type is 'def (abc: builtins.int) -> builtins.str' - -# Ensure that the method's type is preserved -reveal_type(ClassWithAtomicMethod().atomic_method3) # N: Revealed type is 'def (myparam: builtins.str) -> builtins.int' - -[out] \ No newline at end of file