Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,7 @@ This happens because these Django classes do not support [`__class_getitem__`](h

### How can I create a HttpRequest that's guaranteed to have an authenticated user?

Django's built in [`HttpRequest`](https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.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.
Django's built in [`HttpRequest`](https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpRequest) has the attribute `user` that resolves to the type `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:

Expand Down
6 changes: 3 additions & 3 deletions django-stubs/db/models/fields/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):

.. code:: python

from typing import Generic, Union
from typing import Generic

class Model(object):
...

_SetType = Union[int, float] # You can assign ints and floats
_SetType = int | float # You can assign ints and floats
_GetType = int # access type is always `int`

class IntField(object):
Expand Down Expand Up @@ -100,7 +100,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
example.count = 1.5 # ok
example.count = 'a'
# Incompatible types in assignment
# (expression has type "str", variable has type "Union[int, float]")
# (expression has type "str", variable has type "int | float")

Notice, that this is not magic. This is how descriptors work with ``mypy``.

Expand Down
2 changes: 1 addition & 1 deletion django-stubs/forms/fields.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ from typing_extensions import Self, override

# Problem: attribute `widget` is always of type `Widget` after field instantiation.
# However, on class level it can be set to `Type[Widget]` too.
# If we annotate it as `Union[Widget, Type[Widget]]`, every code that uses field
# If we annotate it as `Widget | Type[Widget]`, every code that uses field
# instances will not typecheck.
# If we annotate it as `Widget`, any widget subclasses that do e.g.
# `widget = Select` will not typecheck.
Expand Down
2 changes: 1 addition & 1 deletion django-stubs/utils/formats.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def number_format(
_T = TypeVar("_T")

# Mypy considers this invalid (overlapping signatures), but thanks to implementation
# details it works as expected (all values from Union are `localize`d to str,
# details it works as expected (all values from the union are `localize`d to str,
# while type of others is preserved)
@overload
def localize(value: builtin_datetime | date | time | Decimal | float | str, use_l10n: bool | None = None) -> str: ...
Expand Down
2 changes: 1 addition & 1 deletion django-stubs/utils/functional.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class _Getter(Protocol[_Get]): # noqa: PYI046
"""
Type fake to declare some read-only properties (until `property` builtin is generic).

We can use something like `Union[_Getter[str], str]` in base class to avoid errors
We can use something like `_Getter[str] | str` in base class to avoid errors
when redefining attribute with property or property with attribute.
"""

Expand Down
2 changes: 2 additions & 0 deletions ext/django_stubs_ext/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from .aliases import QuerySetAny as QuerySetAny
from .aliases import StrOrPromise, StrPromise
from .aliases import ValuesQuerySet as ValuesQuerySet
Expand Down
2 changes: 2 additions & 0 deletions ext/django_stubs_ext/aliases.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import typing

if typing.TYPE_CHECKING:
Expand Down
2 changes: 2 additions & 0 deletions ext/django_stubs_ext/annotations.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Annotated, Any, Generic, TypeVar

Expand Down
2 changes: 2 additions & 0 deletions ext/django_stubs_ext/db/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
Expand Down
2 changes: 2 additions & 0 deletions ext/django_stubs_ext/db/models/manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from typing import TYPE_CHECKING

# Re-export stubs-only classes RelatedManger and ManyRelatedManager.
Expand Down
2 changes: 2 additions & 0 deletions ext/django_stubs_ext/db/router.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
Expand Down
8 changes: 6 additions & 2 deletions ext/django_stubs_ext/patch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import builtins
import logging
from collections.abc import Iterable
from typing import Any, Generic, TypeVar
from typing import TYPE_CHECKING, Any, Generic, TypeVar

from django import VERSION
from django.contrib.admin import ModelAdmin
Expand Down Expand Up @@ -33,6 +34,9 @@
from django.views.generic.list import MultipleObjectMixin
from typing_extensions import override

if TYPE_CHECKING:
from collections.abc import Iterable

__all__ = ["monkeypatch"]

logger = logging.getLogger(__name__)
Expand Down
8 changes: 6 additions & 2 deletions ext/django_stubs_ext/settings.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from pathlib import Path
from typing import Any, TypedDict, type_check_only
from __future__ import annotations

from typing import TYPE_CHECKING, Any, TypedDict, type_check_only

from typing_extensions import NotRequired

if TYPE_CHECKING:
from pathlib import Path


@type_check_only
class TemplatesSetting(TypedDict):
Expand Down
2 changes: 2 additions & 0 deletions ext/django_stubs_ext/types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from typing import Any, Protocol

from typing_extensions import override
Expand Down
2 changes: 2 additions & 0 deletions ext/tests/test_aliases.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from typing import Any

from django_stubs_ext import ValuesQuerySet
Expand Down
13 changes: 9 additions & 4 deletions ext/tests/test_monkeypatching.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
from __future__ import annotations

import builtins
from collections.abc import Iterable
from contextlib import suppress
from typing import Protocol
from typing import TYPE_CHECKING, Protocol

import pytest
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch
from django.db.models import Model
from django.forms.models import ModelForm

import django_stubs_ext
from django_stubs_ext import patch
from django_stubs_ext.patch import _need_generic, _VersionSpec

if TYPE_CHECKING:
from collections.abc import Iterable

from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch


class _MakeGenericClasses(Protocol):
"""Used to represent a type of ``make_generic_classes`` fixture."""
Expand Down
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ incremental = true
# TODO: add type args to all generics
disallow_any_generics = false
strict = true
allow_redefinition_new = true
local_partial_types = true
strict_equality_for_none = true
warn_unreachable = true

disable_error_code = empty-body
Expand All @@ -25,6 +27,7 @@ enable_error_code =
unimported-reveal,
unused-awaitable,

fixed_format_cache = true
show_traceback = true

plugins =
Expand Down
2 changes: 2 additions & 0 deletions mypy_django_plugin/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import configparser
import os
import sys
Expand Down
52 changes: 25 additions & 27 deletions mypy_django_plugin/django/context.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
from __future__ import annotations

import os
import sys
from collections import defaultdict
from collections.abc import Iterable, Iterator, Mapping, Sequence
from contextlib import contextmanager
from functools import cached_property
from typing import TYPE_CHECKING, Any, Literal, Union
from typing import TYPE_CHECKING, Any, Literal

from django.core.exceptions import FieldDoesNotExist, FieldError
from django.db import models
from django.db.models.base import Model
from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import Expression
from django.db.models.fields import AutoField, CharField, Field
from django.db.models.fields.related import ForeignKey, RelatedField
from django.db.models.fields.reverse_related import ForeignObjectRel
from django.db.models.lookups import Exact
from django.db.models.sql.query import Query
from mypy.checker import TypeChecker
from mypy.nodes import TypeInfo
from mypy.plugin import MethodContext
from mypy.typeanal import make_optional_type
from mypy.types import AnyType, Instance, ProperType, TypeOfAny, UnionType, get_proper_type
from mypy.types import Type as MypyType
Expand All @@ -36,9 +33,15 @@ class ArrayField: # type: ignore[no-redef]


if TYPE_CHECKING:
from collections.abc import Iterable, Iterator, Mapping, Sequence

from django.apps.registry import Apps
from django.conf import LazySettings
from django.db.models.expressions import Expression
from django.db.models.options import _AnyField
from mypy.checker import TypeChecker
from mypy.nodes import TypeInfo
from mypy.plugin import MethodContext


@contextmanager
Expand All @@ -52,7 +55,7 @@ def temp_environ() -> Iterator[None]:
os.environ.update(environ)


def initialize_django(settings_module: str) -> tuple["Apps", "LazySettings"]:
def initialize_django(settings_module: str) -> tuple[Apps, LazySettings]:
with temp_environ():
os.environ["DJANGO_SETTINGS_MODULE"] = settings_module

Expand Down Expand Up @@ -133,17 +136,17 @@ def get_model_class_by_fullname(self, fullname: str) -> type[Model] | None:
module, _, model_cls_name = fullname.rpartition(".")
return self.model_modules.get(module, {}).get(model_cls_name)

def get_model_fields(self, model_cls: type[Model]) -> Iterator["Field[Any, Any]"]:
def get_model_fields(self, model_cls: type[Model]) -> Iterator[Field[Any, Any]]:
for field in model_cls._meta.get_fields():
if isinstance(field, Field):
yield field

def get_model_foreign_keys(self, model_cls: type[Model]) -> Iterator["ForeignKey[Any, Any]"]:
def get_model_foreign_keys(self, model_cls: type[Model]) -> Iterator[ForeignKey[Any, Any]]:
for field in model_cls._meta.get_fields():
if isinstance(field, ForeignKey):
yield field

def get_model_related_fields(self, model_cls: type[Model]) -> Iterator["RelatedField[Any, Any]"]:
def get_model_related_fields(self, model_cls: type[Model]) -> Iterator[RelatedField[Any, Any]]:
"""Get model forward relations"""
for field in model_cls._meta.get_fields():
if isinstance(field, RelatedField):
Expand All @@ -155,9 +158,7 @@ def get_model_relations(self, model_cls: type[Model]) -> Iterator[ForeignObjectR
if isinstance(field, ForeignObjectRel):
yield field

def get_field_lookup_exact_type(
self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel]
) -> MypyType:
def get_field_lookup_exact_type(self, api: TypeChecker, field: Field[Any, Any] | ForeignObjectRel) -> MypyType:
if isinstance(field, RelatedField | ForeignObjectRel):
related_model_cls = self.get_field_related_model_cls(field)
rel_model_info = helpers.lookup_class_typeinfo(api, related_model_cls)
Expand All @@ -176,8 +177,8 @@ def get_field_lookup_exact_type(
return helpers.get_private_descriptor_type(field_info, "_pyi_lookup_exact_type", is_nullable=field.null)

def get_related_target_field(
self, related_model_cls: type[Model], field: "ForeignKey[Any, Any]"
) -> "Field[Any, Any] | None":
self, related_model_cls: type[Model], field: ForeignKey[Any, Any]
) -> Field[Any, Any] | None:
# ForeignKey only supports one `to_fields` item (ForeignObject supports many)
assert len(field.to_fields) == 1
to_field_name = field.to_fields[0]
Expand All @@ -188,7 +189,7 @@ def get_related_target_field(
return rel_field
return self.get_primary_key_field(related_model_cls)

def get_primary_key_field(self, model_cls: type[Model]) -> "Field[Any, Any]":
def get_primary_key_field(self, model_cls: type[Model]) -> Field[Any, Any]:
for field in model_cls._meta.get_fields():
if isinstance(field, Field):
if field.primary_key:
Expand Down Expand Up @@ -289,7 +290,7 @@ def model_class_fullnames_by_label(self) -> Mapping[str, str]:
if klass is not models.Model
}

def get_field_nullability(self, field: Union["Field[Any, Any]", ForeignObjectRel], method: str | None) -> bool:
def get_field_nullability(self, field: Field[Any, Any] | ForeignObjectRel, method: str | None) -> bool:
if method in ("values", "values_list"):
return field.null

Expand All @@ -307,7 +308,7 @@ def get_field_nullability(self, field: Union["Field[Any, Any]", ForeignObjectRel
return nullable

def get_field_set_type(
self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel], *, method: str
self, api: TypeChecker, field: Field[Any, Any] | ForeignObjectRel, *, method: str
) -> MypyType:
"""Get a type of __set__ for this specific Django field."""
target_field = field
Expand Down Expand Up @@ -335,7 +336,7 @@ def get_field_get_type(
self,
api: TypeChecker,
model_info: TypeInfo | None,
field: Union["Field[Any, Any]", ForeignObjectRel],
field: Field[Any, Any] | ForeignObjectRel,
*,
method: str,
) -> MypyType:
Expand Down Expand Up @@ -365,15 +366,12 @@ def get_field_get_type(
return Instance(model_info, [])
return helpers.get_private_descriptor_type(field_info, "_pyi_private_get_type", is_nullable=is_nullable)

def get_field_related_model_cls(self, field: Union["RelatedField[Any, Any]", ForeignObjectRel]) -> type[Model]:
def get_field_related_model_cls(self, field: RelatedField[Any, Any] | ForeignObjectRel) -> type[Model]:
if isinstance(field, RelatedField):
related_model_cls = field.remote_field.model
else:
related_model_cls = field.field.model

if related_model_cls is None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case was added in PR #1956 to fix issue #1953 - shouldn't it be kept with # type: ignore[comparison-overlap] instead of removed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Probably. This likely indicates that the stubs are wrong somewhere.

raise UnregisteredModelError

if isinstance(related_model_cls, str):
if related_model_cls == "self": # type: ignore[unreachable]
# same model
Expand All @@ -394,7 +392,7 @@ def get_field_related_model_cls(self, field: Union["RelatedField[Any, Any]", For

def _resolve_field_from_parts(
self, field_parts: Iterable[str], model_cls: type[Model]
) -> tuple[Union["Field[Any, Any]", ForeignObjectRel], type[Model]]:
) -> tuple[Field[Any, Any] | ForeignObjectRel, type[Model]]:
currently_observed_model = model_cls
field: _AnyField | None = None
for field_part in field_parts:
Expand All @@ -420,7 +418,7 @@ def solve_lookup_type(
self, model_cls: type[Model], lookup: str
) -> tuple[Sequence[str], Sequence[str], Expression | Literal[False]] | None:
query = Query(model_cls)
if (lookup == "pk" or lookup.startswith("pk__")) and query.get_meta().pk is None:
if (lookup == "pk" or lookup.startswith("pk__")) and query.get_meta().pk is None: # type: ignore[comparison-overlap]
# Primary key lookup when no primary key field is found, model is presumably
# abstract and we can't say anything about 'pk'.
return None
Comment on lines +421 to 424
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is a little tricky because of the change in e3f5b3d. I decided to punt on removing this block for now - we can follow up later.

Expand Down Expand Up @@ -452,7 +450,7 @@ def solve_lookup_type(

def resolve_lookup_into_field(
self, model_cls: type[Model], lookup: str
) -> tuple[Union["Field[Any, Any]", ForeignObjectRel, None], type[Model]]:
) -> tuple[Field[Any, Any] | ForeignObjectRel | None, type[Model]]:
solved_lookup = self.solve_lookup_type(model_cls, lookup)
if solved_lookup is None:
return None, model_cls
Expand All @@ -462,7 +460,7 @@ def resolve_lookup_into_field(
return self._resolve_field_from_parts(field_parts, model_cls)

def _resolve_lookup_type_from_lookup_class(
self, ctx: MethodContext, lookup_cls: type, field: Union["Field[Any, Any]", ForeignObjectRel] | None = None
self, ctx: MethodContext, lookup_cls: type, field: Field[Any, Any] | ForeignObjectRel | None = None
) -> MypyType | None:
"""Resolve the expected type for a lookup class (used both for regular fields and annotated fields)

Expand Down
2 changes: 2 additions & 0 deletions mypy_django_plugin/errorcodes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from mypy.errorcodes import ErrorCode

MANAGER_MISSING = ErrorCode("django-manager-missing", "Couldn't resolve manager for model", "Django")
3 changes: 3 additions & 0 deletions mypy_django_plugin/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
from __future__ import annotations


class UnregisteredModelError(Exception):
"""The requested model is not registered"""
Loading