Merge branch 'master' into feature/add_subscription

This commit is contained in:
Rob Blackbourn 2020-03-04 21:59:04 +00:00
commit 50c2ac3354
32 changed files with 415 additions and 393 deletions

View File

@ -1,2 +1,2 @@
[settings]
known_third_party = aniso8601,graphql,graphql_relay,promise,pytest,pytz,pyutils,setuptools,six,snapshottest,sphinx_graphene_theme
known_third_party = aniso8601,graphql,graphql_relay,promise,pytest,pytz,pyutils,setuptools,snapshottest,sphinx_graphene_theme

View File

@ -4,6 +4,7 @@ dist: xenial
python:
- "3.6"
- "3.7"
- "3.8"
install:
- pip install tox tox-travis

View File

@ -5,10 +5,10 @@ help:
.PHONY: install-dev ## Install development dependencies
install-dev:
pip install -e ".[test]"
pip install -e ".[dev]"
test:
py.test graphene
py.test graphene examples tests_asyncio
.PHONY: docs ## Generate docs
docs: install-dev
@ -17,3 +17,11 @@ docs: install-dev
.PHONY: docs-live ## Generate docs with live reloading
docs-live: install-dev
cd docs && make install && make livehtml
.PHONY: format
format:
black graphene examples setup.py tests_asyncio
.PHONY: lint
lint:
flake8 graphene examples setup.py tests_asyncio

View File

@ -1,12 +1,18 @@
# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![PyPI version](https://badge.fury.io/py/graphene.svg)](https://badge.fury.io/py/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master)
[💬 Join the community on Slack](https://join.slack.com/t/graphenetools/shared_invite/enQtOTE2MDQ1NTg4MDM1LTA4Nzk0MGU0NGEwNzUxZGNjNDQ4ZjAwNDJjMjY0OGE1ZDgxZTg4YjM2ZTc4MjE2ZTAzZjE2ZThhZTQzZTkyMmM)
**We are looking for contributors**! Please check the [ROADMAP](https://github.com/graphql-python/graphene/blob/master/ROADMAP.md) to see how you can help ❤️
---
# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![PyPI version](https://badge.fury.io/py/graphene.svg)](https://badge.fury.io/py/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master)
**The below readme is the documentation for the `dev` (prerelease) version of Graphene. To view the documentation for the latest stable Graphene version go to the [v2 docs](https://docs.graphene-python.org/en/stable/)**
---
## Introduction
[Graphene](http://graphene-python.org) is a Python library for building GraphQL schemas/types fast and easily.
[Graphene](http://graphene-python.org) is an opinionated Python library for building GraphQL schemas/types fast and easily.
- **Easy to use:** Graphene helps you use GraphQL in Python without effort.
- **Relay:** Graphene has builtin support for Relay.
@ -23,7 +29,6 @@ Graphene has multiple integrations with different frameworks:
| Django | [graphene-django](https://github.com/graphql-python/graphene-django/) |
| SQLAlchemy | [graphene-sqlalchemy](https://github.com/graphql-python/graphene-sqlalchemy/) |
| Google App Engine | [graphene-gae](https://github.com/graphql-python/graphene-gae/) |
| Peewee | _In progress_ ([Tracking Issue](https://github.com/graphql-python/graphene/issues/289)) |
Also, Graphene is fully compatible with the GraphQL spec, working seamlessly with all GraphQL clients, such as [Relay](https://github.com/facebook/relay), [Apollo](https://github.com/apollographql/apollo-client) and [gql](https://github.com/graphql-python/gql).
@ -35,10 +40,6 @@ For instaling graphene, just run this command in your shell
pip install "graphene>=2.0"
```
## 2.0 Upgrade Guide
Please read [UPGRADE-v2.0.md](/UPGRADE-v2.0.md) to learn how to upgrade.
## Examples
Here is one example for you to get started:

View File

@ -4,7 +4,7 @@ Executing a query
=================
For executing a query a schema, you can directly call the ``execute`` method on it.
For executing a query against a schema, you can directly call the ``execute`` method on it.
.. code:: python

View File

@ -0,0 +1,8 @@
File uploading
==============
File uploading is not part of the official GraphQL spec yet and is not natively
implemented in Graphene.
If your server needs to support file uploading then you can use the libary: `graphene-file-upload <https://github.com/lmcgartland/graphene-file-upload>`_ which enhances Graphene to add file
uploads and conforms to the unoffical GraphQL `multipart request spec <https://github.com/jaydenseric/graphql-multipart-request-spec>`_.

View File

@ -8,3 +8,4 @@ Execution
execute
middleware
dataloader
fileuploading

View File

@ -29,7 +29,7 @@ This middleware only continues evaluation if the ``field_name`` is not ``'user'`
.. code:: python
class AuthorizationMiddleware(object):
def resolve(next, root, info, **args):
def resolve(self, next, root, info, **args):
if info.field_name == 'user':
return None
return next(root, info, **args)

View File

@ -1,6 +1,12 @@
Graphene
========
------------
The documentation below is for the ``dev`` (prerelease) version of Graphene. To view the documentation for the latest stable Graphene version go to the `v2 docs <https://docs.graphene-python.org/en/stable/>`_.
------------
Contents:
.. toctree::

View File

@ -30,7 +30,7 @@ Compare Graphene's *code-first* approach to building a GraphQL API with *schema-
.. _Ariadne: https://ariadne.readthedocs.io
Graphene is fully featured with integrations for the most popular web frameworks and ORMs. Graphene produces schemas tha are fully compliant with the GraphQL spec and provides tools and patterns for building a Relay-Compliant API as well.
Graphene is fully featured with integrations for the most popular web frameworks and ORMs. Graphene produces schemas that are fully compliant with the GraphQL spec and provides tools and patterns for building a Relay-Compliant API as well.
An example in Graphene
----------------------
@ -127,7 +127,7 @@ Then we can start querying our **Schema** by passing a GraphQL query string to `
query_string = '{ hello }'
result = schema.execute(query_string)
print(result.data['hello'])
# "Hello stranger"
# "Hello stranger!"
# or passing the argument in the query
query_with_argument = '{ hello(name: "GraphQL") }'

View File

@ -101,7 +101,7 @@ When we execute a query against that schema.
query_string = "{ me { fullName } }"
result = schema.execute(query_string)
assert result["data"]["me"] == {"fullName": "Luke Skywalker")
assert result.data["me"] == {"fullName": "Luke Skywalker")
Then we go through the following steps to resolve this query:
@ -212,7 +212,7 @@ If the :ref:`ResolverParamParent` is a dictionary, the resolver will look for a
from graphene import ObjectType, String, Field, Schema
PersonValueObject = namedtuple('Person', 'first_name', 'last_name')
PersonValueObject = namedtuple("Person", ["first_name", "last_name"])
class Person(ObjectType):
first_name = String()
@ -224,7 +224,7 @@ If the :ref:`ResolverParamParent` is a dictionary, the resolver will look for a
def resolve_me(parent, info):
# always pass an object for `me` field
return PersonValueObject(first_name='Luke', last_name='Skywalker')
return PersonValueObject(first_name="Luke", last_name="Skywalker")
def resolve_my_best_friend(parent, info):
# always pass a dictionary for `my_best_fiend_field`
@ -238,10 +238,10 @@ If the :ref:`ResolverParamParent` is a dictionary, the resolver will look for a
}
''')
# With default resolvers we can resolve attributes from an object..
assert result['data']['me'] == {"firstName": "Luke", "lastName": "Skywalker"}
assert result.data["me"] == {"firstName": "Luke", "lastName": "Skywalker"}
# With default resolvers, we can also resolve keys from a dictionary..
assert result['data']['my_best_friend'] == {"firstName": "R2", "lastName": "D2"}
assert result.data["myBestFriend"] == {"firstName": "R2", "lastName": "D2"}
Advanced
~~~~~~~~
@ -280,7 +280,7 @@ An error will be thrown:
TypeError: resolve_hello() missing 1 required positional argument: 'name'
You can fix this error in serveral ways. Either by combining all keyword arguments
You can fix this error in several ways. Either by combining all keyword arguments
into a dict:
.. code:: python

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots["test_correctly_fetches_id_name_rebels 1"] = {
@ -30,7 +31,16 @@ snapshots["test_correctly_refetches_xwing 1"] = {
snapshots[
"test_str_schema 1"
] = '''"""A faction in the Star Wars saga"""
] = '''type Query {
rebels: Faction
empire: Faction
node(
"""The ID of the object"""
id: ID!
): Node
}
"""A faction in the Star Wars saga"""
type Faction implements Node {
"""The ID of the object"""
id: ID!
@ -42,28 +52,20 @@ type Faction implements Node {
ships(before: String = null, after: String = null, first: Int = null, last: Int = null): ShipConnection
}
input IntroduceShipInput {
shipName: String!
factionId: String!
clientMutationId: String = null
}
type IntroduceShipPayload {
ship: Ship
faction: Faction
clientMutationId: String
}
type Mutation {
introduceShip(input: IntroduceShipInput!): IntroduceShipPayload
}
"""An object with an ID"""
interface Node {
"""The ID of the object"""
id: ID!
}
type ShipConnection {
"""Pagination data for this connection."""
pageInfo: PageInfo!
"""Contains the nodes in this connection."""
edges: [ShipEdge]!
}
"""
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
"""
@ -81,12 +83,13 @@ type PageInfo {
endCursor: String
}
type Query {
rebels: Faction
empire: Faction
"""A Relay edge containing a `Ship` and its cursor."""
type ShipEdge {
"""The item at the end of the edge"""
node: Ship
"""The ID of the object"""
node(id: ID!): Node
"""A cursor for use in pagination"""
cursor: String!
}
"""A ship in the Star Wars saga"""
@ -98,20 +101,19 @@ type Ship implements Node {
name: String
}
type ShipConnection {
"""Pagination data for this connection."""
pageInfo: PageInfo!
"""Contains the nodes in this connection."""
edges: [ShipEdge]!
type Mutation {
introduceShip(input: IntroduceShipInput!): IntroduceShipPayload
}
"""A Relay edge containing a `Ship` and its cursor."""
type ShipEdge {
"""The item at the end of the edge"""
node: Ship
type IntroduceShipPayload {
ship: Ship
faction: Faction
clientMutationId: String
}
"""A cursor for use in pagination"""
cursor: String!
input IntroduceShipInput {
shipName: String!
factionId: String!
clientMutationId: String
}
'''

View File

@ -42,7 +42,7 @@ from .utils.resolve_only_args import resolve_only_args
from .utils.module_loading import lazy_import
VERSION = (3, 0, 0, "alpha", 0)
VERSION = (3, 0, 0, "beta", 0)
__version__ = get_version(VERSION)

View File

@ -90,9 +90,24 @@ class Node(AbstractNode):
def get_node_from_global_id(cls, info, global_id, only_type=None):
try:
_type, _id = cls.from_global_id(global_id)
graphene_type = info.schema.get_type(_type).graphene_type
except Exception:
return None
except Exception as e:
raise Exception(
(
'Unable to parse global ID "{global_id}". '
'Make sure it is a base64 encoded string in the format: "TypeName:id". '
"Exception message: {exception}".format(
global_id=global_id, exception=str(e)
)
)
)
graphene_type = info.schema.get_type(_type)
if graphene_type is None:
raise Exception(
'Relay Node "{_type}" not found in schema'.format(_type=_type)
)
graphene_type = graphene_type.graphene_type
if only_type:
assert graphene_type == only_type, ("Must receive a {} id.").format(
@ -101,7 +116,11 @@ class Node(AbstractNode):
# We make sure the ObjectType implements the "Node" interface
if cls not in graphene_type._meta.interfaces:
return None
raise Exception(
'ObjectType "{_type}" does not implement the "{cls}" interface.'.format(
_type=_type, cls=cls
)
)
get_node = getattr(graphene_type, "get_node", None)
if get_node:

View File

@ -80,11 +80,11 @@ class OtherMutation(ClientIDMutation):
@staticmethod
def mutate_and_get_payload(
self, info, shared, additional_field, client_mutation_id=None
self, info, shared="", additional_field="", client_mutation_id=None
):
edge_type = MyEdge
return OtherMutation(
name=(shared or "") + (additional_field or ""),
name=shared + additional_field,
my_node_edge=edge_type(cursor="1", node=MyNode(name="name")),
)

View File

@ -1,3 +1,4 @@
import re
from graphql_relay import to_global_id
from graphql.pyutils import dedent
@ -83,6 +84,20 @@ def test_node_requesting_non_node():
executed = schema.execute(
'{ node(id:"%s") { __typename } } ' % Node.to_global_id("RootQuery", 1)
)
assert executed.errors
assert re.match(
r"ObjectType .* does not implement the .* interface.",
executed.errors[0].message,
)
assert executed.data == {"node": None}
def test_node_requesting_unknown_type():
executed = schema.execute(
'{ node(id:"%s") { __typename } } ' % Node.to_global_id("UnknownType", 1)
)
assert executed.errors
assert re.match(r"Relay Node .* not found in schema", executed.errors[0].message)
assert executed.data == {"node": None}
@ -90,7 +105,8 @@ def test_node_query_incorrect_id():
executed = schema.execute(
'{ node(id:"%s") { ... on MyNode { name } } }' % "something:2"
)
assert not executed.errors
assert executed.errors
assert re.match(r"Unable to parse global ID .*", executed.errors[0].message)
assert executed.data == {"node": None}
@ -167,6 +183,12 @@ def test_str_schema():
name: String
}
"""An object with an ID"""
interface Node {
"""The ID of the object"""
id: ID!
}
type MyOtherNode implements Node {
"""The ID of the object"""
id: ID!
@ -175,23 +197,20 @@ def test_str_schema():
extraField: String
}
"""An object with an ID"""
interface Node {
"""The ID of the object"""
id: ID!
}
type RootQuery {
first: String
node(
"""The ID of the object"""
node(id: ID!): Node
id: ID!
): Node
onlyNode(
"""The ID of the object"""
onlyNode(id: ID!): MyNode
id: ID!
): MyNode
onlyNodeLazy(
"""The ID of the object"""
onlyNodeLazy(id: ID!): MyNode
id: ID!
): MyNode
}
'''
)

View File

@ -59,9 +59,12 @@ def test_str_schema_correct():
query: RootQuery
}
interface BasePhoto {
"""The width of the photo in pixels"""
width: Int
type User implements Node {
"""The ID of the object"""
id: ID!
"""The full name of the user"""
name: String
}
interface Node {
@ -77,17 +80,16 @@ def test_str_schema_correct():
width: Int
}
type RootQuery {
"""The ID of the object"""
node(id: ID!): Node
interface BasePhoto {
"""The width of the photo in pixels"""
width: Int
}
type User implements Node {
type RootQuery {
node(
"""The ID of the object"""
id: ID!
"""The full name of the user"""
name: String
): Node
}
'''
)

View File

@ -27,7 +27,7 @@ def test_issue():
graphene.Schema(query=Query)
assert str(exc_info.value) == (
"Query fields cannot be resolved:"
"Query fields cannot be resolved."
" IterableConnectionField type has to be a subclass of Connection."
' Received "MyUnion".'
)

View File

@ -3,8 +3,8 @@ from __future__ import absolute_import
import datetime
from aniso8601 import parse_date, parse_datetime, parse_time
from graphql.error import INVALID
from graphql.language import StringValueNode
from graphql.error import GraphQLError
from graphql.language import StringValueNode, print_ast
from .scalars import Scalar
@ -20,25 +20,30 @@ class Date(Scalar):
def serialize(date):
if isinstance(date, datetime.datetime):
date = date.date()
assert isinstance(
date, datetime.date
), 'Received not compatible date "{}"'.format(repr(date))
if not isinstance(date, datetime.date):
raise GraphQLError("Date cannot represent value: {}".format(repr(date)))
return date.isoformat()
@classmethod
def parse_literal(cls, node):
if isinstance(node, StringValueNode):
if not isinstance(node, StringValueNode):
raise GraphQLError(
"Date cannot represent non-string value: {}".format(print_ast(node))
)
return cls.parse_value(node.value)
@staticmethod
def parse_value(value):
try:
if isinstance(value, datetime.date):
return value
elif isinstance(value, str):
if not isinstance(value, str):
raise GraphQLError(
"Date cannot represent non-string value: {}".format(repr(value))
)
try:
return parse_date(value)
except ValueError:
return INVALID
raise GraphQLError("Date cannot represent value: {}".format(repr(value)))
class DateTime(Scalar):
@ -50,25 +55,32 @@ class DateTime(Scalar):
@staticmethod
def serialize(dt):
assert isinstance(
dt, (datetime.datetime, datetime.date)
), 'Received not compatible datetime "{}"'.format(repr(dt))
if not isinstance(dt, (datetime.datetime, datetime.date)):
raise GraphQLError("DateTime cannot represent value: {}".format(repr(dt)))
return dt.isoformat()
@classmethod
def parse_literal(cls, node):
if isinstance(node, StringValueNode):
if not isinstance(node, StringValueNode):
raise GraphQLError(
"DateTime cannot represent non-string value: {}".format(print_ast(node))
)
return cls.parse_value(node.value)
@staticmethod
def parse_value(value):
try:
if isinstance(value, datetime.datetime):
return value
elif isinstance(value, str):
if not isinstance(value, str):
raise GraphQLError(
"DateTime cannot represent non-string value: {}".format(repr(value))
)
try:
return parse_datetime(value)
except ValueError:
return INVALID
raise GraphQLError(
"DateTime cannot represent value: {}".format(repr(value))
)
class Time(Scalar):
@ -80,22 +92,27 @@ class Time(Scalar):
@staticmethod
def serialize(time):
assert isinstance(
time, datetime.time
), 'Received not compatible time "{}"'.format(repr(time))
if not isinstance(time, datetime.time):
raise GraphQLError("Time cannot represent value: {}".format(repr(time)))
return time.isoformat()
@classmethod
def parse_literal(cls, node):
if isinstance(node, StringValueNode):
if not isinstance(node, StringValueNode):
raise GraphQLError(
"Time cannot represent non-string value: {}".format(print_ast(node))
)
return cls.parse_value(node.value)
@classmethod
def parse_value(cls, value):
try:
if isinstance(value, datetime.time):
return value
elif isinstance(value, str):
if not isinstance(value, str):
raise GraphQLError(
"Time cannot represent non-string value: {}".format(repr(value))
)
try:
return parse_time(value)
except ValueError:
return INVALID
raise GraphQLError("Time cannot represent value: {}".format(repr(value)))

View File

@ -1,3 +1,5 @@
from graphql import Undefined
from .mountedtype import MountedType
from .structures import NonNull
from .utils import get_type
@ -48,7 +50,7 @@ class InputField(MountedType):
self,
type,
name=None,
default_value=None,
default_value=Undefined,
deprecation_reason=None,
description=None,
required=False,

View File

@ -68,4 +68,4 @@ class Interface(BaseType):
return type(instance)
def __init__(self, *args, **kwargs):
raise Exception("An Interface cannot be intitialized")
raise Exception("An Interface cannot be initialized")

View File

@ -7,7 +7,6 @@ from graphql import (
graphql,
graphql_sync,
introspection_types,
is_type,
parse,
print_schema,
subscribe,
@ -24,7 +23,7 @@ from graphql import (
GraphQLObjectType,
GraphQLSchema,
GraphQLString,
INVALID,
Undefined,
)
from ..utils.str_converters import to_camel_case
@ -73,118 +72,74 @@ def is_graphene_type(type_):
return True
def resolve_type(resolve_type_func, map_, type_name, root, info, _type):
type_ = resolve_type_func(root, info)
if not type_:
return_type = map_[type_name]
return default_type_resolver(root, info, return_type)
if inspect.isclass(type_) and issubclass(type_, ObjectType):
graphql_type = map_.get(type_._meta.name)
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
return type_
def is_type_of_from_possible_types(possible_types, root, _info):
return isinstance(root, possible_types)
class GrapheneGraphQLSchema(GraphQLSchema):
"""A GraphQLSchema that can deal with Graphene types as well."""
class TypeMap(dict):
def __init__(
self,
query=None,
mutation=None,
subscription=None,
types=None,
directives=None,
auto_camelcase=True,
):
assert_valid_root_type(query)
assert_valid_root_type(mutation)
assert_valid_root_type(subscription)
if types is None:
types = []
for type_ in types:
assert is_graphene_type(type_)
self.auto_camelcase = auto_camelcase
super().__init__(query, mutation, subscription, types, directives)
if query:
self.query_type = self.get_type(
query.name if isinstance(query, GraphQLObjectType) else query._meta.name
)
if mutation:
self.mutation_type = self.get_type(
mutation.name
if isinstance(mutation, GraphQLObjectType)
else mutation._meta.name
)
if subscription:
self.subscription_type = self.get_type(
subscription.name
if isinstance(subscription, GraphQLObjectType)
else subscription._meta.name
)
create_graphql_type = self.add_type
def get_graphql_type(self, _type):
if not _type:
return _type
if is_type(_type):
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
self.query = create_graphql_type(query) if query else None
self.mutation = create_graphql_type(mutation) if mutation else None
self.subscription = create_graphql_type(subscription) if subscription else None
self.types = [create_graphql_type(graphene_type) for graphene_type in types]
def add_type(self, graphene_type):
if inspect.isfunction(graphene_type):
graphene_type = graphene_type()
if isinstance(graphene_type, List):
return GraphQLList(self.add_type(graphene_type.of_type))
if isinstance(graphene_type, NonNull):
return GraphQLNonNull(self.add_type(graphene_type.of_type))
try:
name = graphene_type._meta.name
except AttributeError:
raise TypeError(
"Expected Graphene type, but received: {}.".format(graphene_type)
)
assert graphql_type.graphene_type == _type
graphql_type = self.get(name)
if graphql_type:
return graphql_type
raise Exception("{} is not a valid GraphQL type.".format(_type))
# noinspection PyMethodOverriding
def type_map_reducer(self, map_, type_):
if not type_:
return map_
if inspect.isfunction(type_):
type_ = type_()
if is_graphene_type(type_):
return self.graphene_reducer(map_, type_)
return super().type_map_reducer(map_, type_)
def graphene_reducer(self, map_, type_):
if isinstance(type_, (List, NonNull)):
return self.type_map_reducer(map_, type_.of_type)
if type_._meta.name in map_:
_type = map_[type_._meta.name]
if isinstance(_type, GrapheneGraphQLType):
assert _type.graphene_type == type_, (
"Found different types with the same name in the schema: {}, {}."
).format(_type.graphene_type, type_)
return map_
if issubclass(type_, ObjectType):
internal_type = self.construct_objecttype(map_, type_)
elif issubclass(type_, InputObjectType):
internal_type = self.construct_inputobjecttype(map_, type_)
elif issubclass(type_, Interface):
internal_type = self.construct_interface(map_, type_)
elif issubclass(type_, Scalar):
internal_type = self.construct_scalar(type_)
elif issubclass(type_, Enum):
internal_type = self.construct_enum(type_)
elif issubclass(type_, Union):
internal_type = self.construct_union(map_, type_)
if issubclass(graphene_type, ObjectType):
graphql_type = self.create_objecttype(graphene_type)
elif issubclass(graphene_type, InputObjectType):
graphql_type = self.create_inputobjecttype(graphene_type)
elif issubclass(graphene_type, Interface):
graphql_type = self.create_interface(graphene_type)
elif issubclass(graphene_type, Scalar):
graphql_type = self.create_scalar(graphene_type)
elif issubclass(graphene_type, Enum):
graphql_type = self.create_enum(graphene_type)
elif issubclass(graphene_type, Union):
graphql_type = self.construct_union(graphene_type)
else:
raise Exception("Expected Graphene type, but received: {}.".format(type_))
return super().type_map_reducer(map_, internal_type)
raise TypeError(
"Expected Graphene type, but received: {}.".format(graphene_type)
)
self[name] = graphql_type
return graphql_type
@staticmethod
def construct_scalar(type_):
def create_scalar(graphene_type):
# We have a mapping to the original GraphQL types
# so there are no collisions.
_scalars = {
@ -194,29 +149,31 @@ class GrapheneGraphQLSchema(GraphQLSchema):
Boolean: GraphQLBoolean,
ID: GraphQLID,
}
if type_ in _scalars:
return _scalars[type_]
if graphene_type in _scalars:
return _scalars[graphene_type]
return GrapheneScalarType(
graphene_type=type_,
name=type_._meta.name,
description=type_._meta.description,
serialize=getattr(type_, "serialize", None),
parse_value=getattr(type_, "parse_value", None),
parse_literal=getattr(type_, "parse_literal", None),
graphene_type=graphene_type,
name=graphene_type._meta.name,
description=graphene_type._meta.description,
serialize=getattr(graphene_type, "serialize", None),
parse_value=getattr(graphene_type, "parse_value", None),
parse_literal=getattr(graphene_type, "parse_literal", None),
)
@staticmethod
def construct_enum(type_):
def create_enum(graphene_type):
values = {}
for name, value in type_._meta.enum.__members__.items():
for name, value in graphene_type._meta.enum.__members__.items():
description = getattr(value, "description", None)
deprecation_reason = getattr(value, "deprecation_reason", None)
if not description and callable(type_._meta.description):
description = type_._meta.description(value)
if not description and callable(graphene_type._meta.description):
description = graphene_type._meta.description(value)
if not deprecation_reason and callable(type_._meta.deprecation_reason):
deprecation_reason = type_._meta.deprecation_reason(value)
if not deprecation_reason and callable(
graphene_type._meta.deprecation_reason
):
deprecation_reason = graphene_type._meta.deprecation_reason(value)
values[name] = GraphQLEnumValue(
value=value.value,
@ -225,107 +182,98 @@ class GrapheneGraphQLSchema(GraphQLSchema):
)
type_description = (
type_._meta.description(None)
if callable(type_._meta.description)
else type_._meta.description
graphene_type._meta.description(None)
if callable(graphene_type._meta.description)
else graphene_type._meta.description
)
return GrapheneEnumType(
graphene_type=type_,
graphene_type=graphene_type,
values=values,
name=type_._meta.name,
name=graphene_type._meta.name,
description=type_description,
)
def construct_objecttype(self, map_, type_):
if type_._meta.name in map_:
_type = map_[type_._meta.name]
if isinstance(_type, GrapheneGraphQLType):
assert _type.graphene_type == type_, (
"Found different types with the same name in the schema: {}, {}."
).format(_type.graphene_type, type_)
return _type
def create_objecttype(self, graphene_type):
create_graphql_type = self.add_type
def interfaces():
interfaces = []
for interface in type_._meta.interfaces:
self.graphene_reducer(map_, interface)
internal_type = map_[interface._meta.name]
assert internal_type.graphene_type == interface
interfaces.append(internal_type)
for graphene_interface in graphene_type._meta.interfaces:
interface = create_graphql_type(graphene_interface)
assert interface.graphene_type == graphene_interface
interfaces.append(interface)
return interfaces
if type_._meta.possible_types:
if graphene_type._meta.possible_types:
is_type_of = partial(
is_type_of_from_possible_types, type_._meta.possible_types
is_type_of_from_possible_types, graphene_type._meta.possible_types
)
else:
is_type_of = type_.is_type_of
is_type_of = graphene_type.is_type_of
return GrapheneObjectType(
graphene_type=type_,
name=type_._meta.name,
description=type_._meta.description,
fields=partial(self.construct_fields_for_type, map_, type_),
graphene_type=graphene_type,
name=graphene_type._meta.name,
description=graphene_type._meta.description,
fields=partial(self.create_fields_for_type, graphene_type),
is_type_of=is_type_of,
interfaces=interfaces,
)
def construct_interface(self, map_, type_):
if type_._meta.name in map_:
_type = map_[type_._meta.name]
if isinstance(_type, GrapheneInterfaceType):
assert _type.graphene_type == type_, (
"Found different types with the same name in the schema: {}, {}."
).format(_type.graphene_type, type_)
return _type
_resolve_type = None
if type_.resolve_type:
_resolve_type = partial(
resolve_type, type_.resolve_type, map_, type_._meta.name
def create_interface(self, graphene_type):
resolve_type = (
partial(
self.resolve_type, graphene_type.resolve_type, graphene_type._meta.name
)
if graphene_type.resolve_type
else None
)
return GrapheneInterfaceType(
graphene_type=type_,
name=type_._meta.name,
description=type_._meta.description,
fields=partial(self.construct_fields_for_type, map_, type_),
resolve_type=_resolve_type,
graphene_type=graphene_type,
name=graphene_type._meta.name,
description=graphene_type._meta.description,
fields=partial(self.create_fields_for_type, graphene_type),
resolve_type=resolve_type,
)
def construct_inputobjecttype(self, map_, type_):
def create_inputobjecttype(self, graphene_type):
return GrapheneInputObjectType(
graphene_type=type_,
name=type_._meta.name,
description=type_._meta.description,
out_type=type_._meta.container,
graphene_type=graphene_type,
name=graphene_type._meta.name,
description=graphene_type._meta.description,
out_type=graphene_type._meta.container,
fields=partial(
self.construct_fields_for_type, map_, type_, is_input_type=True
self.create_fields_for_type, graphene_type, is_input_type=True
),
)
def construct_union(self, map_, type_):
_resolve_type = None
if type_.resolve_type:
_resolve_type = partial(
resolve_type, type_.resolve_type, map_, type_._meta.name
)
def construct_union(self, graphene_type):
create_graphql_type = self.add_type
def types():
union_types = []
for objecttype in type_._meta.types:
self.graphene_reducer(map_, objecttype)
internal_type = map_[objecttype._meta.name]
assert internal_type.graphene_type == objecttype
union_types.append(internal_type)
for graphene_objecttype in graphene_type._meta.types:
object_type = create_graphql_type(graphene_objecttype)
assert object_type.graphene_type == graphene_objecttype
union_types.append(object_type)
return union_types
resolve_type = (
partial(
self.resolve_type, graphene_type.resolve_type, graphene_type._meta.name
)
if graphene_type.resolve_type
else None
)
return GrapheneUnionType(
graphene_type=type_,
name=type_._meta.name,
description=type_._meta.description,
graphene_type=graphene_type,
name=graphene_type._meta.name,
description=graphene_type._meta.description,
types=types,
resolve_type=_resolve_type,
resolve_type=resolve_type,
)
def get_name(self, name):
@ -333,15 +281,16 @@ class GrapheneGraphQLSchema(GraphQLSchema):
return to_camel_case(name)
return name
def construct_fields_for_type(self, map_, type_, is_input_type=False):
def create_fields_for_type(self, graphene_type, is_input_type=False):
create_graphql_type = self.add_type
fields = {}
for name, field in type_._meta.fields.items():
for name, field in graphene_type._meta.fields.items():
if isinstance(field, Dynamic):
field = get_field_as(field.get_type(self), _as=Field)
if not field:
continue
map_ = self.type_map_reducer(map_, field.type)
field_type = self.get_field_type(map_, field.type)
field_type = create_graphql_type(field.type)
if is_input_type:
_field = GraphQLInputField(
field_type,
@ -352,17 +301,19 @@ class GrapheneGraphQLSchema(GraphQLSchema):
else:
args = {}
for arg_name, arg in field.args.items():
map_ = self.type_map_reducer(map_, arg.type)
arg_type = self.get_field_type(map_, arg.type)
arg_type = create_graphql_type(arg.type)
processed_arg_name = arg.name or self.get_name(arg_name)
args[processed_arg_name] = GraphQLArgument(
arg_type,
out_name=arg_name,
description=arg.description,
default_value=INVALID
default_value=Undefined
if isinstance(arg.type, NonNull)
else arg.default_value,
)
resolve = field.get_resolver(
self.get_resolver(graphene_type, name, field.default_value)
)
_field = GraphQLField(
field_type,
args=args,
@ -392,7 +343,7 @@ class GrapheneGraphQLSchema(GraphQLSchema):
# If we don't find the resolver in the ObjectType class, then try to
# find it in each of the interfaces
interface_resolver = None
for interface in type_._meta.interfaces:
for interface in graphene_type._meta.interfaces:
if name not in interface._meta.fields:
continue
interface_resolver = getattr(interface, func_name, None)
@ -404,16 +355,11 @@ class GrapheneGraphQLSchema(GraphQLSchema):
if resolver:
return get_unbound_function(resolver)
default_resolver = type_._meta.default_resolver or get_default_resolver()
default_resolver = (
graphene_type._meta.default_resolver or get_default_resolver()
)
return partial(default_resolver, name, default_value)
def get_field_type(self, map_, type_):
if isinstance(type_, List):
return GraphQLList(self.get_field_type(map_, type_.of_type))
if isinstance(type_, NonNull):
return GraphQLNonNull(self.get_field_type(map_, type_.of_type))
return map_.get(type_._meta.name)
class Schema:
"""Schema Definition.
@ -423,17 +369,17 @@ class Schema:
questions about the types through introspection.
Args:
query (ObjectType): Root query *ObjectType*. Describes entry point for fields to *read*
query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read*
data in your Schema.
mutation (ObjectType, optional): Root mutation *ObjectType*. Describes entry point for
mutation (Optional[Type[ObjectType]]): Root mutation *ObjectType*. Describes entry point for
fields to *create, update or delete* data in your API.
subscription (ObjectType, optional): Root subscription *ObjectType*. Describes entry point
subscription (Optional[Type[ObjectType]]): Root subscription *ObjectType*. Describes entry point
for fields to receive continuous updates.
types (Optional[List[Type[ObjectType]]]): List of any types to include in schema that
may not be introspected through root types.
directives (List[GraphQLDirective], optional): List of custom directives to include in the
GraphQL schema. Defaults to only include directives defined by GraphQL spec (@include
and @skip) [GraphQLIncludeDirective, GraphQLSkipDirective].
types (List[GraphQLType], optional): List of any types to include in schema that
may not be introspected through root types.
auto_camelcase (bool): Fieldnames will be transformed in Schema's TypeMap from snake_case
to camelCase (preferred by GraphQL standard). Default True.
"""
@ -450,13 +396,15 @@ class Schema:
self.query = query
self.mutation = mutation
self.subscription = subscription
self.graphql_schema = GrapheneGraphQLSchema(
query,
mutation,
subscription,
types,
type_map = TypeMap(
query, mutation, subscription, types, auto_camelcase=auto_camelcase
)
self.graphql_schema = GraphQLSchema(
type_map.query,
type_map.mutation,
type_map.subscription,
type_map.types,
directives,
auto_camelcase=auto_camelcase,
)
def __str__(self):

View File

@ -3,7 +3,7 @@ import datetime
import pytz
from graphql import GraphQLError
from pytest import fixture, mark
from pytest import fixture
from ..datetime import Date, DateTime, Time
from ..objecttype import ObjectType
@ -84,8 +84,9 @@ def test_bad_datetime_query():
assert result.errors and len(result.errors) == 1
error = result.errors[0]
assert isinstance(error, GraphQLError)
assert error.message == (
'Expected type DateTime, found "Some string that\'s not a datetime".'
assert (
error.message == "DateTime cannot represent value:"
' "Some string that\'s not a datetime"'
)
assert result.data is None
@ -97,8 +98,9 @@ def test_bad_date_query():
error = result.errors[0]
assert isinstance(error, GraphQLError)
assert error.message == (
'Expected type Date, found "Some string that\'s not a date".'
assert (
error.message == "Date cannot represent value:"
' "Some string that\'s not a date"'
)
assert result.data is None
@ -110,8 +112,9 @@ def test_bad_time_query():
error = result.errors[0]
assert isinstance(error, GraphQLError)
assert error.message == (
'Expected type Time, found "Some string that\'s not a time".'
assert (
error.message == "Time cannot represent value:"
' "Some string that\'s not a time"'
)
assert result.data is None
@ -174,9 +177,6 @@ def test_time_query_variable(sample_time):
assert result.data == {"time": isoformat}
@mark.xfail(
reason="creating the error message fails when un-parsable object is not JSON serializable."
)
def test_bad_variables(sample_date, sample_datetime, sample_time):
def _test_bad_variables(type_, input_):
result = schema.execute(
@ -185,8 +185,6 @@ def test_bad_variables(sample_date, sample_datetime, sample_time):
),
variables={"input": input_},
)
# when `input` is not JSON serializable formatting the error message in
# `graphql.utils.is_valid_value` line 79 fails with a TypeError
assert isinstance(result.errors, list)
assert len(result.errors) == 1
assert isinstance(result.errors[0], GraphQLError)
@ -205,7 +203,6 @@ def test_bad_variables(sample_date, sample_datetime, sample_time):
("DateTime", time),
("Date", not_a_date),
("Date", not_a_date_str),
("Date", now),
("Date", time),
("Time", not_a_date),
("Time", not_a_date_str),

View File

@ -262,17 +262,14 @@ def test_query_input_field():
result = test_schema.execute('{ test(aInput: {aField: "String!"} ) }', "Source!")
assert not result.errors
assert result.data == {
"test": '["Source!",{"a_input":{"a_field":"String!","recursive_field":null}}]'
}
assert result.data == {"test": '["Source!",{"a_input":{"a_field":"String!"}}]'}
result = test_schema.execute(
'{ test(aInput: {recursiveField: {aField: "String!"}}) }', "Source!"
)
assert not result.errors
assert result.data == {
"test": '["Source!",{"a_input":{"a_field":null,"recursive_field":'
'{"a_field":"String!","recursive_field":null}}}]'
"test": '["Source!",{"a_input":{"recursive_field":{"a_field":"String!"}}}]'
}
@ -408,7 +405,7 @@ def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark):
def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark(
benchmark
benchmark,
):
class Container(ObjectType):
x = Int()

View File

@ -1,5 +1,6 @@
from pytest import raises
from graphql.type import GraphQLObjectType, GraphQLSchema
from graphql.pyutils import dedent
from ..field import Field
@ -17,8 +18,13 @@ class Query(ObjectType):
def test_schema():
schema = Schema(Query).graphql_schema
assert schema.query_type == schema.get_graphql_type(Query)
schema = Schema(Query)
graphql_schema = schema.graphql_schema
assert isinstance(graphql_schema, GraphQLSchema)
query_type = graphql_schema.query_type
assert isinstance(query_type, GraphQLObjectType)
assert query_type.name == "Query"
assert query_type.graphene_type is Query
def test_schema_get_type():
@ -39,13 +45,13 @@ def test_schema_str():
schema = Schema(Query)
assert str(schema) == dedent(
"""
type MyOtherType {
field: String
}
type Query {
inner: MyOtherType
}
type MyOtherType {
field: String
}
"""
)

View File

@ -1,5 +1,3 @@
from pytest import raises
from graphql.type import (
GraphQLArgument,
GraphQLEnumType,
@ -21,13 +19,13 @@ from ..interface import Interface
from ..objecttype import ObjectType
from ..scalars import Int, String
from ..structures import List, NonNull
from ..schema import GrapheneGraphQLSchema, resolve_type
from ..schema import Schema
def create_type_map(types, auto_camelcase=True):
query = GraphQLObjectType("Query", {})
schema = GrapheneGraphQLSchema(query, types=types, auto_camelcase=auto_camelcase)
return schema.type_map
query = type("Query", (ObjectType,), {})
schema = Schema(query, types=types, auto_camelcase=auto_camelcase)
return schema.graphql_schema.type_map
def test_enum():
@ -272,20 +270,3 @@ 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
type_map = create_type_map([MyObjectType])
with raises(AssertionError) as excinfo:
resolve_type(resolve_type_func, type_map, "MyOtherObjectType", {}, {}, None)
assert "MyOtherObjectTyp" in str(excinfo.value)

View File

@ -1,4 +1,5 @@
import re
from unidecode import unidecode
# Adapted from this response in Stackoverflow
@ -18,4 +19,4 @@ def to_snake_case(name):
def to_const(string):
return re.sub(r"[\W|^]+", "_", string).upper() # noqa
return re.sub(r"[\W|^]+", "_", unidecode(string)).upper()

View File

@ -19,7 +19,6 @@ class SubclassWithMeta_Meta(InitSubclassMeta):
class SubclassWithMeta(metaclass=SubclassWithMeta_Meta):
"""This class improves __init_subclass__ to receive automatically the options from meta"""
# We will only have the metaclass in Python 2
def __init_subclass__(cls, **meta_options):
"""This method just terminates the super() chain"""
_Meta = getattr(cls, "Meta", None)

View File

@ -21,3 +21,7 @@ def test_camel_case():
def test_to_const():
assert to_const('snakes $1. on a "#plane') == "SNAKES_1_ON_A_PLANE"
def test_to_const_unicode():
assert to_const("Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF"

View File

@ -45,20 +45,21 @@ class PyTest(TestCommand):
tests_require = [
"pytest",
"pytest-benchmark",
"pytest-cov",
"pytest-mock",
"pytest-asyncio",
"snapshottest",
"coveralls",
"promise",
"six",
"mock",
"pytz",
"iso8601",
"pytest>=5.3,<6",
"pytest-benchmark>=3.2,<4",
"pytest-cov>=2.8,<3",
"pytest-mock>=2,<3",
"pytest-asyncio>=0.10,<2",
"snapshottest>=0.5,<1",
"coveralls>=1.11,<2",
"promise>=2.3,<3",
"mock>=4.0,<5",
"pytz==2019.3",
"iso8601>=0.1,<2",
]
dev_requires = ["black==19.10b0", "flake8>=3.7,<4"] + tests_require
setup(
name="graphene",
version=version,
@ -76,15 +77,17 @@ setup(
"Topic :: Software Development :: Libraries",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
],
keywords="api graphql protocol rest relay graphene",
packages=find_packages(exclude=["tests", "tests.*", "examples"]),
install_requires=[
"graphql-core>=3.0.0a0,<4",
"graphql-relay>=3.0.0a0,<4",
"aniso8601>=6,<8",
"graphql-core>=3.1.0b1,<4",
"graphql-relay>=3.0,<4",
"aniso8601>=8,<9",
"unidecode>=1.1.1,<2",
],
tests_require=tests_require,
extras_require={"test": tests_require},
extras_require={"test": tests_require, "dev": dev_requires},
cmdclass={"test": PyTest},
)

View File

@ -42,11 +42,11 @@ class OtherMutation(ClientIDMutation):
@staticmethod
def mutate_and_get_payload(
self, info, shared, additional_field, client_mutation_id=None
self, info, shared="", additional_field="", client_mutation_id=None
):
edge_type = MyEdge
return OtherMutation(
name=(shared or "") + (additional_field or ""),
name=shared + additional_field,
my_node_edge=edge_type(cursor="1", node=MyNode(name="name")),
)

10
tox.ini
View File

@ -1,5 +1,5 @@
[tox]
envlist = flake8,py36,py37,pre-commit,mypy
envlist = flake8,py36,py37,py38,pre-commit,mypy
skipsdist = true
[testenv]
@ -8,12 +8,12 @@ deps =
setenv =
PYTHONPATH = .:{envdir}
commands =
py{36,37}: py.test --cov=graphene graphene examples tests_asyncio {posargs}
py{36,37}: pytest --cov=graphene graphene examples tests_asyncio {posargs}
[testenv:pre-commit]
basepython=python3.7
deps =
pre-commit>0.12.0
pre-commit>=2,<3
setenv =
LC_CTYPE=en_US.UTF-8
commands =
@ -22,12 +22,12 @@ commands =
[testenv:mypy]
basepython=python3.7
deps =
mypy>=0.720
mypy>=0.761,<1
commands =
mypy graphene
[testenv:flake8]
basepython=python3.6
basepython=python3.7
deps =
flake8>=3.7,<4
commands =