31 Commits

Author SHA1 Message Date
sobolevn
645ee97e78 Release 1.6.0 2020-10-01 10:57:05 +03:00
William Schwartz
5bc3759ea2 Template Loader.get_template_sources yield Origins (#476)
django.template.loaders.base.Loader.get_template_sources should yield Origins:
https://docs.djangoproject.com/en/3.1/ref/templates/api/#django.template.loaders.base.Loader.get_template_sources

Currently it's returning none. Objects that yield should be marked as iterables.
https://mypy.readthedocs.io/en/stable/kinds_of_types.html#generators
2020-09-30 01:21:38 +03:00
henribru
534a028ea2 Use AbstractBaseUser where possible (#475)
Fixes #431
2020-09-29 22:04:20 +03:00
Nicolas Delaby
87856754ea psycopg2-binary is faster to install (#473) 2020-09-29 12:50:11 +03:00
Szymon Pyżalski
2f7fac2eaf Allowed to use decimal in combinables (#454)
Co-authored-by: Szymon Pyżalski <spyzalski@egnyte.com>
2020-09-21 15:00:32 +03:00
Marti Raudsepp
5ff99fd047 Check arguments to django.urls.{path,re_path} functions (#464)
These functions were added in Django 2.0, but the stubs have been
incomplete since commit 9a68263257

The implementation is almost identical to `url()` from
`conf/urls/__init__.pyi`
2020-09-17 18:08:26 +03:00
Marti Raudsepp
f77ebcd22c Add db.models.constants stub module (#462) 2020-09-17 18:07:03 +03:00
Alexander Viklund
34b126e3da Fix wrong type for parse_until in template parser (#460) 2020-09-12 01:58:09 +03:00
Jonathan Moss
6e5f5f2cdb Allows FileField storage to be a Callable (#453)
* Allows FileField storage to be a Callable

The `storage` parameter of `FileField` (and by extension `ImageField`)
is not limited to just taking an instance of `Storage`. It can also take
a no-args callable that returns an instance of `Storage`.

* correcting linting issue in forms.pyi
2020-08-28 10:14:57 +03:00
James Perretta
95252cde60 Add "one_to_many", "one_to_one", "many_to_many," and "many_to_one" attributes to Field base class (#449)
* Add "_to_many" and "_to_one" bool attributes to Field base class.

* Remove unused import from test case.
2020-08-24 23:09:15 +03:00
Zach Waggoner
6ef2cf0331 Add force_color parameter to django.core.management.color.color_style (#448) 2020-08-21 12:41:51 +03:00
Alexander Viklund
9f3b95841b return type for translation.get_language should not be Optional (#446) 2020-08-18 12:41:52 +03:00
Federico Bond
e764b1cf4c Add route argument to ResolverMatch constructor (#442) 2020-08-12 15:36:01 +03:00
Šarūnas Nejus
8a64d87917 Output a more clear configuration error (#421)
* Output a more clear configuration error

* Performance related improvements in config read handling

* Check python 3.6 with travis

* Revert the .travis.yml py36 inclusion

* Added tests for input error handling

* Thanks isort, isorted it out

* Make exit() function a bit more aesthetic

* Single quote -> double quote docstrings

* Whitespace removed
2020-08-09 11:27:21 +03:00
Daniel Hillier
60f3f9dd9f Change select_for_update's 'of' type to Sequence[str] (#440) 2020-08-08 20:21:55 +03:00
Mantas Zimnickas
ca10ee9242 Fix staff_member_required annotations (#436)
* Fix staff_member_required annotations

`redirect_field_name` can be `None`.

* Reformat code with black
2020-07-31 10:35:35 +03:00
Sigurd Ljødal
f651f27ddf Fix type of update_fields parameter to Model.save (#437)
Sending in a single string leads to unexpected behavior and the method
accepts any iterable, not just sequences, of strings.

Technically sending in a single string is still allowed because a string
is an iterable of strings, but this way the intention of that argument
is clearer.
2020-07-31 10:14:35 +03:00
Kacper
3915aa0639 AttributeError exception in build_unannotated_method_args (#429)
* AttributeError exception in build_unannotated_method_args

* assigning [] to arguments if AttributeError

Co-authored-by: Kacper Szmigiel <szmigielkacper@gmai.com>
2020-07-21 15:31:19 +03:00
Nate Clark
97ec2ee43b Allow Field.widget to be a class or instance (#426)
According to source below, a Field's widget is allowed to be a class or instance of Widget.

156a2138db/django/forms/fields.py (L63-L66)
2020-07-16 00:51:29 +03:00
Daniel Hillier
19c73a106d admin: Allow ModelAdmin.actions to contain str (#425)
Strings are allowed in ModelAdmin.actions when they refer to a method on
the ModelAdmin subclass.
2020-07-14 10:43:50 +03:00
Kacper
92ef5d9d95 additional defer() to offset the effect of using the type before decl… (#424)
* additional defer() to offset the effect of using the type before declaring

* linter fix

* linter fix

* linter fix

* test case

Co-authored-by: Kacper Szmigiel <szmigielkacper@gmai.com>
2020-07-13 16:31:13 +03:00
Daniel Hillier
f16d1b8cb6 Improve *HttpResponse.getvalue() types (#422)
* Change returned type of StreamingHttpResponse.getvalue() to bytes

* Add HttpResponse.getvalue() definition
2020-07-11 15:57:35 +03:00
Alexander Viklund
c3cdc1c2d5 Allow more types for args to Paginator.get_page (#418) 2020-07-08 22:53:38 +03:00
Alexander Viklund
3704d0ab98 Don't change type of HttpRequest.user if type has been changed by subclassing (#415)
* Don't change type of HttpRequest.user if type has been changed by subclassing

* Asserts for typing

* Add tests

* Add description of HttpRequest subclassing to README

* Dummy to rebuild travis
2020-07-07 12:52:21 +03:00
Kacper
b1d619edb2 Bumped required Mypy version to newest in requirements (#414)
* pytest-mypy-plugins package newer version

* Revert "pytest-mypy-plugins package newer version"

This reverts commit 871347a86577a5dad867bc751689bbc06d2bcae0.

* update tests ignores for django

* Revert "update tests ignores for django"

This reverts commit 93fc66e311af62cf8cf5b79a72ab723bf3cf060a.

* changed required mypy version to the newest

Co-authored-by: Kacper Szmigiel <szmigielkacper@gmai.com>
2020-07-06 10:34:17 +03:00
Akseli Nelander
e680326c72 Fix partition signature (#413) 2020-07-01 15:35:35 +03:00
Étienne
574a87e68c explicitly mark value and label types on choices enum (#406)
In addition to the meta properties added
to the Choices enums, value and label are
set for each choice.
This commit explicitly types those instead of the
tuple value and non-extant label types
2020-06-22 11:08:09 +03:00
coiax
82ae1751ed 🐑 Update update_session_auth_hash() stub (#401)
The update_session_auth_hash() stub doesn't match the documented and
actual type that the function expects.

---

Resolves #400.
2020-06-16 19:19:11 +03:00
Ceesjan Luiten
69042783b1 Allow form.save() in CreateView / UpdateView (#374) 2020-06-12 21:12:56 +03:00
Kacper
391bbc59d5 WIP Fix ci/cd (#396)
* pytest-mypy-plugins package newer version

* Revert "pytest-mypy-plugins package newer version"

This reverts commit 871347a86577a5dad867bc751689bbc06d2bcae0.

* update tests ignores for django

* Revert "update tests ignores for django"

This reverts commit 93fc66e311af62cf8cf5b79a72ab723bf3cf060a.

* lets see what happen :)

Co-authored-by: Kacper Szmigiel <szmigielkacper@gmai.com>
2020-06-11 23:05:59 +03:00
Michael
28c76df3b2 Fix compatibility table in readme (#395) 2020-06-10 16:03:00 +03:00
39 changed files with 375 additions and 107 deletions

View File

@@ -47,8 +47,9 @@ We rely on different `django` and `mypy` versions:
| django-stubs | mypy version | django version | python version
| ------------ | ---- | ---- | ---- |
| 1.5.0 | 0.780 | 2.2.x \|\| 3.x | ^3.6
| 1.4.0 | 0.770 | 2.2.x \|\| 3.x | ^3.6
| 1.6.0 | 0.780 | 2.2.x \|\| 3.x | ^3.6
| 1.5.0 | 0.770 | 2.2.x \|\| 3.x | ^3.6
| 1.4.0 | 0.760 | 2.2.x \|\| 3.x | ^3.6
| 1.3.0 | 0.750 | 2.2.x \|\| 3.x | ^3.6
| 1.2.0 | 0.730 | 2.2.x | ^3.6
| 1.1.0 | 0.720 | 2.2.x | ^3.6
@@ -89,19 +90,24 @@ You can use strings instead: `'QuerySet[MyModel]'` and `'Manager[MyModel]'`, thi
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?
### How can I create a HttpRequest that's guaranteed to have an authenticated user?
You can subclass standard request like so:
Django's built in `HttpRequest` has the attribute `user` that resolves to the type
```python
Union[User, AnonymousUser]
```
where `User` is the user model specified by the `AUTH_USER_MODEL` setting.
If you want a `HttpRequest` that you can type-annotate with where you know that the user is authenticated you can subclass the normal `HttpRequest` class like so:
```python
from django.http import HttpRequest
from my_user_app.models import MyUser
class MyRequest(HttpRequest):
class AuthenticatedHttpRequest(HttpRequest):
user: MyUser
```
And then use `MyRequest` instead of standard `HttpRequest` inside your project.
And then use `AuthenticatedHttpRequest` instead of the standard `HttpRequest` for when you know that the user is authenticated. For example in views using the `@login_required` decorator.
## Related projects

View File

@@ -1,6 +1,6 @@
black
pytest-mypy-plugins==1.3.0
psycopg2
psycopg2-binary
flake8==3.7.9
flake8-pyi==19.3.0
isort==4.3.21

View File

@@ -135,7 +135,7 @@ class ModelAdmin(BaseModelAdmin):
delete_selected_confirmation_template: str = ...
object_history_template: str = ...
popup_response_template: str = ...
actions: Sequence[Callable[[ModelAdmin, HttpRequest, QuerySet], None]] = ...
actions: Sequence[Union[Callable[[ModelAdmin, HttpRequest, QuerySet], None], str]] = ...
action_form: Any = ...
actions_on_top: bool = ...
actions_on_bottom: bool = ...

View File

@@ -1,7 +1,11 @@
from typing import Callable, TypeVar, overload
from typing import Callable, Optional, TypeVar, overload
_C = TypeVar("_C", bound=Callable)
@overload
def staff_member_required(view_func: _C = ..., redirect_field_name: str = ..., login_url: str = ...) -> _C: ...
def staff_member_required(
view_func: _C = ..., redirect_field_name: Optional[str] = ..., login_url: str = ...
) -> _C: ...
@overload
def staff_member_required(view_func: None = ..., redirect_field_name: str = ..., login_url: str = ...) -> Callable: ...
def staff_member_required(
view_func: None = ..., redirect_field_name: Optional[str] = ..., login_url: str = ...
) -> Callable: ...

View File

@@ -2,7 +2,7 @@ from typing import Any, List, Optional, Type, Union
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import AbstractUser, AnonymousUser
from django.contrib.auth.models import AnonymousUser
from django.core.handlers.wsgi import WSGIRequest
from django.db.models.base import Model
from django.db.models.options import Options
@@ -29,6 +29,6 @@ def logout(request: HttpRequest) -> None: ...
def get_user_model() -> Type[Model]: ...
def get_user(request: HttpRequest) -> Union[AbstractBaseUser, AnonymousUser]: ...
def get_permission_codename(action: str, opts: Options) -> str: ...
def update_session_auth_hash(request: WSGIRequest, user: AbstractUser) -> None: ...
def update_session_auth_hash(request: HttpRequest, user: AbstractBaseUser) -> None: ...
default_app_config: str

View File

@@ -1,7 +1,7 @@
from typing import Any, Dict, Iterator, Optional
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import AbstractUser, User
from django.contrib.auth.models import User
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.core.handlers.wsgi import WSGIRequest
@@ -86,6 +86,6 @@ class AdminPasswordChangeForm(forms.Form):
password1: Any = ...
password2: Any = ...
user: User = ...
def __init__(self, user: AbstractUser, *args: Any, **kwargs: Any) -> None: ...
def __init__(self, user: AbstractBaseUser, *args: Any, **kwargs: Any) -> None: ...
def clean_password2(self) -> str: ...
def save(self, commit: bool = ...) -> AbstractUser: ...
def save(self, commit: bool = ...) -> AbstractBaseUser: ...

View File

@@ -22,4 +22,4 @@ class Style:
def make_style(config_string: str = ...) -> Style: ...
def no_style() -> Style: ...
def color_style() -> Style: ...
def color_style(force_color: bool = ...) -> Style: ...

View File

@@ -29,8 +29,8 @@ class Paginator:
orphans: int = ...,
allow_empty_first_page: bool = ...,
) -> None: ...
def validate_number(self, number: Optional[Union[float, str]]) -> int: ...
def get_page(self, number: Optional[int]) -> Page: ...
def validate_number(self, number: Optional[Union[int, float, str]]) -> int: ...
def get_page(self, number: Optional[Union[int, float, str]]) -> Page: ...
def page(self, number: Union[int, str]) -> Page: ...
@property
def count(self) -> int: ...

View File

@@ -1,4 +1,4 @@
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union, Collection, ClassVar
from typing import Any, Callable, Collection, Dict, Iterable, List, Optional, Set, Tuple, Type, TypeVar, Union
from django.core.checks.messages import CheckMessage
from django.core.exceptions import ValidationError
@@ -22,7 +22,7 @@ class Model(metaclass=ModelBase):
class Meta: ...
_meta: Options[Any]
_default_manager: BaseManager[Model]
objects: ClassVar[BaseManager[Any]]
objects: BaseManager[Any]
pk: Any = ...
_state: ModelState
def __init__(self: _Self, *args, **kwargs) -> None: ...
@@ -43,7 +43,7 @@ class Model(metaclass=ModelBase):
force_insert: bool = ...,
force_update: bool = ...,
using: Optional[str] = ...,
update_fields: Optional[Union[Sequence[str], str]] = ...,
update_fields: Optional[Iterable[str]] = ...,
) -> None: ...
def save_base(
self,
@@ -51,7 +51,7 @@ class Model(metaclass=ModelBase):
force_insert: bool = ...,
force_update: bool = ...,
using: Optional[str] = ...,
update_fields: Optional[Union[Sequence[str], str]] = ...,
update_fields: Optional[Iterable[str]] = ...,
): ...
def refresh_from_db(self: _Self, using: Optional[str] = ..., fields: Optional[List[str]] = ...) -> None: ...
def get_deferred_fields(self) -> Set[str]: ...

View File

@@ -0,0 +1 @@
LOOKUP_SEP: str = ...

View File

@@ -10,6 +10,10 @@ class ChoicesMeta(enum.EnumMeta):
class Choices(enum.Enum, metaclass=ChoicesMeta):
def __str__(self): ...
@property
def label(self) -> str: ...
@property
def value(self) -> Any: ...
# fake
class _IntegerChoicesMeta(ChoicesMeta):
@@ -18,7 +22,9 @@ class _IntegerChoicesMeta(ChoicesMeta):
labels: List[str] = ...
values: List[int] = ...
class IntegerChoices(int, Choices, metaclass=_IntegerChoicesMeta): ...
class IntegerChoices(int, Choices, metaclass=_IntegerChoicesMeta):
@property
def value(self) -> int: ...
# fake
class _TextChoicesMeta(ChoicesMeta):
@@ -27,4 +33,6 @@ class _TextChoicesMeta(ChoicesMeta):
labels: List[str] = ...
values: List[str] = ...
class TextChoices(str, Choices, metaclass=_TextChoicesMeta): ...
class TextChoices(str, Choices, metaclass=_TextChoicesMeta):
@property
def value(self) -> str: ...

View File

@@ -1,4 +1,5 @@
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union, Iterable
from django.db.models.lookups import Lookup
@@ -15,6 +16,8 @@ class SQLiteNumericMixin:
_Self = TypeVar("_Self")
_Numeric = Union[float, Decimal]
class Combinable:
ADD: str = ...
SUB: str = ...
@@ -27,25 +30,25 @@ class Combinable:
BITLEFTSHIFT: str = ...
BITRIGHTSHIFT: str = ...
def __neg__(self: _Self) -> _Self: ...
def __add__(self: _Self, other: Optional[Union[timedelta, Combinable, float, str]]) -> _Self: ...
def __sub__(self: _Self, other: Union[timedelta, Combinable, float]) -> _Self: ...
def __mul__(self: _Self, other: Union[timedelta, Combinable, float]) -> _Self: ...
def __truediv__(self: _Self, other: Union[Combinable, float]) -> _Self: ...
def __itruediv__(self: _Self, other: Union[Combinable, float]) -> _Self: ...
def __add__(self: _Self, other: Optional[Union[timedelta, Combinable, _Numeric, str]]) -> _Self: ...
def __sub__(self: _Self, other: Union[timedelta, Combinable, _Numeric]) -> _Self: ...
def __mul__(self: _Self, other: Union[timedelta, Combinable, _Numeric]) -> _Self: ...
def __truediv__(self: _Self, other: Union[Combinable, _Numeric]) -> _Self: ...
def __itruediv__(self: _Self, other: Union[Combinable, _Numeric]) -> _Self: ...
def __mod__(self: _Self, other: Union[int, Combinable]) -> _Self: ...
def __pow__(self: _Self, other: Union[float, Combinable]) -> _Self: ...
def __pow__(self: _Self, other: Union[_Numeric, Combinable]) -> _Self: ...
def __and__(self: _Self, other: Combinable) -> _Self: ...
def bitand(self: _Self, other: int) -> _Self: ...
def bitleftshift(self: _Self, other: int) -> _Self: ...
def bitrightshift(self: _Self, other: int) -> _Self: ...
def __or__(self: _Self, other: Combinable) -> _Self: ...
def bitor(self: _Self, other: int) -> _Self: ...
def __radd__(self, other: Optional[Union[datetime, float, Combinable]]) -> Combinable: ...
def __rsub__(self, other: Union[float, Combinable]) -> Combinable: ...
def __rmul__(self, other: Union[float, Combinable]) -> Combinable: ...
def __rtruediv__(self, other: Union[float, Combinable]) -> Combinable: ...
def __radd__(self, other: Optional[Union[datetime, _Numeric, Combinable]]) -> Combinable: ...
def __rsub__(self, other: Union[_Numeric, Combinable]) -> Combinable: ...
def __rmul__(self, other: Union[_Numeric, Combinable]) -> Combinable: ...
def __rtruediv__(self, other: Union[_Numeric, Combinable]) -> Combinable: ...
def __rmod__(self, other: Union[int, Combinable]) -> Combinable: ...
def __rpow__(self, other: Union[float, Combinable]) -> Combinable: ...
def __rpow__(self, other: Union[_Numeric, Combinable]) -> Combinable: ...
def __rand__(self, other: Any) -> Combinable: ...
def __ror__(self, other: Any) -> Combinable: ...

View File

@@ -56,6 +56,10 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
remote_field: Field
is_relation: bool
related_model: Optional[Type[Model]]
one_to_many: Optional[bool] = ...
one_to_one: Optional[bool] = ...
many_to_many: Optional[bool] = ...
many_to_one: Optional[bool] = ...
max_length: int
model: Type[Model]
name: str

View File

@@ -39,7 +39,7 @@ class FileField(Field):
def __init__(
self,
upload_to: Union[str, Callable, Path] = ...,
storage: Optional[Storage] = ...,
storage: Optional[Union[Storage, Callable[[], Storage]]] = ...,
verbose_name: Optional[Union[str, bytes]] = ...,
name: Optional[str] = ...,
max_length: Optional[int] = ...,

View File

@@ -96,7 +96,7 @@ class _BaseQuerySet(Generic[_T], Sized):
def union(self: _QS, *other_qs: Any, all: bool = ...) -> _QS: ...
def intersection(self: _QS, *other_qs: Any) -> _QS: ...
def difference(self: _QS, *other_qs: Any) -> _QS: ...
def select_for_update(self: _QS, nowait: bool = ..., skip_locked: bool = ..., of: Tuple = ...) -> _QS: ...
def select_for_update(self: _QS, nowait: bool = ..., skip_locked: bool = ..., of: Sequence[str] = ...) -> _QS: ...
def select_related(self: _QS, *fields: Any) -> _QS: ...
def prefetch_related(self: _QS, *lookups: Any) -> _QS: ...
# TODO: return type

View File

@@ -14,7 +14,7 @@ class Field:
initial: Any
label: Optional[str]
required: bool
widget: Type[Widget] = ...
widget: Union[Type[Widget], Widget] = ...
hidden_widget: Any = ...
default_validators: Any = ...
default_error_messages: Any = ...

View File

@@ -70,7 +70,12 @@ class BaseForm:
def visible_fields(self): ...
def get_initial_for_field(self, field: Field, field_name: str) -> Any: ...
def _html_output(
self, normal_row: str, error_row: str, row_ender: str, help_text_html: str, errors_on_separate_row: bool,
self,
normal_row: str,
error_row: str,
row_ender: str,
help_text_html: str,
errors_on_separate_row: bool,
) -> SafeText: ...
class Form(BaseForm):

View File

@@ -83,12 +83,13 @@ class HttpResponse(HttpResponseBase):
context: Context
resolver_match: ResolverMatch
def json(self) -> Any: ...
def getvalue(self) -> bytes: ...
class StreamingHttpResponse(HttpResponseBase):
content: Any
streaming_content: Iterator[Any]
def __init__(self, streaming_content: Iterable[Any] = ..., *args: Any, **kwargs: Any) -> None: ...
def getvalue(self) -> Any: ...
def getvalue(self) -> bytes: ...
class FileResponse(StreamingHttpResponse):
client: Client

View File

@@ -109,7 +109,7 @@ class Parser:
builtins: Optional[List[Library]] = ...,
origin: Optional[Origin] = ...,
) -> None: ...
def parse(self, parse_until: Optional[Tuple[str]] = ...) -> NodeList: ...
def parse(self, parse_until: Optional[Tuple[str, ...]] = ...) -> NodeList: ...
def skip_past(self, endtag: str) -> None: ...
def extend_nodelist(self, nodelist: NodeList, node: Node, token: Token) -> None: ...
def error(self, token: Token, e: Union[Exception, str]) -> Exception: ...

View File

@@ -1,4 +1,4 @@
from typing import Any, List, Optional, Dict
from typing import Any, List, Iterable, Optional, Dict
from django.template.base import Origin, Template
from django.template.engine import Engine
@@ -8,5 +8,5 @@ class Loader:
get_template_cache: Dict[str, Any] = ...
def __init__(self, engine: Engine) -> None: ...
def get_template(self, template_name: str, skip: Optional[List[Origin]] = ...) -> Template: ...
def get_template_sources(self, template_name: str) -> None: ...
def get_template_sources(self, template_name: str) -> Iterable[Origin]: ...
def reset(self) -> None: ...

View File

@@ -2,7 +2,7 @@ from io import BytesIO
from types import TracebackType
from typing import Any, Dict, List, Optional, Pattern, Tuple, Type, Union
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.sessions.backends.base import SessionBase
from django.core.handlers.base import BaseHandler
from django.http.cookie import SimpleCookie
@@ -126,7 +126,7 @@ class Client(RequestFactory):
@property
def session(self) -> SessionBase: ...
def login(self, **credentials: Any) -> bool: ...
def force_login(self, user: AbstractUser, backend: Optional[str] = ...) -> None: ...
def force_login(self, user: AbstractBaseUser, backend: Optional[str] = ...) -> None: ...
def logout(self) -> None: ...
def conditional_content_removal(request: HttpRequest, response: HttpResponseBase) -> HttpResponse: ...

View File

@@ -1,8 +1,31 @@
from typing import Any, List, Optional, Tuple
from typing import Any, List, Optional, Tuple, overload, Callable, Dict, Union
from .resolvers import URLResolver
from .resolvers import URLResolver, URLPattern
from ..conf.urls import IncludedURLConf
from ..http.response import HttpResponseBase
def include(arg: Any, namespace: Optional[str] = ...) -> Tuple[List[URLResolver], Optional[str], Optional[str]]: ...
path: Any
re_path: Any
# path()
@overload
def path(
route: str, view: Callable[..., HttpResponseBase], kwargs: Dict[str, Any] = ..., name: str = ...
) -> URLPattern: ...
@overload
def path(route: str, view: IncludedURLConf, kwargs: Dict[str, Any] = ..., name: str = ...) -> URLResolver: ...
@overload
def path(
route: str, view: List[Union[URLResolver, str]], kwargs: Dict[str, Any] = ..., name: str = ...
) -> URLResolver: ...
# re_path()
@overload
def re_path(
route: str, view: Callable[..., HttpResponseBase], kwargs: Dict[str, Any] = ..., name: str = ...
) -> URLPattern: ...
@overload
def re_path(route: str, view: IncludedURLConf, kwargs: Dict[str, Any] = ..., name: str = ...) -> URLResolver: ...
@overload
def re_path(
route: str, view: List[Union[URLResolver, str]], kwargs: Dict[str, Any] = ..., name: str = ...
) -> URLResolver: ...

View File

@@ -22,6 +22,7 @@ class ResolverMatch:
url_name: Optional[str] = ...,
app_names: Optional[List[Optional[str]]] = ...,
namespaces: Optional[List[Optional[str]]] = ...,
route: Optional[str] = ...,
) -> None: ...
def __getitem__(self, index: int) -> Any: ...
# for tuple unpacking

View File

@@ -56,4 +56,8 @@ class SimpleLazyObject(LazyObject):
def __copy__(self) -> List[int]: ...
def __deepcopy__(self, memo: Dict[Any, Any]) -> List[int]: ...
def partition(predicate: Callable, values: List[Model]) -> Tuple[List[Model], List[Model]]: ...
_PartitionMember = TypeVar("_PartitionMember")
def partition(
predicate: Callable, values: List[_PartitionMember]
) -> Tuple[List[_PartitionMember], List[_PartitionMember]]: ...

View File

@@ -57,7 +57,7 @@ class override(ContextDecorator):
def __enter__(self) -> None: ...
def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None: ...
def get_language() -> Optional[str]: ...
def get_language() -> str: ...
def get_language_from_path(path: str) -> Optional[str]: ...
def get_language_bidi() -> bool: ...
def check_for_language(lang_code: Optional[str]) -> bool: ...

View File

@@ -1,28 +1,35 @@
from typing import Any, Callable, Dict, Optional, Sequence, Type, Union
from django.forms.forms import BaseForm
from django.forms.models import BaseModelForm
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
from django.views.generic.detail import BaseDetailView, SingleObjectMixin, SingleObjectTemplateResponseMixin
from typing_extensions import Literal
from django.http import HttpRequest, HttpResponse
class FormMixin(ContextMixin):
class AbstractFormMixin(ContextMixin):
initial: Dict[str, Any] = ...
form_class: Optional[Type[BaseForm]] = ...
success_url: Optional[Union[str, Callable[..., Any]]] = ...
prefix: Optional[str] = ...
def get_initial(self) -> Dict[str, Any]: ...
def get_prefix(self) -> Optional[str]: ...
def get_form_class(self) -> Type[BaseForm]: ...
def get_form(self, form_class: Optional[Type[BaseForm]] = ...) -> BaseForm: ...
def get_form_kwargs(self) -> Dict[str, Any]: ...
def get_success_url(self) -> str: ...
class FormMixin(AbstractFormMixin):
def get_form_class(self) -> Type[BaseForm]: ...
def get_form(self, form_class: Optional[Type[BaseForm]] = ...) -> BaseForm: ...
def form_valid(self, form: BaseForm) -> HttpResponse: ...
def form_invalid(self, form: BaseForm) -> HttpResponse: ...
class ModelFormMixin(FormMixin, SingleObjectMixin):
class ModelFormMixin(AbstractFormMixin, SingleObjectMixin):
fields: Optional[Union[Sequence[str], Literal["__all__"]]] = ...
def get_form_class(self) -> Type[BaseModelForm]: ...
def get_form(self, form_class: Optional[Type[BaseModelForm]] = ...) -> BaseModelForm: ...
def form_valid(self, form: BaseModelForm) -> HttpResponse: ...
def form_invalid(self, form: BaseModelForm) -> HttpResponse: ...
class ProcessFormView(View):
def get(self, request: HttpRequest, *args: str, **kwargs: Any) -> HttpResponse: ...

View File

@@ -311,7 +311,11 @@ def add_new_sym_for_info(info: TypeInfo, *, name: str, sym_type: MypyType) -> No
def build_unannotated_method_args(method_node: FuncDef) -> Tuple[List[Argument], MypyType]:
prepared_arguments = []
for argument in method_node.arguments[1:]:
try:
arguments = method_node.arguments[1:]
except AttributeError:
arguments = []
for argument in arguments:
argument.type_annotation = AnyType(TypeOfAny.unannotated)
prepared_arguments.append(argument)
return_type = AnyType(TypeOfAny.unannotated)
@@ -343,6 +347,7 @@ def copy_method_to_another_class(ctx: ClassDefContext, self_type: Instance,
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):
@@ -352,6 +357,10 @@ def copy_method_to_another_class(ctx: ClassDefContext, self_type: Instance,
method_type.arg_types[1:],
method_node.arguments[1:]):
bound_arg_type = semanal_api.anal_type(arg_type, allow_placeholder=True)
if bound_arg_type is None and not semanal_api.final_iteration:
semanal_api.defer()
return
assert bound_arg_type is not None
if isinstance(bound_arg_type, PlaceholderNode):

View File

@@ -1,9 +1,8 @@
import configparser
from functools import partial
from typing import Callable, Dict, List, Optional, Tuple
from typing import Callable, Dict, List, NoReturn, Optional, Tuple, cast
from django.db.models.fields.related import RelatedField
from mypy.errors import Errors
from mypy.nodes import MypyFile, TypeInfo
from mypy.options import Options
from mypy.plugin import (
@@ -52,25 +51,40 @@ def add_new_manager_base(ctx: ClassDefContext) -> None:
def extract_django_settings_module(config_file_path: Optional[str]) -> str:
errors = Errors()
if config_file_path is None:
errors.report(0, None, "'django_settings_module' is not set: no mypy config file specified")
errors.raise_error()
def exit(error_type: int) -> NoReturn:
"""Using mypy's argument parser, raise `SystemExit` to fail hard if validation fails.
Considering that the plugin's startup duration is around double as long as mypy's, this aims to
import and construct objects only when that's required - which happens once and terminates the
run. Considering that most of the runs are successful, there's no need for this to linger in the
global scope.
"""
from mypy.main import CapturableArgumentParser
usage = """(config)
...
[mypy.plugins.django_stubs]
django_settings_module: str (required)
...
""".replace("\n" + 8 * " ", "\n")
handler = CapturableArgumentParser(prog='(django-stubs) mypy', usage=usage)
messages = {1: 'mypy config file is not specified or found',
2: 'no section [mypy.plugins.django-stubs]',
3: 'the setting is not provided'}
handler.error("'django_settings_module' is not set: " + messages[error_type])
parser = configparser.ConfigParser()
parser.read(config_file_path) # type: ignore
try:
parser.read_file(open(cast(str, config_file_path), 'r'), source=config_file_path)
except (IsADirectoryError, OSError):
exit(1)
if not parser.has_section('mypy.plugins.django-stubs'):
errors.report(0, None, "'django_settings_module' is not set: no section [mypy.plugins.django-stubs]",
file=config_file_path)
errors.raise_error()
if not parser.has_option('mypy.plugins.django-stubs', 'django_settings_module'):
errors.report(0, None, "'django_settings_module' is not set: setting is not provided",
file=config_file_path)
errors.raise_error()
django_settings_module = parser.get('mypy.plugins.django-stubs', 'django_settings_module').strip('\'"')
return django_settings_module
section = 'mypy.plugins.django-stubs'
if not parser.has_section(section):
exit(2)
settings = parser.get(section, 'django_settings_module', fallback=None) or exit(3)
return cast(str, settings).strip('\'"')
class NewSemanalDjangoPlugin(Plugin):

View File

@@ -8,6 +8,22 @@ from mypy_django_plugin.lib import helpers
def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_context: DjangoContext) -> MypyType:
# Imported here because django isn't properly loaded yet when module is loaded
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import AnonymousUser
abstract_base_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AbstractBaseUser)
anonymous_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AnonymousUser)
# This shouldn't be able to happen, as we managed to import the models above.
assert abstract_base_user_info is not None
assert anonymous_user_info is not None
if ctx.default_attr_type != UnionType([Instance(abstract_base_user_info, []), Instance(anonymous_user_info, [])]):
# Type has been changed from the default in django-stubs.
# I.e. HttpRequest has been subclassed and user-type overridden, so let's leave it as is.
return ctx.default_attr_type
auth_user_model = django_context.settings.AUTH_USER_MODEL
user_cls = django_context.apps_registry.get_model(auth_user_model)
user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), user_cls)
@@ -15,12 +31,4 @@ def set_auth_user_model_as_type_for_request_user(ctx: AttributeContext, django_c
if user_info is None:
return ctx.default_attr_type
# Imported here because django isn't properly loaded yet when module is loaded
from django.contrib.auth.models import AnonymousUser
anonymous_user_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), AnonymousUser)
if anonymous_user_info is None:
# This shouldn't be able to happen, as we managed to import the model above...
return Instance(user_info, [])
return UnionType([Instance(user_info, []), Instance(anonymous_user_info, [])])

View File

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

View File

@@ -451,9 +451,11 @@ IGNORED_ERRORS = {
'urlpatterns': [
'"object" not callable',
'"None" not callable',
'Argument 2 to "path" has incompatible type "Callable[[Any], None]"',
'Incompatible return value type (got "None", expected "HttpResponseBase")',
],
'urlpatterns_reverse': [
'List or tuple expected as variable arguments',
'No overload variant of "path" matches argument types "str", "None"',
'No overload variant of "zip" matches argument types "Any", "object"',
'Argument 1 to "get_callable" has incompatible type "int"'
],

View File

@@ -14,8 +14,8 @@ from scripts.enabled_test_modules import (
)
DJANGO_COMMIT_REFS: Dict[str, Tuple[str, str]] = {
'2.2': ('stable/2.2.x', '996be04c3ceb456754d9d527d4d708f30727f07e'),
'3.0': ('stable/3.0.x', 'd9f1792c7649e9f946f4a3a35a76bddf5a412b8b')
'2.2': ('stable/2.2.x', '8093aaa8ff9dd7386a069c6eb49fcc1c5980c033'),
'3.0': ('stable/3.0.x', '44da7abda848f05caaed74f6a749038c87dedfda')
}
PROJECT_DIRECTORY = Path(__file__).parent.parent
DJANGO_SOURCE_DIRECTORY = PROJECT_DIRECTORY / 'django-sources' # type: Path

View File

@@ -9,7 +9,7 @@ def find_stub_files(name: str) -> List[str]:
result = []
for root, dirs, files in os.walk(name):
for file in files:
if file.endswith('.pyi'):
if file.endswith(".pyi"):
if os.path.sep in root:
sub_root = root.split(os.path.sep, 1)[-1]
file = os.path.join(sub_root, file)
@@ -17,38 +17,42 @@ def find_stub_files(name: str) -> List[str]:
return result
with open('README.md', 'r') as f:
with open("README.md", "r") as f:
readme = f.read()
dependencies = [
'mypy>=0.780,<0.790',
'typing-extensions',
'django',
"mypy>=0.782,<0.790",
"typing-extensions",
"django",
]
setup(
name="django-stubs",
version="1.5.0",
description='Mypy stubs for Django',
version="1.6.0",
description="Mypy stubs for Django",
long_description=readme,
long_description_content_type='text/markdown',
license='MIT',
long_description_content_type="text/markdown",
license="MIT",
url="https://github.com/typeddjango/django-stubs",
author="Maksim Kurnikov",
author_email="maxim.kurnikov@gmail.com",
py_modules=[],
python_requires='>=3.6',
python_requires=">=3.6",
install_requires=dependencies,
packages=['django-stubs', *find_packages(exclude=['scripts'])],
package_data={'django-stubs': find_stub_files('django-stubs')},
packages=["django-stubs", *find_packages(exclude=["scripts"])],
package_data={"django-stubs": find_stub_files("django-stubs")},
classifiers=[
'Development Status :: 3 - Alpha',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8'
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Framework :: Django",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
"Typing :: Typed",
],
project_urls={
'Release notes': 'https://github.com/typeddjango/django-stubs/releases',
"Release notes": "https://github.com/typeddjango/django-stubs/releases",
},
)

View File

@@ -65,11 +65,15 @@
delete_selected_confirmation_template = "template"
object_history_template = "template"
popup_response_template = "template"
actions = (an_action,)
actions = (an_action, "a_method_action")
actions_on_top = True
actions_on_bottom = False
actions_selection_counter = True
admin_site = AdminSite()
def a_method_action(self, request, queryset):
pass
# This test is here to make sure we're not running into a mypy issue which is
# worked around using a somewhat complicated _ListOrTuple union type. Once the
# issue is solved upstream this test should pass even with the workaround
@@ -123,4 +127,4 @@
pass
class A(admin.ModelAdmin):
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Callable[[ModelAdmin, HttpRequest, QuerySet[Any]], None]"
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[ModelAdmin, HttpRequest, QuerySet[Any]], None], str]"

View File

@@ -0,0 +1,26 @@
- case: field_to_many_and_to_one_attrs_bool_or_none_in_field_base_class
main: |
from django.db.models import Field
field: Field
my_bool: bool
my_bool = field.one_to_many
my_bool = field.one_to_one
my_bool = field.many_to_many
my_bool = field.many_to_one
# Narrowing the types should give us bool
assert field.one_to_many is not None
my_bool = field.one_to_many
assert field.one_to_one is not None
my_bool = field.one_to_one
assert field.many_to_many is not None
my_bool = field.many_to_many
assert field.many_to_one is not None
my_bool = field.many_to_one
out: |
main:6: error: Incompatible types in assignment (expression has type "Optional[bool]", variable has type "bool")
main:7: error: Incompatible types in assignment (expression has type "Optional[bool]", variable has type "bool")
main:8: error: Incompatible types in assignment (expression has type "Optional[bool]", variable has type "bool")
main:9: error: Incompatible types in assignment (expression has type "Optional[bool]", variable has type "bool")

View File

@@ -335,3 +335,25 @@
objects = MyManager()
class ChildUser(models.Model):
objects = MyManager()
- case: custom_manager_annotate_method_before_type_declaration
main: |
from myapp.models import ModelA, ModelB, ManagerA
reveal_type(ModelA.objects) # N: Revealed type is 'myapp.models.ModelA_ManagerA1[myapp.models.ModelA]'
reveal_type(ModelA.objects.do_something) # N: Revealed type is 'def (other_obj: myapp.models.ModelB) -> builtins.str'
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class ManagerA(models.Manager):
def do_something(self, other_obj: "ModelB") -> str:
return 'test'
class ModelA(models.Model):
title = models.TextField()
objects = ManagerA()
class ModelB(models.Model):
movie = models.TextField()

View File

@@ -46,6 +46,18 @@
reveal_type(self.get_form(form_class)) # N: Revealed type is 'main.MyForm'
reveal_type(self.get_form(MyForm2)) # N: Revealed type is 'main.MyForm2'
- case: updateview_form_valid_has_form_save
main: |
from django import forms
from django.views.generic.edit import UpdateView
class MyForm(forms.ModelForm):
pass
class MyView(UpdateView):
form_class = MyForm
def form_valid(self, form: forms.BaseModelForm):
reveal_type(form.save) # N: Revealed type is 'def (commit: builtins.bool =) -> Any'
- case: successmessagemixin_compatible_with_formmixin
main: |
from django.views.generic.edit import FormMixin

View File

@@ -27,3 +27,28 @@
reveal_type(request.user) # N: Revealed type is 'django.contrib.auth.models.User'
custom_settings: |
INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth')
- case: subclass_request_not_changed_user_type
disable_cache: true
main: |
from django.http.request import HttpRequest
class MyRequest(HttpRequest):
foo: int # Just do something
request = MyRequest()
reveal_type(request.user) # N: Revealed type is 'Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]'
custom_settings: |
INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth')
- case: subclass_request_changed_user_type
disable_cache: true
main: |
from django.http.request import HttpRequest
from django.contrib.auth.models import User
class MyRequest(HttpRequest):
user: User # Override the type of user
request = MyRequest()
reveal_type(request.user) # N: Revealed type is 'django.contrib.auth.models.User'
custom_settings: |
INSTALLED_APPS = ('django.contrib.contenttypes', 'django.contrib.auth')

View File

@@ -0,0 +1,73 @@
import tempfile
import typing
import pytest
from mypy_django_plugin.main import extract_django_settings_module
TEMPLATE = """usage: (config)
...
[mypy.plugins.django_stubs]
django_settings_module: str (required)
...
(django-stubs) mypy: error: 'django_settings_module' is not set: {}
"""
@pytest.mark.parametrize(
'config_file_contents,message_part',
[
pytest.param(
None,
'mypy config file is not specified or found',
id='missing-file',
),
pytest.param(
['[not-really-django-stubs]'],
'no section [mypy.plugins.django-stubs]',
id='missing-section',
),
pytest.param(
['[mypy.plugins.django-stubs]',
'\tnot_django_not_settings_module = badbadmodule'],
'the setting is not provided',
id='missing-settings-module',
),
pytest.param(
['[mypy.plugins.django-stubs]'],
'the setting is not provided',
id='no-settings-given',
),
],
)
def test_misconfiguration_handling(capsys, config_file_contents, message_part):
# type: (typing.Any, typing.List[str], str) -> None
"""Invalid configuration raises `SystemExit` with a precise error message."""
with tempfile.NamedTemporaryFile(mode='w+') as config_file:
if not config_file_contents:
config_file.close()
else:
config_file.write('\n'.join(config_file_contents).expandtabs(4))
config_file.seek(0)
with pytest.raises(SystemExit, match='2'):
extract_django_settings_module(config_file.name)
error_message = TEMPLATE.format(message_part)
assert error_message == capsys.readouterr().err
def test_correct_configuration() -> None:
"""Django settings module gets extracted given valid configuration."""
config_file_contents = [
'[mypy.plugins.django-stubs]',
'\tsome_other_setting = setting',
'\tdjango_settings_module = my.module',
]
with tempfile.NamedTemporaryFile(mode='w+') as config_file:
config_file.write('\n'.join(config_file_contents).expandtabs(4))
config_file.seek(0)
extracted = extract_django_settings_module(config_file.name)
assert extracted == 'my.module'