From fc3ef75480ba34c3148fb3833f6b062653194752 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Thu, 10 Aug 2023 15:07:37 -0700 Subject: [PATCH 1/7] Move ModelBase.objects declaration to Model.objects, for mypy 1.5.0 mypy 1.5.0 was fixed to understand that metaclass attributes take precedence over attributes in the regular class. So we need to declare `objects` in the regular class to allow it to be overridden in subclasses. Fixes #1648. Signed-off-by: Anders Kaseorg --- django-stubs/db/models/base.pyi | 4 ++-- scripts/stubtest/allowlist_todo.txt | 8 -------- tests/typecheck/models/test_abstract.yml | 3 ++- tests/typecheck/models/test_meta_options.yml | 2 +- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/django-stubs/db/models/base.pyi b/django-stubs/db/models/base.pyi index b000a2f5c..216823b3f 100644 --- a/django-stubs/db/models/base.pyi +++ b/django-stubs/db/models/base.pyi @@ -19,14 +19,14 @@ class ModelState: fields_cache: ModelStateFieldsCacheDescriptor class ModelBase(type): - @property - def objects(cls: type[_Self]) -> BaseManager[_Self]: ... # type: ignore[misc] @property def _default_manager(cls: type[_Self]) -> BaseManager[_Self]: ... # type: ignore[misc] @property def _base_manager(cls: type[_Self]) -> BaseManager[_Self]: ... # type: ignore[misc] class Model(metaclass=ModelBase): + objects: BaseManager[Self] + # Note: these two metaclass generated attributes don't really exist on the 'Model' # class, runtime they are only added on concrete subclasses of 'Model'. The # metaclass also sets up correct inheritance from concrete parent models exceptions. diff --git a/scripts/stubtest/allowlist_todo.txt b/scripts/stubtest/allowlist_todo.txt index d268ad53c..a13a14670 100644 --- a/scripts/stubtest/allowlist_todo.txt +++ b/scripts/stubtest/allowlist_todo.txt @@ -315,17 +315,13 @@ django.contrib.gis.db.backends.oracle.features.DatabaseFeatures.supports_toleran django.contrib.gis.db.backends.oracle.features.DatabaseFeatures.unsupported_geojson_options django.contrib.gis.db.backends.oracle.introspection django.contrib.gis.db.backends.oracle.models.OracleGeometryColumns.Meta -django.contrib.gis.db.backends.oracle.models.OracleGeometryColumns.objects django.contrib.gis.db.backends.oracle.models.OracleSpatialRefSys.Meta -django.contrib.gis.db.backends.oracle.models.OracleSpatialRefSys.objects django.contrib.gis.db.backends.oracle.operations django.contrib.gis.db.backends.postgis.adapter.PostGISAdapter.prepare django.contrib.gis.db.backends.postgis.features.DatabaseFeatures.empty_intersection_returns_none django.contrib.gis.db.backends.postgis.features.DatabaseFeatures.supports_geography django.contrib.gis.db.backends.postgis.models.PostGISGeometryColumns.Meta -django.contrib.gis.db.backends.postgis.models.PostGISGeometryColumns.objects django.contrib.gis.db.backends.postgis.models.PostGISSpatialRefSys.Meta -django.contrib.gis.db.backends.postgis.models.PostGISSpatialRefSys.objects django.contrib.gis.db.backends.postgis.operations.PostGISOperations.convert_extent django.contrib.gis.db.backends.postgis.operations.PostGISOperations.convert_extent3d django.contrib.gis.db.backends.postgis.operations.PostGISOperator.check_geography @@ -335,9 +331,7 @@ django.contrib.gis.db.backends.spatialite.features.DatabaseFeatures.can_alter_ge django.contrib.gis.db.backends.spatialite.features.DatabaseFeatures.django_test_skips django.contrib.gis.db.backends.spatialite.features.DatabaseFeatures.supports_area_geodetic django.contrib.gis.db.backends.spatialite.models.SpatialiteGeometryColumns.Meta -django.contrib.gis.db.backends.spatialite.models.SpatialiteGeometryColumns.objects django.contrib.gis.db.backends.spatialite.models.SpatialiteSpatialRefSys.Meta -django.contrib.gis.db.backends.spatialite.models.SpatialiteSpatialRefSys.objects django.contrib.gis.db.backends.spatialite.operations.SpatiaLiteOperations.convert_extent django.contrib.gis.db.backends.spatialite.operations.SpatiaLiteOperations.from_text django.contrib.gis.db.backends.spatialite.operations.SpatiaLiteOperations.geom_lib_version @@ -1276,7 +1270,6 @@ django.db.migrations.questioner.NonInteractiveMigrationQuestioner.log_lack_of_mi django.db.migrations.recorder.MigrationRecorder.Migration.get_next_by_applied django.db.migrations.recorder.MigrationRecorder.Migration.get_previous_by_applied django.db.migrations.recorder.MigrationRecorder.Migration.id -django.db.migrations.recorder.MigrationRecorder.Migration.objects django.db.migrations.serializer.ChoicesSerializer django.db.migrations.utils.FieldReference django.db.migrations.utils.field_is_referenced @@ -1534,7 +1527,6 @@ django.db.models.base.Model.Meta django.db.models.base.Model.add_to_class django.db.models.base.ModelBase.__new__ django.db.models.base.ModelBase.add_to_class -django.db.models.base.ModelBase.objects django.db.models.base.ModelStateFieldsCacheDescriptor.__get__ django.db.models.base.make_foreign_order_accessors django.db.models.base.method_get_order diff --git a/tests/typecheck/models/test_abstract.yml b/tests/typecheck/models/test_abstract.yml index 887fd78f6..ca2bc7b13 100644 --- a/tests/typecheck/models/test_abstract.yml +++ b/tests/typecheck/models/test_abstract.yml @@ -51,6 +51,7 @@ Recursive(parent=Recursive(parent=None)) Concrete(parent=Concrete(parent=None)) out: | + main:4: error: Access to generic instance variables via class is ambiguous main:4: error: Unexpected attribute "parent" for model "Recursive" main:5: error: Cannot instantiate abstract class "Recursive" with abstract attributes "DoesNotExist" and "MultipleObjectsReturned" main:5: error: Unexpected attribute "parent" for model "Recursive" @@ -209,4 +210,4 @@ def create_animal_generic(klass: Type[T], name: str) -> T: reveal_type(klass) # N: Revealed type is "Type[T`-1]" - return klass.objects.create(name=name) + return klass.objects.create(name=name) # E: Incompatible return value type (got "Animal", expected "T") diff --git a/tests/typecheck/models/test_meta_options.yml b/tests/typecheck/models/test_meta_options.yml index 94208a366..eb6ef30f2 100644 --- a/tests/typecheck/models/test_meta_options.yml +++ b/tests/typecheck/models/test_meta_options.yml @@ -80,7 +80,7 @@ # Errors: AbstractModel() # E: Cannot instantiate abstract class "AbstractModel" with abstract attributes "DoesNotExist" and "MultipleObjectsReturned" - AbstractModel.objects.create() + AbstractModel.objects.create() # E: Access to generic instance variables via class is ambiguous installed_apps: - myapp files: From 662f4ee34fdaee4edb1e25c05a36f38182f3e915 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Sun, 3 Sep 2023 20:36:32 +0200 Subject: [PATCH 2/7] Declare manager class attributes on models as `ClassVar`s Inclusions: - Adjustments for the plugin to make generated managers `ClassVar`s - Changes the default 'objects' to 'ClassVar' and controls it via the plugin - Plugin ensures to only add the 'objects' manager to models it exists on during runtime --- django-stubs/contrib/admin/models.pyi | 4 ++-- django-stubs/contrib/auth/models.pyi | 10 ++++---- django-stubs/contrib/contenttypes/models.pyi | 4 ++-- .../contrib/gis/db/backends/oracle/models.pyi | 6 ++++- .../gis/db/backends/postgis/models.pyi | 5 +++- .../gis/db/backends/spatialite/models.pyi | 5 +++- django-stubs/contrib/sites/models.pyi | 4 ++-- django-stubs/db/migrations/recorder.pyi | 5 +++- django-stubs/db/models/base.pyi | 9 +++---- mypy_django_plugin/lib/helpers.py | 5 +++- mypy_django_plugin/transformers/models.py | 21 ++++++++++++---- tests/typecheck/managers/test_managers.yml | 24 +++++++++++++------ tests/typecheck/models/test_abstract.yml | 5 ++-- .../typecheck/models/test_contrib_models.yml | 21 +++++++++++----- tests/typecheck/models/test_meta_options.yml | 3 ++- 15 files changed, 90 insertions(+), 41 deletions(-) diff --git a/django-stubs/contrib/admin/models.pyi b/django-stubs/contrib/admin/models.pyi index 3657b5710..a1ac77b49 100644 --- a/django-stubs/contrib/admin/models.pyi +++ b/django-stubs/contrib/admin/models.pyi @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, ClassVar from uuid import UUID from django.db import models @@ -28,7 +28,7 @@ class LogEntry(models.Model): object_repr: models.CharField action_flag: models.PositiveSmallIntegerField change_message: models.TextField - objects: LogEntryManager + objects: ClassVar[LogEntryManager] def is_addition(self) -> bool: ... def is_change(self) -> bool: ... def is_deletion(self) -> bool: ... diff --git a/django-stubs/contrib/auth/models.pyi b/django-stubs/contrib/auth/models.pyi index 21b50fc87..4019d33bd 100644 --- a/django-stubs/contrib/auth/models.pyi +++ b/django-stubs/contrib/auth/models.pyi @@ -1,5 +1,5 @@ from collections.abc import Iterable -from typing import Any, Literal, TypeVar +from typing import Any, ClassVar, Literal, TypeVar from django.contrib.auth.base_user import AbstractBaseUser as AbstractBaseUser from django.contrib.auth.base_user import BaseUserManager as BaseUserManager @@ -10,7 +10,7 @@ from django.db.models import QuerySet from django.db.models.base import Model from django.db.models.manager import EmptyManager from django.utils.functional import _StrOrPromise -from typing_extensions import TypeAlias +from typing_extensions import Self, TypeAlias _AnyUser: TypeAlias = Model | AnonymousUser @@ -21,7 +21,7 @@ class PermissionManager(models.Manager[Permission]): class Permission(models.Model): content_type_id: int - objects: PermissionManager + objects: ClassVar[PermissionManager] name = models.CharField(max_length=255) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) @@ -32,7 +32,7 @@ class GroupManager(models.Manager[Group]): def get_by_natural_key(self, name: str) -> Group: ... class Group(models.Model): - objects: GroupManager + objects: ClassVar[GroupManager] name = models.CharField(max_length=150) permissions = models.ManyToManyField(Permission) @@ -81,6 +81,8 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin): is_active = models.BooleanField() date_joined = models.DateTimeField() + objects: ClassVar[UserManager[Self]] + EMAIL_FIELD: str USERNAME_FIELD: str diff --git a/django-stubs/contrib/contenttypes/models.pyi b/django-stubs/contrib/contenttypes/models.pyi index d54089ddd..e7206aed0 100644 --- a/django-stubs/contrib/contenttypes/models.pyi +++ b/django-stubs/contrib/contenttypes/models.pyi @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, ClassVar from django.db import models from django.db.models.base import Model @@ -15,7 +15,7 @@ class ContentType(models.Model): id: int app_label: models.CharField model: models.CharField - objects: ContentTypeManager + objects: ClassVar[ContentTypeManager] @property def name(self) -> str: ... def model_class(self) -> type[Model] | None: ... diff --git a/django-stubs/contrib/gis/db/backends/oracle/models.pyi b/django-stubs/contrib/gis/db/backends/oracle/models.pyi index e9a803705..e21a7850f 100644 --- a/django-stubs/contrib/gis/db/backends/oracle/models.pyi +++ b/django-stubs/contrib/gis/db/backends/oracle/models.pyi @@ -1,12 +1,15 @@ -from typing import Any +from typing import Any, ClassVar from django.contrib.gis.db import models from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin +from django.db.models.manager import Manager +from typing_extensions import Self class OracleGeometryColumns(models.Model): table_name: Any column_name: Any srid: Any + objects: ClassVar[Manager[Self]] class Meta: app_label: str @@ -24,6 +27,7 @@ class OracleSpatialRefSys(models.Model, SpatialRefSysMixin): auth_name: Any wktext: Any cs_bounds: Any + objects: ClassVar[Manager[Self]] class Meta: app_label: str diff --git a/django-stubs/contrib/gis/db/backends/postgis/models.pyi b/django-stubs/contrib/gis/db/backends/postgis/models.pyi index 8c39d9759..aae3576eb 100644 --- a/django-stubs/contrib/gis/db/backends/postgis/models.pyi +++ b/django-stubs/contrib/gis/db/backends/postgis/models.pyi @@ -1,7 +1,8 @@ -from typing import Any +from typing import Any, ClassVar from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin from django.db import models +from typing_extensions import Self class PostGISGeometryColumns(models.Model): f_table_catalog: Any @@ -11,6 +12,7 @@ class PostGISGeometryColumns(models.Model): coord_dimension: Any srid: Any type: Any + objects: ClassVar[models.Manager[Self]] class Meta: app_label: str @@ -27,6 +29,7 @@ class PostGISSpatialRefSys(models.Model, SpatialRefSysMixin): auth_srid: Any srtext: Any proj4text: Any + objects: ClassVar[models.Manager[Self]] class Meta: app_label: str diff --git a/django-stubs/contrib/gis/db/backends/spatialite/models.pyi b/django-stubs/contrib/gis/db/backends/spatialite/models.pyi index ab7062eec..651d9b924 100644 --- a/django-stubs/contrib/gis/db/backends/spatialite/models.pyi +++ b/django-stubs/contrib/gis/db/backends/spatialite/models.pyi @@ -1,7 +1,8 @@ -from typing import Any +from typing import Any, ClassVar from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin from django.db import models +from typing_extensions import Self class SpatialiteGeometryColumns(models.Model): f_table_name: Any @@ -10,6 +11,7 @@ class SpatialiteGeometryColumns(models.Model): srid: Any spatial_index_enabled: Any type: Any + objects: ClassVar[models.Manager[Self]] class Meta: app_label: str @@ -27,6 +29,7 @@ class SpatialiteSpatialRefSys(models.Model, SpatialRefSysMixin): ref_sys_name: Any proj4text: Any srtext: Any + objects: ClassVar[models.Manager[Self]] class Meta: app_label: str diff --git a/django-stubs/contrib/sites/models.pyi b/django-stubs/contrib/sites/models.pyi index 15a7d9e08..e4181d66f 100644 --- a/django-stubs/contrib/sites/models.pyi +++ b/django-stubs/contrib/sites/models.pyi @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, ClassVar from django.db import models from django.http.request import HttpRequest @@ -11,7 +11,7 @@ class SiteManager(models.Manager[Site]): def get_by_natural_key(self, domain: str) -> Site: ... class Site(models.Model): - objects: SiteManager + objects: ClassVar[SiteManager] domain = models.CharField(max_length=100) name = models.CharField(max_length=50) diff --git a/django-stubs/db/migrations/recorder.pyi b/django-stubs/db/migrations/recorder.pyi index af95d9ce2..652a309ca 100644 --- a/django-stubs/db/migrations/recorder.pyi +++ b/django-stubs/db/migrations/recorder.pyi @@ -1,14 +1,17 @@ -from typing import Any +from typing import Any, ClassVar from django.db.backends.base.base import BaseDatabaseWrapper from django.db.models.base import Model +from django.db.models.manager import Manager from django.db.models.query import QuerySet +from typing_extensions import Self class MigrationRecorder: class Migration(Model): app: Any name: Any applied: Any + objects: ClassVar[Manager[Self]] connection: BaseDatabaseWrapper def __init__(self, connection: BaseDatabaseWrapper) -> None: ... @property diff --git a/django-stubs/db/models/base.pyi b/django-stubs/db/models/base.pyi index 216823b3f..eb3bb35f0 100644 --- a/django-stubs/db/models/base.pyi +++ b/django-stubs/db/models/base.pyi @@ -1,11 +1,11 @@ from collections.abc import Collection, Iterable, Sequence -from typing import Any, Final, TypeVar +from typing import Any, ClassVar, Final, TypeVar from django.core.checks.messages import CheckMessage from django.core.exceptions import MultipleObjectsReturned as BaseMultipleObjectsReturned from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db.models import BaseConstraint, Field -from django.db.models.manager import BaseManager +from django.db.models.manager import BaseManager, Manager from django.db.models.options import Options from typing_extensions import Self @@ -25,8 +25,6 @@ class ModelBase(type): def _base_manager(cls: type[_Self]) -> BaseManager[_Self]: ... # type: ignore[misc] class Model(metaclass=ModelBase): - objects: BaseManager[Self] - # Note: these two metaclass generated attributes don't really exist on the 'Model' # class, runtime they are only added on concrete subclasses of 'Model'. The # metaclass also sets up correct inheritance from concrete parent models exceptions. @@ -34,6 +32,9 @@ class Model(metaclass=ModelBase): # and re-add them to correct concrete subclasses of 'Model' DoesNotExist: Final[type[ObjectDoesNotExist]] MultipleObjectsReturned: Final[type[BaseMultipleObjectsReturned]] + # This 'objects' attribute will be deleted, via the plugin, in favor of managing it + # to only exist on subclasses it exists on during runtime. + objects: ClassVar[Manager[Self]] class Meta: ... _meta: Options[Any] diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index bfbb298a3..3b33b8f52 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -379,7 +379,9 @@ def check_types_compatible( api.check_subtype(actual_type, expected_type, ctx.context, error_message, "got", "expected") -def add_new_sym_for_info(info: TypeInfo, *, name: str, sym_type: MypyType, no_serialize: bool = False) -> None: +def add_new_sym_for_info( + info: TypeInfo, *, name: str, sym_type: MypyType, no_serialize: bool = False, is_classvar: bool = False +) -> None: # type=: type of the variable itself var = Var(name=name, type=sym_type) # var.info: type of the object variable is bound to @@ -387,6 +389,7 @@ def add_new_sym_for_info(info: TypeInfo, *, name: str, sym_type: MypyType, no_se var._fullname = info.fullname + "." + name var.is_initialized_in_class = True var.is_inferred = True + var.is_classvar = is_classvar info.names[name] = SymbolTableNode(MDEF, var, plugin_generated=True, no_serialize=no_serialize) diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index a56b3c945..d9c3ab56b 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -70,8 +70,13 @@ def create_new_var(self, name: str, typ: MypyType) -> Var: var.is_inferred = True return var - def add_new_node_to_model_class(self, name: str, typ: MypyType, no_serialize: bool = False) -> None: - helpers.add_new_sym_for_info(self.model_classdef.info, name=name, sym_type=typ, no_serialize=no_serialize) + def add_new_node_to_model_class( + self, name: str, typ: MypyType, no_serialize: bool = False, is_classvar: bool = False + ) -> None: + # TODO: Rename to signal that it is a `Var` that is added.. + helpers.add_new_sym_for_info( + self.model_classdef.info, name=name, sym_type=typ, no_serialize=no_serialize, is_classvar=is_classvar + ) def add_new_class_for_current_module(self, name: str, bases: List[Instance]) -> TypeInfo: current_module = self.api.modules[self.model_classdef.info.module_name] @@ -311,7 +316,7 @@ def reparametrize_dynamically_created_manager(self, manager_name: str, manager_i assert manager_info is not None # Reparameterize dynamically created manager with model type manager_type = Instance(manager_info, [Instance(self.model_classdef.info, [])]) - self.add_new_node_to_model_class(manager_name, manager_type) + self.add_new_node_to_model_class(manager_name, manager_type, is_classvar=True) def run_with_model_cls(self, model_cls: Type[Model]) -> None: manager_info: Optional[TypeInfo] @@ -336,7 +341,7 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None: continue manager_type = Instance(manager_info, [Instance(self.model_classdef.info, [])]) - self.add_new_node_to_model_class(manager_name, manager_type) + self.add_new_node_to_model_class(manager_name, manager_type, is_classvar=True) if incomplete_manager_defs: if not self.api.final_iteration: @@ -351,7 +356,9 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None: # setting _some_ type fallback_manager_info = self.get_or_create_manager_with_any_fallback() self.add_new_node_to_model_class( - manager_name, Instance(fallback_manager_info, [Instance(self.model_classdef.info, [])]) + manager_name, + Instance(fallback_manager_info, [Instance(self.model_classdef.info, [])]), + is_classvar=True, ) # Find expression for e.g. `objects = SomeManager()` @@ -623,6 +630,10 @@ def adjust_model_class(cls, ctx: ClassDefContext) -> None: ): del ctx.cls.info.names["MultipleObjectsReturned"] + objects = ctx.cls.info.names.get("objects") + if objects is not None and isinstance(objects.node, Var) and not objects.plugin_generated: + del ctx.cls.info.names["objects"] + return def get_exception_bases(self, name: str) -> List[Instance]: diff --git a/tests/typecheck/managers/test_managers.yml b/tests/typecheck/managers/test_managers.yml index 4a9f4b16f..72c79b2e5 100644 --- a/tests/typecheck/managers/test_managers.yml +++ b/tests/typecheck/managers/test_managers.yml @@ -194,7 +194,16 @@ - case: managers_inherited_from_abstract_classes_multiple_inheritance main: | - from myapp.models import Child + from myapp.models import AbstractBase1, AbstractBase2, Child + reveal_type(Child.manager1) + reveal_type(Child.restricted) + reveal_type(AbstractBase1.manager1) + reveal_type(AbstractBase2.restricted) + out: | + main:2: note: Revealed type is "myapp.models.CustomManager1[myapp.models.Child]" + main:3: note: Revealed type is "myapp.models.CustomManager2[myapp.models.Child]" + main:4: note: Revealed type is "myapp.models.CustomManager1[myapp.models.AbstractBase1]" + main:5: note: Revealed type is "myapp.models.CustomManager2[myapp.models.AbstractBase2]" installed_apps: - myapp files: @@ -297,12 +306,13 @@ - path: myapp/__init__.py - path: myapp/models.py content: | + from typing import ClassVar from django.db import models class ParentOfMyModel4(models.Model): objects = models.Manager() class MyModel4(ParentOfMyModel4): - objects = models.Manager['MyModel4']() + objects: ClassVar[models.Manager["MyModel4"]] = models.Manager["MyModel4"]() # TODO: make it work someday #- case: inheritance_of_two_models_with_custom_objects_manager @@ -570,7 +580,7 @@ - path: myapp/__init__.py - path: myapp/models.py content: | - from typing import TypeVar + from typing import ClassVar, TypeVar from django.db import models T = TypeVar("T", bound="MyModel") @@ -585,7 +595,7 @@ pass class MySubModel(MyModel): - objects = MySubManager() + objects: ClassVar[MySubManager["MySubModel"]] = MySubManager() - case: subclass_manager_without_type_parameters_disallow_any_generics main: | @@ -598,14 +608,14 @@ [mypy-myapp.models] disallow_any_generics = true out: | - main:2: note: Revealed type is "myapp.models.MySubManager[myapp.models.MySubModel]" + main:2: note: Revealed type is "myapp.models.MySubManager" main:3: note: Revealed type is "Any" myapp/models:9: error: Missing type parameters for generic type "MyManager" files: - path: myapp/__init__.py - path: myapp/models.py content: | - from typing import TypeVar + from typing import ClassVar, TypeVar from django.db import models T = TypeVar("T", bound="MyModel") @@ -620,7 +630,7 @@ pass class MySubModel(MyModel): - objects = MySubManager() + objects: ClassVar[MySubManager] = MySubManager() - case: nested_manager_class_definition main: | diff --git a/tests/typecheck/models/test_abstract.yml b/tests/typecheck/models/test_abstract.yml index ca2bc7b13..ec4648680 100644 --- a/tests/typecheck/models/test_abstract.yml +++ b/tests/typecheck/models/test_abstract.yml @@ -51,8 +51,7 @@ Recursive(parent=Recursive(parent=None)) Concrete(parent=Concrete(parent=None)) out: | - main:4: error: Access to generic instance variables via class is ambiguous - main:4: error: Unexpected attribute "parent" for model "Recursive" + main:4: error: "Type[Recursive]" has no attribute "objects" main:5: error: Cannot instantiate abstract class "Recursive" with abstract attributes "DoesNotExist" and "MultipleObjectsReturned" main:5: error: Unexpected attribute "parent" for model "Recursive" installed_apps: @@ -210,4 +209,4 @@ def create_animal_generic(klass: Type[T], name: str) -> T: reveal_type(klass) # N: Revealed type is "Type[T`-1]" - return klass.objects.create(name=name) # E: Incompatible return value type (got "Animal", expected "T") + return klass._default_manager.create(name=name) diff --git a/tests/typecheck/models/test_contrib_models.yml b/tests/typecheck/models/test_contrib_models.yml index fa4065e34..9c8ed4f10 100644 --- a/tests/typecheck/models/test_contrib_models.yml +++ b/tests/typecheck/models/test_contrib_models.yml @@ -31,9 +31,11 @@ - case: can_override_abstract_user_manager main: | - from myapp.models import User - reveal_type(User.objects) # N: Revealed type is "myapp.models.UserManager[myapp.models.User]" - reveal_type(User.objects.all()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.User, myapp.models.User]" + from myapp.models import MyBaseUser, MyUser + reveal_type(MyBaseUser.objects) # N: Revealed type is "myapp.models.MyBaseUserManager[myapp.models.MyBaseUser]" + reveal_type(MyBaseUser.objects.all()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.MyBaseUser, myapp.models.MyBaseUser]" + reveal_type(MyUser.objects) # N: Revealed type is "myapp.models.MyUserManager" + reveal_type(MyUser.objects.all()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.MyUser, myapp.models.MyUser]" installed_apps: - django.contrib.auth - myapp @@ -42,8 +44,15 @@ - path: myapp/models.py content: | from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager - class UserManager(BaseUserManager["User"]): + from django.contrib.auth.models import AbstractUser, UserManager + from typing import ClassVar + class MyBaseUserManager(BaseUserManager["MyBaseUser"]): ... - class User(AbstractBaseUser): - objects = UserManager() + class MyBaseUser(AbstractBaseUser): + objects = MyBaseUserManager() + + class MyUserManager(UserManager["MyUser"]): + ... + class MyUser(AbstractUser): + objects: ClassVar[MyUserManager] = MyUserManager() diff --git a/tests/typecheck/models/test_meta_options.yml b/tests/typecheck/models/test_meta_options.yml index eb6ef30f2..e89b90fd3 100644 --- a/tests/typecheck/models/test_meta_options.yml +++ b/tests/typecheck/models/test_meta_options.yml @@ -77,10 +77,11 @@ # Should not raise: MyModel(field=1) MyModel.objects.create(field=2) + AbstractModel._default_manager.create() # Errors: AbstractModel() # E: Cannot instantiate abstract class "AbstractModel" with abstract attributes "DoesNotExist" and "MultipleObjectsReturned" - AbstractModel.objects.create() # E: Access to generic instance variables via class is ambiguous + AbstractModel.objects.create() # E: "Type[AbstractModel]" has no attribute "objects" installed_apps: - myapp files: From cc1f7c9f12e592061078825d39a735d8375a9583 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Mon, 4 Sep 2023 21:06:56 +0200 Subject: [PATCH 3/7] fixup! Declare manager class attributes on models as `ClassVar`s --- mypy_django_plugin/transformers/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index d9c3ab56b..14523ec29 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -440,7 +440,7 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None: default_manager_info = generated_manager_info default_manager = Instance(default_manager_info, [Instance(self.model_classdef.info, [])]) - self.add_new_node_to_model_class("_default_manager", default_manager) + self.add_new_node_to_model_class("_default_manager", default_manager, is_classvar=True) class AddRelatedManagers(ModelClassInitializer): From 2393ed5efaaeb18d672a9ba956bfe8a013436079 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Mon, 4 Sep 2023 21:47:58 +0200 Subject: [PATCH 4/7] Enforce appropriate keyword only arguments Co-authored-by: Nikita Sobolev --- mypy_django_plugin/lib/helpers.py | 2 +- mypy_django_plugin/transformers/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 3b33b8f52..f297ef943 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -380,7 +380,7 @@ def check_types_compatible( def add_new_sym_for_info( - info: TypeInfo, *, name: str, sym_type: MypyType, no_serialize: bool = False, is_classvar: bool = False + info: TypeInfo, name: str, sym_type: MypyType, *, no_serialize: bool = False, is_classvar: bool = False ) -> None: # type=: type of the variable itself var = Var(name=name, type=sym_type) diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index 14523ec29..fd0f77bef 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -71,7 +71,7 @@ def create_new_var(self, name: str, typ: MypyType) -> Var: return var def add_new_node_to_model_class( - self, name: str, typ: MypyType, no_serialize: bool = False, is_classvar: bool = False + self, name: str, typ: MypyType, *, no_serialize: bool = False, is_classvar: bool = False ) -> None: # TODO: Rename to signal that it is a `Var` that is added.. helpers.add_new_sym_for_info( From 69199297392c5bcb49662fd2643edeea5792de7d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 19:48:13 +0000 Subject: [PATCH 5/7] [pre-commit.ci] auto fixes from pre-commit.com hooks --- mypy_django_plugin/transformers/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index fd0f77bef..9c41ba961 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -71,7 +71,7 @@ def create_new_var(self, name: str, typ: MypyType) -> Var: return var def add_new_node_to_model_class( - self, name: str, typ: MypyType, *, no_serialize: bool = False, is_classvar: bool = False + self, name: str, typ: MypyType, *, no_serialize: bool = False, is_classvar: bool = False ) -> None: # TODO: Rename to signal that it is a `Var` that is added.. helpers.add_new_sym_for_info( From e96954f9e48218d553d207a96eb2c7a7299532e8 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Mon, 4 Sep 2023 21:49:49 +0200 Subject: [PATCH 6/7] Bump mypy --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index b2b58a5a6..7bc585b0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,4 @@ Django==4.2.4 -e .[compatible-mypy] # Overrides: -mypy==1.4.1 +mypy==1.5.1 diff --git a/setup.py b/setup.py index 254ceab3f..c9ca427a0 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def find_stub_files(name: str) -> List[str]: # Keep compatible-mypy major.minor version pinned to what we use in CI (requirements.txt) extras_require = { - "compatible-mypy": ["mypy==1.4.*"], + "compatible-mypy": ["mypy==1.5.*"], } setup( From 7e585207390ac00c1cc95624337c940aaf5b4c9f Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Mon, 4 Sep 2023 22:00:15 +0200 Subject: [PATCH 7/7] Remove a bunch of `-redefinition` lines from `allowlist_todo.txt` --- scripts/stubtest/allowlist_todo.txt | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/scripts/stubtest/allowlist_todo.txt b/scripts/stubtest/allowlist_todo.txt index a13a14670..bc8c92c4a 100644 --- a/scripts/stubtest/allowlist_todo.txt +++ b/scripts/stubtest/allowlist_todo.txt @@ -55,11 +55,8 @@ django.contrib.admin.models.LogEntry.change_message django.contrib.admin.models.LogEntry.content_type django.contrib.admin.models.LogEntry.content_type_id django.contrib.admin.models.LogEntry.get_action_flag_display -django.contrib.admin.models.LogEntry.get_action_flag_display-redefinition django.contrib.admin.models.LogEntry.get_next_by_action_time -django.contrib.admin.models.LogEntry.get_next_by_action_time-redefinition django.contrib.admin.models.LogEntry.get_previous_by_action_time -django.contrib.admin.models.LogEntry.get_previous_by_action_time-redefinition django.contrib.admin.models.LogEntry.id django.contrib.admin.models.LogEntry.object_id django.contrib.admin.models.LogEntry.object_repr @@ -156,21 +153,7 @@ django.contrib.auth.models.AbstractUser.email django.contrib.auth.models.AbstractUser.email_user django.contrib.auth.models.AbstractUser.first_name django.contrib.auth.models.AbstractUser.get_next_by_date_joined -django.contrib.auth.models.AbstractUser.get_next_by_date_joined-redefinition -django.contrib.auth.models.AbstractUser.get_next_by_date_joined-redefinition2 -django.contrib.auth.models.AbstractUser.get_next_by_date_joined-redefinition3 -django.contrib.auth.models.AbstractUser.get_next_by_date_joined-redefinition4 -django.contrib.auth.models.AbstractUser.get_next_by_date_joined-redefinition5 -django.contrib.auth.models.AbstractUser.get_next_by_date_joined-redefinition6 -django.contrib.auth.models.AbstractUser.get_next_by_date_joined-redefinition7 django.contrib.auth.models.AbstractUser.get_previous_by_date_joined -django.contrib.auth.models.AbstractUser.get_previous_by_date_joined-redefinition -django.contrib.auth.models.AbstractUser.get_previous_by_date_joined-redefinition2 -django.contrib.auth.models.AbstractUser.get_previous_by_date_joined-redefinition3 -django.contrib.auth.models.AbstractUser.get_previous_by_date_joined-redefinition4 -django.contrib.auth.models.AbstractUser.get_previous_by_date_joined-redefinition5 -django.contrib.auth.models.AbstractUser.get_previous_by_date_joined-redefinition6 -django.contrib.auth.models.AbstractUser.get_previous_by_date_joined-redefinition7 django.contrib.auth.models.AbstractUser.groups django.contrib.auth.models.AbstractUser.is_active django.contrib.auth.models.AbstractUser.is_staff @@ -205,13 +188,7 @@ django.contrib.auth.models.User.date_joined django.contrib.auth.models.User.email django.contrib.auth.models.User.first_name django.contrib.auth.models.User.get_next_by_date_joined -django.contrib.auth.models.User.get_next_by_date_joined-redefinition -django.contrib.auth.models.User.get_next_by_date_joined-redefinition2 -django.contrib.auth.models.User.get_next_by_date_joined-redefinition3 django.contrib.auth.models.User.get_previous_by_date_joined -django.contrib.auth.models.User.get_previous_by_date_joined-redefinition -django.contrib.auth.models.User.get_previous_by_date_joined-redefinition2 -django.contrib.auth.models.User.get_previous_by_date_joined-redefinition3 django.contrib.auth.models.User.groups django.contrib.auth.models.User.id django.contrib.auth.models.User.is_active @@ -835,9 +812,7 @@ django.contrib.sessions.base_session.AbstractBaseSession.Meta.verbose_name django.contrib.sessions.base_session.AbstractBaseSession.Meta.verbose_name_plural django.contrib.sessions.base_session.AbstractBaseSession.expire_date django.contrib.sessions.base_session.AbstractBaseSession.get_next_by_expire_date -django.contrib.sessions.base_session.AbstractBaseSession.get_next_by_expire_date-redefinition django.contrib.sessions.base_session.AbstractBaseSession.get_previous_by_expire_date -django.contrib.sessions.base_session.AbstractBaseSession.get_previous_by_expire_date-redefinition django.contrib.sessions.base_session.AbstractBaseSession.session_data django.contrib.sessions.base_session.AbstractBaseSession.session_key django.contrib.sessions.base_session.BaseSessionManager.__slotnames__ @@ -845,9 +820,7 @@ django.contrib.sessions.exceptions.SessionInterrupted django.contrib.sessions.management.commands.clearsessions.Command.handle django.contrib.sessions.models.Session.expire_date django.contrib.sessions.models.Session.get_next_by_expire_date -django.contrib.sessions.models.Session.get_next_by_expire_date-redefinition django.contrib.sessions.models.Session.get_previous_by_expire_date -django.contrib.sessions.models.Session.get_previous_by_expire_date-redefinition django.contrib.sessions.models.Session.session_data django.contrib.sessions.models.Session.session_key django.contrib.sessions.models.SessionManager.__slotnames__