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.expressions import F
from django.db.models.expressions import Combinable
if TYPE_CHECKING:
from django.db.models.manager import RelatedManager
@@ -105,11 +105,12 @@ class ForeignObject(RelatedField):
class ForeignKey(RelatedField, Generic[_T]):
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: ...
class OneToOneField(RelatedField, Generic[_T]):
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: ...
class ManyToManyField(RelatedField, Generic[_T]):

View File

@@ -1,5 +1,5 @@
from configparser import ConfigParser
from typing import Optional
from typing import List, Optional
from dataclasses import dataclass
@@ -15,7 +15,11 @@ class Config:
ini_config.read(fpath)
if not ini_config.has_section('mypy_django_plugin'):
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',
fallback=False))

View File

@@ -2,11 +2,10 @@ import os
from typing import Callable, Dict, Optional, cast
from mypy.checker import TypeChecker
from mypy.nodes import TypeInfo
from mypy.nodes import MemberExpr, TypeInfo
from mypy.options import Options
from mypy.plugin import ClassDefContext, FunctionContext, MethodContext, Plugin, AttributeContext
from mypy.types import Instance, Type, TypeType, AnyType, TypeOfAny
from mypy.plugin import AttributeContext, ClassDefContext, FunctionContext, MethodContext, Plugin
from mypy.types import AnyType, Instance, Type, TypeOfAny, TypeType
from mypy_django_plugin import helpers, monkeypatch
from mypy_django_plugin.config import Config
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.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.settings import AddSettingValuesToDjangoConfObject
from mypy_django_plugin.plugins.settings import AddSettingValuesToDjangoConfObject, SettingContext, get_settings_metadata
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
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):
def __init__(self, options: Options) -> None:
super().__init__(options)
@@ -116,17 +134,17 @@ class DjangoPlugin(Plugin):
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 = self.config.django_settings_module
self.django_settings_module = self.config.django_settings_module
else:
self.config = Config()
self.django_settings = None
self.django_settings_module = None
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']
if self.django_settings:
settings_modules.append(self.django_settings)
if self.django_settings_module:
settings_modules.append(self.django_settings_module)
monkeypatch.add_modules_as_a_source_seed_files(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:
settings_modules = ['django.conf.global_settings']
if self.django_settings:
settings_modules.append(self.django_settings)
if self.django_settings_module:
settings_modules.append(self.django_settings_module)
return AddSettingValuesToDjangoConfObject(settings_modules,
self.config.ignore_missing_settings)
@@ -209,11 +227,14 @@ class DjangoPlugin(Plugin):
def get_attribute_hook(self, fullname: str
) -> Optional[Callable[[AttributeContext], Type]]:
# sym = self.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME)
# if sym and isinstance(sym.node, TypeInfo):
# if fullname.rpartition('.')[-1] in helpers.get_related_field_primary_key_names(sym.node):
return extract_and_return_primary_key_of_bound_related_field_parameter
module, _, name = fullname.rpartition('.')
sym = self.lookup_fully_qualified('django.conf.LazySettings')
if sym and isinstance(sym.node, TypeInfo):
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):

View File

@@ -127,16 +127,17 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo) -> Dict[str, T
if name in {'_meta', 'pk'}:
continue
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
field_type = extract_field_setter_type(tp)
if field_type is None:
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}:
ref_to_model = tp.args[0]
primary_key_type = AnyType(TypeOfAny.special_form)
@@ -145,9 +146,7 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo) -> Dict[str, T
if typ:
primary_key_type = typ
expected_types[name + '_id'] = primary_key_type
if field_type:
expected_types[name] = field_type
elif isinstance(sym.node.type, AnyType):
expected_types[name] = sym.node.type
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.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:
@@ -36,14 +37,44 @@ def make_sym_copy_of_setting(sym: SymbolTableNode) -> Optional[SymbolTableNode]:
return None
def load_settings_from_module(settings_classdef: ClassDef, module: MypyFile) -> None:
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
def get_settings_metadata(lazy_settings_info: TypeInfo):
return lazy_settings_info.metadata.setdefault('django', {}).setdefault('settings', {})
def load_settings_from_names(settings_classdef: ClassDef,
modules: Iterable[MypyFile],
api: SemanticAnalyzerPass2) -> None:
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:
@@ -55,7 +86,9 @@ class AddSettingValuesToDjangoConfObject:
api = cast(SemanticAnalyzerPass2, ctx.api)
for module_name in self.settings_modules:
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:
ctx.cls.info.fallback_to_any = True

View File

@@ -2,13 +2,18 @@
from django.conf import settings
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.NUMBERS) # E: Revealed type is 'builtins.list[builtins.str]'
reveal_type(settings.DICT) # E: Revealed type is 'builtins.dict[Any, Any]'
[env DJANGO_SETTINGS_MODULE=mysettings]
[file mysettings.py]
SECRET_KEY = 112233
[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