Merge pull request #115 from graphql-python/features/global-registry

Improved the global registry
This commit is contained in:
Syrus Akbary 2017-02-20 00:56:15 -08:00 committed by GitHub
commit c635db5e5a
12 changed files with 201 additions and 21 deletions

View File

@ -11,3 +11,4 @@ Contents:
authorization authorization
debug debug
introspection introspection
registry

60
docs/registry.rst Normal file
View File

@ -0,0 +1,60 @@
Graphene-Django Registry
========================
Graphene-Django uses a Registry to keep track of all the Django Models
and the ``DjangoObjectTypes`` associated to them.
This way, we make the library smart enough to convert automatically the
relations between models to Graphene fields automatically (when possible).
Global registry
---------------
By default, all model/objecttype relations will live in the global registry.
You retrieve using the function ``get_global_registry`` in
``graphene_django.registry``.
.. code:: python
from graphene_django.registry get_global_registry
class Reporter(DjangoObjectType):
'''Reporter description'''
class Meta:
model = ReporterModel
global_registry = get_global_registry
global_registry.get_unique_type_for_model(ReporterModel) # == Reporter
Multiple types for one model
----------------------------
There will be some cases where we need one Django Model to
have multiple graphene ``ObjectType``s associated to it.
In this case, we can either use ``skip_global_registry`` to create
a new isolated registry for that type (so it doesn't interfere with
the global registry), or we can create a custom registry for it.
.. code:: python
from graphene_django.registry import Registry
class Reporter(DjangoObjectType):
'''Reporter description'''
class Meta:
model = ReporterModel
class Reporter2(DjangoObjectType):
'''Reporter2 description'''
class Meta:
model = ReporterModel
skip_global_registry = True
# We can also specify a custom registry with
# registry = Registry()
This way, the ``ReporterModel`` could have two different types living in the same
schema.

View File

@ -126,7 +126,7 @@ def convert_onetoone_field_to_djangomodel(field, registry=None):
model = get_related_model(field) model = get_related_model(field)
def dynamic_type(): def dynamic_type():
_type = registry.get_type_for_model(model) _type = registry.get_unique_type_for_model(model)
if not _type: if not _type:
return return
@ -145,7 +145,7 @@ def convert_field_to_list_or_connection(field, registry=None):
model = get_related_model(field) model = get_related_model(field)
def dynamic_type(): def dynamic_type():
_type = registry.get_type_for_model(model) _type = registry.get_unique_type_for_model(model)
if not _type: if not _type:
return return
@ -163,7 +163,7 @@ def convert_relatedfield_to_djangomodel(field, registry=None):
model = field.model model = field.model
def dynamic_type(): def dynamic_type():
_type = registry.get_type_for_model(model) _type = registry.get_unique_type_for_model(model)
if not _type: if not _type:
return return
@ -183,7 +183,7 @@ def convert_field_to_djangomodel(field, registry=None):
model = get_related_model(field) model = get_related_model(field)
def dynamic_type(): def dynamic_type():
_type = registry.get_type_for_model(model) _type = registry.get_unique_type_for_model(model)
if not _type: if not _type:
return return

View File

@ -6,10 +6,15 @@ from graphene_django import DjangoConnectionField, DjangoObjectType
from graphene_django.utils import DJANGO_FILTER_INSTALLED from graphene_django.utils import DJANGO_FILTER_INSTALLED
from ...tests.models import Reporter from ...tests.models import Reporter
from ...registry import reset_global_registry
from ..middleware import DjangoDebugMiddleware from ..middleware import DjangoDebugMiddleware
from ..types import DjangoDebug from ..types import DjangoDebug
def setup_function(function):
reset_global_registry()
class context(object): class context(object):
pass pass

View File

@ -9,6 +9,7 @@ from graphene_django.forms import (GlobalIDFormField,
GlobalIDMultipleChoiceField) GlobalIDMultipleChoiceField)
from graphene_django.tests.models import Article, Pet, Reporter from graphene_django.tests.models import Article, Pet, Reporter
from graphene_django.utils import DJANGO_FILTER_INSTALLED from graphene_django.utils import DJANGO_FILTER_INSTALLED
from graphene_django.registry import Registry, reset_global_registry
pytestmark = [] pytestmark = []
@ -24,6 +25,8 @@ pytestmark.append(pytest.mark.django_db)
if DJANGO_FILTER_INSTALLED: if DJANGO_FILTER_INSTALLED:
reset_global_registry()
class ArticleNode(DjangoObjectType): class ArticleNode(DjangoObjectType):
class Meta: class Meta:
@ -47,6 +50,10 @@ if DJANGO_FILTER_INSTALLED:
# schema = Schema() # schema = Schema()
@pytest.fixture
def _registry():
return Registry()
def get_args(field): def get_args(field):
return field.args return field.args
@ -134,26 +141,28 @@ def test_filter_shortcut_filterset_extra_meta():
assert 'headline' not in field.filterset_class.get_fields() assert 'headline' not in field.filterset_class.get_fields()
def test_filter_filterset_information_on_meta(): def test_filter_filterset_information_on_meta(_registry):
class ReporterFilterNode(DjangoObjectType): class ReporterFilterNode(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node, ) interfaces = (Node, )
filter_fields = ['first_name', 'articles'] filter_fields = ['first_name', 'articles']
registry = _registry
field = DjangoFilterConnectionField(ReporterFilterNode) field = DjangoFilterConnectionField(ReporterFilterNode)
assert_arguments(field, 'first_name', 'articles') assert_arguments(field, 'first_name', 'articles')
assert_not_orderable(field) assert_not_orderable(field)
def test_filter_filterset_information_on_meta_related(): def test_filter_filterset_information_on_meta_related(_registry):
class ReporterFilterNode(DjangoObjectType): class ReporterFilterNode(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node, ) interfaces = (Node, )
filter_fields = ['first_name', 'articles'] filter_fields = ['first_name', 'articles']
registry = _registry
class ArticleFilterNode(DjangoObjectType): class ArticleFilterNode(DjangoObjectType):
@ -161,6 +170,7 @@ def test_filter_filterset_information_on_meta_related():
model = Article model = Article
interfaces = (Node, ) interfaces = (Node, )
filter_fields = ['headline', 'reporter'] filter_fields = ['headline', 'reporter']
registry = _registry
class Query(ObjectType): class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
@ -174,13 +184,14 @@ def test_filter_filterset_information_on_meta_related():
assert_not_orderable(articles_field) assert_not_orderable(articles_field)
def test_filter_filterset_related_results(): def test_filter_filterset_related_results(_registry):
class ReporterFilterNode(DjangoObjectType): class ReporterFilterNode(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node, ) interfaces = (Node, )
filter_fields = ['first_name', 'articles'] filter_fields = ['first_name', 'articles']
registry = _registry
class ArticleFilterNode(DjangoObjectType): class ArticleFilterNode(DjangoObjectType):
@ -188,6 +199,7 @@ def test_filter_filterset_related_results():
interfaces = (Node, ) interfaces = (Node, )
model = Article model = Article
filter_fields = ['headline', 'reporter'] filter_fields = ['headline', 'reporter']
registry = _registry
class Query(ObjectType): class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
@ -315,7 +327,7 @@ def test_global_id_multiple_field_explicit_reverse():
assert multiple_filter.field_class == GlobalIDMultipleChoiceField assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_filter_filterset_related_results(): def test_filter_filterset_related_results(_registry):
class ReporterFilterNode(DjangoObjectType): class ReporterFilterNode(DjangoObjectType):
class Meta: class Meta:
@ -324,6 +336,7 @@ def test_filter_filterset_related_results():
filter_fields = { filter_fields = {
'first_name': ['icontains'] 'first_name': ['icontains']
} }
registry = _registry
class Query(ObjectType): class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_reporters = DjangoFilterConnectionField(ReporterFilterNode)

View File

@ -1,22 +1,40 @@
from collections import defaultdict
class Registry(object): class Registry(object):
def __init__(self): def __init__(self):
self._registry = {} self._registry = defaultdict(list)
self._registry_models = {}
def register(self, cls): def register(self, cls):
from .types import DjangoObjectType from .types import DjangoObjectType
model = cls._meta.model
assert issubclass( assert issubclass(
cls, DjangoObjectType), 'Only DjangoObjectTypes can be registered, received "{}"'.format( cls, DjangoObjectType), 'Only DjangoObjectTypes can be registered, received "{}"'.format(
cls.__name__) cls.__name__)
assert cls._meta.registry == self, 'Registry for a Model have to match.' assert cls._meta.registry == self, 'Registry for a Model have to match.'
# assert self.get_type_for_model(cls._meta.model) == cls, ( self._registry[model].append(cls)
# 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model)
# )
if not getattr(cls._meta, 'skip_registry', False):
self._registry[cls._meta.model] = cls
def get_type_for_model(self, model): def get_unique_type_for_model(self, model):
types = self.get_types_for_model(model)
if not types:
return None
# If there is more than one type for the model, we should
# raise an error so both types don't collide in the same schema.
assert len(types) == 1, (
'Found multiple ObjectTypes associated with the same Django Model "{}.{}": {}. '
'You can use a different registry for each or skip '
'the global Registry with Meta.skip_global_registry = True". '
'Read more at http://docs.graphene-python.org/projects/django/en/latest/registry/ .'
).format(
model._meta.app_label,
model._meta.object_name,
repr(types),
)
return types[0]
def get_types_for_model(self, model):
return self._registry.get(model) return self._registry.get(model)

View File

@ -11,12 +11,13 @@ from graphene.types.json import JSONString
from ..compat import (ArrayField, HStoreField, JSONField, MissingType, from ..compat import (ArrayField, HStoreField, JSONField, MissingType,
RangeField, UUIDField, DurationField) RangeField, UUIDField, DurationField)
from ..converter import convert_django_field, convert_django_field_with_choices from ..converter import convert_django_field, convert_django_field_with_choices
from ..registry import Registry from ..registry import Registry, reset_global_registry
from ..types import DjangoObjectType from ..types import DjangoObjectType
from .models import Article, Film, FilmDetails, Reporter from .models import Article, Film, FilmDetails, Reporter
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString def setup_function(function):
reset_global_registry()
def assert_conversion(django_field, graphene_field, *args, **kwargs): def assert_conversion(django_field, graphene_field, *args, **kwargs):

View File

@ -12,11 +12,16 @@ from ..utils import DJANGO_FILTER_INSTALLED
from ..compat import MissingType, RangeField from ..compat import MissingType, RangeField
from ..fields import DjangoConnectionField from ..fields import DjangoConnectionField
from ..types import DjangoObjectType from ..types import DjangoObjectType
from ..registry import reset_global_registry
from .models import Article, Reporter from .models import Article, Reporter
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
def setup_function(function):
reset_global_registry()
def test_should_query_only_fields(): def test_should_query_only_fields():
with raises(Exception): with raises(Exception):
class ReporterType(DjangoObjectType): class ReporterType(DjangoObjectType):

View File

@ -0,0 +1,64 @@
from pytest import raises
from ..registry import Registry, get_global_registry, reset_global_registry
from ..types import DjangoObjectType
from .models import Reporter as ReporterModel
def setup_function(function):
reset_global_registry()
def test_registry_basic():
global_registry = get_global_registry()
class Reporter(DjangoObjectType):
'''Reporter description'''
class Meta:
model = ReporterModel
assert Reporter._meta.registry == global_registry
assert global_registry.get_unique_type_for_model(ReporterModel) == Reporter
def test_registry_multiple_types():
global_registry = get_global_registry()
class Reporter(DjangoObjectType):
'''Reporter description'''
class Meta:
model = ReporterModel
class Reporter2(DjangoObjectType):
'''Reporter2 description'''
class Meta:
model = ReporterModel
assert global_registry.get_types_for_model(ReporterModel) == [Reporter, Reporter2]
with raises(Exception) as exc_info:
global_registry.get_unique_type_for_model(ReporterModel) == [Reporter, Reporter2]
assert str(exc_info.value) == (
'Found multiple ObjectTypes associated with the same '
'Django Model "tests.Reporter": {}. You can use a different '
'registry for each or skip the global Registry with '
'Meta.skip_global_registry = True". '
'Read more at http://docs.graphene-python.org/projects/django/en/latest/registry/ .'
).format(repr([Reporter, Reporter2]))
def test_registry_multiple_types_dont_collision_if_skip_global_registry():
class Reporter(DjangoObjectType):
'''Reporter description'''
class Meta:
model = ReporterModel
class Reporter2(DjangoObjectType):
'''Reporter2 description'''
class Meta:
model = ReporterModel
skip_global_registry = True
assert Reporter._meta.registry != Reporter2._meta.registry
assert Reporter2._meta.registry != get_global_registry()

View File

@ -1,10 +1,14 @@
from py.test import raises from py.test import raises
from ..registry import Registry from ..registry import Registry, reset_global_registry
from ..types import DjangoObjectType from ..types import DjangoObjectType
from .models import Reporter from .models import Reporter
def setup_function(function):
reset_global_registry()
def test_should_raise_if_no_model(): def test_should_raise_if_no_model():
with raises(Exception) as excinfo: with raises(Exception) as excinfo:
class Character1(DjangoObjectType): class Character1(DjangoObjectType):

View File

@ -3,11 +3,12 @@ from mock import patch
from graphene import Interface, ObjectType, Schema from graphene import Interface, ObjectType, Schema
from graphene.relay import Node from graphene.relay import Node
from ..registry import reset_global_registry from ..registry import Registry, reset_global_registry
from ..types import DjangoObjectType from ..types import DjangoObjectType
from .models import Article as ArticleModel from .models import Article as ArticleModel
from .models import Reporter as ReporterModel from .models import Reporter as ReporterModel
reset_global_registry() reset_global_registry()

View File

@ -58,7 +58,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta):
only_fields=(), only_fields=(),
exclude_fields=(), exclude_fields=(),
interfaces=(), interfaces=(),
skip_registry=False, skip_global_registry=False,
registry=None registry=None
) )
if DJANGO_FILTER_INSTALLED: if DJANGO_FILTER_INSTALLED:
@ -72,6 +72,14 @@ class DjangoObjectTypeMeta(ObjectTypeMeta):
attrs.pop('Meta', None), attrs.pop('Meta', None),
**defaults **defaults
) )
# If the DjangoObjectType wants to skip the global registry
# we will automatically create one, so the model is isolated
# there.
if options.skip_global_registry:
assert not options.registry, (
"The attribute skip_global_registry requires have an empty registry in {}.Meta"
).format(name)
options.registry = Registry()
if not options.registry: if not options.registry:
options.registry = get_global_registry() options.registry = get_global_registry()
assert isinstance(options.registry, Registry), ( assert isinstance(options.registry, Registry), (