diff --git a/README.md b/README.md index ffd6a3c43..42a069d5b 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,9 @@ 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 + +# if True, unknown attributes on Model instances won't produce errors +ignore_missing_model_attributes = True ``` ## To get help diff --git a/mypy_django_plugin/config.py b/mypy_django_plugin/config.py index 884b303ff..c23df20ae 100644 --- a/mypy_django_plugin/config.py +++ b/mypy_django_plugin/config.py @@ -8,6 +8,7 @@ class Config: django_settings_module: Optional[str] = None ignore_missing_settings: bool = False + ignore_missing_model_attributes: bool = False @classmethod def from_config_file(cls, fpath: str) -> 'Config': @@ -22,5 +23,9 @@ def from_config_file(cls, fpath: str) -> 'Config': django_settings = django_settings.strip() return Config(django_settings_module=django_settings, - ignore_missing_settings=bool(ini_config.get('mypy_django_plugin', 'ignore_missing_settings', - fallback=False))) + ignore_missing_settings=bool(ini_config.get('mypy_django_plugin', + 'ignore_missing_settings', + fallback=False)), + ignore_missing_model_attributes=bool(ini_config.get('mypy_django_plugin', + 'ignore_missing_model_attributes', + fallback=False))) diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 5e3c73937..f31b853a0 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -31,7 +31,7 @@ ) -def transform_model_class(ctx: ClassDefContext) -> None: +def transform_model_class(ctx: ClassDefContext, ignore_missing_model_attributes: bool) -> None: try: sym = ctx.api.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME) except KeyError: @@ -41,7 +41,7 @@ def transform_model_class(ctx: ClassDefContext) -> None: if sym is not None and isinstance(sym.node, TypeInfo): helpers.get_django_metadata(sym.node)['model_bases'][ctx.cls.fullname] = 1 - process_model_class(ctx) + process_model_class(ctx, ignore_missing_model_attributes) def transform_manager_class(ctx: ClassDefContext) -> None: @@ -248,7 +248,8 @@ def get_method_hook(self, fullname: str def get_base_class_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: if fullname in self._get_current_model_bases(): - return transform_model_class + return partial(transform_model_class, + ignore_missing_model_attributes=self.config.ignore_missing_model_attributes) if fullname in self._get_current_manager_bases(): return transform_manager_class diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index 9e4f7de50..099855b61 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -286,7 +286,7 @@ def add_get_set_attr_fallback_to_any(ctx: ClassDefContext): add_method(ctx, '__setattr__', [name_arg, value_arg], any) -def process_model_class(ctx: ClassDefContext) -> None: +def process_model_class(ctx: ClassDefContext, ignore_unknown_attributes: bool) -> None: initializers = [ InjectAnyAsBaseForNestedMeta, AddDefaultObjectsManager, @@ -299,5 +299,5 @@ def process_model_class(ctx: ClassDefContext) -> None: add_dummy_init_method(ctx) - # allow unspecified attributes for now - add_get_set_attr_fallback_to_any(ctx) + if ignore_unknown_attributes: + add_get_set_attr_fallback_to_any(ctx) diff --git a/test-data/typecheck/fields.test b/test-data/typecheck/fields.test index ac6f4d633..dda6892b4 100644 --- a/test-data/typecheck/fields.test +++ b/test-data/typecheck/fields.test @@ -75,7 +75,7 @@ reveal_type(User().my_pk) # E: Revealed type is 'builtins.int*' reveal_type(User().id) [out] main:7: error: Revealed type is 'Any' -main:7: error: Default primary key 'id' is not defined +main:7: error: "User" has no attribute "id" [/CASE] [CASE test_meta_nested_class_allows_subclassing_in_multiple_inheritance] diff --git a/test-data/typecheck/model.test b/test-data/typecheck/model.test index 84ecf6b03..ae45a4d0a 100644 --- a/test-data/typecheck/model.test +++ b/test-data/typecheck/model.test @@ -1,28 +1,60 @@ -[CASE test_model_subtype_relationship_and_getting_and_setting_attributes] +[CASE test_typechecking_for_model_subclasses] from django.db import models class A(models.Model): pass - class B(models.Model): b_attr = 1 pass - class C(A): pass def service(a: A) -> int: pass +b_instance = B() +service(b_instance) # E: Argument 1 to "service" has incompatible type "B"; expected "A" + a_instance = A() +c_instance = C() +service(a_instance) +service(c_instance) +[/CASE] + + +[CASE fail_if_no_such_attribute_on_model] +from django.db import models + +class B(models.Model): + b_attr = 1 + pass + b_instance = B() reveal_type(b_instance.b_attr) # E: Revealed type is 'builtins.int' +reveal_type(b_instance.non_existent_attribute) +b_instance.non_existent_attribute = 2 +[out] +main:10: error: Revealed type is 'Any' +main:10: error: "B" has no attribute "non_existent_attribute" +main:11: error: "B" has no attribute "non_existent_attribute" +[/CASE] + +[CASE ignore_missing_attributes_if_setting_is_passed] +from django.db import models + +class B(models.Model): + pass + +b_instance = B() reveal_type(b_instance.non_existent_attribute) # E: Revealed type is 'Any' b_instance.non_existent_attribute = 2 -service(b_instance) # E: Argument 1 to "service" has incompatible type "B"; expected "A" +[env MYPY_DJANGO_CONFIG=${MYPY_CWD}/mypy_django.ini] -c_instance = C() -service(c_instance) +[file mypy_django.ini] +[[mypy_django_plugin] +ignore_missing_model_attributes = True + +[/CASE] \ No newline at end of file diff --git a/test-data/typecheck/related_fields.test b/test-data/typecheck/related_fields.test index 6255758f1..3be5eb8b3 100644 --- a/test-data/typecheck/related_fields.test +++ b/test-data/typecheck/related_fields.test @@ -109,8 +109,10 @@ class View(models.Model): app = models.ForeignKey(to=App, related_name='views', on_delete=models.CASCADE) reveal_type(View().app.views) # E: Revealed type is 'django.db.models.manager.RelatedManager[main.View]' -reveal_type(View().app.unknown) # E: Revealed type is 'Any' +reveal_type(View().app.unknown) [out] +main:7: error: Revealed type is 'Any' +main:7: error: "App" has no attribute "unknown" [file myapp/__init__.py] [file myapp/models.py] @@ -307,6 +309,10 @@ book = Book() reveal_type(book.publisher) # E: Revealed type is 'main.Publisher*' publisher = Publisher() -reveal_type(publisher.books) # E: Revealed type is 'Any' +reveal_type(publisher.books) reveal_type(publisher.books2) # E: Revealed type is 'django.db.models.manager.RelatedManager[main.Book]' +[out] +main:16: error: Revealed type is 'Any' +main:16: error: "Publisher" has no attribute "books"; maybe "books2"? +[/CASE]