Refactor type variable declarations and manager mixins for improved type checking compatibility

This commit is contained in:
Benedikt Willi 2025-12-16 09:02:41 +01:00
parent 152c619716
commit 548fc6735a
3 changed files with 32 additions and 22 deletions

View file

@ -10,23 +10,29 @@ from django.db.models.fields.related import OneToOneField, OneToOneRel
from django.db.models.query import ModelIterable, QuerySet
from django.db.models.sql.datastructures import Join
ModelT = TypeVar('ModelT', bound=models.Model, covariant=True)
ModelT = TypeVar('ModelT', bound=models.Model)
if TYPE_CHECKING:
from collections.abc import Iterator
from django.db.models.query import BaseIterable
# Generic base for mixin classes - enables type checking support
# while avoiding runtime issues with __class__ assignment
# (e.g., when used with django-modeltranslation).
_GenericMixin = Generic[ModelT]
# During type checking, _GenericMixin is just an empty base class.
# The actual generic behavior comes from Generic[ModelT] in the mixin classes.
class _GenericMixin:
"""Type checking placeholder - generics handled by Generic[ModelT]."""
pass
else:
# At runtime, use a subscriptable but non-Generic class to avoid
# __class__ assignment issues that occur when Generic[T] is in
# the class hierarchy (e.g., django-modeltranslation compatibility).
class _GenericMixin:
"""Runtime placeholder for Generic[ModelT] that supports subscripting."""
"""Runtime placeholder for Generic[ModelT] that supports subscripting.
This class serves as a base for mixin classes to enable type checking support
while avoiding runtime issues with __class__ assignment (e.g., when used with
django-modeltranslation).
"""
def __class_getitem__(cls, item: Any) -> type[_GenericMixin]:
return cls
@ -76,7 +82,7 @@ else:
return _iter_inheritance_queryset(self.queryset)
class InheritanceQuerySetMixin(_GenericMixin):
class InheritanceQuerySetMixin(_GenericMixin, Generic[ModelT]):
model: type[ModelT]
subclasses: Sequence[str]
@ -239,7 +245,7 @@ class InheritanceQuerySet(InheritanceQuerySetMixin[ModelT], QuerySet[ModelT]):
)
class InheritanceManagerMixin(_GenericMixin):
class InheritanceManagerMixin(_GenericMixin, Generic[ModelT]):
_queryset_class = InheritanceQuerySet
if TYPE_CHECKING:
@ -335,7 +341,7 @@ class InheritanceManager(InheritanceManagerMixin[ModelT], models.Manager[ModelT]
pass
class QueryManagerMixin(_GenericMixin):
class QueryManagerMixin(_GenericMixin, Generic[ModelT]):
@overload
def __init__(self, *args: models.Q):
@ -369,7 +375,7 @@ class QueryManager(QueryManagerMixin[ModelT], models.Manager[ModelT]): # type:
pass
class SoftDeletableQuerySetMixin(_GenericMixin):
class SoftDeletableQuerySetMixin(_GenericMixin, Generic[ModelT]):
"""
QuerySet for SoftDeletableModel. Instead of removing instance sets
its ``is_removed`` field to True.
@ -389,7 +395,7 @@ class SoftDeletableQuerySet(SoftDeletableQuerySetMixin[ModelT], QuerySet[ModelT]
pass
class SoftDeletableManagerMixin(_GenericMixin):
class SoftDeletableManagerMixin(_GenericMixin, Generic[ModelT]):
"""
Manager that limits the queryset by default to show only not removed
instances of model.

View file

@ -27,7 +27,7 @@ from model_utils.models import (
from model_utils.tracker import FieldInstanceTracker, FieldTracker, ModelTracker
from tests.fields import MutableField
ModelT = TypeVar('ModelT', bound=models.Model, covariant=True)
ModelT = TypeVar('ModelT', bound=models.Model)
class InheritanceManagerTestRelated(models.Model):
@ -44,7 +44,7 @@ class InheritanceManagerTestParent(models.Model):
related_self = models.OneToOneField(
"self", related_name="imtests_self", null=True,
on_delete=models.CASCADE)
objects: ClassVar[InheritanceManager[InheritanceManagerTestParent]] = InheritanceManager()
objects: ClassVar[InheritanceManager[InheritanceManagerTestParent]] = InheritanceManager() # type: ignore[assignment]
def __str__(self) -> str:
return "{}({})".format(
@ -56,7 +56,7 @@ class InheritanceManagerTestParent(models.Model):
class InheritanceManagerTestChild1(InheritanceManagerTestParent):
non_related_field_using_descriptor_2 = models.FileField(upload_to="test")
normal_field_2 = models.TextField()
objects: ClassVar[InheritanceManager[InheritanceManagerTestParent]] = InheritanceManager()
objects: ClassVar[InheritanceManager[InheritanceManagerTestParent]] = InheritanceManager() # type: ignore[assignment]
class InheritanceManagerTestGrandChild1(InheritanceManagerTestChild1):
@ -184,10 +184,10 @@ class Post(models.Model):
order = models.IntegerField()
objects = models.Manager()
public: ClassVar[QueryManager[Post]] = QueryManager(published=True)
public_confirmed: ClassVar[QueryManager[Post]] = QueryManager(
public: ClassVar[QueryManager[Post]] = QueryManager(published=True) # type: ignore[assignment]
public_confirmed: ClassVar[QueryManager[Post]] = QueryManager( # type: ignore[assignment]
models.Q(published=True) & models.Q(confirmed=True))
public_reversed: ClassVar[QueryManager[Post]] = QueryManager(
public_reversed: ClassVar[QueryManager[Post]] = QueryManager( # type: ignore[assignment]
published=True).order_by("-order")
class Meta:

View file

@ -1,15 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from django.db import models
from django.test import SimpleTestCase
from model_utils.managers import (
InheritanceManager,
JoinManager,
QueryManager,
SoftDeletableManager,
)
if not TYPE_CHECKING:
from model_utils.managers import JoinManager
class ManagerClassAssignmentTests(SimpleTestCase):
"""
@ -27,7 +31,7 @@ class ManagerClassAssignmentTests(SimpleTestCase):
def test_softdeletable_manager_class_can_be_reassigned(self) -> None:
"""SoftDeletableManager instances support __class__ reassignment."""
manager = SoftDeletableManager()
manager: Any = SoftDeletableManager()
class PatchedManager(SoftDeletableManager):
pass
@ -37,7 +41,7 @@ class ManagerClassAssignmentTests(SimpleTestCase):
def test_inheritance_manager_class_can_be_reassigned(self) -> None:
"""InheritanceManager instances support __class__ reassignment."""
manager = InheritanceManager()
manager: Any = InheritanceManager()
class PatchedManager(InheritanceManager):
pass
@ -47,7 +51,7 @@ class ManagerClassAssignmentTests(SimpleTestCase):
def test_query_manager_class_can_be_reassigned(self) -> None:
"""QueryManager instances support __class__ reassignment."""
manager = QueryManager(is_active=True)
manager: Any = QueryManager(is_active=True)
class PatchedManager(models.Manager):
pass
@ -57,7 +61,7 @@ class ManagerClassAssignmentTests(SimpleTestCase):
def test_join_manager_class_can_be_reassigned(self) -> None:
"""JoinManager instances support __class__ reassignment."""
manager = JoinManager()
manager: Any = JoinManager() # type: ignore[name-defined]
class PatchedManager(models.Manager):
pass