Skip to content

Commit

Permalink
Merge 09dd6af into 3c3442e
Browse files Browse the repository at this point in the history
  • Loading branch information
dpep committed Feb 24, 2020
2 parents 3c3442e + 09dd6af commit de70604
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 4 deletions.
36 changes: 35 additions & 1 deletion graphene_sqlalchemy/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from singledispatch import singledispatch
from sqlalchemy import types
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import interfaces, strategies
from sqlalchemy.orm import (ColumnProperty, RelationshipProperty, class_mapper,
interfaces, strategies)

from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List,
String)
Expand Down Expand Up @@ -33,6 +34,39 @@ def is_column_nullable(column):
return bool(getattr(column, "nullable", True))


def convert_sqlalchemy_association_proxy(parent, assoc_prop, obj_type, registry,
connection_field_factory, batching, resolver, **field_kwargs):
def dynamic_type():
prop = class_mapper(parent).attrs[assoc_prop.target_collection]
scalar = not prop.uselist
model = prop.mapper.class_
attr = class_mapper(model).attrs[assoc_prop.value_attr]

if isinstance(attr, ColumnProperty):
field = convert_sqlalchemy_column(
attr,
registry,
resolver,
**field_kwargs
)
if not scalar:
# repackage as List
field.__dict__['_type'] = List(field.type)
return field
elif isinstance(attr, RelationshipProperty):
return convert_sqlalchemy_relationship(
attr,
obj_type,
connection_field_factory,
field_kwargs.pop('batching', batching),
assoc_prop.value_attr,
**field_kwargs
).get_type()
# else, not supported

return Dynamic(dynamic_type)


def convert_sqlalchemy_relationship(relationship_prop, obj_type, connection_field_factory, batching,
orm_field_name, **field_kwargs):
"""
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 @@ -75,13 +76,16 @@ def hybrid_prop(self):

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

headlines = association_proxy('articles', 'headline')


class Article(Base):
__tablename__ = "articles"
id = Column(Integer(), primary_key=True)
headline = Column(String(100))
pub_date = Column(Date())
reporter_id = Column(Integer(), ForeignKey("reporters.id"))
recommended_reads = association_proxy('reporter', 'articles')


class ReflectedEditor(type):
Expand Down
37 changes: 36 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 @@ -285,6 +286,40 @@ class Meta:
assert graphene_type.type == A


def test_should_convert_association_proxy():
class PetType(SQLAlchemyObjectType):
class Meta:
model = Pet
class ArticleType(SQLAlchemyObjectType):
class Meta:
model = Article

field = convert_sqlalchemy_association_proxy(
Reporter,
Reporter.headlines,
PetType,
get_global_registry(),
default_connection_field_factory,
True,
mock_resolver,
)
assert isinstance(field, graphene.Dynamic)
assert isinstance(field.get_type().type, graphene.List)
assert field.get_type().type.of_type == graphene.String

dynamic_field = convert_sqlalchemy_association_proxy(
Article,
Article.recommended_reads,
ArticleType,
get_global_registry(),
default_connection_field_factory,
True,
mock_resolver,
)
assert isinstance(dynamic_field, graphene.Dynamic)
assert dynamic_field.get_type().type.of_type == ArticleType


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

Expand Down
2 changes: 2 additions & 0 deletions graphene_sqlalchemy/tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def resolve_reporters(self, _info):
columnProp
hybridProp
compositeProp
headlines
}
reporters {
firstName
Expand All @@ -69,6 +70,7 @@ def resolve_reporters(self, _info):
"hybridProp": "John",
"columnProp": 2,
"compositeProp": "John Doe",
"headlines": ['Hi!'],
},
"reporters": [{"firstName": "John"}, {"firstName": "Jane"}],
}
Expand Down
22 changes: 21 additions & 1 deletion graphene_sqlalchemy/tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ class Meta:
model = Article
interfaces = (Node,)

class PetType(SQLAlchemyObjectType):
class Meta:
model = Pet
interfaces = (Node,)


assert list(ReporterType._meta.fields.keys()) == [
# Columns
"column_prop", # SQLAlchemy retuns column properties first
Expand All @@ -83,6 +89,8 @@ class Meta:
"composite_prop",
# Hybrid
"hybrid_prop",
# AssociationProxy
"headlines",
# Relationship
"pets",
"articles",
Expand Down Expand Up @@ -118,6 +126,16 @@ class Meta:
assert favorite_article_field.type().type == ArticleType
assert favorite_article_field.type().description is None

# assocation proxy
assoc_field = ReporterType._meta.fields['headlines']
assert isinstance(assoc_field, Dynamic)
assert isinstance(assoc_field.type().type, List)
assert assoc_field.type().type.of_type == String

assoc_field = ArticleType._meta.fields['recommended_reads']
assert isinstance(assoc_field, Dynamic)
assert assoc_field.type().type == ArticleType.connection


def test_sqlalchemy_override_fields():
@convert_sqlalchemy_composite.register(CompositeFullName)
Expand Down Expand Up @@ -179,6 +197,7 @@ class Meta:
# Then the automatic SQLAlchemy fields
"id",
"favorite_pet_kind",
"headlines",
]

first_name_field = ReporterType._meta.fields['first_name']
Expand Down Expand Up @@ -276,6 +295,7 @@ class Meta:
"favorite_pet_kind",
"composite_prop",
"hybrid_prop",
"headlines",
"pets",
"articles",
"favorite_article",
Expand Down Expand Up @@ -384,7 +404,7 @@ class Meta:

assert issubclass(CustomReporterType, ObjectType)
assert CustomReporterType._meta.model == Reporter
assert len(CustomReporterType._meta.fields) == 11
assert len(CustomReporterType._meta.fields) == 12


# Test Custom SQLAlchemyObjectType with Custom Options
Expand Down
17 changes: 16 additions & 1 deletion 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)
Expand All @@ -12,7 +13,8 @@
from graphene.types.utils import yank_fields_from_attrs
from graphene.utils.orderedtype import OrderedType

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 @@ -114,6 +116,8 @@ def construct_fields(
inspected_model.composites.items() +
[(name, item) for name, item in inspected_model.all_orm_descriptors.items()
if isinstance(item, hybrid_property)] +
[(name, item) for name, item in inspected_model.all_orm_descriptors.items()
if isinstance(item, AssociationProxy)] +
inspected_model.relationships.items()
)

Expand Down Expand Up @@ -172,6 +176,17 @@ def construct_fields(
field = convert_sqlalchemy_composite(attr, registry, resolver)
elif isinstance(attr, hybrid_property):
field = convert_sqlalchemy_hybrid_method(attr, resolver, **orm_field.kwargs)
elif isinstance(attr, AssociationProxy):
field = convert_sqlalchemy_association_proxy(
model,
attr,
obj_type,
registry,
connection_field_factory,
batching,
resolver,
**orm_field.kwargs
)
else:
raise Exception('Property type is not supported') # Should never happen

Expand Down

0 comments on commit de70604

Please sign in to comment.