Skip to content

Commit c36ba71

Browse files
authored
Merge branch 'master' into feat/builtin-models-types
2 parents 60aa790 + 022237e commit c36ba71

File tree

96 files changed

+929
-597
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

96 files changed

+929
-597
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
6969
# Must match `shard` definition in the test matrix:
7070
- name: Run pytest tests
71-
run: PYTHONPATH='.' pytest --num-shards=4 --shard-id=${{ matrix.shard }} tests
71+
run: PYTHONPATH='.' pytest --num-shards=4 --shard-id=${{ matrix.shard }} -n auto tests
7272
- name: Run mypy on the test cases
7373
run: mypy --strict tests/assert_type
7474

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ repos:
1717
args: [--fix=lf]
1818
- id: check-case-conflict
1919
- repo: https://github.com/astral-sh/ruff-pre-commit
20-
rev: v0.9.10
20+
rev: v0.11.2
2121
hooks:
2222
- id: ruff
2323
args: ["--fix", "--exit-non-zero-on-fix"]

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ You can also run pre-commit per file or for a specific path, simply replace "--a
6868
To execute the unit tests, simply run:
6969

7070
```bash
71-
pytest
71+
pytest -n auto
7272
```
7373

7474
If you get some unexpected results or want to be sure that tests run is not affected by previous one, remove `mypy` cache:

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,60 @@ def assert_zero_count(model_type: type[models.Model]) -> None:
340340
```
341341

342342

343+
### How to type a custom `models.Field`?
344+
345+
> [!NOTE]
346+
> This require type generic support, see <a href="#i-cannot-use-queryset-or-manager-with-type-annotations">this section</a> to enable it.
347+
348+
349+
Django `models.Field` (and subclasses) are generic types with two parameters:
350+
- `_ST`: type that can be used when setting a value
351+
- `_GT`: type that will be returned when getting a value
352+
353+
When you create a subclass, you have two options depending on how strict you want
354+
the type to be for consumers of your custom field.
355+
356+
1. Generic subclass:
357+
358+
```python
359+
from typing import TypeVar, reveal_type
360+
from django.db import models
361+
362+
_ST = TypeVar("_ST", contravariant=True)
363+
_GT = TypeVar("_GT", covariant=True)
364+
365+
class MyIntegerField(models.IntegerField[_ST, _GT]):
366+
...
367+
368+
class User(models.Model):
369+
my_field = MyIntegerField()
370+
371+
372+
reveal_type(User().my_field) # N: Revealed type is "int"
373+
User().my_field = "12" # OK (because Django IntegerField allows str and will try to coerce it)
374+
```
375+
376+
2. Non-generic subclass (more strict):
377+
378+
```python
379+
from typing import reveal_type
380+
from django.db import models
381+
382+
# This is a non-generic subclass being very explicit
383+
# that it expects only int when setting values.
384+
class MyStrictIntegerField(models.IntegerField[int, int]):
385+
...
386+
387+
class User(models.Model):
388+
my_field = MyStrictIntegerField()
389+
390+
391+
reveal_type(User().my_field) # N: Revealed type is "int"
392+
User().my_field = "12" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
393+
```
394+
395+
See mypy section on [generic classes subclasses](https://mypy.readthedocs.io/en/stable/generics.html#defining-subclasses-of-generic-classes).
396+
343397
## Related projects
344398

345399
- [`awesome-python-typing`](https://github.com/typeddjango/awesome-python-typing) - Awesome list of all typing-related things in Python.

django-stubs/contrib/admin/decorators.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,29 @@ _F = TypeVar("_F", bound=Callable[..., Any])
1313
@overload
1414
def action(
1515
function: _F,
16+
*,
1617
permissions: Sequence[str] | None = ...,
1718
description: _StrOrPromise | None = ...,
1819
) -> _F: ...
1920
@overload
2021
def action(
22+
function: None = None,
2123
*,
2224
permissions: Sequence[str] | None = ...,
2325
description: _StrOrPromise | None = ...,
2426
) -> Callable[[_F], _F]: ...
2527
@overload
2628
def display(
2729
function: _F,
30+
*,
2831
boolean: bool | None = ...,
2932
ordering: str | Combinable | BaseExpression | None = ...,
3033
description: _StrOrPromise | None = ...,
3134
empty_value: str | None = ...,
3235
) -> _F: ...
3336
@overload
3437
def display(
38+
function: None = None,
3539
*,
3640
boolean: bool | None = ...,
3741
ordering: str | Combinable | BaseExpression | None = ...,

django-stubs/contrib/admin/helpers.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ from django.forms.boundfield import BoundField
1010
from django.forms.models import ModelForm
1111
from django.forms.utils import ErrorDict, ErrorList
1212
from django.forms.widgets import Media, Widget
13+
from django.utils.functional import cached_property
1314
from django.utils.safestring import SafeString
1415
from typing_extensions import TypedDict
1516

@@ -62,6 +63,8 @@ class Fieldset:
6263
description: str | None = ...,
6364
model_admin: ModelAdmin | None = ...,
6465
) -> None: ...
66+
@cached_property
67+
def is_collapsible(self) -> bool: ...
6568
@property
6669
def media(self) -> Media: ...
6770
def __iter__(self) -> Iterator[Fieldline]: ...
@@ -146,6 +149,8 @@ class InlineAdminFormSet:
146149
def inline_formset_data(self) -> str: ...
147150
@property
148151
def forms(self) -> list[BaseForm]: ...
152+
@cached_property
153+
def is_collapsible(self) -> bool: ...
149154
def non_form_errors(self) -> ErrorList: ...
150155
@property
151156
def media(self) -> Media: ...

django-stubs/contrib/admin/options.pyi

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class IncorrectLookupParameters(Exception): ...
5656
FORMFIELD_FOR_DBFIELD_DEFAULTS: Any
5757
csrf_protect_m: Any
5858

59-
_FieldGroups: TypeAlias = Sequence[str | Sequence[str]]
59+
_FieldGroups: TypeAlias = _ListOrTuple[str | _ListOrTuple[str]]
6060

6161
@type_check_only
6262
class _OptionalFieldOpts(TypedDict, total=False):
@@ -67,9 +67,6 @@ class _OptionalFieldOpts(TypedDict, total=False):
6767
class _FieldOpts(_OptionalFieldOpts, total=True):
6868
fields: _FieldGroups
6969

70-
# Workaround for mypy issue, a Sequence type should be preferred here.
71-
# https://github.com/python/mypy/issues/8921
72-
# _FieldsetSpec = Sequence[Tuple[Optional[str], _FieldOpts]]
7370
_FieldsetSpec: TypeAlias = _ListOrTuple[tuple[_StrOrPromise | None, _FieldOpts]]
7471
_ListFilterT: TypeAlias = (
7572
type[ListFilter]
@@ -82,7 +79,8 @@ _ListFilterT: TypeAlias = (
8279
# Generic type specifically for models, for use in BaseModelAdmin and subclasses
8380
# https://github.com/typeddjango/django-stubs/issues/482
8481
_ModelT = TypeVar("_ModelT", bound=Model)
85-
_DisplayT: TypeAlias = _ListOrTuple[str | Callable[[_ModelT], str | bool]]
82+
_DisplayT: TypeAlias = str | Callable[[_ModelT], str | bool]
83+
_ListDisplayT: TypeAlias = _ListOrTuple[_DisplayT[_ModelT]]
8684

8785
# Options `form`, `list_display`, `list_display_links` and `actions` are not marked as `ClassVar` due to the
8886
# limitations of the current type system: `ClassVar` cannot contain type variables.
@@ -125,12 +123,12 @@ class BaseModelAdmin(Generic[_ModelT]):
125123
def get_exclude(self, request: HttpRequest, obj: _ModelT | None = ...) -> _ListOrTuple[str] | None: ...
126124
def get_fields(self, request: HttpRequest, obj: _ModelT | None = ...) -> _FieldGroups: ...
127125
def get_fieldsets(self, request: HttpRequest, obj: _ModelT | None = ...) -> _FieldsetSpec: ...
128-
def get_inlines(self, request: HttpRequest, obj: _ModelT | None) -> list[type[InlineModelAdmin]]: ...
126+
def get_inlines(self, request: HttpRequest, obj: _ModelT | None) -> _ListOrTuple[type[InlineModelAdmin]]: ...
129127
def get_ordering(self, request: HttpRequest) -> _ListOrTuple[str]: ...
130128
def get_readonly_fields(self, request: HttpRequest, obj: _ModelT | None = ...) -> _ListOrTuple[str]: ...
131129
def get_prepopulated_fields(self, request: HttpRequest, obj: _ModelT | None = ...) -> dict[str, Sequence[str]]: ...
132130
def get_queryset(self, request: HttpRequest) -> QuerySet[_ModelT]: ...
133-
def get_sortable_by(self, request: HttpRequest) -> _DisplayT[_ModelT]: ...
131+
def get_sortable_by(self, request: HttpRequest) -> _ListDisplayT[_ModelT]: ...
134132
@overload
135133
@deprecated("None value for the request parameter will be removed in Django 6.0.")
136134
def lookup_allowed(self, lookup: str, value: str, request: None = None) -> bool: ...
@@ -150,8 +148,8 @@ _ModelAdmin = TypeVar("_ModelAdmin", bound=ModelAdmin[Any])
150148
_ActionCallable: TypeAlias = Callable[[_ModelAdmin, HttpRequest, QuerySet[_ModelT]], HttpResponseBase | None]
151149

152150
class ModelAdmin(BaseModelAdmin[_ModelT]):
153-
list_display: _DisplayT[_ModelT]
154-
list_display_links: _DisplayT[_ModelT] | None
151+
list_display: _ListDisplayT[_ModelT]
152+
list_display_links: _ListDisplayT[_ModelT] | None
155153
list_filter: ClassVar[_ListOrTuple[_ListFilterT]]
156154
list_select_related: ClassVar[bool | _ListOrTuple[str]]
157155
list_per_page: ClassVar[int]
@@ -220,8 +218,10 @@ class ModelAdmin(BaseModelAdmin[_ModelT]):
220218
self, request: HttpRequest, default_choices: list[tuple[str, str]] = ...
221219
) -> list[tuple[str, str]]: ...
222220
def get_action(self, action: Callable | str) -> tuple[Callable[..., str], str, str] | None: ...
223-
def get_list_display(self, request: HttpRequest) -> _DisplayT[_ModelT]: ...
224-
def get_list_display_links(self, request: HttpRequest, list_display: _DisplayT[_ModelT]) -> _DisplayT[_ModelT]: ...
221+
def get_list_display(self, request: HttpRequest) -> _ListDisplayT[_ModelT]: ...
222+
def get_list_display_links(
223+
self, request: HttpRequest, list_display: _ListDisplayT[_ModelT]
224+
) -> _ListDisplayT[_ModelT]: ...
225225
def get_list_filter(self, request: HttpRequest) -> _ListOrTuple[_ListFilterT]: ...
226226
def get_list_select_related(self, request: HttpRequest) -> bool | _ListOrTuple[str]: ...
227227
def get_search_fields(self, request: HttpRequest) -> _ListOrTuple[str]: ...

django-stubs/contrib/admin/tests.pyi

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from collections.abc import Callable, Generator
2-
from contextlib import contextmanager
1+
from collections.abc import Callable
2+
from contextlib import AbstractContextManager
33
from typing import Any
44

55
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
@@ -17,8 +17,7 @@ class AdminSeleniumTestCase(SeleniumTestCase, StaticLiveServerTestCase):
1717
def wait_until_visible(self, css_selector: str, timeout: int = ...) -> None: ...
1818
def wait_until_invisible(self, css_selector: str, timeout: int = ...) -> None: ...
1919
def wait_page_ready(self, timeout: int = ...) -> None: ...
20-
@contextmanager
21-
def wait_page_loaded(self, timeout: int = ...) -> Generator[None, None, None]: ...
20+
def wait_page_loaded(self, timeout: int = ...) -> AbstractContextManager[None]: ...
2221
def admin_login(self, username: str, password: str, login_url: str = ...) -> None: ...
2322
def select_option(self, selector: str, value: Any) -> None: ...
2423
def deselect_option(self, selector: str, value: Any) -> None: ...

django-stubs/contrib/admin/utils.pyi

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ from typing import Any, Literal, TypeVar, overload, type_check_only
55
from uuid import UUID
66

77
from _typeshed import Unused
8-
from django.contrib.admin.options import BaseModelAdmin
8+
from django.contrib.admin.options import BaseModelAdmin, _DisplayT, _FieldGroups, _FieldsetSpec, _ListDisplayT, _ModelT
99
from django.contrib.admin.sites import AdminSite
1010
from django.db.models.base import Model
1111
from django.db.models.deletion import Collector
@@ -31,8 +31,11 @@ def prepare_lookup_value(
3131
def build_q_object_from_lookup_parameters(parameters: dict[str, list[str]]) -> Q: ...
3232
def quote(s: int | str | UUID) -> str: ...
3333
def unquote(s: str) -> str: ...
34-
def flatten(fields: Any) -> list[Callable | str]: ...
35-
def flatten_fieldsets(fieldsets: Any) -> list[Callable | str]: ...
34+
@overload
35+
def flatten(fields: _FieldGroups) -> list[str]: ...
36+
@overload
37+
def flatten(fields: _ListDisplayT[_ModelT]) -> list[_DisplayT[_ModelT]]: ...
38+
def flatten_fieldsets(fieldsets: _FieldsetSpec) -> list[str]: ...
3639
def get_deleted_objects(
3740
objs: Sequence[Model | None] | QuerySet[Model], request: HttpRequest, admin_site: AdminSite
3841
) -> tuple[list[str], dict[str, int], set[str], list[str]]: ...

django-stubs/contrib/admin/views/main.pyi

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from typing import Any, Literal
33

44
from django import forms
55
from django.contrib.admin.filters import ListFilter
6-
from django.contrib.admin.options import ModelAdmin, _DisplayT, _ListFilterT
6+
from django.contrib.admin.options import ModelAdmin, _ListDisplayT, _ListFilterT
77
from django.db.models.base import Model
88
from django.db.models.expressions import Expression
99
from django.db.models.options import Options
@@ -26,8 +26,8 @@ class ChangeList:
2626
opts: Options
2727
lookup_opts: Options
2828
root_queryset: QuerySet
29-
list_display: _DisplayT
30-
list_display_links: _DisplayT
29+
list_display: _ListDisplayT
30+
list_display_links: _ListDisplayT
3131
list_filter: Sequence[_ListFilterT]
3232
date_hierarchy: Any
3333
search_fields: Sequence[str]
@@ -58,8 +58,8 @@ class ChangeList:
5858
self,
5959
request: HttpRequest,
6060
model: type[Model],
61-
list_display: _DisplayT,
62-
list_display_links: _DisplayT,
61+
list_display: _ListDisplayT,
62+
list_display_links: _ListDisplayT,
6363
list_filter: Sequence[_ListFilterT],
6464
date_hierarchy: str | None,
6565
search_fields: Sequence[str],

0 commit comments

Comments
 (0)