From a1fb7f7afe9668c00a6db76b17f4ede2e83acca0 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 14 May 2024 20:17:36 +0200 Subject: [PATCH 01/10] Fix signature of Choices member creation --- django-stubs/db/models/enums.pyi | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/django-stubs/db/models/enums.pyi b/django-stubs/db/models/enums.pyi index e179273e2..a3e927f35 100644 --- a/django-stubs/db/models/enums.pyi +++ b/django-stubs/db/models/enums.pyi @@ -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") @@ -56,7 +58,10 @@ class _IntegerChoicesMeta(ChoicesType): def values(self) -> list[int]: ... 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: ... @@ -69,6 +74,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: ... From c947f61172d13c1b32a3655ecccddf1c8edf96de Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 14 May 2024 22:14:48 +0200 Subject: [PATCH 02/10] Add comment regarding overloads --- django-stubs/db/models/enums.pyi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/django-stubs/db/models/enums.pyi b/django-stubs/db/models/enums.pyi index a3e927f35..1393c4517 100644 --- a/django-stubs/db/models/enums.pyi +++ b/django-stubs/db/models/enums.pyi @@ -57,6 +57,9 @@ 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): @overload def __init__(self, x: ConvertibleToInt) -> None: ... From 176dc6cad99675ebe84e5379c2159646a7cd3740 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Wed, 15 May 2024 19:18:56 +0200 Subject: [PATCH 03/10] Add pyright to CI, add test --- .github/workflows/test.yml | 9 ++++++-- pyproject.toml | 24 +-------------------- pyrightconfig.json | 23 ++++++++++++++++++++ pyrightconfig.testcases.json | 23 ++++++++++++++++++++ requirements.txt | 1 + tests/python_files/db/models/check_enums.py | 19 ++++++++++++++++ 6 files changed, 74 insertions(+), 25 deletions(-) create mode 100644 pyrightconfig.json create mode 100644 pyrightconfig.testcases.json create mode 100644 tests/python_files/db/models/check_enums.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5658849e3..b60ce1891 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -112,12 +112,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 diff --git a/pyproject.toml b/pyproject.toml index 251e919ff..a8fccfbc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ @@ -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"] diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 000000000..5259a7157 --- /dev/null +++ b/pyrightconfig.json @@ -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", +} diff --git a/pyrightconfig.testcases.json b/pyrightconfig.testcases.json new file mode 100644 index 000000000..7f4d963d5 --- /dev/null +++ b/pyrightconfig.testcases.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json", + "include": [ + "tests/python_files/" + ], + "typeCheckingMode": "strict", + // Extra strict settings + "reportShadowedImports": "error", // Don't accidentally name a file something that shadows stdlib + "reportImplicitStringConcatenation": "error", + "reportUninitializedInstanceVariable": "error", + "reportUnnecessaryTypeIgnoreComment": "error", + // Using unspecific `type: ignore` comments in test_cases. + "enableTypeIgnoreComments": true, + // 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", +} diff --git a/requirements.txt b/requirements.txt index 63b65b286..0c41d3b76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ Django==5.0.6; python_version >= '3.10' # Overrides: mypy==1.10.0 +pyright==1.1.363 diff --git a/tests/python_files/db/models/check_enums.py b/tests/python_files/db/models/check_enums.py new file mode 100644 index 000000000..30830bc9e --- /dev/null +++ b/tests/python_files/db/models/check_enums.py @@ -0,0 +1,19 @@ +from django.db.models import IntegerChoices, TextChoices +from django.utils.translation import gettext_lazy as _ + + +class MyIntegerChoices(IntegerChoices): + A = 1 + B = 2, "B" + C = 3, "B", "..." # type: ignore + D = 4, _("D") + E = 5, 1 # type: ignore + F = "1" + + +class MyTextChoices(TextChoices): + A = "a" + B = "b", "B" + C = "c", _("C") + D = 1 # type: ignore + E = "e", 1 # type: ignore From 78a14d75f1272d4670c4298de35eb3600bcc893b Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Wed, 15 May 2024 19:27:17 +0200 Subject: [PATCH 04/10] Run mypy on the new test cases --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b60ce1891..9be83a5d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,8 +67,11 @@ 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 + # TODO: When mypy aligns with pyright, remove the `--no-warn-unused-ignores`. + run: mypy tests/python_files --no-warn-unused-ignores stubtest: timeout-minutes: 10 From 3114d8cb4f7efc06e189c4a96769b150d3185b87 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Wed, 22 May 2024 14:12:56 +0200 Subject: [PATCH 05/10] Add more assertions, rename test folder --- .github/workflows/test.yml | 2 +- pyrightconfig.testcases.json | 2 +- tests/assert_type/db/models/check_enums.py | 46 +++++++++++++++++++++ tests/python_files/db/models/check_enums.py | 19 --------- 4 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 tests/assert_type/db/models/check_enums.py delete mode 100644 tests/python_files/db/models/check_enums.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9be83a5d6..1c11636f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,7 +71,7 @@ jobs: run: PYTHONPATH='.' pytest --num-shards=4 --shard-id=${{ matrix.shard }} tests - name: Run mypy on the test cases # TODO: When mypy aligns with pyright, remove the `--no-warn-unused-ignores`. - run: mypy tests/python_files --no-warn-unused-ignores + run: mypy tests/assert_type --no-warn-unused-ignores stubtest: timeout-minutes: 10 diff --git a/pyrightconfig.testcases.json b/pyrightconfig.testcases.json index 7f4d963d5..c38933315 100644 --- a/pyrightconfig.testcases.json +++ b/pyrightconfig.testcases.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/vscode-pyright/schemas/pyrightconfig.schema.json", "include": [ - "tests/python_files/" + "tests/assert_type/" ], "typeCheckingMode": "strict", // Extra strict settings diff --git a/tests/assert_type/db/models/check_enums.py b/tests/assert_type/db/models/check_enums.py new file mode 100644 index 000000000..9fbdcc463 --- /dev/null +++ b/tests/assert_type/db/models/check_enums.py @@ -0,0 +1,46 @@ +from typing import Literal + +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", "..." # type: ignore + D = 4, _("D") + E = 5, 1 # type: ignore + 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 # type: ignore + E = "e", 1 # type: ignore + + +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]]) diff --git a/tests/python_files/db/models/check_enums.py b/tests/python_files/db/models/check_enums.py deleted file mode 100644 index 30830bc9e..000000000 --- a/tests/python_files/db/models/check_enums.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.db.models import IntegerChoices, TextChoices -from django.utils.translation import gettext_lazy as _ - - -class MyIntegerChoices(IntegerChoices): - A = 1 - B = 2, "B" - C = 3, "B", "..." # type: ignore - D = 4, _("D") - E = 5, 1 # type: ignore - F = "1" - - -class MyTextChoices(TextChoices): - A = "a" - B = "b", "B" - C = "c", _("C") - D = 1 # type: ignore - E = "e", 1 # type: ignore From 25cc223b51bf7dbb8cf0877ca4b900a17e5eb3e9 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Wed, 22 May 2024 14:13:23 +0200 Subject: [PATCH 06/10] Update to `pyright==1.1.364` --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0c41d3b76..0dca2f13a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,4 @@ Django==5.0.6; python_version >= '3.10' # Overrides: mypy==1.10.0 -pyright==1.1.363 +pyright==1.1.364 From 082f6013fbf7723de4d74090bef2136ce1fe645d Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Wed, 22 May 2024 14:26:58 +0200 Subject: [PATCH 07/10] Add `.gitattributes` for correct syntax highlighting --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..d11ed7dd0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +scripts/allowlist_*.txt linguist-language=ini +pyrightconfig*.json linguist-language=jsonc From eebe0cf98595c6508409c8497836ae3351b9fceb Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Wed, 22 May 2024 17:47:21 +0200 Subject: [PATCH 08/10] Python compat --- tests/assert_type/db/models/check_enums.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/assert_type/db/models/check_enums.py b/tests/assert_type/db/models/check_enums.py index 9fbdcc463..a271c1519 100644 --- a/tests/assert_type/db/models/check_enums.py +++ b/tests/assert_type/db/models/check_enums.py @@ -1,4 +1,4 @@ -from typing import Literal +from typing import Literal, List, Tuple from django.db.models import IntegerChoices, TextChoices from django.utils.translation import gettext_lazy as _ @@ -40,7 +40,7 @@ class MyTextChoices(TextChoices): # 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]]) +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]]) From c0f8cfdf68ca8a6230e223cccdaead13e2b08e57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 15:48:16 +0000 Subject: [PATCH 09/10] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/assert_type/db/models/check_enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/assert_type/db/models/check_enums.py b/tests/assert_type/db/models/check_enums.py index a271c1519..5282e5449 100644 --- a/tests/assert_type/db/models/check_enums.py +++ b/tests/assert_type/db/models/check_enums.py @@ -1,4 +1,4 @@ -from typing import Literal, List, Tuple +from typing import List, Literal, Tuple from django.db.models import IntegerChoices, TextChoices from django.utils.translation import gettext_lazy as _ From fe18e5d2f65f4267c380738041fde6341e16a2df Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 24 May 2024 10:04:49 +0200 Subject: [PATCH 10/10] type ignore comments compatibility between pyright and mypy --- .github/workflows/test.yml | 3 +-- pyrightconfig.testcases.json | 4 ++-- tests/assert_type/db/models/check_enums.py | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c11636f6..4ea7d82e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,8 +70,7 @@ jobs: - name: Run pytest tests run: PYTHONPATH='.' pytest --num-shards=4 --shard-id=${{ matrix.shard }} tests - name: Run mypy on the test cases - # TODO: When mypy aligns with pyright, remove the `--no-warn-unused-ignores`. - run: mypy tests/assert_type --no-warn-unused-ignores + run: mypy tests/assert_type stubtest: timeout-minutes: 10 diff --git a/pyrightconfig.testcases.json b/pyrightconfig.testcases.json index c38933315..f939a0905 100644 --- a/pyrightconfig.testcases.json +++ b/pyrightconfig.testcases.json @@ -9,8 +9,8 @@ "reportImplicitStringConcatenation": "error", "reportUninitializedInstanceVariable": "error", "reportUnnecessaryTypeIgnoreComment": "error", - // Using unspecific `type: ignore` comments in test_cases. - "enableTypeIgnoreComments": true, + // 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", diff --git a/tests/assert_type/db/models/check_enums.py b/tests/assert_type/db/models/check_enums.py index 5282e5449..2c88d76f5 100644 --- a/tests/assert_type/db/models/check_enums.py +++ b/tests/assert_type/db/models/check_enums.py @@ -8,9 +8,9 @@ class MyIntegerChoices(IntegerChoices): A = 1 B = 2, "B" - C = 3, "B", "..." # type: ignore + C = 3, "B", "..." # pyright: ignore[reportCallIssue] D = 4, _("D") - E = 5, 1 # type: ignore + E = 5, 1 # pyright: ignore[reportArgumentType] F = "1" @@ -29,8 +29,8 @@ class MyTextChoices(TextChoices): A = "a" B = "b", "B" C = "c", _("C") - D = 1 # type: ignore - E = "e", 1 # type: ignore + D = 1 # pyright: ignore[reportArgumentType] + E = "e", 1 # pyright: ignore[reportArgumentType] assert_type(MyTextChoices.A, Literal[MyTextChoices.A])