mirror of
https://github.com/Hopiu/django-model-utils.git
synced 2026-03-16 20:00:23 +00:00
Refactor type variable declarations and manager mixins for improved type checking compatibility
This commit is contained in:
parent
152c619716
commit
548fc6735a
3 changed files with 32 additions and 22 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue