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
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.
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.
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,
},
)
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.
Most helpful comment
My solve problem, something like this
iteration.py
tasks.py