From f7dfbefbd6a6eb4fbb76d449c8b4e830c7981ca7 Mon Sep 17 00:00:00 2001 From: Richard Eames Date: Tue, 5 Mar 2019 15:37:44 -0700 Subject: [PATCH] Make CharField(blank=True) not be considered nullable (#39) * Make CharField(blank=True) not be considered nullable The documentation on [blank](https://docs.djangoproject.com/en/2.1/ref/models/fields/#blank) says that it "will allow the entry of an empty value", which for a string is just a 0-length string. This patch allows `CharField(blank=True,...)` to no longer be considered `Optional`. closes #38 * fixed tests for `CharField(blank=True)` * allow blank CharField to be nullable in the constructor, but the underlying type is str (unless `null=True`) --- mypy_django_plugin/transformers/fields.py | 4 ---- .../transformers/init_create.py | 7 +++++++ test-data/typecheck/fields.test | 19 ++++++++++++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/mypy_django_plugin/transformers/fields.py b/mypy_django_plugin/transformers/fields.py index 11a616b..f780580 100644 --- 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 index 77b7b7f..7b8e982 100644 --- 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 diff --git a/test-data/typecheck/fields.test b/test-data/typecheck/fields.test index ef0105f..7ebd6ec 100644 --- 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]