From d56f13134cf781ce4b46c81b55efb922a8a549f2 Mon Sep 17 00:00:00 2001 From: David Vogt Date: Tue, 30 Jan 2024 14:49:54 +0100 Subject: [PATCH] fix(graphene): add a custom metaclass factory for interface types Interface types, such as `Question`, `Answer`, and `Task` have subclasses, and connections using those types may return a mix of specific object types. Examples are form -> questions, which may return different question types. For these interface types, the `graphene` internals require access to a `FooType.Meta._meta.registry` property, to access the type registry. Somehow, the graphene metaclass system does not automatically build this up correctly, so we have to help a little bit to make it work --- caluma/caluma_core/filters.py | 32 +++++++++++++++++++++ caluma/caluma_form/schema.py | 5 ++++ caluma/caluma_workflow/schema.py | 3 ++ caluma/tests/__snapshots__/test_schema.ambr | 15 ++++++++++ 4 files changed, 55 insertions(+) diff --git a/caluma/caluma_core/filters.py b/caluma/caluma_core/filters.py index 1e8621315..54631d1b6 100644 --- a/caluma/caluma_core/filters.py +++ b/caluma/caluma_core/filters.py @@ -200,6 +200,38 @@ def _should_include_filter(filt): return filter_coll() +def InterfaceMetaFactory(): + """ + Metaclass factory for interface types. + + Build a meta class suitable for the schema type classes that represent + "interface" types (those that have concrete subclasses, but could be mixed + in a connection type, such as Question, Answer, and Task). + + Usage: + + >>> class Foo(Node, graphene.Interface): + >>> ... + >>> Meta = InterfaceMetaFactory() + """ + + class _meta(graphene.types.interface.InterfaceOptions): + @classmethod + # This is kinda useless but required as graphene tries to freeze() + # it's meta class objects + def freeze(cls): + cls._frozen = True + + # This is what we're actually "fixing": On non-Interface types, + # this somehow works (or isn't needed), but here, if _meta.registry + # is not set, the whole schema construction fails + registry = get_global_registry() + + # We need a new type (= class) each time it's called, because reuse + # triggers some weird errors + return type("Meta", (), {"_meta": _meta}) + + def CollectionFilterSetFactory(filterset_class, orderset_class=None): """ Build single-filter filterset classes. diff --git a/caluma/caluma_form/schema.py b/caluma/caluma_form/schema.py index c72597ea3..37c6adb02 100644 --- a/caluma/caluma_form/schema.py +++ b/caluma/caluma_form/schema.py @@ -8,6 +8,7 @@ CollectionFilterSetFactory, DjangoFilterConnectionField, DjangoFilterInterfaceConnectionField, + InterfaceMetaFactory, ) from ..caluma_core.mutation import Mutation, UserDefinedPrimaryKeyMixin from ..caluma_core.relay import extract_global_id @@ -159,6 +160,8 @@ def get_queryset(cls, queryset, info): def resolve_type(cls, instance, info): return resolve_question(instance) + Meta = InterfaceMetaFactory() + class Option(FormDjangoObjectType): meta = generic.GenericScalar() @@ -810,6 +813,8 @@ class Answer(Node, graphene.Interface): def resolve_type(cls, instance, info): return resolve_answer(instance) + Meta = InterfaceMetaFactory() + class AnswerQuerysetMixin(object): """Mixin to combine all different answer types into one queryset.""" diff --git a/caluma/caluma_workflow/schema.py b/caluma/caluma_workflow/schema.py index 0b38c2ce8..1d0d07140 100644 --- a/caluma/caluma_workflow/schema.py +++ b/caluma/caluma_workflow/schema.py @@ -10,6 +10,7 @@ CollectionFilterSetFactory, DjangoFilterConnectionField, DjangoFilterInterfaceConnectionField, + InterfaceMetaFactory, ) from ..caluma_core.mutation import Mutation, UserDefinedPrimaryKeyMixin from ..caluma_core.types import ( @@ -97,6 +98,8 @@ def resolve_type(cls, instance, info): return TASK_TYPE[instance.type] + Meta = InterfaceMetaFactory() + class TaskConnection(CountableConnectionBase): class Meta: diff --git a/caluma/tests/__snapshots__/test_schema.ambr b/caluma/tests/__snapshots__/test_schema.ambr index cd64e5396..88c6f40e1 100644 --- a/caluma/tests/__snapshots__/test_schema.ambr +++ b/caluma/tests/__snapshots__/test_schema.ambr @@ -831,6 +831,21 @@ type DjangoDebug { """Executed SQL queries for this API query.""" sql: [DjangoDebugSQL] + + """Raise exceptions for this API query.""" + exceptions: [DjangoDebugException] + } + + """Represents a single exception raised.""" + type DjangoDebugException { + """The class of the exception""" + excType: String! + + """The message of the exception""" + message: String! + + """The stack trace""" + stack: String! } """Represents a single database query made to a Django managed DB."""