Skip to content

Commit

Permalink
association_proxy support
Browse files Browse the repository at this point in the history
  • Loading branch information
dpep committed Feb 12, 2020
1 parent 6dca279 commit 07b20a8
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 5 deletions.
58 changes: 56 additions & 2 deletions graphene_sqlalchemy/converter.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from enum import EnumMeta

from singledispatch import singledispatch
from sqlalchemy import types
from sqlalchemy import inspect, types
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import interfaces
from sqlalchemy.ext.associationproxy import AssociationProxy
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import (ColumnProperty, CompositeProperty,
interfaces, RelationshipProperty)
from sqlalchemy.orm.attributes import InstrumentedAttribute

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



def get_column_doc(column):
return getattr(column, "doc", None)

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


def convert_sqlalchemy_association_proxy(association_prop, registry, connection_field_factory, resolver, **field_kwargs):
model = association_prop.target_class

attr = getattr(model, association_prop.value_attr)
if isinstance(attr, InstrumentedAttribute):
attr = inspect(attr).property

def dynamic_type():
if isinstance(attr, AssociationProxy):
return convert_sqlalchemy_association_proxy(
attr,
registry,
resolver,
**field_kwargs
)
elif isinstance(attr, ColumnProperty):
return convert_sqlalchemy_column(
attr,
registry,
resolver,
**field_kwargs
)
elif isinstance(attr, CompositeProperty):
return convert_sqlalchemy_composite(
attr,
registry,
resolver
)
elif isinstance(attr, RelationshipProperty):
return convert_sqlalchemy_relationship(
attr,
registry,
connection_field_factory,
resolver,
**field_kwargs
# resolve Dynamic type
).get_type()
elif isinstance(attr, hybrid_property):
return convert_sqlalchemy_hybrid_method(
attr,
resolver,
**field_kwargs
)

raise NotImplementedError(attr)

return Dynamic(dynamic_type)


def convert_sqlalchemy_relationship(relationship_prop, registry, connection_field_factory, resolver, **field_kwargs):
direction = relationship_prop.direction
model = relationship_prop.mapper.entity
Expand Down
4 changes: 4 additions & 0 deletions graphene_sqlalchemy/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

pet_names = association_proxy('pets', 'name')

@hybrid_property
def hybrid_prop(self):
return self.first_name
Expand All @@ -82,6 +85,7 @@ class Article(Base):
headline = Column(String(100))
pub_date = Column(Date())
reporter_id = Column(Integer(), ForeignKey("reporters.id"))
reporter_pets = association_proxy('reporter', 'pets')


class ReflectedEditor(type):
Expand Down
29 changes: 28 additions & 1 deletion graphene_sqlalchemy/tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from graphene.types.datetime import DateTime
from graphene.types.json import JSONString

from ..converter import (convert_sqlalchemy_column,
from ..converter import (convert_sqlalchemy_association_proxy,
convert_sqlalchemy_column,
convert_sqlalchemy_composite,
convert_sqlalchemy_relationship)
from ..fields import (UnsortedSQLAlchemyConnectionField,
Expand Down Expand Up @@ -291,6 +292,32 @@ class Meta:
assert graphene_type.type == A


def test_should_convert_association_proxy():
class P(SQLAlchemyObjectType):
class Meta:
model = Pet

dynamic_field = convert_sqlalchemy_association_proxy(
Reporter.pet_names,
P._meta.registry,
default_connection_field_factory,
mock_resolver,
)
assert isinstance(dynamic_field, graphene.Dynamic)
graphene_type = dynamic_field.get_type()
assert isinstance(graphene_type, graphene.Field)
assert graphene_type.type == graphene.String

dynamic_field = convert_sqlalchemy_association_proxy(
Article.reporter_pets,
P._meta.registry,
default_connection_field_factory,
mock_resolver,
)
assert isinstance(dynamic_field.get_type().type, graphene.List)
assert dynamic_field.get_type().type.of_type == P


def test_should_postgresql_uuid_convert():
assert get_field(postgresql.UUID()).type == graphene.String

Expand Down
14 changes: 12 additions & 2 deletions graphene_sqlalchemy/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections import OrderedDict

import sqlalchemy
from sqlalchemy.ext.associationproxy import AssociationProxy
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import (ColumnProperty, CompositeProperty,
RelationshipProperty, strategies)
Expand All @@ -14,7 +15,8 @@
from graphene.utils.orderedtype import OrderedType

from .batching import get_batch_resolver
from .converter import (convert_sqlalchemy_column,
from .converter import (convert_sqlalchemy_association_proxy,
convert_sqlalchemy_column,
convert_sqlalchemy_composite,
convert_sqlalchemy_hybrid_method,
convert_sqlalchemy_relationship)
Expand Down Expand Up @@ -110,7 +112,7 @@ def construct_fields(
inspected_model.column_attrs.items() +
inspected_model.composites.items() +
[(name, item) for name, item in inspected_model.all_orm_descriptors.items()
if isinstance(item, hybrid_property)] +
if isinstance(item, (AssociationProxy, hybrid_property))] +
inspected_model.relationships.items()
)

Expand Down Expand Up @@ -186,6 +188,14 @@ def construct_fields(
custom_resolver or _get_attr_resolver(obj_type, orm_field_name, attr_name),
**orm_field.kwargs
)
elif isinstance(attr, AssociationProxy):
field = convert_sqlalchemy_association_proxy(
attr.for_class(model),
registry,
connection_field_factory,
custom_resolver or _get_attr_resolver(obj_type, orm_field_name, attr_name),
**orm_field.kwargs
)
else:
raise Exception('Property type is not supported') # Should never happen

Expand Down

0 comments on commit 07b20a8

Please sign in to comment.