Skip to content

Commit d8c39a5

Browse files
committed
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

Diff for: graphene_sqlalchemy/converter.py

+6-6
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

Diff for: graphene_sqlalchemy/registry.py

+37-1
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

Diff for: graphene_sqlalchemy/tests/models.py

+5-4
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)

Diff for: graphene_sqlalchemy/tests/test_converter.py

+13-8
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():

Diff for: graphene_sqlalchemy/tests/test_query.py

+75-58
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+

Diff for: graphene_sqlalchemy/tests/test_schema.py

+1
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']

0 commit comments

Comments
 (0)