Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeError on django startup #955

Closed
hsej opened this issue Jul 27, 2018 · 12 comments
Closed

TypeError on django startup #955

hsej opened this issue Jul 27, 2018 · 12 comments

Comments

@hsej
Copy link

hsej commented Jul 27, 2018

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?

@carltongibson
Copy link
Owner

carltongibson commented Jul 27, 2018

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?)

@hsej
Copy link
Author

hsej commented Jul 27, 2018

Hi! It works without the ABC inheritance 馃憤

@hsej
Copy link
Author

hsej commented Jul 27, 2018

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

@rpkilby
Copy link
Collaborator

rpkilby commented Jul 27, 2018

A minimal example to recreate the TypeError is:

class Foo(abc.ABC, FilterView):
    pass

@rpkilby
Copy link
Collaborator

rpkilby commented Jul 27, 2018

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.

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

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

@carltongibson
Copy link
Owner

Thank you for the write-up @rpkilby!

@hsej
Copy link
Author

hsej commented Jul 27, 2018

Hi both of you!

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

All the best, Henrik

@hsej
Copy link
Author

hsej commented Jul 27, 2018

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"?

@rpkilby
Copy link
Collaborator

rpkilby commented Jul 27, 2018

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.

@hsej
Copy link
Author

hsej commented Jul 27, 2018

Thanks again, that was helpful.

@rpkilby
Copy link
Collaborator

rpkilby commented Jul 29, 2018

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).

@hsej
Copy link
Author

hsej commented Jul 29, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants