Skip to content

Commit 4f3d44f

Browse files
committed
add support for sqlalchemy 1.1
1 parent 6b4b63c commit 4f3d44f

File tree

6 files changed

+54
-43
lines changed

6 files changed

+54
-43
lines changed

graphene_sqlalchemy/converter.py

+14-20
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from enum import EnumMeta
22

33
from singledispatch import singledispatch
4-
from sqlalchemy import inspect, types
4+
from sqlalchemy import types
55
from sqlalchemy.dialects import postgresql
6-
from sqlalchemy.ext.hybrid import hybrid_property
7-
from sqlalchemy.orm import (ColumnProperty, CompositeProperty,
8-
RelationshipProperty, interfaces, strategies)
9-
from sqlalchemy.orm.attributes import InstrumentedAttribute
6+
from sqlalchemy.orm import (ColumnProperty, RelationshipProperty, class_mapper,
7+
interfaces, strategies)
108

119
from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List,
1210
String)
@@ -36,39 +34,35 @@ def is_column_nullable(column):
3634
return bool(getattr(column, "nullable", True))
3735

3836

39-
def convert_sqlalchemy_association_proxy(association_prop, obj_type, registry, connection_field_factory, batching, resolver, **field_kwargs):
40-
model = association_prop.target_class
41-
42-
attr = getattr(model, association_prop.value_attr)
43-
if isinstance(attr, InstrumentedAttribute):
44-
attr = inspect(attr).property
45-
37+
def convert_sqlalchemy_association_proxy(parent, assoc_prop, obj_type, registry,
38+
connection_field_factory, batching, resolver, **field_kwargs):
4639
def dynamic_type():
40+
prop = class_mapper(parent).attrs[assoc_prop.target_collection]
41+
scalar = not prop.uselist
42+
model = prop.mapper.class_
43+
attr = class_mapper(model).attrs[assoc_prop.value_attr]
44+
4745
if isinstance(attr, ColumnProperty):
4846
field = convert_sqlalchemy_column(
4947
attr,
5048
registry,
5149
resolver,
5250
**field_kwargs
5351
)
54-
if not association_prop.scalar:
52+
if not scalar:
5553
# repackage as List
5654
field.__dict__['_type'] = List(field.type)
57-
5855
return field
5956
elif isinstance(attr, RelationshipProperty):
60-
batching_ = field_kwargs.pop('batching', batching)
6157
return convert_sqlalchemy_relationship(
6258
attr,
6359
obj_type,
6460
connection_field_factory,
65-
batching_,
66-
association_prop.value_attr,
61+
field_kwargs.pop('batching', batching),
62+
assoc_prop.value_attr,
6763
**field_kwargs
68-
# resolve Dynamic type
6964
).get_type()
70-
71-
raise NotImplementedError(attr)
65+
# else, not supported
7266

7367
return Dynamic(dynamic_type)
7468

graphene_sqlalchemy/tests/models.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def hybrid_prop(self):
7676

7777
composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite")
7878

79-
pet_names = association_proxy('pets', 'name')
79+
headlines = association_proxy('articles', 'headline')
8080

8181

8282
class Article(Base):
@@ -85,7 +85,7 @@ class Article(Base):
8585
headline = Column(String(100))
8686
pub_date = Column(Date())
8787
reporter_id = Column(Integer(), ForeignKey("reporters.id"))
88-
reporter_pets = association_proxy('reporter', 'pets')
88+
recommended_reads = association_proxy('reporter', 'articles')
8989

9090

9191
class ReflectedEditor(type):

graphene_sqlalchemy/tests/test_converter.py

+18-13
Original file line numberDiff line numberDiff line change
@@ -287,33 +287,38 @@ class Meta:
287287

288288

289289
def test_should_convert_association_proxy():
290-
class P(SQLAlchemyObjectType):
290+
class ReporterType(SQLAlchemyObjectType):
291291
class Meta:
292-
model = Pet
292+
model = Reporter
293293

294-
dynamic_field = convert_sqlalchemy_association_proxy(
295-
Reporter.pet_names,
296-
P,
294+
class ArticleType(SQLAlchemyObjectType):
295+
class Meta:
296+
model = Article
297+
298+
field = convert_sqlalchemy_association_proxy(
299+
Reporter,
300+
Reporter.headlines,
301+
ReporterType,
297302
get_global_registry(),
298303
default_connection_field_factory,
299304
True,
300305
mock_resolver,
301306
)
302-
assert isinstance(dynamic_field, graphene.Dynamic)
303-
graphene_type = dynamic_field.get_type()
304-
assert isinstance(graphene_type, graphene.Field)
305-
assert isinstance(graphene_type.type, graphene.List)
306-
assert graphene_type.type.of_type == graphene.String
307+
assert isinstance(field, graphene.Dynamic)
308+
assert isinstance(field.get_type().type, graphene.List)
309+
assert field.get_type().type.of_type == graphene.String
307310

308311
dynamic_field = convert_sqlalchemy_association_proxy(
309-
Article.reporter_pets,
310-
P,
312+
Article,
313+
Article.recommended_reads,
314+
ArticleType,
311315
get_global_registry(),
312316
default_connection_field_factory,
313317
True,
314318
mock_resolver,
315319
)
316-
assert dynamic_field.get_type().type.of_type == P
320+
assert isinstance(dynamic_field, graphene.Dynamic)
321+
assert dynamic_field.get_type().type.of_type == ArticleType
317322

318323

319324
def test_should_postgresql_uuid_convert():

graphene_sqlalchemy/tests/test_query.py

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def resolve_reporters(self, _info):
5757
columnProp
5858
hybridProp
5959
compositeProp
60+
headlines
6061
}
6162
reporters {
6263
firstName
@@ -69,6 +70,7 @@ def resolve_reporters(self, _info):
6970
"hybridProp": "John",
7071
"columnProp": 2,
7172
"compositeProp": "John Doe",
73+
"headlines": ['Hi!'],
7274
},
7375
"reporters": [{"firstName": "John"}, {"firstName": "Jane"}],
7476
}

graphene_sqlalchemy/tests/test_types.py

+16-6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ class Meta:
7171
model = Article
7272
interfaces = (Node,)
7373

74+
class PetType(SQLAlchemyObjectType):
75+
class Meta:
76+
model = Pet
77+
interfaces = (Node,)
78+
7479
assert list(ReporterType._meta.fields.keys()) == [
7580
# Columns
7681
"column_prop", # SQLAlchemy retuns column properties first
@@ -84,7 +89,7 @@ class Meta:
8489
# Hybrid
8590
"hybrid_prop",
8691
# AssociationProxy
87-
"pet_names",
92+
"headlines",
8893
# Relationship
8994
"pets",
9095
"articles",
@@ -121,9 +126,14 @@ class Meta:
121126
assert favorite_article_field.type().description is None
122127

123128
# assocation proxy
124-
assoc_prop = ReporterType._meta.fields['pet_names']
125-
assert isinstance(assoc_prop, Dynamic)
126-
assert assoc_prop.type().type == List(String)
129+
assoc_field = ReporterType._meta.fields['headlines']
130+
assert isinstance(assoc_field, Dynamic)
131+
assert isinstance(assoc_field.type().type, List)
132+
assert assoc_field.type().type.of_type == String
133+
134+
assoc_field = ArticleType._meta.fields['recommended_reads']
135+
assert isinstance(assoc_field, Dynamic)
136+
assert assoc_field.type().type == ArticleType.connection
127137

128138

129139
def test_sqlalchemy_override_fields():
@@ -186,7 +196,7 @@ class Meta:
186196
# Then the automatic SQLAlchemy fields
187197
"id",
188198
"favorite_pet_kind",
189-
"pet_names",
199+
"headlines",
190200
]
191201

192202
first_name_field = ReporterType._meta.fields['first_name']
@@ -284,7 +294,7 @@ class Meta:
284294
"favorite_pet_kind",
285295
"composite_prop",
286296
"hybrid_prop",
287-
"pet_names",
297+
"headlines",
288298
"pets",
289299
"articles",
290300
"favorite_article",

graphene_sqlalchemy/types.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from graphene.types.utils import yank_fields_from_attrs
1414
from graphene.utils.orderedtype import OrderedType
1515

16-
from .batching import get_batch_resolver
1716
from .converter import (convert_sqlalchemy_association_proxy,
1817
convert_sqlalchemy_column,
1918
convert_sqlalchemy_composite,
@@ -179,7 +178,8 @@ def construct_fields(
179178
field = convert_sqlalchemy_hybrid_method(attr, resolver, **orm_field.kwargs)
180179
elif isinstance(attr, AssociationProxy):
181180
field = convert_sqlalchemy_association_proxy(
182-
attr.for_class(model),
181+
model,
182+
attr,
183183
obj_type,
184184
registry,
185185
connection_field_factory,

0 commit comments

Comments
 (0)