From b06e33ddd7407b4e1cf468117904a34309e51763 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Feb 2017 14:02:31 -0800 Subject: [PATCH 1/6] Improved registry --- graphene_django/debug/tests/test_query.py | 5 ++ graphene_django/filter/tests/test_fields.py | 21 ++++++-- graphene_django/registry.py | 11 ++-- graphene_django/tests/test_converter.py | 5 +- graphene_django/tests/test_query.py | 5 ++ graphene_django/tests/test_registry.py | 56 +++++++++++++++++++++ graphene_django/tests/test_schema.py | 6 ++- graphene_django/tests/test_types.py | 3 +- graphene_django/types.py | 10 +++- 9 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 graphene_django/tests/test_registry.py diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py index e0851e700..c26532942 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -6,10 +6,15 @@ from graphene_django.utils import DJANGO_FILTER_INSTALLED from ...tests.models import Reporter +from ...registry import reset_global_registry from ..middleware import DjangoDebugMiddleware from ..types import DjangoDebug +def setup_function(function): + reset_global_registry() + + class context(object): pass diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 63c9e373c..fdf6219b4 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -9,6 +9,7 @@ GlobalIDMultipleChoiceField) from graphene_django.tests.models import Article, Pet, Reporter from graphene_django.utils import DJANGO_FILTER_INSTALLED +from graphene_django.registry import Registry, reset_global_registry pytestmark = [] @@ -24,6 +25,8 @@ if DJANGO_FILTER_INSTALLED: + reset_global_registry() + class ArticleNode(DjangoObjectType): class Meta: @@ -47,6 +50,10 @@ class Meta: # schema = Schema() +@pytest.fixture +def _registry(): + return Registry() + def get_args(field): return field.args @@ -134,26 +141,28 @@ def test_filter_shortcut_filterset_extra_meta(): 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 Meta: model = Reporter interfaces = (Node, ) filter_fields = ['first_name', 'articles'] + registry = _registry field = DjangoFilterConnectionField(ReporterFilterNode) assert_arguments(field, 'first_name', 'articles') 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 Meta: model = Reporter interfaces = (Node, ) filter_fields = ['first_name', 'articles'] + registry = _registry class ArticleFilterNode(DjangoObjectType): @@ -161,6 +170,7 @@ class Meta: model = Article interfaces = (Node, ) filter_fields = ['headline', 'reporter'] + registry = _registry class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) @@ -174,13 +184,14 @@ class Query(ObjectType): assert_not_orderable(articles_field) -def test_filter_filterset_related_results(): +def test_filter_filterset_related_results(_registry): class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node, ) filter_fields = ['first_name', 'articles'] + registry = _registry class ArticleFilterNode(DjangoObjectType): @@ -188,6 +199,7 @@ class Meta: interfaces = (Node, ) model = Article filter_fields = ['headline', 'reporter'] + registry = _registry class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) @@ -315,7 +327,7 @@ class Meta: assert multiple_filter.field_class == GlobalIDMultipleChoiceField -def test_filter_filterset_related_results(): +def test_filter_filterset_related_results(_registry): class ReporterFilterNode(DjangoObjectType): class Meta: @@ -324,6 +336,7 @@ class Meta: filter_fields = { 'first_name': ['icontains'] } + registry = _registry class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) diff --git a/graphene_django/registry.py b/graphene_django/registry.py index 21fed12cb..33b05d5c3 100644 --- a/graphene_django/registry.py +++ b/graphene_django/registry.py @@ -6,15 +6,16 @@ def __init__(self): def register(self, cls): from .types import DjangoObjectType + model = cls._meta.model + assert self._registry.get(model, cls) == cls, ( + 'Django Model "{}.{}" already associated with {}. ' + 'You can use a different registry for {} or skip the global Registry with "{}.Meta.skip_global_registry = True".' + ).format(model._meta.app_label, model._meta.object_name, repr(self.get_type_for_model(cls._meta.model)), repr(cls), cls) assert issubclass( cls, DjangoObjectType), 'Only DjangoObjectTypes can be registered, received "{}"'.format( cls.__name__) assert cls._meta.registry == self, 'Registry for a Model have to match.' - # assert self.get_type_for_model(cls._meta.model) == cls, ( - # 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model) - # ) - if not getattr(cls._meta, 'skip_registry', False): - self._registry[cls._meta.model] = cls + self._registry[cls._meta.model] = cls def get_type_for_model(self, model): return self._registry.get(model) diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 997b03cea..a01253b52 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -11,12 +11,13 @@ from ..compat import (ArrayField, HStoreField, JSONField, MissingType, RangeField, UUIDField, DurationField) 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 .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): diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 750a42155..8cdf4f599 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -12,11 +12,16 @@ from ..compat import MissingType, RangeField from ..fields import DjangoConnectionField from ..types import DjangoObjectType +from ..registry import reset_global_registry from .models import Article, Reporter pytestmark = pytest.mark.django_db +def setup_function(function): + reset_global_registry() + + def test_should_query_only_fields(): with raises(Exception): class ReporterType(DjangoObjectType): diff --git a/graphene_django/tests/test_registry.py b/graphene_django/tests/test_registry.py new file mode 100644 index 000000000..a7327bf01 --- /dev/null +++ b/graphene_django/tests/test_registry.py @@ -0,0 +1,56 @@ +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_type_for_model(ReporterModel) == Reporter + + +def test_registry_multiple_types(): + class Reporter(DjangoObjectType): + '''Reporter description''' + class Meta: + model = ReporterModel + + with raises(Exception) as exc_info: + class Reporter2(DjangoObjectType): + '''Reporter2 description''' + class Meta: + model = ReporterModel + + assert str(exc_info.value) == ( + 'Django Model "tests.Reporter" already associated with . ' + 'You can use a different registry for ' + 'or skip the global Registry with "Reporter2.Meta.skip_global_registry = True".' + ) + + +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() diff --git a/graphene_django/tests/test_schema.py b/graphene_django/tests/test_schema.py index 32db1724a..40698e3e9 100644 --- a/graphene_django/tests/test_schema.py +++ b/graphene_django/tests/test_schema.py @@ -1,10 +1,14 @@ from py.test import raises -from ..registry import Registry +from ..registry import Registry, reset_global_registry from ..types import DjangoObjectType from .models import Reporter +def setup_function(function): + reset_global_registry() + + def test_should_raise_if_no_model(): with raises(Exception) as excinfo: class Character1(DjangoObjectType): diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index c617fe466..f6e2543f1 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -3,11 +3,12 @@ from graphene import Interface, ObjectType, Schema from graphene.relay import Node -from ..registry import reset_global_registry +from ..registry import Registry, reset_global_registry from ..types import DjangoObjectType from .models import Article as ArticleModel from .models import Reporter as ReporterModel + reset_global_registry() diff --git a/graphene_django/types.py b/graphene_django/types.py index ff8877938..8c5229868 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -58,7 +58,7 @@ def __new__(cls, name, bases, attrs): only_fields=(), exclude_fields=(), interfaces=(), - skip_registry=False, + skip_global_registry=False, registry=None ) if DJANGO_FILTER_INSTALLED: @@ -72,6 +72,14 @@ def __new__(cls, name, bases, attrs): attrs.pop('Meta', None), **defaults ) + # If the DjangoObjectType wants to skip the 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: options.registry = get_global_registry() assert isinstance(options.registry, Registry), ( From d81892e0d4fdd5f70c702810877f14c4f89ded41 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Feb 2017 14:20:23 -0800 Subject: [PATCH 2/6] Fixed lint and registry error description --- graphene_django/registry.py | 11 +++++++++-- graphene_django/tests/test_registry.py | 9 ++++++--- graphene_django/types.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/graphene_django/registry.py b/graphene_django/registry.py index 33b05d5c3..a2021ff28 100644 --- a/graphene_django/registry.py +++ b/graphene_django/registry.py @@ -9,8 +9,15 @@ def register(self, cls): model = cls._meta.model assert self._registry.get(model, cls) == cls, ( 'Django Model "{}.{}" already associated with {}. ' - 'You can use a different registry for {} or skip the global Registry with "{}.Meta.skip_global_registry = True".' - ).format(model._meta.app_label, model._meta.object_name, repr(self.get_type_for_model(cls._meta.model)), repr(cls), cls) + 'You can use a different registry for {} or skip ' + 'the global Registry with "{}.Meta.skip_global_registry = True".' + ).format( + model._meta.app_label, + model._meta.object_name, + repr(self.get_type_for_model(cls._meta.model)), + repr(cls), + cls + ) assert issubclass( cls, DjangoObjectType), 'Only DjangoObjectTypes can be registered, received "{}"'.format( cls.__name__) diff --git a/graphene_django/tests/test_registry.py b/graphene_django/tests/test_registry.py index a7327bf01..6527b3270 100644 --- a/graphene_django/tests/test_registry.py +++ b/graphene_django/tests/test_registry.py @@ -27,6 +27,9 @@ class Reporter(DjangoObjectType): class Meta: model = ReporterModel + class Reporter2(object): + pass + with raises(Exception) as exc_info: class Reporter2(DjangoObjectType): '''Reporter2 description''' @@ -34,10 +37,10 @@ class Meta: model = ReporterModel assert str(exc_info.value) == ( - 'Django Model "tests.Reporter" already associated with . ' - 'You can use a different registry for ' + 'Django Model "tests.Reporter" already associated with {}. ' + 'You can use a different registry for {} ' 'or skip the global Registry with "Reporter2.Meta.skip_global_registry = True".' - ) + ).format(repr(Reporter), repr(Reporter2)) def test_registry_multiple_types_dont_collision_if_skip_global_registry(): diff --git a/graphene_django/types.py b/graphene_django/types.py index 8c5229868..f2114c507 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -72,7 +72,7 @@ def __new__(cls, name, bases, attrs): attrs.pop('Meta', None), **defaults ) - # If the DjangoObjectType wants to skip the registry + # 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: From 19f77698614fb9dda77619d51f5997f8a06c7958 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 18 Feb 2017 14:49:25 -0800 Subject: [PATCH 3/6] Added registry docs --- docs/registry.rst | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/registry.rst diff --git a/docs/registry.rst b/docs/registry.rst new file mode 100644 index 000000000..af63e2b1a --- /dev/null +++ b/docs/registry.rst @@ -0,0 +1,53 @@ +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). + + +get_global_registry +------------------- + +By default, all model/objecttype relations will live in the global 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_type_for_model(ReporterModel) # == Reporter + + +One Model, multiple types +------------------------- + +There will be some cases where we need one Django Model to +have multiple graphene ``ObjectType``s associated to 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. From 5c827538c78222f7083b601dd588269f8b77ee6a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 00:21:05 -0800 Subject: [PATCH 4/6] Improved docs --- docs/registry.rst | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/registry.rst b/docs/registry.rst index af63e2b1a..da42b357e 100644 --- a/docs/registry.rst +++ b/docs/registry.rst @@ -8,10 +8,12 @@ This way, we make the library smart enough to convert automatically the relations between models to Graphene fields automatically (when possible). -get_global_registry -------------------- +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 @@ -26,12 +28,16 @@ By default, all model/objecttype relations will live in the global registry. global_registry.get_type_for_model(ReporterModel) # == Reporter -One Model, multiple types -------------------------- +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 @@ -49,5 +55,6 @@ have multiple graphene ``ObjectType``s associated to it. # We can also specify a custom registry with # registry = Registry() + This way, the ``ReporterModel`` could have two different types living in the same schema. From 4a60da3c368360702c7ae4474d0ba36ee3db6c82 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 00:22:11 -0800 Subject: [PATCH 5/6] Added registry to the index --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index c8b5515f2..6b27c19f1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,3 +11,4 @@ Contents: authorization debug introspection + registry From eadd63d0963bf6153384cb37dfc8c5896b0be2de Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Feb 2017 00:47:54 -0800 Subject: [PATCH 6/6] Improved registry, raising only when constructing the schema. --- docs/registry.rst | 2 +- graphene_django/converter.py | 8 +++--- graphene_django/registry.py | 40 ++++++++++++++++---------- graphene_django/tests/test_registry.py | 27 ++++++++++------- 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/docs/registry.rst b/docs/registry.rst index da42b357e..9c4b56f3d 100644 --- a/docs/registry.rst +++ b/docs/registry.rst @@ -25,7 +25,7 @@ You retrieve using the function ``get_global_registry`` in model = ReporterModel global_registry = get_global_registry - global_registry.get_type_for_model(ReporterModel) # == Reporter + global_registry.get_unique_type_for_model(ReporterModel) # == Reporter Multiple types for one model diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 92812d199..74f594403 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -126,7 +126,7 @@ def convert_onetoone_field_to_djangomodel(field, registry=None): model = get_related_model(field) def dynamic_type(): - _type = registry.get_type_for_model(model) + _type = registry.get_unique_type_for_model(model) if not _type: return @@ -145,7 +145,7 @@ def convert_field_to_list_or_connection(field, registry=None): model = get_related_model(field) def dynamic_type(): - _type = registry.get_type_for_model(model) + _type = registry.get_unique_type_for_model(model) if not _type: return @@ -163,7 +163,7 @@ def convert_relatedfield_to_djangomodel(field, registry=None): model = field.model def dynamic_type(): - _type = registry.get_type_for_model(model) + _type = registry.get_unique_type_for_model(model) if not _type: return @@ -183,7 +183,7 @@ def convert_field_to_djangomodel(field, registry=None): model = get_related_model(field) def dynamic_type(): - _type = registry.get_type_for_model(model) + _type = registry.get_unique_type_for_model(model) if not _type: return diff --git a/graphene_django/registry.py b/graphene_django/registry.py index a2021ff28..b4b88061b 100644 --- a/graphene_django/registry.py +++ b/graphene_django/registry.py @@ -1,30 +1,40 @@ +from collections import defaultdict + + class Registry(object): def __init__(self): - self._registry = {} - self._registry_models = {} + self._registry = defaultdict(list) def register(self, cls): from .types import DjangoObjectType model = cls._meta.model - assert self._registry.get(model, cls) == cls, ( - 'Django Model "{}.{}" already associated with {}. ' - 'You can use a different registry for {} or skip ' - 'the global Registry with "{}.Meta.skip_global_registry = True".' - ).format( - model._meta.app_label, - model._meta.object_name, - repr(self.get_type_for_model(cls._meta.model)), - repr(cls), - cls - ) assert issubclass( cls, DjangoObjectType), 'Only DjangoObjectTypes can be registered, received "{}"'.format( cls.__name__) assert cls._meta.registry == self, 'Registry for a Model have to match.' - self._registry[cls._meta.model] = cls + self._registry[model].append(cls) + + 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_type_for_model(self, model): + def get_types_for_model(self, model): return self._registry.get(model) diff --git a/graphene_django/tests/test_registry.py b/graphene_django/tests/test_registry.py index 6527b3270..eece4cfc1 100644 --- a/graphene_django/tests/test_registry.py +++ b/graphene_django/tests/test_registry.py @@ -18,29 +18,34 @@ class Meta: model = ReporterModel assert Reporter._meta.registry == global_registry - assert global_registry.get_type_for_model(ReporterModel) == Reporter + 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(object): - pass + 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: - class Reporter2(DjangoObjectType): - '''Reporter2 description''' - class Meta: - model = ReporterModel + global_registry.get_unique_type_for_model(ReporterModel) == [Reporter, Reporter2] assert str(exc_info.value) == ( - 'Django Model "tests.Reporter" already associated with {}. ' - 'You can use a different registry for {} ' - 'or skip the global Registry with "Reporter2.Meta.skip_global_registry = True".' - ).format(repr(Reporter), repr(Reporter2)) + '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():