Skip to content

Commit 07b20a8

Browse files
committed
association_proxy support
1 parent 6dca279 commit 07b20a8

File tree

4 files changed

+100
-5
lines changed

4 files changed

+100
-5
lines changed

Diff for: graphene_sqlalchemy/converter.py

+56-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
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
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+
from sqlalchemy.orm.attributes import InstrumentedAttribute
711

812
from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List,
913
String)
@@ -18,6 +22,7 @@
1822
ChoiceType = JSONType = ScalarListType = TSVectorType = object
1923

2024

25+
2126
def get_column_doc(column):
2227
return getattr(column, "doc", None)
2328

@@ -26,6 +31,55 @@ def is_column_nullable(column):
2631
return bool(getattr(column, "nullable", True))
2732

2833

34+
def convert_sqlalchemy_association_proxy(association_prop, registry, connection_field_factory, resolver, **field_kwargs):
35+
model = association_prop.target_class
36+
37+
attr = getattr(model, association_prop.value_attr)
38+
if isinstance(attr, InstrumentedAttribute):
39+
attr = inspect(attr).property
40+
41+
def dynamic_type():
42+
if isinstance(attr, AssociationProxy):
43+
return convert_sqlalchemy_association_proxy(
44+
attr,
45+
registry,
46+
resolver,
47+
**field_kwargs
48+
)
49+
elif isinstance(attr, ColumnProperty):
50+
return convert_sqlalchemy_column(
51+
attr,
52+
registry,
53+
resolver,
54+
**field_kwargs
55+
)
56+
elif isinstance(attr, CompositeProperty):
57+
return convert_sqlalchemy_composite(
58+
attr,
59+
registry,
60+
resolver
61+
)
62+
elif isinstance(attr, RelationshipProperty):
63+
return convert_sqlalchemy_relationship(
64+
attr,
65+
registry,
66+
connection_field_factory,
67+
resolver,
68+
**field_kwargs
69+
# resolve Dynamic type
70+
).get_type()
71+
elif isinstance(attr, hybrid_property):
72+
return convert_sqlalchemy_hybrid_method(
73+
attr,
74+
resolver,
75+
**field_kwargs
76+
)
77+
78+
raise NotImplementedError(attr)
79+
80+
return Dynamic(dynamic_type)
81+
82+
2983
def convert_sqlalchemy_relationship(relationship_prop, registry, connection_field_factory, resolver, **field_kwargs):
3084
direction = relationship_prop.direction
3185
model = relationship_prop.mapper.entity

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

+28-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,
@@ -291,6 +292,32 @@ class Meta:
291292
assert graphene_type.type == A
292293

293294

295+
def test_should_convert_association_proxy():
296+
class P(SQLAlchemyObjectType):
297+
class Meta:
298+
model = Pet
299+
300+
dynamic_field = convert_sqlalchemy_association_proxy(
301+
Reporter.pet_names,
302+
P._meta.registry,
303+
default_connection_field_factory,
304+
mock_resolver,
305+
)
306+
assert isinstance(dynamic_field, graphene.Dynamic)
307+
graphene_type = dynamic_field.get_type()
308+
assert isinstance(graphene_type, graphene.Field)
309+
assert graphene_type.type == graphene.String
310+
311+
dynamic_field = convert_sqlalchemy_association_proxy(
312+
Article.reporter_pets,
313+
P._meta.registry,
314+
default_connection_field_factory,
315+
mock_resolver,
316+
)
317+
assert isinstance(dynamic_field.get_type().type, graphene.List)
318+
assert dynamic_field.get_type().type.of_type == P
319+
320+
294321
def test_should_postgresql_uuid_convert():
295322
assert get_field(postgresql.UUID()).type == graphene.String
296323

Diff for: graphene_sqlalchemy/types.py

+12-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, strategies)
@@ -14,7 +15,8 @@
1415
from graphene.utils.orderedtype import OrderedType
1516

1617
from .batching import get_batch_resolver
17-
from .converter import (convert_sqlalchemy_column,
18+
from .converter import (convert_sqlalchemy_association_proxy,
19+
convert_sqlalchemy_column,
1820
convert_sqlalchemy_composite,
1921
convert_sqlalchemy_hybrid_method,
2022
convert_sqlalchemy_relationship)
@@ -110,7 +112,7 @@ def construct_fields(
110112
inspected_model.column_attrs.items() +
111113
inspected_model.composites.items() +
112114
[(name, item) for name, item in inspected_model.all_orm_descriptors.items()
113-
if isinstance(item, hybrid_property)] +
115+
if isinstance(item, (AssociationProxy, hybrid_property))] +
114116
inspected_model.relationships.items()
115117
)
116118

@@ -186,6 +188,14 @@ def construct_fields(
186188
custom_resolver or _get_attr_resolver(obj_type, orm_field_name, attr_name),
187189
**orm_field.kwargs
188190
)
191+
elif isinstance(attr, AssociationProxy):
192+
field = convert_sqlalchemy_association_proxy(
193+
attr.for_class(model),
194+
registry,
195+
connection_field_factory,
196+
custom_resolver or _get_attr_resolver(obj_type, orm_field_name, attr_name),
197+
**orm_field.kwargs
198+
)
189199
else:
190200
raise Exception('Property type is not supported') # Should never happen
191201

0 commit comments

Comments
 (0)