13 Commits

Author SHA1 Message Date
Maxim Kurnikov
8978ad471f bump version 2019-03-06 12:05:24 +03:00
Richard Eames
f7dfbefbd6 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`)
2019-03-06 01:37:44 +03:00
Maxim Kurnikov
627daa55f5 fix extension of django.views __init__ file 2019-03-06 01:28:42 +03:00
Maxim Kurnikov
194489ee8d bump version 2019-03-05 20:21:43 +03:00
Maxim Kurnikov
1d2c7fb805 Remove _Val alias for MultiValueDict so that generic evaluate (#36)
* remove _Val alias for MultiValueDict so that generic evaluate

* fix multivaluedict init argument
2019-03-05 20:16:24 +03:00
Maxim Kurnikov
18c908bf98 set plugin_generated on new symbol nodes 2019-03-05 20:15:46 +03:00
Maxim Kurnikov
e0e8814804 Revert "dont convert to optional, if anytype"
This reverts commit 53f5d2214b.
2019-03-05 19:11:02 +03:00
Maxim Kurnikov
53f5d2214b dont convert to optional, if anytype 2019-03-05 18:43:10 +03:00
Maxim Kurnikov
9e4ed70fc5 Disable note: messages (#35)
* add global note: ignore
2019-03-01 05:15:05 +03:00
Maxim Kurnikov
18445f686f set fallback= for ini parser 2019-03-01 02:25:15 +03:00
Maxim Kurnikov
c962b8ac68 attempt to add flake8 and isort 2019-03-01 02:07:53 +03:00
Maxim Kurnikov
70c3126348 add plugin testing for python3.6 2019-02-27 18:12:29 +03:00
Maxim Kurnikov
af8ecc5520 remove django from dependencies, it's not required for static analysis 2019-02-27 18:11:54 +03:00
20 changed files with 183 additions and 89 deletions

View File

@@ -4,20 +4,34 @@ dist: xenial
sudo: required sudo: required
jobs: jobs:
include: include:
- name: Typecheck Django test suite
python: 3.7
script: 'python ./scripts/typecheck_tests.py'
- name: Run plugin test suite with python 3.7 - name: Run plugin test suite with python 3.7
python: 3.7 python: 3.7
script: | script: |
set -e set -e
pytest pytest
- name: Run plugin test suite with python 3.6
python: 3.6
script: |
set -e
pytest
- name: Typecheck Django test suite
python: 3.7
script: 'python ./scripts/typecheck_tests.py'
- name: Lint with black - name: Lint with black
python: 3.7 python: 3.7
script: 'black --check --line-length=120 django-stubs/' script: 'black --check --line-length=120 django-stubs/'
- name: Lint plugin code with flake8
python: 3.7
script: 'flake8'
- name: Lint plugin code with isort
python: 3.7
script: 'isort --check'
before_install: | before_install: |
# Upgrade pip, setuptools, and wheel # Upgrade pip, setuptools, and wheel
pip install -U pip setuptools wheel pip install -U pip setuptools wheel

View File

@@ -1,3 +1,5 @@
black black
pytest-mypy-plugins pytest-mypy-plugins
flake8
isort==4.3.4
-e . -e .

View File

@@ -12,8 +12,11 @@ from typing import (
Union, Union,
overload, overload,
Iterator, Iterator,
Optional,
) )
from typing_extensions import Literal
_K = TypeVar("_K") _K = TypeVar("_K")
_V = TypeVar("_V") _V = TypeVar("_V")
@@ -27,24 +30,22 @@ class OrderedSet(MutableSet[_K]):
class MultiValueDictKeyError(KeyError): ... class MultiValueDictKeyError(KeyError): ...
_Val = Union[_V, List[_V]]
class MultiValueDict(MutableMapping[_K, _V]): class MultiValueDict(MutableMapping[_K, _V]):
@overload @overload
def __init__(self, key_to_list_mapping: Iterable[Tuple[_K, _Val]] = ...) -> None: ... def __init__(self, key_to_list_mapping: Mapping[_K, Optional[List[_V]]] = ...) -> None: ...
@overload @overload
def __init__(self, key_to_list_mapping: Mapping[_K, _Val] = ...) -> None: ... def __init__(self, key_to_list_mapping: Iterable[Tuple[_K, List[_V]]] = ...) -> None: ...
def getlist(self, key: _K, default: List[_V] = None) -> List[_V]: ... def getlist(self, key: _K, default: List[_V] = None) -> List[_V]: ...
def setlist(self, key: _K, list_: List[_V]) -> None: ... def setlist(self, key: _K, list_: List[_V]) -> None: ...
def setlistdefault(self, key: _K, default_list: List[_V] = None) -> List[_V]: ... def setlistdefault(self, key: _K, default_list: List[_V] = None) -> List[_V]: ...
def appendlist(self, key: _K, value: _V) -> None: ... def appendlist(self, key: _K, value: _V) -> None: ...
def lists(self) -> Iterable[Tuple[_K, List[_V]]]: ... def lists(self) -> Iterable[Tuple[_K, List[_V]]]: ...
def dict(self) -> Dict[_K, _Val]: ... def dict(self) -> Dict[_K, Union[_V, List[_V]]]: ...
def copy(self) -> MultiValueDict[_K, _V]: ... def copy(self) -> MultiValueDict[_K, _V]: ...
# These overrides are needed to convince mypy that this isn't an abstract class # These overrides are needed to convince mypy that this isn't an abstract class
def __delitem__(self, item: _K) -> None: ... def __delitem__(self, item: _K) -> None: ...
def __getitem__(self, item: _K) -> _Val: ... # type: ignore def __getitem__(self, item: _K) -> Union[_V, Literal[[]]]: ... # type: ignore
def __setitem__(self, k: _K, v: _Val) -> None: ... def __setitem__(self, k: _K, v: Union[_V, List[_V]]) -> None: ...
def __len__(self) -> int: ... def __len__(self) -> int: ...
def __iter__(self) -> Iterator[_K]: ... def __iter__(self) -> Iterator[_K]: ...

View File

@@ -1,5 +1,5 @@
from configparser import ConfigParser from configparser import ConfigParser
from typing import List, Optional from typing import Optional
from dataclasses import dataclass from dataclasses import dataclass
@@ -20,6 +20,7 @@ class Config:
fallback=None) fallback=None)
if django_settings: if django_settings:
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=ini_config.get('mypy_django_plugin', 'ignore_missing_settings', ignore_missing_settings=bool(ini_config.get('mypy_django_plugin', 'ignore_missing_settings',
fallback=False)) fallback=False)))

View File

@@ -2,10 +2,13 @@ import typing
from typing import Dict, Optional from typing import Dict, Optional
from mypy.checker import TypeChecker from mypy.checker import TypeChecker
from mypy.nodes import AssignmentStmt, ClassDef, Expression, ImportedName, Lvalue, MypyFile, NameExpr, SymbolNode, \ from mypy.nodes import (
TypeInfo AssignmentStmt, ClassDef, Expression, ImportedName, Lvalue, MypyFile, NameExpr, SymbolNode, TypeInfo,
)
from mypy.plugin import FunctionContext, MethodContext from mypy.plugin import FunctionContext, MethodContext
from mypy.types import AnyType, Instance, NoneTyp, Type, TypeOfAny, TypeVarType, UnionType from mypy.types import (
AnyType, Instance, NoneTyp, Type, TypeOfAny, TypeVarType, UnionType,
)
MODEL_CLASS_FULLNAME = 'django.db.models.base.Model' MODEL_CLASS_FULLNAME = 'django.db.models.base.Model'
FIELD_FULLNAME = 'django.db.models.fields.Field' FIELD_FULLNAME = 'django.db.models.fields.Field'

View File

@@ -2,19 +2,28 @@ import os
from typing import Callable, Dict, Optional, Union, cast from typing import Callable, Dict, Optional, Union, cast
from mypy.checker import TypeChecker from mypy.checker import TypeChecker
from mypy.nodes import MemberExpr, TypeInfo, NameExpr from mypy.nodes import MemberExpr, NameExpr, TypeInfo
from mypy.options import Options from mypy.options import Options
from mypy.plugin import AttributeContext, ClassDefContext, FunctionContext, MethodContext, Plugin from mypy.plugin import (
from mypy.types import AnyType, Instance, Type, TypeOfAny, TypeType, UnionType, CallableType, NoneTyp AttributeContext, ClassDefContext, FunctionContext, MethodContext, Plugin,
)
from mypy.types import (
AnyType, CallableType, Instance, NoneTyp, Type, TypeOfAny, TypeType, UnionType,
)
from mypy_django_plugin import helpers, monkeypatch from mypy_django_plugin import helpers, monkeypatch
from mypy_django_plugin.config import Config from mypy_django_plugin.config import Config
from mypy_django_plugin.transformers import fields, init_create from mypy_django_plugin.transformers import fields, init_create
from mypy_django_plugin.transformers.forms import make_meta_nested_class_inherit_from_any from mypy_django_plugin.transformers.forms import (
from mypy_django_plugin.transformers.migrations import determine_model_cls_from_string_for_migrations, \ make_meta_nested_class_inherit_from_any,
get_string_value_from_expr )
from mypy_django_plugin.transformers.migrations import (
determine_model_cls_from_string_for_migrations, get_string_value_from_expr,
)
from mypy_django_plugin.transformers.models import process_model_class from mypy_django_plugin.transformers.models import process_model_class
from mypy_django_plugin.transformers.settings import AddSettingValuesToDjangoConfObject, get_settings_metadata from mypy_django_plugin.transformers.settings import (
AddSettingValuesToDjangoConfObject, get_settings_metadata,
)
def transform_model_class(ctx: ClassDefContext) -> None: def transform_model_class(ctx: ClassDefContext) -> None:

View File

@@ -1,4 +0,0 @@
from .dependencies import (add_modules_as_a_source_seed_files,
inject_modules_as_dependencies_for_django_conf_settings,
restore_original_load_graph,
restore_original_dependencies_handling)

View File

@@ -3,9 +3,11 @@ from typing import Optional, cast
from mypy.checker import TypeChecker from mypy.checker import TypeChecker
from mypy.nodes import ListExpr, NameExpr, StrExpr, TupleExpr, TypeInfo, Var from mypy.nodes import ListExpr, NameExpr, StrExpr, TupleExpr, TypeInfo, Var
from mypy.plugin import FunctionContext from mypy.plugin import FunctionContext
from mypy.types import AnyType, CallableType, Instance, TupleType, Type, TypeOfAny, UnionType from mypy.types import (
AnyType, CallableType, Instance, TupleType, Type, TypeOfAny, UnionType,
)
from mypy_django_plugin import helpers from mypy_django_plugin import helpers
from mypy_django_plugin.transformers.models import iter_over_assignments
def extract_referred_to_type(ctx: FunctionContext) -> Optional[Instance]: def extract_referred_to_type(ctx: FunctionContext) -> Optional[Instance]:
@@ -99,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: def set_descriptor_types_for_field(ctx: FunctionContext) -> Instance:
default_return_type = cast(Instance, ctx.default_return_type) default_return_type = cast(Instance, ctx.default_return_type)
is_nullable = helpers.parse_bool(helpers.get_argument_by_name(ctx, 'null')) 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', set_type = get_private_descriptor_type(default_return_type.type, '_pyi_private_set_type',
is_nullable=is_nullable) is_nullable=is_nullable)
get_type = get_private_descriptor_type(default_return_type.type, '_pyi_private_get_type', get_type = get_private_descriptor_type(default_return_type.type, '_pyi_private_get_type',
@@ -154,7 +152,7 @@ def record_field_properties_into_outer_model_class(ctx: FunctionContext) -> None
return return
field_name = None field_name = None
for name_expr, stmt in iter_over_assignments(outer_model.defn): for name_expr, stmt in helpers.iter_over_assignments(outer_model.defn):
if stmt == ctx.context and isinstance(name_expr, NameExpr): if stmt == ctx.context and isinstance(name_expr, NameExpr):
field_name = name_expr.name field_name = name_expr.name
break break

View File

@@ -1,4 +1,5 @@
from mypy.plugin import ClassDefContext from mypy.plugin import ClassDefContext
from mypy_django_plugin import helpers from mypy_django_plugin import helpers

View File

@@ -4,8 +4,8 @@ from mypy.checker import TypeChecker
from mypy.nodes import TypeInfo, Var from mypy.nodes import TypeInfo, Var
from mypy.plugin import FunctionContext, MethodContext from mypy.plugin import FunctionContext, MethodContext
from mypy.types import AnyType, Instance, Type, TypeOfAny from mypy.types import AnyType, Instance, Type, TypeOfAny
from mypy_django_plugin import helpers from mypy_django_plugin import helpers
from mypy_django_plugin.helpers import extract_field_setter_type, extract_explicit_set_type_of_model_primary_key, get_fields_metadata
from mypy_django_plugin.transformers.fields import get_private_descriptor_type from mypy_django_plugin.transformers.fields import get_private_descriptor_type
@@ -106,7 +106,7 @@ def redefine_and_typecheck_model_create(ctx: MethodContext) -> Type:
def extract_choices_type(model: TypeInfo, field_name: str) -> Optional[str]: def extract_choices_type(model: TypeInfo, field_name: str) -> Optional[str]:
field_metadata = get_fields_metadata(model).get(field_name, {}) field_metadata = helpers.get_fields_metadata(model).get(field_name, {})
if 'choices' in field_metadata: if 'choices' in field_metadata:
return field_metadata['choices'] return field_metadata['choices']
return None return None
@@ -117,7 +117,7 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo,
api = cast(TypeChecker, ctx.api) api = cast(TypeChecker, ctx.api)
expected_types: Dict[str, Type] = {} expected_types: Dict[str, Type] = {}
primary_key_type = extract_explicit_set_type_of_model_primary_key(model) primary_key_type = helpers.extract_explicit_set_type_of_model_primary_key(model)
if not primary_key_type: if not primary_key_type:
# no explicit primary key, set pk to Any and add id # no explicit primary key, set pk to Any and add id
primary_key_type = AnyType(TypeOfAny.special_form) primary_key_type = AnyType(TypeOfAny.special_form)
@@ -143,7 +143,7 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo,
expected_types[name + '_id'] = AnyType(TypeOfAny.from_unimported_type) expected_types[name + '_id'] = AnyType(TypeOfAny.from_unimported_type)
elif isinstance(typ, Instance): elif isinstance(typ, Instance):
field_type = extract_field_setter_type(typ) field_type = helpers.extract_field_setter_type(typ)
if field_type is None: if field_type is None:
continue continue
@@ -156,8 +156,9 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo,
if is_nullable: if is_nullable:
referred_to_model = helpers.make_required(typ.args[1]) referred_to_model = helpers.make_required(typ.args[1])
if isinstance(referred_to_model, Instance) and referred_to_model.type.has_base(helpers.MODEL_CLASS_FULLNAME): if isinstance(referred_to_model, Instance) and referred_to_model.type.has_base(
pk_type = extract_explicit_set_type_of_model_primary_key(referred_to_model.type) helpers.MODEL_CLASS_FULLNAME):
pk_type = helpers.extract_explicit_set_type_of_model_primary_key(referred_to_model.type)
if not pk_type: if not pk_type:
# extract set type of AutoField # extract set type of AutoField
autofield_info = api.lookup_typeinfo('django.db.models.fields.AutoField') autofield_info = api.lookup_typeinfo('django.db.models.fields.AutoField')
@@ -170,7 +171,7 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo,
expected_types[name + '_id'] = related_primary_key_type expected_types[name + '_id'] = related_primary_key_type
field_metadata = get_fields_metadata(model).get(name, {}) field_metadata = helpers.get_fields_metadata(model).get(name, {})
if field_type: if field_type:
# related fields could be None in __init__ (but should be specified before save()) # related fields could be None in __init__ (but should be specified before save())
if helpers.has_any_of_bases(typ.type, (helpers.FOREIGN_KEY_FULLNAME, if helpers.has_any_of_bases(typ.type, (helpers.FOREIGN_KEY_FULLNAME,
@@ -178,7 +179,15 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo,
field_type = helpers.make_optional(field_type) field_type = helpers.make_optional(field_type)
# if primary_key=True and default specified # if primary_key=True and default specified
elif field_metadata.get('primary_key', False) and field_metadata.get('default_specified', False): elif field_metadata.get('primary_key', False) and field_metadata.get('default_specified',
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) field_type = helpers.make_optional(field_type)
expected_types[name] = field_type expected_types[name] = field_type

View File

@@ -4,6 +4,7 @@ from mypy.checker import TypeChecker
from mypy.nodes import Expression, StrExpr, TypeInfo from mypy.nodes import Expression, StrExpr, TypeInfo
from mypy.plugin import MethodContext from mypy.plugin import MethodContext
from mypy.types import Instance, Type, TypeType from mypy.types import Instance, Type, TypeType
from mypy_django_plugin import helpers from mypy_django_plugin import helpers

View File

@@ -2,14 +2,16 @@ from abc import ABCMeta, abstractmethod
from typing import Dict, Iterator, List, Optional, Tuple, cast from typing import Dict, Iterator, List, Optional, Tuple, cast
import dataclasses import dataclasses
from mypy.nodes import ARG_STAR, ARG_STAR2, Argument, CallExpr, ClassDef, Expression, IndexExpr, \ from mypy.nodes import (
Lvalue, MDEF, MemberExpr, MypyFile, NameExpr, StrExpr, SymbolTableNode, TypeInfo, Var ARG_STAR, ARG_STAR2, MDEF, Argument, CallExpr, ClassDef, Expression, IndexExpr, Lvalue, MemberExpr, MypyFile,
NameExpr, StrExpr, SymbolTableNode, TypeInfo, Var,
)
from mypy.plugin import ClassDefContext from mypy.plugin import ClassDefContext
from mypy.plugins.common import add_method from mypy.plugins.common import add_method
from mypy.semanal import SemanticAnalyzerPass2 from mypy.semanal import SemanticAnalyzerPass2
from mypy.types import AnyType, Instance, NoneTyp, TypeOfAny from mypy.types import AnyType, Instance, NoneTyp, TypeOfAny
from mypy_django_plugin import helpers from mypy_django_plugin import helpers
from mypy_django_plugin.helpers import iter_over_assignments
@dataclasses.dataclass @dataclasses.dataclass
@@ -40,7 +42,7 @@ class ModelClassInitializer(metaclass=ABCMeta):
var._fullname = self.model_classdef.info.fullname() + '.' + name var._fullname = self.model_classdef.info.fullname() + '.' + name
var.is_inferred = True var.is_inferred = True
var.is_initialized_in_class = True var.is_initialized_in_class = True
self.model_classdef.info.names[name] = SymbolTableNode(MDEF, var) self.model_classdef.info.names[name] = SymbolTableNode(MDEF, var, plugin_generated=True)
@abstractmethod @abstractmethod
def run(self) -> None: def run(self) -> None:
@@ -48,7 +50,7 @@ class ModelClassInitializer(metaclass=ABCMeta):
def iter_call_assignments(klass: ClassDef) -> Iterator[Tuple[Lvalue, CallExpr]]: def iter_call_assignments(klass: ClassDef) -> Iterator[Tuple[Lvalue, CallExpr]]:
for lvalue, rvalue in iter_over_assignments(klass): for lvalue, rvalue in helpers.iter_over_assignments(klass):
if isinstance(rvalue, CallExpr): if isinstance(rvalue, CallExpr):
yield lvalue, rvalue yield lvalue, rvalue
@@ -56,7 +58,7 @@ def iter_call_assignments(klass: ClassDef) -> Iterator[Tuple[Lvalue, CallExpr]]:
def iter_over_one_to_n_related_fields(klass: ClassDef) -> Iterator[Tuple[NameExpr, CallExpr]]: def iter_over_one_to_n_related_fields(klass: ClassDef) -> Iterator[Tuple[NameExpr, CallExpr]]:
for lvalue, rvalue in iter_call_assignments(klass): for lvalue, rvalue in iter_call_assignments(klass):
if (isinstance(lvalue, NameExpr) if (isinstance(lvalue, NameExpr)
and isinstance(rvalue.callee, MemberExpr)): and isinstance(rvalue.callee, MemberExpr)):
if rvalue.callee.fullname in {helpers.FOREIGN_KEY_FULLNAME, if rvalue.callee.fullname in {helpers.FOREIGN_KEY_FULLNAME,
helpers.ONETOONE_FIELD_FULLNAME}: helpers.ONETOONE_FIELD_FULLNAME}:
yield lvalue, rvalue yield lvalue, rvalue
@@ -107,8 +109,8 @@ class AddDefaultObjectsManager(ModelClassInitializer):
if isinstance(callee_expr, IndexExpr): if isinstance(callee_expr, IndexExpr):
callee_expr = callee_expr.analyzed.expr callee_expr = callee_expr.analyzed.expr
if isinstance(callee_expr, (MemberExpr, NameExpr)) \ if isinstance(callee_expr, (MemberExpr, NameExpr)) \
and isinstance(callee_expr.node, TypeInfo) \ and isinstance(callee_expr.node, TypeInfo) \
and callee_expr.node.has_base(helpers.BASE_MANAGER_CLASS_FULLNAME): and callee_expr.node.has_base(helpers.BASE_MANAGER_CLASS_FULLNAME):
managers.append((manager_name, callee_expr.node)) managers.append((manager_name, callee_expr.node))
return managers return managers
@@ -147,7 +149,7 @@ class AddIdAttributeIfPrimaryKeyTrueIsNotSet(ModelClassInitializer):
for _, rvalue in iter_call_assignments(self.model_classdef): for _, rvalue in iter_call_assignments(self.model_classdef):
if ('primary_key' in rvalue.arg_names if ('primary_key' in rvalue.arg_names
and self.api.parse_bool(rvalue.args[rvalue.arg_names.index('primary_key')])): and self.api.parse_bool(rvalue.args[rvalue.arg_names.index('primary_key')])):
break break
else: else:
self.add_new_node_to_model_class('id', self.api.builtin_type('builtins.object')) self.add_new_node_to_model_class('id', self.api.builtin_type('builtins.object'))
@@ -202,10 +204,10 @@ def is_related_field(expr: CallExpr, module_file: MypyFile) -> bool:
if isinstance(expr.callee, MemberExpr) and isinstance(expr.callee.expr, NameExpr): if isinstance(expr.callee, MemberExpr) and isinstance(expr.callee.expr, NameExpr):
module = module_file.names.get(expr.callee.expr.name) module = module_file.names.get(expr.callee.expr.name)
if module \ if module \
and module.fullname == 'django.db.models' \ and module.fullname == 'django.db.models' \
and expr.callee.name in {'ForeignKey', and expr.callee.name in {'ForeignKey',
'OneToOneField', 'OneToOneField',
'ManyToManyField'}: 'ManyToManyField'}:
return True return True
return False return False

View File

@@ -1,6 +1,8 @@
from typing import Iterable, List, Optional, cast from typing import Iterable, List, Optional, cast
from mypy.nodes import ClassDef, Context, ImportAll, MypyFile, SymbolNode, SymbolTableNode, TypeInfo, Var from mypy.nodes import (
ClassDef, Context, ImportAll, MypyFile, SymbolNode, SymbolTableNode, TypeInfo, Var,
)
from mypy.plugin import ClassDefContext from mypy.plugin import ClassDefContext
from mypy.semanal import SemanticAnalyzerPass2 from mypy.semanal import SemanticAnalyzerPass2
from mypy.types import AnyType, Instance, NoneTyp, Type, TypeOfAny, UnionType from mypy.types import AnyType, Instance, NoneTyp, Type, TypeOfAny, UnionType
@@ -56,8 +58,8 @@ def load_settings_from_names(settings_classdef: ClassDef,
settings_classdef.info.names[name] = copied settings_classdef.info.names[name] = copied
else: else:
var = Var(name, AnyType(TypeOfAny.unannotated)) var = Var(name, AnyType(TypeOfAny.unannotated))
var.info = api.named_type('__builtins__.object').type var.info = api.named_type('__builtins__.object').type # outer class type
settings_classdef.info.names[name] = SymbolTableNode(sym.kind, var) settings_classdef.info.names[name] = SymbolTableNode(sym.kind, var, plugin_generated=True)
settings_metadata[name] = module.fullname() settings_metadata[name] = module.fullname()
@@ -67,11 +69,12 @@ def get_import_star_modules(api: SemanticAnalyzerPass2, module: MypyFile) -> Lis
for module_import in module.imports: for module_import in module.imports:
# relative import * are not resolved by mypy # relative import * are not resolved by mypy
if isinstance(module_import, ImportAll) and module_import.relative: if isinstance(module_import, ImportAll) and module_import.relative:
absolute_import_path, correct = correct_relative_import(module.fullname(), module_import.relative, module_import.id, absolute_import_path, correct = correct_relative_import(module.fullname(), module_import.relative,
is_cur_package_init_file=False) module_import.id, is_cur_package_init_file=False)
if not correct: if not correct:
return [] return []
for path in [absolute_import_path] + get_import_star_modules(api, module=api.modules.get(absolute_import_path)): for path in [absolute_import_path] + get_import_star_modules(api,
module=api.modules.get(absolute_import_path)):
if path not in import_star_modules: if path not in import_star_modules:
import_star_modules.append(path) import_star_modules.append(path)
return import_star_modules return import_star_modules

View File

@@ -1,5 +1,4 @@
[pytest] [pytest]
testpaths = ./test-data testpaths = ./test-data
addopts = addopts =
--tb=native --tb=native

View File

@@ -1,3 +1,4 @@
import itertools
import os import os
import re import re
import sys import sys
@@ -37,7 +38,8 @@ IGNORED_ERRORS = {
# settings # settings
re.compile(r'Module has no attribute "[A-Z_]+"'), re.compile(r'Module has no attribute "[A-Z_]+"'),
# attributes assigned to test functions # attributes assigned to test functions
re.compile(r'"Callable\[(\[(Any(, )?)*((, )?VarArg\(Any\))?((, )?KwArg\(Any\))?\]|\.\.\.), Any\]" has no attribute'), re.compile(
r'"Callable\[(\[(Any(, )?)*((, )?VarArg\(Any\))?((, )?KwArg\(Any\))?\]|\.\.\.), Any\]" has no attribute'),
# assign empty tuple # assign empty tuple
re.compile(r'Incompatible types in assignment \(expression has type "Tuple\[\]", ' re.compile(r'Incompatible types in assignment \(expression has type "Tuple\[\]", '
r'variable has type "Tuple\[[A-Za-z, ]+\]"'), r'variable has type "Tuple\[[A-Za-z, ]+\]"'),
@@ -54,8 +56,10 @@ IGNORED_ERRORS = {
'ValuesIterable', 'ValuesIterable',
'Value of type "Optional[Dict[str, Any]]" is not indexable', 'Value of type "Optional[Dict[str, Any]]" is not indexable',
'Argument 1 to "len" has incompatible type "Optional[List[_Record]]"; expected "Sized"', 'Argument 1 to "len" has incompatible type "Optional[List[_Record]]"; expected "Sized"',
'Argument 1 to "loads" has incompatible type "Union[bytes, str, None]"; expected "Union[str, bytes, bytearray]"', 'Argument 1 to "loads" has incompatible type "Union[bytes, str, None]"; '
'Incompatible types in assignment (expression has type "None", variable has type Module)' + 'expected "Union[str, bytes, bytearray]"',
'Incompatible types in assignment (expression has type "None", variable has type Module)',
'note:'
], ],
'admin_changelist': [ 'admin_changelist': [
'Incompatible types in assignment (expression has type "FilteredChildAdmin", variable has type "ChildAdmin")' 'Incompatible types in assignment (expression has type "FilteredChildAdmin", variable has type "ChildAdmin")'
@@ -65,8 +69,9 @@ IGNORED_ERRORS = {
], ],
'admin_widgets': [ 'admin_widgets': [
'Incompatible types in assignment (expression has type "RelatedFieldWidgetWrapper", ' 'Incompatible types in assignment (expression has type "RelatedFieldWidgetWrapper", '
'variable has type "AdminRadioSelect")', + 'variable has type "AdminRadioSelect")',
'Incompatible types in assignment (expression has type "Union[Widget, Any]", variable has type "AutocompleteSelect")' 'Incompatible types in assignment (expression has type "Union[Widget, Any]", '
+ 'variable has type "AutocompleteSelect")'
], ],
'admin_utils': [ 'admin_utils': [
re.compile(r'Argument [0-9] to "lookup_field" has incompatible type'), re.compile(r'Argument [0-9] to "lookup_field" has incompatible type'),
@@ -91,11 +96,13 @@ IGNORED_ERRORS = {
'Incompatible types in assignment (expression has type "FlatValuesListIterable", ' 'Incompatible types in assignment (expression has type "FlatValuesListIterable", '
+ 'variable has type "ValuesListIterable")', + 'variable has type "ValuesListIterable")',
'Incompatible type for "contact" of "Book" (got "Optional[Author]", expected "Union[Author, Combinable]")', 'Incompatible type for "contact" of "Book" (got "Optional[Author]", expected "Union[Author, Combinable]")',
'Incompatible type for "publisher" of "Book" (got "Optional[Publisher]", expected "Union[Publisher, Combinable]")' 'Incompatible type for "publisher" of "Book" (got "Optional[Publisher]", '
+ 'expected "Union[Publisher, Combinable]")'
], ],
'aggregation_regress': [ 'aggregation_regress': [
'Incompatible types in assignment (expression has type "List[str]", variable has type "QuerySet[Author]")', 'Incompatible types in assignment (expression has type "List[str]", variable has type "QuerySet[Author]")',
'Incompatible types in assignment (expression has type "FlatValuesListIterable", variable has type "QuerySet[Any]")', 'Incompatible types in assignment (expression has type "FlatValuesListIterable", '
+ 'variable has type "QuerySet[Any]")',
'Too few arguments for "count" of "Sequence"' 'Too few arguments for "count" of "Sequence"'
], ],
'apps': [ 'apps': [
@@ -176,8 +183,8 @@ IGNORED_ERRORS = {
'variable has type "SongForm"', 'variable has type "SongForm"',
'"full_clean" of "BaseForm" does not return a value', '"full_clean" of "BaseForm" does not return a value',
'No overload variant of "zip" matches argument types "Tuple[str, str, str]", "object"', 'No overload variant of "zip" matches argument types "Tuple[str, str, str]", "object"',
'note:', 'Incompatible types in assignment (expression has type "GetDateShowHiddenInitial", '
'Incompatible types in assignment (expression has type "GetDateShowHiddenInitial", variable has type "GetDate")', + 'variable has type "GetDate")',
re.compile(r'Incompatible types in assignment \(expression has type "[a-zA-Z]+Field", ' re.compile(r'Incompatible types in assignment \(expression has type "[a-zA-Z]+Field", '
r'base class "BaseForm" defined the type as "Dict\[str, Any\]"\)'), r'base class "BaseForm" defined the type as "Dict\[str, Any\]"\)'),
'List or tuple expected as variable arguments', 'List or tuple expected as variable arguments',
@@ -188,7 +195,7 @@ IGNORED_ERRORS = {
'Incompatible types in assignment (expression has type "TestForm", variable has type "Person")', 'Incompatible types in assignment (expression has type "TestForm", variable has type "Person")',
'Incompatible types in assignment (expression has type "Type[Textarea]", ' 'Incompatible types in assignment (expression has type "Type[Textarea]", '
+ 'base class "Field" defined the type as "Widget")', + 'base class "Field" defined the type as "Widget")',
'Incompatible types in assignment (expression has type "SimpleUploadedFile", variable has type "BinaryIO")' 'Incompatible types in assignment (expression has type "SimpleUploadedFile", variable has type "BinaryIO")',
], ],
'get_object_or_404': [ 'get_object_or_404': [
'Argument 1 to "get_object_or_404" has incompatible type "str"; ' 'Argument 1 to "get_object_or_404" has incompatible type "str"; '
@@ -216,13 +223,13 @@ IGNORED_ERRORS = {
], ],
'lookup': [ 'lookup': [
'Unexpected keyword argument "headline__startswith" for "in_bulk" of "QuerySet"', 'Unexpected keyword argument "headline__startswith" for "in_bulk" of "QuerySet"',
'note: '
], ],
'many_to_one': [ 'many_to_one': [
'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")' 'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")'
], ],
'model_inheritance_regress': [ 'model_inheritance_regress': [
'Incompatible types in assignment (expression has type "List[Supplier]", variable has type "QuerySet[Supplier]")' 'Incompatible types in assignment (expression has type "List[Supplier]", '
+ 'variable has type "QuerySet[Supplier]")'
], ],
'model_meta': [ 'model_meta': [
'"object" has no attribute "items"', '"object" has no attribute "items"',
@@ -238,10 +245,10 @@ IGNORED_ERRORS = {
'Cannot assign multiple types to name "PersonTwoImages" without an explicit "Type[...]" annotation', 'Cannot assign multiple types to name "PersonTwoImages" without an explicit "Type[...]" annotation',
'Incompatible types in assignment (expression has type "Type[Person]", ' 'Incompatible types in assignment (expression has type "Type[Person]", '
+ 'base class "ImageFieldTestMixin" defined the type as "Type[PersonWithHeightAndWidth]")', + 'base class "ImageFieldTestMixin" defined the type as "Type[PersonWithHeightAndWidth]")',
'note: "Person" defined here'
], ],
'model_formsets': [ 'model_formsets': [
'Incompatible types in string interpolation (expression has type "object", placeholder has type "Union[int, float]")' 'Incompatible types in string interpolation (expression has type "object", '
+ 'placeholder has type "Union[int, float]")'
], ],
'model_formsets_regress': [ 'model_formsets_regress': [
'Incompatible types in assignment (expression has type "Model", variable has type "User")' 'Incompatible types in assignment (expression has type "Model", variable has type "User")'
@@ -287,9 +294,11 @@ IGNORED_ERRORS = {
'Incompatible types in assignment (expression has type "Type[Field[Any, Any]]', 'Incompatible types in assignment (expression has type "Type[Field[Any, Any]]',
'DummyArrayField', 'DummyArrayField',
'DummyJSONField', 'DummyJSONField',
'Argument "encoder" to "JSONField" has incompatible type "DjangoJSONEncoder"; expected "Optional[Type[JSONEncoder]]"', 'Argument "encoder" to "JSONField" has incompatible type "DjangoJSONEncoder"; '
+ 'expected "Optional[Type[JSONEncoder]]"',
'for model "CITestModel"', 'for model "CITestModel"',
'Incompatible type for "field" of "IntegerArrayModel" (got "None", expected "Union[Sequence[int], Combinable]")' 'Incompatible type for "field" of "IntegerArrayModel" (got "None", '
+ 'expected "Union[Sequence[int], Combinable]")'
], ],
'properties': [ 'properties': [
re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"') re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"')
@@ -334,11 +343,14 @@ IGNORED_ERRORS = {
'Incompatible types in assignment (expression has type "Thread", variable has type "Callable[[], Any]")' 'Incompatible types in assignment (expression has type "Thread", variable has type "Callable[[], Any]")'
], ],
'test_client': [ 'test_client': [
'Incompatible types in assignment (expression has type "StreamingHttpResponse", variable has type "HttpResponse")', 'Incompatible types in assignment (expression has type "StreamingHttpResponse", '
'Incompatible types in assignment (expression has type "HttpResponse", variable has type "StreamingHttpResponse")' + 'variable has type "HttpResponse")',
'Incompatible types in assignment (expression has type "HttpResponse", '
+ 'variable has type "StreamingHttpResponse")'
], ],
'test_client_regress': [ 'test_client_regress': [
'Incompatible types in assignment (expression has type "Dict[<nothing>, <nothing>]", variable has type "SessionBase")', 'Incompatible types in assignment (expression has type "Dict[<nothing>, <nothing>]", '
+ 'variable has type "SessionBase")',
'Unsupported left operand type for + ("None")', 'Unsupported left operand type for + ("None")',
'Both left and right operands are unions' 'Both left and right operands are unions'
], ],
@@ -348,7 +360,8 @@ IGNORED_ERRORS = {
'test_runner': [ 'test_runner': [
'Value of type "TestSuite" is not indexable', 'Value of type "TestSuite" is not indexable',
'"TestSuite" has no attribute "_tests"', '"TestSuite" has no attribute "_tests"',
'Argument "result" to "run" of "TestCase" has incompatible type "RemoteTestResult"; expected "Optional[TestResult]"', 'Argument "result" to "run" of "TestCase" has incompatible type "RemoteTestResult"; '
+ 'expected "Optional[TestResult]"',
'Item "TestSuite" of "Union[TestCase, TestSuite]" has no attribute "id"', 'Item "TestSuite" of "Union[TestCase, TestSuite]" has no attribute "id"',
'MockTestRunner', 'MockTestRunner',
'Incompatible types in assignment (expression has type "Tuple[Union[TestCase, TestSuite], ...]", ' 'Incompatible types in assignment (expression has type "Tuple[Union[TestCase, TestSuite], ...]", '
@@ -631,7 +644,8 @@ def cd(path):
def is_ignored(line: str, test_folder_name: str) -> bool: def is_ignored(line: str, test_folder_name: str) -> bool:
for pattern in IGNORED_ERRORS['__common__'] + IGNORED_ERRORS.get(test_folder_name, []): for pattern in itertools.chain(IGNORED_ERRORS['__common__'],
IGNORED_ERRORS.get(test_folder_name, [])):
if isinstance(pattern, Pattern): if isinstance(pattern, Pattern):
if pattern.search(line): if pattern.search(line):
return True return True

29
setup.cfg Normal file
View File

@@ -0,0 +1,29 @@
[isort]
skip =
django-sources,
django-stubs,
test-data
include_trailing_comma = true
multi_line_output = 5
wrap_length = 120
[flake8]
exclude =
django-sources,
django-stubs,
test-data
max_line_length = 120
[tool:pytest]
testpaths = ./test-data
addopts =
--tb=native
--mypy-ini-file=./test-data/plugins.ini
-s
-v
[bdist_wheel]
universal = 1
[metadata]
license_file = LICENSE.txt

View File

@@ -21,7 +21,6 @@ with open('README.md', 'r') as f:
readme = f.read() readme = f.read()
dependencies = [ dependencies = [
'Django',
'mypy>=0.670', 'mypy>=0.670',
'typing-extensions' 'typing-extensions'
] ]
@@ -31,7 +30,7 @@ if sys.version_info[:2] < (3, 7):
setup( setup(
name="django-stubs", name="django-stubs",
version="0.8.0", version="0.8.2",
description='Django mypy stubs', description='Django mypy stubs',
long_description=readme, long_description=readme,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',

View File

@@ -110,9 +110,22 @@ class MyModel(ParentModel):
reveal_type(MyModel().id) # E: Revealed type is 'uuid.UUID*' reveal_type(MyModel().id) # E: Revealed type is 'uuid.UUID*'
[/CASE] [/CASE]
[CASE blank_for_charfield_is_the_same_as_null] [CASE blank_and_null_char_field_allows_none]
from django.db import models from django.db import models
class MyModel(models.Model): class MyModel(models.Model):
text = models.CharField(max_length=30, blank=True) nulltext=models.CharField(max_length=1, blank=True, null=True)
MyModel(text=None) 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] [/CASE]