Skip to content

Commit 5a09141

Browse files
committed
MongoengineObjectType subclasses can pass own _meta
1 parent 4b2db8a commit 5a09141

File tree

3 files changed

+244
-3
lines changed

3 files changed

+244
-3
lines changed

graphene_mongo/tests/test_types.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from graphene.relay import Node, is_node
55

66
from .. import registry
7-
from ..types import MongoengineObjectType
7+
from ..types import MongoengineObjectType, MongoengineObjectTypeOptions
88
from .models import Article, EmbeddedArticle, Reporter
99
from .models import Parent, Child
1010
from .utils import with_local_registry
@@ -127,3 +127,40 @@ class Meta:
127127
exclude_fields = ('headline')
128128

129129
assert 'headline' not in list(A._meta.fields.keys())
130+
131+
132+
@with_local_registry
133+
def test_passing_meta_when_subclassing_mongoengine_objecttype():
134+
class TypeSubclassWithBadOptions(MongoengineObjectType):
135+
class Meta:
136+
abstract = True
137+
138+
@classmethod
139+
def __init_subclass_with_meta__(cls, **kwargs):
140+
_meta = ['hi']
141+
super(TypeSubclassWithBadOptions, cls). \
142+
__init_subclass_with_meta__(_meta=_meta, **kwargs)
143+
144+
with raises(Exception) as einfo:
145+
class A(TypeSubclassWithBadOptions):
146+
class Meta:
147+
model = Article
148+
assert 'instance of MongoengineObjectTypeOptions' in str(einfo.value)
149+
150+
class TypeSubclass(MongoengineObjectType):
151+
class Meta:
152+
abstract = True
153+
154+
@classmethod
155+
def __init_subclass_with_meta__(cls, some_subclass_attr=None,
156+
**kwargs):
157+
_meta = MongoengineObjectTypeOptions(cls)
158+
_meta.some_subclass_attr = some_subclass_attr
159+
super(TypeSubclass, cls). \
160+
__init_subclass_with_meta__(_meta=_meta, **kwargs)
161+
162+
class B(TypeSubclass):
163+
class Meta:
164+
model = Article
165+
some_subclass_attr = 'someval'
166+
assert hasattr(B._meta, 'some_subclass_attr')

graphene_mongo/types.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ class MongoengineObjectType(ObjectType):
7575
def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False,
7676
only_fields=(), exclude_fields=(), filter_fields=None,
7777
connection=None, connection_class=None, use_connection=None,
78-
connection_field_class=None, interfaces=(), **options):
78+
connection_field_class=None, interfaces=(),
79+
_meta=None, **options):
7980

8081
assert is_valid_mongoengine_model(model), (
8182
'The attribute model in {}.Meta must be a valid Mongoengine Model. '
@@ -118,7 +119,14 @@ def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=Fa
118119
else:
119120
connection_field_class = MongoengineConnectionField
120121

121-
_meta = MongoengineObjectTypeOptions(cls)
122+
if _meta:
123+
assert isinstance(_meta, MongoengineObjectTypeOptions), (
124+
'_meta must be an instance of MongoengineObjectTypeOptions, '
125+
'received {}'
126+
).format(_meta.__class__)
127+
else:
128+
_meta = MongoengineObjectTypeOptions(cls)
129+
122130
_meta.model = model
123131
_meta.registry = registry
124132
_meta.fields = mongoengine_fields

graphene_mongo/types.refactor.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import graphene
2+
import mongoengine
3+
4+
from collections import OrderedDict
5+
from graphene.relay import Connection, Node
6+
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
7+
from graphene.types.utils import yank_fields_from_attrs
8+
9+
from graphene_mongo import MongoengineConnectionField
10+
from .converter import convert_mongoengine_field
11+
from .registry import Registry, get_global_registry
12+
from .utils import (get_model_fields, is_valid_mongoengine_model)
13+
14+
15+
def construct_fields(model, registry, only_fields, exclude_fields):
16+
"""
17+
Args:
18+
model (mongoengine.Document):
19+
registry (graphene_mongo.registry.Registry):
20+
only_fields ([str]):
21+
exclude_fields ([str]):
22+
23+
Returns:
24+
(OrderedDict, OrderedDict): coverted fields and self reference fields.
25+
26+
"""
27+
_model_fields = get_model_fields(model)
28+
fields = OrderedDict()
29+
self_referenced = OrderedDict()
30+
for name, field in _model_fields.items():
31+
is_not_in_only = only_fields and name not in only_fields
32+
is_excluded = name in exclude_fields
33+
if is_not_in_only or is_excluded:
34+
# We skip this field if we specify only_fields and is not
35+
# in there. Or when we exclude this field in exclude_fields
36+
continue
37+
if isinstance(field, mongoengine.ListField):
38+
# Take care of list of self-reference.
39+
document_type_obj = field.field.__dict__.get('document_type_obj', None)
40+
if document_type_obj == model._class_name \
41+
or isinstance(document_type_obj, model) \
42+
or document_type_obj == model:
43+
self_referenced[name] = field
44+
continue
45+
converted = convert_mongoengine_field(field, registry)
46+
if not converted:
47+
continue
48+
fields[name] = converted
49+
50+
return fields, self_referenced
51+
52+
53+
def construct_self_referenced_fields(self_referenced, registry):
54+
fields = OrderedDict()
55+
for name, field in self_referenced.items():
56+
converted = convert_mongoengine_field(field, registry)
57+
if not converted:
58+
continue
59+
fields[name] = converted
60+
61+
return fields
62+
63+
64+
class MongoengineObjectTypeOptions(ObjectTypeOptions):
65+
66+
model = None # type: Model
67+
registry = None # type: Registry
68+
only_fields = ()
69+
exclude_fields = ()
70+
filter_fields = ()
71+
connection = None # type: Type[Connection]
72+
connection_class = None
73+
use_connection = None
74+
connection_field_class = None
75+
interfaces = ()
76+
77+
78+
class MongoengineObjectType(ObjectType):
79+
80+
@classmethod
81+
def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False,
82+
only_fields=(), exclude_fields=(), filter_fields=None,
83+
connection=None, connection_class=None, use_connection=None,
84+
connection_field_class=None, interfaces=(), _meta=None,
85+
**options):
86+
87+
if not _meta:
88+
_meta = MongoengineObjectTypeOptions(cls)
89+
_meta.model = model
90+
_meta.registry = registry
91+
_meta.filter_fields = filter_fields
92+
_meta.connection = connection
93+
_meta.connection_class = connection_class
94+
_meta.use_connection = use_connection
95+
_meta.connection_field_class = connection_field_class
96+
_meta.interfaces = interfaces
97+
# Save them for later
98+
_meta.only_fields = only_fields
99+
_meta.exclude_fields = exclude_fields
100+
101+
assert is_valid_mongoengine_model(_meta.model), (
102+
'The attribute model in {}.Meta must be a valid Mongoengine Model. '
103+
'Received "{}" instead.'
104+
).format(cls.__name__, type(_meta.model))
105+
106+
if not _meta.registry:
107+
_meta.registry = get_global_registry()
108+
109+
assert isinstance(_meta.registry, Registry), (
110+
'The attribute registry in {}.Meta needs to be an instance of '
111+
'Registry, received "{}".'
112+
).format(cls.__name__, _meta.registry)
113+
converted_fields, self_referenced = construct_fields(
114+
_meta.model, _meta.registry, _meta.only_fields,
115+
_meta.exclude_fields
116+
)
117+
mongoengine_fields = yank_fields_from_attrs(converted_fields, _as=graphene.Field)
118+
_meta.fields = mongoengine_fields
119+
120+
if _meta.use_connection is None and _meta.interfaces:
121+
_meta.use_connection = any((issubclass(interface, Node)
122+
for interface in _meta.interfaces))
123+
124+
if _meta.use_connection and not _meta.connection:
125+
# We create the connection automatically
126+
if not _meta.connection_class:
127+
_meta.connection_class = Connection
128+
129+
_meta.connection = _meta.connection_class.create_type(
130+
'{}Connection'.format(cls.__name__), node=cls)
131+
if _meta.connection is not None:
132+
assert issubclass(_meta.connection, Connection), (
133+
'The attribute connection in {}.Meta must be of type Connection. '
134+
'Received "{}" instead.'
135+
).format(cls.__name__, type(_meta.connection))
136+
137+
if _meta.connection_field_class is not None:
138+
assert issubclass(_meta.connection_field_class, graphene.ConnectionField), (
139+
'The attribute connection_field_class in {}.Meta must be of type graphene.ConnectionField. '
140+
'Received "{}" instead.'
141+
).format(cls.__name__, type(_meta.connection_field_class))
142+
else:
143+
_meta.connection_field_class = MongoengineConnectionField
144+
145+
super(MongoengineObjectType, cls).__init_subclass_with_meta__(
146+
_meta=_meta, interfaces=_meta.interfaces, **options
147+
)
148+
149+
if not skip_registry:
150+
_meta.registry.register(cls)
151+
# Notes: Take care list of self-reference fields.
152+
converted_fields = construct_self_referenced_fields(self_referenced, _meta.registry)
153+
if converted_fields:
154+
mongoengine_fields = yank_fields_from_attrs(converted_fields, _as=graphene.Field)
155+
cls._meta.fields.update(mongoengine_fields)
156+
_meta.registry.register(cls)
157+
158+
@classmethod
159+
def rescan_fields(cls):
160+
"""Attempts to rescan fields and will insert any not converted initially"""
161+
162+
converted_fields, self_referenced = construct_fields(
163+
cls._meta.model, cls._meta.registry,
164+
cls._meta.only_fields, cls._meta.exclude_fields
165+
)
166+
167+
mongoengine_fields = yank_fields_from_attrs(converted_fields, _as=graphene.Field)
168+
169+
# The initial scan should take precidence
170+
for field in mongoengine_fields:
171+
if field not in cls._meta.fields:
172+
cls._meta.fields.update({field: mongoengine_fields[field]})
173+
# Self-referenced fields can't change between scans!
174+
175+
176+
# noqa
177+
@classmethod
178+
def is_type_of(cls, root, info):
179+
if isinstance(root, cls):
180+
return True
181+
if not is_valid_mongoengine_model(type(root)):
182+
raise Exception((
183+
'Received incompatible instance "{}".'
184+
).format(root))
185+
return isinstance(root, cls._meta.model)
186+
187+
@classmethod
188+
def get_node(cls, info, id):
189+
return cls._meta.model.objects.get(pk=id)
190+
191+
def resolve_id(self, info):
192+
return str(self.id)
193+
194+
# @classmethod
195+
# def get_connection(cls):
196+
# return connection_for_type(cls)

0 commit comments

Comments
 (0)