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
pytest-mypy-plugins==1.1.0
pytest-mypy-plugins==1.2.0
psycopg2
flake8==3.7.9
flake8-pyi==19.3.0

View File

@@ -1,11 +1,24 @@
def supports_color() -> bool: ...
class Style:
def DEBUG(self, text: str) -> str: ...
def INFO(self, text: str) -> str: ...
def ERROR(self, text: str) -> str: ...
def SUCCESS(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 no_style() -> Style: ...

View File

@@ -207,7 +207,7 @@ class CallableChoiceIterator:
class ChoiceField(Field):
def __init__(
self,
choices: _FieldChoices = ...,
choices: Union[_FieldChoices, Callable[[], _FieldChoices]] = ...,
required: bool = ...,
widget: Optional[Union[Widget, Type[Widget]]] = ...,
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,
new_method_name: str, method_node: FuncDef) -> None:
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,
new_method_name,
args=arguments,

View File

@@ -4,7 +4,7 @@ from mypy.nodes import (
from mypy.plugin import ClassDefContext, DynamicClassDefContext
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:
@@ -65,9 +65,13 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
class_def_context = ClassDefContext(cls=new_manager_info.defn,
reason=ctx.call, api=semanal_api)
self_type = Instance(new_manager_info, [])
for name, sym in derived_queryset_info.names.items():
if isinstance(sym.node, FuncDef):
helpers.copy_method_to_another_class(class_def_context,
self_type,
new_method_name=name,
method_node=sym.node)
# we need to copy all methods in MRO before django.db.models.query.QuerySet
for class_mro_info in derived_queryset_info.mro:
if class_mro_info.fullname == fullnames.QUERYSET_CLASS_FULLNAME:
break
for name, sym in class_mro_info.names.items():
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.fields import DateField, DateTimeField
@@ -58,6 +58,12 @@ class ModelClassInitializer:
name=name,
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:
model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname)
if model_cls is None:
@@ -164,14 +170,12 @@ class AddManagers(ModelClassInitializer):
[Instance(self.model_classdef.info, [])])
bases.append(original_base)
current_module = self.api.modules[self.model_classdef.info.module_name]
custom_manager_info = helpers.add_new_class_for_module(current_module,
name=name, bases=bases)
new_manager_info = self.add_new_class_for_current_module(name, bases)
# 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,
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():
# replace self type with new class, if copying method
@@ -185,10 +189,10 @@ class AddManagers(ModelClassInitializer):
new_sym = sym.copy()
if isinstance(new_sym.node, Var):
new_var = Var(name, type=sym.type)
new_var.info = custom_manager_info
new_var._fullname = custom_manager_info.fullname + '.' + name
new_var.info = new_manager_info
new_var._fullname = new_manager_info.fullname + '.' + name
new_sym.node = new_var
custom_manager_info.names[name] = new_sym
new_manager_info.names[name] = new_sym
return custom_manager_type
@@ -268,15 +272,30 @@ class AddRelatedManagers(ModelClassInitializer):
if isinstance(relation, (ManyToOneRel, ManyToManyRel)):
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:
if not self.api.final_iteration:
raise exc
else:
continue
self.add_new_node_to_model_class(attname,
Instance(manager_info, [Instance(related_model_info, [])]))
continue
# create new RelatedManager subclass
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):

View File

@@ -178,7 +178,6 @@ IGNORED_ERRORS = {
],
'files': [
'Incompatible types in assignment (expression has type "IOBase", variable has type "File")',
'Argument 1 to "write" of "SpooledTemporaryFile"',
],
'filtered_relation': [
'has no attribute "name"',
@@ -231,13 +230,8 @@ IGNORED_ERRORS = {
],
'mail': [
'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 '
+ '(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': [
'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'),
],
'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]"',
],
'many_to_one': [
@@ -320,9 +314,6 @@ IGNORED_ERRORS = {
'model_enums': [
"'bool' is not a valid base class",
],
'multiple_database': [
'Unexpected attribute "extra_arg" for model "Book"'
],
'null_queries': [
"Cannot resolve keyword 'foo' into field"
],

View File

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

View File

@@ -648,3 +648,27 @@
abstract = True
class User(AbstractUser):
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):
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)