11 Commits
1.3.2 ... 1.4.0

Author SHA1 Message Date
Maksim Kurnikov
38135f2d1f Merge pull request #281 from mkurnikov/update-deps
Update dev deps, mypy to 0.760
2019-12-18 00:13:11 +03:00
Maxim Kurnikov
998b659749 bump to 1.4.0 2019-12-18 00:03:29 +03:00
Maxim Kurnikov
72f69e1c5e remove unused ignores 2019-12-18 00:02:55 +03:00
Maxim Kurnikov
d666ecd36f update dev deps, mypy to 0.760 2019-12-17 23:50:50 +03:00
Maksim Kurnikov
c1af26c027 handle return value of anal_type properly (#280) 2019-12-17 23:36:44 +03:00
Maxim Kurnikov
3c3dfcbc9f bump to 1.3.3 2019-12-17 19:20:27 +03:00
Maksim Kurnikov
1196336e3b Perform anal_type for arguments and return type when copying methods to another class (#279)
* Found the reproducible test case

* fix import resolution for method copy

* remove irrelevant parts from test

* fix mypy errors

Co-authored-by: Boger <kotvberloge@gmail.com>
2019-12-17 19:19:31 +03:00
Maksim Kurnikov
665f4d8ea1 Make related manager inherit from objects of related model (#278)
* related manager inherits from objects of related model

* fix test typechecking

* lint
2019-12-17 19:06:27 +03:00
Dima Boger
b3ed9e4827 Add inheritance QuerySet support for from_queryset (#275)
* Add testcase for queryset inheritance

* Add PoC

* Add condition for stop to looping over mro

* Change harcoded queryset class name to constant from fullnames
2019-12-16 20:16:41 +03:00
Marti Raudsepp
fb1593630a Expand stubs for django.core.management.color (#276)
Many attributes were previously missing; 'DEBUG' and 'INFO' attributes
were never supported by Django.
2019-12-16 20:16:21 +03:00
JR Heard
031d42a75d update ChoiceField's choices kwarg's annotation (#273)
per the [docs](https://docs.djangoproject.com/en/3.0/ref/forms/fields/#choicefield), `choices` is:

> Either an iterable of 2-tuples to use as choices for this field, or a callable that returns such an iterable.
2019-12-14 09:30:50 +03:00
10 changed files with 153 additions and 37 deletions

View File

@@ -1,5 +1,5 @@
black black
pytest-mypy-plugins==1.1.0 pytest-mypy-plugins==1.2.0
psycopg2 psycopg2
flake8==3.7.9 flake8==3.7.9
flake8-pyi==19.3.0 flake8-pyi==19.3.0

View File

@@ -1,11 +1,24 @@
def supports_color() -> bool: ... def supports_color() -> bool: ...
class Style: class Style:
def DEBUG(self, text: str) -> str: ... def ERROR(self, text: str) -> str: ...
def INFO(self, text: str) -> str: ...
def SUCCESS(self, text: str) -> str: ... def SUCCESS(self, text: str) -> str: ...
def WARNING(self, text: str) -> str: ... def WARNING(self, text: str) -> str: ...
def ERROR(self, text: str) -> str: ... def NOTICE(self, text: str) -> str: ...
def SQL_FIELD(self, text: str) -> str: ...
def SQL_COLTYPE(self, text: str) -> str: ...
def SQL_KEYWORD(self, text: str) -> str: ...
def SQL_TABLE(self, text: str) -> str: ...
def HTTP_INFO(self, text: str) -> str: ...
def HTTP_SUCCESS(self, text: str) -> str: ...
def HTTP_REDIRECT(self, text: str) -> str: ...
def HTTP_NOT_MODIFIED(self, text: str) -> str: ...
def HTTP_BAD_REQUEST(self, text: str) -> str: ...
def HTTP_NOT_FOUND(self, text: str) -> str: ...
def HTTP_SERVER_ERROR(self, text: str) -> str: ...
def MIGRATE_HEADING(self, text: str) -> str: ...
def MIGRATE_LABEL(self, text: str) -> str: ...
def ERROR_OUTPUT(self, text: str) -> str: ...
def make_style(config_string: str = ...) -> Style: ... def make_style(config_string: str = ...) -> Style: ...
def no_style() -> Style: ... def no_style() -> Style: ...

View File

@@ -207,7 +207,7 @@ class CallableChoiceIterator:
class ChoiceField(Field): class ChoiceField(Field):
def __init__( def __init__(
self, self,
choices: _FieldChoices = ..., choices: Union[_FieldChoices, Callable[[], _FieldChoices]] = ...,
required: bool = ..., required: bool = ...,
widget: Optional[Union[Widget, Type[Widget]]] = ..., widget: Optional[Union[Widget, Type[Widget]]] = ...,
label: Optional[Any] = ..., label: Optional[Any] = ...,

View File

@@ -327,6 +327,19 @@ def _prepare_new_method_arguments(node: FuncDef) -> Tuple[List[Argument], MypyTy
def copy_method_to_another_class(ctx: ClassDefContext, self_type: Instance, def copy_method_to_another_class(ctx: ClassDefContext, self_type: Instance,
new_method_name: str, method_node: FuncDef) -> None: new_method_name: str, method_node: FuncDef) -> None:
arguments, return_type = _prepare_new_method_arguments(method_node) arguments, return_type = _prepare_new_method_arguments(method_node)
semanal_api = get_semanal_api(ctx)
for argument in arguments:
if argument.type_annotation is not None:
argument.type_annotation = semanal_api.anal_type(argument.type_annotation,
allow_placeholder=True)
if return_type is not None:
ret = semanal_api.anal_type(return_type,
allow_placeholder=True)
assert ret is not None
return_type = ret
add_method(ctx, add_method(ctx,
new_method_name, new_method_name,
args=arguments, args=arguments,

View File

@@ -4,7 +4,7 @@ from mypy.nodes import (
from mypy.plugin import ClassDefContext, DynamicClassDefContext from mypy.plugin import ClassDefContext, DynamicClassDefContext
from mypy.types import AnyType, Instance, TypeOfAny from mypy.types import AnyType, Instance, TypeOfAny
from mypy_django_plugin.lib import helpers from mypy_django_plugin.lib import fullnames, helpers
def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefContext) -> None: def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefContext) -> None:
@@ -65,9 +65,13 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
class_def_context = ClassDefContext(cls=new_manager_info.defn, class_def_context = ClassDefContext(cls=new_manager_info.defn,
reason=ctx.call, api=semanal_api) reason=ctx.call, api=semanal_api)
self_type = Instance(new_manager_info, []) self_type = Instance(new_manager_info, [])
for name, sym in derived_queryset_info.names.items(): # we need to copy all methods in MRO before django.db.models.query.QuerySet
if isinstance(sym.node, FuncDef): for class_mro_info in derived_queryset_info.mro:
helpers.copy_method_to_another_class(class_def_context, if class_mro_info.fullname == fullnames.QUERYSET_CLASS_FULLNAME:
self_type, break
new_method_name=name, for name, sym in class_mro_info.names.items():
method_node=sym.node) if isinstance(sym.node, FuncDef):
helpers.copy_method_to_another_class(class_def_context,
self_type,
new_method_name=name,
method_node=sym.node)

View File

@@ -1,4 +1,4 @@
from typing import Dict, Optional, Type, cast from typing import Dict, List, Optional, Type, cast
from django.db.models.base import Model from django.db.models.base import Model
from django.db.models.fields import DateField, DateTimeField from django.db.models.fields import DateField, DateTimeField
@@ -58,6 +58,12 @@ class ModelClassInitializer:
name=name, name=name,
sym_type=typ) sym_type=typ)
def add_new_class_for_current_module(self, name: str, bases: List[Instance]) -> TypeInfo:
current_module = self.api.modules[self.model_classdef.info.module_name]
new_class_info = helpers.add_new_class_for_module(current_module,
name=name, bases=bases)
return new_class_info
def run(self) -> None: def run(self) -> None:
model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname) model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname)
if model_cls is None: if model_cls is None:
@@ -164,14 +170,12 @@ class AddManagers(ModelClassInitializer):
[Instance(self.model_classdef.info, [])]) [Instance(self.model_classdef.info, [])])
bases.append(original_base) bases.append(original_base)
current_module = self.api.modules[self.model_classdef.info.module_name] new_manager_info = self.add_new_class_for_current_module(name, bases)
custom_manager_info = helpers.add_new_class_for_module(current_module,
name=name, bases=bases)
# copy fields to a new manager # copy fields to a new manager
new_cls_def_context = ClassDefContext(cls=custom_manager_info.defn, new_cls_def_context = ClassDefContext(cls=new_manager_info.defn,
reason=self.ctx.reason, reason=self.ctx.reason,
api=self.api) api=self.api)
custom_manager_type = Instance(custom_manager_info, [Instance(self.model_classdef.info, [])]) custom_manager_type = Instance(new_manager_info, [Instance(self.model_classdef.info, [])])
for name, sym in base_manager_info.names.items(): for name, sym in base_manager_info.names.items():
# replace self type with new class, if copying method # replace self type with new class, if copying method
@@ -185,10 +189,10 @@ class AddManagers(ModelClassInitializer):
new_sym = sym.copy() new_sym = sym.copy()
if isinstance(new_sym.node, Var): if isinstance(new_sym.node, Var):
new_var = Var(name, type=sym.type) new_var = Var(name, type=sym.type)
new_var.info = custom_manager_info new_var.info = new_manager_info
new_var._fullname = custom_manager_info.fullname + '.' + name new_var._fullname = new_manager_info.fullname + '.' + name
new_sym.node = new_var new_sym.node = new_var
custom_manager_info.names[name] = new_sym new_manager_info.names[name] = new_sym
return custom_manager_type return custom_manager_type
@@ -268,15 +272,30 @@ class AddRelatedManagers(ModelClassInitializer):
if isinstance(relation, (ManyToOneRel, ManyToManyRel)): if isinstance(relation, (ManyToOneRel, ManyToManyRel)):
try: try:
manager_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.RELATED_MANAGER_CLASS) related_manager_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.RELATED_MANAGER_CLASS) # noqa: E501
if 'objects' not in related_model_info.names:
raise helpers.IncompleteDefnException()
except helpers.IncompleteDefnException as exc: except helpers.IncompleteDefnException as exc:
if not self.api.final_iteration: if not self.api.final_iteration:
raise exc raise exc
else: else:
continue continue
self.add_new_node_to_model_class(attname,
Instance(manager_info, [Instance(related_model_info, [])])) # create new RelatedManager subclass
continue parametrized_related_manager_type = Instance(related_manager_info,
[Instance(related_model_info, [])])
default_manager_type = related_model_info.names['objects'].type
if (default_manager_type is None
or not isinstance(default_manager_type, Instance)
or default_manager_type.type.fullname == fullnames.MANAGER_CLASS_FULLNAME):
self.add_new_node_to_model_class(attname, parametrized_related_manager_type)
continue
name = related_model_cls.__name__ + '_' + 'RelatedManager'
bases = [parametrized_related_manager_type, default_manager_type]
new_related_manager_info = self.add_new_class_for_current_module(name, bases)
self.add_new_node_to_model_class(attname, Instance(new_related_manager_info, []))
class AddExtraFieldMethods(ModelClassInitializer): class AddExtraFieldMethods(ModelClassInitializer):

View File

@@ -178,7 +178,6 @@ IGNORED_ERRORS = {
], ],
'files': [ 'files': [
'Incompatible types in assignment (expression has type "IOBase", variable has type "File")', 'Incompatible types in assignment (expression has type "IOBase", variable has type "File")',
'Argument 1 to "write" of "SpooledTemporaryFile"',
], ],
'filtered_relation': [ 'filtered_relation': [
'has no attribute "name"', 'has no attribute "name"',
@@ -231,13 +230,8 @@ IGNORED_ERRORS = {
], ],
'mail': [ 'mail': [
'List item 1 has incompatible type "None"; expected "str"', 'List item 1 has incompatible type "None"; expected "str"',
'Argument 1 to "push" of "SMTPChannel" has incompatible type "str"; expected "bytes"',
'Value of type "Union[List[Message], str, bytes, None]" is not indexable',
'Incompatible types in assignment ' 'Incompatible types in assignment '
+ '(expression has type "bool", variable has type "Union[SMTP_SSL, SMTP, None]")', + '(expression has type "bool", variable has type "Union[SMTP_SSL, SMTP, None]")',
re.compile(
r'Item "(int|str)" of "Union\[Message, str, int, Any\]" has no attribute "(get_content_type|get_filename)"'
)
], ],
'messages_tests': [ 'messages_tests': [
'List item 0 has incompatible type "Dict[str, Message]"; expected "Message"', 'List item 0 has incompatible type "Dict[str, Message]"; expected "Message"',
@@ -248,7 +242,7 @@ IGNORED_ERRORS = {
re.compile(r'"(HttpRequest|WSGIRequest)" has no attribute'), re.compile(r'"(HttpRequest|WSGIRequest)" has no attribute'),
], ],
'many_to_many': [ 'many_to_many': [
'(expression has type "List[Article]", variable has type "RelatedManager[Article]"', '(expression has type "List[Article]", variable has type "Article_RelatedManager2',
'"add" of "RelatedManager" has incompatible type "Article"; expected "Union[Publication, int]"', '"add" of "RelatedManager" has incompatible type "Article"; expected "Union[Publication, int]"',
], ],
'many_to_one': [ 'many_to_one': [
@@ -320,9 +314,6 @@ IGNORED_ERRORS = {
'model_enums': [ 'model_enums': [
"'bool' is not a valid base class", "'bool' is not a valid base class",
], ],
'multiple_database': [
'Unexpected attribute "extra_arg" for model "Book"'
],
'null_queries': [ 'null_queries': [
"Cannot resolve keyword 'foo' into field" "Cannot resolve keyword 'foo' into field"
], ],

View File

@@ -21,14 +21,14 @@ with open('README.md', 'r') as f:
readme = f.read() readme = f.read()
dependencies = [ dependencies = [
'mypy>=0.750,<0.760', 'mypy>=0.760,<0.770',
'typing-extensions', 'typing-extensions',
'django', 'django',
] ]
setup( setup(
name="django-stubs", name="django-stubs",
version="1.3.2", version="1.4.0",
description='Mypy stubs for Django', description='Mypy stubs for Django',
long_description=readme, long_description=readme,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',

View File

@@ -648,3 +648,27 @@
abstract = True abstract = True
class User(AbstractUser): class User(AbstractUser):
pass pass
- case: related_manager_is_a_subclass_of_default_manager
main: |
from myapp.models import User
reveal_type(User().orders) # N: Revealed type is 'myapp.models.Order_RelatedManager'
reveal_type(User().orders.get()) # N: Revealed type is 'myapp.models.Order*'
reveal_type(User().orders.manager_method()) # N: Revealed type is 'builtins.int'
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
pass
class OrderManager(models.Manager):
def manager_method(self) -> int:
pass
class Order(models.Model):
objects = OrderManager()
user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='orders')

View File

@@ -94,3 +94,55 @@
class MyModel(models.Model): class MyModel(models.Model):
objects = NewManager() objects = NewManager()
- case: from_queryset_with_class_inheritance
main: |
from myapp.models import MyModel
reveal_type(MyModel().objects) # N: Revealed type is 'myapp.models.MyModel_NewManager[myapp.models.MyModel]'
reveal_type(MyModel().objects.get()) # N: Revealed type is 'myapp.models.MyModel*'
reveal_type(MyModel().objects.queryset_method()) # N: Revealed type is 'builtins.str'
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
from django.db.models.manager import BaseManager
class BaseQuerySet(models.QuerySet):
def queryset_method(self) -> str:
return 'hello'
class ModelQuerySet(BaseQuerySet):
pass
NewManager = BaseManager.from_queryset(ModelQuerySet)
class MyModel(models.Model):
objects = NewManager()
- case: from_queryset_with_manager_in_another_directory_and_imports
main: |
from myapp.models import MyModel
reveal_type(MyModel().objects) # N: Revealed type is 'myapp.models.MyModel_NewManager[myapp.models.MyModel]'
reveal_type(MyModel().objects.get()) # N: Revealed type is 'myapp.models.MyModel*'
reveal_type(MyModel().objects.queryset_method) # N: Revealed type is 'def (param: Union[builtins.str, None] =) -> Union[builtins.str, None]'
reveal_type(MyModel().objects.queryset_method('str')) # N: Revealed type is 'Union[builtins.str, None]'
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
from myapp.managers import NewManager
class MyModel(models.Model):
objects = NewManager()
- path: myapp/managers.py
content: |
from typing import Optional
from django.db import models
class ModelQuerySet(models.QuerySet):
def queryset_method(self, param: Optional[str] = None) -> Optional[str]:
return param
NewManager = models.Manager.from_queryset(ModelQuerySet)