From 9e00bec37fdd01047e0bb030dde3a31f5d00ea29 Mon Sep 17 00:00:00 2001 From: Bob Gregory Date: Fri, 7 Jul 2023 09:17:44 +0100 Subject: [PATCH 1/3] User.objects is UserManager[Self] This commit fixes the issue where you extend the User class. Previously, MySpecialUser.objects would return a UserManager[User]. This commit updates the objects field to paramterise over Self, so that MySpecialUser.objects returns a UserManager[MySpecialUser]. --- django-stubs/contrib/auth/models.pyi | 7 ++++++- tests/pyright/test_user.py | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/pyright/test_user.py diff --git a/django-stubs/contrib/auth/models.pyi b/django-stubs/contrib/auth/models.pyi index babfb17c3..8407551fe 100644 --- a/django-stubs/contrib/auth/models.pyi +++ b/django-stubs/contrib/auth/models.pyi @@ -15,6 +15,11 @@ if sys.version_info < (3, 8): else: from typing import Literal +if sys.version_info < (3, 11): + from typing_extensions import Self +else: + from typing import Self + _AnyUser = Union[Model, "AnonymousUser"] def update_last_login( @@ -104,7 +109,7 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin): ) -> None: ... class User(AbstractUser): - objects: UserManager[User] + objects: UserManager[Self] class AnonymousUser: id: Any = ... diff --git a/tests/pyright/test_user.py b/tests/pyright/test_user.py new file mode 100644 index 000000000..b9f499e69 --- /dev/null +++ b/tests/pyright/test_user.py @@ -0,0 +1,24 @@ +from .base import Result, run_pyright + +def test_user_manager_specialises_to_self(): + results = run_pyright( + """\ +from django.db import models +from django.contrib.auth import models as auth_models + +class MyUser(auth_models.User): + age = models.IntegerField() + +u = MyUser() +reveal_type(u.objects) +""" + ) + + assert results == [ + Result( + type="information", + message='Type of "u.objects" is "UserManager[MyUser]"', + line=8, + column=13, + ), + ] From 18a2b4a527abc4bbee4aedfa26f757a21557d982 Mon Sep 17 00:00:00 2001 From: Bob Gregory Date: Fri, 7 Jul 2023 17:16:27 +0100 Subject: [PATCH 2/3] Fine. --- tests/pyright/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pyright/test_user.py b/tests/pyright/test_user.py index b9f499e69..160191ae3 100644 --- a/tests/pyright/test_user.py +++ b/tests/pyright/test_user.py @@ -1,6 +1,6 @@ from .base import Result, run_pyright -def test_user_manager_specialises_to_self(): +def test_user_manager_specialises_to_self() -> None: results = run_pyright( """\ from django.db import models From b5b3cd9e63d568e8e90c716607bb8e51633b5667 Mon Sep 17 00:00:00 2001 From: Bob Gregory Date: Sat, 8 Jul 2023 09:39:51 +0100 Subject: [PATCH 3/3] Make linter happy --- django-stubs/db/models/base.pyi | 5 ++++- django-stubs/db/transaction.pyi | 8 ++++++-- tests/pyright/test_user.py | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/django-stubs/db/models/base.pyi b/django-stubs/db/models/base.pyi index 5d063aa94..76e7c801b 100644 --- a/django-stubs/db/models/base.pyi +++ b/django-stubs/db/models/base.pyi @@ -67,7 +67,10 @@ class Model(metaclass=ModelBase): self, using: Any = ..., keep_parents: bool = ... ) -> Tuple[int, Dict[str, int]]: ... def full_clean( - self, exclude: Optional[Collection[str]] = ..., validate_unique: bool = True, validate_constraints: bool = True + self, + exclude: Optional[Collection[str]] = ..., + validate_unique: bool = True, + validate_constraints: bool = True, ) -> None: ... def clean(self) -> None: ... def clean_fields(self, exclude: Optional[Collection[str]] = ...) -> None: ... diff --git a/django-stubs/db/transaction.pyi b/django-stubs/db/transaction.pyi index 60db02161..8e41d64ab 100644 --- a/django-stubs/db/transaction.pyi +++ b/django-stubs/db/transaction.pyi @@ -28,7 +28,9 @@ _C = TypeVar("_C", bound=Callable[..., Any]) class Atomic: using: Optional[str] = ... savepoint: bool = ... - def __init__(self, using: Optional[str], savepoint: bool, durable: bool = ...) -> None: ... + def __init__( + self, using: Optional[str], savepoint: bool, durable: bool = ... + ) -> None: ... # When decorating, return the decorated function as-is, rather than clobbering it as ContextDecorator does. def __call__(self, func: _C) -> _C: ... def __enter__(self) -> None: ... @@ -40,7 +42,9 @@ def atomic(using: _C) -> _C: ... # Decorator or context-manager with parameters @overload -def atomic(using: Optional[str] = ..., savepoint: bool = ..., durable: bool = ...) -> Atomic: ... +def atomic( + using: Optional[str] = ..., savepoint: bool = ..., durable: bool = ... +) -> Atomic: ... # Bare decorator @overload diff --git a/tests/pyright/test_user.py b/tests/pyright/test_user.py index 160191ae3..409fee743 100644 --- a/tests/pyright/test_user.py +++ b/tests/pyright/test_user.py @@ -1,5 +1,6 @@ from .base import Result, run_pyright + def test_user_manager_specialises_to_self() -> None: results = run_pyright( """\ @@ -12,7 +13,7 @@ class MyUser(auth_models.User): u = MyUser() reveal_type(u.objects) """ - ) + ) assert results == [ Result(