Brady Dean
Brady Dean

Reputation: 3573

How can a metaclass access attributes defined on a child class?

I'm working on a class generator of sorts. I want to be able to create many children of BasePermission that are customizable using a required_scopes attribute. This is what I have so far.

from rest_framework.permissions import BasePermission


class ScopedPermissionMeta(type(BasePermission)):

    def __init__(self, name, bases, attrs):
        try:
            required_scopes = attrs['required_scopes']
        except KeyError:
            raise TypeError(f'{name} must include required_scopes attribute.')

        required_scopes_list = ' '.join(required_scopes)
        attrs['message'] = f'Resource requires scope={required_scopes_list}'

        def has_permission(self, request, _view):
            """Check required scopes against requested scopes."""
            try:
                requested_scopes = request.auth.claims['scope']
            except (AttributeError, KeyError):
                return False

            return all(scope in requested_scopes for scope in required_scopes)

        attrs['has_permission'] = has_permission


class ReadJwtPermission(BasePermission, metaclass=ScopedPermissionMeta):
    required_scopes = ['read:jwt']

However, I do not like how the ReadJwtPermisson class (and the many more children) must specify a metaclass. Idealy I want that detail abstracted away. I want to be able to do something like:

class ScopedPermission(BasePermission, metaclass=ScopedPermissionMeta):
    pass


class ReadJwtPermission(ScopedPermission):
    required_scopes = ['read:jwt']

but in this situation the metaclass see's ScopedPermission and no required_scopes. Is there a way to allow the metaclass to see through this inheritance relationship?

Upvotes: 1

Views: 416

Answers (1)

S.B
S.B

Reputation: 16476

but in this situation the metaclass see's ScopedPermission and no required_scopes. Is there a way to allow the metaclass to see through this inheritance relationship?

At the moment ScopedPermission class is being created, there is no ReadJwtPermission class. Interpreter can't predict the future that a class is going to subclass ScopedPermission which has required_scopes attribute. But you can do a bit different.

Child classes inherit parent's metaclass. If the parent class uses that metaclass, every child classes have to have that attribute you want. Also I used __new__ to check that attribute "before" the class creation. Here is the example :

class Metaclass(type):
    def __new__(mcs, name, bases, attrs):

        # This condition skips Base class's requiement for having "required_scopes"
        # otherwise you should specify "required_scopes" for Base class as well.
        if name == 'Base':
            return super().__new__(mcs, name, bases, attrs)

        try:
            required_scopes = attrs['required_scopes']
        except KeyError:
            raise TypeError(f'{name} must include "required_scopes" attribute.')

        required_scopes_list = ' '.join(required_scopes)
        attrs['message'] = f'Resource requires scope={required_scopes_list}'

        # setting "has_permission attribute here"
        attrs['has_permission'] = mcs.has_permission()

        return super().__new__(mcs, name, bases, attrs)

    # I just removed the implementation so that I can be able to run this class.
    @staticmethod
    def has_permission():
        pass


class Base(metaclass=Metaclass):
    pass

class A(Base):
    required_scopes = ['read:jwt']

print(A.message)

output :

Resource requires scope=read:jwt

But now with :

class B(Base):
    pass

It raises error...

Traceback (most recent call last):
  File "<--->", line 9, in __new__
    required_scopes = attrs['required_scopes']
KeyError: 'required_scopes'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<--->", line 34, in <module>
    class B(Base):
  File "<--->", line 11, in __new__
    raise TypeError(f'{name} must include "required_scopes" attribute.')
TypeError: B must include "required_scopes" attribute.

Upvotes: 2

Related Questions