diff --git a/README.md b/README.md index 440d184..f7c6202 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,23 @@ plugins = in your `mypy.ini` file. -### `django.conf.settings` support +## Configuration -`settings.SETTING_NAME` will only work if `DJANGO_SETTINGS_MODULE` will be present in the environment, when mypy is executed. +In order to specify config file, set `MYPY_DJANGO_CONFIG` environment variable with path to the config file. -If some setting is not recognized to the plugin, but it's clearly there, try adding type annotation to it. +Config file format (.ini): +``` +[mypy_django_plugin] +# specify settings module to use for django.conf.settings, this setting +# could also be specified with DJANGO_SETTINGS_MODULE environment variable +# (it also takes priority over config file) +django_settings = mysettings.local + +# if True, all unknown settings in django.conf.settings will fallback to Any, +# specify it if your settings are loaded dynamically to avoid false positives +ignore_missing_settings = True +``` ## To get help diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 38a49ef..8ff0216 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -1,6 +1,8 @@ import os +from configparser import ConfigParser from typing import Callable, Dict, Optional, cast +from dataclasses import dataclass from mypy.checker import TypeChecker from mypy.nodes import TypeInfo from mypy.options import Options @@ -89,6 +91,23 @@ def set_primary_key_marking(ctx: FunctionContext) -> Type: return ctx.default_return_type +@dataclass +class Config: + django_settings_module: Optional[str] = None + ignore_missing_settings: bool = False + + @classmethod + def from_config_file(self, fpath: str) -> 'Config': + ini_config = ConfigParser() + 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), + ignore_missing_settings=ini_config.get('mypy_django_plugin', 'ignore_missing_settings', + fallback=False)) + + class DjangoPlugin(Plugin): def __init__(self, options: Options) -> None: super().__init__(options) @@ -96,8 +115,18 @@ class DjangoPlugin(Plugin): monkeypatch.restore_original_load_graph() monkeypatch.restore_original_dependencies_handling() + config_fpath = os.environ.get('MYPY_DJANGO_CONFIG') + if config_fpath: + self.config = Config.from_config_file(config_fpath) + self.django_settings = self.config.django_settings_module + else: + self.config = Config() + self.django_settings = None + + if 'DJANGO_SETTINGS_MODULE' in os.environ: + self.django_settings = os.environ['DJANGO_SETTINGS_MODULE'] + settings_modules = ['django.conf.global_settings'] - self.django_settings = os.environ.get('DJANGO_SETTINGS_MODULE') if self.django_settings: settings_modules.append(self.django_settings) @@ -163,7 +192,8 @@ class DjangoPlugin(Plugin): settings_modules = ['django.conf.global_settings'] if self.django_settings: settings_modules.append(self.django_settings) - return AddSettingValuesToDjangoConfObject(settings_modules) + return AddSettingValuesToDjangoConfObject(settings_modules, + self.config.ignore_missing_settings) if fullname in self._get_current_manager_bases(): return transform_manager_class diff --git a/mypy_django_plugin/plugins/settings.py b/mypy_django_plugin/plugins/settings.py index 1f29871..3f8172b 100644 --- a/mypy_django_plugin/plugins/settings.py +++ b/mypy_django_plugin/plugins/settings.py @@ -47,11 +47,15 @@ def load_settings_from_module(settings_classdef: ClassDef, module: MypyFile) -> class AddSettingValuesToDjangoConfObject: - def __init__(self, settings_modules: List[str]): + def __init__(self, settings_modules: List[str], ignore_missing_settings: bool): self.settings_modules = settings_modules + self.ignore_missing_settings = ignore_missing_settings def __call__(self, ctx: ClassDefContext) -> None: 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) + + if self.ignore_missing_settings: + ctx.cls.info.fallback_to_any = True diff --git a/test-data/typecheck/config.test b/test-data/typecheck/config.test new file mode 100644 index 0000000..2421347 --- /dev/null +++ b/test-data/typecheck/config.test @@ -0,0 +1,23 @@ +[CASE missing_settings_ignored_flag] +from django.conf import settings +reveal_type(settings.NO_SUCH_SETTING) # E: Revealed type is 'Any' + +[env MYPY_DJANGO_CONFIG=${MYPY_CWD}/mypy_django.ini] + +[file mypy_django.ini] +[[mypy_django_plugin] +ignore_missing_settings = True +[out] + +[CASE django_settings_via_config_file] +from django.conf import settings +reveal_type(settings.MY_SETTING) # E: Revealed type is 'builtins.int' + +[env MYPY_DJANGO_CONFIG=${MYPY_CWD}/mypy_django.ini] +[file mypy_django.ini] +[[mypy_django_plugin] +django_settings = mysettings + +[file mysettings.py] +MY_SETTING: int = 1 +[out] \ No newline at end of file