From b369eb6d4c21492bd5032190117b9155a4b66581 Mon Sep 17 00:00:00 2001 From: dpepper Date: Wed, 12 Feb 2020 14:53:51 -0800 Subject: [PATCH 1/7] association_proxy support --- graphene_sqlalchemy/converter.py | 62 ++++++++++++++++++++- graphene_sqlalchemy/tests/models.py | 4 ++ graphene_sqlalchemy/tests/test_converter.py | 33 ++++++++++- graphene_sqlalchemy/types.py | 17 +++++- 4 files changed, 111 insertions(+), 5 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index f4b805e2..991c07ae 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,9 +1,14 @@ 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, strategies +from sqlalchemy.ext.associationproxy import AssociationProxy +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import (ColumnProperty, CompositeProperty, + interfaces, RelationshipProperty, + strategies) +from sqlalchemy.orm.attributes import InstrumentedAttribute from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List, String) @@ -33,6 +38,59 @@ def is_column_nullable(column): return bool(getattr(column, "nullable", True)) +def convert_sqlalchemy_association_proxy(association_prop, obj_type, registry, connection_field_factory, batching, 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, + connection_field_factory, + batching, + 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): + batching_ = field_kwargs.pop('batching', batching) + return convert_sqlalchemy_relationship( + attr, + obj_type, + connection_field_factory, + batching_, + association_prop.value_attr, + **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, obj_type, connection_field_factory, batching, orm_field_name, **field_kwargs): """ diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index 88e992b9..d7b089ce 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -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 @@ -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 @@ -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): diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index e9ee2379..1794ffa3 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -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, @@ -285,6 +286,36 @@ 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, + get_global_registry(), + default_connection_field_factory, + True, + 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, + get_global_registry(), + default_connection_field_factory, + True, + 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 diff --git a/graphene_sqlalchemy/types.py b/graphene_sqlalchemy/types.py index ff22cded..034f712a 100644 --- a/graphene_sqlalchemy/types.py +++ b/graphene_sqlalchemy/types.py @@ -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) @@ -12,7 +13,9 @@ from graphene.types.utils import yank_fields_from_attrs from graphene.utils.orderedtype import OrderedType -from .converter import (convert_sqlalchemy_column, +from .batching import get_batch_resolver +from .converter import (convert_sqlalchemy_association_proxy, + convert_sqlalchemy_column, convert_sqlalchemy_composite, convert_sqlalchemy_hybrid_method, convert_sqlalchemy_relationship) @@ -113,7 +116,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() ) @@ -172,6 +175,16 @@ 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( + attr.for_class(model), + obj_type, + registry, + connection_field_factory, + batching, + resolver, + **orm_field.kwargs + ) else: raise Exception('Property type is not supported') # Should never happen From c50b265c7858072ed2421075b9ecd92f6b6ee301 Mon Sep 17 00:00:00 2001 From: dpepper Date: Wed, 12 Feb 2020 16:30:31 -0800 Subject: [PATCH 2/7] better support for assoc proxy lists (rather than one-to-one) --- graphene_sqlalchemy/converter.py | 14 ++++++++++---- graphene_sqlalchemy/tests/test_converter.py | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 991c07ae..f9dc24c3 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -49,6 +49,7 @@ def dynamic_type(): if isinstance(attr, AssociationProxy): return convert_sqlalchemy_association_proxy( attr, + obj_type, registry, connection_field_factory, batching, @@ -56,10 +57,15 @@ def dynamic_type(): **field_kwargs ) elif isinstance(attr, ColumnProperty): - return convert_sqlalchemy_column( - attr, - registry, - resolver, + # similar to convert_sqlalchemy_column, but supports lists + column = attr.columns[0] + type_ = field_kwargs.pop('type', convert_sqlalchemy_type(getattr(column, "type", None), column, registry)) + field_kwargs.setdefault('required', not is_column_nullable(column)) + field_kwargs.setdefault('description', get_column_doc(column)) + + return Field( + type_ if association_prop.scalar else List(type_), + resolver=resolver, **field_kwargs ) elif isinstance(attr, CompositeProperty): diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 1794ffa3..071f5794 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -302,7 +302,8 @@ class Meta: assert isinstance(dynamic_field, graphene.Dynamic) graphene_type = dynamic_field.get_type() assert isinstance(graphene_type, graphene.Field) - assert graphene_type.type == graphene.String + assert isinstance(graphene_type.type, graphene.List) + assert graphene_type.type.of_type == graphene.String dynamic_field = convert_sqlalchemy_association_proxy( Article.reporter_pets, @@ -312,7 +313,6 @@ class Meta: True, mock_resolver, ) - assert isinstance(dynamic_field.get_type().type, graphene.List) assert dynamic_field.get_type().type.of_type == P From 6b4b63c0b2bce5ab59719e8439c96ce69cc663d7 Mon Sep 17 00:00:00 2001 From: dpepper Date: Thu, 13 Feb 2020 10:37:49 -0800 Subject: [PATCH 3/7] scope down --- graphene_sqlalchemy/converter.py | 40 +++++-------------------- graphene_sqlalchemy/tests/models.py | 4 +-- graphene_sqlalchemy/tests/test_types.py | 11 ++++++- graphene_sqlalchemy/types.py | 4 ++- 4 files changed, 23 insertions(+), 36 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index f9dc24c3..1c69da70 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -3,11 +3,9 @@ from singledispatch import singledispatch from sqlalchemy import inspect, types from sqlalchemy.dialects import postgresql -from sqlalchemy.ext.associationproxy import AssociationProxy from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import (ColumnProperty, CompositeProperty, - interfaces, RelationshipProperty, - strategies) + RelationshipProperty, interfaces, strategies) from sqlalchemy.orm.attributes import InstrumentedAttribute from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List, @@ -46,34 +44,18 @@ def convert_sqlalchemy_association_proxy(association_prop, obj_type, registry, c attr = inspect(attr).property def dynamic_type(): - if isinstance(attr, AssociationProxy): - return convert_sqlalchemy_association_proxy( + if isinstance(attr, ColumnProperty): + field = convert_sqlalchemy_column( attr, - obj_type, registry, - connection_field_factory, - batching, resolver, **field_kwargs ) - elif isinstance(attr, ColumnProperty): - # similar to convert_sqlalchemy_column, but supports lists - column = attr.columns[0] - type_ = field_kwargs.pop('type', convert_sqlalchemy_type(getattr(column, "type", None), column, registry)) - field_kwargs.setdefault('required', not is_column_nullable(column)) - field_kwargs.setdefault('description', get_column_doc(column)) - - return Field( - type_ if association_prop.scalar else List(type_), - resolver=resolver, - **field_kwargs - ) - elif isinstance(attr, CompositeProperty): - return convert_sqlalchemy_composite( - attr, - registry, - resolver - ) + if not association_prop.scalar: + # repackage as List + field.__dict__['_type'] = List(field.type) + + return field elif isinstance(attr, RelationshipProperty): batching_ = field_kwargs.pop('batching', batching) return convert_sqlalchemy_relationship( @@ -85,12 +67,6 @@ def dynamic_type(): **field_kwargs # resolve Dynamic type ).get_type() - elif isinstance(attr, hybrid_property): - return convert_sqlalchemy_hybrid_method( - attr, - resolver, - **field_kwargs - ) raise NotImplementedError(attr) diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index d7b089ce..d84cfcf9 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -66,8 +66,6 @@ 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 @@ -78,6 +76,8 @@ def hybrid_prop(self): composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite") + pet_names = association_proxy('pets', 'name') + class Article(Base): __tablename__ = "articles" diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index bf563b6e..f51f7c51 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -83,6 +83,8 @@ class Meta: "composite_prop", # Hybrid "hybrid_prop", + # AssociationProxy + "pet_names", # Relationship "pets", "articles", @@ -118,6 +120,11 @@ class Meta: assert favorite_article_field.type().type == ArticleType assert favorite_article_field.type().description is None + # assocation proxy + assoc_prop = ReporterType._meta.fields['pet_names'] + assert isinstance(assoc_prop, Dynamic) + assert assoc_prop.type().type == List(String) + def test_sqlalchemy_override_fields(): @convert_sqlalchemy_composite.register(CompositeFullName) @@ -179,6 +186,7 @@ class Meta: # Then the automatic SQLAlchemy fields "id", "favorite_pet_kind", + "pet_names", ] first_name_field = ReporterType._meta.fields['first_name'] @@ -276,6 +284,7 @@ class Meta: "favorite_pet_kind", "composite_prop", "hybrid_prop", + "pet_names", "pets", "articles", "favorite_article", @@ -384,7 +393,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 diff --git a/graphene_sqlalchemy/types.py b/graphene_sqlalchemy/types.py index 034f712a..ffa8348d 100644 --- a/graphene_sqlalchemy/types.py +++ b/graphene_sqlalchemy/types.py @@ -116,7 +116,9 @@ 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, (AssociationProxy, hybrid_property))] + + 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() ) From 4f3d44f1f0e22fc9a8fdb21ec2f17e0fd8af64db Mon Sep 17 00:00:00 2001 From: dpepper Date: Sun, 23 Feb 2020 13:29:21 -0800 Subject: [PATCH 4/7] add support for sqlalchemy 1.1 --- graphene_sqlalchemy/converter.py | 34 +++++++++------------ graphene_sqlalchemy/tests/models.py | 4 +-- graphene_sqlalchemy/tests/test_converter.py | 31 +++++++++++-------- graphene_sqlalchemy/tests/test_query.py | 2 ++ graphene_sqlalchemy/tests/test_types.py | 22 +++++++++---- graphene_sqlalchemy/types.py | 4 +-- 6 files changed, 54 insertions(+), 43 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 1c69da70..66634baa 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,12 +1,10 @@ from enum import EnumMeta from singledispatch import singledispatch -from sqlalchemy import inspect, types +from sqlalchemy import types from sqlalchemy.dialects import postgresql -from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import (ColumnProperty, CompositeProperty, - RelationshipProperty, interfaces, strategies) -from sqlalchemy.orm.attributes import InstrumentedAttribute +from sqlalchemy.orm import (ColumnProperty, RelationshipProperty, class_mapper, + interfaces, strategies) from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List, String) @@ -36,14 +34,14 @@ def is_column_nullable(column): return bool(getattr(column, "nullable", True)) -def convert_sqlalchemy_association_proxy(association_prop, obj_type, registry, connection_field_factory, batching, resolver, **field_kwargs): - model = association_prop.target_class - - attr = getattr(model, association_prop.value_attr) - if isinstance(attr, InstrumentedAttribute): - attr = inspect(attr).property - +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, @@ -51,24 +49,20 @@ def dynamic_type(): resolver, **field_kwargs ) - if not association_prop.scalar: + if not scalar: # repackage as List field.__dict__['_type'] = List(field.type) - return field elif isinstance(attr, RelationshipProperty): - batching_ = field_kwargs.pop('batching', batching) return convert_sqlalchemy_relationship( attr, obj_type, connection_field_factory, - batching_, - association_prop.value_attr, + field_kwargs.pop('batching', batching), + assoc_prop.value_attr, **field_kwargs - # resolve Dynamic type ).get_type() - - raise NotImplementedError(attr) + # else, not supported return Dynamic(dynamic_type) diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index d84cfcf9..fdd5e41f 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -76,7 +76,7 @@ def hybrid_prop(self): composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite") - pet_names = association_proxy('pets', 'name') + headlines = association_proxy('articles', 'headline') class Article(Base): @@ -85,7 +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') + recommended_reads = association_proxy('reporter', 'articles') class ReflectedEditor(type): diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 071f5794..7c854f4c 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -287,33 +287,38 @@ class Meta: def test_should_convert_association_proxy(): - class P(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Pet + model = Reporter - dynamic_field = convert_sqlalchemy_association_proxy( - Reporter.pet_names, - P, + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + + field = convert_sqlalchemy_association_proxy( + Reporter, + Reporter.headlines, + ReporterType, get_global_registry(), default_connection_field_factory, True, mock_resolver, ) - assert isinstance(dynamic_field, graphene.Dynamic) - graphene_type = dynamic_field.get_type() - assert isinstance(graphene_type, graphene.Field) - assert isinstance(graphene_type.type, graphene.List) - assert graphene_type.type.of_type == graphene.String + 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.reporter_pets, - P, + Article, + Article.recommended_reads, + ArticleType, get_global_registry(), default_connection_field_factory, True, mock_resolver, ) - assert dynamic_field.get_type().type.of_type == P + assert isinstance(dynamic_field, graphene.Dynamic) + assert dynamic_field.get_type().type.of_type == ArticleType def test_should_postgresql_uuid_convert(): diff --git a/graphene_sqlalchemy/tests/test_query.py b/graphene_sqlalchemy/tests/test_query.py index 39140814..80418824 100644 --- a/graphene_sqlalchemy/tests/test_query.py +++ b/graphene_sqlalchemy/tests/test_query.py @@ -57,6 +57,7 @@ def resolve_reporters(self, _info): columnProp hybridProp compositeProp + headlines } reporters { firstName @@ -69,6 +70,7 @@ def resolve_reporters(self, _info): "hybridProp": "John", "columnProp": 2, "compositeProp": "John Doe", + "headlines": ['Hi!'], }, "reporters": [{"firstName": "John"}, {"firstName": "Jane"}], } diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index f51f7c51..735e9f85 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -71,6 +71,11 @@ 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 @@ -84,7 +89,7 @@ class Meta: # Hybrid "hybrid_prop", # AssociationProxy - "pet_names", + "headlines", # Relationship "pets", "articles", @@ -121,9 +126,14 @@ class Meta: assert favorite_article_field.type().description is None # assocation proxy - assoc_prop = ReporterType._meta.fields['pet_names'] - assert isinstance(assoc_prop, Dynamic) - assert assoc_prop.type().type == List(String) + 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(): @@ -186,7 +196,7 @@ class Meta: # Then the automatic SQLAlchemy fields "id", "favorite_pet_kind", - "pet_names", + "headlines", ] first_name_field = ReporterType._meta.fields['first_name'] @@ -284,7 +294,7 @@ class Meta: "favorite_pet_kind", "composite_prop", "hybrid_prop", - "pet_names", + "headlines", "pets", "articles", "favorite_article", diff --git a/graphene_sqlalchemy/types.py b/graphene_sqlalchemy/types.py index ffa8348d..96ce0479 100644 --- a/graphene_sqlalchemy/types.py +++ b/graphene_sqlalchemy/types.py @@ -13,7 +13,6 @@ from graphene.types.utils import yank_fields_from_attrs from graphene.utils.orderedtype import OrderedType -from .batching import get_batch_resolver from .converter import (convert_sqlalchemy_association_proxy, convert_sqlalchemy_column, convert_sqlalchemy_composite, @@ -179,7 +178,8 @@ def construct_fields( field = convert_sqlalchemy_hybrid_method(attr, resolver, **orm_field.kwargs) elif isinstance(attr, AssociationProxy): field = convert_sqlalchemy_association_proxy( - attr.for_class(model), + model, + attr, obj_type, registry, connection_field_factory, From 70f866b5855f93c52ed038ebe236a1be84fedcd0 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Fri, 13 May 2022 13:33:57 +0200 Subject: [PATCH 5/7] fix pytest due to master merge --- graphene_sqlalchemy/tests/test_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index b4c2674d..8d7dd282 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -466,7 +466,7 @@ class Meta: assert issubclass(CustomReporterType, ObjectType) assert CustomReporterType._meta.model == Reporter - assert len(CustomReporterType._meta.fields) == 17 + assert len(CustomReporterType._meta.fields) == 18 # Test Custom SQLAlchemyObjectType with Custom Options From b49c63ee9dbe3d0deaf6186b9fa62da2e1ec3a4c Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Mon, 2 Jan 2023 16:58:17 +0100 Subject: [PATCH 6/7] fix: throw error when association proxy could not be converted Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 7 +++++++ graphene_sqlalchemy/tests/models.py | 12 ++++++++++++ graphene_sqlalchemy/tests/test_converter.py | 20 ++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 881f1333..b5ad58aa 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -91,6 +91,13 @@ def dynamic_type(): assoc_prop.value_attr, **field_kwargs, ).get_type() + else: + raise TypeError( + "Unsupported association proxy target type: {} for prop {} on type {}. " + "Please disable the conversion of this field using an ORMField.".format( + type(attr), assoc_prop, obj_type + ) + ) # else, not supported return graphene.Dynamic(dynamic_type) diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index d8becce7..5b3f30e9 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -68,6 +68,18 @@ def __repr__(self): return "{} {}".format(self.first_name, self.last_name) +class ProxiedReporter(Base): + __tablename__ = "reporters_error" + id = Column(Integer(), primary_key=True) + first_name = Column(String(30), doc="First name") + last_name = Column(String(30), doc="Last name") + reporter_id = Column(Integer(), ForeignKey("reporters.id")) + reporter = relationship("Reporter", uselist=False) + + # This is a hybrid property, we don't support proxies on hybrids yet + composite_prop = association_proxy("reporter", "composite_prop") + + class Reporter(Base): __tablename__ = "reporters" diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 2fcf5d75..0160c00d 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -29,6 +29,7 @@ Article, CompositeFullName, Pet, + ProxiedReporter, Reporter, ShoppingCart, ShoppingCartItem, @@ -523,6 +524,25 @@ class Meta: assert dynamic_field.get_type().type.of_type == ArticleType +def test_should_throw_error_association_proxy_unsupported_target(): + class ProxiedReporterType(SQLAlchemyObjectType): + class Meta: + model = ProxiedReporter + + field = convert_sqlalchemy_association_proxy( + ProxiedReporter, + ProxiedReporter.composite_prop, + ProxiedReporterType, + get_global_registry(), + default_connection_field_factory, + True, + mock_resolver, + ) + + with pytest.raises(TypeError): + field.get_type() + + def test_should_postgresql_uuid_convert(): assert get_field(postgresql.UUID()).type == graphene.UUID From 64ee90798a059086897f425db0d888dad7c3e6ef Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Tue, 14 Mar 2023 10:40:17 +0100 Subject: [PATCH 7/7] fix: adjust association proxy to new relationship handling --- graphene_sqlalchemy/tests/test_converter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 3fbc8e6b..1512d2e4 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -666,8 +666,12 @@ class Meta: True, mock_resolver, ) + dynamic_field_type = dynamic_field.get_type().type assert isinstance(dynamic_field, graphene.Dynamic) - assert dynamic_field.get_type().type.of_type == ArticleType + assert isinstance(dynamic_field_type, graphene.NonNull) + assert isinstance(dynamic_field_type.of_type, graphene.List) + assert isinstance(dynamic_field_type.of_type.of_type, graphene.NonNull) + assert dynamic_field_type.of_type.of_type.of_type == ArticleType def test_should_throw_error_association_proxy_unsupported_target():