DEV Community

Aman Mamgain
Aman Mamgain

Posted on

Keeping things DRY with Python Meta Programming

A very common and useful pattern when using frameworks is they force coupling between class attributes and methods to work properly.

An Example:


class LeadChoiceQueries(ObjectType):
    industries = graphene.List(ChoiceType) 

    def resolve_industries(parent, info):
        return [
            ChoiceType(value, _) for value, _ in
            IndustryCategory.CHOICES]

In this example we have an attribute industries it's type is graphene List and to finally put data in it, the framework relies on the resolve_industries method.

Recently I had to write a lot of these resolvers

class LeadChoiceQueries(ObjectType):
    industries = graphene.List(ChoiceType)
    residence_types = graphene.List(ChoiceType)
    organization_types = graphene.List(ChoiceType)
    designations = graphene.List(ChoiceType)
    education_levels = graphene.List(ChoiceType)
    profession_categories = graphene.List(ChoiceType)
    dependent_choices = graphene.List(ChoiceType)
    marital_statuses = graphene.List(ChoiceType)
    gender_choices = graphene.List(ChoiceType)

    def resolve_industries(parent, info):
        return [
            ChoiceType(value, _) for value, _ in
            IndustryCategory.CHOICES]

    def resolve_residence_types(parent, info):
        return [
            ChoiceType(value, _) for value, _ in
            ResidenceType.CHOICES]

    def resolve_organization_types(parent, info):
        return [
            ChoiceType(value, _) for value, _ in
            OrganizationType.CHOICES]

    def resolve_designations(parent, info):
        return [
            ChoiceType(value, _) for value, _ in
            Designation.CHOICES]

    def resolve_education_levels(parent, info):
        return [
            ChoiceType(value, _) for value, _ in
            EducationLevel.CHOICES]

    def resolve_profession_categories(parent, info):
        return [
            ChoiceType(value, _) for value, _ in
            ProfessionCategory.CHOICES]

    def resolve_dependent_choices(parent, info):
        return [
            ChoiceType(value, _) for value, _ in
            DependentChoices.CHOICES]

    def resolve_marital_statuses(parent, info):
        return [
            ChoiceType(value, _) for value, _ in
            MaritalStatus.CHOICES]

    def resolve_gender_choices(parent, info):
        return [
            ChoiceType(value, _) for value, _ in
            Gender.CHOICES]

As you can see, so many resolvers doing the same thing. So Sad!

If there was just some way to dynamically add these resolver methods to the class.

Python Meta-Programming to the rescue


class ChoiceQueryResolverAdder(type(ObjectType)):
    def __new__(cls, name, bases, attr):
        def resolve_choices(choices):
            def resolve_choices_inner(parent, info):
                return [ChoiceType(value, _) for value, _ in choices]
            return resolve_choices_inner

        new_attr = {}
        for key in attr:
            new_attr[key] = attr[key]
            if hasattr(attr[key], "CHOICES"):
                new_attr["resolve_" + key] = resolve_choices(attr[key].CHOICES)
                new_attr[key] = graphene.List(ChoiceType)
        return super(ChoiceQueryResolverAdder, cls).__new__(
            cls, name, bases, new_attr)

class LeadChoiceQueries(ObjectType, metaclass=ChoiceQueryResolverAdder):
    industries = IndustryCategory
    residence_types = ResidenceType
    organization_types = OrganizationType
    designations = Designation
    education_levels = EducationLevel
    profession_categories = ProfessionCategory
    dependent_choices = DependentChoices
    marital_statuses = MaritalStatus
    gender_choices = Gender

The key to it is ChoiceQueryResolverAdder metaclass.
It iterates over the attributes of the class it will generate and checks if the attribute has a CHOICE attribute and if it does, adds the resolver method using those choices.

And voila, 100 lines of code down to 35.
And it's super DRY
Sucesss!!

Top comments (0)