Skip to content

Commit 3b43bb7

Browse files
committed
association_proxy support
1 parent 3c3442e commit 3b43bb7

File tree

4 files changed

+108
-5
lines changed

4 files changed

+108
-5
lines changed

Diff for: graphene_sqlalchemy/converter.py

+60-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
from enum import EnumMeta
22

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

813
from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List,
914
String)
@@ -33,6 +38,59 @@ def is_column_nullable(column):
3338
return bool(getattr(column, "nullable", True))
3439

3540

41+
def convert_sqlalchemy_association_proxy(association_prop, registry, connection_field_factory, batching, resolver, **field_kwargs):
42+
model = association_prop.target_class
43+
44+
attr = getattr(model, association_prop.value_attr)
45+
if isinstance(attr, InstrumentedAttribute):
46+
attr = inspect(attr).property
47+
48+
def dynamic_type():
49+
if isinstance(attr, AssociationProxy):
50+
return convert_sqlalchemy_association_proxy(
51+
attr,
52+
registry,
53+
connection_field_factory,
54+
batching,
55+
resolver,
56+
**field_kwargs
57+
)
58+
elif isinstance(attr, ColumnProperty):
59+
return convert_sqlalchemy_column(
60+
attr,
61+
registry,
62+
resolver,
63+
**field_kwargs
64+
)
65+
elif isinstance(attr, CompositeProperty):
66+
return convert_sqlalchemy_composite(
67+
attr,
68+
registry,
69+
resolver
70+
)
71+
elif isinstance(attr, RelationshipProperty):
72+
batching_ = field_kwargs.pop('batching', batching)
73+
return convert_sqlalchemy_relationship(
74+
attr,
75+
registry,
76+
connection_field_factory,
77+
batching_,
78+
association_prop.value_attr,
79+
**field_kwargs
80+
# resolve Dynamic type
81+
).get_type()
82+
elif isinstance(attr, hybrid_property):
83+
return convert_sqlalchemy_hybrid_method(
84+
attr,
85+
resolver,
86+
**field_kwargs
87+
)
88+
89+
raise NotImplementedError(attr)
90+
91+
return Dynamic(dynamic_type)
92+
93+
3694
def convert_sqlalchemy_relationship(relationship_prop, obj_type, connection_field_factory, batching,
3795
orm_field_name, **field_kwargs):
3896
"""

Diff for: graphene_sqlalchemy/tests/models.py

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from sqlalchemy import (Column, Date, Enum, ForeignKey, Integer, String, Table,
66
func, select)
7+
from sqlalchemy.ext.associationproxy import association_proxy
78
from sqlalchemy.ext.declarative import declarative_base
89
from sqlalchemy.ext.hybrid import hybrid_property
910
from sqlalchemy.orm import column_property, composite, mapper, relationship
@@ -65,6 +66,8 @@ class Reporter(Base):
6566
articles = relationship("Article", backref="reporter")
6667
favorite_article = relationship("Article", uselist=False)
6768

69+
pet_names = association_proxy('pets', 'name')
70+
6871
@hybrid_property
6972
def hybrid_prop(self):
7073
return self.first_name
@@ -82,6 +85,7 @@ class Article(Base):
8285
headline = Column(String(100))
8386
pub_date = Column(Date())
8487
reporter_id = Column(Integer(), ForeignKey("reporters.id"))
88+
reporter_pets = association_proxy('reporter', 'pets')
8589

8690

8791
class ReflectedEditor(type):

Diff for: graphene_sqlalchemy/tests/test_converter.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from graphene.types.datetime import DateTime
1414
from graphene.types.json import JSONString
1515

16-
from ..converter import (convert_sqlalchemy_column,
16+
from ..converter import (convert_sqlalchemy_association_proxy,
17+
convert_sqlalchemy_column,
1718
convert_sqlalchemy_composite,
1819
convert_sqlalchemy_relationship)
1920
from ..fields import (UnsortedSQLAlchemyConnectionField,
@@ -285,6 +286,34 @@ class Meta:
285286
assert graphene_type.type == A
286287

287288

289+
def test_should_convert_association_proxy():
290+
class P(SQLAlchemyObjectType):
291+
class Meta:
292+
model = Pet
293+
294+
dynamic_field = convert_sqlalchemy_association_proxy(
295+
Reporter.pet_names,
296+
P,
297+
default_connection_field_factory,
298+
True,
299+
mock_resolver,
300+
)
301+
assert isinstance(dynamic_field, graphene.Dynamic)
302+
graphene_type = dynamic_field.get_type()
303+
assert isinstance(graphene_type, graphene.Field)
304+
assert graphene_type.type == graphene.String
305+
306+
dynamic_field = convert_sqlalchemy_association_proxy(
307+
Article.reporter_pets,
308+
P,
309+
default_connection_field_factory,
310+
True,
311+
mock_resolver,
312+
)
313+
assert isinstance(dynamic_field.get_type().type, graphene.List)
314+
assert dynamic_field.get_type().type.of_type == P
315+
316+
288317
def test_should_postgresql_uuid_convert():
289318
assert get_field(postgresql.UUID()).type == graphene.String
290319

Diff for: graphene_sqlalchemy/types.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from collections import OrderedDict
22

33
import sqlalchemy
4+
from sqlalchemy.ext.associationproxy import AssociationProxy
45
from sqlalchemy.ext.hybrid import hybrid_property
56
from sqlalchemy.orm import (ColumnProperty, CompositeProperty,
67
RelationshipProperty)
@@ -12,7 +13,9 @@
1213
from graphene.types.utils import yank_fields_from_attrs
1314
from graphene.utils.orderedtype import OrderedType
1415

15-
from .converter import (convert_sqlalchemy_column,
16+
from .batching import get_batch_resolver
17+
from .converter import (convert_sqlalchemy_association_proxy,
18+
convert_sqlalchemy_column,
1619
convert_sqlalchemy_composite,
1720
convert_sqlalchemy_hybrid_method,
1821
convert_sqlalchemy_relationship)
@@ -113,7 +116,7 @@ def construct_fields(
113116
inspected_model.column_attrs.items() +
114117
inspected_model.composites.items() +
115118
[(name, item) for name, item in inspected_model.all_orm_descriptors.items()
116-
if isinstance(item, hybrid_property)] +
119+
if isinstance(item, (AssociationProxy, hybrid_property))] +
117120
inspected_model.relationships.items()
118121
)
119122

@@ -172,6 +175,15 @@ def construct_fields(
172175
field = convert_sqlalchemy_composite(attr, registry, resolver)
173176
elif isinstance(attr, hybrid_property):
174177
field = convert_sqlalchemy_hybrid_method(attr, resolver, **orm_field.kwargs)
178+
elif isinstance(attr, AssociationProxy):
179+
field = convert_sqlalchemy_association_proxy(
180+
attr.for_class(model),
181+
registry,
182+
connection_field_factory,
183+
batching,
184+
resolver,
185+
**orm_field.kwargs
186+
)
175187
else:
176188
raise Exception('Property type is not supported') # Should never happen
177189

0 commit comments

Comments
 (0)