fix star import parsing for settings

This commit is contained in:
Maxim Kurnikov
2019-02-14 03:16:07 +03:00
parent f30cd092f1
commit a08ad80a0d
6 changed files with 106 additions and 43 deletions

View File

@@ -33,7 +33,7 @@ from django.db.models.fields.reverse_related import (
) )
from django.db.models.query_utils import PathInfo, Q from django.db.models.query_utils import PathInfo, Q
from django.db.models.expressions import F from django.db.models.expressions import Combinable
if TYPE_CHECKING: if TYPE_CHECKING:
from django.db.models.manager import RelatedManager from django.db.models.manager import RelatedManager
@@ -105,11 +105,12 @@ class ForeignObject(RelatedField):
class ForeignKey(RelatedField, Generic[_T]): class ForeignKey(RelatedField, Generic[_T]):
def __init__(self, to: Union[Type[_T], str], on_delete: Any, related_name: str = ..., **kwargs): ... def __init__(self, to: Union[Type[_T], str], on_delete: Any, related_name: str = ..., **kwargs): ...
def __set__(self, instance, value: Union[Model, F]) -> None: ... def __set__(self, instance, value: Union[Model, Combinable]) -> None: ...
def __get__(self, instance, owner) -> _T: ... def __get__(self, instance, owner) -> _T: ...
class OneToOneField(RelatedField, Generic[_T]): class OneToOneField(RelatedField, Generic[_T]):
def __init__(self, to: Union[Type[_T], str], on_delete: Any, related_name: str = ..., **kwargs): ... def __init__(self, to: Union[Type[_T], str], on_delete: Any, related_name: str = ..., **kwargs): ...
def __set__(self, instance, value: Union[Model, Combinable]) -> None: ...
def __get__(self, instance, owner) -> _T: ... def __get__(self, instance, owner) -> _T: ...
class ManyToManyField(RelatedField, Generic[_T]): class ManyToManyField(RelatedField, Generic[_T]):

View File

@@ -1,5 +1,5 @@
from configparser import ConfigParser from configparser import ConfigParser
from typing import Optional from typing import List, Optional
from dataclasses import dataclass from dataclasses import dataclass
@@ -15,7 +15,11 @@ class Config:
ini_config.read(fpath) ini_config.read(fpath)
if not ini_config.has_section('mypy_django_plugin'): if not ini_config.has_section('mypy_django_plugin'):
raise ValueError('Invalid config file: no [mypy_django_plugin] section') raise ValueError('Invalid config file: no [mypy_django_plugin] section')
return Config(django_settings_module=ini_config.get('mypy_django_plugin', 'django_settings',
fallback=None), 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=ini_config.get('mypy_django_plugin', 'ignore_missing_settings', ignore_missing_settings=ini_config.get('mypy_django_plugin', 'ignore_missing_settings',
fallback=False)) fallback=False))

View File

@@ -2,11 +2,10 @@ import os
from typing import Callable, Dict, Optional, cast from typing import Callable, Dict, Optional, cast
from mypy.checker import TypeChecker from mypy.checker import TypeChecker
from mypy.nodes import TypeInfo from mypy.nodes import MemberExpr, TypeInfo
from mypy.options import Options from mypy.options import Options
from mypy.plugin import ClassDefContext, FunctionContext, MethodContext, Plugin, AttributeContext from mypy.plugin import AttributeContext, ClassDefContext, FunctionContext, MethodContext, Plugin
from mypy.types import Instance, Type, TypeType, AnyType, TypeOfAny from mypy.types import AnyType, Instance, Type, TypeOfAny, TypeType
from mypy_django_plugin import helpers, monkeypatch from mypy_django_plugin import helpers, monkeypatch
from mypy_django_plugin.config import Config from mypy_django_plugin.config import Config
from mypy_django_plugin.plugins import init_create from mypy_django_plugin.plugins import init_create
@@ -14,7 +13,7 @@ from mypy_django_plugin.plugins.fields import determine_type_of_array_field, rec
from mypy_django_plugin.plugins.migrations import determine_model_cls_from_string_for_migrations, get_string_value_from_expr from mypy_django_plugin.plugins.migrations import determine_model_cls_from_string_for_migrations, get_string_value_from_expr
from mypy_django_plugin.plugins.models import process_model_class from mypy_django_plugin.plugins.models import process_model_class
from mypy_django_plugin.plugins.related_fields import extract_to_parameter_as_get_ret_type_for_related_field, reparametrize_with from mypy_django_plugin.plugins.related_fields import extract_to_parameter_as_get_ret_type_for_related_field, reparametrize_with
from mypy_django_plugin.plugins.settings import AddSettingValuesToDjangoConfObject from mypy_django_plugin.plugins.settings import AddSettingValuesToDjangoConfObject, SettingContext, get_settings_metadata
def transform_model_class(ctx: ClassDefContext) -> None: def transform_model_class(ctx: ClassDefContext) -> None:
@@ -106,6 +105,25 @@ def extract_and_return_primary_key_of_bound_related_field_parameter(ctx: Attribu
return ctx.default_attr_type return ctx.default_attr_type
class ExtractSettingType:
def __init__(self, module_fullname: str):
self.module_fullname = module_fullname
def __call__(self, ctx: AttributeContext) -> Type:
api = cast(TypeChecker, ctx.api)
original_module = api.modules.get(self.module_fullname)
if original_module is None:
return ctx.default_attr_type
definition = ctx.context
if isinstance(definition, MemberExpr):
sym = original_module.names.get(definition.name)
if sym and sym.type:
return sym.type
return ctx.default_attr_type
class DjangoPlugin(Plugin): class DjangoPlugin(Plugin):
def __init__(self, options: Options) -> None: def __init__(self, options: Options) -> None:
super().__init__(options) super().__init__(options)
@@ -116,17 +134,17 @@ class DjangoPlugin(Plugin):
config_fpath = os.environ.get('MYPY_DJANGO_CONFIG', 'mypy_django.ini') config_fpath = os.environ.get('MYPY_DJANGO_CONFIG', 'mypy_django.ini')
if config_fpath and os.path.exists(config_fpath): if config_fpath and os.path.exists(config_fpath):
self.config = Config.from_config_file(config_fpath) self.config = Config.from_config_file(config_fpath)
self.django_settings = self.config.django_settings_module self.django_settings_module = self.config.django_settings_module
else: else:
self.config = Config() self.config = Config()
self.django_settings = None self.django_settings_module = None
if 'DJANGO_SETTINGS_MODULE' in os.environ: if 'DJANGO_SETTINGS_MODULE' in os.environ:
self.django_settings = os.environ['DJANGO_SETTINGS_MODULE'] self.django_settings_module = os.environ['DJANGO_SETTINGS_MODULE']
settings_modules = ['django.conf.global_settings'] settings_modules = ['django.conf.global_settings']
if self.django_settings: if self.django_settings_module:
settings_modules.append(self.django_settings) settings_modules.append(self.django_settings_module)
monkeypatch.add_modules_as_a_source_seed_files(settings_modules) monkeypatch.add_modules_as_a_source_seed_files(settings_modules)
monkeypatch.inject_modules_as_dependencies_for_django_conf_settings(settings_modules) monkeypatch.inject_modules_as_dependencies_for_django_conf_settings(settings_modules)
@@ -197,8 +215,8 @@ class DjangoPlugin(Plugin):
if fullname == helpers.DUMMY_SETTINGS_BASE_CLASS: if fullname == helpers.DUMMY_SETTINGS_BASE_CLASS:
settings_modules = ['django.conf.global_settings'] settings_modules = ['django.conf.global_settings']
if self.django_settings: if self.django_settings_module:
settings_modules.append(self.django_settings) settings_modules.append(self.django_settings_module)
return AddSettingValuesToDjangoConfObject(settings_modules, return AddSettingValuesToDjangoConfObject(settings_modules,
self.config.ignore_missing_settings) self.config.ignore_missing_settings)
@@ -209,11 +227,14 @@ class DjangoPlugin(Plugin):
def get_attribute_hook(self, fullname: str def get_attribute_hook(self, fullname: str
) -> Optional[Callable[[AttributeContext], Type]]: ) -> Optional[Callable[[AttributeContext], Type]]:
# sym = self.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME) module, _, name = fullname.rpartition('.')
# if sym and isinstance(sym.node, TypeInfo): sym = self.lookup_fully_qualified('django.conf.LazySettings')
# if fullname.rpartition('.')[-1] in helpers.get_related_field_primary_key_names(sym.node): if sym and isinstance(sym.node, TypeInfo):
return extract_and_return_primary_key_of_bound_related_field_parameter metadata = get_settings_metadata(sym.node)
if module == 'builtins.object' and name in metadata:
return ExtractSettingType(module_fullname=metadata[name])
return extract_and_return_primary_key_of_bound_related_field_parameter
def plugin(version): def plugin(version):

View File

@@ -127,16 +127,17 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo) -> Dict[str, T
if name in {'_meta', 'pk'}: if name in {'_meta', 'pk'}:
continue continue
if isinstance(sym.node, Var): if isinstance(sym.node, Var):
if isinstance(sym.node.type, Instance): if sym.node.type is None or isinstance(sym.node.type, 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(sym.node.type, Instance):
tp = sym.node.type tp = sym.node.type
field_type = extract_field_setter_type(tp) field_type = extract_field_setter_type(tp)
if field_type is None: if field_type is None:
continue continue
choices_type_fullname = extract_choices_type(model, name)
if choices_type_fullname:
field_type = UnionType([field_type, ctx.api.named_generic_type(choices_type_fullname, [])])
if tp.type.fullname() in {helpers.FOREIGN_KEY_FULLNAME, helpers.ONETOONE_FIELD_FULLNAME}: if tp.type.fullname() in {helpers.FOREIGN_KEY_FULLNAME, helpers.ONETOONE_FIELD_FULLNAME}:
ref_to_model = tp.args[0] ref_to_model = tp.args[0]
primary_key_type = AnyType(TypeOfAny.special_form) primary_key_type = AnyType(TypeOfAny.special_form)
@@ -145,9 +146,7 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo) -> Dict[str, T
if typ: if typ:
primary_key_type = typ primary_key_type = typ
expected_types[name + '_id'] = primary_key_type expected_types[name + '_id'] = primary_key_type
if field_type: if field_type:
expected_types[name] = field_type expected_types[name] = field_type
elif isinstance(sym.node.type, AnyType):
expected_types[name] = sym.node.type
return expected_types return expected_types

View File

@@ -1,9 +1,10 @@
from typing import List, Optional, cast from typing import Iterable, List, Optional, cast
from mypy.nodes import ClassDef, Context, MypyFile, SymbolNode, SymbolTableNode, Var from mypy.nodes import ClassDef, Context, ImportAll, MypyFile, SymbolNode, SymbolTableNode, TypeInfo, Var
from mypy.plugin import ClassDefContext from mypy.plugin import ClassDefContext
from mypy.semanal import SemanticAnalyzerPass2 from mypy.semanal import SemanticAnalyzerPass2
from mypy.types import Instance, NoneTyp, Type, UnionType from mypy.types import AnyType, Instance, NoneTyp, Type, TypeOfAny, UnionType
from mypy.util import correct_relative_import
def get_error_context(node: SymbolNode) -> Context: def get_error_context(node: SymbolNode) -> Context:
@@ -36,14 +37,44 @@ def make_sym_copy_of_setting(sym: SymbolTableNode) -> Optional[SymbolTableNode]:
return None return None
def load_settings_from_module(settings_classdef: ClassDef, module: MypyFile) -> None: def get_settings_metadata(lazy_settings_info: TypeInfo):
for name, sym in module.names.items(): return lazy_settings_info.metadata.setdefault('django', {}).setdefault('settings', {})
if name.isupper() and isinstance(sym.node, Var):
if sym.type is not None:
copied = make_sym_copy_of_setting(sym) def load_settings_from_names(settings_classdef: ClassDef,
if copied is None: modules: Iterable[MypyFile],
continue api: SemanticAnalyzerPass2) -> None:
settings_classdef.info.names[name] = copied settings_metadata = get_settings_metadata(settings_classdef.info)
for module in modules:
for name, sym in module.names.items():
if name.isupper() and isinstance(sym.node, Var):
if sym.type is not None:
copied = make_sym_copy_of_setting(sym)
if copied is None:
continue
settings_classdef.info.names[name] = copied
else:
var = Var(name, AnyType(TypeOfAny.unannotated))
var.info = api.named_type('__builtins__.object').type
settings_classdef.info.names[name] = SymbolTableNode(sym.kind, var)
settings_metadata[name] = module.fullname()
def get_import_star_modules(api: SemanticAnalyzerPass2, module: MypyFile) -> List[str]:
import_star_modules = []
for module_import in module.imports:
# relative import * are not resolved by mypy
if isinstance(module_import, ImportAll) and module_import.relative:
absolute_import_path, correct = correct_relative_import(module.fullname(), module_import.relative, module_import.id,
is_cur_package_init_file=False)
if not correct:
return []
for path in [absolute_import_path] + get_import_star_modules(api, module=api.modules.get(absolute_import_path)):
if path not in import_star_modules:
import_star_modules.append(path)
return import_star_modules
class AddSettingValuesToDjangoConfObject: class AddSettingValuesToDjangoConfObject:
@@ -55,7 +86,9 @@ class AddSettingValuesToDjangoConfObject:
api = cast(SemanticAnalyzerPass2, ctx.api) api = cast(SemanticAnalyzerPass2, ctx.api)
for module_name in self.settings_modules: for module_name in self.settings_modules:
module = api.modules[module_name] module = api.modules[module_name]
load_settings_from_module(ctx.cls, module=module) star_deps = [api.modules[star_dep]
for star_dep in reversed(get_import_star_modules(api, module))]
load_settings_from_names(ctx.cls, modules=star_deps + [module], api=api)
if self.ignore_missing_settings: if self.ignore_missing_settings:
ctx.cls.info.fallback_to_any = True ctx.cls.info.fallback_to_any = True

View File

@@ -2,13 +2,18 @@
from django.conf import settings from django.conf import settings
reveal_type(settings.ROOT_DIR) # E: Revealed type is 'builtins.str' reveal_type(settings.ROOT_DIR) # E: Revealed type is 'builtins.str'
reveal_type(settings.APPS_DIR) # E: Revealed type is 'pathlib.Path'
reveal_type(settings.OBJ) # E: Revealed type is 'django.utils.functional.LazyObject' reveal_type(settings.OBJ) # E: Revealed type is 'django.utils.functional.LazyObject'
reveal_type(settings.NUMBERS) # E: Revealed type is 'builtins.list[builtins.str]' reveal_type(settings.NUMBERS) # E: Revealed type is 'builtins.list[builtins.str]'
reveal_type(settings.DICT) # E: Revealed type is 'builtins.dict[Any, Any]' reveal_type(settings.DICT) # E: Revealed type is 'builtins.dict[Any, Any]'
[env DJANGO_SETTINGS_MODULE=mysettings] [env DJANGO_SETTINGS_MODULE=mysettings]
[file mysettings.py] [file base.py]
SECRET_KEY = 112233 from pathlib import Path
ROOT_DIR = '/etc' ROOT_DIR = '/etc'
APPS_DIR = Path(ROOT_DIR)
[file mysettings.py]
from base import *
SECRET_KEY = 112233
NUMBERS = ['one', 'two'] NUMBERS = ['one', 'two']
DICT = {} # type: ignore DICT = {} # type: ignore
from django.utils.functional import LazyObject from django.utils.functional import LazyObject