Skip to content

Commit 3c3122a

Browse files
committed
add settings support
1 parent 7b1b1b6 commit 3c3122a

File tree

7 files changed

+63
-56
lines changed

7 files changed

+63
-56
lines changed

mypy_django_plugin_newsemanal/django/context.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,16 @@ def __init__(self, plugin_toml_config: Optional[Dict[str, Any]]) -> None:
8787
self.config = DjangoPluginConfig()
8888
self.fields_context = DjangoFieldsContext()
8989

90-
django_settings_module = None
90+
self.django_settings_module = None
9191
if plugin_toml_config:
9292
self.config.ignore_missing_settings = plugin_toml_config.get('ignore_missing_settings', False)
9393
self.config.ignore_missing_model_attributes = plugin_toml_config.get('ignore_missing_model_attributes', False)
94-
django_settings_module = plugin_toml_config.get('django_settings_module', None)
94+
self.django_settings_module = plugin_toml_config.get('django_settings_module', None)
9595

9696
self.apps_registry: Optional[Dict[str, str]] = None
9797
self.settings: LazySettings = None
98-
if django_settings_module:
99-
apps, settings = initialize_django(django_settings_module)
98+
if self.django_settings_module:
99+
apps, settings = initialize_django(self.django_settings_module)
100100
self.apps_registry = apps
101101
self.settings = settings
102102

mypy_django_plugin_newsemanal/lib/helpers.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Dict, List, Optional, Set, Union
22

33
from mypy.checker import TypeChecker
4-
from mypy.nodes import Expression, MypyFile, NameExpr, SymbolNode, TypeInfo, Var
4+
from mypy.nodes import Expression, MypyFile, NameExpr, SymbolNode, TypeInfo, Var, SymbolTableNode
55
from mypy.plugin import FunctionContext, MethodContext
66
from mypy.types import AnyType, Instance, NoneTyp, Type as MypyType, TypeOfAny, UnionType
77

@@ -10,15 +10,22 @@ class IncompleteDefnException(Exception):
1010
pass
1111

1212

13-
def lookup_fully_qualified_generic(name: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolNode]:
14-
if '.' not in name:
13+
def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolTableNode]:
14+
if '.' not in fullname:
1515
return None
16-
module, cls_name = name.rsplit('.', 1)
16+
module, cls_name = fullname.rsplit('.', 1)
1717

1818
module_file = all_modules.get(module)
1919
if module_file is None:
2020
return None
2121
sym = module_file.names.get(cls_name)
22+
if sym is None:
23+
return None
24+
return sym
25+
26+
27+
def lookup_fully_qualified_generic(name: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolNode]:
28+
sym = lookup_fully_qualified_sym(name, all_modules)
2229
if sym is None:
2330
return None
2431
return sym.node

mypy_django_plugin_newsemanal/main.py

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import toml
66
from mypy.nodes import MypyFile, TypeInfo
77
from mypy.options import Options
8-
from mypy.plugin import ClassDefContext, FunctionContext, Plugin, MethodContext
8+
from mypy.plugin import ClassDefContext, FunctionContext, Plugin, MethodContext, AttributeContext
99
from mypy.types import Type as MypyType
1010

1111
from django.db.models.fields.related import RelatedField
@@ -81,6 +81,10 @@ def _new_dependency(self, module: str) -> Tuple[int, str, int]:
8181
return 10, module, -1
8282

8383
def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
84+
# for settings
85+
if file.fullname() == 'django.conf' and self.django_context.django_settings_module:
86+
return [self._new_dependency(self.django_context.django_settings_module)]
87+
8488
# for `get_user_model()`
8589
if file.fullname() == 'django.contrib.auth':
8690
auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL
@@ -142,34 +146,12 @@ def get_base_class_hook(self, fullname: str
142146
if fullname in self._get_current_manager_bases():
143147
return add_new_manager_base
144148

145-
# def get_attribute_hook(self, fullname: str
146-
# ) -> Optional[Callable[[AttributeContext], MypyType]]:
147-
# print(fullname)
148-
# class_name, _, attr_name = fullname.rpartition('.')
149-
# # if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS:
150-
# # return partial(get_type_of_setting,
151-
# # setting_name=attr_name,
152-
# # settings_modules=self._get_settings_modules_in_order_of_priority(),
153-
# # ignore_missing_settings=self.config.ignore_missing_settings)
154-
#
155-
# if class_name in self._get_current_model_bases():
156-
# # if attr_name == 'id':
157-
# # return return_type_for_id_field
158-
#
159-
# model_info = self._get_typeinfo_or_none(class_name)
160-
# if model_info:
161-
# attr_sym = model_info.get(attr_name)
162-
# if attr_sym and isinstance(attr_sym.node, TypeInfo) \
163-
# and helpers.has_any_of_bases(attr_sym.node, fullnames.MANAGER_CLASSES):
164-
# return partial(querysets.determite_manager_type, django_context=self.django_context)
165-
#
166-
# # related_managers = metadata.get_related_managers_metadata(model_info)
167-
# # if attr_name in related_managers:
168-
# # return partial(determine_type_of_related_manager,
169-
# # related_manager_name=attr_name)
170-
#
171-
# # if attr_name.endswith('_id'):
172-
# # return extract_and_return_primary_key_of_bound_related_field_parameter
149+
def get_attribute_hook(self, fullname: str
150+
) -> Optional[Callable[[AttributeContext], MypyType]]:
151+
class_name, _, attr_name = fullname.rpartition('.')
152+
if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS:
153+
return partial(settings.get_type_of_settings_attribute,
154+
django_context=self.django_context)
173155

174156
# def get_type_analyze_hook(self, fullname: str
175157
# ) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]:

mypy_django_plugin_newsemanal/transformers/settings.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from mypy.nodes import TypeInfo
2-
from mypy.plugin import FunctionContext
1+
from mypy.nodes import TypeInfo, MemberExpr
2+
from mypy.plugin import FunctionContext, AttributeContext
33
from mypy.types import Type as MypyType, TypeType, Instance
44

55
from mypy_django_plugin_newsemanal.django.context import DjangoContext
@@ -16,3 +16,30 @@ def get_user_model_hook(ctx: FunctionContext, django_context: DjangoContext) ->
1616

1717
return TypeType(Instance(model_info, []))
1818

19+
20+
def get_type_of_settings_attribute(ctx: AttributeContext, django_context: DjangoContext) -> MypyType:
21+
assert isinstance(ctx.context, MemberExpr)
22+
setting_name = ctx.context.name
23+
if not hasattr(django_context.settings, setting_name):
24+
ctx.api.fail(f"'Settings' object has no attribute {setting_name!r}", ctx.context)
25+
return ctx.default_attr_type
26+
27+
# first look for the setting in the project settings file, then global settings
28+
settings_module = ctx.api.modules.get(django_context.django_settings_module)
29+
global_settings_module = ctx.api.modules.get('django.conf.global_settings')
30+
for module in [settings_module, global_settings_module]:
31+
if module is not None:
32+
sym = module.names.get(setting_name)
33+
if sym is not None and sym.type is not None:
34+
return sym.type
35+
36+
# if by any reason it isn't present there, get type from django settings
37+
value = getattr(django_context.settings, setting_name)
38+
value_fullname = helpers.get_class_fullname(value.__class__)
39+
40+
value_info = helpers.lookup_fully_qualified_typeinfo(ctx.api, value_fullname)
41+
if value_info is None:
42+
return ctx.default_attr_type
43+
44+
return Instance(value_info, [])
45+

pytest_plugin/collect.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ def custom_init(self):
2424
name, _, val = item.partition('=')
2525
settings[name] = val
2626

27-
installed_apps = self.parsed_test_data.get('installed_apps')
28-
if installed_apps:
29-
installed_apps_as_str = '(' + ','.join([repr(app) for app in installed_apps]) + ',)'
27+
installed_apps = self.parsed_test_data.get('installed_apps', None)
28+
if installed_apps is not None:
29+
if not installed_apps:
30+
installed_apps_as_str = '()'
31+
else:
32+
installed_apps_as_str = '(' + ','.join([repr(app) for app in installed_apps]) + ',)'
3033

3134
pyproject_toml_file = File(path='pyproject.toml',
3235
content='[tool.django-stubs]\ndjango_settings_module=\'mysettings\'')

test-data-newsemanal/typecheck/test_config.yml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,6 @@
2424
if TYPE_CHECKING:
2525
reveal_type(MyModel.user) # N: Revealed type is 'django.contrib.auth.models.User*'
2626
27-
- case: missing_settings_ignored_flag
28-
main: |
29-
from django.conf import settings
30-
reveal_type(settings.NO_SUCH_SETTING) # N: Revealed type is 'Any'
31-
files:
32-
- path: pyproject.toml
33-
content: |
34-
[tool.django-stubs]
35-
ignore_missing_settings = true
36-
3727
- case: generate_pyproject_toml_and_settings_file_from_installed_apps_key
3828
main: |
3929
from myapp.models import MyModel

test-data-newsemanal/typecheck/test_settings.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
- case: settings_loaded_from_different_files
2+
disable_cache: true
23
main: |
34
from django.conf import settings
45
# standard settings
56
reveal_type(settings.AUTH_USER_MODEL) # N: Revealed type is 'builtins.str'
67
reveal_type(settings.ROOT_DIR) # N: Revealed type is 'builtins.str'
78
reveal_type(settings.APPS_DIR) # N: Revealed type is 'pathlib.Path'
8-
reveal_type(settings.OBJ) # N: Revealed type is 'django.utils.functional.LazyObject'
99
reveal_type(settings.NUMBERS) # N: Revealed type is 'builtins.list[builtins.str*]'
1010
reveal_type(settings.DICT) # N: Revealed type is 'builtins.dict[Any, Any]'
1111
files:
@@ -19,9 +19,6 @@
1919
SECRET_KEY = 112233
2020
NUMBERS = ['one', 'two']
2121
DICT = {} # type: ignore
22-
23-
from django.utils.functional import LazyObject
24-
OBJ = LazyObject()
2522
- path: base.py
2623
content: |
2724
from pathlib import Path
@@ -33,6 +30,7 @@
3330
from django.conf import settings
3431
reveal_type(settings.AUTH_USER_MODEL) # N: Revealed type is 'builtins.str'
3532
reveal_type(settings.AUTHENTICATION_BACKENDS) # N: Revealed type is 'typing.Sequence[builtins.str]'
33+
installed_apps: []
3634

3735
- case: fail_if_there_is_no_setting
3836
main: |

0 commit comments

Comments
 (0)