Skip to content

Reparametrize implicit generic QuerySet subclasses#3217

Merged
sobolevn merged 1 commit into
typeddjango:masterfrom
federicobond:fix/annotate-non-generic-custom-queryset
Mar 24, 2026
Merged

Reparametrize implicit generic QuerySet subclasses#3217
sobolevn merged 1 commit into
typeddjango:masterfrom
federicobond:fix/annotate-non-generic-custom-queryset

Conversation

@federicobond

Copy link
Copy Markdown
Contributor

When a custom QuerySet subclass is defined without explicit type parameters (e.g. class MyQS(QuerySet): ...), make it implicitly generic by copying the parent QuerySet's type variables, analogous to the existing reparametrize_any_manager_hook for Manager subclasses.

This enables the annotate plugin hook to propagate annotation type information through custom querysets via copy_modified(args=...), which requires the queryset class to have type variables.

Disclosure: I've used Claude Opus 4.6 to assist in writing this changeset, but I have carefully iterated and manually reviewed the results.

@federicobond federicobond force-pushed the fix/annotate-non-generic-custom-queryset branch from 2ceb4e0 to e771a82 Compare March 24, 2026 04:59

@sobolevn sobolevn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, test this with explicit disallow_any_generics = True, since your docstring mentions this.

When a custom QuerySet subclass is defined without explicit type
parameters (e.g. `class MyQS(QuerySet): ...`), make it implicitly
generic by copying the parent QuerySet's type variables, analogous
to the existing `reparametrize_any_manager_hook` for Manager
subclasses.

This enables the annotate plugin hook to propagate annotation type
information through custom querysets via `copy_modified(args=...)`,
which requires the queryset class to have type variables.
@federicobond federicobond force-pushed the fix/annotate-non-generic-custom-queryset branch from e771a82 to eaa2701 Compare March 24, 2026 08:05
@sobolevn sobolevn merged commit ca4389e into typeddjango:master Mar 24, 2026
55 checks passed
@sobolevn

Copy link
Copy Markdown
Member

Thanks a lot!

@federicobond

Copy link
Copy Markdown
Contributor Author

Thank you. Was just about to comment on the new test, got sidetracked.

@UnknownPlatypus

UnknownPlatypus commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

You are too fast ahah, I also did that in #2776 + a small refactor, I'll extract the refactor, it was rather nice.
Currently both functions are exact duplicates of 35+ lines with only variable names changing

This is what I ended up with:

def reparametrize_any_manager_hook(ctx: ClassDefContext) -> None:
    """
    Add implicit generics to manager classes that are defined without generic.

    Eg.

        class MyManager(models.Manager): ...

    is interpreted as:

        _T = TypeVar("_T", bound=Model, covariant=True)
        _QS = TypeVar("_QS", bound=QuerySet[Any], covariant=True, default=QuerySet[_T])
        class MyManager(models.Manager[_T, _QS]): ...
    """
    reparametrize_generic_class(ctx, fullnames.BASE_MANAGER_CLASS_FULLNAME)


def reparametrize_any_queryset_hook(ctx: ClassDefContext) -> None:
    """
    Add implicit generics to queryset classes that are defined without generic.

    Eg.

        class MyQuerySet(models.QuerySet): ...

    is interpreted as:

        _T = TypeVar("_T", bound=Model, covariant=True)
        class MyQuerySet(models.QuerySet[_T]): ...
    """
    reparametrize_generic_class(ctx, fullnames.QUERYSET_CLASS_FULLNAME)

@federicobond

Copy link
Copy Markdown
Contributor Author

Makes sense, I decided not to prematurely abstract without a better understanding of the project, but you have more context than I do.

@UnknownPlatypus

Copy link
Copy Markdown
Contributor

No worries, thanks for the contribution, this is very useful !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants