new semanal wip 1

This commit is contained in:
Maxim Kurnikov
2019-07-16 01:22:20 +03:00
parent 9c5a6be9a7
commit b11a9a85f9
96 changed files with 4441 additions and 2370 deletions

View File

@@ -0,0 +1,80 @@
import os
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Type
from django.db.models.base import Model
from django.utils.functional import cached_property
from pytest_mypy.utils import temp_environ
if TYPE_CHECKING:
from django.apps.registry import Apps
from django.conf import LazySettings
@dataclass
class DjangoPluginConfig:
ignore_missing_settings: bool = False
ignore_missing_model_attributes: bool = False
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)
models.Manager.__class_getitem__ = classmethod(noop_class_getitem)
from django.conf import settings
from django.apps import apps
apps.get_models.cache_clear()
apps.get_swappable_settings_name.cache_clear()
apps.populate(settings.INSTALLED_APPS)
assert apps.apps_ready
assert settings.configured
return apps, settings
class DjangoContext:
def __init__(self, plugin_toml_config: Optional[Dict[str, Any]]) -> None:
self.config = DjangoPluginConfig()
django_settings_module = None
if plugin_toml_config:
self.config.ignore_missing_settings = plugin_toml_config.get('ignore_missing_settings', False)
self.config.ignore_missing_model_attributes = plugin_toml_config.get('ignore_missing_model_attributes', False)
django_settings_module = plugin_toml_config.get('django_settings_module', None)
self.apps_registry: Optional[Dict[str, str]] = None
self.settings: LazySettings = None
if django_settings_module:
apps, settings = initialize_django(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

View File

@@ -0,0 +1,35 @@
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
}

View File

@@ -0,0 +1,99 @@
from typing import Dict, List, Optional, Set, Union
from mypy.nodes import Expression, MypyFile, NameExpr, SymbolNode, TypeInfo, Var
from mypy.plugin import FunctionContext, MethodContext
from mypy.types import AnyType, Instance, NoneTyp, Type as MypyType, TypeOfAny, UnionType
class IncompleteDefnException(Exception):
pass
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 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:
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 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

View File

@@ -0,0 +1,27 @@
from typing import Any, Dict, List
from mypy.nodes import TypeInfo
def get_django_metadata(model_info: TypeInfo) -> Dict[str, Any]:
return model_info.metadata.setdefault('django', {})
def get_related_field_primary_key_names(base_model: TypeInfo) -> List[str]:
return get_django_metadata(base_model).setdefault('related_field_primary_keys', [])
def get_fields_metadata(model: TypeInfo) -> Dict[str, Any]:
return get_django_metadata(model).setdefault('fields', {})
def get_lookups_metadata(model: TypeInfo) -> Dict[str, Any]:
return get_django_metadata(model).setdefault('lookups', {})
def get_related_managers_metadata(model: TypeInfo) -> Dict[str, Any]:
return get_django_metadata(model).setdefault('related_managers', {})
def get_managers_metadata(model: TypeInfo) -> Dict[str, Any]:
return get_django_metadata(model).setdefault('managers', {})

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample_django_project.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class MyappConfig(AppConfig):
label = 'myapp22'

View File

@@ -0,0 +1,6 @@
from django.db import models
# Create your models here.
class MyModel(models.Model):
pass

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1,121 @@
"""
Django settings for sample_django_project project.
Generated by 'django-admin startproject' using Django 2.2.3.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'e6gj!2x(*odqwmjafrn7#35%)&rnn&^*0x-f&j0prgr--&xf+%'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'mypy_django_plugin.lib.tests.sample_django_project.myapp'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'sample_django_project.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'sample_django_project.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/'

View File

@@ -0,0 +1,21 @@
"""sample_django_project URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]

View File

@@ -0,0 +1,16 @@
"""
WSGI config for sample_django_project project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample_django_project.settings')
application = get_wsgi_application()

View File

@@ -0,0 +1,14 @@
from mypy.options import Options
from mypy_django_plugin.lib.config import extract_app_model_aliases
from mypy_django_plugin.main import DjangoPlugin
def test_parse_django_settings():
app_model_mapping = extract_app_model_aliases('mypy_django_plugin.lib.tests.sample_django_project.root.settings')
assert app_model_mapping['myapp.MyModel'] == 'mypy_django_plugin.lib.tests.sample_django_project.myapp.models.MyModel'
def test_instantiate_plugin_with_config():
plugin = DjangoPlugin(Options())

View File

@@ -0,0 +1,173 @@
import os
from functools import partial
from typing import Callable, Dict, List, Optional, Tuple, Type
import toml
from mypy.nodes import MypyFile, TypeInfo
from mypy.options import Options
from mypy.plugin import ClassDefContext, FunctionContext, Plugin, MethodContext
from mypy.types import Type as MypyType
from mypy_django_plugin_newsemanal.context import DjangoContext
from mypy_django_plugin_newsemanal.lib import fullnames, metadata
from mypy_django_plugin_newsemanal.transformers import fields, settings, querysets, init_create
from mypy_django_plugin_newsemanal.transformers.models import process_model_class
def transform_model_class(ctx: ClassDefContext,
django_context: DjangoContext) -> None:
sym = ctx.api.lookup_fully_qualified_or_none(fullnames.MODEL_CLASS_FULLNAME)
if sym is not None and isinstance(sym.node, TypeInfo):
metadata.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_manager_class(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):
metadata.get_django_metadata(sym.node)['manager_bases'][ctx.cls.fullname] = 1
class NewSemanalDjangoPlugin(Plugin):
def __init__(self, options: Options) -> None:
super().__init__(options)
plugin_toml_config = None
if os.path.exists('pyproject.toml'):
with open('pyproject.toml', 'r') as f:
pyproject_toml = toml.load(f)
plugin_toml_config = pyproject_toml.get('tool', {}).get('django-stubs')
self.django_context = DjangoContext(plugin_toml_config)
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 (metadata.get_django_metadata(model_sym.node)
.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(fullnames.MANAGER_CLASS_FULLNAME)
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
return (metadata.get_django_metadata(model_sym.node)
.setdefault('manager_bases', {fullnames.MANAGER_CLASS_FULLNAME: 1}))
else:
return {}
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 metadata.get_django_metadata(model_sym.node).setdefault('model_bases',
{fullnames.MODEL_CLASS_FULLNAME: 1})
else:
return {}
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 _new_dependency(self, module: str) -> Tuple[int, str, int]:
return 10, module, -1
def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
# for `get_user_model()`
if file.fullname() == 'django.contrib.auth':
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:
for related_object in model_class._meta.related_objects:
related_model_module = related_object.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], MypyType]]:
if fullname == 'django.contrib.auth.get_user_model':
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 querysets.determine_proper_manager_type
info = self._get_typeinfo_or_none(fullname)
if info:
if info.has_base(fullnames.FIELD_FULLNAME):
return partial(fields.process_field_instantiation, django_context=self.django_context)
# if info.has_base(fullnames.MODEL_CLASS_FULLNAME):
# return partial(init_create.redefine_and_typecheck_model_init, django_context=self.django_context)
# def get_method_hook(self, fullname: str
# ) -> Optional[Callable[[MethodContext], Type]]:
# class_name, _, method_name = fullname.rpartition('.')
#
#
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, django_context=self.django_context)
if fullname in self._get_current_manager_bases():
return transform_manager_class
# def get_attribute_hook(self, fullname: str
# ) -> Optional[Callable[[AttributeContext], MypyType]]:
# print(fullname)
# class_name, _, attr_name = fullname.rpartition('.')
# # if class_name == fullnames.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:
# attr_sym = model_info.get(attr_name)
# if attr_sym and isinstance(attr_sym.node, TypeInfo) \
# and helpers.has_any_of_bases(attr_sym.node, fullnames.MANAGER_CLASSES):
# return partial(querysets.determite_manager_type, django_context=self.django_context)
#
# # related_managers = metadata.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
# def get_type_analyze_hook(self, fullname: str
# ) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]:
# queryset_bases = self._get_current_queryset_bases()
# if fullname in queryset_bases:
# return partial(querysets.set_first_generic_param_as_default_for_second, fullname=fullname)
def plugin(version):
return NewSemanalDjangoPlugin

View File

@@ -0,0 +1,264 @@
from typing import Optional, Tuple, cast
from mypy.checker import TypeChecker
from mypy.nodes import Expression, ListExpr, NameExpr, StrExpr, TupleExpr, TypeInfo
from mypy.plugin import FunctionContext
from mypy.types import AnyType, CallableType, Instance, TupleType, Type as MypyType, UnionType
from mypy_django_plugin_newsemanal.context import DjangoContext
from mypy_django_plugin_newsemanal.lib import fullnames, helpers, metadata
def extract_referred_to_type(ctx: FunctionContext, django_context: DjangoContext) -> 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!r}',
context=ctx.context)
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
model_string = to_arg_expr.value
if model_string == 'self':
model_fullname = api.tscope.classes[-1].fullname()
elif '.' not in model_string:
model_fullname = api.tscope.classes[-1].module_name + '.' + model_string
else:
if django_context.app_models is not None and model_string in django_context.app_models:
model_fullname = django_context.app_models[model_string]
else:
ctx.api.fail(f'Cannot find referenced model for {model_string!r}', context=ctx.context)
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):
raise helpers.IncompleteDefnException(model_fullname)
return Instance(model_info, [])
referred_to_type = arg_type.ret_type
assert isinstance(referred_to_type, Instance)
if not referred_to_type.type.has_base(fullnames.MODEL_CLASS_FULLNAME):
ctx.api.msg.fail(f'to= parameter value must be a subclass of {fullnames.MODEL_CLASS_FULLNAME!r}',
context=ctx.context)
return None
return referred_to_type
def convert_any_to_type(typ: MypyType, replacement_type: MypyType) -> MypyType:
"""
Converts any encountered Any (in typ itself, or in generic parameters) into referred_to_type
"""
if isinstance(typ, UnionType):
converted_items = []
for item in typ.items:
converted_items.append(convert_any_to_type(item, replacement_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(replacement_type)
else:
args.append(default_arg)
return helpers.reparametrize_instance(typ, args)
if isinstance(typ, AnyType):
return replacement_type
return typ
def get_referred_to_model_fullname(ctx: FunctionContext, django_context: DjangoContext) -> str:
to_arg_type = helpers.get_call_argument_type_by_name(ctx, 'to')
if isinstance(to_arg_type, CallableType):
assert isinstance(to_arg_type.ret_type, Instance)
return to_arg_type.ret_type.type.fullname()
to_arg_expr = helpers.get_call_argument_by_name(ctx, 'to')
if not isinstance(to_arg_expr, StrExpr):
raise helpers.IncompleteDefnException(f'Not a string: {to_arg_expr}')
outer_model_info = ctx.api.tscope.classes[-1]
assert isinstance(outer_model_info, TypeInfo)
model_string = to_arg_expr.value
if model_string == 'self':
return outer_model_info.fullname()
if '.' not in model_string:
# same file class
return outer_model_info.module_name + '.' + model_string
model_cls = django_context.apps_registry.get_model(model_string)
model_fullname = helpers.get_class_fullname(model_cls)
return model_fullname
def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context: DjangoContext) -> MypyType:
referred_to_fullname = get_referred_to_model_fullname(ctx, django_context)
referred_to_typeinfo = helpers.lookup_fully_qualified_generic(referred_to_fullname, ctx.api.modules)
assert isinstance(referred_to_typeinfo, TypeInfo)
referred_to_type = Instance(referred_to_typeinfo, [])
default_related_field_type = set_descriptor_types_for_field(ctx)
# replace Any with referred_to_type
args = []
for default_arg in default_related_field_type.args:
args.append(convert_any_to_type(default_arg, referred_to_type))
return helpers.reparametrize_instance(default_related_field_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_call_argument_by_name(ctx, 'null'))
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 transform_into_proper_return_type(ctx: FunctionContext, django_context: DjangoContext) -> MypyType:
default_return_type = ctx.default_return_type
assert isinstance(default_return_type, Instance)
if helpers.has_any_of_bases(default_return_type.type, fullnames.RELATED_FIELDS_CLASSES):
return fill_descriptor_types_for_related_field(ctx, django_context)
if default_return_type.type.has_base(fullnames.ARRAY_FIELD_FULLNAME):
return determine_type_of_array_field(ctx, django_context)
return set_descriptor_types_for_field(ctx)
def process_field_instantiation(ctx: FunctionContext, django_context: DjangoContext) -> MypyType:
# Parse __init__ parameters of field into corresponding Model's metadata
# parse_field_init_arguments_into_model_metadata(ctx)
return transform_into_proper_return_type(ctx, django_context)
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_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))
return helpers.reparametrize_instance(default_return_type, args)
# def _parse_choices_type(ctx: FunctionContext, choices_arg: Expression) -> Optional[str]:
# if isinstance(choices_arg, (TupleExpr, ListExpr)):
# # iterable of 2 element tuples of two kinds
# _, analyzed_choices = ctx.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):
# return first_element_type.type.fullname()
# def _parse_referenced_model(ctx: FunctionContext, to_arg: Expression) -> Optional[TypeInfo]:
# if isinstance(to_arg, NameExpr) and isinstance(to_arg.node, TypeInfo):
# # reference to the model class
# return to_arg.node
#
# elif isinstance(to_arg, StrExpr):
# referenced_model_info = helpers.get_model_info(to_arg.value, ctx.api.modules)
# if referenced_model_info is not None:
# return referenced_model_info
# def parse_field_init_arguments_into_model_metadata(ctx: FunctionContext) -> None:
# outer_model = ctx.api.scope.active_class()
# if outer_model is None or not outer_model.has_base(fullnames.MODEL_CLASS_FULLNAME):
# # outside models.Model class, undetermined
# return
#
# # Determine name of the current field
# for attr_name, stmt in helpers.iter_over_class_level_assignments(outer_model.defn):
# if stmt == ctx.context:
# field_name = attr_name
# break
# else:
# return
#
# model_fields_metadata = metadata.get_fields_metadata(outer_model)
#
# # primary key
# is_primary_key = False
# primary_key_arg = helpers.get_call_argument_by_name(ctx, 'primary_key')
# if primary_key_arg:
# is_primary_key = helpers.parse_bool(primary_key_arg)
# model_fields_metadata[field_name] = {'primary_key': is_primary_key}
#
# # choices
# choices_arg = helpers.get_call_argument_by_name(ctx, 'choices')
# if choices_arg:
# choices_type_fullname = _parse_choices_type(ctx.api, choices_arg)
# if choices_type_fullname:
# model_fields_metadata[field_name]['choices_type'] = choices_type_fullname
#
# # nullability
# null_arg = helpers.get_call_argument_by_name(ctx, 'null')
# is_nullable = False
# if null_arg:
# is_nullable = helpers.parse_bool(null_arg)
# model_fields_metadata[field_name]['null'] = is_nullable
#
# # is_blankable
# blank_arg = helpers.get_call_argument_by_name(ctx, 'blank')
# is_blankable = False
# if blank_arg:
# is_blankable = helpers.parse_bool(blank_arg)
# model_fields_metadata[field_name]['blank'] = is_blankable
#
# # default
# default_arg = helpers.get_call_argument_by_name(ctx, 'default')
# if default_arg and not helpers.is_none_expr(default_arg):
# model_fields_metadata[field_name]['default_specified'] = True
#
# if helpers.has_any_of_bases(ctx.default_return_type.type, fullnames.RELATED_FIELDS_CLASSES):
# # to
# to_arg = helpers.get_call_argument_by_name(ctx, 'to')
# if to_arg:
# referenced_model = _parse_referenced_model(ctx, to_arg)
# if referenced_model is not None:
# model_fields_metadata[field_name]['to'] = referenced_model.fullname()
# else:
# model_fields_metadata[field_name]['to'] = to_arg.value
# # referenced_model = to_arg.value
# # raise helpers.IncompleteDefnException()
#
# # model_fields_metadata[field_name]['to'] = referenced_model.fullname()
# # if referenced_model is not None:
# # model_fields_metadata[field_name]['to'] = referenced_model.fullname()
# # else:
# # assert isinstance(to_arg, StrExpr)
# # model_fields_metadata[field_name]['to'] = to_arg.value
#
# # related_name
# related_name_arg = helpers.get_call_argument_by_name(ctx, 'related_name')
# if related_name_arg:
# if isinstance(related_name_arg, StrExpr):
# model_fields_metadata[field_name]['related_name'] = related_name_arg.value
# else:
# model_fields_metadata[field_name]['related_name'] = outer_model.name().lower() + '_set'

View File

@@ -0,0 +1,35 @@
from typing import cast
from mypy.checker import TypeChecker
from mypy.nodes import Argument, Var, ARG_NAMED
from mypy.plugin import FunctionContext
from mypy.types import Type as MypyType, Instance
from mypy_django_plugin_newsemanal.context import DjangoContext
from mypy_django_plugin_newsemanal.lib import helpers
def redefine_and_typecheck_model_init(ctx: FunctionContext, django_context: DjangoContext) -> MypyType:
assert isinstance(ctx.default_return_type, Instance)
api = cast(TypeChecker, ctx.api)
model_info = ctx.default_return_type.type
model_cls = django_context.get_model_class_by_fullname(model_info.fullname())
# expected_types = {}
# for field in model_cls._meta.get_fields():
# field_fullname = helpers.get_class_fullname(field.__class__)
# field_info = api.lookup_typeinfo(field_fullname)
# field_set_type = helpers.get_private_descriptor_type(field_info, '_pyi_private_set_type',
# is_nullable=False)
# field_kwarg = Argument(variable=Var(field.attname, field_set_type),
# type_annotation=field_set_type,
# initializer=None,
# kind=ARG_NAMED)
# expected_types[field.attname] = field_set_type
# for field_name, field in model_cls._meta.fields_map.items():
# print()
# print()
return ctx.default_return_type

View File

@@ -0,0 +1,255 @@
import dataclasses
from abc import ABCMeta, abstractmethod
from typing import Optional, Type, cast
from django.db.models.base import Model
from django.db.models.fields.related import ForeignKey
from mypy.newsemanal.semanal import NewSemanticAnalyzer
from mypy.nodes import ARG_NAMED_OPT, Argument, ClassDef, MDEF, SymbolTableNode, TypeInfo, Var
from mypy.plugin import ClassDefContext
from mypy.plugins import common
from mypy.types import AnyType, Instance, NoneType, Type as MypyType, UnionType
from django.contrib.postgres.fields import ArrayField
from django.db.models.fields import CharField, Field
from mypy_django_plugin_newsemanal.context import DjangoContext
from mypy_django_plugin_newsemanal.lib import helpers
from mypy_django_plugin_newsemanal.transformers import fields
from mypy_django_plugin_newsemanal.transformers.fields import get_field_descriptor_types
@dataclasses.dataclass
class ModelClassInitializer(metaclass=ABCMeta):
api: NewSemanticAnalyzer
model_classdef: ClassDef
django_context: DjangoContext
ctx: ClassDefContext
@classmethod
def from_ctx(cls, ctx: ClassDefContext, django_context: DjangoContext):
return cls(api=cast(NewSemanticAnalyzer, ctx.api),
model_classdef=ctx.cls,
django_context=django_context,
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_field_typeinfo_or_incomplete_defn_error(self, field: Field) -> TypeInfo:
fullname = helpers.get_class_fullname(field.__class__)
field_info = self.lookup_typeinfo_or_incomplete_defn_error(fullname)
return field_info
def add_new_node_to_model_class(self, name: str, typ: Instance) -> None:
# 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_initialized_in_class = True
var.is_inferred = True
self.model_classdef.info.names[name] = SymbolTableNode(MDEF, var, plugin_generated=True)
# assert self.model_classdef.info == self.api.type
# self.api.add_symbol_table_node(name, SymbolTableNode(MDEF, var, plugin_generated=True))
def convert_any_to_type(self, typ: MypyType, referred_to_type: MypyType) -> MypyType:
if isinstance(typ, UnionType):
converted_items = []
for item in typ.items:
converted_items.append(self.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)
if isinstance(typ, AnyType):
return referred_to_type
return typ
def get_field_set_type(self, field: Field, method: str) -> MypyType:
target_field = field
if isinstance(field, ForeignKey):
target_field = field.target_field
field_fullname = helpers.get_class_fullname(target_field.__class__)
field_info = self.lookup_typeinfo_or_incomplete_defn_error(field_fullname)
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(target_field.base_field, method)
field_set_type = self.convert_any_to_type(field_set_type, argument_field_type)
return field_set_type
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
return nullable
def get_field_kind(self, field: Field, method: str):
if method == '__init__':
# all arguments are optional in __init__
return ARG_NAMED_OPT
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 make_field_kwarg(self, name: str, field: Field, method: str) -> Argument:
field_set_type = self.get_field_set_type(field, method)
kind = self.get_field_kind(field, method)
field_kwarg = Argument(variable=Var(name, field_set_type),
type_annotation=field_set_type,
initializer=None,
kind=kind)
return field_kwarg
def get_field_kwargs(self, model_cls: Type[Model], method: str):
field_kwargs = []
if method == '__init__':
# add primary key `pk`
primary_key_field = self.get_primary_key_field(model_cls)
field_kwarg = self.make_field_kwarg('pk', primary_key_field, method)
field_kwargs.append(field_kwarg)
for field in model_cls._meta.get_fields():
if isinstance(field, Field):
field_kwarg = self.make_field_kwarg(field.attname, field, method)
field_kwargs.append(field_kwarg)
if isinstance(field, ForeignKey):
attname = field.name
related_model_fullname = helpers.get_class_fullname(field.related_model)
model_info = self.lookup_typeinfo_or_incomplete_defn_error(related_model_fullname)
is_nullable = self.get_field_nullability(field, method)
field_set_type = Instance(model_info, [])
if is_nullable:
field_set_type = helpers.make_optional(field_set_type)
kind = self.get_field_kind(field, method)
field_kwarg = Argument(variable=Var(attname, field_set_type),
type_annotation=field_set_type,
initializer=None,
kind=kind)
field_kwargs.append(field_kwarg)
return field_kwargs
@abstractmethod
def run(self) -> None:
raise NotImplementedError()
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:
return None
meta_node.fallback_to_any = True
class AddDefaultPrimaryKey(ModelClassInitializer):
def run(self) -> None:
model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname)
if model_cls is None:
return
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(self) -> None:
model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname)
if model_cls is None:
return
for field in model_cls._meta.get_fields():
if isinstance(field, ForeignKey):
rel_primary_key_field = self.get_primary_key_field(field.related_model)
field_info = self.lookup_field_typeinfo_or_incomplete_defn_error(rel_primary_key_field)
is_nullable = self.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 run(self):
model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname)
if model_cls is None:
return
for manager_name, manager in model_cls._meta.managers_map.items():
if manager_name not in self.model_classdef.info.names:
manager_fullname = helpers.get_class_fullname(manager.__class__)
manager_info = self.lookup_typeinfo_or_incomplete_defn_error(manager_fullname)
manager = Instance(manager_info, [Instance(self.model_classdef.info, [])])
self.add_new_node_to_model_class(manager_name, manager)
# 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)
class AddInitMethod(ModelClassInitializer):
def run(self):
model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.info.fullname())
if model_cls is None:
return
field_kwargs = self.get_field_kwargs(model_cls, '__init__')
common.add_method(self.ctx, '__init__', field_kwargs, NoneType())
def process_model_class(ctx: ClassDefContext,
django_context: DjangoContext) -> None:
initializers = [
InjectAnyAsBaseForNestedMeta,
AddDefaultPrimaryKey,
AddRelatedModelsId,
AddManagers,
AddInitMethod
]
for initializer_cls in initializers:
try:
initializer_cls.from_ctx(ctx, django_context).run()
except helpers.IncompleteDefnException:
if not ctx.api.final_iteration:
ctx.api.defer()

View File

@@ -0,0 +1,39 @@
from mypy.plugin import AnalyzeTypeContext, FunctionContext
from mypy.types import AnyType, Instance, Type as MypyType, TypeOfAny
from mypy_django_plugin_newsemanal.lib import fullnames, helpers
def set_first_generic_param_as_default_for_second(ctx: AnalyzeTypeContext, fullname: str) -> MypyType:
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]
ctx.api.analyze_type(ctx.type)
try:
return ctx.api.named_type(fullname, analyzed_args)
except KeyError:
return AnyType(TypeOfAny.explicit)
def determine_proper_manager_type(ctx: FunctionContext) -> MypyType:
ret = ctx.default_return_type
assert isinstance(ret, Instance)
if not ctx.api.tscope.classes:
# not in class
return ret
outer_model_info = ctx.api.tscope.classes[0]
if not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME):
return ret
return helpers.reparametrize_instance(ret, [Instance(outer_model_info, [])])

View File

@@ -0,0 +1,18 @@
from mypy.nodes import TypeInfo
from mypy.plugin import FunctionContext
from mypy.types import Type as MypyType, TypeType, Instance
from mypy_django_plugin_newsemanal.context import DjangoContext
from mypy_django_plugin_newsemanal.lib import helpers
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_generic(model_cls_fullname, ctx.api.modules)
assert isinstance(model_info, TypeInfo)
return TypeType(Instance(model_info, []))