Merge branch 'master' into feature/date-improvements

# Conflicts:
#	graphene/types/datetime.py
This commit is contained in:
Syrus Akbary 2018-03-29 22:10:18 -07:00
commit 85e354c139
33 changed files with 547 additions and 92 deletions

1
.gitignore vendored
View File

@ -42,6 +42,7 @@ htmlcov/
.coverage .coverage
.coverage.* .coverage.*
.cache .cache
.pytest_cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*,cover *,cover

View File

@ -21,7 +21,7 @@ developer has to write to use them.
> The type metaclasses are now deleted as they are no longer necessary. If your code was depending > The type metaclasses are now deleted as they are no longer necessary. If your code was depending
> on this strategy for creating custom attrs, see an [example on how to do it in 2.0](https://github.com/graphql-python/graphene/blob/2.0/graphene/tests/issues/test_425.py). > on this strategy for creating custom attrs, see an [example on how to do it in 2.0](https://github.com/graphql-python/graphene/blob/v2.0.0/graphene/tests/issues/test_425.py).
## Deprecations ## Deprecations

View File

@ -3,7 +3,7 @@ Middleware
You can use ``middleware`` to affect the evaluation of fields in your schema. You can use ``middleware`` to affect the evaluation of fields in your schema.
A middleware is any object that responds to ``resolve(*args, next_middleware)``. A middleware is any object or function that responds to ``resolve(next_middleware, *args)``.
Inside that method, it should either: Inside that method, it should either:
@ -18,10 +18,8 @@ Middlewares ``resolve`` is invoked with several arguments:
- ``next`` represents the execution chain. Call ``next`` to continue evalution. - ``next`` represents the execution chain. Call ``next`` to continue evalution.
- ``root`` is the root value object passed throughout the query. - ``root`` is the root value object passed throughout the query.
- ``args`` is the hash of arguments passed to the field.
- ``context`` is the context object passed throughout the query.
- ``info`` is the resolver info. - ``info`` is the resolver info.
- ``args`` is the dict of arguments passed to the field.
Example Example
------- -------
@ -42,3 +40,32 @@ And then execute it with:
.. code:: python .. code:: python
result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()]) result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()])
Functional example
------------------
Middleware can also be defined as a function. Here we define a middleware that
logs the time it takes to resolve each field
.. code:: python
from time import time as timer
def timing_middleware(next, root, info, **args):
start = timer()
return_value = next(root, info, **args)
duration = timer() - start
logger.debug("{parent_type}.{field_name}: {duration} ms".format(
parent_type=root._meta.name if root and hasattr(root, '_meta') else '',
field_name=info.field_name,
duration=round(duration * 1000, 2)
))
return return_value
And then execute it with:
.. code:: python
result = schema.execute('THE QUERY', middleware=[timing_middleware])

View File

@ -21,9 +21,9 @@ Useful links
- `Relay Cursor Connection Specification`_ - `Relay Cursor Connection Specification`_
- `Relay input Object Mutation`_ - `Relay input Object Mutation`_
.. _Relay: https://facebook.github.io/relay/docs/graphql-relay-specification.html .. _Relay: https://facebook.github.io/relay/docs/en/graphql-server-specification.html
.. _Relay specification: https://facebook.github.io/relay/graphql/objectidentification.htm#sec-Node-root-field .. _Relay specification: https://facebook.github.io/relay/graphql/objectidentification.htm#sec-Node-root-field
.. _Getting started with Relay: https://facebook.github.io/relay/docs/graphql-relay-specification.html .. _Getting started with Relay: https://facebook.github.io/relay/docs/en/quick-start-guide.html
.. _Relay Global Identification Specification: https://facebook.github.io/relay/graphql/objectidentification.htm .. _Relay Global Identification Specification: https://facebook.github.io/relay/graphql/objectidentification.htm
.. _Relay Cursor Connection Specification: https://facebook.github.io/relay/graphql/connections.htm .. _Relay Cursor Connection Specification: https://facebook.github.io/relay/graphql/connections.htm
.. _Relay input Object Mutation: https://facebook.github.io/relay/graphql/mutations.htm .. _Relay input Object Mutation: https://facebook.github.io/relay/graphql/mutations.htm

View File

@ -55,12 +55,12 @@ Example of a custom node:
return '{}:{}'.format(type, id) return '{}:{}'.format(type, id)
@staticmethod @staticmethod
def get_node_from_global_id(info global_id, only_type=None): def get_node_from_global_id(info, global_id, only_type=None):
type, id = global_id.split(':') type, id = global_id.split(':')
if only_node: if only_type:
# We assure that the node type that we want to retrieve # We assure that the node type that we want to retrieve
# is the same that was indicated in the field type # is the same that was indicated in the field type
assert type == only_node._meta.name, 'Received not compatible node.' assert type == only_type._meta.name, 'Received not compatible node.'
if type == 'User': if type == 'User':
return get_user(id) return get_user(id)
@ -75,10 +75,10 @@ Accessing node types
-------------------- --------------------
If we want to retrieve node instances from a ``global_id`` (scalar that identifies an instance by it's type name and id), If we want to retrieve node instances from a ``global_id`` (scalar that identifies an instance by it's type name and id),
we can simply do ``Node.get_node_from_global_id(global_id, context, info)``. we can simply do ``Node.get_node_from_global_id(info, global_id)``.
In the case we want to restrict the instance retrieval to a specific type, we can do: In the case we want to restrict the instance retrieval to a specific type, we can do:
``Node.get_node_from_global_id(global_id, context, info, only_type=Ship)``. This will raise an error ``Node.get_node_from_global_id(info, global_id, only_type=Ship)``. This will raise an error
if the ``global_id`` doesn't correspond to a Ship type. if the ``global_id`` doesn't correspond to a Ship type.

View File

@ -1,4 +1,4 @@
# Required library # Required library
Sphinx==1.5.3 Sphinx==1.5.3
# Docs template # Docs template
https://github.com/graphql-python/graphene-python.org/archive/docs.zip http://graphene-python.org/sphinx_graphene_theme.zip

View File

@ -54,6 +54,13 @@ the ``Enum.from_enum`` function.
graphene.Enum.from_enum(AlreadyExistingPyEnum) graphene.Enum.from_enum(AlreadyExistingPyEnum)
``Enum.from_enum`` supports a ``description`` and ``deprecation_reason`` lambdas as input so
you can add description etc. to your enum without changing the original:
.. code:: python
graphene.Enum.from_enum(AlreadyExistingPyEnum, description=lambda value: return 'foo' if value == AlreadyExistingPyEnum.Foo else 'bar')
Notes Notes
----- -----
@ -65,7 +72,7 @@ member getters.
In the Python ``Enum`` implementation you can access a member by initing the Enum. In the Python ``Enum`` implementation you can access a member by initing the Enum.
.. code:: python .. code:: python
from enum import Enum from enum import Enum
class Color(Enum): class Color(Enum):
RED = 1 RED = 1
@ -78,7 +85,7 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu
However, in Graphene ``Enum`` you need to call get to have the same effect: However, in Graphene ``Enum`` you need to call get to have the same effect:
.. code:: python .. code:: python
from graphene import Enum from graphene import Enum
class Color(Enum): class Color(Enum):
RED = 1 RED = 1

View File

@ -48,3 +48,24 @@ Lists work in a similar way: We can use a type modifier to mark a type as a
``List``, which indicates that this field will return a list of that type. ``List``, which indicates that this field will return a list of that type.
It works the same for arguments, where the validation step will expect a list It works the same for arguments, where the validation step will expect a list
for that value. for that value.
NonNull Lists
-------------
By default items in a list will be considered nullable. To define a list without
any nullable items the type needs to be marked as ``NonNull``. For example:
.. code:: python
import graphene
class Character(graphene.ObjectType):
appears_in = graphene.List(graphene.NonNull(graphene.String))
The above results in the type definition:
.. code::
type Character {
appearsIn: [String!]
}

View File

@ -1,19 +1,83 @@
Scalars Scalars
======= =======
All Scalar types accept the following arguments. All are optional:
``name``: *string*
Override the name of the Field.
``description``: *string*
A description of the type to show in the GraphiQL browser.
``required``: *boolean*
If ``True``, the server will enforce a value for this field. See `NonNull <./list-and-nonnull.html#nonnull>`_. Default is ``False``.
``deprecation_reason``: *string*
Provide a deprecation reason for the Field.
``default_value``: *any*
Provide a default value for the Field.
Base scalars
------------
Graphene defines the following base Scalar Types: Graphene defines the following base Scalar Types:
- ``graphene.String`` ``graphene.String``
- ``graphene.Int``
- ``graphene.Float`` Represents textual data, represented as UTF-8
- ``graphene.Boolean`` character sequences. The String type is most often used by GraphQL to
- ``graphene.ID`` represent free-form human-readable text.
``graphene.Int``
Represents non-fractional signed whole numeric
values. Int can represent values between `-(2^53 - 1)` and `2^53 - 1` since
represented in JSON as double-precision floating point numbers specified
by `IEEE 754 <http://en.wikipedia.org/wiki/IEEE_floating_point>`_.
``graphene.Float``
Represents signed double-precision fractional
values as specified by
`IEEE 754 <http://en.wikipedia.org/wiki/IEEE_floating_point>`_.
``graphene.Boolean``
Represents `true` or `false`.
``graphene.ID``
Represents a unique identifier, often used to
refetch an object or as key for a cache. The ID type appears in a JSON
response as a String; however, it is not intended to be human-readable.
When expected as an input type, any string (such as `"4"`) or integer
(such as `4`) input value will be accepted as an ID.
Graphene also provides custom scalars for Dates, Times, and JSON: Graphene also provides custom scalars for Dates, Times, and JSON:
- ``graphene.types.datetime.DateTime`` ``graphene.types.datetime.Date``
- ``graphene.types.datetime.Time``
- ``graphene.types.json.JSONString`` Represents a Date value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
``graphene.types.datetime.DateTime``
Represents a DateTime value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
``graphene.types.datetime.Time``
Represents a Time value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
``graphene.types.json.JSONString``
Represents a JSON string.
Custom scalars Custom scalars

View File

@ -12,8 +12,8 @@ The basics:
Quick example Quick example
------------- -------------
This example model defines a ``Character`` interface with a name. ``Human`` This example model defines several ObjectTypes with their own fields.
and ``Droid`` are two implementations of that interface. ``SearchResult`` is the implementation of ``Union`` of this object types.
.. code:: python .. code:: python

View File

@ -34,7 +34,7 @@ from .utils.resolve_only_args import resolve_only_args
from .utils.module_loading import lazy_import from .utils.module_loading import lazy_import
VERSION = (2, 0, 0, 'final', 0) VERSION = (2, 0, 1, 'final', 0)
__version__ = get_version(VERSION) __version__ = get_version(VERSION)

View File

@ -73,7 +73,7 @@ class Connection(ObjectType):
edge = type(edge_name, edge_bases, {}) edge = type(edge_name, edge_bases, {})
cls.Edge = edge cls.Edge = edge
_meta.name = name options['name'] = name
_meta.node = node _meta.node = node
_meta.fields = OrderedDict([ _meta.fields = OrderedDict([
('page_info', Field(PageInfo, name='pageInfo', required=True)), ('page_info', Field(PageInfo, name='pageInfo', required=True)),
@ -99,16 +99,19 @@ class IterableConnectionField(Field):
def type(self): def type(self):
type = super(IterableConnectionField, self).type type = super(IterableConnectionField, self).type
connection_type = type connection_type = type
if is_node(type): if isinstance(type, NonNull):
connection_type = type.of_type
if is_node(connection_type):
raise Exception( raise Exception(
"ConnectionField's now need a explicit ConnectionType for Nodes.\n" "ConnectionField's now need a explicit ConnectionType for Nodes.\n"
"Read more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#node-connections" "Read more: https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#node-connections"
) )
assert issubclass(connection_type, Connection), ( assert issubclass(connection_type, Connection), (
'{} type have to be a subclass of Connection. Received "{}".' '{} type have to be a subclass of Connection. Received "{}".'
).format(self.__class__.__name__, connection_type) ).format(self.__class__.__name__, connection_type)
return connection_type return type
@classmethod @classmethod
def resolve_connection(cls, connection_type, args, resolved): def resolve_connection(cls, connection_type, args, resolved):
@ -133,6 +136,9 @@ class IterableConnectionField(Field):
def connection_resolver(cls, resolver, connection_type, root, info, **args): def connection_resolver(cls, resolver, connection_type, root, info, **args):
resolved = resolver(root, info, **args) resolved = resolver(root, info, **args)
if isinstance(connection_type, NonNull):
connection_type = connection_type.of_type
on_resolve = partial(cls.resolve_connection, connection_type, args) on_resolve = partial(cls.resolve_connection, connection_type, args)
if is_thenable(resolved): if is_thenable(resolved):
return Promise.resolve(resolved).then(on_resolve) return Promise.resolve(resolved).then(on_resolve)

View File

@ -1,6 +1,6 @@
import pytest import pytest
from ...types import Argument, Field, Int, List, NonNull, ObjectType, String from ...types import Argument, Field, Int, List, NonNull, ObjectType, String, Schema
from ..connection import Connection, ConnectionField, PageInfo from ..connection import Connection, ConnectionField, PageInfo
from ..node import Node from ..node import Node
@ -52,6 +52,21 @@ def test_connection_inherit_abstracttype():
assert list(fields.keys()) == ['page_info', 'edges', 'extra'] assert list(fields.keys()) == ['page_info', 'edges', 'extra']
def test_connection_name():
custom_name = "MyObjectCustomNameConnection"
class BaseConnection(object):
extra = String()
class MyObjectConnection(BaseConnection, Connection):
class Meta:
node = MyObject
name = custom_name
assert MyObjectConnection._meta.name == custom_name
def test_edge(): def test_edge():
class MyObjectConnection(Connection): class MyObjectConnection(Connection):
@ -122,9 +137,10 @@ def test_connectionfield_node_deprecated():
field = ConnectionField(MyObject) field = ConnectionField(MyObject)
with pytest.raises(Exception) as exc_info: with pytest.raises(Exception) as exc_info:
field.type field.type
assert "ConnectionField's now need a explicit ConnectionType for Nodes." in str(exc_info.value) assert "ConnectionField's now need a explicit ConnectionType for Nodes." in str(exc_info.value)
def test_connectionfield_custom_args(): def test_connectionfield_custom_args():
class MyObjectConnection(Connection): class MyObjectConnection(Connection):
@ -139,3 +155,23 @@ def test_connectionfield_custom_args():
'last': Argument(Int), 'last': Argument(Int),
'extra': Argument(String), 'extra': Argument(String),
} }
def test_connectionfield_required():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
class Query(ObjectType):
test_connection = ConnectionField(MyObjectConnection, required=True)
def resolve_test_connection(root, info, **args):
return []
schema = Schema(query=Query)
executed = schema.execute(
'{ testConnection { edges { cursor } } }'
)
assert not executed.errors
assert executed.data == {'testConnection': {'edges': []}}

View File

@ -2,8 +2,11 @@
# Adapted for Graphene 2.0 # Adapted for Graphene 2.0
from graphene.types.objecttype import ObjectType, ObjectTypeOptions from graphene.types.objecttype import ObjectType, ObjectTypeOptions
from graphene.types.inputobjecttype import InputObjectType, InputObjectTypeOptions
from graphene.types.enum import Enum, EnumOptions
# ObjectType
class SpecialOptions(ObjectTypeOptions): class SpecialOptions(ObjectTypeOptions):
other_attr = None other_attr = None
@ -40,3 +43,77 @@ def test_special_objecttype_inherit_meta_options():
assert MyType._meta.name == 'MyType' assert MyType._meta.name == 'MyType'
assert MyType._meta.default_resolver is None assert MyType._meta.default_resolver is None
assert MyType._meta.interfaces == () assert MyType._meta.interfaces == ()
# InputObjectType
class SpecialInputObjectTypeOptions(ObjectTypeOptions):
other_attr = None
class SpecialInputObjectType(InputObjectType):
@classmethod
def __init_subclass_with_meta__(cls, other_attr='default', **options):
_meta = SpecialInputObjectTypeOptions(cls)
_meta.other_attr = other_attr
super(SpecialInputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options)
def test_special_inputobjecttype_could_be_subclassed():
class MyInputObjectType(SpecialInputObjectType):
class Meta:
other_attr = 'yeah!'
assert MyInputObjectType._meta.other_attr == 'yeah!'
def test_special_inputobjecttype_could_be_subclassed_default():
class MyInputObjectType(SpecialInputObjectType):
pass
assert MyInputObjectType._meta.other_attr == 'default'
def test_special_inputobjecttype_inherit_meta_options():
class MyInputObjectType(SpecialInputObjectType):
pass
assert MyInputObjectType._meta.name == 'MyInputObjectType'
# Enum
class SpecialEnumOptions(EnumOptions):
other_attr = None
class SpecialEnum(Enum):
@classmethod
def __init_subclass_with_meta__(cls, other_attr='default', **options):
_meta = SpecialEnumOptions(cls)
_meta.other_attr = other_attr
super(SpecialEnum, cls).__init_subclass_with_meta__(_meta=_meta, **options)
def test_special_enum_could_be_subclassed():
class MyEnum(SpecialEnum):
class Meta:
other_attr = 'yeah!'
assert MyEnum._meta.other_attr == 'yeah!'
def test_special_enum_could_be_subclassed_default():
class MyEnum(SpecialEnum):
pass
assert MyEnum._meta.other_attr == 'default'
def test_special_enum_inherit_meta_options():
class MyEnum(SpecialEnum):
pass
assert MyEnum._meta.name == 'MyEnum'

View File

@ -7,6 +7,6 @@ class AbstractType(SubclassWithMeta):
def __init_subclass__(cls, *args, **kwargs): def __init_subclass__(cls, *args, **kwargs):
warn_deprecation( warn_deprecation(
"Abstract type is deprecated, please use normal object inheritance instead.\n" "Abstract type is deprecated, please use normal object inheritance instead.\n"
"See more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#deprecations" "See more: https://github.com/graphql-python/graphene/blob/master/UPGRADE-v2.0.md#deprecations"
) )
super(AbstractType, cls).__init_subclass__(*args, **kwargs) super(AbstractType, cls).__init_subclass__(*args, **kwargs)

View File

@ -21,7 +21,7 @@ class BaseOptions(object):
raise Exception("Can't modify frozen Options {0}".format(self)) raise Exception("Can't modify frozen Options {0}".format(self))
def __repr__(self): def __repr__(self):
return "<{} type={}>".format(self.__class__.__name__, self.class_type.__name__) return "<{} name={}>".format(self.__class__.__name__, repr(self.name))
class BaseType(SubclassWithMeta): class BaseType(SubclassWithMeta):

View File

@ -31,7 +31,10 @@ class Date(Scalar):
@staticmethod @staticmethod
def parse_value(value): def parse_value(value):
return parse_date(value) try:
return parse_date(value)
except ValueError:
return None
class DateTime(Scalar): class DateTime(Scalar):
@ -55,7 +58,10 @@ class DateTime(Scalar):
@staticmethod @staticmethod
def parse_value(value): def parse_value(value):
return parse_datetime(value) try:
return parse_datetime(value)
except ValueError:
return None
class Time(Scalar): class Time(Scalar):
@ -79,4 +85,7 @@ class Time(Scalar):
@classmethod @classmethod
def parse_value(cls, value): def parse_value(cls, value):
return parse_time(value) try:
return parse_time(value)
except ValueError:
return None

View File

@ -27,7 +27,11 @@ class EnumOptions(BaseOptions):
class EnumMeta(SubclassWithMeta_Meta): class EnumMeta(SubclassWithMeta_Meta):
def __new__(cls, name, bases, classdict, **options): def __new__(cls, name, bases, classdict, **options):
enum = PyEnum(cls.__name__, OrderedDict(classdict, __eq__=eq_enum)) enum_members = OrderedDict(classdict, __eq__=eq_enum)
# We remove the Meta attribute from the class to not collide
# with the enum values.
enum_members.pop('Meta', None)
enum = PyEnum(cls.__name__, enum_members)
return SubclassWithMeta_Meta.__new__(cls, name, bases, OrderedDict(classdict, __enum__=enum), **options) return SubclassWithMeta_Meta.__new__(cls, name, bases, OrderedDict(classdict, __enum__=enum), **options)
def get(cls, value): def get(cls, value):
@ -60,8 +64,9 @@ class EnumMeta(SubclassWithMeta_Meta):
class Enum(six.with_metaclass(EnumMeta, UnmountedType, BaseType)): class Enum(six.with_metaclass(EnumMeta, UnmountedType, BaseType)):
@classmethod @classmethod
def __init_subclass_with_meta__(cls, enum=None, **options): def __init_subclass_with_meta__(cls, enum=None, _meta=None, **options):
_meta = EnumOptions(cls) if not _meta:
_meta = EnumOptions(cls)
_meta.enum = enum or cls.__enum__ _meta.enum = enum or cls.__enum__
_meta.deprecation_reason = options.pop('deprecation_reason', None) _meta.deprecation_reason = options.pop('deprecation_reason', None)
for key, value in _meta.enum.__members__.items(): for key, value in _meta.enum.__members__.items():

View File

@ -5,7 +5,6 @@ from .inputfield import InputField
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
from .utils import yank_fields_from_attrs from .utils import yank_fields_from_attrs
# For static type checking with Mypy # For static type checking with Mypy
MYPY = False MYPY = False
if MYPY: if MYPY:
@ -14,7 +13,7 @@ if MYPY:
class InputObjectTypeOptions(BaseOptions): class InputObjectTypeOptions(BaseOptions):
fields = None # type: Dict[str, InputField] fields = None # type: Dict[str, InputField]
create_container = None # type: Callable container = None # type: InputObjectTypeContainer
class InputObjectTypeContainer(dict, BaseType): class InputObjectTypeContainer(dict, BaseType):
@ -23,8 +22,8 @@ class InputObjectTypeContainer(dict, BaseType):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs) dict.__init__(self, *args, **kwargs)
for key, value in self.items(): for key in self._meta.fields.keys():
setattr(self, key, value) setattr(self, key, self.get(key, None))
def __init_subclass__(cls, *args, **kwargs): def __init_subclass__(cls, *args, **kwargs):
pass pass
@ -41,8 +40,9 @@ class InputObjectType(UnmountedType, BaseType):
''' '''
@classmethod @classmethod
def __init_subclass_with_meta__(cls, container=None, **options): def __init_subclass_with_meta__(cls, container=None, _meta=None, **options):
_meta = InputObjectTypeOptions(cls) if not _meta:
_meta = InputObjectTypeOptions(cls)
fields = OrderedDict() fields = OrderedDict()
for base in reversed(cls.__mro__): for base in reversed(cls.__mro__):
@ -54,7 +54,8 @@ class InputObjectType(UnmountedType, BaseType):
if container is None: if container is None:
container = type(cls.__name__, (InputObjectTypeContainer, cls), {}) container = type(cls.__name__, (InputObjectTypeContainer, cls), {})
_meta.container = container _meta.container = container
super(InputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options) super(InputObjectType, cls).__init_subclass_with_meta__(
_meta=_meta, **options)
@classmethod @classmethod
def get_type(cls): def get_type(cls):

View File

@ -50,7 +50,8 @@ class Mutation(ObjectType):
warn_deprecation(( warn_deprecation((
"Please use {name}.Arguments instead of {name}.Input." "Please use {name}.Arguments instead of {name}.Input."
"Input is now only used in ClientMutationID.\n" "Input is now only used in ClientMutationID.\n"
"Read more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#mutation-input" "Read more:"
" https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#mutation-input"
).format(name=cls.__name__)) ).format(name=cls.__name__))
if input_class: if input_class:
@ -72,10 +73,17 @@ class Mutation(ObjectType):
_meta.resolver = resolver _meta.resolver = resolver
_meta.arguments = arguments _meta.arguments = arguments
super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options) super(Mutation, cls).__init_subclass_with_meta__(
_meta=_meta, **options)
@classmethod @classmethod
def Field(cls, *args, **kwargs): def Field(cls, name=None, description=None, deprecation_reason=None, required=False):
return Field( return Field(
cls._meta.output, args=cls._meta.arguments, resolver=cls._meta.resolver cls._meta.output,
args=cls._meta.arguments,
resolver=cls._meta.resolver,
name=name,
description=description,
deprecation_reason=deprecation_reason,
required=required,
) )

View File

@ -1,6 +1,6 @@
import inspect import inspect
from graphql import GraphQLSchema, graphql, is_type from graphql import GraphQLSchema, graphql, is_type, GraphQLObjectType
from graphql.type.directives import (GraphQLDirective, GraphQLIncludeDirective, from graphql.type.directives import (GraphQLDirective, GraphQLIncludeDirective,
GraphQLSkipDirective) GraphQLSkipDirective)
from graphql.type.introspection import IntrospectionSchema from graphql.type.introspection import IntrospectionSchema
@ -12,6 +12,17 @@ from .objecttype import ObjectType
from .typemap import TypeMap, is_graphene_type from .typemap import TypeMap, is_graphene_type
def assert_valid_root_type(_type):
if _type is None:
return
is_graphene_objecttype = inspect.isclass(
_type) and issubclass(_type, ObjectType)
is_graphql_objecttype = isinstance(_type, GraphQLObjectType)
assert is_graphene_objecttype or is_graphql_objecttype, (
"Type {} is not a valid ObjectType."
).format(_type)
class Schema(GraphQLSchema): class Schema(GraphQLSchema):
''' '''
Schema Definition Schema Definition
@ -20,21 +31,23 @@ class Schema(GraphQLSchema):
query and mutation (optional). query and mutation (optional).
''' '''
def __init__(self, query=None, mutation=None, subscription=None, def __init__(self,
directives=None, types=None, auto_camelcase=True): query=None,
assert inspect.isclass(query) and issubclass(query, ObjectType), ( mutation=None,
'Schema query must be Object Type but got: {}.' subscription=None,
).format(query) directives=None,
types=None,
auto_camelcase=True):
assert_valid_root_type(query)
assert_valid_root_type(mutation)
assert_valid_root_type(subscription)
self._query = query self._query = query
self._mutation = mutation self._mutation = mutation
self._subscription = subscription self._subscription = subscription
self.types = types self.types = types
self.auto_camelcase = auto_camelcase self.auto_camelcase = auto_camelcase
if directives is None: if directives is None:
directives = [ directives = [GraphQLIncludeDirective, GraphQLSkipDirective]
GraphQLIncludeDirective,
GraphQLSkipDirective
]
assert all(isinstance(d, GraphQLDirective) for d in directives), \ assert all(isinstance(d, GraphQLDirective) for d in directives), \
'Schema directives must be List[GraphQLDirective] if provided but got: {}.'.format( 'Schema directives must be List[GraphQLDirective] if provided but got: {}.'.format(
@ -61,7 +74,8 @@ class Schema(GraphQLSchema):
''' '''
_type = super(Schema, self).get_type(type_name) _type = super(Schema, self).get_type(type_name)
if _type is None: if _type is None:
raise AttributeError('Type "{}" not found in the Schema'.format(type_name)) raise AttributeError(
'Type "{}" not found in the Schema'.format(type_name))
if isinstance(_type, GrapheneGraphQLType): if isinstance(_type, GrapheneGraphQLType):
return _type.graphene_type return _type.graphene_type
return _type return _type
@ -73,7 +87,8 @@ class Schema(GraphQLSchema):
return _type return _type
if is_graphene_type(_type): if is_graphene_type(_type):
graphql_type = self.get_type(_type._meta.name) graphql_type = self.get_type(_type._meta.name)
assert graphql_type, "Type {} not found in this schema.".format(_type._meta.name) assert graphql_type, "Type {} not found in this schema.".format(
_type._meta.name)
assert graphql_type.graphene_type == _type assert graphql_type.graphene_type == _type
return graphql_type return graphql_type
raise Exception("{} is not a valid GraphQL type.".format(_type)) raise Exception("{} is not a valid GraphQL type.".format(_type))
@ -102,4 +117,8 @@ class Schema(GraphQLSchema):
] ]
if self.types: if self.types:
initial_types += self.types initial_types += self.types
self._type_map = TypeMap(initial_types, auto_camelcase=self.auto_camelcase, schema=self) self._type_map = TypeMap(
initial_types,
auto_camelcase=self.auto_camelcase,
schema=self
)

View File

@ -2,6 +2,8 @@ import datetime
import pytz import pytz
from graphql import GraphQLError
from ..datetime import DateTime, Date, Time from ..datetime import DateTime, Date, Time
from ..objecttype import ObjectType from ..objecttype import ObjectType
from ..schema import Schema from ..schema import Schema
@ -34,7 +36,7 @@ def test_datetime_query():
assert result.data == {'datetime': isoformat} assert result.data == {'datetime': isoformat}
def test_datetime_query(): def test_date_query():
now = datetime.datetime.now().replace(tzinfo=pytz.utc).date() now = datetime.datetime.now().replace(tzinfo=pytz.utc).date()
isoformat = now.isoformat() isoformat = now.isoformat()
@ -53,6 +55,32 @@ def test_time_query():
assert not result.errors assert not result.errors
assert result.data == {'time': isoformat} assert result.data == {'time': isoformat}
def test_bad_datetime_query():
not_a_date = "Some string that's not a date"
result = schema.execute('''{ datetime(in: "%s") }''' % not_a_date)
assert len(result.errors) == 1
assert isinstance(result.errors[0], GraphQLError)
assert result.data == None
def test_bad_date_query():
not_a_date = "Some string that's not a date"
result = schema.execute('''{ date(in: "%s") }''' % not_a_date)
assert len(result.errors) == 1
assert isinstance(result.errors[0], GraphQLError)
assert result.data == None
def test_bad_time_query():
not_a_date = "Some string that's not a date"
result = schema.execute('''{ time(at: "%s") }''' % not_a_date)
assert len(result.errors) == 1
assert isinstance(result.errors[0], GraphQLError)
assert result.data == None
def test_datetime_query_variable(): def test_datetime_query_variable():
now = datetime.datetime.now().replace(tzinfo=pytz.utc) now = datetime.datetime.now().replace(tzinfo=pytz.utc)

View File

@ -80,7 +80,8 @@ def test_enum_from_builtin_enum_accepts_lambda_description():
return 'meh' if value == Episode.NEWHOPE else None return 'meh' if value == Episode.NEWHOPE else None
PyEpisode = PyEnum('PyEpisode', 'NEWHOPE,EMPIRE,JEDI') PyEpisode = PyEnum('PyEpisode', 'NEWHOPE,EMPIRE,JEDI')
Episode = Enum.from_enum(PyEpisode, description=custom_description, deprecation_reason=custom_deprecation_reason) Episode = Enum.from_enum(PyEpisode, description=custom_description,
deprecation_reason=custom_deprecation_reason)
class Query(ObjectType): class Query(ObjectType):
foo = Episode() foo = Episode()
@ -214,3 +215,19 @@ def test_enum_to_enum_comparison_should_differ():
assert RGB1.RED != RGB2.RED assert RGB1.RED != RGB2.RED
assert RGB1.GREEN != RGB2.GREEN assert RGB1.GREEN != RGB2.GREEN
assert RGB1.BLUE != RGB2.BLUE assert RGB1.BLUE != RGB2.BLUE
def test_enum_skip_meta_from_members():
class RGB1(Enum):
class Meta:
name = 'RGB'
RED = 1
GREEN = 2
BLUE = 3
assert dict(RGB1._meta.enum.__members__) == {
'RED': RGB1.RED,
'GREEN': RGB1.GREEN,
'BLUE': RGB1.BLUE,
}

View File

@ -5,6 +5,8 @@ from ..inputfield import InputField
from ..inputobjecttype import InputObjectType from ..inputobjecttype import InputObjectType
from ..objecttype import ObjectType from ..objecttype import ObjectType
from ..unmountedtype import UnmountedType from ..unmountedtype import UnmountedType
from ..scalars import String, Boolean
from ..schema import Schema
class MyType(object): class MyType(object):
@ -51,7 +53,8 @@ def test_ordered_fields_in_inputobjecttype():
field = MyScalar() field = MyScalar()
asa = InputField(MyType) asa = InputField(MyType)
assert list(MyInputObjectType._meta.fields.keys()) == ['b', 'a', 'field', 'asa'] assert list(MyInputObjectType._meta.fields.keys()) == [
'b', 'a', 'field', 'asa']
def test_generate_inputobjecttype_unmountedtype(): def test_generate_inputobjecttype_unmountedtype():
@ -86,7 +89,8 @@ def test_generate_inputobjecttype_inherit_abstracttype():
field2 = MyScalar(MyType) field2 = MyScalar(MyType)
assert list(MyInputObjectType._meta.fields.keys()) == ['field1', 'field2'] assert list(MyInputObjectType._meta.fields.keys()) == ['field1', 'field2']
assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [InputField, InputField] assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [
InputField, InputField]
def test_generate_inputobjecttype_inherit_abstracttype_reversed(): def test_generate_inputobjecttype_inherit_abstracttype_reversed():
@ -97,4 +101,34 @@ def test_generate_inputobjecttype_inherit_abstracttype_reversed():
field2 = MyScalar(MyType) field2 = MyScalar(MyType)
assert list(MyInputObjectType._meta.fields.keys()) == ['field1', 'field2'] assert list(MyInputObjectType._meta.fields.keys()) == ['field1', 'field2']
assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [InputField, InputField] assert [type(x) for x in MyInputObjectType._meta.fields.values()] == [
InputField, InputField]
def test_inputobjecttype_of_input():
class Child(InputObjectType):
first_name = String()
last_name = String()
@property
def full_name(self):
return "{} {}".format(self.first_name, self.last_name)
class Parent(InputObjectType):
child = InputField(Child)
class Query(ObjectType):
is_child = Boolean(parent=Parent())
def resolve_is_child(self, info, parent):
return isinstance(parent.child, Child) and parent.child.full_name == "Peter Griffin"
schema = Schema(query=Query)
result = schema.execute('''query basequery {
isChild(parent: {child: {firstName: "Peter", lastName: "Griffin"}})
}
''')
assert not result.errors
assert result.data == {
'isChild': True
}

View File

@ -6,6 +6,7 @@ from ..mutation import Mutation
from ..objecttype import ObjectType from ..objecttype import ObjectType
from ..scalars import String from ..scalars import String
from ..schema import Schema from ..schema import Schema
from ..structures import NonNull
def test_generate_mutation_no_args(): def test_generate_mutation_no_args():
@ -133,3 +134,27 @@ def test_mutation_no_fields_output():
'name': None, 'name': None,
} }
} }
def test_mutation_allow_to_have_custom_args():
class CreateUser(Mutation):
class Arguments:
name = String()
name = String()
def mutate(self, info, name):
return CreateUser(name=name)
class MyMutation(ObjectType):
create_user = CreateUser.Field(
description='Create a user',
deprecation_reason='Is deprecated',
required=True
)
field = MyMutation._meta.fields['create_user']
assert field.description == 'Create a user'
assert field.deprecation_reason == 'Is deprecated'
assert field.type == NonNull(CreateUser)

View File

@ -44,6 +44,8 @@ def test_generate_objecttype():
assert MyObjectType._meta.description == "Documentation" assert MyObjectType._meta.description == "Documentation"
assert MyObjectType._meta.interfaces == tuple() assert MyObjectType._meta.interfaces == tuple()
assert MyObjectType._meta.fields == {} assert MyObjectType._meta.fields == {}
assert repr(
MyObjectType) == "<MyObjectType meta=<ObjectTypeOptions name='MyObjectType'>>"
def test_generate_objecttype_with_meta(): def test_generate_objecttype_with_meta():
@ -65,7 +67,6 @@ def test_generate_lazy_objecttype():
class InnerObjectType(ObjectType): class InnerObjectType(ObjectType):
field = Field(MyType) field = Field(MyType)
assert MyObjectType._meta.name == "MyObjectType" assert MyObjectType._meta.name == "MyObjectType"
example_field = MyObjectType._meta.fields['example'] example_field = MyObjectType._meta.fields['example']
@ -115,7 +116,8 @@ def test_generate_objecttype_inherit_abstracttype():
assert MyObjectType._meta.interfaces == () assert MyObjectType._meta.interfaces == ()
assert MyObjectType._meta.name == "MyObjectType" assert MyObjectType._meta.name == "MyObjectType"
assert list(MyObjectType._meta.fields.keys()) == ['field1', 'field2'] assert list(MyObjectType._meta.fields.keys()) == ['field1', 'field2']
assert list(map(type, MyObjectType._meta.fields.values())) == [Field, Field] assert list(map(type, MyObjectType._meta.fields.values())) == [
Field, Field]
def test_generate_objecttype_inherit_abstracttype_reversed(): def test_generate_objecttype_inherit_abstracttype_reversed():
@ -129,7 +131,8 @@ def test_generate_objecttype_inherit_abstracttype_reversed():
assert MyObjectType._meta.interfaces == () assert MyObjectType._meta.interfaces == ()
assert MyObjectType._meta.name == "MyObjectType" assert MyObjectType._meta.name == "MyObjectType"
assert list(MyObjectType._meta.fields.keys()) == ['field1', 'field2'] assert list(MyObjectType._meta.fields.keys()) == ['field1', 'field2']
assert list(map(type, MyObjectType._meta.fields.values())) == [Field, Field] assert list(map(type, MyObjectType._meta.fields.values())) == [
Field, Field]
def test_generate_objecttype_unmountedtype(): def test_generate_objecttype_unmountedtype():
@ -145,7 +148,8 @@ def test_parent_container_get_fields():
def test_parent_container_interface_get_fields(): def test_parent_container_interface_get_fields():
assert list(ContainerWithInterface._meta.fields.keys()) == ['ifield', 'field1', 'field2'] assert list(ContainerWithInterface._meta.fields.keys()) == [
'ifield', 'field1', 'field2']
def test_objecttype_as_container_only_args(): def test_objecttype_as_container_only_args():
@ -182,7 +186,8 @@ def test_objecttype_as_container_invalid_kwargs():
with pytest.raises(TypeError) as excinfo: with pytest.raises(TypeError) as excinfo:
Container(unexisting_field="3") Container(unexisting_field="3")
assert "'unexisting_field' is an invalid keyword argument for Container" == str(excinfo.value) assert "'unexisting_field' is an invalid keyword argument for Container" == str(
excinfo.value)
def test_objecttype_container_benchmark(benchmark): def test_objecttype_container_benchmark(benchmark):
@ -238,7 +243,6 @@ def test_objecttype_no_fields_output():
def resolve_user(self, info): def resolve_user(self, info):
return User() return User()
schema = Schema(query=Query) schema = Schema(query=Query)
result = schema.execute(''' query basequery { result = schema.execute(''' query basequery {
user { user {
@ -252,3 +256,12 @@ def test_objecttype_no_fields_output():
'name': None, 'name': None,
} }
} }
def test_abstract_objecttype_can_str():
class MyObjectType(ObjectType):
class Meta:
abstract = True
field = MyScalar()
assert str(MyObjectType) == "MyObjectType"

View File

@ -1,9 +1,11 @@
import pytest
from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, from graphql.type import (GraphQLArgument, GraphQLEnumType, GraphQLEnumValue,
GraphQLField, GraphQLInputObjectField, GraphQLField, GraphQLInputObjectField,
GraphQLInputObjectType, GraphQLInterfaceType, GraphQLInputObjectType, GraphQLInterfaceType,
GraphQLObjectType, GraphQLString) GraphQLObjectType, GraphQLString)
from ..structures import List, NonNull
from ..dynamic import Dynamic from ..dynamic import Dynamic
from ..enum import Enum from ..enum import Enum
from ..field import Field from ..field import Field
@ -11,8 +13,8 @@ from ..inputfield import InputField
from ..inputobjecttype import InputObjectType from ..inputobjecttype import InputObjectType
from ..interface import Interface from ..interface import Interface
from ..objecttype import ObjectType from ..objecttype import ObjectType
from ..scalars import String from ..scalars import String, Int
from ..typemap import TypeMap from ..typemap import TypeMap, resolve_type
def test_enum(): def test_enum():
@ -38,7 +40,8 @@ def test_enum():
assert graphql_enum.description == 'Description' assert graphql_enum.description == 'Description'
values = graphql_enum.values values = graphql_enum.values
assert values == [ assert values == [
GraphQLEnumValue(name='foo', value=1, description='Description foo=1', deprecation_reason='Is deprecated'), GraphQLEnumValue(name='foo', value=1, description='Description foo=1',
deprecation_reason='Is deprecated'),
GraphQLEnumValue(name='bar', value=2, description='Description bar=2'), GraphQLEnumValue(name='bar', value=2, description='Description bar=2'),
] ]
@ -46,7 +49,8 @@ def test_enum():
def test_objecttype(): def test_objecttype():
class MyObjectType(ObjectType): class MyObjectType(ObjectType):
'''Description''' '''Description'''
foo = String(bar=String(description='Argument description', default_value='x'), description='Field description') foo = String(bar=String(description='Argument description',
default_value='x'), description='Field description')
bar = String(name='gizmo') bar = String(name='gizmo')
def resolve_foo(self, bar): def resolve_foo(self, bar):
@ -91,8 +95,10 @@ def test_dynamic_objecttype():
def test_interface(): def test_interface():
class MyInterface(Interface): class MyInterface(Interface):
'''Description''' '''Description'''
foo = String(bar=String(description='Argument description', default_value='x'), description='Field description') foo = String(bar=String(description='Argument description',
bar = String(name='gizmo', first_arg=String(), other_arg=String(name='oth_arg')) default_value='x'), description='Field description')
bar = String(name='gizmo', first_arg=String(),
other_arg=String(name='oth_arg'))
own = Field(lambda: MyInterface) own = Field(lambda: MyInterface)
def resolve_foo(self, args, info): def resolve_foo(self, args, info):
@ -119,10 +125,18 @@ def test_interface():
def test_inputobject(): def test_inputobject():
class OtherObjectType(InputObjectType):
thingy = NonNull(Int)
class MyInnerObjectType(InputObjectType):
some_field = String()
some_other_field = List(OtherObjectType)
class MyInputObjectType(InputObjectType): class MyInputObjectType(InputObjectType):
'''Description''' '''Description'''
foo_bar = String(description='Field description') foo_bar = String(description='Field description')
bar = String(name='gizmo') bar = String(name='gizmo')
baz = NonNull(MyInnerObjectType)
own = InputField(lambda: MyInputObjectType) own = InputField(lambda: MyInputObjectType)
def resolve_foo_bar(self, args, info): def resolve_foo_bar(self, args, info):
@ -135,15 +149,28 @@ def test_inputobject():
assert graphql_type.name == 'MyInputObjectType' assert graphql_type.name == 'MyInputObjectType'
assert graphql_type.description == 'Description' assert graphql_type.description == 'Description'
# Container other_graphql_type = typemap['OtherObjectType']
container = graphql_type.create_container({'bar': 'oh!'}) inner_graphql_type = typemap['MyInnerObjectType']
container = graphql_type.create_container({
'bar': 'oh!',
'baz': inner_graphql_type.create_container({
'some_other_field': [
other_graphql_type.create_container({'thingy': 1}),
other_graphql_type.create_container({'thingy': 2})
]
})
})
assert isinstance(container, MyInputObjectType) assert isinstance(container, MyInputObjectType)
assert 'bar' in container assert 'bar' in container
assert container.bar == 'oh!' assert container.bar == 'oh!'
assert 'foo_bar' not in container assert 'foo_bar' not in container
assert container.foo_bar is None
assert container.baz.some_field is None
assert container.baz.some_other_field[0].thingy == 1
assert container.baz.some_other_field[1].thingy == 2
fields = graphql_type.fields fields = graphql_type.fields
assert list(fields.keys()) == ['fooBar', 'gizmo', 'own'] assert list(fields.keys()) == ['fooBar', 'gizmo', 'baz', 'own']
own_field = fields['own'] own_field = fields['own']
assert own_field.type == graphql_type assert own_field.type == graphql_type
foo_field = fields['fooBar'] foo_field = fields['fooBar']
@ -206,3 +233,22 @@ def test_objecttype_with_possible_types():
assert graphql_type.is_type_of assert graphql_type.is_type_of
assert graphql_type.is_type_of({}, None) is True assert graphql_type.is_type_of({}, None) is True
assert graphql_type.is_type_of(MyObjectType(), None) is False assert graphql_type.is_type_of(MyObjectType(), None) is False
def test_resolve_type_with_missing_type():
class MyObjectType(ObjectType):
foo_bar = String()
class MyOtherObjectType(ObjectType):
fizz_buzz = String()
def resolve_type_func(root, info):
return MyOtherObjectType
typemap = TypeMap([MyObjectType])
with pytest.raises(AssertionError) as excinfo:
resolve_type(
resolve_type_func, typemap, 'MyOtherObjectType', {}, {}
)
assert 'MyOtherObjectTyp' in str(excinfo.value)

View File

@ -46,7 +46,10 @@ def resolve_type(resolve_type_func, map, type_name, root, info):
if inspect.isclass(_type) and issubclass(_type, ObjectType): if inspect.isclass(_type) and issubclass(_type, ObjectType):
graphql_type = map.get(_type._meta.name) graphql_type = map.get(_type._meta.name)
assert graphql_type and graphql_type.graphene_type == _type, ( assert graphql_type, "Can't find type {} in schema".format(
_type._meta.name
)
assert graphql_type.graphene_type == _type, (
'The type {} does not match with the associated graphene type {}.' 'The type {} does not match with the associated graphene type {}.'
).format(_type, graphql_type.graphene_type) ).format(_type, graphql_type.graphene_type)
return graphql_type return graphql_type

View File

@ -1,13 +1,13 @@
import re import re
# From this response in Stackoverflow # Adapted from this response in Stackoverflow
# http://stackoverflow.com/a/19053800/1072990 # http://stackoverflow.com/a/19053800/1072990
def to_camel_case(snake_str): def to_camel_case(snake_str):
components = snake_str.split('_') components = snake_str.split('_')
# We capitalize the first letter of each component except the first one # We capitalize the first letter of each component except the first one
# with the 'title' method and join them together. # with the 'capitalize' method and join them together.
return components[0] + "".join(x.title() if x else '_' for x in components[1:]) return components[0] + ''.join(x.capitalize() if x else '_' for x in components[1:])
# From this response in Stackoverflow # From this response in Stackoverflow

View File

@ -6,9 +6,15 @@ from .props import props
class SubclassWithMeta_Meta(InitSubclassMeta): class SubclassWithMeta_Meta(InitSubclassMeta):
_meta = None
def __str__(cls):
if cls._meta:
return cls._meta.name
return cls.__name__
def __repr__(cls): def __repr__(cls):
return cls._meta.name return "<{} meta={}>".format(cls.__name__, repr(cls._meta))
class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)): class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)):
@ -24,7 +30,8 @@ class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)):
elif isclass(_Meta): elif isclass(_Meta):
_meta_props = props(_Meta) _meta_props = props(_Meta)
else: else:
raise Exception("Meta have to be either a class or a dict. Received {}".format(_Meta)) raise Exception(
"Meta have to be either a class or a dict. Received {}".format(_Meta))
delattr(cls, "Meta") delattr(cls, "Meta")
options = dict(meta_options, **_meta_props) options = dict(meta_options, **_meta_props)

View File

@ -16,6 +16,7 @@ def test_camel_case():
assert to_camel_case('snakes_on_a_plane') == 'snakesOnAPlane' assert to_camel_case('snakes_on_a_plane') == 'snakesOnAPlane'
assert to_camel_case('snakes_on_a__plane') == 'snakesOnA_Plane' assert to_camel_case('snakes_on_a__plane') == 'snakesOnA_Plane'
assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria' assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria'
assert to_camel_case('field_i18n') == 'fieldI18n'
def test_to_const(): def test_to_const():

View File

@ -83,7 +83,7 @@ setup(
keywords='api graphql protocol rest relay graphene', keywords='api graphql protocol rest relay graphene',
packages=find_packages(exclude=['tests', 'tests.*']), packages=find_packages(exclude=['tests', 'tests.*', 'examples']),
install_requires=[ install_requires=[
'six>=1.10.0,<2', 'six>=1.10.0,<2',

View File

@ -1,5 +1,5 @@
[tox] [tox]
envlist = flake8,py27,py33,py34,py35,pypy envlist = flake8,py27,py33,py34,py35,py36,pypy
skipsdist = true skipsdist = true
[testenv] [testenv]