create(id=None) is valid, if id is AutoField

This commit is contained in:
Maxim Kurnikov
2019-07-22 20:14:59 +03:00
parent 46c48b504f
commit 57796077c6
4 changed files with 86 additions and 54 deletions

View File

@@ -8,7 +8,7 @@ from typing import (
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.db.models.base import Model from django.db.models.base import Model
from django.db.models.fields import CharField, Field from django.db.models.fields import CharField, Field, AutoField
from django.db.models.fields.related import ForeignKey, RelatedField from django.db.models.fields.related import ForeignKey, RelatedField
from django.db.models.fields.reverse_related import ForeignObjectRel from django.db.models.fields.reverse_related import ForeignObjectRel
from django.db.models.sql.query import Query from django.db.models.sql.query import Query
@@ -79,6 +79,9 @@ class DjangoFieldsContext:
if method == '__init__': if method == '__init__':
if field.primary_key or isinstance(field, ForeignKey): if field.primary_key or isinstance(field, ForeignKey):
return True return True
if method == 'create':
if isinstance(field, AutoField):
return True
if field.has_default(): if field.has_default():
return True return True
return nullable return nullable

View File

@@ -1,75 +1,45 @@
from typing import Optional, Tuple, cast from typing import Optional, Tuple, cast
from mypy.nodes import MypyFile, TypeInfo from django.db.models.fields.related import RelatedField
from mypy.nodes import AssignmentStmt, TypeInfo
from mypy.plugin import FunctionContext from mypy.plugin import FunctionContext
from mypy.types import AnyType, CallableType, Instance from mypy.types import AnyType, Instance, Type as MypyType, TypeOfAny
from mypy.types import Type as MypyType
from mypy.types import TypeOfAny
from django.db.models.fields import Field
from mypy_django_plugin.django.context import DjangoContext from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import fullnames, helpers from mypy_django_plugin.lib import fullnames, helpers
def get_referred_to_model_fullname(ctx: FunctionContext, django_context: DjangoContext) -> Optional[str]: def _get_current_field_from_assignment(ctx: FunctionContext, django_context: DjangoContext) -> Optional[Field]:
to_arg_type = helpers.get_call_argument_type_by_name(ctx, 'to')
if isinstance(to_arg_type, CallableType):
assert isinstance(to_arg_type.ret_type, Instance)
return to_arg_type.ret_type.type.fullname()
outer_model_info = ctx.api.scope.active_class() outer_model_info = ctx.api.scope.active_class()
assert isinstance(outer_model_info, TypeInfo) assert isinstance(outer_model_info, TypeInfo)
if not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME):
to_arg_expr = helpers.get_call_argument_by_name(ctx, 'to')
model_string = helpers.resolve_string_attribute_value(to_arg_expr, ctx, django_context)
if model_string is None:
# unresolvable
return None return None
if model_string == 'self': field_name = None
return outer_model_info.fullname() for stmt in outer_model_info.defn.defs.body:
if '.' not in model_string: if isinstance(stmt, AssignmentStmt):
# same file class if stmt.rvalue == ctx.context:
model_cls_is_accessible = False field_name = stmt.lvalues[0].name
for scope in ctx.api.scope.stack:
if isinstance(scope, (MypyFile, TypeInfo)):
model_class_candidate = scope.names.get(model_string)
model_cls_is_accessible = (model_class_candidate is not None
and isinstance(model_class_candidate.node, TypeInfo)
and model_class_candidate.node.has_base(fullnames.MODEL_CLASS_FULLNAME))
if model_cls_is_accessible:
break break
# TODO: FuncItem if field_name is None:
if not model_cls_is_accessible:
ctx.api.fail(f'No model {model_string!r} defined in the current module', ctx.context)
return None return None
return outer_model_info.module_name + '.' + model_string model_cls = django_context.get_model_class_by_fullname(outer_model_info.fullname())
if model_cls is None:
app_label, model_name = model_string.split('.')
if app_label not in django_context.apps_registry.app_configs:
ctx.api.fail(f'No installed app with label {app_label!r}', ctx.context)
return None return None
try: current_field = model_cls._meta.get_field(field_name)
model_cls = django_context.apps_registry.get_model(app_label, model_name) return current_field
except LookupError as exc:
# no model in app
ctx.api.fail(exc.args[0], ctx.context)
return None
model_fullname = helpers.get_class_fullname(model_cls)
return model_fullname
def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context: DjangoContext) -> MypyType: def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context: DjangoContext) -> MypyType:
referred_to_fullname = get_referred_to_model_fullname(ctx, django_context) current_field = _get_current_field_from_assignment(ctx, django_context)
if referred_to_fullname is None: if current_field is None:
return AnyType(TypeOfAny.from_error) return AnyType(TypeOfAny.from_error)
referred_to_typeinfo = helpers.lookup_fully_qualified_generic(referred_to_fullname, ctx.api.modules) assert isinstance(current_field, RelatedField)
assert isinstance(referred_to_typeinfo, TypeInfo), f'Cannot resolve {referred_to_fullname!r}' referred_to_typeinfo = helpers.lookup_class_typeinfo(ctx.api, current_field.related_model)
referred_to_type = Instance(referred_to_typeinfo, []) referred_to_type = Instance(referred_to_typeinfo, [])
default_related_field_type = set_descriptor_types_for_field(ctx) default_related_field_type = set_descriptor_types_for_field(ctx)

View File

@@ -478,3 +478,46 @@
class MyApp2Config(AppConfig): class MyApp2Config(AppConfig):
name = 'myapp2' name = 'myapp2'
label = 'myapp2__user' label = 'myapp2__user'
- case: related_field_to_extracted_from_function
main: |
from myapp.models import Profile
reveal_type(Profile().user) # N: Revealed type is 'myapp.models.User*'
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
pass
def get_user_model_name():
return 'myapp.User'
class Profile(models.Model):
user = models.ForeignKey(to=get_user_model_name(), on_delete=models.CASCADE)
- case: related_manager_name_defined_by_pattern
main: |
from myapp.models import Publisher
reveal_type(Publisher().books) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Book]'
reveal_type(Publisher().articles) # N: Revealed type is 'django.db.models.manager.RelatedManager[myapp.models.Article]'
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class Publisher(models.Model):
pass
class Entry(models.Model):
class Meta:
abstract = True
publisher = models.ForeignKey(to=Publisher, related_name='%(class)ss', on_delete=models.CASCADE)
class Book(Entry):
pass
class Article(Entry):
pass

View File

@@ -55,10 +55,11 @@
class Child4(Child1): class Child4(Child1):
value4 = models.IntegerField() value4 = models.IntegerField()
- case: optional_id_fields_for_create_is_error - case: optional_id_fields_for_create_is_error_if_not_autofield
main: | main: |
from myapp.models import Publisher, Book from myapp.models import Publisher, Book
Book.objects.create(id=None) # E: Incompatible type for "id" of "Book" (got "None", expected "Union[Combinable, int, str]")
Book.objects.create(id=None) # E: Incompatible type for "id" of "Book" (got "None", expected "Union[float, int, str, Combinable]")
Book.objects.create(publisher=None) # E: Incompatible type for "publisher" of "Book" (got "None", expected "Union[Publisher, Combinable]") Book.objects.create(publisher=None) # E: Incompatible type for "publisher" of "Book" (got "None", expected "Union[Publisher, Combinable]")
Book.objects.create(publisher_id=None) # E: Incompatible type for "publisher_id" of "Book" (got "None", expected "Union[Combinable, int, str]") Book.objects.create(publisher_id=None) # E: Incompatible type for "publisher_id" of "Book" (got "None", expected "Union[Combinable, int, str]")
installed_apps: installed_apps:
@@ -71,8 +72,23 @@
class Publisher(models.Model): class Publisher(models.Model):
pass pass
class Book(models.Model): class Book(models.Model):
id = models.IntegerField(primary_key=True)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
- case: none_for_primary_key_is_allowed_if_field_is_autogenerated
main: |
from myapp.models import Book
Book.objects.create(id=None)
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class Book(models.Model):
pass
- case: when_default_for_primary_key_is_specified_allow_none_to_be_set - case: when_default_for_primary_key_is_specified_allow_none_to_be_set
main: | main: |
from myapp.models import MyModel from myapp.models import MyModel