From 3c3122a93f65e6b34f6ba226d14b417e1f3ce1ca Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Tue, 16 Jul 2019 19:09:05 +0300 Subject: [PATCH] add settings support --- .../django/context.py | 8 ++-- mypy_django_plugin_newsemanal/lib/helpers.py | 15 +++++-- mypy_django_plugin_newsemanal/main.py | 40 +++++-------------- .../transformers/settings.py | 31 +++++++++++++- pytest_plugin/collect.py | 9 +++-- .../typecheck/test_config.yml | 10 ----- .../typecheck/test_settings.yml | 6 +-- 7 files changed, 63 insertions(+), 56 deletions(-) diff --git a/mypy_django_plugin_newsemanal/django/context.py b/mypy_django_plugin_newsemanal/django/context.py index bf67cda..54cb912 100644 --- a/mypy_django_plugin_newsemanal/django/context.py +++ b/mypy_django_plugin_newsemanal/django/context.py @@ -87,16 +87,16 @@ class DjangoContext: self.config = DjangoPluginConfig() self.fields_context = DjangoFieldsContext() - django_settings_module = None + self.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.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) + if self.django_settings_module: + apps, settings = initialize_django(self.django_settings_module) self.apps_registry = apps self.settings = settings diff --git a/mypy_django_plugin_newsemanal/lib/helpers.py b/mypy_django_plugin_newsemanal/lib/helpers.py index 7915ecf..0dc68e7 100644 --- a/mypy_django_plugin_newsemanal/lib/helpers.py +++ b/mypy_django_plugin_newsemanal/lib/helpers.py @@ -1,7 +1,7 @@ from typing import Dict, List, Optional, Set, Union 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.types import AnyType, Instance, NoneTyp, Type as MypyType, TypeOfAny, UnionType @@ -10,15 +10,22 @@ class IncompleteDefnException(Exception): pass -def lookup_fully_qualified_generic(name: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolNode]: - if '.' not in name: +def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolTableNode]: + if '.' not in fullname: return None - module, cls_name = name.rsplit('.', 1) + 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 diff --git a/mypy_django_plugin_newsemanal/main.py b/mypy_django_plugin_newsemanal/main.py index 2205047..9d42ecb 100644 --- a/mypy_django_plugin_newsemanal/main.py +++ b/mypy_django_plugin_newsemanal/main.py @@ -5,7 +5,7 @@ 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.plugin import ClassDefContext, FunctionContext, Plugin, MethodContext, AttributeContext from mypy.types import Type as MypyType from django.db.models.fields.related import RelatedField @@ -81,6 +81,10 @@ class NewSemanalDjangoPlugin(Plugin): return 10, module, -1 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()` if file.fullname() == 'django.contrib.auth': 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(): return add_new_manager_base - # 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_attribute_hook(self, fullname: str + ) -> Optional[Callable[[AttributeContext], MypyType]]: + class_name, _, attr_name = fullname.rpartition('.') + 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], MypyType]]: diff --git a/mypy_django_plugin_newsemanal/transformers/settings.py b/mypy_django_plugin_newsemanal/transformers/settings.py index 6abfc55..dc3458c 100644 --- a/mypy_django_plugin_newsemanal/transformers/settings.py +++ b/mypy_django_plugin_newsemanal/transformers/settings.py @@ -1,5 +1,5 @@ -from mypy.nodes import TypeInfo -from mypy.plugin import FunctionContext +from mypy.nodes import TypeInfo, MemberExpr +from mypy.plugin import FunctionContext, AttributeContext from mypy.types import Type as MypyType, TypeType, Instance 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, [])) + +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, []) + diff --git a/pytest_plugin/collect.py b/pytest_plugin/collect.py index 4bed7e7..ded17b2 100644 --- a/pytest_plugin/collect.py +++ b/pytest_plugin/collect.py @@ -24,9 +24,12 @@ class NewSemanalDjangoTestItem(YamlTestItem): name, _, val = item.partition('=') settings[name] = val - installed_apps = self.parsed_test_data.get('installed_apps') - if installed_apps: - installed_apps_as_str = '(' + ','.join([repr(app) for app in installed_apps]) + ',)' + installed_apps = self.parsed_test_data.get('installed_apps', None) + if installed_apps is not None: + 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', content='[tool.django-stubs]\ndjango_settings_module=\'mysettings\'') diff --git a/test-data-newsemanal/typecheck/test_config.yml b/test-data-newsemanal/typecheck/test_config.yml index e228f7d..b9698f4 100644 --- a/test-data-newsemanal/typecheck/test_config.yml +++ b/test-data-newsemanal/typecheck/test_config.yml @@ -24,16 +24,6 @@ if TYPE_CHECKING: 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 main: | from myapp.models import MyModel diff --git a/test-data-newsemanal/typecheck/test_settings.yml b/test-data-newsemanal/typecheck/test_settings.yml index 6ddbda0..3e0a8d3 100644 --- a/test-data-newsemanal/typecheck/test_settings.yml +++ b/test-data-newsemanal/typecheck/test_settings.yml @@ -1,11 +1,11 @@ - 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.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]' files: @@ -19,9 +19,6 @@ SECRET_KEY = 112233 NUMBERS = ['one', 'two'] DICT = {} # type: ignore - - from django.utils.functional import LazyObject - OBJ = LazyObject() - path: base.py content: | from pathlib import Path @@ -33,6 +30,7 @@ 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: |