1 Commits

Author SHA1 Message Date
Dave Halter
fd057010f6 Model.objects should be a ClassVar
Model.objects is only available on classes and while we cannot force this, without ClassVar it's kind of only available on instances.

I have noticed this when I was writing a Jedi plugin that makes django-stubs work properly.
2020-06-11 23:21:10 +02:00
39 changed files with 107 additions and 375 deletions

View File

@@ -47,9 +47,8 @@ We rely on different `django` and `mypy` versions:
| django-stubs | mypy version | django version | python version
| ------------ | ---- | ---- | ---- |
| 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.5.0 | 0.780 | 2.2.x \|\| 3.x | ^3.6
| 1.4.0 | 0.770 | 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
@@ -90,24 +89,19 @@ 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 create a HttpRequest that's guaranteed to have an authenticated user?
### How can I use HttpRequest with custom user model?
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.
You can subclass standard request like so:
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 AuthenticatedHttpRequest(HttpRequest):
class MyRequest(HttpRequest):
user: MyUser
```
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.
And then use `MyRequest` instead of standard `HttpRequest` inside your project.
## Related projects

View File

@@ -1,6 +1,6 @@
black
pytest-mypy-plugins==1.3.0
psycopg2-binary
psycopg2
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[Union[Callable[[ModelAdmin, HttpRequest, QuerySet], None], str]] = ...
actions: Sequence[Callable[[ModelAdmin, HttpRequest, QuerySet], None]] = ...
action_form: Any = ...
actions_on_top: bool = ...
actions_on_bottom: bool = ...

View File

@@ -1,11 +1,7 @@
from typing import Callable, Optional, TypeVar, overload
from typing import Callable, TypeVar, overload
_C = TypeVar("_C", bound=Callable)
@overload
def staff_member_required(
view_func: _C = ..., redirect_field_name: Optional[str] = ..., login_url: str = ...
) -> _C: ...
def staff_member_required(view_func: _C = ..., redirect_field_name: str = ..., login_url: str = ...) -> _C: ...
@overload
def staff_member_required(
view_func: None = ..., redirect_field_name: Optional[str] = ..., login_url: str = ...
) -> Callable: ...
def staff_member_required(view_func: None = ..., redirect_field_name: 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 AnonymousUser
from django.contrib.auth.models import AbstractUser, 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: HttpRequest, user: AbstractBaseUser) -> None: ...
def update_session_auth_hash(request: WSGIRequest, user: AbstractUser) -> 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 User
from django.contrib.auth.models import AbstractUser, 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: AbstractBaseUser, *args: Any, **kwargs: Any) -> None: ...
def __init__(self, user: AbstractUser, *args: Any, **kwargs: Any) -> None: ...
def clean_password2(self) -> str: ...
def save(self, commit: bool = ...) -> AbstractBaseUser: ...
def save(self, commit: bool = ...) -> AbstractUser: ...

View File

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

View File

@@ -29,8 +29,8 @@ class Paginator:
orphans: int = ...,
allow_empty_first_page: bool = ...,
) -> None: ...
def validate_number(self, number: Optional[Union[int, float, str]]) -> int: ...
def get_page(self, number: Optional[Union[int, float, str]]) -> Page: ...
def validate_number(self, number: Optional[Union[float, str]]) -> int: ...
def get_page(self, number: Optional[int]) -> 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, Collection, Dict, Iterable, List, Optional, Set, Tuple, Type, TypeVar, Union
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union, Collection, ClassVar
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: BaseManager[Any]
objects: ClassVar[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[Iterable[str]] = ...,
update_fields: Optional[Union[Sequence[str], 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[Iterable[str]] = ...,
update_fields: Optional[Union[Sequence[str], str]] = ...,
): ...
def refresh_from_db(self: _Self, using: Optional[str] = ..., fields: Optional[List[str]] = ...) -> None: ...
def get_deferred_fields(self) -> Set[str]: ...

View File

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

View File

@@ -10,10 +10,6 @@ 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):
@@ -22,9 +18,7 @@ class _IntegerChoicesMeta(ChoicesMeta):
labels: List[str] = ...
values: List[int] = ...
class IntegerChoices(int, Choices, metaclass=_IntegerChoicesMeta):
@property
def value(self) -> int: ...
class IntegerChoices(int, Choices, metaclass=_IntegerChoicesMeta): ...
# fake
class _TextChoicesMeta(ChoicesMeta):
@@ -33,6 +27,4 @@ class _TextChoicesMeta(ChoicesMeta):
labels: List[str] = ...
values: List[str] = ...
class TextChoices(str, Choices, metaclass=_TextChoicesMeta):
@property
def value(self) -> str: ...
class TextChoices(str, Choices, metaclass=_TextChoicesMeta): ...

View File

@@ -1,5 +1,4 @@
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
@@ -16,8 +15,6 @@ class SQLiteNumericMixin:
_Self = TypeVar("_Self")
_Numeric = Union[float, Decimal]
class Combinable:
ADD: str = ...
SUB: str = ...
@@ -30,25 +27,25 @@ class Combinable:
BITLEFTSHIFT: str = ...
BITRIGHTSHIFT: str = ...
def __neg__(self: _Self) -> _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 __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 __mod__(self: _Self, other: Union[int, Combinable]) -> _Self: ...
def __pow__(self: _Self, other: Union[_Numeric, Combinable]) -> _Self: ...
def __pow__(self: _Self, other: Union[float, 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, _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 __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 __rmod__(self, other: Union[int, Combinable]) -> Combinable: ...
def __rpow__(self, other: Union[_Numeric, Combinable]) -> Combinable: ...
def __rpow__(self, other: Union[float, Combinable]) -> Combinable: ...
def __rand__(self, other: Any) -> Combinable: ...
def __ror__(self, other: Any) -> Combinable: ...

View File

@@ -56,10 +56,6 @@ 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[Union[Storage, Callable[[], Storage]]] = ...,
storage: Optional[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: Sequence[str] = ...) -> _QS: ...
def select_for_update(self: _QS, nowait: bool = ..., skip_locked: bool = ..., of: Tuple = ...) -> _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: Union[Type[Widget], Widget] = ...
widget: Type[Widget] = ...
hidden_widget: Any = ...
default_validators: Any = ...
default_error_messages: Any = ...

View File

@@ -70,12 +70,7 @@ 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,13 +83,12 @@ 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) -> bytes: ...
def getvalue(self) -> Any: ...
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, Iterable, Optional, Dict
from typing import Any, List, 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) -> Iterable[Origin]: ...
def get_template_sources(self, template_name: str) -> None: ...
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.base_user import AbstractBaseUser
from django.contrib.auth.models import AbstractUser
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: AbstractBaseUser, backend: Optional[str] = ...) -> None: ...
def force_login(self, user: AbstractUser, backend: Optional[str] = ...) -> None: ...
def logout(self) -> None: ...
def conditional_content_removal(request: HttpRequest, response: HttpResponseBase) -> HttpResponse: ...

View File

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

View File

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

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() -> str: ...
def get_language() -> Optional[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,35 +1,28 @@
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 AbstractFormMixin(ContextMixin):
class FormMixin(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_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 get_form_kwargs(self) -> Dict[str, Any]: ...
def get_success_url(self) -> str: ...
def form_valid(self, form: BaseForm) -> HttpResponse: ...
def form_invalid(self, form: BaseForm) -> HttpResponse: ...
class ModelFormMixin(AbstractFormMixin, SingleObjectMixin):
class ModelFormMixin(FormMixin, 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,11 +311,7 @@ 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 = []
try:
arguments = method_node.arguments[1:]
except AttributeError:
arguments = []
for argument in arguments:
for argument in method_node.arguments[1:]:
argument.type_annotation = AnyType(TypeOfAny.unannotated)
prepared_arguments.append(argument)
return_type = AnyType(TypeOfAny.unannotated)
@@ -347,7 +343,6 @@ 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):
@@ -357,10 +352,6 @@ 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,8 +1,9 @@
import configparser
from functools import partial
from typing import Callable, Dict, List, NoReturn, Optional, Tuple, cast
from typing import Callable, Dict, List, Optional, Tuple
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 (
@@ -51,40 +52,25 @@ def add_new_manager_base(ctx: ClassDefContext) -> None:
def extract_django_settings_module(config_file_path: Optional[str]) -> str:
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])
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()
parser = configparser.ConfigParser()
try:
parser.read_file(open(cast(str, config_file_path), 'r'), source=config_file_path)
except (IsADirectoryError, OSError):
exit(1)
parser.read(config_file_path) # type: ignore
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('\'"')
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
class NewSemanalDjangoPlugin(Plugin):

View File

@@ -8,22 +8,6 @@ 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)
@@ -31,4 +15,12 @@ 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,7 +1,5 @@
[pytest]
testpaths =
./test-plugin
./test-data
testpaths = ./test-data
addopts =
--tb=native
-s

View File

@@ -451,11 +451,9 @@ 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': [
'No overload variant of "path" matches argument types "str", "None"',
'List or tuple expected as variable arguments',
'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', '8093aaa8ff9dd7386a069c6eb49fcc1c5980c033'),
'3.0': ('stable/3.0.x', '44da7abda848f05caaed74f6a749038c87dedfda')
'2.2': ('stable/2.2.x', '996be04c3ceb456754d9d527d4d708f30727f07e'),
'3.0': ('stable/3.0.x', 'd9f1792c7649e9f946f4a3a35a76bddf5a412b8b')
}
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,42 +17,38 @@ 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.782,<0.790",
"typing-extensions",
"django",
'mypy>=0.780,<0.790',
'typing-extensions',
'django',
]
setup(
name="django-stubs",
version="1.6.0",
description="Mypy stubs for Django",
version="1.5.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",
"Framework :: Django",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
"Typing :: Typed",
'Development Status :: 3 - Alpha',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8'
],
project_urls={
"Release notes": "https://github.com/typeddjango/django-stubs/releases",
'Release notes': 'https://github.com/typeddjango/django-stubs/releases',
},
)

View File

@@ -65,15 +65,11 @@
delete_selected_confirmation_template = "template"
object_history_template = "template"
popup_response_template = "template"
actions = (an_action, "a_method_action")
actions = (an_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
@@ -127,4 +123,4 @@
pass
class A(admin.ModelAdmin):
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[ModelAdmin, HttpRequest, QuerySet[Any]], None], str]"
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Callable[[ModelAdmin, HttpRequest, QuerySet[Any]], None]"

View File

@@ -1,26 +0,0 @@
- 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,25 +335,3 @@
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,18 +46,6 @@
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,28 +27,3 @@
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

@@ -1,73 +0,0 @@
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'