Skip to content

Commit 5dd6ecc

Browse files
authored
make ignore_missing_model_attributes behaviour optional (#66)
make "ignore_missing_model_attributes" behaviour opt-in
1 parent fd06816 commit 5dd6ecc

File tree

7 files changed

+64
-17
lines changed

7 files changed

+64
-17
lines changed

Diff for: README.md

+3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ django_settings = mysettings.local
4444
# if True, all unknown settings in django.conf.settings will fallback to Any,
4545
# specify it if your settings are loaded dynamically to avoid false positives
4646
ignore_missing_settings = True
47+
48+
# if True, unknown attributes on Model instances won't produce errors
49+
ignore_missing_model_attributes = True
4750
```
4851

4952
## To get help

Diff for: mypy_django_plugin/config.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
class Config:
99
django_settings_module: Optional[str] = None
1010
ignore_missing_settings: bool = False
11+
ignore_missing_model_attributes: bool = False
1112

1213
@classmethod
1314
def from_config_file(cls, fpath: str) -> 'Config':
@@ -22,5 +23,9 @@ def from_config_file(cls, fpath: str) -> 'Config':
2223
django_settings = django_settings.strip()
2324

2425
return Config(django_settings_module=django_settings,
25-
ignore_missing_settings=bool(ini_config.get('mypy_django_plugin', 'ignore_missing_settings',
26-
fallback=False)))
26+
ignore_missing_settings=bool(ini_config.get('mypy_django_plugin',
27+
'ignore_missing_settings',
28+
fallback=False)),
29+
ignore_missing_model_attributes=bool(ini_config.get('mypy_django_plugin',
30+
'ignore_missing_model_attributes',
31+
fallback=False)))

Diff for: mypy_django_plugin/main.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
)
3232

3333

34-
def transform_model_class(ctx: ClassDefContext) -> None:
34+
def transform_model_class(ctx: ClassDefContext, ignore_missing_model_attributes: bool) -> None:
3535
try:
3636
sym = ctx.api.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME)
3737
except KeyError:
@@ -41,7 +41,7 @@ def transform_model_class(ctx: ClassDefContext) -> None:
4141
if sym is not None and isinstance(sym.node, TypeInfo):
4242
helpers.get_django_metadata(sym.node)['model_bases'][ctx.cls.fullname] = 1
4343

44-
process_model_class(ctx)
44+
process_model_class(ctx, ignore_missing_model_attributes)
4545

4646

4747
def transform_manager_class(ctx: ClassDefContext) -> None:
@@ -248,7 +248,8 @@ def get_method_hook(self, fullname: str
248248
def get_base_class_hook(self, fullname: str
249249
) -> Optional[Callable[[ClassDefContext], None]]:
250250
if fullname in self._get_current_model_bases():
251-
return transform_model_class
251+
return partial(transform_model_class,
252+
ignore_missing_model_attributes=self.config.ignore_missing_model_attributes)
252253

253254
if fullname in self._get_current_manager_bases():
254255
return transform_manager_class

Diff for: mypy_django_plugin/transformers/models.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ def add_get_set_attr_fallback_to_any(ctx: ClassDefContext):
286286
add_method(ctx, '__setattr__', [name_arg, value_arg], any)
287287

288288

289-
def process_model_class(ctx: ClassDefContext) -> None:
289+
def process_model_class(ctx: ClassDefContext, ignore_unknown_attributes: bool) -> None:
290290
initializers = [
291291
InjectAnyAsBaseForNestedMeta,
292292
AddDefaultObjectsManager,
@@ -299,5 +299,5 @@ def process_model_class(ctx: ClassDefContext) -> None:
299299

300300
add_dummy_init_method(ctx)
301301

302-
# allow unspecified attributes for now
303-
add_get_set_attr_fallback_to_any(ctx)
302+
if ignore_unknown_attributes:
303+
add_get_set_attr_fallback_to_any(ctx)

Diff for: test-data/typecheck/fields.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ reveal_type(User().my_pk) # E: Revealed type is 'builtins.int*'
7575
reveal_type(User().id)
7676
[out]
7777
main:7: error: Revealed type is 'Any'
78-
main:7: error: Default primary key 'id' is not defined
78+
main:7: error: "User" has no attribute "id"
7979
[/CASE]
8080

8181
[CASE test_meta_nested_class_allows_subclassing_in_multiple_inheritance]

Diff for: test-data/typecheck/model.test

+38-6
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,60 @@
1-
[CASE test_model_subtype_relationship_and_getting_and_setting_attributes]
1+
[CASE test_typechecking_for_model_subclasses]
22
from django.db import models
33

44
class A(models.Model):
55
pass
6-
76
class B(models.Model):
87
b_attr = 1
98
pass
10-
119
class C(A):
1210
pass
1311

1412
def service(a: A) -> int:
1513
pass
1614

15+
b_instance = B()
16+
service(b_instance) # E: Argument 1 to "service" has incompatible type "B"; expected "A"
17+
1718
a_instance = A()
19+
c_instance = C()
20+
service(a_instance)
21+
service(c_instance)
22+
[/CASE]
23+
24+
25+
[CASE fail_if_no_such_attribute_on_model]
26+
from django.db import models
27+
28+
class B(models.Model):
29+
b_attr = 1
30+
pass
31+
1832
b_instance = B()
1933
reveal_type(b_instance.b_attr) # E: Revealed type is 'builtins.int'
2034

35+
reveal_type(b_instance.non_existent_attribute)
36+
b_instance.non_existent_attribute = 2
37+
[out]
38+
main:10: error: Revealed type is 'Any'
39+
main:10: error: "B" has no attribute "non_existent_attribute"
40+
main:11: error: "B" has no attribute "non_existent_attribute"
41+
[/CASE]
42+
2143

44+
[CASE ignore_missing_attributes_if_setting_is_passed]
45+
from django.db import models
46+
47+
class B(models.Model):
48+
pass
49+
50+
b_instance = B()
2251
reveal_type(b_instance.non_existent_attribute) # E: Revealed type is 'Any'
2352
b_instance.non_existent_attribute = 2
2453

25-
service(b_instance) # E: Argument 1 to "service" has incompatible type "B"; expected "A"
54+
[env MYPY_DJANGO_CONFIG=${MYPY_CWD}/mypy_django.ini]
2655

27-
c_instance = C()
28-
service(c_instance)
56+
[file mypy_django.ini]
57+
[[mypy_django_plugin]
58+
ignore_missing_model_attributes = True
59+
60+
[/CASE]

Diff for: test-data/typecheck/related_fields.test

+8-2
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,10 @@ class View(models.Model):
109109
app = models.ForeignKey(to=App, related_name='views', on_delete=models.CASCADE)
110110

111111
reveal_type(View().app.views) # E: Revealed type is 'django.db.models.manager.RelatedManager[main.View]'
112-
reveal_type(View().app.unknown) # E: Revealed type is 'Any'
112+
reveal_type(View().app.unknown)
113113
[out]
114+
main:7: error: Revealed type is 'Any'
115+
main:7: error: "App" has no attribute "unknown"
114116

115117
[file myapp/__init__.py]
116118
[file myapp/models.py]
@@ -307,6 +309,10 @@ book = Book()
307309
reveal_type(book.publisher) # E: Revealed type is 'main.Publisher*'
308310

309311
publisher = Publisher()
310-
reveal_type(publisher.books) # E: Revealed type is 'Any'
312+
reveal_type(publisher.books)
311313
reveal_type(publisher.books2) # E: Revealed type is 'django.db.models.manager.RelatedManager[main.Book]'
314+
[out]
315+
main:16: error: Revealed type is 'Any'
316+
main:16: error: "Publisher" has no attribute "books"; maybe "books2"?
317+
[/CASE]
312318

0 commit comments

Comments
 (0)