From 9e185b01c284dcd10d583658a4c966a85e45ff4c Mon Sep 17 00:00:00 2001 From: Simon Hewitt Date: Fri, 19 Aug 2016 12:13:13 -0700 Subject: [PATCH] add support for sqlalchemy composite columns --- graphene/contrib/sqlalchemy/converter.py | 24 ++++++++++ .../sqlalchemy/tests/test_converter.py | 48 +++++++++++++++++++ graphene/contrib/sqlalchemy/types.py | 23 ++++++--- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/graphene/contrib/sqlalchemy/converter.py b/graphene/contrib/sqlalchemy/converter.py index 242606d4..b81a2c3b 100644 --- a/graphene/contrib/sqlalchemy/converter.py +++ b/graphene/contrib/sqlalchemy/converter.py @@ -31,6 +31,30 @@ def convert_sqlalchemy_column(column): return convert_sqlalchemy_type(getattr(column, 'type', None), column) +def convert_sqlalchemy_composite(composite): + try: + return convert_sqlalchemy_composite.registry[composite.composite_class](composite) + except KeyError: + try: + raise Exception( + "Don't know how to convert the composite field %s (%s)" % + (composite, composite.composite_class)) + except AttributeError: + # handle fields that are not attached to a class yet (don't have a parent) + raise Exception( + "Don't know how to convert the composite field %r (%s)" % + (composite, composite.composite_class)) + + +def _register_composite_class(cls): + def inner(fn): + convert_sqlalchemy_composite.registry[cls] = fn + return inner + +convert_sqlalchemy_composite.registry = {} +convert_sqlalchemy_composite.register = _register_composite_class + + @singledispatch def convert_sqlalchemy_type(type, column): raise Exception( diff --git a/graphene/contrib/sqlalchemy/tests/test_converter.py b/graphene/contrib/sqlalchemy/tests/test_converter.py index 521911ee..1da2273a 100644 --- a/graphene/contrib/sqlalchemy/tests/test_converter.py +++ b/graphene/contrib/sqlalchemy/tests/test_converter.py @@ -1,5 +1,6 @@ from py.test import raises from sqlalchemy import Column, Table, types +from sqlalchemy.orm import composite from sqlalchemy.ext.declarative import declarative_base from sqlalchemy_utils.types.choice import ChoiceType from sqlalchemy.dialects import postgresql @@ -7,6 +8,7 @@ from sqlalchemy.dialects import postgresql import graphene from graphene.core.types.custom_scalars import JSONString from graphene.contrib.sqlalchemy.converter import (convert_sqlalchemy_column, + convert_sqlalchemy_composite, convert_sqlalchemy_relationship) from graphene.contrib.sqlalchemy.fields import (ConnectionOrListField, SQLAlchemyModelField) @@ -23,6 +25,19 @@ def assert_column_conversion(sqlalchemy_type, graphene_field, **kwargs): return field +def assert_composite_conversion(composite_class, composite_columns, graphene_field, + **kwargs): + composite_column = composite(composite_class, *composite_columns, + doc='Custom Help Text', **kwargs) + graphene_type = convert_sqlalchemy_composite(composite_column) + assert isinstance(graphene_type, graphene_field) + field = graphene_type.as_field() + # SQLAlchemy currently does not persist the doc onto the column, even though + # the documentation says it does.... + # assert field.description == 'Custom Help Text' + return field + + def test_should_unknown_sqlalchemy_field_raise_exception(): with raises(Exception) as excinfo: convert_sqlalchemy_column(None) @@ -148,3 +163,36 @@ def test_should_postgresql_jsonb_convert(): def test_should_postgresql_hstore_convert(): assert_column_conversion(postgresql.HSTORE(), JSONString) + + +def test_should_composite_convert(): + + class CompositeClass(object): + def __init__(self, col1, col2): + self.col1 = col1 + self.col2 = col2 + + @convert_sqlalchemy_composite.register(CompositeClass) + def convert_composite_class(composite): + return graphene.String(description=composite.doc) + + assert_composite_conversion(CompositeClass, + (Column(types.Unicode(50)), + Column(types.Unicode(50))), + graphene.String) + + +def test_should_unknown_sqlalchemy_composite_raise_exception(): + with raises(Exception) as excinfo: + + class CompositeClass(object): + def __init__(self, col1, col2): + self.col1 = col1 + self.col2 = col2 + + assert_composite_conversion(CompositeClass, + (Column(types.Unicode(50)), + Column(types.Unicode(50))), + graphene.String) + + assert 'Don\'t know how to convert the composite field' in str(excinfo.value) diff --git a/graphene/contrib/sqlalchemy/types.py b/graphene/contrib/sqlalchemy/types.py index 7f9ab98b..994b3953 100644 --- a/graphene/contrib/sqlalchemy/types.py +++ b/graphene/contrib/sqlalchemy/types.py @@ -5,9 +5,10 @@ from sqlalchemy.inspection import inspect as sqlalchemyinspect from sqlalchemy.orm.exc import NoResultFound from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta -from ...relay.types import Node, NodeMeta from ...relay.connection import Connection +from ...relay.types import Node, NodeMeta from .converter import (convert_sqlalchemy_column, + convert_sqlalchemy_composite, convert_sqlalchemy_relationship) from .options import SQLAlchemyOptions from .utils import get_query, is_mapped @@ -34,17 +35,25 @@ class SQLAlchemyObjectTypeMeta(ObjectTypeMeta): converted_relationship = convert_sqlalchemy_relationship(relationship) cls.add_to_class(relationship.key, converted_relationship) - for name, column in inspected_model.columns.items(): - is_not_in_only = only_fields and name not in only_fields - is_already_created = name in already_created_fields - is_excluded = name in exclude_fields or is_already_created - if is_not_in_only or is_excluded: + def filter_included(l): + for name, value in l.items(): + is_not_in_only = only_fields and name not in only_fields + is_already_created = name in already_created_fields + is_excluded = name in exclude_fields or is_already_created # We skip this field if we specify only_fields and is not # in there. Or when we excldue this field in exclude_fields - continue + if is_not_in_only or is_excluded: + continue + yield name, value + + for name, column in filter_included(inspected_model.columns): converted_column = convert_sqlalchemy_column(column) cls.add_to_class(name, converted_column) + for name, composite in filter_included(inspected_model.composites): + converted_composite = convert_sqlalchemy_composite(composite) + cls.add_to_class(name, converted_composite) + def construct(cls, *args, **kwargs): cls = super(SQLAlchemyObjectTypeMeta, cls).construct(*args, **kwargs) if not cls._meta.abstract: