Skip to content

Commit

Permalink
make ignore_missing_model_attributes behaviour optional (#66)
Browse files Browse the repository at this point in the history
make "ignore_missing_model_attributes" behaviour opt-in
  • Loading branch information
mkurnikov authored Apr 12, 2019
1 parent fd06816 commit 5dd6ecc
Show file tree
Hide file tree
Showing 7 changed files with 64 additions and 17 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions mypy_django_plugin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -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)))
7 changes: 4 additions & 3 deletions mypy_django_plugin/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions mypy_django_plugin/transformers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
2 changes: 1 addition & 1 deletion test-data/typecheck/fields.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
44 changes: 38 additions & 6 deletions test-data/typecheck/model.test
Original file line number Diff line number Diff line change
@@ -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]
10 changes: 8 additions & 2 deletions test-data/typecheck/related_fields.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]

0 comments on commit 5dd6ecc

Please sign in to comment.