Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d8c39a5

Browse files
committedDec 1, 2017
Improve auto creation of Graphene Enums.
The created Graphene Enums are now registered and reused, because their names must be unique in a GraphQL schema. Also the naming conventions for Enum type names (CamelCase) and options (UPPERCASE) are applied when creating them.
1 parent 4827ce2 commit d8c39a5

File tree

9 files changed

+150
-78
lines changed

9 files changed

+150
-78
lines changed
 

‎graphene_sqlalchemy/converter.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,12 @@ def convert_column_to_float(type, column, registry=None):
131131

132132

133133
@convert_sqlalchemy_type.register(types.Enum)
134-
def convert_enum_to_enum(type, column, registry=None):
135-
try:
136-
items = type.enum_class.__members__.items()
137-
except AttributeError:
138-
items = zip(type.enums, type.enums)
139-
return Field(Enum(type.name, items),
134+
def convert_column_to_enum(type, column, registry=None):
135+
if registry is None:
136+
from .registry import get_global_registry
137+
registry = get_global_registry()
138+
type = registry.get_type_for_enum(type)
139+
return Field(type,
140140
description=get_column_doc(column),
141141
required=not(is_column_nullable(column)))
142142

‎graphene_sqlalchemy/registry.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
from graphene import Enum
2+
3+
from sqlalchemy.types import Enum as SQLAlchemyEnumType
4+
5+
from .utils import to_type_name
6+
7+
18
class Registry(object):
29

310
def __init__(self):
411
self._registry = {}
5-
self._registry_models = {}
612
self._registry_composites = {}
13+
self._registry_enums = {}
714

815
def register(self, cls):
916
from .types import SQLAlchemyObjectType
@@ -27,6 +34,35 @@ def register_composite_converter(self, composite, converter):
2734
def get_converter_for_composite(self, composite):
2835
return self._registry_composites.get(composite)
2936

37+
def get_type_for_enum(self, sql_type):
38+
assert isinstance(sql_type, SQLAlchemyEnumType), (
39+
'Only sqlalchemy.Enum objects can be registered as enum, '
40+
'received "{}"'
41+
).format(sql_type)
42+
if sql_type.enum_class:
43+
name = sql_type.enum_class.__name__
44+
items = [(key.upper(), value.value)
45+
for key, value in sql_type.enum_class.__members__.items()]
46+
else:
47+
name = to_type_name(sql_type.name)
48+
if not name:
49+
name = 'Enum{}'.format(len(self._registry_enums) + 1)
50+
items = [(key.upper(), key) for key in sql_type.enums]
51+
if name:
52+
gql_type = self._registry_enums.get(name)
53+
if gql_type:
54+
if dict(items) != {
55+
key: value.value for key, value
56+
in gql_type._meta.enum.__members__.items()}:
57+
raise TypeError(
58+
'Different enums with the same name {}'.format(name))
59+
else:
60+
name = 'Enum{}'.format(len(self._registry_enums) + 1)
61+
gql_type = None
62+
if not gql_type:
63+
gql_type = Enum(name, items)
64+
self._registry_enums[name] = gql_type
65+
return gql_type
3066

3167
registry = None
3268

‎graphene_sqlalchemy/tests/models.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from __future__ import absolute_import
22

3-
import enum
4-
53
from sqlalchemy import Column, Date, Enum, ForeignKey, Integer, String, Table
64
from sqlalchemy.ext.declarative import declarative_base
75
from sqlalchemy.orm import mapper, relationship
@@ -13,6 +11,9 @@
1311
Column('reporter_id', Integer, ForeignKey('reporters.id')))
1412

1513

14+
PetKind = Enum('cat', 'dog', name='pet_kind_enum')
15+
16+
1617
class Editor(Base):
1718
__tablename__ = 'editors'
1819
editor_id = Column(Integer(), primary_key=True)
@@ -23,8 +24,7 @@ class Pet(Base):
2324
__tablename__ = 'pets'
2425
id = Column(Integer(), primary_key=True)
2526
name = Column(String(30))
26-
pet_kind = Column(Enum('cat', 'dog', name='pet_kind'), nullable=False)
27-
reporter_id = Column(Integer(), ForeignKey('reporters.id'))
27+
pet_kind = Column(PetKind, nullable=False)
2828

2929

3030
class Reporter(Base):
@@ -33,6 +33,7 @@ class Reporter(Base):
3333
first_name = Column(String(30))
3434
last_name = Column(String(30))
3535
email = Column(String())
36+
favorite_pet_kind = Column(PetKind)
3637
pets = relationship('Pet', secondary=association_table, backref='reporters')
3738
articles = relationship('Article', backref='reporter')
3839
favorite_article = relationship("Article", uselist=False)

‎graphene_sqlalchemy/tests/test_converter.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,21 @@ def test_should_unicodetext_convert_string():
8181

8282
def test_should_enum_convert_enum():
8383
field = assert_column_conversion(
84-
types.Enum(enum.Enum('one', 'two')), graphene.Field)
84+
types.Enum(enum.Enum('TwoNumbersPyEnum', 'one two')), graphene.Field)
8585
field_type = field.type()
86+
assert field_type.__class__.__name__ == 'TwoNumbersPyEnum'
8687
assert isinstance(field_type, graphene.Enum)
87-
assert hasattr(field_type, 'two')
88+
assert hasattr(field_type, 'ONE')
89+
assert not hasattr(field_type, 'one')
90+
assert hasattr(field_type, 'TWO')
8891
field = assert_column_conversion(
89-
types.Enum('one', 'two', name='two_numbers'), graphene.Field)
92+
types.Enum('one', 'two', name='two_numbers_db_enum'), graphene.Field)
9093
field_type = field.type()
91-
assert field_type.__class__.__name__ == 'two_numbers'
94+
assert field_type.__class__.__name__ == 'TwoNumbersDbEnum'
9295
assert isinstance(field_type, graphene.Enum)
93-
assert hasattr(field_type, 'two')
96+
assert hasattr(field_type, 'ONE')
97+
assert not hasattr(field_type, 'one')
98+
assert hasattr(field_type, 'TWO')
9499

95100

96101
def test_should_small_integer_convert_int():
@@ -262,11 +267,11 @@ def test_should_postgresql_uuid_convert():
262267

263268
def test_should_postgresql_enum_convert():
264269
field = assert_column_conversion(postgresql.ENUM(
265-
enum.Enum('one', 'two'), name='two_numbers'), graphene.Field)
270+
enum.Enum('TwoNumbers', 'one two')), graphene.Field)
266271
field_type = field.type()
267-
assert field_type.__class__.__name__ == 'two_numbers'
272+
assert field_type.__class__.__name__ == 'TwoNumbers'
268273
assert isinstance(field_type, graphene.Enum)
269-
assert hasattr(field_type, 'two')
274+
assert hasattr(field_type, 'TWO')
270275

271276

272277
def test_should_postgresql_array_convert():

‎graphene_sqlalchemy/tests/test_query.py

Lines changed: 75 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@
1313
db = create_engine('sqlite:///test_sqlalchemy.sqlite3')
1414

1515

16+
def normalize(value):
17+
"""Convert nested ordered dicts ot normal dicts for better comparison."""
18+
if isinstance(value, dict):
19+
return {k: normalize(v) for k, v in value.items()}
20+
elif isinstance(value, list):
21+
return [normalize(v) for v in value]
22+
else:
23+
return value
24+
25+
1626
@pytest.yield_fixture(scope='function')
1727
def session():
1828
reset_global_registry()
@@ -33,23 +43,34 @@ def session():
3343

3444

3545
def setup_fixtures(session):
36-
pet = Pet(name='Lassie', pet_kind='dog')
37-
session.add(pet)
38-
reporter = Reporter(first_name='ABA', last_name='X')
46+
reporter = Reporter(
47+
first_name='John', last_name='Doe', favorite_pet_kind='cat')
3948
session.add(reporter)
40-
reporter2 = Reporter(first_name='ABO', last_name='Y')
41-
session.add(reporter2)
49+
pet = Pet(name='Garfield', pet_kind='cat')
50+
session.add(pet)
51+
pet.reporters.append(reporter)
4252
article = Article(headline='Hi!')
4353
article.reporter = reporter
4454
session.add(article)
45-
editor = Editor(name="John")
55+
reporter = Reporter(
56+
first_name='Jane', last_name='Roe', favorite_pet_kind='dog')
57+
session.add(reporter)
58+
pet = Pet(name='Lassie', pet_kind='dog')
59+
pet.reporters.append(reporter)
60+
session.add(pet)
61+
editor = Editor(name="Jack")
4662
session.add(editor)
4763
session.commit()
4864

4965

5066
def test_should_query_well(session):
5167
setup_fixtures(session)
5268

69+
class PetType(SQLAlchemyObjectType):
70+
71+
class Meta:
72+
model = Pet
73+
5374
class ReporterType(SQLAlchemyObjectType):
5475

5576
class Meta:
@@ -58,75 +79,68 @@ class Meta:
5879
class Query(graphene.ObjectType):
5980
reporter = graphene.Field(ReporterType)
6081
reporters = graphene.List(ReporterType)
82+
pets = graphene.List(PetType, kind=graphene.Argument(
83+
PetType._meta.fields['pet_kind'].type))
6184

62-
def resolve_reporter(self, *args, **kwargs):
85+
def resolve_reporter(self, _info):
6386
return session.query(Reporter).first()
6487

65-
def resolve_reporters(self, *args, **kwargs):
88+
def resolve_reporters(self, _info):
6689
return session.query(Reporter)
6790

91+
def resolve_pets(self, _info, kind):
92+
query = session.query(Pet)
93+
if kind:
94+
query = query.filter_by(pet_kind=kind)
95+
return query
96+
6897
query = '''
6998
query ReporterQuery {
7099
reporter {
71100
firstName,
72101
lastName,
73-
email
102+
email,
103+
favoritePetKind,
104+
pets {
105+
name
106+
petKind
107+
}
74108
}
75109
reporters {
76110
firstName
77111
}
112+
pets(kind: DOG) {
113+
name
114+
petKind
115+
}
78116
}
79117
'''
80118
expected = {
81119
'reporter': {
82-
'firstName': 'ABA',
83-
'lastName': 'X',
84-
'email': None
120+
'firstName': 'John',
121+
'lastName': 'Doe',
122+
'email': None,
123+
'favoritePetKind': 'CAT',
124+
'pets': [{
125+
'name': 'Garfield',
126+
'petKind': 'CAT'
127+
}]
85128
},
86129
'reporters': [{
87-
'firstName': 'ABA',
130+
'firstName': 'John',
88131
}, {
89-
'firstName': 'ABO',
90-
}]
91-
}
92-
schema = graphene.Schema(query=Query)
93-
result = schema.execute(query)
94-
assert not result.errors
95-
assert result.data == expected
96-
97-
98-
def test_should_query_enums(session):
99-
setup_fixtures(session)
100-
101-
class PetType(SQLAlchemyObjectType):
102-
103-
class Meta:
104-
model = Pet
105-
106-
class Query(graphene.ObjectType):
107-
pet = graphene.Field(PetType)
108-
109-
def resolve_pet(self, *args, **kwargs):
110-
return session.query(Pet).first()
111-
112-
query = '''
113-
query PetQuery {
114-
pet {
115-
name,
116-
petKind
117-
}
118-
}
119-
'''
120-
expected = {
121-
'pet': {
132+
'firstName': 'Jane',
133+
}],
134+
'pets': [{
122135
'name': 'Lassie',
123-
'petKind': 'dog'
124-
}
136+
'petKind': 'DOG'
137+
}]
125138
}
126139
schema = graphene.Schema(query=Query)
127140
result = schema.execute(query)
128141
assert not result.errors
129-
assert result.data == expected, result.data
142+
result = normalize(result.data)
143+
assert result == expected
130144

131145

132146
def test_should_node(session):
@@ -158,10 +172,10 @@ class Query(graphene.ObjectType):
158172
article = graphene.Field(ArticleNode)
159173
all_articles = SQLAlchemyConnectionField(ArticleNode)
160174

161-
def resolve_reporter(self, *args, **kwargs):
175+
def resolve_reporter(self, _info):
162176
return session.query(Reporter).first()
163177

164-
def resolve_article(self, *args, **kwargs):
178+
def resolve_article(self, _info):
165179
return session.query(Article).first()
166180

167181
query = '''
@@ -200,8 +214,8 @@ def resolve_article(self, *args, **kwargs):
200214
expected = {
201215
'reporter': {
202216
'id': 'UmVwb3J0ZXJOb2RlOjE=',
203-
'firstName': 'ABA',
204-
'lastName': 'X',
217+
'firstName': 'John',
218+
'lastName': 'Doe',
205219
'email': None,
206220
'articles': {
207221
'edges': [{
@@ -226,7 +240,8 @@ def resolve_article(self, *args, **kwargs):
226240
schema = graphene.Schema(query=Query)
227241
result = schema.execute(query, context_value={'session': session})
228242
assert not result.errors
229-
assert result.data == expected
243+
result = normalize(result.data)
244+
assert result == expected
230245

231246

232247
def test_should_custom_identifier(session):
@@ -264,12 +279,12 @@ class Query(graphene.ObjectType):
264279
'edges': [{
265280
'node': {
266281
'id': 'RWRpdG9yTm9kZTox',
267-
'name': 'John'
282+
'name': 'Jack'
268283
}
269284
}]
270285
},
271286
'node': {
272-
'name': 'John'
287+
'name': 'Jack'
273288
}
274289
}
275290

@@ -355,7 +370,7 @@ class Mutation(graphene.ObjectType):
355370
'headline': 'My Article',
356371
'reporter': {
357372
'id': 'UmVwb3J0ZXJOb2RlOjE=',
358-
'firstName': 'ABA'
373+
'firstName': 'John'
359374
}
360375
}
361376
},
@@ -364,4 +379,6 @@ class Mutation(graphene.ObjectType):
364379
schema = graphene.Schema(query=Query, mutation=Mutation)
365380
result = schema.execute(query, context_value={'session': session})
366381
assert not result.errors
367-
assert result.data == expected
382+
result = normalize(result.data)
383+
assert result == expected
384+

‎graphene_sqlalchemy/tests/test_schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class Meta:
3434
'first_name',
3535
'last_name',
3636
'email',
37+
'favorite_pet_kind',
3738
'pets',
3839
'articles',
3940
'favorite_article']

‎graphene_sqlalchemy/tests/test_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def test_objecttype_registered():
5050
'first_name',
5151
'last_name',
5252
'email',
53+
'favorite_pet_kind',
5354
'pets',
5455
'articles',
5556
'favorite_article']
@@ -113,6 +114,7 @@ def test_custom_objecttype_registered():
113114
'first_name',
114115
'last_name',
115116
'email',
117+
'favorite_pet_kind',
116118
'pets',
117119
'articles',
118120
'favorite_article']

‎graphene_sqlalchemy/tests/test_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from graphene import ObjectType, Schema, String
22

3-
from ..utils import get_session
3+
from ..utils import get_session, to_type_name
44

55

66
def test_get_session():
@@ -22,3 +22,8 @@ def resolve_x(self, info):
2222
result = schema.execute(query, context_value={'session': session})
2323
assert not result.errors
2424
assert result.data['x'] == session
25+
26+
27+
def test_get_enum_name():
28+
assert to_type_name('make_camel_case') == 'MakeCamelCase'
29+
assert to_type_name('AlreadyCamelCase') == 'AlreadyCamelCase'

‎graphene_sqlalchemy/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,8 @@ def is_mapped_instance(cls):
3434
return False
3535
else:
3636
return True
37+
38+
39+
def to_type_name(name):
40+
return ''.join(part[:1].upper() + part[1:] for part in name.split('_'))
41+

0 commit comments

Comments
 (0)
Please sign in to comment.