12 Commits

Author SHA1 Message Date
Nikita Sobolev
b846c0f2ed Update README.md 2020-02-06 10:32:47 +03:00
Nikita Sobolev
7af2adb665 Update README.md 2020-02-05 00:03:40 +03:00
Nikita Sobolev
0052a83968 Update README.md 2020-02-04 13:52:06 +03:00
Vince Salvino
5b3088a17a Fix cache arg type for django.utils.cache.learn_cache_key() (#316) 2020-02-01 22:03:46 +03:00
Konstantin Alekseev
f89a0fbbaa Cleanup checks framework (#310) 2020-01-30 14:38:23 +03:00
Tatsh
438f8b5829 Add OutputWrapper write() signature (#301)
* Add OutputWrapper write() signature

* Add #type: ignore[override] to OutputWrapper write() signature
2020-01-28 01:34:13 +03:00
Joseph Kahn
836d5acd8f Allow error handlers to be strings (#307)
Based on the docs on this page: https://docs.djangoproject.com/en/3.0/ref/urls/
2020-01-28 01:33:49 +03:00
Cesar Canassa
bfae51e64c Added manager _db (#296) 2020-01-14 17:05:29 +03:00
Daniel Hillier
e3801918e3 Fix incorrect type for django.core.files.File.file (#293) (#294) 2020-01-11 14:27:46 +03:00
Marti Raudsepp
6f296b0a91 Implement @cached_property attribute inference (#292) 2020-01-10 13:57:09 +03:00
Nikita Sobolev
7ba578f6b2 Refactors validators types (#284)
* Update __init__.pyi

* It should not fail right now 🤔

* Refactor validators a bit

* Fixes CI

* Update __init__.pyi
2019-12-21 15:44:26 +03:00
Maksim Kurnikov
cb123de105 BaseManager.from_queryset(): properly resolve methods for QuerySet defined in another file (#282)
* BaseManager.from_queryset() from another file

* only anal_type per argument

* add resolve for return_type

* fix mypy errors

* remove leftover comment
2019-12-18 20:01:20 +03:00
15 changed files with 234 additions and 111 deletions

101
README.md
View File

@@ -1,14 +1,12 @@
<img src="http://mypy-lang.org/static/mypy_light.svg" alt="mypy logo" width="300px"/>
# pep484 stubs for Django framework
# Typesafe Django Framework
[![Build Status](https://travis-ci.com/typeddjango/django-stubs.svg?branch=master)](https://travis-ci.com/typeddjango/django-stubs)
[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
[![Gitter](https://badges.gitter.im/mypy-django/Lobby.svg)](https://gitter.im/mypy-django/Lobby)
This package contains type stubs and mypy plugin to provide more precise static types and type inference for Django framework. Django uses some Python "magic" that makes having precise types for some code patterns problematic. This is why we need to accompany the stubs with mypy plugins. The final goal is to be able to get precise types for most common patterns.
Could be run on earlier versions of Django, but expect some missing imports warnings.
This package contains [type stubs](https://www.python.org/dev/peps/pep-0561/) and a custom mypy plugin to provide more precise static types and type inference for Django framework. Django uses some Python "magic" that makes having precise types for some code patterns problematic. This is why we need this project. The final goal is to be able to get precise types for most common patterns.
## Installation
@@ -17,8 +15,35 @@ Could be run on earlier versions of Django, but expect some missing imports warn
pip install django-stubs
```
See [Configutation](#configuration) section to get started.
## Mypy compatibility
## Configuration
To make `mypy` happy, you will need to add:
```ini
[mypy]
plugins =
mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = "myproject.settings"
```
in your `mypy.ini` or `setup.cfg` [file](https://mypy.readthedocs.io/en/latest/config_file.html).
Two things happeining here:
1. We need to explicitly list our plugin to be loaded by `mypy`
2. Our plugin also requires `django` settings module (what you put into `DJANGO_SETTINGS_MODULE` variable) to be specified
This fully working [typed boilerplate](https://github.com/wemake-services/wemake-django-template) can serve you as an example.
## Version compatibility
We rely on different `django` and `mypy` versions:
| django-stubs | mypy version | django version | python version
| ------------ | ---- | ---- | ---- |
@@ -28,58 +53,66 @@ pip install django-stubs
| 0.12.x | old semantic analyzer (<0.711), dmypy support | 2.1.x | ^3.6
## Configuration
## FAQ
To make mypy aware of the plugin, you need to add
> Is this an official Django project?
```ini
[mypy]
plugins =
mypy_django_plugin.main
```
No, it is not. We are indendepent from Django at the moment.
There's a [proposal](https://github.com/django/deps/pull/65) to merge our project into the Django itself.
You show your support by linking the PR.
in your `mypy.ini` or `setup.cfg` [file](https://mypy.readthedocs.io/en/latest/config_file.html).
> Is it safe to use this in production?
Plugin also requires Django settings module (what you put into `DJANGO_SETTINGS_MODULE` variable) to be specified.
Yes, it is! This project does not affect your runtime at all.
It only affects `mypy` type checking process.
```ini
[mypy]
strict_optional = True
But, it does not make sense to use this project without `mypy`.
# This one is new:
[mypy.plugins.django-stubs]
django_settings_module = mysettings
```
Where `mysettings` is a value of `DJANGO_SETTINGS_MODULE` (with or without quotes)
> mypy crashes when I run it with this plugin installed
Current implementation uses Django runtime to extract models information, so it will crash, if your installed apps `models.py` is not correct. For this same reason, you cannot use `reveal_type` inside global scope of any Python file that will be executed for `django.setup()`.
In other words, if your `manage.py runserver` crashes, mypy will crash too.
You can also run `mypy` with [`--tb`](https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-show-traceback)
option to get extra information about the error.
This fully working [typed boilerplate](https://github.com/wemake-services/wemake-django-template) can serve you as an example.
> I cannot use QuerySet or Manager with type annotations
You can get a `TypeError: 'type' object is not subscriptable`
when you will try to use `QuerySet[MyModel]` or `Manager[MyModel]`.
## Notes
This happens because Django classes do not support [`__class_getitem__`](https://www.python.org/dev/peps/pep-0560/#class-getitem) magic method.
Type implementation monkey-patches Django to add `__class_getitem__` to the `Manager` class.
If you would use Python3.7 and do that too in your code, you can make things like
There are several things you can use strings instead: `'QuerySet[MyModel]'` and `'Manager[MyModel'`, this way it will work as a type for `mypy` and as a regular `str` in runtime.
Currently we [are working](https://github.com/django/django/pull/12405) on providing `__class_getitem__` to the classes where we need them.
> How can I use HttpRequest with custom user model?
You can subclass standard request like so:
```python
class MyUserManager(models.Manager['MyUser']):
pass
from django.http import HttpRequest
from my_user_app.models import MyUser
class MyUser(models.Model):
objects = MyUserManager()
class MyRequest(HttpRequest):
user: MyUser
```
work, which should make a error messages a bit better.
And then use `MyRequest` instead of standard `HttpRequest` inside your project.
## Related projects
- [`awesome-python-typing`](https://github.com/typeddjango/awesome-python-typing) - Awesome list of all typing-related things in Python.
- [`djangorestframework-stubs`](https://github.com/typeddjango/djangorestframework-stubs) - Stubs for Django REST Framework.
- [`pytest-mypy-plugins`](https://github.com/typeddjango/pytest-mypy-plugins) - `pytest` plugin that we use for testing `mypy` stubs and plugins.
- [`wemake-django-template`](https://github.com/wemake-services/wemake-django-template) - Create new typed Django projects in seconds.
Otherwise, custom type will be created in mypy, named `MyUser__MyUserManager`, which will rewrite base manager as `models.Manager[User]` to make methods like `get_queryset()` and others return properly typed `QuerySet`.
## To get help
We have Gitter here: <https://gitter.im/mypy-django/Lobby>
If you think you have more generic typing issue, please refer to https://github.com/python/mypy and their Gitter.
If you think you have more generic typing issue, please refer to <https://github.com/python/mypy> and their Gitter.

View File

@@ -5,10 +5,10 @@ from django.http.response import HttpResponse, HttpResponseBase
from django.urls import URLResolver, URLPattern
handler400: Callable[..., HttpResponse] = ...
handler403: Callable[..., HttpResponse] = ...
handler404: Callable[..., HttpResponse] = ...
handler500: Callable[..., HttpResponse] = ...
handler400: Union[str, Callable[..., HttpResponse]] = ...
handler403: Union[str, Callable[..., HttpResponse]] = ...
handler404: Union[str, Callable[..., HttpResponse]] = ...
handler500: Union[str, Callable[..., HttpResponse]] = ...
IncludedURLConf = Tuple[List[URLResolver], Optional[str], Optional[str]]

View File

@@ -1,5 +1,17 @@
from .messages import Warning as Warning, Info as Info, Debug as Debug, Error as Error, Critical as Critical
from .messages import (
CheckMessage as CheckMessage,
Debug as Debug,
Info as Info,
Warning as Warning,
Error as Error,
Critical as Critical,
DEBUG as DEBUG,
INFO as INFO,
WARNING as WARNING,
ERROR as ERROR,
CRITICAL as CRITICAL,
)
from .registry import run_checks as run_checks, Tags as Tags, register as register
from .registry import register as register, run_checks as run_checks, tag_exists as tag_exists, Tags as Tags
from . import model_checks as model_checks

View File

@@ -7,11 +7,11 @@ ERROR: int
CRITICAL: int
class CheckMessage:
level: Any = ...
msg: Any = ...
hint: Any = ...
level: int = ...
msg: str = ...
hint: Optional[str] = ...
obj: Any = ...
id: Any = ...
id: Optional[str] = ...
def __init__(
self, level: int, msg: str, hint: Optional[str] = ..., obj: Any = ..., id: Optional[str] = ...
) -> None: ...
@@ -25,19 +25,9 @@ class Info(CheckMessage):
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
class Warning(CheckMessage):
hint: str
id: str
level: int
msg: str
obj: Any
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
class Error(CheckMessage):
hint: None
id: str
level: int
msg: str
obj: Any
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
class Critical(CheckMessage):

View File

@@ -8,7 +8,7 @@ _T = TypeVar("_T", bound="File")
class File(FileProxyMixin, IO[Any]):
DEFAULT_CHUNK_SIZE: Any = ...
file: StringIO = ...
file: IO[Any] = ...
name: str = ...
mode: str = ...
def __init__(self, file: Any, name: Optional[str] = ...) -> None: ...

View File

@@ -23,13 +23,16 @@ class OutputWrapper(TextIOBase):
@property
def style_func(self): ...
@style_func.setter
def style_func(self, style_func: Any): ...
def style_func(self, style_func: Callable[[str], str]): ...
ending: str = ...
def __init__(
self, out: Union[StringIO, TextIOWrapper], style_func: Optional[Callable] = ..., ending: str = ...
self, out: Union[StringIO, TextIOWrapper], style_func: Optional[Callable[[str], str]] = ..., ending: str = ...
) -> None: ...
def __getattr__(self, name: str) -> Callable: ...
def isatty(self) -> bool: ...
def write( # type: ignore[override]
self, msg: str, style_func: Optional[Callable[[str], str]] = ..., ending: Optional[str] = ...
) -> None: ...
class BaseCommand:
help: str = ...

View File

@@ -1,6 +1,6 @@
from decimal import Decimal
from re import RegexFlag
from typing import Any, Dict, List, Optional, Union, Pattern, Collection, Callable, Tuple
from typing import Any, Callable, Collection, Dict, List, Optional, Pattern, Tuple, Union
from django.core.files.base import File
@@ -13,7 +13,7 @@ def _lazy_re_compile(regex: _Regex, flags: int = ...): ...
class RegexValidator:
regex: _Regex = ...
message: Any = ...
message: str = ...
code: str = ...
inverse_match: bool = ...
flags: int = ...
@@ -31,24 +31,24 @@ class URLValidator(RegexValidator):
ul: str = ...
ipv4_re: str = ...
ipv6_re: str = ...
hostname_re: Any = ...
domain_re: Any = ...
tld_re: Any = ...
host_re: Any = ...
schemes: Any = ...
hostname_re: str = ...
domain_re: str = ...
tld_re: str = ...
host_re: str = ...
schemes: List[str] = ...
def __init__(self, schemes: Optional[Collection[str]] = ..., **kwargs: Any) -> None: ...
integer_validator: Any
integer_validator: RegexValidator = ...
def validate_integer(value: Optional[Union[float, str]]) -> None: ...
class EmailValidator:
message: str = ...
code: str = ...
user_regex: Any = ...
domain_regex: Any = ...
literal_regex: Any = ...
domain_whitelist: Any = ...
user_regex: Pattern = ...
domain_regex: Pattern = ...
literal_regex: Pattern = ...
domain_whitelist: List[str] = ...
def __init__(
self,
message: Optional[_ErrorMessage] = ...,
@@ -68,9 +68,10 @@ def validate_ipv4_address(value: str) -> None: ...
def validate_ipv6_address(value: str) -> None: ...
def validate_ipv46_address(value: str) -> None: ...
ip_address_validator_map: Dict[str, Tuple[Callable[[Any], None], str]]
_IPValidator = Tuple[Callable[[Any], None], str]
ip_address_validator_map: Dict[str, _IPValidator]
def ip_address_validators(protocol: str, unpack_ipv4: bool) -> Any: ...
def ip_address_validators(protocol: str, unpack_ipv4: bool) -> _IPValidator: ...
def int_list_validator(
sep: str = ..., message: Optional[_ErrorMessage] = ..., code: str = ..., allow_negative: bool = ...
) -> RegexValidator: ...
@@ -92,7 +93,7 @@ class MinLengthValidator(BaseValidator): ...
class MaxLengthValidator(BaseValidator): ...
class DecimalValidator:
messages: Any = ...
messages: Dict[str, str] = ...
max_digits: int = ...
decimal_places: int = ...
def __init__(self, max_digits: Optional[Union[int, str]], decimal_places: Optional[Union[int, str]]) -> None: ...

View File

@@ -13,6 +13,7 @@ class BaseManager(QuerySet[_T]):
name: str = ...
model: Type[_T] = ...
db: str
_db: Optional[str]
def __init__(self) -> None: ...
def deconstruct(self) -> Tuple[bool, str, None, Tuple, Dict[str, int]]: ...
def check(self, **kwargs: Any) -> List[Any]: ...

View File

@@ -1,7 +1,6 @@
from typing import Any, Optional, Tuple
from django.core.cache.backends.base import BaseCache
from django.core.cache.backends.locmem import LocMemCache
from django.core.handlers.wsgi import WSGIRequest
from django.http.response import HttpResponse, HttpResponseBase
@@ -28,5 +27,5 @@ def learn_cache_key(
response: HttpResponse,
cache_timeout: Optional[float] = ...,
key_prefix: Optional[str] = ...,
cache: Optional[LocMemCache] = ...,
cache: Optional[BaseCache] = ...,
) -> str: ...

View File

@@ -1,16 +1,21 @@
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union, TypeVar, Generic, overload
from functools import wraps as wraps # noqa: F401
from django.db.models.base import Model
def curry(_curried_func: Any, *args: Any, **kwargs: Any): ...
class cached_property:
func: Callable = ...
_T = TypeVar("_T")
class cached_property(Generic[_T]):
func: Callable[..., _T] = ...
__doc__: Any = ...
name: str = ...
def __init__(self, func: Callable, name: Optional[str] = ...) -> None: ...
def __get__(self, instance: Any, cls: Type[Any] = ...) -> Any: ...
def __init__(self, func: Callable[..., _T], name: Optional[str] = ...): ...
@overload
def __get__(self, instance: None, cls: Type[Any] = ...) -> "cached_property[_T]": ...
@overload
def __get__(self, instance: object, cls: Type[Any] = ...) -> _T: ...
class Promise: ...

View File

@@ -10,8 +10,8 @@ from mypy import checker
from mypy.checker import TypeChecker
from mypy.mro import calculate_mro
from mypy.nodes import (
GDEF, MDEF, Argument, Block, ClassDef, Expression, FuncDef, MemberExpr, MypyFile, NameExpr, StrExpr, SymbolNode,
SymbolTable, SymbolTableNode, TypeInfo, Var,
GDEF, MDEF, Argument, Block, ClassDef, Expression, FuncDef, MemberExpr, MypyFile, NameExpr, PlaceholderNode,
StrExpr, SymbolNode, SymbolTable, SymbolTableNode, TypeInfo, Var,
)
from mypy.plugin import (
AttributeContext, CheckerPluginInterface, ClassDefContext, DynamicClassDefContext, FunctionContext, MethodContext,
@@ -309,39 +309,67 @@ def add_new_sym_for_info(info: TypeInfo, *, name: str, sym_type: MypyType) -> No
plugin_generated=True)
def _prepare_new_method_arguments(node: FuncDef) -> Tuple[List[Argument], MypyType]:
arguments = []
for argument in node.arguments[1:]:
if argument.type_annotation is None:
argument.type_annotation = AnyType(TypeOfAny.unannotated)
arguments.append(argument)
if isinstance(node.type, CallableType):
return_type = node.type.ret_type
else:
return_type = AnyType(TypeOfAny.unannotated)
return arguments, return_type
def build_unannotated_method_args(method_node: FuncDef) -> Tuple[List[Argument], MypyType]:
prepared_arguments = []
for argument in method_node.arguments[1:]:
argument.type_annotation = AnyType(TypeOfAny.unannotated)
prepared_arguments.append(argument)
return_type = AnyType(TypeOfAny.unannotated)
return prepared_arguments, return_type
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 method_node.type is None:
if not semanal_api.final_iteration:
semanal_api.defer()
return
if return_type is not None:
ret = semanal_api.anal_type(return_type,
allow_placeholder=True)
assert ret is not None
return_type = ret
arguments, return_type = build_unannotated_method_args(method_node)
add_method(ctx,
new_method_name,
args=arguments,
return_type=return_type,
self_type=self_type)
return
method_type = method_node.type
if not isinstance(method_type, CallableType):
if not semanal_api.final_iteration:
semanal_api.defer()
return
arguments = []
bound_return_type = semanal_api.anal_type(method_type.ret_type,
allow_placeholder=True)
assert bound_return_type is not None
if isinstance(bound_return_type, PlaceholderNode):
return
for arg_name, arg_type, original_argument in zip(method_type.arg_names[1:],
method_type.arg_types[1:],
method_node.arguments[1:]):
bound_arg_type = semanal_api.anal_type(arg_type, allow_placeholder=True)
assert bound_arg_type is not None
if isinstance(bound_arg_type, PlaceholderNode):
return
var = Var(name=original_argument.variable.name,
type=arg_type)
var.line = original_argument.variable.line
var.column = original_argument.variable.column
argument = Argument(variable=var,
type_annotation=bound_arg_type,
initializer=original_argument.initializer,
kind=original_argument.kind)
argument.set_line(original_argument)
arguments.append(argument)
add_method(ctx,
new_method_name,
args=arguments,
return_type=return_type,
return_type=bound_return_type,
self_type=self_type)

View File

@@ -457,7 +457,7 @@ IGNORED_ERRORS = {
'No overload variant of "join" matches argument types "str", "None"',
'Argument 1 to "Archive" has incompatible type "None"; expected "str"',
'Argument 1 to "to_path" has incompatible type "int"; expected "Union[Path, str]"',
'Cannot infer type argument 1 of "cached_property"',
],
'view_tests': [
"Module 'django.views.debug' has no attribute 'Path'",

View File

@@ -146,3 +146,36 @@
return param
NewManager = models.Manager.from_queryset(ModelQuerySet)
- case: from_queryset_with_inherited_manager_and_typing_no_return
disable_cache: true
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.base_queryset_method) # N: Revealed type is 'def (param: Union[builtins.int, builtins.str]) -> <nothing>'
reveal_type(MyModel().objects.base_queryset_method(2)) # N: Revealed type is '<nothing>'
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 django.db import models
from myapp.base_queryset import BaseQuerySet
class ModelQuerySet(BaseQuerySet):
pass
NewManager = models.Manager.from_queryset(ModelQuerySet)
- path: myapp/base_queryset.py
content: |
from typing import NoReturn, Union
from django.db import models
class BaseQuerySet(models.QuerySet):
def base_queryset_method(self, param: Union[int, str]) -> NoReturn:
raise ValueError

View File

@@ -307,15 +307,15 @@
- case: custom_manager_returns_proper_model_types
main: |
from myapp.models import User
reveal_type(User.objects) # N: Revealed type is 'myapp.models.User_MyManager[myapp.models.User]'
reveal_type(User.objects.select_related()) # N: Revealed type is 'myapp.models.User_MyManager[myapp.models.User]'
reveal_type(User.objects) # N: Revealed type is 'myapp.models.User_MyManager2[myapp.models.User]'
reveal_type(User.objects.select_related()) # N: Revealed type is 'myapp.models.User_MyManager2[myapp.models.User]'
reveal_type(User.objects.get()) # N: Revealed type is 'myapp.models.User*'
reveal_type(User.objects.get_instance()) # N: Revealed type is 'builtins.int'
reveal_type(User.objects.get_instance_untyped('hello')) # N: Revealed type is 'Any'
from myapp.models import ChildUser
reveal_type(ChildUser.objects) # N: Revealed type is 'myapp.models.ChildUser_MyManager[myapp.models.ChildUser]'
reveal_type(ChildUser.objects.select_related()) # N: Revealed type is 'myapp.models.ChildUser_MyManager[myapp.models.ChildUser]'
reveal_type(ChildUser.objects) # N: Revealed type is 'myapp.models.ChildUser_MyManager2[myapp.models.ChildUser]'
reveal_type(ChildUser.objects.select_related()) # N: Revealed type is 'myapp.models.ChildUser_MyManager2[myapp.models.ChildUser]'
reveal_type(ChildUser.objects.get()) # N: Revealed type is 'myapp.models.ChildUser*'
reveal_type(ChildUser.objects.get_instance()) # N: Revealed type is 'builtins.int'
reveal_type(ChildUser.objects.get_instance_untyped('hello')) # N: Revealed type is 'Any'

View File

@@ -0,0 +1,18 @@
- case: cached_property_class_vs_instance_attributes
main: |
from django.utils.functional import cached_property
from typing import List
class Foo:
@cached_property
def attr(self) -> List[str]: ...
reveal_type(attr) # N: Revealed type is 'django.utils.functional.cached_property[builtins.list*[builtins.str]]'
reveal_type(attr.name) # N: Revealed type is 'builtins.str'
reveal_type(Foo.attr) # N: Revealed type is 'django.utils.functional.cached_property[builtins.list*[builtins.str]]'
reveal_type(Foo.attr.func) # N: Revealed type is 'def (*Any, **Any) -> builtins.list*[builtins.str]'
f = Foo()
reveal_type(f.attr) # N: Revealed type is 'builtins.list*[builtins.str]'
f.attr.name # E: "List[str]" has no attribute "name"