Merge pull request #265 from sjhewitt/next-sql-composite

next: handle sqlalchemy composite columns
This commit is contained in:
Syrus Akbary 2016-08-31 14:48:19 -07:00 committed by GitHub
commit 3edb4baf9b
4 changed files with 100 additions and 0 deletions

View File

@ -37,6 +37,33 @@ def convert_sqlalchemy_relationship(relationship, registry):
return Dynamic(dynamic_type) 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): def convert_sqlalchemy_column(column, registry=None):
return convert_sqlalchemy_type(getattr(column, 'type', None), column, registry) return convert_sqlalchemy_type(getattr(column, 'type', None), column, registry)

View File

@ -2,6 +2,7 @@ class Registry(object):
def __init__(self): def __init__(self):
self._registry = {} self._registry = {}
self._registry_models = {} self._registry_models = {}
self._registry_composites = {}
def register(self, cls): def register(self, cls):
from .types import SQLAlchemyObjectType from .types import SQLAlchemyObjectType
@ -16,6 +17,12 @@ class Registry(object):
def get_type_for_model(self, model): def get_type_for_model(self, model):
return self._registry.get(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 registry = None

View File

@ -1,5 +1,6 @@
from py.test import raises from py.test import raises
from sqlalchemy import Column, Table, types from sqlalchemy import Column, Table, types
from sqlalchemy.orm import composite
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy_utils import ChoiceType, ScalarListType from sqlalchemy_utils import ChoiceType, ScalarListType
from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import postgresql
@ -8,6 +9,7 @@ import graphene
from graphene.relay import Node from graphene.relay import Node
from graphene.types.json import JSONString from graphene.types.json import JSONString
from ..converter import (convert_sqlalchemy_column, from ..converter import (convert_sqlalchemy_column,
convert_sqlalchemy_composite,
convert_sqlalchemy_relationship) convert_sqlalchemy_relationship)
from ..fields import SQLAlchemyConnectionField from ..fields import SQLAlchemyConnectionField
from ..types import SQLAlchemyObjectType from ..types import SQLAlchemyObjectType
@ -25,6 +27,19 @@ def assert_column_conversion(sqlalchemy_type, graphene_field, **kwargs):
return field 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(): def test_should_unknown_sqlalchemy_field_raise_exception():
with raises(Exception) as excinfo: with raises(Exception) as excinfo:
convert_sqlalchemy_column(None) convert_sqlalchemy_column(None)
@ -210,3 +225,42 @@ def test_should_postgresql_jsonb_convert():
def test_should_postgresql_hstore_convert(): def test_should_postgresql_hstore_convert():
assert_column_conversion(postgresql.HSTORE(), JSONString) 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)

View File

@ -6,6 +6,7 @@ from sqlalchemy.orm.exc import NoResultFound
from graphene import ObjectType, Field from graphene import ObjectType, Field
from graphene.relay import is_node from graphene.relay import is_node
from .converter import (convert_sqlalchemy_column, from .converter import (convert_sqlalchemy_column,
convert_sqlalchemy_composite,
convert_sqlalchemy_relationship) convert_sqlalchemy_relationship)
from .utils import is_mapped from .utils import is_mapped
@ -35,6 +36,17 @@ def construct_fields(options):
converted_column = convert_sqlalchemy_column(column, options.registry) converted_column = convert_sqlalchemy_column(column, options.registry)
fields[name] = converted_column 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 # Get all the columns for the relationships on the model
for relationship in inspected_model.relationships: for relationship in inspected_model.relationships:
is_not_in_only = only_fields and relationship.key not in only_fields is_not_in_only = only_fields and relationship.key not in only_fields