Repository: typeddjango/django-stubs

Issue:
CharField with blank=True should not be considered nullable
AFAIK, `CharField(blank=True,...)` just means that the string can be 0-length, and doesn't mean that it's `Optional`/nullable. The documentation on [`blank`](https://docs.djangoproject.com/en/2.1/ref/models/fields/#blank) also seems to suggest this, saying that it "will allow the entry of an empty value", which for a string is just a 0-length string.

I think the offending code is [fields.py:105](
https://github.com/mkurnikov/django-stubs/blob/c962b8ac68856e5ff39748444c20d8fb99ab9f0b/mypy_django_plugin/transformers/fields.py#L105)

I can make a pull request to remove (or comment out) that block of code if needed


Discussion:
It's fine to remove it. Thanks! 

Resolving patch:
diff --git a/mypy_django_plugin/transformers/fields.py b/mypy_django_plugin/transformers/fields.py
--- a/mypy_django_plugin/transformers/fields.py
+++ b/mypy_django_plugin/transformers/fields.py
@@ -101,10 +101,6 @@ def get_private_descriptor_type(type_info: TypeInfo, private_field_name: str, is
 def set_descriptor_types_for_field(ctx: FunctionContext) -> Instance:
     default_return_type = cast(Instance, ctx.default_return_type)
     is_nullable = helpers.parse_bool(helpers.get_argument_by_name(ctx, 'null'))
-    if not is_nullable and default_return_type.type.has_base(helpers.CHAR_FIELD_FULLNAME):
-        # blank=True for CharField can be interpreted as null=True
-        is_nullable = helpers.parse_bool(helpers.get_argument_by_name(ctx, 'blank'))
-
     set_type = get_private_descriptor_type(default_return_type.type, '_pyi_private_set_type',
                                            is_nullable=is_nullable)
     get_type = get_private_descriptor_type(default_return_type.type, '_pyi_private_get_type',
diff --git a/mypy_django_plugin/transformers/init_create.py b/mypy_django_plugin/transformers/init_create.py
--- a/mypy_django_plugin/transformers/init_create.py
+++ b/mypy_django_plugin/transformers/init_create.py
@@ -183,6 +183,13 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo,
                                                                                              False):
                             field_type = helpers.make_optional(field_type)
 
+                        # if CharField(blank=True,...) and not nullable, then field can be None in __init__
+                        elif (
+                            helpers.has_any_of_bases(typ.type, (helpers.CHAR_FIELD_FULLNAME,)) and is_init and
+                            field_metadata.get('blank', False) and not field_metadata.get('null', False)
+                        ):
+                            field_type = helpers.make_optional(field_type)
+
                         expected_types[name] = field_type
 
     return expected_types


Test patch:
diff --git a/test-data/typecheck/fields.test b/test-data/typecheck/fields.test
--- a/test-data/typecheck/fields.test
+++ b/test-data/typecheck/fields.test
@@ -110,9 +110,22 @@ class MyModel(ParentModel):
 reveal_type(MyModel().id)  # E: Revealed type is 'uuid.UUID*'
 [/CASE]
 
-[CASE blank_for_charfield_is_the_same_as_null]
+[CASE blank_and_null_char_field_allows_none]
 from django.db import models
 class MyModel(models.Model):
-    text = models.CharField(max_length=30, blank=True)
-MyModel(text=None)
+    nulltext=models.CharField(max_length=1, blank=True, null=True)
+MyModel(nulltext="")
+MyModel(nulltext=None)
+MyModel().nulltext=None
+reveal_type(MyModel().nulltext)  # E: Revealed type is 'Union[builtins.str, None]'
+[/CASE]
+
+[CASE blank_and_not_null_charfield_does_not_allow_none]
+from django.db import models
+class MyModel(models.Model):
+    notnulltext=models.CharField(max_length=1, blank=True, null=False)
+MyModel(notnulltext=None)  # Should allow None in constructor
+MyModel(notnulltext="")
+MyModel().notnulltext = None  # E: Incompatible types in assignment (expression has type "None", variable has type "Union[str, int, Combinable]")
+reveal_type(MyModel().notnulltext)  # E: Revealed type is 'builtins.str*'
 [/CASE]