How do calls of models.Manager and custom managers work?

The following question was asked by a different user that subsequently removed the question. But I think it is useful to "dig" a bit in how Django's manager logic works.

I extended the models.Manager class and created a custom manager.

class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status=Post.Status.PUBLISHED)

It's more than understandable, BUT how do calls of managers work?

objects = models.Manager()  # The default manager.
published = PublishedManager()  # Our custom manager.

I don't address get_queryset() method, I just call constructors. Then how does it work?

It does not need to, the .all() [Django-doc], .get(…) [Django-doc], etc. all behind the curtains call get_queryset() and then perform that method on the result [GitHub]:

class BaseManager:
    # ...
    
    @classmethod
    def _get_queryset_methods(cls, queryset_class):
        def create_method(name, method):
            @wraps(method)
            def manager_method(self, *args, **kwargs):
                return getattr(self.get_queryset(), name)(*args, **kwargs)

            return manager_method

        new_methods = {}
        for name, method in inspect.getmembers(
            queryset_class, predicate=inspect.isfunction
        ):
            # Only copy missing methods.
            if hasattr(cls, name):
                continue
            # Only copy public methods or methods with the attribute
            # queryset_only=False.
            queryset_only = getattr(method, "queryset_only", None)
            if queryset_only or (queryset_only is None and name.startswith("_")):
                continue
            # Copy the method onto the manager.
            new_methods[name] = create_method(name, method)
        return new_methods

This inspects the methods of the queryset class it will wrap. Now if the QuerySet has a method .all(), it will create a small function .all() for the manager, that first calls the get_queryset() of the manager, and then .all() on the queryset. It does that for all functions defined in the queryset class, and thus creates for each a function for the manager.

These are then injected as members in the Manager with [GitHub]:

class BaseManager:
    # ...
    
    @classmethod
    def from_queryset(cls, queryset_class, class_name=None):
        if class_name is None:
            class_name = "%sFrom%s" % (cls.__name__, queryset_class.__name__)
        return type(
            class_name,
            (cls,),
            {
                "_queryset_class": queryset_class,
                **cls._get_queryset_methods(queryset_class),
            },
        )

But there is something else that plays here: how does the manager knows what the model is? Django's models call .contribute_to_class(…) on every attribute in the class, if it is available, for a manager, that looks like [GitHub]:

class BaseManager:
    # ...
    
    def contribute_to_class(self, cls, name):
        self.name = self.name or name
        self.model = cls

        setattr(cls, name, ManagerDescriptor(self))

        cls._meta.add_manager(self)

This thus injects the model into the manager, which was not even known at the time you constructed the item. It does not sett the manager itself on the model class, but a descriptor of it.

The descriptor then will [delegate getting the attribute to the manager:

class ManagerDescriptor:
    # ...

    def __get__(self, instance, cls=None):
        if instance is not None:
            raise AttributeError(
                "Manager isn't accessible via %s instances" % cls.__name__
            )

        if cls._meta.abstract:
            raise AttributeError(
                "Manager isn't available; %s is abstract" % (cls._meta.object_name,)
            )

        if cls._meta.swapped:
            raise AttributeError(
                "Manager isn't available; '%s' has been swapped for '%s'"
                % (
                    cls._meta.label,
                    cls._meta.swapped,
                )
            )

        return cls._meta.managers_map[self.manager.name]

This will prevent that you can use my_book.objects, and thus only use Book.objects, and also prevents calling the manager on an abstract models.

Back to Top