Home Reusable Arguments in Graphene-Django
Post
Cancel

Reusable Arguments in Graphene-Django

It’s funny the sort of problems one encounters while working on real-world projects. Problems that seem simple in theory but don’t seem to have been mentioned (even in passing) in the documentation of whatever package you’re using. More than that, you can’t seem to find any question asked by a stranger online along the same lines.

Before we get too far, I will assume that you have a working knowledge of GraphQL as well as Graphene-Django. As such, I won’t go into the details of setting up the codebase or explaining such terms as query, arguments etc.

Some context: I was building a GraphQL service - built with Django using Graphene-Django - and I was looking to define arguments that could be used across different GraphQL queries. (Remember, keep your code DRY.) Sounds simple enough, right?

A quick scan of the documentation didn’t reveal any solution to my problem. So I did what any developer these days does: I searched the web. The top results from StackOverflow didn’t reveal anything that matched my specific need so I decided to try and figure things out on my own; this actually proved to be simpler than I thought it would be.

The Problem

In practical terms, let’s say that I wish to have the search_keyword argument across a number of queries. Applied to one query, this would look something like:

1
2
3
4
5
6
7
8
9
10
11
class MyQuery(graphene.ObjectType):
    """
    `MyQuery` docstring.
    """

    records = graphene.Field(
        graphene.String,
        search_keyword=graphene.String(
            required=False, description="Search phrase."
        ),
    )

But say I wish to have this argument included in 20 or 30 queries. You can see how tiring it would be to have to keep defining the argument. And what if I wish to change the name of the argument somewhere down the road?
“There has to be a better way,” I thought. As it turns out, there is.

The Solution

Our solution is pretty simple:

  • Create a custom class that inherits from graphene.Field and declare our common fields.
  • Use this derived class when defining our queries.

The entire codebase is available on GitHub. The following sections only touch on the relevant blocks of code.

Step 1: Create a custom class

As mentioned above, our custom class inherits from the graphene.Field class.
In the supplied code, we see this in action in the file reusable_args/utils/object_fields.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# reusable_args/utils/object_fields.py

import graphene


class FieldWithArgs(graphene.Field):
    """
    Class with common (reusable) arguments.
    """

    def __init__(
            self,
            type_,
            args=None,
            resolver=None,
            source=None,
            deprecation_reason=None,
            name=None,
            description=None,
            required=False,
            _creation_counter=None,
            default_value=None,
            **extra_args
        ):
        """
        Field that adds the `search_keyword` argument to a query.
        """

        extra_args["search_keyword"] = graphene.String(
            required=False,
            default_value=None,
            description="Search phrase."
        )

        # Truncated for brevity
        super().__init__(...)

As seen above, we override the __init__ method and pass our common arguments to extra_args before calling super().__init__(). This achieves two things:

  1. It utilises the mechanism Graphene uses internally to pass arguments that are to be mounted to the query.
    What this means is that if you were to declare the search_keyword argument at the query level, it would be passed in the extra_keywords dictionary.1

  2. It allows us to declare other arguments at the query level without resorting to unsightly monkey-patching, just like we would if we were to use the regular graphene.Field class.2

Step 2: Use the derived class

We can use our derived class like we would the graphene.Field class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# reusable_args/schema.py

...
class QueryWithReusableArgs(graphene.ObjectType):
    records_reusable = FieldWithArgs(
        graphene.List(FruitType),
    )

    def resolve_records_reusable(self, info, search_keyword):
        records = FRUITS

        if search_keyword:
            records = [
                fruit
                for fruit in FRUITS
                if search_keyword.lower() in fruit.get("name").lower()
                or search_keyword.lower() == fruit.get("name").lower()
            ]
        
        return records
...

It is important to note that we will still need to include the search_keyword argument in the resolver method, much like we would need to if we were to use the graphene.Field class.

Conclusion

And there we have it. While I can be a bit verbose at times, I hope this post has not only highlighted a useful trick but also clearly explained how this trick works.

Until the next time.

Footnotes
  1. To see this in action for yourself, edit the __init__ method in FieldWithArgs and add print extra_args:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    records = FieldWithArgs(
        graphene.String,
        another_field=graphene.String(
            required=False
        ),
    )
    
    class FieldWithArgs(graphene.Field):
        def __init__():
            print(extra_args)
    

  2. In the included snippet, the two query definitions would work the same way:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    records = FieldWithArgs(
        graphene.String,
        another_field=graphene.String(
            required=False
        ),
    )
    
    records = graphene.Field(
        graphene.String,
        search_keyword= graphene.String(
            required=False,
            default_value=None,
            description="Search phrase."
        ),
        another_field=graphene.String(
            required=False
        ),
    )
    

This post is licensed under CC BY-ND 4.0 by the author.