Skip to content

Commit a361c52

Browse files
authored
[Regression Fix] Call custom resolve functions if provided (#241)
Fixes issue #234
1 parent c89cf80 commit a361c52

File tree

6 files changed

+131
-27
lines changed

6 files changed

+131
-27
lines changed

graphene_sqlalchemy/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from .fields import SQLAlchemyConnectionField
33
from .utils import get_query, get_session
44

5-
__version__ = "2.2.1"
5+
__version__ = "2.2.2"
66

77
__all__ = [
88
"__version__",

graphene_sqlalchemy/converter.py

+7-11
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@
1616
ChoiceType = JSONType = ScalarListType = TSVectorType = object
1717

1818

19-
def _get_attr_resolver(attr_name):
20-
return lambda root, _info: getattr(root, attr_name, None)
21-
22-
2319
def get_column_doc(column):
2420
return getattr(column, "doc", None)
2521

@@ -28,7 +24,7 @@ def is_column_nullable(column):
2824
return bool(getattr(column, "nullable", True))
2925

3026

31-
def convert_sqlalchemy_relationship(relationship_prop, registry, connection_field_factory, **field_kwargs):
27+
def convert_sqlalchemy_relationship(relationship_prop, registry, connection_field_factory, resolver, **field_kwargs):
3228
direction = relationship_prop.direction
3329
model = relationship_prop.mapper.entity
3430

@@ -40,7 +36,7 @@ def dynamic_type():
4036
if direction == interfaces.MANYTOONE or not relationship_prop.uselist:
4137
return Field(
4238
_type,
43-
resolver=_get_attr_resolver(relationship_prop.key),
39+
resolver=resolver,
4440
**field_kwargs
4541
)
4642
elif direction in (interfaces.ONETOMANY, interfaces.MANYTOMANY):
@@ -55,18 +51,18 @@ def dynamic_type():
5551
return Dynamic(dynamic_type)
5652

5753

58-
def convert_sqlalchemy_hybrid_method(hybrid_prop, prop_name, **field_kwargs):
54+
def convert_sqlalchemy_hybrid_method(hybrid_prop, resolver, **field_kwargs):
5955
if 'type' not in field_kwargs:
6056
# TODO The default type should be dependent on the type of the property propety.
6157
field_kwargs['type'] = String
6258

6359
return Field(
64-
resolver=_get_attr_resolver(prop_name),
60+
resolver=resolver,
6561
**field_kwargs
6662
)
6763

6864

69-
def convert_sqlalchemy_composite(composite_prop, registry):
65+
def convert_sqlalchemy_composite(composite_prop, registry, resolver):
7066
converter = registry.get_converter_for_composite(composite_prop.composite_class)
7167
if not converter:
7268
try:
@@ -100,14 +96,14 @@ def inner(fn):
10096
convert_sqlalchemy_composite.register = _register_composite_class
10197

10298

103-
def convert_sqlalchemy_column(column_prop, registry, **field_kwargs):
99+
def convert_sqlalchemy_column(column_prop, registry, resolver, **field_kwargs):
104100
column = column_prop.columns[0]
105101
field_kwargs.setdefault('type', convert_sqlalchemy_type(getattr(column, "type", None), column, registry))
106102
field_kwargs.setdefault('required', not is_column_nullable(column))
107103
field_kwargs.setdefault('description', get_column_doc(column))
108104

109105
return Field(
110-
resolver=_get_attr_resolver(column_prop.key),
106+
resolver=resolver,
111107
**field_kwargs
112108
)
113109

graphene_sqlalchemy/tests/conftest.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from sqlalchemy import create_engine
33
from sqlalchemy.orm import scoped_session, sessionmaker
44

5+
import graphene
6+
57
from ..converter import convert_sqlalchemy_composite
68
from ..registry import reset_global_registry
79
from .models import Base, CompositeFullName
@@ -17,7 +19,7 @@ def reset_registry():
1719
# Tests that explicitly depend on this behavior should re-register a converter
1820
@convert_sqlalchemy_composite.register(CompositeFullName)
1921
def convert_composite_class(composite, registry):
20-
pass
22+
return graphene.Field(graphene.Int)
2123

2224

2325
@pytest.yield_fixture(scope="function")

graphene_sqlalchemy/tests/test_converter.py

+24-8
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@
2323
from .models import Article, CompositeFullName, Pet, Reporter
2424

2525

26+
def mock_resolver():
27+
pass
28+
29+
2630
def get_field(sqlalchemy_type, **column_kwargs):
2731
class Model(declarative_base()):
2832
__tablename__ = 'model'
2933
id_ = Column(types.Integer, primary_key=True)
3034
column = Column(sqlalchemy_type, doc="Custom Help Text", **column_kwargs)
3135

3236
column_prop = inspect(Model).column_attrs['column']
33-
return convert_sqlalchemy_column(column_prop, get_global_registry())
37+
return convert_sqlalchemy_column(column_prop, get_global_registry(), mock_resolver)
3438

3539

3640
def get_field_from_column(column_):
@@ -40,7 +44,7 @@ class Model(declarative_base()):
4044
column = column_
4145

4246
column_prop = inspect(Model).column_attrs['column']
43-
return convert_sqlalchemy_column(column_prop, get_global_registry())
47+
return convert_sqlalchemy_column(column_prop, get_global_registry(), mock_resolver)
4448

4549

4650
def test_should_unknown_sqlalchemy_field_raise_exception():
@@ -162,7 +166,7 @@ def test_should_jsontype_convert_jsonstring():
162166
def test_should_manytomany_convert_connectionorlist():
163167
registry = Registry()
164168
dynamic_field = convert_sqlalchemy_relationship(
165-
Reporter.pets.property, registry, default_connection_field_factory
169+
Reporter.pets.property, registry, default_connection_field_factory, mock_resolver,
166170
)
167171
assert isinstance(dynamic_field, graphene.Dynamic)
168172
assert not dynamic_field.get_type()
@@ -174,7 +178,7 @@ class Meta:
174178
model = Pet
175179

176180
dynamic_field = convert_sqlalchemy_relationship(
177-
Reporter.pets.property, A._meta.registry, default_connection_field_factory
181+
Reporter.pets.property, A._meta.registry, default_connection_field_factory, mock_resolver,
178182
)
179183
assert isinstance(dynamic_field, graphene.Dynamic)
180184
graphene_type = dynamic_field.get_type()
@@ -190,7 +194,7 @@ class Meta:
190194
interfaces = (Node,)
191195

192196
dynamic_field = convert_sqlalchemy_relationship(
193-
Reporter.pets.property, A._meta.registry, default_connection_field_factory
197+
Reporter.pets.property, A._meta.registry, default_connection_field_factory, mock_resolver
194198
)
195199
assert isinstance(dynamic_field, graphene.Dynamic)
196200
assert isinstance(dynamic_field.get_type(), UnsortedSQLAlchemyConnectionField)
@@ -199,7 +203,10 @@ class Meta:
199203
def test_should_manytoone_convert_connectionorlist():
200204
registry = Registry()
201205
dynamic_field = convert_sqlalchemy_relationship(
202-
Article.reporter.property, registry, default_connection_field_factory
206+
Article.reporter.property,
207+
registry,
208+
default_connection_field_factory,
209+
mock_resolver,
203210
)
204211
assert isinstance(dynamic_field, graphene.Dynamic)
205212
assert not dynamic_field.get_type()
@@ -211,7 +218,10 @@ class Meta:
211218
model = Reporter
212219

213220
dynamic_field = convert_sqlalchemy_relationship(
214-
Article.reporter.property, A._meta.registry, default_connection_field_factory
221+
Article.reporter.property,
222+
A._meta.registry,
223+
default_connection_field_factory,
224+
mock_resolver,
215225
)
216226
assert isinstance(dynamic_field, graphene.Dynamic)
217227
graphene_type = dynamic_field.get_type()
@@ -226,7 +236,10 @@ class Meta:
226236
interfaces = (Node,)
227237

228238
dynamic_field = convert_sqlalchemy_relationship(
229-
Article.reporter.property, A._meta.registry, default_connection_field_factory
239+
Article.reporter.property,
240+
A._meta.registry,
241+
default_connection_field_factory,
242+
mock_resolver,
230243
)
231244
assert isinstance(dynamic_field, graphene.Dynamic)
232245
graphene_type = dynamic_field.get_type()
@@ -244,6 +257,7 @@ class Meta:
244257
Reporter.favorite_article.property,
245258
A._meta.registry,
246259
default_connection_field_factory,
260+
mock_resolver,
247261
)
248262
assert isinstance(dynamic_field, graphene.Dynamic)
249263
graphene_type = dynamic_field.get_type()
@@ -310,6 +324,7 @@ def convert_composite_class(composite, registry):
310324
field = convert_sqlalchemy_composite(
311325
composite(CompositeClass, (Column(types.Unicode(50)), Column(types.Unicode(50))), doc="Custom Help Text"),
312326
registry,
327+
mock_resolver,
313328
)
314329
assert isinstance(field, graphene.String)
315330

@@ -325,4 +340,5 @@ def __init__(self, col1, col2):
325340
convert_sqlalchemy_composite(
326341
composite(CompositeFullName, (Column(types.Unicode(50)), Column(types.Unicode(50)))),
327342
Registry(),
343+
mock_resolver,
328344
)

graphene_sqlalchemy/tests/test_types.py

+70-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import six # noqa F401
44

55
from graphene import (Dynamic, Field, GlobalID, Int, List, Node, NonNull,
6-
ObjectType, String)
6+
ObjectType, Schema, String)
77

88
from ..converter import convert_sqlalchemy_composite
99
from ..fields import (SQLAlchemyConnectionField,
@@ -264,6 +264,7 @@ class Meta:
264264
"column_prop",
265265
"email",
266266
"favorite_pet_kind",
267+
"composite_prop",
267268
"hybrid_prop",
268269
"pets",
269270
"articles",
@@ -293,6 +294,73 @@ class Meta:
293294
assert first_name_field.type == Int
294295

295296

297+
def test_resolvers(session):
298+
"""Test that the correct resolver functions are called"""
299+
300+
class ReporterMixin(object):
301+
def resolve_id(root, _info):
302+
return 'ID'
303+
304+
class ReporterType(ReporterMixin, SQLAlchemyObjectType):
305+
class Meta:
306+
model = Reporter
307+
308+
email = ORMField()
309+
email_v2 = ORMField(model_attr='email')
310+
favorite_pet_kind = Field(String)
311+
favorite_pet_kind_v2 = Field(String)
312+
313+
def resolve_last_name(root, _info):
314+
return root.last_name.upper()
315+
316+
def resolve_email_v2(root, _info):
317+
return root.email + '_V2'
318+
319+
def resolve_favorite_pet_kind_v2(root, _info):
320+
return str(root.favorite_pet_kind) + '_V2'
321+
322+
class Query(ObjectType):
323+
reporter = Field(ReporterType)
324+
325+
def resolve_reporter(self, _info):
326+
return session.query(Reporter).first()
327+
328+
reporter = Reporter(first_name='first_name', last_name='last_name', email='email', favorite_pet_kind='cat')
329+
session.add(reporter)
330+
session.commit()
331+
332+
schema = Schema(query=Query)
333+
result = schema.execute("""
334+
query {
335+
reporter {
336+
id
337+
firstName
338+
lastName
339+
email
340+
emailV2
341+
favoritePetKind
342+
favoritePetKindV2
343+
}
344+
}
345+
""")
346+
347+
assert not result.errors
348+
# Custom resolver on a base class
349+
assert result.data['reporter']['id'] == 'ID'
350+
# Default field + default resolver
351+
assert result.data['reporter']['firstName'] == 'first_name'
352+
# Default field + custom resolver
353+
assert result.data['reporter']['lastName'] == 'LAST_NAME'
354+
# ORMField + default resolver
355+
assert result.data['reporter']['email'] == 'email'
356+
# ORMField + custom resolver
357+
assert result.data['reporter']['emailV2'] == 'email_V2'
358+
# Field + default resolver
359+
assert result.data['reporter']['favoritePetKind'] == 'cat'
360+
# Field + custom resolver
361+
assert result.data['reporter']['favoritePetKindV2'] == 'cat_V2'
362+
363+
296364
# Test Custom SQLAlchemyObjectType Implementation
297365

298366
def test_custom_objecttype_registered():
@@ -306,7 +374,7 @@ class Meta:
306374

307375
assert issubclass(CustomReporterType, ObjectType)
308376
assert CustomReporterType._meta.model == Reporter
309-
assert len(CustomReporterType._meta.fields) == 10
377+
assert len(CustomReporterType._meta.fields) == 11
310378

311379

312380
# Test Custom SQLAlchemyObjectType with Custom Options

graphene_sqlalchemy/types.py

+26-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from graphene.relay import Connection, Node
1212
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
1313
from graphene.types.utils import yank_fields_from_attrs
14+
from graphene.utils.get_unbound_function import get_unbound_function
1415
from graphene.utils.orderedtype import OrderedType
1516

1617
from .converter import (convert_sqlalchemy_column,
@@ -151,20 +152,22 @@ def construct_fields(
151152
for orm_field_name, orm_field in orm_fields.items():
152153
attr_name = orm_field.kwargs.pop('model_attr')
153154
attr = all_model_attrs[attr_name]
155+
resolver = _get_field_resolver(obj_type, orm_field_name, attr_name)
154156

155157
if isinstance(attr, ColumnProperty):
156-
field = convert_sqlalchemy_column(attr, registry, **orm_field.kwargs)
158+
field = convert_sqlalchemy_column(attr, registry, resolver, **orm_field.kwargs)
157159
elif isinstance(attr, RelationshipProperty):
158-
field = convert_sqlalchemy_relationship(attr, registry, connection_field_factory, **orm_field.kwargs)
160+
field = convert_sqlalchemy_relationship(attr, registry, connection_field_factory, resolver,
161+
**orm_field.kwargs)
159162
elif isinstance(attr, CompositeProperty):
160163
if attr_name != orm_field_name or orm_field.kwargs:
161164
# TODO Add a way to override composite property fields
162165
raise ValueError(
163166
"ORMField kwargs for composite fields must be empty. "
164167
"Field: {}.{}".format(obj_type.__name__, orm_field_name))
165-
field = convert_sqlalchemy_composite(attr, registry)
168+
field = convert_sqlalchemy_composite(attr, registry, resolver)
166169
elif isinstance(attr, hybrid_property):
167-
field = convert_sqlalchemy_hybrid_method(attr, attr_name, **orm_field.kwargs)
170+
field = convert_sqlalchemy_hybrid_method(attr, resolver, **orm_field.kwargs)
168171
else:
169172
raise Exception('Property type is not supported') # Should never happen
170173

@@ -174,6 +177,25 @@ def construct_fields(
174177
return fields
175178

176179

180+
def _get_field_resolver(obj_type, orm_field_name, model_attr):
181+
"""
182+
In order to support field renaming via `ORMField.model_attr`,
183+
we need to define resolver functions for each field.
184+
185+
:param SQLAlchemyObjectType obj_type:
186+
:param model: the SQLAlchemy model
187+
:param str model_attr: the name of SQLAlchemy of the attribute used to resolve the field
188+
:rtype: Callable
189+
"""
190+
# Since `graphene` will call `resolve_<field_name>` on a field only if it
191+
# does not have a `resolver`, we need to re-implement that logic here.
192+
resolver = getattr(obj_type, 'resolve_{}'.format(orm_field_name), None)
193+
if resolver:
194+
return get_unbound_function(resolver)
195+
196+
return lambda root, _info: getattr(root, model_attr, None)
197+
198+
177199
class SQLAlchemyObjectTypeOptions(ObjectTypeOptions):
178200
model = None # type: sqlalchemy.Model
179201
registry = None # type: sqlalchemy.Registry

0 commit comments

Comments
 (0)