mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-10 05:51:53 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
645ee97e78 | ||
|
|
5bc3759ea2 | ||
|
|
534a028ea2 | ||
|
|
87856754ea | ||
|
|
2f7fac2eaf | ||
|
|
5ff99fd047 | ||
|
|
f77ebcd22c | ||
|
|
34b126e3da | ||
|
|
6e5f5f2cdb | ||
|
|
95252cde60 | ||
|
|
6ef2cf0331 | ||
|
|
9f3b95841b | ||
|
|
e764b1cf4c | ||
|
|
8a64d87917 | ||
|
|
60f3f9dd9f | ||
|
|
ca10ee9242 | ||
|
|
f651f27ddf | ||
|
|
3915aa0639 | ||
|
|
97ec2ee43b | ||
|
|
19c73a106d | ||
|
|
92ef5d9d95 | ||
|
|
f16d1b8cb6 | ||
|
|
c3cdc1c2d5 | ||
|
|
3704d0ab98 | ||
|
|
b1d619edb2 | ||
|
|
e680326c72 | ||
|
|
574a87e68c | ||
|
|
82ae1751ed | ||
|
|
69042783b1 | ||
|
|
391bbc59d5 | ||
|
|
28c76df3b2 |
18
README.md
18
README.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ...
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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]: ...
|
||||
|
||||
1
django-stubs/db/models/constants.pyi
Normal file
1
django-stubs/db/models/constants.pyi
Normal file
@@ -0,0 +1 @@
|
||||
LOOKUP_SEP: str = ...
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] = ...,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ...
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]: ...
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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, [])])
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
[pytest]
|
||||
testpaths = ./test-data
|
||||
testpaths =
|
||||
./test-plugin
|
||||
./test-data
|
||||
addopts =
|
||||
--tb=native
|
||||
-s
|
||||
|
||||
@@ -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"'
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
40
setup.py
40
setup.py
@@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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]"
|
||||
|
||||
26
test-data/typecheck/db/models/test_init.yml
Normal file
26
test-data/typecheck/db/models/test_init.yml
Normal 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")
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
73
test-plugin/test_error_handling.py
Normal file
73
test-plugin/test_error_handling.py
Normal 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'
|
||||
Reference in New Issue
Block a user