Skip to content

Commit c635db5

Browse files
authored
Merge pull request #115 from graphql-python/features/global-registry
Improved the global registry
2 parents 0ec8d2c + eadd63d commit c635db5

File tree

12 files changed

+202
-22
lines changed

12 files changed

+202
-22
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Contents:
1111
authorization
1212
debug
1313
introspection
14+
registry

docs/registry.rst

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
Graphene-Django Registry
2+
========================
3+
4+
Graphene-Django uses a Registry to keep track of all the Django Models
5+
and the ``DjangoObjectTypes`` associated to them.
6+
7+
This way, we make the library smart enough to convert automatically the
8+
relations between models to Graphene fields automatically (when possible).
9+
10+
11+
Global registry
12+
---------------
13+
14+
By default, all model/objecttype relations will live in the global registry.
15+
You retrieve using the function ``get_global_registry`` in
16+
``graphene_django.registry``.
17+
18+
.. code:: python
19+
20+
from graphene_django.registry get_global_registry
21+
22+
class Reporter(DjangoObjectType):
23+
'''Reporter description'''
24+
class Meta:
25+
model = ReporterModel
26+
27+
global_registry = get_global_registry
28+
global_registry.get_unique_type_for_model(ReporterModel) # == Reporter
29+
30+
31+
Multiple types for one model
32+
----------------------------
33+
34+
There will be some cases where we need one Django Model to
35+
have multiple graphene ``ObjectType``s associated to it.
36+
37+
In this case, we can either use ``skip_global_registry`` to create
38+
a new isolated registry for that type (so it doesn't interfere with
39+
the global registry), or we can create a custom registry for it.
40+
41+
.. code:: python
42+
43+
from graphene_django.registry import Registry
44+
45+
class Reporter(DjangoObjectType):
46+
'''Reporter description'''
47+
class Meta:
48+
model = ReporterModel
49+
50+
class Reporter2(DjangoObjectType):
51+
'''Reporter2 description'''
52+
class Meta:
53+
model = ReporterModel
54+
skip_global_registry = True
55+
# We can also specify a custom registry with
56+
# registry = Registry()
57+
58+
59+
This way, the ``ReporterModel`` could have two different types living in the same
60+
schema.

graphene_django/converter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def convert_onetoone_field_to_djangomodel(field, registry=None):
126126
model = get_related_model(field)
127127

128128
def dynamic_type():
129-
_type = registry.get_type_for_model(model)
129+
_type = registry.get_unique_type_for_model(model)
130130
if not _type:
131131
return
132132

@@ -145,7 +145,7 @@ def convert_field_to_list_or_connection(field, registry=None):
145145
model = get_related_model(field)
146146

147147
def dynamic_type():
148-
_type = registry.get_type_for_model(model)
148+
_type = registry.get_unique_type_for_model(model)
149149
if not _type:
150150
return
151151

@@ -163,7 +163,7 @@ def convert_relatedfield_to_djangomodel(field, registry=None):
163163
model = field.model
164164

165165
def dynamic_type():
166-
_type = registry.get_type_for_model(model)
166+
_type = registry.get_unique_type_for_model(model)
167167
if not _type:
168168
return
169169

@@ -183,7 +183,7 @@ def convert_field_to_djangomodel(field, registry=None):
183183
model = get_related_model(field)
184184

185185
def dynamic_type():
186-
_type = registry.get_type_for_model(model)
186+
_type = registry.get_unique_type_for_model(model)
187187
if not _type:
188188
return
189189

graphene_django/debug/tests/test_query.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
from graphene_django.utils import DJANGO_FILTER_INSTALLED
77

88
from ...tests.models import Reporter
9+
from ...registry import reset_global_registry
910
from ..middleware import DjangoDebugMiddleware
1011
from ..types import DjangoDebug
1112

1213

14+
def setup_function(function):
15+
reset_global_registry()
16+
17+
1318
class context(object):
1419
pass
1520

graphene_django/filter/tests/test_fields.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
GlobalIDMultipleChoiceField)
1010
from graphene_django.tests.models import Article, Pet, Reporter
1111
from graphene_django.utils import DJANGO_FILTER_INSTALLED
12+
from graphene_django.registry import Registry, reset_global_registry
1213

1314
pytestmark = []
1415

@@ -24,6 +25,8 @@
2425

2526

2627
if DJANGO_FILTER_INSTALLED:
28+
reset_global_registry()
29+
2730
class ArticleNode(DjangoObjectType):
2831

2932
class Meta:
@@ -47,6 +50,10 @@ class Meta:
4750

4851
# schema = Schema()
4952

53+
@pytest.fixture
54+
def _registry():
55+
return Registry()
56+
5057

5158
def get_args(field):
5259
return field.args
@@ -134,33 +141,36 @@ def test_filter_shortcut_filterset_extra_meta():
134141
assert 'headline' not in field.filterset_class.get_fields()
135142

136143

137-
def test_filter_filterset_information_on_meta():
144+
def test_filter_filterset_information_on_meta(_registry):
138145
class ReporterFilterNode(DjangoObjectType):
139146

140147
class Meta:
141148
model = Reporter
142149
interfaces = (Node, )
143150
filter_fields = ['first_name', 'articles']
151+
registry = _registry
144152

145153
field = DjangoFilterConnectionField(ReporterFilterNode)
146154
assert_arguments(field, 'first_name', 'articles')
147155
assert_not_orderable(field)
148156

149157

150-
def test_filter_filterset_information_on_meta_related():
158+
def test_filter_filterset_information_on_meta_related(_registry):
151159
class ReporterFilterNode(DjangoObjectType):
152160

153161
class Meta:
154162
model = Reporter
155163
interfaces = (Node, )
156164
filter_fields = ['first_name', 'articles']
165+
registry = _registry
157166

158167
class ArticleFilterNode(DjangoObjectType):
159168

160169
class Meta:
161170
model = Article
162171
interfaces = (Node, )
163172
filter_fields = ['headline', 'reporter']
173+
registry = _registry
164174

165175
class Query(ObjectType):
166176
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
@@ -174,20 +184,22 @@ class Query(ObjectType):
174184
assert_not_orderable(articles_field)
175185

176186

177-
def test_filter_filterset_related_results():
187+
def test_filter_filterset_related_results(_registry):
178188
class ReporterFilterNode(DjangoObjectType):
179189

180190
class Meta:
181191
model = Reporter
182192
interfaces = (Node, )
183193
filter_fields = ['first_name', 'articles']
194+
registry = _registry
184195

185196
class ArticleFilterNode(DjangoObjectType):
186197

187198
class Meta:
188199
interfaces = (Node, )
189200
model = Article
190201
filter_fields = ['headline', 'reporter']
202+
registry = _registry
191203

192204
class Query(ObjectType):
193205
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
@@ -315,7 +327,7 @@ class Meta:
315327
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
316328

317329

318-
def test_filter_filterset_related_results():
330+
def test_filter_filterset_related_results(_registry):
319331
class ReporterFilterNode(DjangoObjectType):
320332

321333
class Meta:
@@ -324,6 +336,7 @@ class Meta:
324336
filter_fields = {
325337
'first_name': ['icontains']
326338
}
339+
registry = _registry
327340

328341
class Query(ObjectType):
329342
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)

graphene_django/registry.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
1+
from collections import defaultdict
2+
3+
14
class Registry(object):
25

36
def __init__(self):
4-
self._registry = {}
5-
self._registry_models = {}
7+
self._registry = defaultdict(list)
68

79
def register(self, cls):
810
from .types import DjangoObjectType
11+
model = cls._meta.model
912
assert issubclass(
1013
cls, DjangoObjectType), 'Only DjangoObjectTypes can be registered, received "{}"'.format(
1114
cls.__name__)
1215
assert cls._meta.registry == self, 'Registry for a Model have to match.'
13-
# assert self.get_type_for_model(cls._meta.model) == cls, (
14-
# 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model)
15-
# )
16-
if not getattr(cls._meta, 'skip_registry', False):
17-
self._registry[cls._meta.model] = cls
18-
19-
def get_type_for_model(self, model):
16+
self._registry[model].append(cls)
17+
18+
def get_unique_type_for_model(self, model):
19+
types = self.get_types_for_model(model)
20+
if not types:
21+
return None
22+
23+
# If there is more than one type for the model, we should
24+
# raise an error so both types don't collide in the same schema.
25+
assert len(types) == 1, (
26+
'Found multiple ObjectTypes associated with the same Django Model "{}.{}": {}. '
27+
'You can use a different registry for each or skip '
28+
'the global Registry with Meta.skip_global_registry = True". '
29+
'Read more at http://docs.graphene-python.org/projects/django/en/latest/registry/ .'
30+
).format(
31+
model._meta.app_label,
32+
model._meta.object_name,
33+
repr(types),
34+
)
35+
return types[0]
36+
37+
def get_types_for_model(self, model):
2038
return self._registry.get(model)
2139

2240

graphene_django/tests/test_converter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
from ..compat import (ArrayField, HStoreField, JSONField, MissingType,
1212
RangeField, UUIDField, DurationField)
1313
from ..converter import convert_django_field, convert_django_field_with_choices
14-
from ..registry import Registry
14+
from ..registry import Registry, reset_global_registry
1515
from ..types import DjangoObjectType
1616
from .models import Article, Film, FilmDetails, Reporter
1717

1818

19-
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
19+
def setup_function(function):
20+
reset_global_registry()
2021

2122

2223
def assert_conversion(django_field, graphene_field, *args, **kwargs):

graphene_django/tests/test_query.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@
1212
from ..compat import MissingType, RangeField
1313
from ..fields import DjangoConnectionField
1414
from ..types import DjangoObjectType
15+
from ..registry import reset_global_registry
1516
from .models import Article, Reporter
1617

1718
pytestmark = pytest.mark.django_db
1819

1920

21+
def setup_function(function):
22+
reset_global_registry()
23+
24+
2025
def test_should_query_only_fields():
2126
with raises(Exception):
2227
class ReporterType(DjangoObjectType):
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from pytest import raises
2+
3+
from ..registry import Registry, get_global_registry, reset_global_registry
4+
from ..types import DjangoObjectType
5+
from .models import Reporter as ReporterModel
6+
7+
8+
def setup_function(function):
9+
reset_global_registry()
10+
11+
12+
def test_registry_basic():
13+
global_registry = get_global_registry()
14+
15+
class Reporter(DjangoObjectType):
16+
'''Reporter description'''
17+
class Meta:
18+
model = ReporterModel
19+
20+
assert Reporter._meta.registry == global_registry
21+
assert global_registry.get_unique_type_for_model(ReporterModel) == Reporter
22+
23+
24+
def test_registry_multiple_types():
25+
global_registry = get_global_registry()
26+
27+
class Reporter(DjangoObjectType):
28+
'''Reporter description'''
29+
class Meta:
30+
model = ReporterModel
31+
32+
class Reporter2(DjangoObjectType):
33+
'''Reporter2 description'''
34+
class Meta:
35+
model = ReporterModel
36+
37+
assert global_registry.get_types_for_model(ReporterModel) == [Reporter, Reporter2]
38+
39+
with raises(Exception) as exc_info:
40+
global_registry.get_unique_type_for_model(ReporterModel) == [Reporter, Reporter2]
41+
42+
assert str(exc_info.value) == (
43+
'Found multiple ObjectTypes associated with the same '
44+
'Django Model "tests.Reporter": {}. You can use a different '
45+
'registry for each or skip the global Registry with '
46+
'Meta.skip_global_registry = True". '
47+
'Read more at http://docs.graphene-python.org/projects/django/en/latest/registry/ .'
48+
).format(repr([Reporter, Reporter2]))
49+
50+
51+
def test_registry_multiple_types_dont_collision_if_skip_global_registry():
52+
class Reporter(DjangoObjectType):
53+
'''Reporter description'''
54+
class Meta:
55+
model = ReporterModel
56+
57+
class Reporter2(DjangoObjectType):
58+
'''Reporter2 description'''
59+
class Meta:
60+
model = ReporterModel
61+
skip_global_registry = True
62+
63+
assert Reporter._meta.registry != Reporter2._meta.registry
64+
assert Reporter2._meta.registry != get_global_registry()

graphene_django/tests/test_schema.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from py.test import raises
22

3-
from ..registry import Registry
3+
from ..registry import Registry, reset_global_registry
44
from ..types import DjangoObjectType
55
from .models import Reporter
66

77

8+
def setup_function(function):
9+
reset_global_registry()
10+
11+
812
def test_should_raise_if_no_model():
913
with raises(Exception) as excinfo:
1014
class Character1(DjangoObjectType):

0 commit comments

Comments
 (0)