Xavier Arias Botargues
Xavier Arias Botargues

Reputation: 357

Python: Storing class type on a class variable durig class initialization

I'm trying to initialize an objects field with a class that needs to know the type that is using it:

class Device(Model):
    objects = AbstractManager(Device)

    # the rest of the class here

This is how AbstractManager is defined:

class AbstractManager:
    def __init__(self, cls: type):
        self.cls = cls

    def all(self):
        result = []

        for cls in self._get_subclasses():
            result.extend(list(cls.objects.all()))

        return result

    def _get_subclasses(self):
        return self.cls.__subclasses__()

So I can later call this and returns all() from all subclasses:

Device.objects.all()

The issue here is that I cannot use Device while initializing Device.objects, since Device is still not initialized.

As a work-around I'm initializing this outside of the class, but there's gotta be a better way:

class Device(Model):
    objects = None

    # the rest of the class here

Device.objects = AbstractManager(Device)

PD: I have a C#/C++ background, so maybe I'm thinking too much about this in a static-typing mindset, can't tell

Upvotes: 1

Views: 151

Answers (2)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 477676

You do not have to do that. Django will automatically call the contribute_to_class method, where it will pass the model, and for a manager, it will be stored in self.model. You can thus simply implement this as:

from django.db.models.manager import ManagerDescriptor

class AbstractManager(models.Manager):

    def all(self):
        result = []

        for cls in self._get_subclasses():
            result.extend(list(cls.objects.all()))

        return result

    def contribute_to_class(self, model, name):
        self.name = self.name or name
        self.model = model
        setattr(model, name, AbstractManagerDescriptor(self))

        model._meta.add_manager(self)

    def _get_subclasses(self):
        return self.model.__subclasses__()

class AbstractManagerDescriptor(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.swapped:
            raise AttributeError(
                "Manager isn't available; '%s.%s' has been swapped for '%s'" % (
                    cls._meta.app_label,
                    cls._meta.object_name,
                    cls._meta.swapped,
                )
            )

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

and add the manager as:

class Device(models.Model):
    objects = AbstractManager()

That being said, I'm not sure that this is a good idea for two reasons:

  1. you are returning a list, and normally .all() returns a QuerySet, you thus here "destroy" the laziness of the queryset, which can result in expensive querying; and
  2. if one would use Device.objects.filter() for example, it would simply circumvent.

You might want to subclass the queryset, and then aim to implement that differently.

Upvotes: 2

neverwalkaloner
neverwalkaloner

Reputation: 47374

You don't need to add any additional logic for this. Django allows you to access model class from manager using self.model attribute:

def _get_subclasses(self):
    return self.model.__subclasses__()

Upvotes: 2

Related Questions