Graphene: Circular import question

Created on 10 Aug 2017  路  8Comments  路  Source: graphql-python/graphene

In our project we have schema consisting of around 20 different classes and we want to divide it in different files instead of having only one. But while trying to do this, we encountered circular import problem. We have tried using advices given in https://github.com/graphql-python/graphene/issues/110 and https://github.com/graphql-python/graphene/issues/436 but couldn't solve the problem:

# Basic example (we need to import Dictionary on language.py and Language in dictionary.py)
# mymodule/language.py
class Language(graphene.ObjectType):
    id = graphene.Int()
    dictionaries = graphene.List(Dictionary)
    def resolve_dictionaries(self, args, context, info):
        result = list()
        for dictionary in db_list:
            result.append(Dictionary(id=dictionary.id))
        return result


# mymodule/dictionary.py
class Dictionary(graphene.ObjectType):
    id = graphene.Int()
    language = graphene.Field(Language)
    def resolve_language(self, args, context, info):
        language = db_object
        return Language(id=language.id)

# first example (working, but it violates pep8)
# mymodule/language.py
class Language(graphene.ObjectType):
    id = graphene.Int()
    dictionaries = graphene.List(lambda: Dictionary)
    def resolve_dictionaries(self, args, context, info):
        result = list()
        for dictionary in db_list:
            result.append(Dictionary(id=dictionary.id))
        return result
from .dictionary import Dictionary


# mymodule/dictionary.py
class Dictionary(graphene.ObjectType):
    id = graphene.Int()
    language = graphene.Field(lambda: Language)
    def resolve_language(self, args, context, info):
        language = db_object
        return Language(id=language.id)
from .language import Language

# second example (Dictionary(id=dictionary.id) and Language(id=language.id) 
# gives TypeError: import_string() got an unexpected keyword argument 'id')
# mymodule/language.py
Dictionary = lazy_import('mymodule.dictionary.Dictionary')
class Language(graphene.ObjectType):
    id = graphene.Int()
    dictionaries = graphene.List(Dictionary)
    def resolve_dictionaries(self, args, context, info):
        result = list()
        for dictionary in db_list:
            result.append(Dictionary(id=dictionary.id))
        return result


# mymodule/dictionary.py
Dictionary = lazy_import('mymodule.language.Language')
class Dictionary(graphene.ObjectType):
    id = graphene.Int()
    language = graphene.Field(Language)
    def resolve_language(self, args, context, info):
        language = db_object
        return Language(id=language.id)

How can we solve this problem without violating pep8? Thanks in advance

Most helpful comment

My solve problem, something like this
iteration.py

class IterationObject(graphene.ObjectType):
    id = graphene.Int()
    tasks = graphene.List(TaskObject)

tasks.py

class TaskObject(graphene.ObjectType):
    id = graphene.Int()
    iteration = graphene.Field('iteration.IterationObject')

    @property
    def iteration_class(self):
        return self._meta.fields['iteration'].type

All 8 comments

Please use ```python for highlight, and split files and examples

I'm not aware of the graphene.List(lambda: Dictionary) syntax.
But why not use the Dynamic type?

Something like:

class Language(graphene.ObjectType):
    id = graphene.Int()
    dictionaries = graphene.List(Dynamic(Language .get_dictionary_type))

    @staticmethod
    def get_dictionary_type():
        from .dictionary import Dictionary
        return Dictionary

    def resolve_dictionaries(self, args, context, info):
        result = list()
        for dictionary in db_list:
            result.append(Dictionary(id=dictionary.id))
        return result

How can we solve this problem without violating pep8?
There is import inside function

My solve problem, something like this
iteration.py

class IterationObject(graphene.ObjectType):
    id = graphene.Int()
    tasks = graphene.List(TaskObject)

tasks.py

class TaskObject(graphene.ObjectType):
    id = graphene.Int()
    iteration = graphene.Field('iteration.IterationObject')

    @property
    def iteration_class(self):
        return self._meta.fields['iteration'].type

Thank you! This is exactly what we need

@ekampf I wonder if this example would still work now?

For me this gives AttributeError: 'Dynamic' object has no attribute 'name' (2.1.3)

I've run into this issue while working on a schema that has cross module circular dependencies. As a contrived example, imagine these two schemas:

# orders/schema.py
from order_items.schema import OrderItemConnection

class Order(graphene.ObjectType):
    order_items = graphene.ConnectionField(OrderItemConnection)
# order_items/schema.py
from orders.schema import Order

class OrderItem(graphene.ObjectType):
    order = graphene.Field(Order)


class OrderItemConnection(graphene.Connection):
    class Meta:
        node = OrderItem

This won't resolve because of the circular import.

First Pass

I started out using @totaki's implementation above, but that quickly turned in to a lot of copied properties so I moved to mixes that add the field and the _class property to resolve the class name. So I moved to mixins in a lazy module that would add that field to any object it was mixed in with.

# order_items/lazy.py
class OrderItemConnectionMixin:
    order_items = graphene.ConnectionField("order_items.schema.OrderItemConnection")

    @property
    def order_items_class(self):
        return self._meta.fields["order_items"].type
# orders/schema.py updated
from order_items.lazy import OrderItemConnectionMixin

class Order(OrderItemConnectionMixin, graphene.ObjectType):
    pass

With black formatting, that means we get six lines of boilerplate for each lazy connection object that I created. By the time I revisited creating those, I already had three and can easily see more that I'm going to need.

Second Pass

I decided to rewrite this so the above was generic. What I wanted was something like this in order_items/lazy.py

from utils import connection_mixin

OrderItemConnectionMixin = connection_mixin("order_item")

I was able to get to that with the following function:

# utils.py
import inspect

import graphene


def connection_mixin(name, field_name=None, plural_name=None):
    if plural_name is None:
        plural_name = "{}s".format(name)

    if field_name is None:
        calling_module = inspect.getmodule(inspect.stack()[1][0]).__name__
        field_name = "{}.schema.{}Connection".format(
            calling_module.rsplit(".", 1)[0], name.title()
        )

    @property
    def load_class(self):
        return self._meta.fields[plural_name].type

    return type(
        "{}ConnectionMixin".format(name.title()),
        (graphene.AbstractType,),
        {
            plural_name: graphene.ConnectionField(field_name),
            "{}_class".format(plural_name): load_class,
        },
    )

Third Pass?

I've thought about making this in to a LazyConnectionField object. The API inside orders/schema.py would look something like:

class Order(graphene.ObjectType):
    order_items = LazyConnectionType("order_items.schema.OrderItemsConnection")

This feels more Pythonic. Might write that up, but I'm leaving it here as an exercise to the reader.


This will only work as a copy-and-paste solution if you're following the same patterns and structure that I am, but I'm hoping it'll help anyone else who ends up on this ticket as they start to think thru their solution to this issue.

We were able to get around this issue by doing the following:

add this to a utility file:

from functools import partial
from graphene import Dynamic

# Note this has to be a partial as the Dyanmic class doesn't allow
# class methods to be passed
def schema_type(type_name, wrap=None, schema=None):
    dynamic_type = next(x for x in schema.types if x._meta.name == type_name)
    if wrap:
        return wrap(dynamic_type)
    else:
        return dynamic_type


class DynamicSchema(Dynamic):
    def __init__(self, type_name, wrap=None, _creation_counter=None):
        self.schema_type = partial(
            schema_type,
            type_name=type_name,
            wrap=wrap,
        )
        super().__init__(
            self.schema_type,
            with_schema=True,
            _creation_counter=_creation_counter,
        )

In my schema def file:

from x import FooBar
from y import FooBarBaz

SCHEMA = graphene.Schema(query=Query, mutation=Mutations, types=[FooBar, FooBarBaz])

Define something

from graphene import ObjectType, String


class FooBar(ObjectType):
    id =String()

And finally in a schema that would create a circular import

from graphene import ObjectType, List


class FooBarBaz(ObjectType):
    foo_bars = DynamicSchema('FooBar', List)

If anyone thinks this should be a PR and added to the library let me know.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mraak picture mraak  路  3Comments

romaia picture romaia  路  3Comments

dfee picture dfee  路  4Comments

mingzhou picture mingzhou  路  3Comments

Glyphack picture Glyphack  路  3Comments