9 Commits

Author SHA1 Message Date
Maxim Kurnikov
304cb19de6 really drop universal 2019-04-01 01:43:14 +03:00
Maxim Kurnikov
c57f4f7152 only python3.6+ are supported 2019-04-01 01:08:46 +03:00
Maxim Kurnikov
8a826fee1e bump version 2019-04-01 00:54:36 +03:00
Maxim Kurnikov
37d85c2ca6 pin mypy version, django-stubs not yet supports mypyc 2019-04-01 00:54:15 +03:00
Seth Yastrov
71fb0432f3 52/model subtypes dont typecheck (#55)
* Fix problem where Model instancess are not considered subtypes of each other due to fallback_to_any = True. Fixes #52.

- Added a stub for __getstate__ to Model.
- Added a stub for clean() to Model.
- Correct arg type for sort_dependencies so they are covariant (Iterable rather than List).

Test ignores:
- Added some test ignores in cases where a model inherits from 2 different base models.
- Added some test ignores for cases that MyPy flags as errors due to variable redefinitions or imports that are incompatible types.

* Address review comment.
2019-03-28 23:13:02 +03:00
Maxim Kurnikov
9288c34648 fix @classproperty return type (#58) 2019-03-27 23:13:10 +03:00
Maxim Kurnikov
70050f28b9 drop --universal 2019-03-26 03:23:25 +03:00
Maxim Kurnikov
4338c17970 bump version 2019-03-25 14:22:17 +03:00
Maxim Kurnikov
91f789c38c fix SessionBase.exists() return type (#57) 2019-03-25 14:21:12 +03:00
12 changed files with 96 additions and 30 deletions

View File

@@ -1,8 +1,6 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, Optional, Union from typing import Any, Dict, Optional, Union
from django.db.models.base import Model
VALID_KEY_CHARS: Any VALID_KEY_CHARS: Any
class CreateError(Exception): ... class CreateError(Exception): ...
@@ -18,8 +16,8 @@ class SessionBase(Dict[str, Any]):
def set_test_cookie(self) -> None: ... def set_test_cookie(self) -> None: ...
def test_cookie_worked(self) -> bool: ... def test_cookie_worked(self) -> bool: ...
def delete_test_cookie(self) -> None: ... def delete_test_cookie(self) -> None: ...
def encode(self, session_dict: Dict[str, Model]) -> str: ... def encode(self, session_dict: Dict[str, Any]) -> str: ...
def decode(self, session_data: Union[bytes, str]) -> Dict[str, Model]: ... def decode(self, session_data: Union[bytes, str]) -> Dict[str, Any]: ...
def has_key(self, key: Any): ... def has_key(self, key: Any): ...
def keys(self): ... def keys(self): ...
def values(self): ... def values(self): ...
@@ -33,7 +31,7 @@ class SessionBase(Dict[str, Any]):
def get_expire_at_browser_close(self) -> bool: ... def get_expire_at_browser_close(self) -> bool: ...
def flush(self) -> None: ... def flush(self) -> None: ...
def cycle_key(self) -> None: ... def cycle_key(self) -> None: ...
def exists(self, session_key: str) -> None: ... def exists(self, session_key: str) -> bool: ...
def create(self) -> None: ... def create(self) -> None: ...
def save(self, must_create: bool = ...) -> None: ... def save(self, must_create: bool = ...) -> None: ...
def delete(self, session_key: Optional[Any] = ...) -> None: ... def delete(self, session_key: Optional[Any] = ...) -> None: ...

View File

@@ -1,5 +1,4 @@
from collections import OrderedDict from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union, Iterable
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union
from django.apps.config import AppConfig from django.apps.config import AppConfig
from django.db.models.base import Model from django.db.models.base import Model
@@ -33,5 +32,5 @@ def serialize(
) -> Optional[Union[bytes, str]]: ... ) -> Optional[Union[bytes, str]]: ...
def deserialize(format: str, stream_or_string: Any, **options: Any) -> Union[Iterator[Any], Deserializer]: ... def deserialize(format: str, stream_or_string: Any, **options: Any) -> Union[Iterator[Any], Deserializer]: ...
def sort_dependencies( def sort_dependencies(
app_list: Union[List[Tuple[AppConfig, None]], List[Tuple[str, List[Type[Model]]]]] app_list: Union[Iterable[Tuple[AppConfig, None]], Iterable[Tuple[str, Iterable[Type[Model]]]]]
) -> List[Type[Model]]: ... ) -> List[Type[Model]]: ...

View File

@@ -8,6 +8,7 @@ _Self = TypeVar("_Self", bound="Model")
class Model(metaclass=ModelBase): class Model(metaclass=ModelBase):
class DoesNotExist(Exception): ... class DoesNotExist(Exception): ...
class MultipleObjectsReturned(Exception): ...
class Meta: ... class Meta: ...
_meta: Any _meta: Any
_default_manager: Manager[Model] _default_manager: Manager[Model]
@@ -15,6 +16,7 @@ class Model(metaclass=ModelBase):
def __init__(self: _Self, *args, **kwargs) -> None: ... def __init__(self: _Self, *args, **kwargs) -> None: ...
def delete(self, using: Any = ..., keep_parents: bool = ...) -> Tuple[int, Dict[str, int]]: ... def delete(self, using: Any = ..., keep_parents: bool = ...) -> Tuple[int, Dict[str, int]]: ...
def full_clean(self, exclude: Optional[List[str]] = ..., validate_unique: bool = ...) -> None: ... def full_clean(self, exclude: Optional[List[str]] = ..., validate_unique: bool = ...) -> None: ...
def clean(self) -> None: ...
def clean_fields(self, exclude: List[str] = ...) -> None: ... def clean_fields(self, exclude: List[str] = ...) -> None: ...
def validate_unique(self, exclude: List[str] = ...) -> None: ... def validate_unique(self, exclude: List[str] = ...) -> None: ...
def save( def save(
@@ -34,6 +36,7 @@ class Model(metaclass=ModelBase):
): ... ): ...
def refresh_from_db(self: _Self, using: Optional[str] = ..., fields: Optional[List[str]] = ...) -> _Self: ... def refresh_from_db(self: _Self, using: Optional[str] = ..., fields: Optional[List[str]] = ...) -> _Self: ...
def get_deferred_fields(self) -> Set[str]: ... def get_deferred_fields(self) -> Set[str]: ...
def __getstate__(self) -> dict: ...
class ModelStateFieldsCacheDescriptor: ... class ModelStateFieldsCacheDescriptor: ...

View File

@@ -1,7 +1,5 @@
from typing import Any, Callable, Optional, Set, Tuple, Type, Union from typing import Any, Callable, Optional, Set, Tuple, Type, Union
from django.middleware.cache import CacheMiddleware
from django.test.testcases import LiveServerTestCase
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
class classonlymethod(classmethod): ... class classonlymethod(classmethod): ...
@@ -9,7 +7,7 @@ class classonlymethod(classmethod): ...
def method_decorator( def method_decorator(
decorator: Union[Callable, Set[Callable], Tuple[Callable, Callable]], name: str = ... decorator: Union[Callable, Set[Callable], Tuple[Callable, Callable]], name: str = ...
) -> Callable: ... ) -> Callable: ...
def decorator_from_middleware_with_args(middleware_class: Type[CacheMiddleware]) -> Callable: ... def decorator_from_middleware_with_args(middleware_class: Type[MiddlewareMixin]) -> Callable: ...
def decorator_from_middleware(middleware_class: Type[MiddlewareMixin]) -> Callable: ... def decorator_from_middleware(middleware_class: Type[MiddlewareMixin]) -> Callable: ...
def available_attrs(fn: Any): ... def available_attrs(fn: Any): ...
def make_middleware_decorator(middleware_class: Type[MiddlewareMixin]) -> Callable: ... def make_middleware_decorator(middleware_class: Type[MiddlewareMixin]) -> Callable: ...
@@ -17,5 +15,5 @@ def make_middleware_decorator(middleware_class: Type[MiddlewareMixin]) -> Callab
class classproperty: class classproperty:
fget: Optional[Callable] = ... fget: Optional[Callable] = ...
def __init__(self, method: Optional[Callable] = ...) -> None: ... def __init__(self, method: Optional[Callable] = ...) -> None: ...
def __get__(self, instance: Optional[LiveServerTestCase], cls: Type[LiveServerTestCase] = ...) -> str: ... def __get__(self, instance: Any, cls: Optional[type] = ...) -> Any: ...
def getter(self, method: Callable) -> classproperty: ... def getter(self, method: Callable) -> classproperty: ...

View File

@@ -5,7 +5,7 @@ import dataclasses
from mypy.nodes import ( from mypy.nodes import (
ARG_STAR, ARG_STAR2, MDEF, Argument, CallExpr, ClassDef, Expression, IndexExpr, Lvalue, MemberExpr, MypyFile, ARG_STAR, ARG_STAR2, MDEF, Argument, CallExpr, ClassDef, Expression, IndexExpr, Lvalue, MemberExpr, MypyFile,
NameExpr, StrExpr, SymbolTableNode, TypeInfo, Var, NameExpr, StrExpr, SymbolTableNode, TypeInfo, Var,
) ARG_POS)
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
@@ -255,10 +255,23 @@ def add_dummy_init_method(ctx: ClassDefContext) -> None:
type_annotation=any, initializer=None, kind=ARG_STAR2) type_annotation=any, initializer=None, kind=ARG_STAR2)
add_method(ctx, '__init__', [pos_arg, kw_arg], NoneTyp()) add_method(ctx, '__init__', [pos_arg, kw_arg], NoneTyp())
# mark as model class # mark as model class
ctx.cls.info.metadata.setdefault('django', {})['generated_init'] = True ctx.cls.info.metadata.setdefault('django', {})['generated_init'] = True
def add_get_set_attr_fallback_to_any(ctx: ClassDefContext):
any = AnyType(TypeOfAny.special_form)
name_arg = Argument(variable=Var('name', any),
type_annotation=any, initializer=None, kind=ARG_POS)
add_method(ctx, '__getattr__', [name_arg], any)
value_arg = Argument(variable=Var('value', any),
type_annotation=any, initializer=None, kind=ARG_POS)
add_method(ctx, '__setattr__', [name_arg, value_arg], any)
def process_model_class(ctx: ClassDefContext) -> None: def process_model_class(ctx: ClassDefContext) -> None:
initializers = [ initializers = [
InjectAnyAsBaseForNestedMeta, InjectAnyAsBaseForNestedMeta,
@@ -273,4 +286,4 @@ def process_model_class(ctx: ClassDefContext) -> None:
add_dummy_init_method(ctx) add_dummy_init_method(ctx)
# allow unspecified attributes for now # allow unspecified attributes for now
ctx.cls.info.fallback_to_any = True add_get_set_attr_fallback_to_any(ctx)

View File

@@ -2,7 +2,7 @@
try: try:
pip install wheel twine pip install wheel twine
python setup.py sdist bdist_wheel --universal python setup.py sdist bdist_wheel
twine upload dist/* twine upload dist/*
finally: finally:

View File

@@ -59,7 +59,12 @@ IGNORED_ERRORS = {
'Argument 1 to "loads" has incompatible type "Union[bytes, str, None]"; ' 'Argument 1 to "loads" has incompatible type "Union[bytes, str, None]"; '
+ 'expected "Union[str, bytes, bytearray]"', + 'expected "Union[str, bytes, bytearray]"',
'Incompatible types in assignment (expression has type "None", variable has type Module)', 'Incompatible types in assignment (expression has type "None", variable has type Module)',
'note:' 'note:',
# Suppress false-positive error due to mypy being overly strict with base class compatibility checks even though
# objects/_default_manager are redefined in the subclass to be compatible with the base class definition.
# Can be removed when mypy issue is fixed: https://github.com/python/mypy/issues/2619
re.compile(r'Definition of "(objects|_default_manager)" in base class "[A-Za-z0-9]+" is incompatible with '
r'definition in base class "[A-Za-z0-9]+"'),
], ],
'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")'
@@ -213,11 +218,12 @@ IGNORED_ERRORS = {
'Unexpected keyword argument "headline__startswith" for "in_bulk" of "QuerySet"', 'Unexpected keyword argument "headline__startswith" for "in_bulk" of "QuerySet"',
], ],
'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]")',
'Incompatible type for "parent" of "Child" (got "Child", expected "Union[Parent, Combinable]")'
], ],
'model_meta': [ 'model_meta': [
'"object" has no attribute "items"', '"object" has no attribute "items"',
'"Field" has no attribute "many_to_many"' '"Field" has no attribute "many_to_many"',
], ],
'model_forms': [ 'model_forms': [
'Argument "instance" to "InvalidModelForm" has incompatible type "Type[Category]"; expected "Optional[Model]"', 'Argument "instance" to "InvalidModelForm" has incompatible type "Type[Category]"; expected "Optional[Model]"',
@@ -227,8 +233,14 @@ IGNORED_ERRORS = {
'Incompatible types in assignment (expression has type "Type[Person]", variable has type', 'Incompatible types in assignment (expression has type "Type[Person]", variable has type',
'Unexpected keyword argument "name" for "Person"', 'Unexpected keyword argument "name" for "Person"',
'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]", ' re.compile(
+ 'base class "ImageFieldTestMixin" defined the type as "Type[PersonWithHeightAndWidth]")', r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class "IntegerFieldTests"'
r' defined the type as "Type\[IntegerModel\]"\)'),
re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class'
r' "ImageFieldTestMixin" defined the type as "Type\[PersonWithHeightAndWidth\]"\)'),
'Incompatible import of "Person"',
'Incompatible types in assignment (expression has type "FloatModel", variable has type '
'"Union[float, int, str, Combinable]")',
], ],
'model_formsets': [ 'model_formsets': [
'Incompatible types in string interpolation (expression has type "object", ' 'Incompatible types in string interpolation (expression has type "object", '
@@ -261,7 +273,7 @@ IGNORED_ERRORS = {
'Argument 1 to "RunPython" has incompatible type "str"; expected "Callable[..., Any]"', 'Argument 1 to "RunPython" has incompatible type "str"; expected "Callable[..., Any]"',
'FakeLoader', 'FakeLoader',
'Argument 1 to "append" of "list" has incompatible type "AddIndex"; expected "CreateModel"', 'Argument 1 to "append" of "list" has incompatible type "AddIndex"; expected "CreateModel"',
'Unsupported operand types for - ("Set[Any]" and "None")' 'Unsupported operand types for - ("Set[Any]" and "None")',
], ],
'middleware_exceptions': [ 'middleware_exceptions': [
'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any]"; expected "str"' 'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any]"; expected "str"'
@@ -282,7 +294,11 @@ IGNORED_ERRORS = {
+ 'expected "Optional[Type[JSONEncoder]]"', + 'expected "Optional[Type[JSONEncoder]]"',
'for model "CITestModel"', 'for model "CITestModel"',
'Incompatible type for "field" of "IntegerArrayModel" (got "None", ' 'Incompatible type for "field" of "IntegerArrayModel" (got "None", '
+ 'expected "Union[Sequence[int], Combinable]")' + 'expected "Union[Sequence[int], Combinable]")',
re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class "UnaccentTest" '
r'defined the type as "Type\[CharFieldModel\]"\)'),
'Incompatible types in assignment (expression has type "Type[TextFieldModel]", base class "TrigramTest" '
'defined the type as "Type[CharFieldModel]")',
], ],
'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"')
@@ -291,6 +307,12 @@ IGNORED_ERRORS = {
'Incompatible types in assignment (expression has type "None", variable has type "str")', 'Incompatible types in assignment (expression has type "None", variable has type "str")',
'Invalid index type "Optional[str]" for "Dict[str, int]"; expected type "str"', 'Invalid index type "Optional[str]" for "Dict[str, int]"; expected type "str"',
'No overload variant of "values_list" of "QuerySet" matches argument types "str", "bool", "bool"', 'No overload variant of "values_list" of "QuerySet" matches argument types "str", "bool", "bool"',
'Unsupported operand types for & ("QuerySet[Author, Author]" and "QuerySet[Tag, Tag]")',
'Unsupported operand types for | ("QuerySet[Author, Author]" and "QuerySet[Tag, Tag]")',
'Incompatible types in assignment (expression has type "ObjectB", variable has type "ObjectA")',
'Incompatible types in assignment (expression has type "ObjectC", variable has type "ObjectA")',
'Incompatible type for "objectb" of "ObjectC" (got "ObjectA", expected'
' "Union[ObjectB, Combinable, None, None]")',
], ],
'requests': [ 'requests': [
'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")' 'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")'
@@ -303,6 +325,9 @@ IGNORED_ERRORS = {
'"None" has no attribute "__iter__"', '"None" has no attribute "__iter__"',
'has no attribute "read_by"' 'has no attribute "read_by"'
], ],
'proxy_model_inheritance': [
'Incompatible import of "ProxyModel"'
],
'signals': [ 'signals': [
'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any, Optional[Any], Any]"; ' 'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any, Optional[Any], Any]"; '
+ 'expected "Tuple[Any, Any, Any]"' + 'expected "Tuple[Any, Any, Any]"'
@@ -387,7 +412,8 @@ IGNORED_ERRORS = {
'Incompatible types in assignment (expression has type "None", variable has type "int")', 'Incompatible types in assignment (expression has type "None", variable has type "int")',
], ],
'select_related_onetoone': [ 'select_related_onetoone': [
'"None" has no attribute' '"None" has no attribute',
'Incompatible types in assignment (expression has type "Parent2", variable has type "Parent1")',
], ],
'servers': [ 'servers': [
re.compile('Argument [0-9] to "WSGIRequestHandler"') re.compile('Argument [0-9] to "WSGIRequestHandler"')

View File

@@ -22,8 +22,5 @@ addopts =
-s -s
-v -v
[bdist_wheel]
universal = 1
[metadata] [metadata]
license_file = LICENSE.txt license_file = LICENSE.txt

View File

@@ -21,7 +21,7 @@ with open('README.md', 'r') as f:
readme = f.read() readme = f.read()
dependencies = [ dependencies = [
'mypy>=0.670', 'mypy>=0.670,<0.700',
'typing-extensions' 'typing-extensions'
] ]
if sys.version_info[:2] < (3, 7): if sys.version_info[:2] < (3, 7):
@@ -30,7 +30,7 @@ if sys.version_info[:2] < (3, 7):
setup( setup(
name="django-stubs", name="django-stubs",
version="0.10.0", version="0.11.0",
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',
@@ -39,7 +39,7 @@ setup(
author="Maksim Kurnikov", author="Maksim Kurnikov",
author_email="maxim.kurnikov@gmail.com", author_email="maxim.kurnikov@gmail.com",
py_modules=[], py_modules=[],
python_requires='>=3', python_requires='>=3.6',
install_requires=dependencies, install_requires=dependencies,
packages=['django-stubs', *find_packages()], packages=['django-stubs', *find_packages()],
package_data={'django-stubs': find_stub_files('django-stubs')}, package_data={'django-stubs': find_stub_files('django-stubs')},

View File

@@ -21,7 +21,7 @@ from django.db import models
class MyModel(models.Model): class MyModel(models.Model):
authors = models.Manager[MyModel]() authors = models.Manager[MyModel]()
reveal_type(MyModel.authors) # E: Revealed type is 'django.db.models.manager.Manager[main.MyModel]' reveal_type(MyModel.authors) # E: Revealed type is 'django.db.models.manager.Manager[main.MyModel]'
reveal_type(MyModel.objects) # E: Revealed type is 'Any' MyModel.objects # E: "Type[MyModel]" has no attribute "objects"
[out] [out]
[CASE test_model_objects_attribute_present_in_case_of_model_cls_passed_as_generic_parameter] [CASE test_model_objects_attribute_present_in_case_of_model_cls_passed_as_generic_parameter]

View File

@@ -0,0 +1,28 @@
[CASE test_model_subtype_relationship_and_getting_and_setting_attributes]
from django.db import models
class A(models.Model):
pass
class B(models.Model):
b_attr = 1
pass
class C(A):
pass
def service(a: A) -> int:
pass
a_instance = A()
b_instance = B()
reveal_type(b_instance.b_attr) # E: Revealed type is 'builtins.int'
reveal_type(b_instance.non_existent_attribute) # E: Revealed type is 'Any'
b_instance.non_existent_attribute = 2
service(b_instance) # E: Argument 1 to "service" has incompatible type "B"; expected "A"
c_instance = C()
service(c_instance)

View File

@@ -27,12 +27,16 @@ class Parent1(models.Model):
class Parent2(models.Model): class Parent2(models.Model):
id2 = models.AutoField(primary_key=True) id2 = models.AutoField(primary_key=True)
name2 = models.CharField(max_length=50) name2 = models.CharField(max_length=50)
# TODO: Remove the 2 expected errors on the next line once mypy issue https://github.com/python/mypy/issues/2619 is resolved:
class Child1(Parent1, Parent2): class Child1(Parent1, Parent2):
value = models.IntegerField() value = models.IntegerField()
class Child4(Child1): class Child4(Child1):
value4 = models.IntegerField() value4 = models.IntegerField()
Child4.objects.create(name1='n1', name2='n2', value=1, value4=4) Child4.objects.create(name1='n1', name2='n2', value=1, value4=4)
[out] [out]
main:10: error: Definition of "objects" in base class "Parent1" is incompatible with definition in base class "Parent2"
main:10: error: Definition of "_default_manager" in base class "Parent1" is incompatible with definition in base class "Parent2"
[CASE optional_primary_key_for_create_is_error] [CASE optional_primary_key_for_create_is_error]
from django.db import models from django.db import models