add settings support

This commit is contained in:
Maxim Kurnikov
2019-07-16 19:09:05 +03:00
parent 7b1b1b6bfe
commit 3c3122a93f
7 changed files with 63 additions and 56 deletions

View File

@@ -87,16 +87,16 @@ class DjangoContext:
self.config = DjangoPluginConfig() self.config = DjangoPluginConfig()
self.fields_context = DjangoFieldsContext() self.fields_context = DjangoFieldsContext()
django_settings_module = None self.django_settings_module = None
if plugin_toml_config: if plugin_toml_config:
self.config.ignore_missing_settings = plugin_toml_config.get('ignore_missing_settings', False) 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) 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.django_settings_module = plugin_toml_config.get('django_settings_module', None)
self.apps_registry: Optional[Dict[str, str]] = None self.apps_registry: Optional[Dict[str, str]] = None
self.settings: LazySettings = None self.settings: LazySettings = None
if django_settings_module: if self.django_settings_module:
apps, settings = initialize_django(django_settings_module) apps, settings = initialize_django(self.django_settings_module)
self.apps_registry = apps self.apps_registry = apps
self.settings = settings self.settings = settings

View File

@@ -1,7 +1,7 @@
from typing import Dict, List, Optional, Set, Union from typing import Dict, List, Optional, Set, Union
from mypy.checker import TypeChecker from mypy.checker import TypeChecker
from mypy.nodes import Expression, MypyFile, NameExpr, SymbolNode, TypeInfo, Var from mypy.nodes import Expression, MypyFile, NameExpr, SymbolNode, TypeInfo, Var, SymbolTableNode
from mypy.plugin import FunctionContext, MethodContext from mypy.plugin import FunctionContext, MethodContext
from mypy.types import AnyType, Instance, NoneTyp, Type as MypyType, TypeOfAny, UnionType from mypy.types import AnyType, Instance, NoneTyp, Type as MypyType, TypeOfAny, UnionType
@@ -10,15 +10,22 @@ class IncompleteDefnException(Exception):
pass pass
def lookup_fully_qualified_generic(name: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolNode]: def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolTableNode]:
if '.' not in name: if '.' not in fullname:
return None return None
module, cls_name = name.rsplit('.', 1) module, cls_name = fullname.rsplit('.', 1)
module_file = all_modules.get(module) module_file = all_modules.get(module)
if module_file is None: if module_file is None:
return None return None
sym = module_file.names.get(cls_name) 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: if sym is None:
return None return None
return sym.node return sym.node

View File

@@ -5,7 +5,7 @@ from typing import Callable, Dict, List, Optional, Tuple, Type
import toml import toml
from mypy.nodes import MypyFile, TypeInfo from mypy.nodes import MypyFile, TypeInfo
from mypy.options import Options from mypy.options import Options
from mypy.plugin import ClassDefContext, FunctionContext, Plugin, MethodContext from mypy.plugin import ClassDefContext, FunctionContext, Plugin, MethodContext, AttributeContext
from mypy.types import Type as MypyType from mypy.types import Type as MypyType
from django.db.models.fields.related import RelatedField from django.db.models.fields.related import RelatedField
@@ -81,6 +81,10 @@ class NewSemanalDjangoPlugin(Plugin):
return 10, module, -1 return 10, module, -1
def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]: 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 `get_user_model()` # for `get_user_model()`
if file.fullname() == 'django.contrib.auth': if file.fullname() == 'django.contrib.auth':
auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL
@@ -142,34 +146,12 @@ class NewSemanalDjangoPlugin(Plugin):
if fullname in self._get_current_manager_bases(): if fullname in self._get_current_manager_bases():
return add_new_manager_base return add_new_manager_base
# def get_attribute_hook(self, fullname: str def get_attribute_hook(self, fullname: str
# ) -> Optional[Callable[[AttributeContext], MypyType]]: ) -> Optional[Callable[[AttributeContext], MypyType]]:
# print(fullname) class_name, _, attr_name = fullname.rpartition('.')
# class_name, _, attr_name = fullname.rpartition('.') if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS:
# # if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS: return partial(settings.get_type_of_settings_attribute,
# # return partial(get_type_of_setting, django_context=self.django_context)
# # 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 # def get_type_analyze_hook(self, fullname: str
# ) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]: # ) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]:

View File

@@ -1,5 +1,5 @@
from mypy.nodes import TypeInfo from mypy.nodes import TypeInfo, MemberExpr
from mypy.plugin import FunctionContext from mypy.plugin import FunctionContext, AttributeContext
from mypy.types import Type as MypyType, TypeType, Instance from mypy.types import Type as MypyType, TypeType, Instance
from mypy_django_plugin_newsemanal.django.context import DjangoContext from mypy_django_plugin_newsemanal.django.context import DjangoContext
@@ -16,3 +16,30 @@ def get_user_model_hook(ctx: FunctionContext, django_context: DjangoContext) ->
return TypeType(Instance(model_info, [])) return TypeType(Instance(model_info, []))
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
# first look for the setting in the project settings file, then global settings
settings_module = ctx.api.modules.get(django_context.django_settings_module)
global_settings_module = ctx.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
# 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__)
value_info = helpers.lookup_fully_qualified_typeinfo(ctx.api, value_fullname)
if value_info is None:
return ctx.default_attr_type
return Instance(value_info, [])

View File

@@ -24,9 +24,12 @@ class NewSemanalDjangoTestItem(YamlTestItem):
name, _, val = item.partition('=') name, _, val = item.partition('=')
settings[name] = val settings[name] = val
installed_apps = self.parsed_test_data.get('installed_apps') installed_apps = self.parsed_test_data.get('installed_apps', None)
if installed_apps: if installed_apps is not None:
installed_apps_as_str = '(' + ','.join([repr(app) for app in installed_apps]) + ',)' if not installed_apps:
installed_apps_as_str = '()'
else:
installed_apps_as_str = '(' + ','.join([repr(app) for app in installed_apps]) + ',)'
pyproject_toml_file = File(path='pyproject.toml', pyproject_toml_file = File(path='pyproject.toml',
content='[tool.django-stubs]\ndjango_settings_module=\'mysettings\'') content='[tool.django-stubs]\ndjango_settings_module=\'mysettings\'')

View File

@@ -24,16 +24,6 @@
if TYPE_CHECKING: if TYPE_CHECKING:
reveal_type(MyModel.user) # N: Revealed type is 'django.contrib.auth.models.User*' reveal_type(MyModel.user) # N: Revealed type is 'django.contrib.auth.models.User*'
- case: missing_settings_ignored_flag
main: |
from django.conf import settings
reveal_type(settings.NO_SUCH_SETTING) # N: Revealed type is 'Any'
files:
- path: pyproject.toml
content: |
[tool.django-stubs]
ignore_missing_settings = true
- case: generate_pyproject_toml_and_settings_file_from_installed_apps_key - case: generate_pyproject_toml_and_settings_file_from_installed_apps_key
main: | main: |
from myapp.models import MyModel from myapp.models import MyModel

View File

@@ -1,11 +1,11 @@
- case: settings_loaded_from_different_files - case: settings_loaded_from_different_files
disable_cache: true
main: | main: |
from django.conf import settings from django.conf import settings
# standard settings # standard settings
reveal_type(settings.AUTH_USER_MODEL) # N: Revealed type is 'builtins.str' 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.ROOT_DIR) # N: Revealed type is 'builtins.str'
reveal_type(settings.APPS_DIR) # N: Revealed type is 'pathlib.Path' 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.NUMBERS) # N: Revealed type is 'builtins.list[builtins.str*]'
reveal_type(settings.DICT) # N: Revealed type is 'builtins.dict[Any, Any]' reveal_type(settings.DICT) # N: Revealed type is 'builtins.dict[Any, Any]'
files: files:
@@ -19,9 +19,6 @@
SECRET_KEY = 112233 SECRET_KEY = 112233
NUMBERS = ['one', 'two'] NUMBERS = ['one', 'two']
DICT = {} # type: ignore DICT = {} # type: ignore
from django.utils.functional import LazyObject
OBJ = LazyObject()
- path: base.py - path: base.py
content: | content: |
from pathlib import Path from pathlib import Path
@@ -33,6 +30,7 @@
from django.conf import settings from django.conf import settings
reveal_type(settings.AUTH_USER_MODEL) # N: Revealed type is 'builtins.str' 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]' reveal_type(settings.AUTHENTICATION_BACKENDS) # N: Revealed type is 'typing.Sequence[builtins.str]'
installed_apps: []
- case: fail_if_there_is_no_setting - case: fail_if_there_is_no_setting
main: | main: |