Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix signature of Choices member creation, add assert_type test cases, run pyright #2162

Merged
merged 10 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
scripts/allowlist_*.txt linguist-language=ini
pyrightconfig*.json linguist-language=jsonc
13 changes: 10 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ jobs:
SETUPTOOLS_ENABLE_FEATURES=legacy-editable pip install -r ./requirements.txt

# Must match `shard` definition in the test matrix:
- name: Run tests
- name: Run pytest tests
run: PYTHONPATH='.' pytest --num-shards=4 --shard-id=${{ matrix.shard }} tests
- name: Run mypy on the test cases
run: mypy tests/assert_type

stubtest:
timeout-minutes: 10
Expand Down Expand Up @@ -112,12 +114,17 @@ jobs:
run: |
pip install -U pip setuptools wheel
SETUPTOOLS_ENABLE_FEATURES=legacy-editable pip install -r ./requirements.txt
- name: Run pyright
- name: Run pyright on the stubs
uses: jakebailey/pyright-action@v2
with:
pylance-version: latest-release
version: PATH
annotate: false
continue-on-error: true # TODO: remove this part
- name: Run pyright on the test cases
uses: jakebailey/pyright-action@v2
with:
version: PATH
project: ./pyrightconfig.testcases.json

matrix-test:
timeout-minutes: 10
Expand Down
19 changes: 15 additions & 4 deletions django-stubs/db/models/enums.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import enum
import sys
from typing import Any, TypeVar, type_check_only
from typing import Any, TypeVar, overload, type_check_only

from typing_extensions import Self, TypeAlias
from _typeshed import ConvertibleToInt
from django.utils.functional import _StrOrPromise
from typing_extensions import TypeAlias

_Self = TypeVar("_Self")

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

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

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

class TextChoices(Choices, StrEnum, metaclass=_TextChoicesMeta):
def __new__(cls, value: str | tuple[str, str]) -> Self: ...
@overload
def __init__(self, object: str) -> None: ...
@overload
def __init__(self, object: str, label: _StrOrPromise) -> None: ...
@_enum_property
def value(self) -> str: ...
24 changes: 1 addition & 23 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,6 @@ include = '\.pyi?$'
[tool.codespell]
ignore-words-list = "aadd,acount,nam,asend"

[tool.pyright]
include = [
"django-stubs",
"ext/django_stubs_ext",
"mypy_django_plugin",
"scripts",
"tests",
]
exclude = [
".github",
".mypy_cache",
"build",
]
reportMissingTypeArgument = "warning"
reportPrivateUsage = "none"
stubPath = "."
typeCheckingMode = "strict"

pythonVersion = "3.8"
pythonPlatform = "All"


[tool.ruff]
# Adds to default excludes: https://ruff.rs/docs/settings/#exclude
extend-exclude = [
Expand Down Expand Up @@ -68,7 +46,7 @@ ignore = ["PYI021", "PYI024", "PYI041", "PYI043"]
"F822",
"F821",
]
"tests/*.py" = ["INP001"]
"tests/*.py" = ["INP001", "PGH003"]
"ext/tests/*.py" = ["INP001"]
"setup.py" = ["INP001"]

Expand Down
23 changes: 23 additions & 0 deletions pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json",
"include": [
"django-stubs",
"ext/django_stubs_ext",
"mypy_django_plugin",
"scripts",
],
"exclude": [
".github",
".mypy_cache",
"build",
// test cases use a custom config file
"tests/",
],
"typeCheckingMode": "strict",
"reportMissingTypeArgument": "warning",
// Stubs are allowed to use private variables
"reportPrivateUsage": "none",
"stubPath": ".",
"pythonVersion": "3.8",
"pythonPlatform": "All",
}
23 changes: 23 additions & 0 deletions pyrightconfig.testcases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json",
"include": [
"tests/assert_type/"
],
"typeCheckingMode": "strict",
// Extra strict settings
"reportShadowedImports": "error", // Don't accidentally name a file something that shadows stdlib
"reportImplicitStringConcatenation": "error",
"reportUninitializedInstanceVariable": "error",
"reportUnnecessaryTypeIgnoreComment": "error",
// Don't use '# type: ignore' to suppress with pyright
"enableTypeIgnoreComments": false,
// If a test case uses this anti-pattern, there's likely a reason and annoying to `type: ignore`.
// Let Ruff flag it (B006)
"reportCallInDefaultInitializer": "none",
// Too strict and not needed for type testing
"reportMissingSuperCall": "none",
// Stubs are allowed to use private variables. We may want to test those.
"reportPrivateUsage": "none",
// Stubs don't need the actual modules to be installed
"reportMissingModuleSource": "none",
}
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ Django==5.0.6; python_version >= '3.10'

# Overrides:
mypy==1.10.0
pyright==1.1.364
46 changes: 46 additions & 0 deletions tests/assert_type/db/models/check_enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import List, Literal, Tuple

from django.db.models import IntegerChoices, TextChoices
from django.utils.translation import gettext_lazy as _
from typing_extensions import assert_type


class MyIntegerChoices(IntegerChoices):
A = 1
B = 2, "B"
C = 3, "B", "..." # pyright: ignore[reportCallIssue]
D = 4, _("D")
E = 5, 1 # pyright: ignore[reportArgumentType]
F = "1"


assert_type(MyIntegerChoices.A, Literal[MyIntegerChoices.A])
assert_type(MyIntegerChoices.A.label, str)

# For standard enums, type checkers may infer the type of a member's value
# (e.g. `MyIntegerChoices.A.value` inferred as `Literal[1]`).
# However, Django choices metaclass is using the last value for the label.
# Type checkers relies on the stub definition of the `value` property, typed
# as `int`/`str` for `IntegerChoices`/`TextChoices`.
assert_type(MyIntegerChoices.A.value, int)


class MyTextChoices(TextChoices):
A = "a"
B = "b", "B"
C = "c", _("C")
D = 1 # pyright: ignore[reportArgumentType]
E = "e", 1 # pyright: ignore[reportArgumentType]


assert_type(MyTextChoices.A, Literal[MyTextChoices.A])
assert_type(MyTextChoices.A.label, str)
assert_type(MyTextChoices.A.value, str)


# Assertions related to the metaclass:

assert_type(MyIntegerChoices.values, List[int])
assert_type(MyIntegerChoices.choices, List[Tuple[int, str]])
assert_type(MyTextChoices.values, List[str])
assert_type(MyTextChoices.choices, List[Tuple[str, str]])
Loading