From 90ed760c2b310717b0493221d10231999c2d1fac Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 20 Jun 2023 15:51:58 +0300 Subject: [PATCH 01/13] Add `strict_settings` option, allow runtime fallbacks for custom settings --- README.md | 46 +++++++++++++++++++++ mypy_django_plugin/config.py | 24 ++++++++--- mypy_django_plugin/django/context.py | 7 ++-- mypy_django_plugin/main.py | 6 +-- mypy_django_plugin/transformers/settings.py | 9 +++- tests/test_error_handling.py | 36 ++++++++++++---- tests/typecheck/test_settings.yml | 29 +++++++++++++ 7 files changed, 137 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b3fdec973..c79d744fd 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,52 @@ If you encounter this error in your own code, you can either cast the `Promise` If this is reported on Django code, please report an issue or open a pull request to fix the type hints. +### How to use a custom library to handle Django settings? + +Using something like [`django-split-settings`](https://github.com/wemake-services/django-split-settings) or [`django-configurations`](https://github.com/jazzband/django-configurations) will make it hard for mypy to infer your settings. + +This might also be the case when using something like: + +```python +try: + from .local_settings import * +except Exception: + pass +``` + +So, mypy would not like this code: + +```python +from django.conf import settings + +settings.CUSTOM_VALUE # E: 'Settings' object has no attribute 'CUSTOM_SETTING' +``` + +To handle this corner case we have a special setting `strict_settings` (`True` by default), +you can switch it to `False` to always return `Any` and not raise any errors if runtime settings module has the given value: + +```toml +[tool.django-stubs] +strict_settings = false +``` + +or + +```ini +[mypy.plugins.django-stubs] +strict_settings = false +``` + +And then: + +```python +# Works: +reveal_type(settings.EXISTS_IN_RUNTIME) # N: Any + +# Errors: +reveal_type(settings.MISSING) # E: 'Settings' object has no attribute 'MISSING' +``` + ## Related projects - [`awesome-python-typing`](https://github.com/typeddjango/awesome-python-typing) - Awesome list of all typing-related things in Python. diff --git a/mypy_django_plugin/config.py b/mypy_django_plugin/config.py index 28d52e0b6..de68c40cb 100644 --- a/mypy_django_plugin/config.py +++ b/mypy_django_plugin/config.py @@ -14,7 +14,8 @@ (config) ... [mypy.plugins.django-stubs] - django_settings_module: str (required) +django_settings_module = str (required) +strict_settings = bool (default: true) ... """ TOML_USAGE = """ @@ -22,13 +23,14 @@ ... [tool.django-stubs] django_settings_module = str (required) +strict_settings = bool (default: true) ... """ INVALID_FILE = "mypy config file is not specified or found" COULD_NOT_LOAD_FILE = "could not load configuration file" -MISSING_SECTION = "no section [{section}] found".format +MISSING_SECTION = "no section [{section}] found" MISSING_DJANGO_SETTINGS = "missing required 'django_settings_module' config" -INVALID_SETTING = "invalid {key!r}: the setting must be a boolean".format +INVALID_BOOL_SETTING = "invalid {key!r}: the setting must be a boolean" def exit_with_error(msg: str, is_toml: bool = False) -> NoReturn: @@ -48,8 +50,9 @@ def exit_with_error(msg: str, is_toml: bool = False) -> NoReturn: class DjangoPluginConfig: - __slots__ = ("django_settings_module",) + __slots__ = ("django_settings_module", "strict_settings") django_settings_module: str + strict_settings: bool def __init__(self, config_file: Optional[str]) -> None: if not config_file: @@ -75,7 +78,7 @@ def parse_toml_file(self, filepath: Path) -> None: try: config: Dict[str, Any] = data["tool"]["django-stubs"] except KeyError: - toml_exit(MISSING_SECTION(section="tool.django-stubs")) + toml_exit(MISSING_SECTION.format(section="tool.django-stubs")) if "django_settings_module" not in config: toml_exit(MISSING_DJANGO_SETTINGS) @@ -84,6 +87,10 @@ def parse_toml_file(self, filepath: Path) -> None: if not isinstance(self.django_settings_module, str): toml_exit("invalid 'django_settings_module': the setting must be a string") + self.strict_settings = config.get("strict_settings", True) + if not isinstance(self.strict_settings, bool): + toml_exit(INVALID_BOOL_SETTING.format(key="strict_settings")) + def parse_ini_file(self, filepath: Path) -> None: parser = configparser.ConfigParser() try: @@ -94,9 +101,14 @@ def parse_ini_file(self, filepath: Path) -> None: section = "mypy.plugins.django-stubs" if not parser.has_section(section): - exit_with_error(MISSING_SECTION(section=section)) + exit_with_error(MISSING_SECTION.format(section=section)) if not parser.has_option(section, "django_settings_module"): exit_with_error(MISSING_DJANGO_SETTINGS) self.django_settings_module = parser.get(section, "django_settings_module").strip("'\"") + + try: + self.strict_settings = parser.getboolean(section, "strict_settings", fallback=True) + except ValueError: + exit_with_error(INVALID_BOOL_SETTING.format(key="strict_settings")) diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index 0bf52830e..0dfdbac19 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -20,6 +20,7 @@ from mypy.types import AnyType, Instance, TypeOfAny, UnionType from mypy.types import Type as MypyType +from mypy_django_plugin.config import DjangoPluginConfig from mypy_django_plugin.exceptions import UnregisteredModelError from mypy_django_plugin.lib import fullnames, helpers from mypy_django_plugin.lib.fullnames import WITH_ANNOTATIONS_FULLNAME @@ -78,10 +79,10 @@ class LookupsAreUnsupported(Exception): class DjangoContext: - def __init__(self, django_settings_module: str) -> None: - self.django_settings_module = django_settings_module + def __init__(self, plugin_config: DjangoPluginConfig) -> None: + self.plugin_config = plugin_config - apps, settings = initialize_django(self.django_settings_module) + apps, settings = initialize_django(self.plugin_config.django_settings_module) self.apps_registry = apps self.settings = settings diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index a47c655d9..533c96315 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -71,7 +71,7 @@ def __init__(self, options: Options) -> None: sys.path.extend(mypy_path()) # Add paths from mypy_path config option sys.path.extend(options.mypy_path) - self.django_context = DjangoContext(self.plugin_config.django_settings_module) + self.django_context = DjangoContext(self.plugin_config) def _get_current_queryset_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.QUERYSET_CLASS_FULLNAME) @@ -125,8 +125,8 @@ def _new_dependency(self, module: str) -> 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)] + if file.fullname == "django.conf" and self.django_context.plugin_config.django_settings_module: + return [self._new_dependency(self.django_context.plugin_config.django_settings_module)] # for values / values_list if file.fullname == "django.db.models": diff --git a/mypy_django_plugin/transformers/settings.py b/mypy_django_plugin/transformers/settings.py index 67caa6a50..2e082d7f8 100644 --- a/mypy_django_plugin/transformers/settings.py +++ b/mypy_django_plugin/transformers/settings.py @@ -28,7 +28,7 @@ def get_type_of_settings_attribute(ctx: AttributeContext, django_context: Django typechecker_api = helpers.get_typechecker_api(ctx) # first look for the setting in the project settings file, then global settings - settings_module = typechecker_api.modules.get(django_context.django_settings_module) + settings_module = typechecker_api.modules.get(django_context.plugin_config.django_settings_module) global_settings_module = typechecker_api.modules.get("django.conf.global_settings") for module in [settings_module, global_settings_module]: if module is not None: @@ -42,5 +42,12 @@ def get_type_of_settings_attribute(ctx: AttributeContext, django_context: Django return ctx.default_attr_type return sym.type + # Now, we want to check if this setting really exist in runtime. + # If it does, we just return `Any`, not to raise any false-positives. + # But, we cannot reconstruct the exact runtime type. + # See https://github.com/typeddjango/django-stubs/pull/1163 + if not django_context.plugin_config.strict_settings and hasattr(django_context.settings, setting_name): + return AnyType(TypeOfAny.implementation_artifact) + ctx.api.fail(f"'Settings' object has no attribute {setting_name!r}", ctx.context) return ctx.default_attr_type diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index 34c578f22..1adc4d718 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -11,7 +11,8 @@ (config) ... [mypy.plugins.django-stubs] - django_settings_module: str (required) +django_settings_module = str (required) +strict_settings = bool (default: true) ... (django-stubs) mypy: error: {} """ @@ -21,6 +22,7 @@ ... [tool.django-stubs] django_settings_module = str (required) +strict_settings = bool (default: true) ... (django-stubs) mypy: error: {} """ @@ -52,6 +54,11 @@ def write_to_file(file_contents: str, suffix: Optional[str] = None) -> Generator "missing required 'django_settings_module' config", id="no-settings-given", ), + pytest.param( + ["[mypy.plugins.django-stubs]", "django_settings_module = some.module", "strict_settings = bad"], + "invalid 'strict_settings': the setting must be a boolean", + id="missing-settings-module", + ), ], ) def test_misconfiguration_handling(capsys: Any, config_file_contents: List[str], message_part: str) -> None: @@ -113,6 +120,15 @@ def test_handles_filename(capsys: Any, filename: str) -> None: "could not load configuration file", id="invalid toml", ), + pytest.param( + """ + [tool.django-stubs] + django_settings_module = "some.module" + strict_settings = "a" + """, + "invalid 'strict_settings': the setting must be a boolean", + id="invalid strict_settings type", + ), ], ) def test_toml_misconfiguration_handling(capsys: Any, config_file_contents, message_part) -> None: @@ -124,29 +140,35 @@ def test_toml_misconfiguration_handling(capsys: Any, config_file_contents, messa assert error_message == capsys.readouterr().err -def test_correct_toml_configuration() -> None: +@pytest.mark.parametrize('boolean_value', ['true', 'false']) +def test_correct_toml_configuration(boolean_value: str) -> None: config_file_contents = """ [tool.django-stubs] some_other_setting = "setting" django_settings_module = "my.module" - """ + strict_settings = {0} + """.format(boolean_value) with write_to_file(config_file_contents, suffix=".toml") as filename: config = DjangoPluginConfig(filename) assert config.django_settings_module == "my.module" + assert config.strict_settings is (boolean_value == "true") -def test_correct_configuration() -> None: +@pytest.mark.parametrize('boolean_value', ['true', 'True', 'false', 'False']) +def test_correct_configuration(boolean_value) -> None: """Django settings module gets extracted given valid configuration.""" config_file_contents = "\n".join( [ "[mypy.plugins.django-stubs]", - "\tsome_other_setting = setting", - "\tdjango_settings_module = my.module", + "some_other_setting = setting", + "django_settings_module = my.module", + f"strict_settings = {boolean_value}" ] - ).expandtabs(4) + ) with write_to_file(config_file_contents) as filename: config = DjangoPluginConfig(filename) assert config.django_settings_module == "my.module" + assert config.strict_settings is (boolean_value.lower() == "true") diff --git a/tests/typecheck/test_settings.yml b/tests/typecheck/test_settings.yml index ee4e69958..c2401cc97 100644 --- a/tests/typecheck/test_settings.yml +++ b/tests/typecheck/test_settings.yml @@ -58,3 +58,32 @@ main:4: error: 'Settings' object has no attribute 'NON_EXISTANT_SETTING' main:5: error: 'Settings' object has no attribute 'NON_EXISTANT_SETTING' main:5: note: Revealed type is "Any" + + +- case: settings_loaded_from_runtime_magic + disable_cache: true + main: | + from django.conf import settings + reveal_type(settings.A) # N: Revealed type is "Any" + reveal_type(settings.B) # E: 'Settings' object has no attribute 'B' # N: Revealed type is "Any" + custom_settings: | + # Some code that mypy cannot analyze, but values exist in runtime: + exec('A = 1') + mypy_config: | + [mypy.plugins.django-stubs] + django_settings_module = mysettings + strict_settings = false + + +- case: settings_loaded_from_runtime_magic_strict_default + disable_cache: true + main: | + from django.conf import settings + reveal_type(settings.A) # E: 'Settings' object has no attribute 'A' # N: Revealed type is "Any" + reveal_type(settings.B) # E: 'Settings' object has no attribute 'B' # N: Revealed type is "Any" + custom_settings: | + # Some code that mypy cannot analyze, but values exist in runtime: + exec('A = 1') + mypy_config: | + [mypy.plugins.django-stubs] + django_settings_module = mysettings From 4a4a988ea69d0cc2d11acc672f1be9138457bf3d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 12:55:50 +0000 Subject: [PATCH 02/13] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/test_error_handling.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index 1adc4d718..b25f07b48 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -140,14 +140,16 @@ def test_toml_misconfiguration_handling(capsys: Any, config_file_contents, messa assert error_message == capsys.readouterr().err -@pytest.mark.parametrize('boolean_value', ['true', 'false']) +@pytest.mark.parametrize("boolean_value", ["true", "false"]) def test_correct_toml_configuration(boolean_value: str) -> None: config_file_contents = """ [tool.django-stubs] some_other_setting = "setting" django_settings_module = "my.module" - strict_settings = {0} - """.format(boolean_value) + strict_settings = {} + """.format( + boolean_value + ) with write_to_file(config_file_contents, suffix=".toml") as filename: config = DjangoPluginConfig(filename) @@ -156,7 +158,7 @@ def test_correct_toml_configuration(boolean_value: str) -> None: assert config.strict_settings is (boolean_value == "true") -@pytest.mark.parametrize('boolean_value', ['true', 'True', 'false', 'False']) +@pytest.mark.parametrize("boolean_value", ["true", "True", "false", "False"]) def test_correct_configuration(boolean_value) -> None: """Django settings module gets extracted given valid configuration.""" config_file_contents = "\n".join( @@ -164,7 +166,7 @@ def test_correct_configuration(boolean_value) -> None: "[mypy.plugins.django-stubs]", "some_other_setting = setting", "django_settings_module = my.module", - f"strict_settings = {boolean_value}" + f"strict_settings = {boolean_value}", ] ) with write_to_file(config_file_contents) as filename: From ea34cbb0ec7ec27d3f74518b45f45aafa6727478 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Wed, 21 Jun 2023 15:18:46 +0300 Subject: [PATCH 03/13] Apply suggestions from code review Co-authored-by: Marti Raudsepp --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c79d744fd..0c0f3d331 100644 --- a/README.md +++ b/README.md @@ -333,14 +333,15 @@ settings.CUSTOM_VALUE # E: 'Settings' object has no attribute 'CUSTOM_SETTING' ``` To handle this corner case we have a special setting `strict_settings` (`True` by default), -you can switch it to `False` to always return `Any` and not raise any errors if runtime settings module has the given value: +you can switch it to `False` to always return `Any` and not raise any errors if runtime settings module has the given value, +for example `pyproject.toml`: ```toml [tool.django-stubs] strict_settings = false ``` -or +or `mypy.ini`: ```ini [mypy.plugins.django-stubs] From 109aaf7616e241535a98bd9441afa6ce28444bda Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 21 Jun 2023 17:58:53 +0300 Subject: [PATCH 04/13] Address review --- tests/typecheck/test_settings.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/typecheck/test_settings.yml b/tests/typecheck/test_settings.yml index c2401cc97..50ebfe0be 100644 --- a/tests/typecheck/test_settings.yml +++ b/tests/typecheck/test_settings.yml @@ -64,6 +64,11 @@ disable_cache: true main: | from django.conf import settings + + # Global: + reveal_type(settings.SECRET_KEY) # N: Revealed type is "builtins.str" + + # Custom: reveal_type(settings.A) # N: Revealed type is "Any" reveal_type(settings.B) # E: 'Settings' object has no attribute 'B' # N: Revealed type is "Any" custom_settings: | @@ -79,6 +84,11 @@ disable_cache: true main: | from django.conf import settings + + # Global: + reveal_type(settings.SECRET_KEY) # N: Revealed type is "builtins.str" + + # Custom: reveal_type(settings.A) # E: 'Settings' object has no attribute 'A' # N: Revealed type is "Any" reveal_type(settings.B) # E: 'Settings' object has no attribute 'B' # N: Revealed type is "Any" custom_settings: | From 9cd8f467917590f0db6ba973e81989f5149e0511 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 22 Jun 2023 14:01:34 +0300 Subject: [PATCH 05/13] Add compat code --- mypy_django_plugin/django/context.py | 4 ++++ mypy_django_plugin/main.py | 4 ++-- mypy_django_plugin/transformers/settings.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index 8b6eb16d1..6543b82e6 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -79,6 +79,10 @@ def __init__(self, plugin_config: DjangoPluginConfig) -> None: self.apps_registry = apps self.settings = settings + @property # Compat with older API + def django_settings_module(self) -> str: + return self.plugin_config.django_settings_module + @cached_property def model_modules(self) -> Dict[str, Set[Type[Model]]]: """All modules that contain Django models.""" diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 533c96315..0ef9e7884 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -125,8 +125,8 @@ def _new_dependency(self, module: str) -> 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.plugin_config.django_settings_module: - return [self._new_dependency(self.django_context.plugin_config.django_settings_module)] + if file.fullname == "django.conf" and self.django_context.django_settings_module: + return [self._new_dependency(self.django_context.django_settings_module)] # for values / values_list if file.fullname == "django.db.models": diff --git a/mypy_django_plugin/transformers/settings.py b/mypy_django_plugin/transformers/settings.py index 2e082d7f8..d85ea773f 100644 --- a/mypy_django_plugin/transformers/settings.py +++ b/mypy_django_plugin/transformers/settings.py @@ -28,7 +28,7 @@ def get_type_of_settings_attribute(ctx: AttributeContext, django_context: Django typechecker_api = helpers.get_typechecker_api(ctx) # first look for the setting in the project settings file, then global settings - settings_module = typechecker_api.modules.get(django_context.plugin_config.django_settings_module) + settings_module = typechecker_api.modules.get(django_context.django_settings_module) global_settings_module = typechecker_api.modules.get("django.conf.global_settings") for module in [settings_module, global_settings_module]: if module is not None: From 73b630997ae569ad7e5415c1f00ad724d8581536 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 22 Jun 2023 14:03:51 +0300 Subject: [PATCH 06/13] Add compat code --- mypy_django_plugin/django/context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index 6543b82e6..c1c3dcacc 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -73,6 +73,9 @@ class LookupsAreUnsupported(Exception): class DjangoContext: def __init__(self, plugin_config: DjangoPluginConfig) -> None: + if isinstance(plugin_config, str): + raise RuntimeError('Old API is used: we now require DjangoPluginConfig instance') + self.plugin_config = plugin_config apps, settings = initialize_django(self.plugin_config.django_settings_module) From 71abc8c1c742e74604048b1ea193a288687bc8b8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 11:06:33 +0000 Subject: [PATCH 07/13] [pre-commit.ci] auto fixes from pre-commit.com hooks --- mypy_django_plugin/django/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index c1c3dcacc..94db98099 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -74,7 +74,7 @@ class LookupsAreUnsupported(Exception): class DjangoContext: def __init__(self, plugin_config: DjangoPluginConfig) -> None: if isinstance(plugin_config, str): - raise RuntimeError('Old API is used: we now require DjangoPluginConfig instance') + raise RuntimeError("Old API is used: we now require DjangoPluginConfig instance") self.plugin_config = plugin_config From a0d01792731f18d171f766264debcc870c0bd7e9 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 22 Jun 2023 14:31:54 +0300 Subject: [PATCH 08/13] Remove `RuntimeError` --- mypy_django_plugin/django/context.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index c1c3dcacc..6543b82e6 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -73,9 +73,6 @@ class LookupsAreUnsupported(Exception): class DjangoContext: def __init__(self, plugin_config: DjangoPluginConfig) -> None: - if isinstance(plugin_config, str): - raise RuntimeError('Old API is used: we now require DjangoPluginConfig instance') - self.plugin_config = plugin_config apps, settings = initialize_django(self.plugin_config.django_settings_module) From de289d10d8ba79ca2cf93d71824faf7fd362d7bc Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 22 Jun 2023 15:06:41 +0300 Subject: [PATCH 09/13] Solve problems in Python code --- mypy_django_plugin/django/context.py | 10 +++------- mypy_django_plugin/lib/helpers.py | 2 +- mypy_django_plugin/main.py | 12 ++++++------ mypy_django_plugin/transformers/settings.py | 5 +++-- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index 6543b82e6..3af90997c 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -72,17 +72,13 @@ class LookupsAreUnsupported(Exception): class DjangoContext: - def __init__(self, plugin_config: DjangoPluginConfig) -> None: - self.plugin_config = plugin_config + def __init__(self, django_settings_module: str) -> None: + self.django_settings_module = django_settings_module - apps, settings = initialize_django(self.plugin_config.django_settings_module) + apps, settings = initialize_django(self.django_settings_module) self.apps_registry = apps self.settings = settings - @property # Compat with older API - def django_settings_module(self) -> str: - return self.plugin_config.django_settings_module - @cached_property def model_modules(self) -> Dict[str, Set[Type[Model]]]: """All modules that contain Django models.""" diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index c20c16426..f72148ff0 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -333,7 +333,7 @@ def resolve_string_attribute_value(attr_expr: Expression, django_context: "Djang member_name = attr_expr.name if isinstance(attr_expr.expr, NameExpr) and attr_expr.expr.fullname == "django.conf.settings": if hasattr(django_context.settings, member_name): - return getattr(django_context.settings, member_name) # type: ignore + return getattr(django_context.settings, member_name) return None diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 0ef9e7884..32f3499b2 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -71,12 +71,12 @@ def __init__(self, options: Options) -> None: sys.path.extend(mypy_path()) # Add paths from mypy_path config option sys.path.extend(options.mypy_path) - self.django_context = DjangoContext(self.plugin_config) + self.django_context = DjangoContext(self.plugin_config.django_settings_module) def _get_current_queryset_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.QUERYSET_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): - return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return] + return helpers.get_django_metadata(model_sym.node).setdefault( "queryset_bases", {fullnames.QUERYSET_CLASS_FULLNAME: 1} ) else: @@ -85,7 +85,7 @@ def _get_current_queryset_bases(self) -> Dict[str, int]: def _get_current_manager_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.MANAGER_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): - return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return] + return helpers.get_django_metadata(model_sym.node).setdefault( "manager_bases", {fullnames.MANAGER_CLASS_FULLNAME: 1} ) else: @@ -94,7 +94,7 @@ def _get_current_manager_bases(self) -> Dict[str, int]: def _get_current_model_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.MODEL_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): - return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return] + return helpers.get_django_metadata(model_sym.node).setdefault( "model_bases", {fullnames.MODEL_CLASS_FULLNAME: 1} ) else: @@ -103,7 +103,7 @@ def _get_current_model_bases(self) -> Dict[str, int]: def _get_current_form_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.BASEFORM_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): - return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return] + return helpers.get_django_metadata(model_sym.node).setdefault( "baseform_bases", { fullnames.BASEFORM_CLASS_FULLNAME: 1, @@ -274,7 +274,7 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte # Lookup of a settings variable if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS: - return partial(settings.get_type_of_settings_attribute, django_context=self.django_context) + return partial(settings.get_type_of_settings_attribute, django_context=self.django_context, plugin_config=self.plugin_config) info = self._get_typeinfo_or_none(class_name) diff --git a/mypy_django_plugin/transformers/settings.py b/mypy_django_plugin/transformers/settings.py index d85ea773f..a03bbe109 100644 --- a/mypy_django_plugin/transformers/settings.py +++ b/mypy_django_plugin/transformers/settings.py @@ -3,6 +3,7 @@ from mypy.types import AnyType, Instance, TypeOfAny, TypeType from mypy.types import Type as MypyType +from mypy_django_plugin.config import DjangoPluginConfig from mypy_django_plugin.django.context import DjangoContext from mypy_django_plugin.lib import helpers @@ -19,7 +20,7 @@ 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: +def get_type_of_settings_attribute(ctx: AttributeContext, django_context: DjangoContext, plugin_config: DjangoPluginConfig) -> MypyType: if not isinstance(ctx.context, MemberExpr): return ctx.default_attr_type @@ -46,7 +47,7 @@ def get_type_of_settings_attribute(ctx: AttributeContext, django_context: Django # If it does, we just return `Any`, not to raise any false-positives. # But, we cannot reconstruct the exact runtime type. # See https://github.com/typeddjango/django-stubs/pull/1163 - if not django_context.plugin_config.strict_settings and hasattr(django_context.settings, setting_name): + if not plugin_config.strict_settings and hasattr(django_context.settings, setting_name): return AnyType(TypeOfAny.implementation_artifact) ctx.api.fail(f"'Settings' object has no attribute {setting_name!r}", ctx.context) From 2ea7806740919c0cd766fd3c3a14ee928c56fd41 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 12:08:09 +0000 Subject: [PATCH 10/13] [pre-commit.ci] auto fixes from pre-commit.com hooks --- mypy_django_plugin/django/context.py | 1 - mypy_django_plugin/main.py | 6 +++++- mypy_django_plugin/transformers/settings.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index 3af90997c..62d0d2bd8 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -21,7 +21,6 @@ from mypy.types import AnyType, Instance, TypeOfAny, UnionType from mypy.types import Type as MypyType -from mypy_django_plugin.config import DjangoPluginConfig from mypy_django_plugin.exceptions import UnregisteredModelError from mypy_django_plugin.lib import fullnames, helpers from mypy_django_plugin.lib.fullnames import WITH_ANNOTATIONS_FULLNAME diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 32f3499b2..a67598e89 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -274,7 +274,11 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte # Lookup of a settings variable if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS: - return partial(settings.get_type_of_settings_attribute, django_context=self.django_context, plugin_config=self.plugin_config) + return partial( + settings.get_type_of_settings_attribute, + django_context=self.django_context, + plugin_config=self.plugin_config, + ) info = self._get_typeinfo_or_none(class_name) diff --git a/mypy_django_plugin/transformers/settings.py b/mypy_django_plugin/transformers/settings.py index a03bbe109..f821b971e 100644 --- a/mypy_django_plugin/transformers/settings.py +++ b/mypy_django_plugin/transformers/settings.py @@ -20,7 +20,9 @@ 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, plugin_config: DjangoPluginConfig) -> MypyType: +def get_type_of_settings_attribute( + ctx: AttributeContext, django_context: DjangoContext, plugin_config: DjangoPluginConfig +) -> MypyType: if not isinstance(ctx.context, MemberExpr): return ctx.default_attr_type From f2595b95877e43ef6d7c9dd87fb2750876a4fc66 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 22 Jun 2023 15:11:55 +0300 Subject: [PATCH 11/13] Fix CI --- mypy_django_plugin/main.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index a67598e89..1ddc24530 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -76,7 +76,7 @@ def __init__(self, options: Options) -> None: def _get_current_queryset_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.QUERYSET_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): - return helpers.get_django_metadata(model_sym.node).setdefault( + return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return] "queryset_bases", {fullnames.QUERYSET_CLASS_FULLNAME: 1} ) else: @@ -85,7 +85,7 @@ def _get_current_queryset_bases(self) -> Dict[str, int]: def _get_current_manager_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.MANAGER_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): - return helpers.get_django_metadata(model_sym.node).setdefault( + return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return] "manager_bases", {fullnames.MANAGER_CLASS_FULLNAME: 1} ) else: @@ -94,7 +94,7 @@ def _get_current_manager_bases(self) -> Dict[str, int]: def _get_current_model_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.MODEL_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): - return helpers.get_django_metadata(model_sym.node).setdefault( + return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return] "model_bases", {fullnames.MODEL_CLASS_FULLNAME: 1} ) else: @@ -103,7 +103,7 @@ def _get_current_model_bases(self) -> Dict[str, int]: def _get_current_form_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(fullnames.BASEFORM_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): - return helpers.get_django_metadata(model_sym.node).setdefault( + return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return] "baseform_bases", { fullnames.BASEFORM_CLASS_FULLNAME: 1, @@ -274,11 +274,7 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte # Lookup of a settings variable if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS: - return partial( - settings.get_type_of_settings_attribute, - django_context=self.django_context, - plugin_config=self.plugin_config, - ) + return partial(settings.get_type_of_settings_attribute, django_context=self.django_context, plugin_config=self.plugin_config) info = self._get_typeinfo_or_none(class_name) From 294ed48f2c60bf9c7de9f002b4893201538656c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 12:12:32 +0000 Subject: [PATCH 12/13] [pre-commit.ci] auto fixes from pre-commit.com hooks --- mypy_django_plugin/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 1ddc24530..5ea3bb4f5 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -274,7 +274,11 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte # Lookup of a settings variable if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS: - return partial(settings.get_type_of_settings_attribute, django_context=self.django_context, plugin_config=self.plugin_config) + return partial( + settings.get_type_of_settings_attribute, + django_context=self.django_context, + plugin_config=self.plugin_config, + ) info = self._get_typeinfo_or_none(class_name) From c6a0780377eb550194077d2c99801f158962b431 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 22 Jun 2023 15:15:07 +0300 Subject: [PATCH 13/13] Fix CI --- mypy_django_plugin/lib/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index f72148ff0..6f859ec9b 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -333,7 +333,7 @@ def resolve_string_attribute_value(attr_expr: Expression, django_context: "Djang member_name = attr_expr.name if isinstance(attr_expr.expr, NameExpr) and attr_expr.expr.fullname == "django.conf.settings": if hasattr(django_context.settings, member_name): - return getattr(django_context.settings, member_name) + return getattr(django_context.settings, member_name) # type: ignore[no-any-return] return None