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.*
.cache
.pytest_cache
nosetests.xml
coverage.xml
*,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
> 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

View File

@ -3,7 +3,7 @@ Middleware
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:
@ -18,10 +18,8 @@ Middlewares ``resolve`` is invoked with several arguments:
- ``next`` represents the execution chain. Call ``next`` to continue evalution.
- ``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.
- ``args`` is the dict of arguments passed to the field.
Example
-------
@ -42,3 +40,32 @@ And then execute it with:
.. code:: python
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 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
.. _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 Cursor Connection Specification: https://facebook.github.io/relay/graphql/connections.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)
@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(':')
if only_node:
if only_type:
# We assure that the node type that we want to retrieve
# 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':
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),
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:
``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.

View File

@ -1,4 +1,4 @@
# Required library
Sphinx==1.5.3
# 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)
``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
-----
@ -65,7 +72,7 @@ member getters.
In the Python ``Enum`` implementation you can access a member by initing the Enum.
.. code:: python
from enum import Enum
class Color(Enum):
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:
.. code:: python
from graphene import Enum
class Color(Enum):
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.
It works the same for arguments, where the validation step will expect a list
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
=======
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.String``
- ``graphene.Int``
- ``graphene.Float``
- ``graphene.Boolean``
- ``graphene.ID``
``graphene.String``
Represents textual data, represented as UTF-8
character sequences. The String type is most often used by GraphQL to
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.types.datetime.DateTime``
- ``graphene.types.datetime.Time``
- ``graphene.types.json.JSONString``
``graphene.types.datetime.Date``
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

View File

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

View File

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

View File

@ -73,7 +73,7 @@ class Connection(ObjectType):
edge = type(edge_name, edge_bases, {})
cls.Edge = edge
_meta.name = name
options['name'] = name
_meta.node = node
_meta.fields = OrderedDict([
('page_info', Field(PageInfo, name='pageInfo', required=True)),
@ -99,16 +99,19 @@ class IterableConnectionField(Field):
def type(self):
type = super(IterableConnectionField, self).type
connection_type = type
if is_node(type):
if isinstance(type, NonNull):
connection_type = type.of_type
if is_node(connection_type):
raise Exception(
"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), (
'{} type have to be a subclass of Connection. Received "{}".'
).format(self.__class__.__name__, connection_type)
return connection_type
return type
@classmethod
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):
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)
if is_thenable(resolved):
return Promise.resolve(resolved).then(on_resolve)

View File

@ -1,6 +1,6 @@
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 ..node import Node
@ -52,6 +52,21 @@ def test_connection_inherit_abstracttype():
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():
class MyObjectConnection(Connection):
@ -122,9 +137,10 @@ def test_connectionfield_node_deprecated():
field = ConnectionField(MyObject)
with pytest.raises(Exception) as exc_info:
field.type
assert "ConnectionField's now need a explicit ConnectionType for Nodes." in str(exc_info.value)
def test_connectionfield_custom_args():
class MyObjectConnection(Connection):
@ -139,3 +155,23 @@ def test_connectionfield_custom_args():
'last': Argument(Int),
'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
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):
other_attr = None
@ -40,3 +43,77 @@ def test_special_objecttype_inherit_meta_options():
assert MyType._meta.name == 'MyType'
assert MyType._meta.default_resolver is None
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):
warn_deprecation(
"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)

View File

@ -21,7 +21,7 @@ class BaseOptions(object):
raise Exception("Can't modify frozen Options {0}".format(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):

View File

@ -31,7 +31,10 @@ class Date(Scalar):
@staticmethod
def parse_value(value):
return parse_date(value)
try:
return parse_date(value)
except ValueError:
return None
class DateTime(Scalar):
@ -55,7 +58,10 @@ class DateTime(Scalar):
@staticmethod
def parse_value(value):
return parse_datetime(value)
try:
return parse_datetime(value)
except ValueError:
return None
class Time(Scalar):
@ -79,4 +85,7 @@ class Time(Scalar):
@classmethod
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):
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)
def get(cls, value):
@ -60,8 +64,9 @@ class EnumMeta(SubclassWithMeta_Meta):
class Enum(six.with_metaclass(EnumMeta, UnmountedType, BaseType)):
@classmethod
def __init_subclass_with_meta__(cls, enum=None, **options):
_meta = EnumOptions(cls)
def __init_subclass_with_meta__(cls, enum=None, _meta=None, **options):
if not _meta:
_meta = EnumOptions(cls)
_meta.enum = enum or cls.__enum__
_meta.deprecation_reason = options.pop('deprecation_reason', None)
for key, value in _meta.enum.__members__.items():

View File

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

View File

@ -50,7 +50,8 @@ class Mutation(ObjectType):
warn_deprecation((
"Please use {name}.Arguments instead of {name}.Input."
"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__))
if input_class:
@ -72,10 +73,17 @@ class Mutation(ObjectType):
_meta.resolver = resolver
_meta.arguments = arguments
super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options)
super(Mutation, cls).__init_subclass_with_meta__(
_meta=_meta, **options)
@classmethod
def Field(cls, *args, **kwargs):
def Field(cls, name=None, description=None, deprecation_reason=None, required=False):
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
from graphql import GraphQLSchema, graphql, is_type
from graphql import GraphQLSchema, graphql, is_type, GraphQLObjectType
from graphql.type.directives import (GraphQLDirective, GraphQLIncludeDirective,
GraphQLSkipDirective)
from graphql.type.introspection import IntrospectionSchema
@ -12,6 +12,17 @@ from .objecttype import ObjectType
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):
'''
Schema Definition
@ -20,21 +31,23 @@ class Schema(GraphQLSchema):
query and mutation (optional).
'''
def __init__(self, query=None, mutation=None, subscription=None,
directives=None, types=None, auto_camelcase=True):
assert inspect.isclass(query) and issubclass(query, ObjectType), (
'Schema query must be Object Type but got: {}.'
).format(query)
def __init__(self,
query=None,
mutation=None,
subscription=None,
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._mutation = mutation
self._subscription = subscription
self.types = types
self.auto_camelcase = auto_camelcase
if directives is None:
directives = [
GraphQLIncludeDirective,
GraphQLSkipDirective
]
directives = [GraphQLIncludeDirective, GraphQLSkipDirective]
assert all(isinstance(d, GraphQLDirective) for d in directives), \
'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)
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):
return _type.graphene_type
return _type
@ -73,7 +87,8 @@ class Schema(GraphQLSchema):
return _type
if is_graphene_type(_type):
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
return graphql_type
raise Exception("{} is not a valid GraphQL type.".format(_type))
@ -102,4 +117,8 @@ class Schema(GraphQLSchema):
]
if 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
from graphql import GraphQLError
from ..datetime import DateTime, Date, Time
from ..objecttype import ObjectType
from ..schema import Schema
@ -34,7 +36,7 @@ def test_datetime_query():
assert result.data == {'datetime': isoformat}
def test_datetime_query():
def test_date_query():
now = datetime.datetime.now().replace(tzinfo=pytz.utc).date()
isoformat = now.isoformat()
@ -53,6 +55,32 @@ def test_time_query():
assert not result.errors
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():
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
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):
foo = Episode()
@ -214,3 +215,19 @@ def test_enum_to_enum_comparison_should_differ():
assert RGB1.RED != RGB2.RED
assert RGB1.GREEN != RGB2.GREEN
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 ..objecttype import ObjectType
from ..unmountedtype import UnmountedType
from ..scalars import String, Boolean
from ..schema import Schema
class MyType(object):
@ -51,7 +53,8 @@ def test_ordered_fields_in_inputobjecttype():
field = MyScalar()
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():
@ -86,7 +89,8 @@ def test_generate_inputobjecttype_inherit_abstracttype():
field2 = MyScalar(MyType)
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():
@ -97,4 +101,34 @@ def test_generate_inputobjecttype_inherit_abstracttype_reversed():
field2 = MyScalar(MyType)
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 ..scalars import String
from ..schema import Schema
from ..structures import NonNull
def test_generate_mutation_no_args():
@ -133,3 +134,27 @@ def test_mutation_no_fields_output():
'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.interfaces == tuple()
assert MyObjectType._meta.fields == {}
assert repr(
MyObjectType) == "<MyObjectType meta=<ObjectTypeOptions name='MyObjectType'>>"
def test_generate_objecttype_with_meta():
@ -65,7 +67,6 @@ def test_generate_lazy_objecttype():
class InnerObjectType(ObjectType):
field = Field(MyType)
assert MyObjectType._meta.name == "MyObjectType"
example_field = MyObjectType._meta.fields['example']
@ -115,7 +116,8 @@ def test_generate_objecttype_inherit_abstracttype():
assert MyObjectType._meta.interfaces == ()
assert MyObjectType._meta.name == "MyObjectType"
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():
@ -129,7 +131,8 @@ def test_generate_objecttype_inherit_abstracttype_reversed():
assert MyObjectType._meta.interfaces == ()
assert MyObjectType._meta.name == "MyObjectType"
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():
@ -145,7 +148,8 @@ def test_parent_container_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():
@ -182,7 +186,8 @@ def test_objecttype_as_container_invalid_kwargs():
with pytest.raises(TypeError) as excinfo:
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):
@ -238,7 +243,6 @@ def test_objecttype_no_fields_output():
def resolve_user(self, info):
return User()
schema = Schema(query=Query)
result = schema.execute(''' query basequery {
user {
@ -252,3 +256,12 @@ def test_objecttype_no_fields_output():
'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,
GraphQLField, GraphQLInputObjectField,
GraphQLInputObjectType, GraphQLInterfaceType,
GraphQLObjectType, GraphQLString)
from ..structures import List, NonNull
from ..dynamic import Dynamic
from ..enum import Enum
from ..field import Field
@ -11,8 +13,8 @@ from ..inputfield import InputField
from ..inputobjecttype import InputObjectType
from ..interface import Interface
from ..objecttype import ObjectType
from ..scalars import String
from ..typemap import TypeMap
from ..scalars import String, Int
from ..typemap import TypeMap, resolve_type
def test_enum():
@ -38,7 +40,8 @@ def test_enum():
assert graphql_enum.description == 'Description'
values = graphql_enum.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'),
]
@ -46,7 +49,8 @@ def test_enum():
def test_objecttype():
class MyObjectType(ObjectType):
'''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')
def resolve_foo(self, bar):
@ -91,8 +95,10 @@ def test_dynamic_objecttype():
def test_interface():
class MyInterface(Interface):
'''Description'''
foo = String(bar=String(description='Argument description', default_value='x'), description='Field description')
bar = String(name='gizmo', first_arg=String(), other_arg=String(name='oth_arg'))
foo = String(bar=String(description='Argument description',
default_value='x'), description='Field description')
bar = String(name='gizmo', first_arg=String(),
other_arg=String(name='oth_arg'))
own = Field(lambda: MyInterface)
def resolve_foo(self, args, info):
@ -119,10 +125,18 @@ def test_interface():
def test_inputobject():
class OtherObjectType(InputObjectType):
thingy = NonNull(Int)
class MyInnerObjectType(InputObjectType):
some_field = String()
some_other_field = List(OtherObjectType)
class MyInputObjectType(InputObjectType):
'''Description'''
foo_bar = String(description='Field description')
bar = String(name='gizmo')
baz = NonNull(MyInnerObjectType)
own = InputField(lambda: MyInputObjectType)
def resolve_foo_bar(self, args, info):
@ -135,15 +149,28 @@ def test_inputobject():
assert graphql_type.name == 'MyInputObjectType'
assert graphql_type.description == 'Description'
# Container
container = graphql_type.create_container({'bar': 'oh!'})
other_graphql_type = typemap['OtherObjectType']
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 'bar' in container
assert container.bar == 'oh!'
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
assert list(fields.keys()) == ['fooBar', 'gizmo', 'own']
assert list(fields.keys()) == ['fooBar', 'gizmo', 'baz', 'own']
own_field = fields['own']
assert own_field.type == graphql_type
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({}, None) is True
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):
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 {}.'
).format(_type, graphql_type.graphene_type)
return graphql_type

View File

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

View File

@ -6,9 +6,15 @@ from .props import props
class SubclassWithMeta_Meta(InitSubclassMeta):
_meta = None
def __str__(cls):
if cls._meta:
return cls._meta.name
return cls.__name__
def __repr__(cls):
return cls._meta.name
return "<{} meta={}>".format(cls.__name__, repr(cls._meta))
class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)):
@ -24,7 +30,8 @@ class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)):
elif isclass(_Meta):
_meta_props = props(_Meta)
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")
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') == 'snakesOnA_Plane'
assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria'
assert to_camel_case('field_i18n') == 'fieldI18n'
def test_to_const():

View File

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

View File

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