Skip to content

Commit e196985

Browse files
Fix signature of Choices member creation, add assert_type test cases, run pyright (#2162)
* Fix signature of Choices member creation * Add comment regarding overloads * Add pyright to CI, add test * Run mypy on the new test cases * Add more assertions, rename test folder * Update to `pyright==1.1.364` * Add `.gitattributes` for correct syntax highlighting * Python compat * [pre-commit.ci] auto fixes from pre-commit.com hooks * type ignore comments compatibility between pyright and mypy --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent d03eaf1 commit e196985

File tree

8 files changed

+121
-30
lines changed

8 files changed

+121
-30
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
scripts/allowlist_*.txt linguist-language=ini
2+
pyrightconfig*.json linguist-language=jsonc

.github/workflows/test.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ jobs:
6767
SETUPTOOLS_ENABLE_FEATURES=legacy-editable pip install -r ./requirements.txt
6868
6969
# Must match `shard` definition in the test matrix:
70-
- name: Run tests
70+
- name: Run pytest tests
7171
run: PYTHONPATH='.' pytest --num-shards=4 --shard-id=${{ matrix.shard }} tests
72+
- name: Run mypy on the test cases
73+
run: mypy tests/assert_type
7274

7375
stubtest:
7476
timeout-minutes: 10
@@ -112,12 +114,17 @@ jobs:
112114
run: |
113115
pip install -U pip setuptools wheel
114116
SETUPTOOLS_ENABLE_FEATURES=legacy-editable pip install -r ./requirements.txt
115-
- name: Run pyright
117+
- name: Run pyright on the stubs
116118
uses: jakebailey/pyright-action@v2
117119
with:
118-
pylance-version: latest-release
120+
version: PATH
119121
annotate: false
120122
continue-on-error: true # TODO: remove this part
123+
- name: Run pyright on the test cases
124+
uses: jakebailey/pyright-action@v2
125+
with:
126+
version: PATH
127+
project: ./pyrightconfig.testcases.json
121128

122129
matrix-test:
123130
timeout-minutes: 10

django-stubs/db/models/enums.pyi

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import enum
22
import sys
3-
from typing import Any, TypeVar, type_check_only
3+
from typing import Any, TypeVar, overload, type_check_only
44

5-
from typing_extensions import Self, TypeAlias
5+
from _typeshed import ConvertibleToInt
6+
from django.utils.functional import _StrOrPromise
7+
from typing_extensions import TypeAlias
68

79
_Self = TypeVar("_Self")
810

@@ -55,8 +57,14 @@ class _IntegerChoicesMeta(ChoicesType):
5557
@property
5658
def values(self) -> list[int]: ...
5759

60+
# In reality, the `__init__` overloads provided below should also support
61+
# all the arguments of `int.__new__`/`str.__new__` (e.g. `base`, `encoding`).
62+
# They are omitted on purpose to avoid having convoluted stubs for these enums:
5863
class IntegerChoices(Choices, IntEnum, metaclass=_IntegerChoicesMeta):
59-
def __new__(cls, value: int) -> Self: ...
64+
@overload
65+
def __init__(self, x: ConvertibleToInt) -> None: ...
66+
@overload
67+
def __init__(self, x: ConvertibleToInt, label: _StrOrPromise) -> None: ...
6068
@_enum_property
6169
def value(self) -> int: ...
6270

@@ -69,6 +77,9 @@ class _TextChoicesMeta(ChoicesType):
6977
def values(self) -> list[str]: ...
7078

7179
class TextChoices(Choices, StrEnum, metaclass=_TextChoicesMeta):
72-
def __new__(cls, value: str | tuple[str, str]) -> Self: ...
80+
@overload
81+
def __init__(self, object: str) -> None: ...
82+
@overload
83+
def __init__(self, object: str, label: _StrOrPromise) -> None: ...
7384
@_enum_property
7485
def value(self) -> str: ...

pyproject.toml

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,6 @@ include = '\.pyi?$'
66
[tool.codespell]
77
ignore-words-list = "aadd,acount,nam,asend"
88

9-
[tool.pyright]
10-
include = [
11-
"django-stubs",
12-
"ext/django_stubs_ext",
13-
"mypy_django_plugin",
14-
"scripts",
15-
"tests",
16-
]
17-
exclude = [
18-
".github",
19-
".mypy_cache",
20-
"build",
21-
]
22-
reportMissingTypeArgument = "warning"
23-
reportPrivateUsage = "none"
24-
stubPath = "."
25-
typeCheckingMode = "strict"
26-
27-
pythonVersion = "3.8"
28-
pythonPlatform = "All"
29-
30-
319
[tool.ruff]
3210
# Adds to default excludes: https://ruff.rs/docs/settings/#exclude
3311
extend-exclude = [
@@ -68,7 +46,7 @@ ignore = ["PYI021", "PYI024", "PYI041", "PYI043"]
6846
"F822",
6947
"F821",
7048
]
71-
"tests/*.py" = ["INP001"]
49+
"tests/*.py" = ["INP001", "PGH003"]
7250
"ext/tests/*.py" = ["INP001"]
7351
"setup.py" = ["INP001"]
7452

pyrightconfig.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json",
3+
"include": [
4+
"django-stubs",
5+
"ext/django_stubs_ext",
6+
"mypy_django_plugin",
7+
"scripts",
8+
],
9+
"exclude": [
10+
".github",
11+
".mypy_cache",
12+
"build",
13+
// test cases use a custom config file
14+
"tests/",
15+
],
16+
"typeCheckingMode": "strict",
17+
"reportMissingTypeArgument": "warning",
18+
// Stubs are allowed to use private variables
19+
"reportPrivateUsage": "none",
20+
"stubPath": ".",
21+
"pythonVersion": "3.8",
22+
"pythonPlatform": "All",
23+
}

pyrightconfig.testcases.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json",
3+
"include": [
4+
"tests/assert_type/"
5+
],
6+
"typeCheckingMode": "strict",
7+
// Extra strict settings
8+
"reportShadowedImports": "error", // Don't accidentally name a file something that shadows stdlib
9+
"reportImplicitStringConcatenation": "error",
10+
"reportUninitializedInstanceVariable": "error",
11+
"reportUnnecessaryTypeIgnoreComment": "error",
12+
// Don't use '# type: ignore' to suppress with pyright
13+
"enableTypeIgnoreComments": false,
14+
// If a test case uses this anti-pattern, there's likely a reason and annoying to `type: ignore`.
15+
// Let Ruff flag it (B006)
16+
"reportCallInDefaultInitializer": "none",
17+
// Too strict and not needed for type testing
18+
"reportMissingSuperCall": "none",
19+
// Stubs are allowed to use private variables. We may want to test those.
20+
"reportPrivateUsage": "none",
21+
// Stubs don't need the actual modules to be installed
22+
"reportMissingModuleSource": "none",
23+
}

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ Django==5.0.6; python_version >= '3.10'
1515

1616
# Overrides:
1717
mypy==1.10.0
18+
pyright==1.1.364
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from typing import List, Literal, Tuple
2+
3+
from django.db.models import IntegerChoices, TextChoices
4+
from django.utils.translation import gettext_lazy as _
5+
from typing_extensions import assert_type
6+
7+
8+
class MyIntegerChoices(IntegerChoices):
9+
A = 1
10+
B = 2, "B"
11+
C = 3, "B", "..." # pyright: ignore[reportCallIssue]
12+
D = 4, _("D")
13+
E = 5, 1 # pyright: ignore[reportArgumentType]
14+
F = "1"
15+
16+
17+
assert_type(MyIntegerChoices.A, Literal[MyIntegerChoices.A])
18+
assert_type(MyIntegerChoices.A.label, str)
19+
20+
# For standard enums, type checkers may infer the type of a member's value
21+
# (e.g. `MyIntegerChoices.A.value` inferred as `Literal[1]`).
22+
# However, Django choices metaclass is using the last value for the label.
23+
# Type checkers relies on the stub definition of the `value` property, typed
24+
# as `int`/`str` for `IntegerChoices`/`TextChoices`.
25+
assert_type(MyIntegerChoices.A.value, int)
26+
27+
28+
class MyTextChoices(TextChoices):
29+
A = "a"
30+
B = "b", "B"
31+
C = "c", _("C")
32+
D = 1 # pyright: ignore[reportArgumentType]
33+
E = "e", 1 # pyright: ignore[reportArgumentType]
34+
35+
36+
assert_type(MyTextChoices.A, Literal[MyTextChoices.A])
37+
assert_type(MyTextChoices.A.label, str)
38+
assert_type(MyTextChoices.A.value, str)
39+
40+
41+
# Assertions related to the metaclass:
42+
43+
assert_type(MyIntegerChoices.values, List[int])
44+
assert_type(MyIntegerChoices.choices, List[Tuple[int, str]])
45+
assert_type(MyTextChoices.values, List[str])
46+
assert_type(MyTextChoices.choices, List[Tuple[str, str]])

0 commit comments

Comments
 (0)