mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-06 20:24:31 +08:00
make ignore_missing_model_attributes behaviour optional (#66)
make "ignore_missing_model_attributes" behaviour opt-in
This commit is contained in:
@@ -44,6 +44,9 @@ django_settings = mysettings.local
|
|||||||
# if True, all unknown settings in django.conf.settings will fallback to Any,
|
# 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
|
# specify it if your settings are loaded dynamically to avoid false positives
|
||||||
ignore_missing_settings = True
|
ignore_missing_settings = True
|
||||||
|
|
||||||
|
# if True, unknown attributes on Model instances won't produce errors
|
||||||
|
ignore_missing_model_attributes = True
|
||||||
```
|
```
|
||||||
|
|
||||||
## To get help
|
## To get help
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from dataclasses import dataclass
|
|||||||
class Config:
|
class Config:
|
||||||
django_settings_module: Optional[str] = None
|
django_settings_module: Optional[str] = None
|
||||||
ignore_missing_settings: bool = False
|
ignore_missing_settings: bool = False
|
||||||
|
ignore_missing_model_attributes: bool = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_config_file(cls, fpath: str) -> 'Config':
|
def from_config_file(cls, fpath: str) -> 'Config':
|
||||||
@@ -22,5 +23,9 @@ class Config:
|
|||||||
django_settings = django_settings.strip()
|
django_settings = django_settings.strip()
|
||||||
|
|
||||||
return Config(django_settings_module=django_settings,
|
return Config(django_settings_module=django_settings,
|
||||||
ignore_missing_settings=bool(ini_config.get('mypy_django_plugin', 'ignore_missing_settings',
|
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)))
|
fallback=False)))
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from mypy_django_plugin.transformers.settings import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def transform_model_class(ctx: ClassDefContext) -> None:
|
def transform_model_class(ctx: ClassDefContext, ignore_missing_model_attributes: bool) -> None:
|
||||||
try:
|
try:
|
||||||
sym = ctx.api.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME)
|
sym = ctx.api.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -41,7 +41,7 @@ def transform_model_class(ctx: ClassDefContext) -> None:
|
|||||||
if sym is not None and isinstance(sym.node, TypeInfo):
|
if sym is not None and isinstance(sym.node, TypeInfo):
|
||||||
helpers.get_django_metadata(sym.node)['model_bases'][ctx.cls.fullname] = 1
|
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:
|
def transform_manager_class(ctx: ClassDefContext) -> None:
|
||||||
@@ -248,7 +248,8 @@ class DjangoPlugin(Plugin):
|
|||||||
def get_base_class_hook(self, fullname: str
|
def get_base_class_hook(self, fullname: str
|
||||||
) -> Optional[Callable[[ClassDefContext], None]]:
|
) -> Optional[Callable[[ClassDefContext], None]]:
|
||||||
if fullname in self._get_current_model_bases():
|
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():
|
if fullname in self._get_current_manager_bases():
|
||||||
return transform_manager_class
|
return transform_manager_class
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ def add_get_set_attr_fallback_to_any(ctx: ClassDefContext):
|
|||||||
add_method(ctx, '__setattr__', [name_arg, value_arg], any)
|
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 = [
|
initializers = [
|
||||||
InjectAnyAsBaseForNestedMeta,
|
InjectAnyAsBaseForNestedMeta,
|
||||||
AddDefaultObjectsManager,
|
AddDefaultObjectsManager,
|
||||||
@@ -299,5 +299,5 @@ def process_model_class(ctx: ClassDefContext) -> None:
|
|||||||
|
|
||||||
add_dummy_init_method(ctx)
|
add_dummy_init_method(ctx)
|
||||||
|
|
||||||
# allow unspecified attributes for now
|
if ignore_unknown_attributes:
|
||||||
add_get_set_attr_fallback_to_any(ctx)
|
add_get_set_attr_fallback_to_any(ctx)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ reveal_type(User().my_pk) # E: Revealed type is 'builtins.int*'
|
|||||||
reveal_type(User().id)
|
reveal_type(User().id)
|
||||||
[out]
|
[out]
|
||||||
main:7: error: Revealed type is 'Any'
|
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]
|
||||||
|
|
||||||
[CASE test_meta_nested_class_allows_subclassing_in_multiple_inheritance]
|
[CASE test_meta_nested_class_allows_subclassing_in_multiple_inheritance]
|
||||||
|
|||||||
@@ -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
|
from django.db import models
|
||||||
|
|
||||||
class A(models.Model):
|
class A(models.Model):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class B(models.Model):
|
class B(models.Model):
|
||||||
b_attr = 1
|
b_attr = 1
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class C(A):
|
class C(A):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def service(a: A) -> int:
|
def service(a: A) -> int:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
b_instance = B()
|
||||||
|
service(b_instance) # E: Argument 1 to "service" has incompatible type "B"; expected "A"
|
||||||
|
|
||||||
a_instance = 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()
|
b_instance = B()
|
||||||
reveal_type(b_instance.b_attr) # E: Revealed type is 'builtins.int'
|
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'
|
reveal_type(b_instance.non_existent_attribute) # E: Revealed type is 'Any'
|
||||||
b_instance.non_existent_attribute = 2
|
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()
|
[file mypy_django.ini]
|
||||||
service(c_instance)
|
[[mypy_django_plugin]
|
||||||
|
ignore_missing_model_attributes = True
|
||||||
|
|
||||||
|
[/CASE]
|
||||||
@@ -109,8 +109,10 @@ class View(models.Model):
|
|||||||
app = models.ForeignKey(to=App, related_name='views', on_delete=models.CASCADE)
|
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.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]
|
[out]
|
||||||
|
main:7: error: Revealed type is 'Any'
|
||||||
|
main:7: error: "App" has no attribute "unknown"
|
||||||
|
|
||||||
[file myapp/__init__.py]
|
[file myapp/__init__.py]
|
||||||
[file myapp/models.py]
|
[file myapp/models.py]
|
||||||
@@ -307,6 +309,10 @@ book = Book()
|
|||||||
reveal_type(book.publisher) # E: Revealed type is 'main.Publisher*'
|
reveal_type(book.publisher) # E: Revealed type is 'main.Publisher*'
|
||||||
|
|
||||||
publisher = 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]'
|
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]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user