Django-filter: TypeError on django startup

Created on 27 Jul 2018  ·  12Comments  ·  Source: carltongibson/django-filter

First of all, thank you for maintaining this amazing package 👍
I just updated django-filter 1.1.0 to 2.0.0 and now the following CBV fails at django startup:

class ForventetRegnskab(ABC, LoginRequiredMixin, UserPassesTestMixin, FilterView):
    """
    Abstract base class for all "brugervisninger".
    Each subclass must set the appropriate filterset_class and queryset.
    """
    login_url = 'home'
    raise_exception = True
    context_object_name = 'context'
    template_name = 'forventet_regnskab/content.html'

    @property
    @abstractmethod
    def filterset_class(self) -> Optional[FilterView.filterset_class]:
        pass

    @property
    @abstractmethod
    def queryset(self):
        pass

    @has_sted1_permission
    def user_passes_test(self, request, kwargs) -> bool:
        """
        Deny a request with a permission error if the method returns False.

        :param request: The request object
        :param kwargs: self.kwargs
        """
        pass

    def test_func(self):
        return self.user_passes_test(self.request, self.kwargs)

    def get_context_data(self, **kwargs):
        context = super(ForventetRegnskab, self).get_context_data(**kwargs)
        filter_object = self.filterset_class(self.request.GET, queryset=self.get_queryset())

        # Add context variables
        context['title'] = constants.FORVENTET_REGNSKAB_TITLE
        context['filter_sum'] = self.filter_sum(filter_queryset=filter_object.qs)

        return context

    def get_queryset(self):
        aar = self.request.GET.get('aar')
        maaned = self.request.GET.get('maaned')
        kasse = self.request.GET.get('kasse')

        if aar and maaned and kasse:
            query = self.queryset.filter(sted1=self.kwargs['sted1']) \
                .annotate(
                forbrugsprocent=Case(
                    When(Q(korrigeret_budget__gt=0),
                         then=(F('akkumuleret_regnskab') / F('korrigeret_budget')) * 100),
                    default=0,
                    output_field=IntegerField()),
                mer_mindre_forbrug=Func(
                    F('forventet_regnskab'), function='ABS') - Func(F('korrigeret_budget'), function='ABS'))
        else:
            query = self.queryset.none()

        return query

    @staticmethod
    def filter_sum(filter_queryset: QuerySet) -> QuerySet:
        """
        Aggregate the filtered queryset.
        """
        filter_sum = filter_queryset.aggregate(
            korrigeret_budget_sum=Sum('korrigeret_budget'),
            regnskab_sum=Sum('regnskab'),
            akkumuleret_regnskab_sum=Sum('akkumuleret_regnskab'),
            forventet_regnskab_sum=Sum('forventet_regnskab'),
            forbrugsprocent_sum=Case(
                When(Q(korrigeret_budget_sum__gt=0),
                     then=(Sum('akkumuleret_regnskab') / Sum('korrigeret_budget')) * 100),
                default=0,
                output_field=IntegerField()),
            mer_mindre_forbrug_sum=Func(
                Sum('forventet_regnskab'), function='ABS') - Func(Sum('korrigeret_budget'), function='ABS'))

        return filter_sum

Each subclass implements its own filterset_class and queryset. Here is an example:

class SpecialBrugervisning(ForventetRegnskab):
    filterset_class = filters.SpecialBrugervisning
    queryset = models.SpecialBrugervisning.objects.all()

I get this error at startup:

File "[...]\oekonomistyring\forventet_regnskab\views.py", line 18, in
class ForventetRegnskab(ABC, LoginRequiredMixin, UserPassesTestMixin, FilterView):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

Hmmm... !

Is this behavior wanted or is this a bug?

Most helpful comment

This doesn't look specific to django-filter, more that a class cannot inherit two separate metaclasses.

class AMeta(type): pass
class BMeta(type): pass

class A(metaclass=AMeta): pass
class B(metaclass=BMeta): pass

# fails with metaclass error
class C(A, B): pass

# passes
class CMeta(AMeta, BMeta): pass
class C(A, B, metaclass=CMeta): pass

The relevant change in 2.x is that FilterMixin uses the FilterMixinRenames metaclass, creating the conflict with ABCMeta. You should be able to create a metclass without directly referencing the view's metaclass.

```python
class MyMeta(ABCMeta, type(FilterView)):
pass

class ForventetRegnskab(LoginRequiredMixin, UserPassesTestMixin, FilterView, metaclass=MyMeta):
pass
``

All 12 comments

What happens if you drop the ABC inheritance?

(More: any chance you could reduce this to a minimal example so I can have a play myself?)

Hi! It works without the ABC inheritance 👍

The minimal example:

class ForventetRegnskabNoABC(LoginRequiredMixin, UserPassesTestMixin, FilterView):
    login_url = 'home'
    raise_exception = True
    context_object_name = 'context'
    template_name = 'content.html'

    filterset_class = filters.my_filterset
    queryset = models.my_queryset

    def test_func(self):
        return True

    def get_context_data(self, **kwargs):
        context = super(ForventetRegnskabNoABC, self).get_context_data(**kwargs)
        filter_object = self.filterset_class(self.request.GET, queryset=self.get_queryset())

        # Add context variables
        context['filter_sum'] = self.filter_sum(filter_queryset=filter_object.qs)

        return context

    def get_queryset(self):
        return self.queryset

    @staticmethod
    def filter_sum(filter_queryset: QuerySet) -> QuerySet:
        filter_sum = filter_queryset.aggregate()

        return filter_sum

A minimal example to recreate the TypeError is:

class Foo(abc.ABC, FilterView):
    pass

This doesn't look specific to django-filter, more that a class cannot inherit two separate metaclasses.

class AMeta(type): pass
class BMeta(type): pass

class A(metaclass=AMeta): pass
class B(metaclass=BMeta): pass

# fails with metaclass error
class C(A, B): pass

# passes
class CMeta(AMeta, BMeta): pass
class C(A, B, metaclass=CMeta): pass

The relevant change in 2.x is that FilterMixin uses the FilterMixinRenames metaclass, creating the conflict with ABCMeta. You should be able to create a metclass without directly referencing the view's metaclass.

```python
class MyMeta(ABCMeta, type(FilterView)):
pass

class ForventetRegnskab(LoginRequiredMixin, UserPassesTestMixin, FilterView, metaclass=MyMeta):
pass
``

Thank you for the write-up @rpkilby!

Hi both of you!

@rpkilby very clear answer - and your solution works for the record! 🥇

All the best, Henrik

Hi again!

I Hope you have the time to clarify one thing: Why do you use type(FilterView) and not just FilterView?
Will it be interpreted as "metaclass MyABC IS metaclass FilterView"?

The type of a class is it's metaclass. i.e., the default metaclass is type.

type(FilterView) creates a FilterView metaclass

Not exactly, it just gets the metaclass (not creating a new metaclass). This is an alternative to referencing FilterMixinRenames explicitly.

The keyword is used to assign 1) as the only metaclass for all subclasses?

It would apply to the class and its subclasses, unless overridden by another metaclass.

Thanks again, that was helpful.

Everything in Python is an object, and everything is constructed from a class. i.e., the class of an instance is its class, the class of a class is its metaclass. The only special case is the base type, which has a type of type.

If you say that a class can't inherit two separate metaclasses, it makes sence to "merge" them into one and use that one in a subclass?

I mistyped. A class cannot be created from two metaclasses. It can however, be created from a metaclass that inherits multiple other metaclasses. This is similar to how you cannot instantiate an object with two classes. You would need to create one class that inherits both and instantiate the object from that. Class creation is the same way.

I assume that the metaclass of FilterClass is inherited from FilterMixinRenames...

It's metaclass is FilterMixinRenames. You can verify this with type(FilterView).

Thank you! Everything makes sense now - yes, I did the type checking it adds up - it is not magic :relaxed:

Was this page helpful?
0 / 5 - 0 ratings