Skip to content

Making QuerySet classes part of the type of Manager classes #863

@w0rp

Description

@w0rp

This is not a bug report, but a suggestion for a feature. This is a breaking change and requires some hackery. It's something I'm doing in my project, and something to consider for other projects. At the very least this will explain to others how to do what I'm doing.

I use this library with Pyright and without the mypy plugin. I have found a rudimentary way of getting custom QuerySet class methods to appear for my managers by doing something close to the following:

QS = TypeVar('QS', bound='QuerySet')  # type: ignore

class TypedManager(Generic[M, QS], Manager[M]):
   def all(self) -> QS: ...

    # ... Many other methods are re-declared here to use `QS` as the QuerySet type  ...

I can then declare my QuerySet and Manager classes like so:

# `cast` just to avoid runtime errors, as I don't monkey patch the class.
# In my codebase I have a wrapper type for this.
class FooQuerySet(cast('Type[QuerySet[Foo]]', QuerySet)):
    def custom_method(self) -> 'FooQuerySet':
        ...

class FooManager(TypedManager['Foo', FooQuerySet]):
    ...

Then the type of Foo.objects.all().custom_method() will be known. One of the caveats is that you cannot use QuerySet methods in the Manager class with from_queryset, as there's no way to transform types like that in Python typing at the moment, especially considering the queryset_only flag. As such, you have to use .all() before calling custom QuerySet methods.

I suggest changing the Manager class type arguments to include the QuerySet type, especially if these generic parameters are ever going to end up in the Django codebase itself. It's the only way to accurately express the types when doing type checking without a plugin system.

One thing to note is that you have to disable type errors for the bound, but the rest of the code will work in Pyright. This might not work in all type checkers for Python. Ideally, Python typing should be updated to make it possible to express the following instead:

QS = TypeVar('QS', bound='QuerySet[M]')

This is just a feature missing in Python typing at the moment. It is probably worth pushing for Python type checkers to be able to do this before implementing something like this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpyrightRelated to pyright type checker

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions