Skip to content

Commit e9e51a0

Browse files
committed
Merge pull request #100 from graphql-python/features/enum
Add Enum type
2 parents 9f57b45 + b9109dc commit e9e51a0

File tree

18 files changed

+1039
-18
lines changed

18 files changed

+1039
-18
lines changed

.coveragerc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[run]
2+
omit = graphene/utils/enum.py,graphene/contrib/django/debug/sql/tracking.py,*/tests/*

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ before_install:
2323
install:
2424
- |
2525
if [ "$TEST_TYPE" = build ]; then
26-
pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter
26+
pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter sqlalchemy_utils
2727
pip install --download-cache $HOME/.cache/pip/ -e .[django]
2828
pip install --download-cache $HOME/.cache/pip/ -e .[sqlalchemy]
2929
pip install django==$DJANGO_VERSION

docs/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ga = "UA-12613282-7"
1414
"/docs/objecttypes/",
1515
"/docs/mutations/",
1616
"/docs/basic-types/",
17+
"/docs/enums/",
1718
"/docs/relay/",
1819
]
1920

docs/pages/docs/enums.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
title: Enums
3+
description: Walkthrough Enums
4+
---
5+
6+
# Enums
7+
8+
A `Enum` is a special `GraphQL` type that represents a set of symbolic names (members) bound to unique, constant values.
9+
10+
## Enum definition
11+
12+
You can create an `Enum` using classes:
13+
14+
```python
15+
import graphene
16+
17+
class Episode(graphene.Enum):
18+
NEWHOPE = 4
19+
EMPIRE = 5
20+
JEDI = 6
21+
```
22+
23+
But also using instances of Enum:
24+
25+
```python
26+
Episode = graphene.Enum('Episode', [('NEWHOPE', 4), ('EMPIRE', 5), ('JEDI', 6)])
27+
```
28+
29+
## Notes
30+
31+
Internally, `graphene.Enum` uses [`enum.Enum`](https://docs.python.org/3/library/enum.html) Python implementation if available, or a backport if not.
32+
33+
So you can use it in the same way as you would do with Python `enum.Enum`.

examples/starwars/schema.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
from graphql.core.type import GraphQLEnumValue
2-
31
import graphene
42
from graphene import resolve_only_args
53

64
from .data import get_character, get_droid, get_hero, get_human
75

8-
Episode = graphene.Enum('Episode', dict(
9-
NEWHOPE=GraphQLEnumValue(4),
10-
EMPIRE=GraphQLEnumValue(5),
11-
JEDI=GraphQLEnumValue(6)
12-
))
6+
7+
class Episode(graphene.Enum):
8+
NEWHOPE = 4
9+
EMPIRE = 5
10+
JEDI = 6
1311

1412

1513
class Character(graphene.Interface):

graphene/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
from graphql.core.type import (
2-
GraphQLEnumType as Enum
3-
)
4-
51
from graphene import signals
62

73
from .core import (
@@ -11,6 +7,7 @@
117
Interface,
128
Mutation,
139
Scalar,
10+
Enum,
1411
InstanceType,
1512
LazyType,
1613
Argument,
@@ -58,6 +55,7 @@
5855
'Interface',
5956
'Mutation',
6057
'Scalar',
58+
'Enum',
6159
'Field',
6260
'InputField',
6361
'StringField',

graphene/contrib/django/converter.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
from django.db import models
22

33
from ...core.types.scalars import ID, Boolean, Float, Int, String
4+
from ...core.classtypes.enum import Enum
45
from .compat import RelatedObject, UUIDField
56
from .utils import get_related_model, import_single_dispatch
67

78
singledispatch = import_single_dispatch()
89

910

11+
def convert_django_field_with_choices(field):
12+
choices = getattr(field, 'choices', None)
13+
if choices:
14+
meta = field.model._meta
15+
name = '{}_{}_{}'.format(meta.app_label, meta.object_name, field.name)
16+
return Enum(name.upper(), choices, description=field.help_text)
17+
return convert_django_field(field)
18+
19+
1020
@singledispatch
1121
def convert_django_field(field):
1222
raise Exception(

graphene/contrib/django/tests/test_converter.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from py.test import raises
33

44
import graphene
5-
from graphene.contrib.django.converter import convert_django_field
5+
from graphene.contrib.django.converter import (
6+
convert_django_field, convert_django_field_with_choices)
67
from graphene.contrib.django.fields import (ConnectionOrListField,
78
DjangoModelField)
89

@@ -86,6 +87,26 @@ def test_should_nullboolean_convert_boolean():
8687
assert field.required is False
8788

8889

90+
def test_field_with_choices_convert_enum():
91+
field = models.CharField(help_text='Language', choices=(
92+
('es', 'Spanish'),
93+
('en', 'English')
94+
))
95+
96+
class TranslatedModel(models.Model):
97+
language = field
98+
99+
class Meta:
100+
app_label = 'test'
101+
102+
graphene_type = convert_django_field_with_choices(field)
103+
assert issubclass(graphene_type, graphene.Enum)
104+
assert graphene_type._meta.type_name == 'TEST_TRANSLATEDMODEL_LANGUAGE'
105+
assert graphene_type._meta.description == 'Language'
106+
assert graphene_type.__enum__.__members__['es'].value == 'Spanish'
107+
assert graphene_type.__enum__.__members__['en'].value == 'English'
108+
109+
89110
def test_should_float_convert_float():
90111
assert_conversion(models.FloatField, graphene.Float)
91112

graphene/contrib/django/types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta
77
from ...relay.types import Connection, Node, NodeMeta
8-
from .converter import convert_django_field
8+
from .converter import convert_django_field_with_choices
99
from .options import DjangoOptions
1010
from .utils import get_reverse_fields
1111

@@ -29,7 +29,7 @@ def construct_fields(cls):
2929
# We skip this field if we specify only_fields and is not
3030
# in there. Or when we exclude this field in exclude_fields
3131
continue
32-
converted_field = convert_django_field(field)
32+
converted_field = convert_django_field_with_choices(field)
3333
cls.add_to_class(field.name, converted_field)
3434

3535
def construct(cls, *args, **kwargs):

graphene/contrib/sqlalchemy/converter.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
from sqlalchemy import types
44
from sqlalchemy.orm import interfaces
5+
try:
6+
from sqlalchemy_utils.types.choice import ChoiceType
7+
except ImportError:
8+
class ChoiceType(object):
9+
pass
510

11+
from ...core.classtypes.enum import Enum
612
from ...core.types.scalars import ID, Boolean, Float, Int, String
713
from .fields import ConnectionOrListField, SQLAlchemyModelField
814

@@ -59,3 +65,9 @@ def convert_column_to_boolean(type, column):
5965
@convert_sqlalchemy_type.register(types.Numeric)
6066
def convert_column_to_float(type, column):
6167
return Float(description=column.doc)
68+
69+
70+
@convert_sqlalchemy_type.register(ChoiceType)
71+
def convert_column_to_enum(type, column):
72+
name = '{}_{}'.format(column.table.name, column.name).upper()
73+
return Enum(name, type.choices, description=column.doc)

graphene/contrib/sqlalchemy/tests/test_converter.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
convert_sqlalchemy_relationship)
66
from graphene.contrib.sqlalchemy.fields import (ConnectionOrListField,
77
SQLAlchemyModelField)
8-
from sqlalchemy import Column, types
8+
from sqlalchemy import Table, Column, types
9+
from sqlalchemy.ext.declarative import declarative_base
10+
from sqlalchemy_utils.types.choice import ChoiceType
911

1012
from .models import Article, Pet, Reporter
1113

@@ -85,6 +87,23 @@ def test_should_numeric_convert_float():
8587
assert_column_conversion(types.Numeric(), graphene.Float)
8688

8789

90+
def test_should_choice_convert_enum():
91+
TYPES = [
92+
(u'es', u'Spanish'),
93+
(u'en', u'English')
94+
]
95+
column = Column(ChoiceType(TYPES), doc='Language', name='language')
96+
Base = declarative_base()
97+
98+
Table('translatedmodel', Base.metadata, column)
99+
graphene_type = convert_sqlalchemy_column(column)
100+
assert issubclass(graphene_type, graphene.Enum)
101+
assert graphene_type._meta.type_name == 'TRANSLATEDMODEL_LANGUAGE'
102+
assert graphene_type._meta.description == 'Language'
103+
assert graphene_type.__enum__.__members__['es'].value == 'Spanish'
104+
assert graphene_type.__enum__.__members__['en'].value == 'English'
105+
106+
88107
def test_should_manytomany_convert_connectionorlist():
89108
graphene_type = convert_sqlalchemy_relationship(Reporter.pets.property)
90109
assert isinstance(graphene_type, ConnectionOrListField)

graphene/core/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
InputObjectType,
88
Interface,
99
Mutation,
10-
Scalar
10+
Scalar,
11+
Enum
1112
)
1213

1314
from .types import (
@@ -42,5 +43,6 @@
4243
'Interface',
4344
'Mutation',
4445
'Scalar',
46+
'Enum',
4547
'Field',
4648
'InputField']

graphene/core/classtypes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .objecttype import ObjectType
55
from .options import Options
66
from .scalar import Scalar
7+
from .enum import Enum
78
from .uniontype import UnionType
89

910
__all__ = [
@@ -13,4 +14,5 @@
1314
'ObjectType',
1415
'Options',
1516
'Scalar',
17+
'Enum',
1618
'UnionType']

graphene/core/classtypes/enum.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import six
2+
from graphql.core.type import GraphQLEnumType, GraphQLEnumValue
3+
4+
from .base import ClassTypeMeta, ClassType
5+
from ...utils.enum import Enum as PyEnum
6+
7+
8+
class EnumMeta(ClassTypeMeta):
9+
10+
def construct(cls, bases, attrs):
11+
__enum__ = attrs.get('__enum__', None)
12+
if not cls._meta.abstract and not __enum__:
13+
__enum__ = PyEnum(cls._meta.type_name, attrs)
14+
setattr(cls, '__enum__', __enum__)
15+
if __enum__:
16+
for k, v in __enum__.__members__.items():
17+
attrs[k] = v.value
18+
return super(EnumMeta, cls).construct(bases, attrs)
19+
20+
def __call__(cls, name, names=None, description=None):
21+
attrs = {
22+
'__enum__': PyEnum(name, names)
23+
}
24+
if description:
25+
attrs['__doc__'] = description
26+
return type(name, (Enum,), attrs)
27+
28+
29+
class Enum(six.with_metaclass(EnumMeta, ClassType)):
30+
31+
class Meta:
32+
abstract = True
33+
34+
@classmethod
35+
def internal_type(cls, schema):
36+
if cls._meta.abstract:
37+
raise Exception("Abstract Enum don't have a specific type.")
38+
39+
values = {k: GraphQLEnumValue(v.value) for k, v in cls.__enum__.__members__.items()}
40+
# GraphQLEnumValue
41+
return GraphQLEnumType(
42+
cls._meta.type_name,
43+
values=values,
44+
description=cls._meta.description,
45+
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from graphql.core.type import GraphQLEnumType
2+
3+
from graphene.core.schema import Schema
4+
5+
from ..enum import Enum
6+
7+
8+
def test_enum():
9+
class RGB(Enum):
10+
'''RGB enum description'''
11+
RED = 0
12+
GREEN = 1
13+
BLUE = 2
14+
15+
schema = Schema()
16+
17+
object_type = schema.T(RGB)
18+
assert isinstance(object_type, GraphQLEnumType)
19+
assert RGB._meta.type_name == 'RGB'
20+
assert RGB._meta.description == 'RGB enum description'
21+
assert RGB.RED == 0
22+
assert RGB.GREEN == 1
23+
assert RGB.BLUE == 2
24+
25+
26+
def test_enum_values():
27+
RGB = Enum('RGB', dict(RED=0, GREEN=1, BLUE=2), description='RGB enum description')
28+
29+
schema = Schema()
30+
31+
object_type = schema.T(RGB)
32+
assert isinstance(object_type, GraphQLEnumType)
33+
assert RGB._meta.type_name == 'RGB'
34+
assert RGB._meta.description == 'RGB enum description'
35+
assert RGB.RED == 0
36+
assert RGB.GREEN == 1
37+
assert RGB.BLUE == 2

0 commit comments

Comments
 (0)