From b9c4e62e975f3dd199f573f600b83b2835e02f71 Mon Sep 17 00:00:00 2001 From: Simon Hewitt Date: Wed, 31 Aug 2016 14:39:16 -0700 Subject: [PATCH] handle sqlalchemy composite columns --- .../graphene_sqlalchemy/converter.py | 27 ++++++++++ .../graphene_sqlalchemy/registry.py | 7 +++ .../tests/test_converter.py | 54 +++++++++++++++++++ .../graphene_sqlalchemy/types.py | 12 +++++ 4 files changed, 100 insertions(+) diff --git a/graphene-sqlalchemy/graphene_sqlalchemy/converter.py b/graphene-sqlalchemy/graphene_sqlalchemy/converter.py index 8147e1c3..e05ecba9 100644 --- a/graphene-sqlalchemy/graphene_sqlalchemy/converter.py +++ b/graphene-sqlalchemy/graphene_sqlalchemy/converter.py @@ -34,6 +34,33 @@ def convert_sqlalchemy_relationship(relationship, registry): return Dynamic(dynamic_type) +def convert_sqlalchemy_composite(composite, registry): + converter = registry.get_converter_for_composite(composite.composite_class) + if not converter: + 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)) + return converter(composite, registry) + + +def _register_composite_class(cls, registry=None): + if registry is None: + from .registry import get_global_registry + registry = get_global_registry() + + def inner(fn): + registry.register_composite_converter(cls, fn) + return inner + +convert_sqlalchemy_composite.register = _register_composite_class + + def convert_sqlalchemy_column(column, registry=None): return convert_sqlalchemy_type(getattr(column, 'type', None), column, registry) diff --git a/graphene-sqlalchemy/graphene_sqlalchemy/registry.py b/graphene-sqlalchemy/graphene_sqlalchemy/registry.py index adb56e82..43980665 100644 --- a/graphene-sqlalchemy/graphene_sqlalchemy/registry.py +++ b/graphene-sqlalchemy/graphene_sqlalchemy/registry.py @@ -2,6 +2,7 @@ class Registry(object): def __init__(self): self._registry = {} self._registry_models = {} + self._registry_composites = {} def register(self, cls): from .types import SQLAlchemyObjectType @@ -16,6 +17,12 @@ class Registry(object): def get_type_for_model(self, model): return self._registry.get(model) + def register_composite_converter(self, composite, converter): + self._registry_composites[composite] = converter + + def get_converter_for_composite(self, composite): + return self._registry_composites.get(composite) + registry = None diff --git a/graphene-sqlalchemy/graphene_sqlalchemy/tests/test_converter.py b/graphene-sqlalchemy/graphene_sqlalchemy/tests/test_converter.py index 40aded27..d173e43c 100644 --- a/graphene-sqlalchemy/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene-sqlalchemy/graphene_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 @@ -8,6 +9,7 @@ import graphene from graphene.relay import Node from graphene.types.json import JSONString from ..converter import (convert_sqlalchemy_column, + convert_sqlalchemy_composite, convert_sqlalchemy_relationship) from ..fields import SQLAlchemyConnectionField from ..types import SQLAlchemyObjectType @@ -25,6 +27,19 @@ def assert_column_conversion(sqlalchemy_type, graphene_field, **kwargs): return field +def assert_composite_conversion(composite_class, composite_columns, graphene_field, + registry, **kwargs): + composite_column = composite(composite_class, *composite_columns, + doc='Custom Help Text', **kwargs) + graphene_type = convert_sqlalchemy_composite(composite_column, registry) + assert isinstance(graphene_type, graphene_field) + field = graphene_type.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) @@ -206,3 +221,42 @@ 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 + + registry = Registry() + + @convert_sqlalchemy_composite.register(CompositeClass, registry) + def convert_composite_class(composite, registry): + return graphene.String(description=composite.doc) + + assert_composite_conversion(CompositeClass, + (Column(types.Unicode(50)), + Column(types.Unicode(50))), + graphene.String, + registry) + + +def test_should_unknown_sqlalchemy_composite_raise_exception(): + registry = Registry() + + 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, + registry) + + assert 'Don\'t know how to convert the composite field' in str(excinfo.value) diff --git a/graphene-sqlalchemy/graphene_sqlalchemy/types.py b/graphene-sqlalchemy/graphene_sqlalchemy/types.py index c4e4c8cd..9237f840 100644 --- a/graphene-sqlalchemy/graphene_sqlalchemy/types.py +++ b/graphene-sqlalchemy/graphene_sqlalchemy/types.py @@ -6,6 +6,7 @@ from sqlalchemy.orm.exc import NoResultFound from graphene import ObjectType, Field from graphene.relay import is_node from .converter import (convert_sqlalchemy_column, + convert_sqlalchemy_composite, convert_sqlalchemy_relationship) from .utils import is_mapped @@ -35,6 +36,17 @@ def construct_fields(options): converted_column = convert_sqlalchemy_column(column, options.registry) fields[name] = converted_column + for name, composite in inspected_model.composites.items(): + is_not_in_only = only_fields and name not in only_fields + is_already_created = name in options.fields + is_excluded = name in exclude_fields or is_already_created + if is_not_in_only or is_excluded: + # We skip this field if we specify only_fields and is not + # in there. Or when we excldue this field in exclude_fields + continue + converted_composite = convert_sqlalchemy_composite(composite, options.registry) + fields[name] = converted_composite + # Get all the columns for the relationships on the model for relationship in inspected_model.relationships: is_not_in_only = only_fields and relationship.key not in only_fields