mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-23 23:20:47 +03:00
Merge branch 'master' into apollo-docs
This commit is contained in:
commit
86f5cbc08e
|
@ -2,3 +2,4 @@ global-exclude tests/*
|
||||||
recursive-exclude tests *
|
recursive-exclude tests *
|
||||||
recursive-exclude tests_py35 *
|
recursive-exclude tests_py35 *
|
||||||
recursive-exclude examples *
|
recursive-exclude examples *
|
||||||
|
include LICENSE
|
||||||
|
|
|
@ -8,7 +8,7 @@ Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade to Graph
|
||||||
[Graphene](http://graphene-python.org) is a Python library for building GraphQL schemas/types fast and easily.
|
[Graphene](http://graphene-python.org) is a Python library for building GraphQL schemas/types fast and easily.
|
||||||
|
|
||||||
- **Easy to use:** Graphene helps you use GraphQL in Python without effort.
|
- **Easy to use:** Graphene helps you use GraphQL in Python without effort.
|
||||||
- **Relay:** Graphene has builtin support for Relay
|
- **Relay:** Graphene has builtin support for Relay.
|
||||||
- **Data agnostic:** Graphene supports any kind of data source: SQL (Django, SQLAlchemy), NoSQL, custom Python objects, etc.
|
- **Data agnostic:** Graphene supports any kind of data source: SQL (Django, SQLAlchemy), NoSQL, custom Python objects, etc.
|
||||||
We believe that by providing a complete API you could plug Graphene anywhere your data lives and make your data available
|
We believe that by providing a complete API you could plug Graphene anywhere your data lives and make your data available
|
||||||
through GraphQL.
|
through GraphQL.
|
||||||
|
@ -25,6 +25,7 @@ Graphene has multiple integrations with different frameworks:
|
||||||
| Google App Engine | [graphene-gae](https://github.com/graphql-python/graphene-gae/) |
|
| 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)) |
|
| 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).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -75,7 +76,7 @@ If you want to learn even more, you can also check the following [examples](exam
|
||||||
After cloning this repo, ensure dependencies are installed by running:
|
After cloning this repo, ensure dependencies are installed by running:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install .[test]
|
pip install -e ".[test]"
|
||||||
```
|
```
|
||||||
|
|
||||||
After developing, the full test suite can be evaluated by running:
|
After developing, the full test suite can be evaluated by running:
|
||||||
|
|
10
README.rst
10
README.rst
|
@ -11,7 +11,7 @@ building GraphQL schemas/types fast and easily.
|
||||||
|
|
||||||
- **Easy to use:** Graphene helps you use GraphQL in Python without
|
- **Easy to use:** Graphene helps you use GraphQL in Python without
|
||||||
effort.
|
effort.
|
||||||
- **Relay:** Graphene has builtin support for Relay
|
- **Relay:** Graphene has builtin support for both Relay.
|
||||||
- **Data agnostic:** Graphene supports any kind of data source: SQL
|
- **Data agnostic:** Graphene supports any kind of data source: SQL
|
||||||
(Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe
|
(Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe
|
||||||
that by providing a complete API you could plug Graphene anywhere
|
that by providing a complete API you could plug Graphene anywhere
|
||||||
|
@ -34,6 +34,12 @@ Graphene has multiple integrations with different frameworks:
|
||||||
| Peewee | *In progress* (`Tracking Issue <https://github.com/graphql-python/graphene/issues/289>`__) |
|
| 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>`__.
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -89,7 +95,7 @@ After cloning this repo, ensure dependencies are installed by running:
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
pip install .[test]
|
pip install -e ".[test]"
|
||||||
|
|
||||||
After developing, the full test suite can be evaluated by running:
|
After developing, the full test suite can be evaluated by running:
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ schema = graphene.Schema(
|
||||||
|
|
||||||
## Interfaces
|
## Interfaces
|
||||||
|
|
||||||
For implementing an Interface in a ObjectType, you have to it onto `Meta.interfaces`.
|
For implementing an Interface in an ObjectType, you have to add it onto `Meta.interfaces`.
|
||||||
|
|
||||||
Like:
|
Like:
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ class Query(ObjectType):
|
||||||
|
|
||||||
## Nodes
|
## Nodes
|
||||||
|
|
||||||
Apart of implementing as showed in the previous section, for use the node field you have to
|
Apart from implementing as shown in the previous section, to use the node field you have to
|
||||||
specify the node Type.
|
specify the node Type.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
@ -155,16 +155,16 @@ class Query(ObjectType):
|
||||||
node = relay.Node.Field() # New way
|
node = relay.Node.Field() # New way
|
||||||
```
|
```
|
||||||
|
|
||||||
Also, if wanted to create an `ObjectType` that implements `Node`, you have to do it
|
Also, if you wanted to create an `ObjectType` that implements `Node`, you have to do it
|
||||||
explicity.
|
explicity.
|
||||||
|
|
||||||
|
|
||||||
## Django
|
## Django
|
||||||
|
|
||||||
The Django integration with Graphene now have an independent package: `graphene-django`.
|
The Django integration with Graphene now has an independent package: `graphene-django`.
|
||||||
For installing, you have to replace the old `graphene[django]` with `graphene-django`.
|
For installing, you have to replace the old `graphene[django]` with `graphene-django`.
|
||||||
|
|
||||||
* As the package is now independent, you have to import now from `graphene_django`.
|
* As the package is now independent, you now have to import from `graphene_django`.
|
||||||
* **DjangoNode no longer exists**, please use `relay.Node` instead:
|
* **DjangoNode no longer exists**, please use `relay.Node` instead:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -178,7 +178,7 @@ For installing, you have to replace the old `graphene[django]` with `graphene-dj
|
||||||
|
|
||||||
## SQLAlchemy
|
## SQLAlchemy
|
||||||
|
|
||||||
The SQLAlchemy integration with Graphene now have an independent package: `graphene-sqlalchemy`.
|
The SQLAlchemy integration with Graphene now has an independent package: `graphene-sqlalchemy`.
|
||||||
For installing, you have to replace the old `graphene[sqlalchemy]` with `graphene-sqlalchemy`.
|
For installing, you have to replace the old `graphene[sqlalchemy]` with `graphene-sqlalchemy`.
|
||||||
|
|
||||||
* As the package is now independent, you have to import now from `graphene_sqlalchemy`.
|
* As the package is now independent, you have to import now from `graphene_sqlalchemy`.
|
||||||
|
|
106
docs/execution/dataloader.rst
Normal file
106
docs/execution/dataloader.rst
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
Dataloader
|
||||||
|
==========
|
||||||
|
|
||||||
|
DataLoader is a generic utility to be used as part of your application's
|
||||||
|
data fetching layer to provide a simplified and consistent API over
|
||||||
|
various remote data sources such as databases or web services via batching
|
||||||
|
and caching.
|
||||||
|
|
||||||
|
|
||||||
|
Batching
|
||||||
|
--------
|
||||||
|
|
||||||
|
Batching is not an advanced feature, it's DataLoader's primary feature.
|
||||||
|
Create loaders by providing a batch loading function.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from promise import Promise
|
||||||
|
from promise.dataloader import DataLoader
|
||||||
|
|
||||||
|
class UserLoader(DataLoader):
|
||||||
|
def batch_load_fn(self, keys):
|
||||||
|
# Here we return a promise that will result on the
|
||||||
|
# corresponding user for each key in keys
|
||||||
|
return Promise.resolve([get_user(id=key) for key in keys])
|
||||||
|
|
||||||
|
|
||||||
|
A batch loading function accepts an list of keys, and returns a ``Promise``
|
||||||
|
which resolves to an list of ``values``.
|
||||||
|
|
||||||
|
Then load individual values from the loader. ``DataLoader`` will coalesce all
|
||||||
|
individual loads which occur within a single frame of execution (executed once
|
||||||
|
the wrapping promise is resolved) and then call your batch function with all
|
||||||
|
requested keys.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
user_loader = UserLoader()
|
||||||
|
|
||||||
|
user_loader.load(1).then(lambda user: user_loader.load(user.best_friend_id))
|
||||||
|
|
||||||
|
user_loader.load(2).then(lambda user: user_loader.load(user.best_friend_id))
|
||||||
|
|
||||||
|
|
||||||
|
A naive application may have issued *four* round-trips to a backend for the
|
||||||
|
required information, but with ``DataLoader`` this application will make at most *two*.
|
||||||
|
|
||||||
|
``DataLoader`` allows you to decouple unrelated parts of your application without
|
||||||
|
sacrificing the performance of batch data-loading. While the loader presents
|
||||||
|
an API that loads individual values, all concurrent requests will be coalesced
|
||||||
|
and presented to your batch loading function. This allows your application to
|
||||||
|
safely distribute data fetching requirements throughout your application and
|
||||||
|
maintain minimal outgoing data requests.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Using with Graphene
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
DataLoader pairs nicely well with Graphene/GraphQL. GraphQL fields are designed
|
||||||
|
to be stand-alone functions. Without a caching or batching mechanism, it's easy
|
||||||
|
for a naive GraphQL server to issue new database requests each time a field is resolved.
|
||||||
|
|
||||||
|
Consider the following GraphQL request:
|
||||||
|
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
{
|
||||||
|
me {
|
||||||
|
name
|
||||||
|
bestFriend {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
friends(first: 5) {
|
||||||
|
name
|
||||||
|
bestFriend {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Naively, if ``me``, ``bestFriend`` and ``friends`` each need to request the backend,
|
||||||
|
there could be at most 13 database requests!
|
||||||
|
|
||||||
|
|
||||||
|
When using DataLoader, we could define the User type using our previous example with
|
||||||
|
leaner code and at most 4 database requests, and possibly fewer if there are cache hits.
|
||||||
|
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class User(graphene.ObjectType):
|
||||||
|
name = graphene.String()
|
||||||
|
best_friend = graphene.Field(lambda: User)
|
||||||
|
friends = graphene.List(lambda: User)
|
||||||
|
|
||||||
|
def resolve_best_friend(self, args, context, info):
|
||||||
|
return user_loader.load(self.best_friend_id)
|
||||||
|
|
||||||
|
def resolve_friends(self, args, context, info):
|
||||||
|
return user_loader.load_many(self.friend_ids)
|
32
docs/execution/execute.rst
Normal file
32
docs/execution/execute.rst
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
Executing a query
|
||||||
|
=================
|
||||||
|
|
||||||
|
|
||||||
|
For executing a query a schema, you can directly call the ``execute`` method on it.
|
||||||
|
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
schema = graphene.Schema(...)
|
||||||
|
result = schema.execute('{ name }')
|
||||||
|
|
||||||
|
``result`` represents the result of execution. ``result.data`` is the result of executing the query, ``result.errors`` is ``None`` if no errors occurred, and is a non-empty list if an error occurred.
|
||||||
|
|
||||||
|
|
||||||
|
Context
|
||||||
|
_______
|
||||||
|
|
||||||
|
You can pass context to a query via ``context_value``.
|
||||||
|
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
name = graphene.String()
|
||||||
|
|
||||||
|
def resolve_name(self, args, context, info):
|
||||||
|
return context.get('name')
|
||||||
|
|
||||||
|
schema = graphene.Schema(Query)
|
||||||
|
result = schema.execute('{ name }', context_value={'name': 'Syrus'})
|
||||||
|
|
|
@ -2,39 +2,9 @@
|
||||||
Execution
|
Execution
|
||||||
=========
|
=========
|
||||||
|
|
||||||
For executing a query a schema, you can directly call the ``execute`` method on it.
|
|
||||||
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
schema = graphene.Schema(...)
|
|
||||||
result = schema.execute('{ name }')
|
|
||||||
|
|
||||||
``result`` represents he result of execution. ``result.data`` is the result of executing the query, ``result.errors`` is ``None`` if no errors occurred, and is a non-empty list if an error occurred.
|
|
||||||
|
|
||||||
|
|
||||||
Context
|
|
||||||
_______
|
|
||||||
|
|
||||||
You can pass context to a query via ``context_value``.
|
|
||||||
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
|
||||||
name = graphene.String()
|
|
||||||
|
|
||||||
def resolve_name(self, args, context, info):
|
|
||||||
return context.get('name')
|
|
||||||
|
|
||||||
schema = graphene.Schema(Query)
|
|
||||||
result = schema.execute('{ name }', context_value={'name': 'Syrus'})
|
|
||||||
|
|
||||||
|
|
||||||
Middleware
|
|
||||||
__________
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 2
|
||||||
|
|
||||||
|
execute
|
||||||
middleware
|
middleware
|
||||||
|
dataloader
|
||||||
|
|
|
@ -30,15 +30,15 @@ This middleware only continues evaluation if the ``field_name`` is not ``'user'`
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
class AuthorizationMiddleware(object):
|
class AuthorizationMiddleware(object):
|
||||||
def resolve(self, next, root, args, context, info):
|
def resolve(self, next, root, args, context, info):
|
||||||
if info.field_name == 'user':
|
if info.field_name == 'user':
|
||||||
return None
|
return None
|
||||||
return next(root, args, context, info)
|
return next(root, args, context, info)
|
||||||
|
|
||||||
|
|
||||||
And then execute it with:
|
And then execute it with:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()])
|
result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()])
|
||||||
|
|
|
@ -11,6 +11,7 @@ Contents:
|
||||||
execution/index
|
execution/index
|
||||||
relay/index
|
relay/index
|
||||||
apollo/index
|
apollo/index
|
||||||
|
testing/index
|
||||||
|
|
||||||
Integrations
|
Integrations
|
||||||
-----
|
-----
|
||||||
|
|
|
@ -5,7 +5,7 @@ What is GraphQL?
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
For an introduction to GraphQL and an overview of its concepts, please refer
|
For an introduction to GraphQL and an overview of its concepts, please refer
|
||||||
to `the official introduction <http://graphql.org/learn/>`.
|
to `the official introduction <http://graphql.org/learn/>`_.
|
||||||
|
|
||||||
Let’s build a basic GraphQL schema from scratch.
|
Let’s build a basic GraphQL schema from scratch.
|
||||||
|
|
||||||
|
@ -30,17 +30,17 @@ server with an associated set of resolve methods that know how to fetch
|
||||||
data.
|
data.
|
||||||
|
|
||||||
We are going to create a very simple schema, with a ``Query`` with only
|
We are going to create a very simple schema, with a ``Query`` with only
|
||||||
one field: ``hello``. And when we query it, it should return ``"World"``.
|
one field: ``hello`` and an input name. And when we query it, it should return ``"Hello {name}"``.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
hello = graphene.String()
|
hello = graphene.String(name=graphene.Argument(graphene.String, default_value="stranger"))
|
||||||
|
|
||||||
def resolve_hello(self, args, context, info):
|
def resolve_hello(self, args, context, info):
|
||||||
return 'World'
|
return 'Hello ' + args['name']
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
|
@ -52,6 +52,6 @@ Then we can start querying our schema:
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
result = schema.execute('{ hello }')
|
result = schema.execute('{ hello }')
|
||||||
print result.data['hello'] # "World"
|
print result.data['hello'] # "Hello stranger"
|
||||||
|
|
||||||
Congrats! You got your first graphene schema working!
|
Congrats! You got your first graphene schema working!
|
||||||
|
|
|
@ -3,7 +3,7 @@ Nodes
|
||||||
|
|
||||||
A ``Node`` is an Interface provided by ``graphene.relay`` that contains
|
A ``Node`` is an Interface provided by ``graphene.relay`` that contains
|
||||||
a single field ``id`` (which is a ``ID!``). Any object that inherits
|
a single field ``id`` (which is a ``ID!``). Any object that inherits
|
||||||
from it have to implement a ``get_node`` method for retrieving a
|
from it has to implement a ``get_node`` method for retrieving a
|
||||||
``Node`` by an *id*.
|
``Node`` by an *id*.
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,8 +26,8 @@ Example usage (taken from the `Starwars Relay example`_):
|
||||||
return get_ship(id)
|
return get_ship(id)
|
||||||
|
|
||||||
The ``id`` returned by the ``Ship`` type when you query it will be a
|
The ``id`` returned by the ``Ship`` type when you query it will be a
|
||||||
scalar which contains the enough info for the server for knowing it’s
|
scalar which contains enough info for the server to know its type and
|
||||||
type and it’s id.
|
its id.
|
||||||
|
|
||||||
For example, the instance ``Ship(id=1)`` will return ``U2hpcDox`` as the
|
For example, the instance ``Ship(id=1)`` will return ``U2hpcDox`` as the
|
||||||
id when you query it (which is the base64 encoding of ``Ship:1``), and
|
id when you query it (which is the base64 encoding of ``Ship:1``), and
|
||||||
|
@ -77,7 +77,7 @@ 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(global_id, context, info)``.
|
||||||
|
|
||||||
In the case we want to restrict the instnance retrieval to an 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(global_id, context, info, 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.
|
||||||
|
|
||||||
|
@ -98,4 +98,5 @@ Example usage:
|
||||||
# Should be CustomNode.Field() if we want to use our custom Node
|
# Should be CustomNode.Field() if we want to use our custom Node
|
||||||
node = relay.Node.Field()
|
node = relay.Node.Field()
|
||||||
|
|
||||||
|
.. _Relay specification: https://facebook.github.io/relay/docs/graphql-relay-specification.html
|
||||||
.. _Starwars Relay example: https://github.com/graphql-python/graphene/blob/master/examples/starwars_relay/schema.py
|
.. _Starwars Relay example: https://github.com/graphql-python/graphene/blob/master/examples/starwars_relay/schema.py
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
|
# Required library
|
||||||
|
Sphinx==1.5.3
|
||||||
# Docs template
|
# Docs template
|
||||||
https://github.com/graphql-python/graphene-python.org/archive/docs.zip
|
https://github.com/graphql-python/graphene-python.org/archive/docs.zip
|
||||||
|
|
111
docs/testing/index.rst
Normal file
111
docs/testing/index.rst
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
===================
|
||||||
|
Testing in Graphene
|
||||||
|
===================
|
||||||
|
|
||||||
|
|
||||||
|
Automated testing is an extremely useful bug-killing tool for the modern developer. You can use a collection of tests – a test suite – to solve, or avoid, a number of problems:
|
||||||
|
|
||||||
|
- When you’re writing new code, you can use tests to validate your code works as expected.
|
||||||
|
- When you’re refactoring or modifying old code, you can use tests to ensure your changes haven’t affected your application’s behavior unexpectedly.
|
||||||
|
|
||||||
|
Testing a GraphQL application is a complex task, because a GraphQL application is made of several layers of logic – schema definition, schema validation, permissions and field resolution.
|
||||||
|
|
||||||
|
With Graphene test-execution framework and assorted utilities, you can simulate GraphQL requests, execute mutations, inspect your application’s output and generally verify your code is doing what it should be doing.
|
||||||
|
|
||||||
|
|
||||||
|
Testing tools
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Graphene provides a small set of tools that come in handy when writing tests.
|
||||||
|
|
||||||
|
|
||||||
|
Test Client
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
The test client is a Python class that acts as a dummy GraphQL client, allowing you to test your views and interact with your Graphene-powered application programmatically.
|
||||||
|
|
||||||
|
Some of the things you can do with the test client are:
|
||||||
|
|
||||||
|
- Simulate Queries and Mutations and observe the response.
|
||||||
|
- Test that a given query request is rendered by a given Django template, with a template context that contains certain values.
|
||||||
|
|
||||||
|
|
||||||
|
Overview and a quick example
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To use the test client, instantiate ``graphene.test.Client`` and retrieve GraphQL responses:
|
||||||
|
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene.test import Client
|
||||||
|
|
||||||
|
def test_hey():
|
||||||
|
client = Client(my_schema)
|
||||||
|
executed = client.execute('''{ hey }''')
|
||||||
|
assert executed == {
|
||||||
|
'data': {
|
||||||
|
'hey': 'hello!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Execute parameters
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can also add extra keyword arguments to the ``execute`` method, such as
|
||||||
|
``context_value``, ``root_value``, ``variable_values``, ...:
|
||||||
|
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene.test import Client
|
||||||
|
|
||||||
|
def test_hey():
|
||||||
|
client = Client(my_schema)
|
||||||
|
executed = client.execute('''{ hey }''', context_value={'user': 'Peter'})
|
||||||
|
assert executed == {
|
||||||
|
'data': {
|
||||||
|
'hey': 'hello Peter!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Snapshot testing
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
As our APIs evolve, we need to know when our changes introduce any breaking changes that might break
|
||||||
|
some of the clients of our GraphQL app.
|
||||||
|
|
||||||
|
However, writing tests and replicate the same response we expect from our GraphQL application can be
|
||||||
|
tedious and repetitive task, and sometimes it's easier to skip this process.
|
||||||
|
|
||||||
|
Because of that, we recommend the usage of `SnapshotTest <https://github.com/syrusakbary/snapshottest/>`_.
|
||||||
|
|
||||||
|
SnapshotTest let us write all this tests in a breeze, as creates automatically the ``snapshots`` for us
|
||||||
|
the first time the test is executed.
|
||||||
|
|
||||||
|
|
||||||
|
Here is a simple example on how our tests will look if we use ``pytest``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
def test_hey(snapshot):
|
||||||
|
client = Client(my_schema)
|
||||||
|
# This will create a snapshot dir and a snapshot file
|
||||||
|
# the first time the test is executed, with the response
|
||||||
|
# of the execution.
|
||||||
|
snapshot.assert_match(client.execute('''{ hey }'''))
|
||||||
|
|
||||||
|
|
||||||
|
If we are using ``unittest``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from snapshottest import TestCase
|
||||||
|
|
||||||
|
class APITestCase(TestCase):
|
||||||
|
def test_api_me(self):
|
||||||
|
"""Testing the API for /me"""
|
||||||
|
client = Client(my_schema)
|
||||||
|
self.assertMatchSnapshot(client.execute('''{ hey }'''))
|
|
@ -59,7 +59,33 @@ Notes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
``graphene.Enum`` uses |enum.Enum|_ internally (or a backport if
|
``graphene.Enum`` uses |enum.Enum|_ internally (or a backport if
|
||||||
that's not available) and can be used in the exact same way.
|
that's not available) and can be used in a similar way, with the exception of
|
||||||
|
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
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
assert Color(1) == Color.RED
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
assert Color.get(1) == Color.RED
|
||||||
|
|
||||||
.. |enum.Enum| replace:: ``enum.Enum``
|
.. |enum.Enum| replace:: ``enum.Enum``
|
||||||
.. _enum.Enum: https://docs.python.org/3/library/enum.html
|
.. _enum.Enum: https://docs.python.org/3/library/enum.html
|
||||||
|
|
|
@ -7,6 +7,7 @@ Types Reference
|
||||||
|
|
||||||
enums
|
enums
|
||||||
scalars
|
scalars
|
||||||
|
list-and-nonnull
|
||||||
interfaces
|
interfaces
|
||||||
abstracttypes
|
abstracttypes
|
||||||
objecttypes
|
objecttypes
|
||||||
|
|
50
docs/types/list-and-nonnull.rst
Normal file
50
docs/types/list-and-nonnull.rst
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
Lists and Non-Null
|
||||||
|
==================
|
||||||
|
|
||||||
|
Object types, scalars, and enums are the only kinds of types you can
|
||||||
|
define in Graphene. But when you use the types in other parts of the
|
||||||
|
schema, or in your query variable declarations, you can apply additional
|
||||||
|
type modifiers that affect validation of those values.
|
||||||
|
|
||||||
|
NonNull
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
class Character(graphene.ObjectType):
|
||||||
|
name = graphene.NonNull(graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
Here, we're using a ``String`` type and marking it as Non-Null by wrapping
|
||||||
|
it using the ``NonNull`` class. This means that our server always expects
|
||||||
|
to return a non-null value for this field, and if it ends up getting a
|
||||||
|
null value that will actually trigger a GraphQL execution error,
|
||||||
|
letting the client know that something has gone wrong.
|
||||||
|
|
||||||
|
|
||||||
|
The previous ``NonNull`` code snippet is also equivalent to:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
class Character(graphene.ObjectType):
|
||||||
|
name = graphene.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
List
|
||||||
|
----
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
class Character(graphene.ObjectType):
|
||||||
|
appears_in = graphene.List(graphene.String)
|
||||||
|
|
||||||
|
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.
|
|
@ -19,7 +19,8 @@ This example defines a Mutation:
|
||||||
ok = graphene.Boolean()
|
ok = graphene.Boolean()
|
||||||
person = graphene.Field(lambda: Person)
|
person = graphene.Field(lambda: Person)
|
||||||
|
|
||||||
def mutate(self, args, context, info):
|
@staticmethod
|
||||||
|
def mutate(root, args, context, info):
|
||||||
person = Person(name=args.get('name'))
|
person = Person(name=args.get('name'))
|
||||||
ok = True
|
ok = True
|
||||||
return CreatePerson(person=person, ok=ok)
|
return CreatePerson(person=person, ok=ok)
|
||||||
|
@ -42,11 +43,16 @@ So, we can finish our schema like this:
|
||||||
|
|
||||||
class Person(graphene.ObjectType):
|
class Person(graphene.ObjectType):
|
||||||
name = graphene.String()
|
name = graphene.String()
|
||||||
|
age = graphene.Int()
|
||||||
|
|
||||||
class MyMutations(graphene.ObjectType):
|
class MyMutations(graphene.ObjectType):
|
||||||
create_person = CreatePerson.Field()
|
create_person = CreatePerson.Field()
|
||||||
|
|
||||||
schema = graphene.Schema(mutation=MyMutations)
|
# We must define a query for our schema
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
person = graphene.Field(Person)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query, mutation=MyMutations)
|
||||||
|
|
||||||
Executing the Mutation
|
Executing the Mutation
|
||||||
----------------------
|
----------------------
|
||||||
|
@ -96,11 +102,12 @@ To use an InputField you define an InputObjectType that specifies the structure
|
||||||
|
|
||||||
class CreatePerson(graphene.Mutation):
|
class CreatePerson(graphene.Mutation):
|
||||||
class Input:
|
class Input:
|
||||||
person_data = graphene.InputField(PersonInput)
|
person_data = graphene.Argument(PersonInput)
|
||||||
|
|
||||||
person = graphene.Field(lambda: Person)
|
person = graphene.Field(lambda: Person)
|
||||||
|
|
||||||
def mutate(self, args, context, info):
|
@staticmethod
|
||||||
|
def mutate(root, args, context, info):
|
||||||
p_data = args.get('person_data')
|
p_data = args.get('person_data')
|
||||||
|
|
||||||
name = p_data.get('name')
|
name = p_data.get('name')
|
||||||
|
|
|
@ -72,4 +72,4 @@ Types mounted in a ``Field`` act as ``Argument``\ s.
|
||||||
graphene.Field(graphene.String, to=graphene.String())
|
graphene.Field(graphene.String, to=graphene.String())
|
||||||
|
|
||||||
# Is equivalent to:
|
# Is equivalent to:
|
||||||
graphene.Field(graphene.String, to=graphene.Argument(graphene.String()))
|
graphene.Field(graphene.String, to=graphene.Argument(graphene.String))
|
||||||
|
|
0
examples/starwars/tests/snapshots/__init__.py
Normal file
0
examples/starwars/tests/snapshots/__init__.py
Normal file
202
examples/starwars/tests/snapshots/snap_test_query.py
Normal file
202
examples/starwars/tests/snapshots/snap_test_query.py
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# snapshottest: v1 - https://goo.gl/zC4yUc
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from snapshottest import Snapshot
|
||||||
|
|
||||||
|
|
||||||
|
snapshots = Snapshot()
|
||||||
|
|
||||||
|
snapshots['test_hero_name_query 1'] = {
|
||||||
|
'data': {
|
||||||
|
'hero': {
|
||||||
|
'name': 'R2-D2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_hero_name_and_friends_query 1'] = {
|
||||||
|
'data': {
|
||||||
|
'hero': {
|
||||||
|
'id': '2001',
|
||||||
|
'name': 'R2-D2',
|
||||||
|
'friends': [
|
||||||
|
{
|
||||||
|
'name': 'Luke Skywalker'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Han Solo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Leia Organa'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_nested_query 1'] = {
|
||||||
|
'data': {
|
||||||
|
'hero': {
|
||||||
|
'name': 'R2-D2',
|
||||||
|
'friends': [
|
||||||
|
{
|
||||||
|
'name': 'Luke Skywalker',
|
||||||
|
'appearsIn': [
|
||||||
|
'NEWHOPE',
|
||||||
|
'EMPIRE',
|
||||||
|
'JEDI'
|
||||||
|
],
|
||||||
|
'friends': [
|
||||||
|
{
|
||||||
|
'name': 'Han Solo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Leia Organa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'C-3PO'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'R2-D2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Han Solo',
|
||||||
|
'appearsIn': [
|
||||||
|
'NEWHOPE',
|
||||||
|
'EMPIRE',
|
||||||
|
'JEDI'
|
||||||
|
],
|
||||||
|
'friends': [
|
||||||
|
{
|
||||||
|
'name': 'Luke Skywalker'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Leia Organa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'R2-D2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Leia Organa',
|
||||||
|
'appearsIn': [
|
||||||
|
'NEWHOPE',
|
||||||
|
'EMPIRE',
|
||||||
|
'JEDI'
|
||||||
|
],
|
||||||
|
'friends': [
|
||||||
|
{
|
||||||
|
'name': 'Luke Skywalker'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Han Solo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'C-3PO'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'R2-D2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_fetch_luke_query 1'] = {
|
||||||
|
'data': {
|
||||||
|
'human': {
|
||||||
|
'name': 'Luke Skywalker'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_fetch_some_id_query 1'] = {
|
||||||
|
'data': {
|
||||||
|
'human': {
|
||||||
|
'name': 'Luke Skywalker'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_fetch_some_id_query2 1'] = {
|
||||||
|
'data': {
|
||||||
|
'human': {
|
||||||
|
'name': 'Han Solo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_invalid_id_query 1'] = {
|
||||||
|
'data': {
|
||||||
|
'human': None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_fetch_luke_aliased 1'] = {
|
||||||
|
'data': {
|
||||||
|
'luke': {
|
||||||
|
'name': 'Luke Skywalker'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_fetch_luke_and_leia_aliased 1'] = {
|
||||||
|
'data': {
|
||||||
|
'luke': {
|
||||||
|
'name': 'Luke Skywalker'
|
||||||
|
},
|
||||||
|
'leia': {
|
||||||
|
'name': 'Leia Organa'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_duplicate_fields 1'] = {
|
||||||
|
'data': {
|
||||||
|
'luke': {
|
||||||
|
'name': 'Luke Skywalker',
|
||||||
|
'homePlanet': 'Tatooine'
|
||||||
|
},
|
||||||
|
'leia': {
|
||||||
|
'name': 'Leia Organa',
|
||||||
|
'homePlanet': 'Alderaan'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_use_fragment 1'] = {
|
||||||
|
'data': {
|
||||||
|
'luke': {
|
||||||
|
'name': 'Luke Skywalker',
|
||||||
|
'homePlanet': 'Tatooine'
|
||||||
|
},
|
||||||
|
'leia': {
|
||||||
|
'name': 'Leia Organa',
|
||||||
|
'homePlanet': 'Alderaan'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_check_type_of_r2 1'] = {
|
||||||
|
'data': {
|
||||||
|
'hero': {
|
||||||
|
'__typename': 'Droid',
|
||||||
|
'name': 'R2-D2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_check_type_of_luke 1'] = {
|
||||||
|
'data': {
|
||||||
|
'hero': {
|
||||||
|
'__typename': 'Human',
|
||||||
|
'name': 'Luke Skywalker'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
|
from graphene.test import Client
|
||||||
from ..data import setup
|
from ..data import setup
|
||||||
from ..schema import schema
|
from ..schema import schema
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
|
|
||||||
|
client = Client(schema)
|
||||||
|
|
||||||
def test_hero_name_query():
|
def test_hero_name_query(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query HeroNameQuery {
|
query HeroNameQuery {
|
||||||
hero {
|
hero {
|
||||||
|
@ -13,17 +14,11 @@ def test_hero_name_query():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'hero': {
|
|
||||||
'name': 'R2-D2'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_hero_name_and_friends_query():
|
def test_hero_name_and_friends_query(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query HeroNameAndFriendsQuery {
|
query HeroNameAndFriendsQuery {
|
||||||
hero {
|
hero {
|
||||||
|
@ -35,23 +30,10 @@ def test_hero_name_and_friends_query():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'hero': {
|
|
||||||
'id': '2001',
|
|
||||||
'name': 'R2-D2',
|
|
||||||
'friends': [
|
|
||||||
{'name': 'Luke Skywalker'},
|
|
||||||
{'name': 'Han Solo'},
|
|
||||||
{'name': 'Leia Organa'},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_nested_query():
|
def test_nested_query(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query NestedQuery {
|
query NestedQuery {
|
||||||
hero {
|
hero {
|
||||||
|
@ -66,70 +48,10 @@ def test_nested_query():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'hero': {
|
|
||||||
'name': 'R2-D2',
|
|
||||||
'friends': [
|
|
||||||
{
|
|
||||||
'name': 'Luke Skywalker',
|
|
||||||
'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'],
|
|
||||||
'friends': [
|
|
||||||
{
|
|
||||||
'name': 'Han Solo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Leia Organa',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'C-3PO',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'R2-D2',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Han Solo',
|
|
||||||
'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'],
|
|
||||||
'friends': [
|
|
||||||
{
|
|
||||||
'name': 'Luke Skywalker',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Leia Organa',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'R2-D2',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Leia Organa',
|
|
||||||
'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'],
|
|
||||||
'friends': [
|
|
||||||
{
|
|
||||||
'name': 'Luke Skywalker',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Han Solo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'C-3PO',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'R2-D2',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_luke_query():
|
def test_fetch_luke_query(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query FetchLukeQuery {
|
query FetchLukeQuery {
|
||||||
human(id: "1000") {
|
human(id: "1000") {
|
||||||
|
@ -137,17 +59,10 @@ def test_fetch_luke_query():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'human': {
|
|
||||||
'name': 'Luke Skywalker',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_some_id_query():
|
def test_fetch_some_id_query(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query FetchSomeIDQuery($someId: String!) {
|
query FetchSomeIDQuery($someId: String!) {
|
||||||
human(id: $someId) {
|
human(id: $someId) {
|
||||||
|
@ -158,17 +73,10 @@ def test_fetch_some_id_query():
|
||||||
params = {
|
params = {
|
||||||
'someId': '1000',
|
'someId': '1000',
|
||||||
}
|
}
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query, variable_values=params))
|
||||||
'human': {
|
|
||||||
'name': 'Luke Skywalker',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query, None, variable_values=params)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_some_id_query2():
|
def test_fetch_some_id_query2(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query FetchSomeIDQuery($someId: String!) {
|
query FetchSomeIDQuery($someId: String!) {
|
||||||
human(id: $someId) {
|
human(id: $someId) {
|
||||||
|
@ -179,17 +87,10 @@ def test_fetch_some_id_query2():
|
||||||
params = {
|
params = {
|
||||||
'someId': '1002',
|
'someId': '1002',
|
||||||
}
|
}
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query, variable_values=params))
|
||||||
'human': {
|
|
||||||
'name': 'Han Solo',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query, None, variable_values=params)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_id_query():
|
def test_invalid_id_query(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query humanQuery($id: String!) {
|
query humanQuery($id: String!) {
|
||||||
human(id: $id) {
|
human(id: $id) {
|
||||||
|
@ -200,15 +101,10 @@ def test_invalid_id_query():
|
||||||
params = {
|
params = {
|
||||||
'id': 'not a valid id',
|
'id': 'not a valid id',
|
||||||
}
|
}
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query, variable_values=params))
|
||||||
'human': None
|
|
||||||
}
|
|
||||||
result = schema.execute(query, None, variable_values=params)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_luke_aliased():
|
def test_fetch_luke_aliased(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query FetchLukeAliased {
|
query FetchLukeAliased {
|
||||||
luke: human(id: "1000") {
|
luke: human(id: "1000") {
|
||||||
|
@ -216,17 +112,10 @@ def test_fetch_luke_aliased():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'luke': {
|
|
||||||
'name': 'Luke Skywalker',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_luke_and_leia_aliased():
|
def test_fetch_luke_and_leia_aliased(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query FetchLukeAndLeiaAliased {
|
query FetchLukeAndLeiaAliased {
|
||||||
luke: human(id: "1000") {
|
luke: human(id: "1000") {
|
||||||
|
@ -237,20 +126,10 @@ def test_fetch_luke_and_leia_aliased():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'luke': {
|
|
||||||
'name': 'Luke Skywalker',
|
|
||||||
},
|
|
||||||
'leia': {
|
|
||||||
'name': 'Leia Organa',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate_fields():
|
def test_duplicate_fields(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query DuplicateFields {
|
query DuplicateFields {
|
||||||
luke: human(id: "1000") {
|
luke: human(id: "1000") {
|
||||||
|
@ -263,22 +142,10 @@ def test_duplicate_fields():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'luke': {
|
|
||||||
'name': 'Luke Skywalker',
|
|
||||||
'homePlanet': 'Tatooine',
|
|
||||||
},
|
|
||||||
'leia': {
|
|
||||||
'name': 'Leia Organa',
|
|
||||||
'homePlanet': 'Alderaan',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_use_fragment():
|
def test_use_fragment(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query UseFragment {
|
query UseFragment {
|
||||||
luke: human(id: "1000") {
|
luke: human(id: "1000") {
|
||||||
|
@ -293,22 +160,10 @@ def test_use_fragment():
|
||||||
homePlanet
|
homePlanet
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'luke': {
|
|
||||||
'name': 'Luke Skywalker',
|
|
||||||
'homePlanet': 'Tatooine',
|
|
||||||
},
|
|
||||||
'leia': {
|
|
||||||
'name': 'Leia Organa',
|
|
||||||
'homePlanet': 'Alderaan',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_type_of_r2():
|
def test_check_type_of_r2(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query CheckTypeOfR2 {
|
query CheckTypeOfR2 {
|
||||||
hero {
|
hero {
|
||||||
|
@ -317,18 +172,10 @@ def test_check_type_of_r2():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'hero': {
|
|
||||||
'__typename': 'Droid',
|
|
||||||
'name': 'R2-D2',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_type_of_luke():
|
def test_check_type_of_luke(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query CheckTypeOfLuke {
|
query CheckTypeOfLuke {
|
||||||
hero(episode: EMPIRE) {
|
hero(episode: EMPIRE) {
|
||||||
|
@ -337,12 +184,4 @@ def test_check_type_of_luke():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'hero': {
|
|
||||||
'__typename': 'Human',
|
|
||||||
'name': 'Luke Skywalker',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
0
examples/starwars_relay/tests/snapshots/__init__.py
Normal file
0
examples/starwars_relay/tests/snapshots/__init__.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# snapshottest: v1 - https://goo.gl/zC4yUc
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from snapshottest import Snapshot
|
||||||
|
|
||||||
|
|
||||||
|
snapshots = Snapshot()
|
||||||
|
|
||||||
|
snapshots['test_correct_fetch_first_ship_rebels 1'] = {
|
||||||
|
'data': {
|
||||||
|
'rebels': {
|
||||||
|
'name': 'Alliance to Restore the Republic',
|
||||||
|
'ships': {
|
||||||
|
'pageInfo': {
|
||||||
|
'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
|
||||||
|
'endCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
|
||||||
|
'hasNextPage': True,
|
||||||
|
'hasPreviousPage': False
|
||||||
|
},
|
||||||
|
'edges': [
|
||||||
|
{
|
||||||
|
'cursor': 'YXJyYXljb25uZWN0aW9uOjA=',
|
||||||
|
'node': {
|
||||||
|
'name': 'X-Wing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# snapshottest: v1 - https://goo.gl/zC4yUc
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from snapshottest import Snapshot
|
||||||
|
|
||||||
|
|
||||||
|
snapshots = Snapshot()
|
||||||
|
|
||||||
|
snapshots['test_mutations 1'] = {
|
||||||
|
'data': {
|
||||||
|
'introduceShip': {
|
||||||
|
'ship': {
|
||||||
|
'id': 'U2hpcDo5',
|
||||||
|
'name': 'Peter'
|
||||||
|
},
|
||||||
|
'faction': {
|
||||||
|
'name': 'Alliance to Restore the Republic',
|
||||||
|
'ships': {
|
||||||
|
'edges': [
|
||||||
|
{
|
||||||
|
'node': {
|
||||||
|
'id': 'U2hpcDox',
|
||||||
|
'name': 'X-Wing'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'node': {
|
||||||
|
'id': 'U2hpcDoy',
|
||||||
|
'name': 'Y-Wing'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'node': {
|
||||||
|
'id': 'U2hpcDoz',
|
||||||
|
'name': 'A-Wing'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'node': {
|
||||||
|
'id': 'U2hpcDo0',
|
||||||
|
'name': 'Millenium Falcon'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'node': {
|
||||||
|
'id': 'U2hpcDo1',
|
||||||
|
'name': 'Home One'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'node': {
|
||||||
|
'id': 'U2hpcDo5',
|
||||||
|
'name': 'Peter'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# snapshottest: v1 - https://goo.gl/zC4yUc
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from snapshottest import Snapshot
|
||||||
|
|
||||||
|
|
||||||
|
snapshots = Snapshot()
|
||||||
|
|
||||||
|
snapshots['test_correctly_fetches_id_name_rebels 1'] = {
|
||||||
|
'data': {
|
||||||
|
'rebels': {
|
||||||
|
'id': 'RmFjdGlvbjox',
|
||||||
|
'name': 'Alliance to Restore the Republic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_correctly_refetches_rebels 1'] = {
|
||||||
|
'data': {
|
||||||
|
'node': {
|
||||||
|
'id': 'RmFjdGlvbjox',
|
||||||
|
'name': 'Alliance to Restore the Republic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_correctly_fetches_id_name_empire 1'] = {
|
||||||
|
'data': {
|
||||||
|
'empire': {
|
||||||
|
'id': 'RmFjdGlvbjoy',
|
||||||
|
'name': 'Galactic Empire'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_correctly_refetches_empire 1'] = {
|
||||||
|
'data': {
|
||||||
|
'node': {
|
||||||
|
'id': 'RmFjdGlvbjoy',
|
||||||
|
'name': 'Galactic Empire'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots['test_correctly_refetches_xwing 1'] = {
|
||||||
|
'data': {
|
||||||
|
'node': {
|
||||||
|
'id': 'U2hpcDox',
|
||||||
|
'name': 'X-Wing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
|
from graphene.test import Client
|
||||||
from ..data import setup
|
from ..data import setup
|
||||||
from ..schema import schema
|
from ..schema import schema
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
|
|
||||||
|
client = Client(schema)
|
||||||
|
|
||||||
def test_correct_fetch_first_ship_rebels():
|
|
||||||
|
def test_correct_fetch_first_ship_rebels(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query RebelsShipsQuery {
|
query RebelsShipsQuery {
|
||||||
rebels {
|
rebels {
|
||||||
|
@ -26,27 +29,4 @@ def test_correct_fetch_first_ship_rebels():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'rebels': {
|
|
||||||
'name': 'Alliance to Restore the Republic',
|
|
||||||
'ships': {
|
|
||||||
'pageInfo': {
|
|
||||||
'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
|
|
||||||
'endCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
|
|
||||||
'hasNextPage': True,
|
|
||||||
'hasPreviousPage': False
|
|
||||||
},
|
|
||||||
'edges': [
|
|
||||||
{
|
|
||||||
'cursor': 'YXJyYXljb25uZWN0aW9uOjA=',
|
|
||||||
'node': {
|
|
||||||
'name': 'X-Wing'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
|
from graphene.test import Client
|
||||||
from ..data import setup
|
from ..data import setup
|
||||||
from ..schema import schema
|
from ..schema import schema
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
|
|
||||||
|
client = Client(schema)
|
||||||
|
|
||||||
def test_mutations():
|
|
||||||
|
def test_mutations(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
mutation MyMutation {
|
mutation MyMutation {
|
||||||
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
|
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
|
||||||
|
@ -26,51 +29,4 @@ def test_mutations():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'introduceShip': {
|
|
||||||
'ship': {
|
|
||||||
'id': 'U2hpcDo5',
|
|
||||||
'name': 'Peter'
|
|
||||||
},
|
|
||||||
'faction': {
|
|
||||||
'name': 'Alliance to Restore the Republic',
|
|
||||||
'ships': {
|
|
||||||
'edges': [{
|
|
||||||
'node': {
|
|
||||||
'id': 'U2hpcDox',
|
|
||||||
'name': 'X-Wing'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
'node': {
|
|
||||||
'id': 'U2hpcDoy',
|
|
||||||
'name': 'Y-Wing'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
'node': {
|
|
||||||
'id': 'U2hpcDoz',
|
|
||||||
'name': 'A-Wing'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
'node': {
|
|
||||||
'id': 'U2hpcDo0',
|
|
||||||
'name': 'Millenium Falcon'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
'node': {
|
|
||||||
'id': 'U2hpcDo1',
|
|
||||||
'name': 'Home One'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
'node': {
|
|
||||||
'id': 'U2hpcDo5',
|
|
||||||
'name': 'Peter'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
# raise result.errors[0].original_error, None, result.errors[0].stack
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
from graphene.test import Client
|
||||||
from ..data import setup
|
from ..data import setup
|
||||||
from ..schema import schema
|
from ..schema import schema
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
|
|
||||||
|
client = Client(schema)
|
||||||
|
|
||||||
|
|
||||||
def test_str_schema():
|
def test_str_schema():
|
||||||
assert str(schema) == '''schema {
|
assert str(schema) == '''schema {
|
||||||
|
@ -66,7 +69,7 @@ type ShipEdge {
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
def test_correctly_fetches_id_name_rebels():
|
def test_correctly_fetches_id_name_rebels(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query RebelsQuery {
|
query RebelsQuery {
|
||||||
rebels {
|
rebels {
|
||||||
|
@ -75,18 +78,10 @@ def test_correctly_fetches_id_name_rebels():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'rebels': {
|
|
||||||
'id': 'RmFjdGlvbjox',
|
|
||||||
'name': 'Alliance to Restore the Republic'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_correctly_refetches_rebels():
|
def test_correctly_refetches_rebels(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query RebelsRefetchQuery {
|
query RebelsRefetchQuery {
|
||||||
node(id: "RmFjdGlvbjox") {
|
node(id: "RmFjdGlvbjox") {
|
||||||
|
@ -97,18 +92,10 @@ def test_correctly_refetches_rebels():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'node': {
|
|
||||||
'id': 'RmFjdGlvbjox',
|
|
||||||
'name': 'Alliance to Restore the Republic'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_correctly_fetches_id_name_empire():
|
def test_correctly_fetches_id_name_empire(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query EmpireQuery {
|
query EmpireQuery {
|
||||||
empire {
|
empire {
|
||||||
|
@ -117,18 +104,10 @@ def test_correctly_fetches_id_name_empire():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'empire': {
|
|
||||||
'id': 'RmFjdGlvbjoy',
|
|
||||||
'name': 'Galactic Empire'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_correctly_refetches_empire():
|
def test_correctly_refetches_empire(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query EmpireRefetchQuery {
|
query EmpireRefetchQuery {
|
||||||
node(id: "RmFjdGlvbjoy") {
|
node(id: "RmFjdGlvbjoy") {
|
||||||
|
@ -139,18 +118,10 @@ def test_correctly_refetches_empire():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'node': {
|
|
||||||
'id': 'RmFjdGlvbjoy',
|
|
||||||
'name': 'Galactic Empire'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_correctly_refetches_xwing():
|
def test_correctly_refetches_xwing(snapshot):
|
||||||
query = '''
|
query = '''
|
||||||
query XWingRefetchQuery {
|
query XWingRefetchQuery {
|
||||||
node(id: "U2hpcDox") {
|
node(id: "U2hpcDox") {
|
||||||
|
@ -161,12 +132,4 @@ def test_correctly_refetches_xwing():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
expected = {
|
snapshot.assert_match(client.execute(query))
|
||||||
'node': {
|
|
||||||
'id': 'U2hpcDox',
|
|
||||||
'name': 'X-Wing'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert not result.errors
|
|
||||||
assert result.data == expected
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ except NameError:
|
||||||
__SETUP__ = False
|
__SETUP__ = False
|
||||||
|
|
||||||
|
|
||||||
VERSION = (1, 1, 3, 'final', 0)
|
VERSION = (1, 4, 0, 'final', 0)
|
||||||
|
|
||||||
__version__ = get_version(VERSION)
|
__version__ = get_version(VERSION)
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ if not __SETUP__:
|
||||||
PageInfo
|
PageInfo
|
||||||
)
|
)
|
||||||
from .utils.resolve_only_args import resolve_only_args
|
from .utils.resolve_only_args import resolve_only_args
|
||||||
|
from .utils.module_loading import lazy_import
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AbstractType',
|
'AbstractType',
|
||||||
|
@ -72,4 +73,6 @@ if not __SETUP__:
|
||||||
'ClientIDMutation',
|
'ClientIDMutation',
|
||||||
'Connection',
|
'Connection',
|
||||||
'ConnectionField',
|
'ConnectionField',
|
||||||
'PageInfo']
|
'PageInfo',
|
||||||
|
'lazy_import',
|
||||||
|
]
|
||||||
|
|
|
@ -5,7 +5,7 @@ from functools import partial
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from graphql_relay import connection_from_list
|
from graphql_relay import connection_from_list
|
||||||
from promise import is_thenable, promisify
|
from promise import Promise, is_thenable
|
||||||
|
|
||||||
from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String,
|
from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String,
|
||||||
Union)
|
Union)
|
||||||
|
@ -143,7 +143,7 @@ class IterableConnectionField(Field):
|
||||||
|
|
||||||
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 promisify(resolved).then(on_resolve)
|
return Promise.resolve(resolved).then(on_resolve)
|
||||||
|
|
||||||
return on_resolve(resolved)
|
return on_resolve(resolved)
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import six
|
||||||
from graphql_relay import from_global_id, to_global_id
|
from graphql_relay import from_global_id, to_global_id
|
||||||
|
|
||||||
from ..types import ID, Field, Interface, ObjectType
|
from ..types import ID, Field, Interface, ObjectType
|
||||||
|
from ..types.utils import get_type
|
||||||
from ..types.interface import InterfaceMeta
|
from ..types.interface import InterfaceMeta
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,17 +65,18 @@ class NodeField(Field):
|
||||||
name=None, **kwargs):
|
name=None, **kwargs):
|
||||||
assert issubclass(node, Node), 'NodeField can only operate in Nodes'
|
assert issubclass(node, Node), 'NodeField can only operate in Nodes'
|
||||||
self.node_type = node
|
self.node_type = node
|
||||||
|
self.field_type = type
|
||||||
# If we don's specify a type, the field type will be the node interface
|
|
||||||
field_type = type or node
|
|
||||||
|
|
||||||
super(NodeField, self).__init__(
|
super(NodeField, self).__init__(
|
||||||
field_type,
|
# If we don's specify a type, the field type will be the node interface
|
||||||
|
type or node,
|
||||||
description='The ID of the object',
|
description='The ID of the object',
|
||||||
id=ID(required=True),
|
id=ID(required=True)
|
||||||
resolver=partial(node.node_resolver, only_type=type)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_resolver(self, parent_resolver):
|
||||||
|
return partial(self.node_type.node_resolver, only_type=get_type(self.field_type))
|
||||||
|
|
||||||
|
|
||||||
class Node(six.with_metaclass(NodeMeta, Interface)):
|
class Node(six.with_metaclass(NodeMeta, Interface)):
|
||||||
'''An object with an ID'''
|
'''An object with an ID'''
|
||||||
|
|
|
@ -45,6 +45,7 @@ class RootQuery(ObjectType):
|
||||||
first = String()
|
first = String()
|
||||||
node = Node.Field()
|
node = Node.Field()
|
||||||
only_node = Node.Field(MyNode)
|
only_node = Node.Field(MyNode)
|
||||||
|
only_node_lazy = Node.Field(lambda: MyNode)
|
||||||
|
|
||||||
schema = Schema(query=RootQuery, types=[MyNode, MyOtherNode])
|
schema = Schema(query=RootQuery, types=[MyNode, MyOtherNode])
|
||||||
|
|
||||||
|
@ -116,6 +117,23 @@ def test_node_field_only_type_wrong():
|
||||||
assert executed.data == { 'onlyNode': None }
|
assert executed.data == { 'onlyNode': None }
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_field_only_lazy_type():
|
||||||
|
executed = schema.execute(
|
||||||
|
'{ onlyNodeLazy(id:"%s") { __typename, name } } ' % Node.to_global_id("MyNode", 1)
|
||||||
|
)
|
||||||
|
assert not executed.errors
|
||||||
|
assert executed.data == {'onlyNodeLazy': {'__typename': 'MyNode', 'name': '1'}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_field_only_lazy_type_wrong():
|
||||||
|
executed = schema.execute(
|
||||||
|
'{ onlyNodeLazy(id:"%s") { __typename, name } } ' % Node.to_global_id("MyOtherNode", 1)
|
||||||
|
)
|
||||||
|
assert len(executed.errors) == 1
|
||||||
|
assert str(executed.errors[0]) == 'Must receive an MyOtherNode id.'
|
||||||
|
assert executed.data == { 'onlyNodeLazy': None }
|
||||||
|
|
||||||
|
|
||||||
def test_str_schema():
|
def test_str_schema():
|
||||||
assert str(schema) == """
|
assert str(schema) == """
|
||||||
schema {
|
schema {
|
||||||
|
@ -142,5 +160,6 @@ type RootQuery {
|
||||||
first: String
|
first: String
|
||||||
node(id: ID!): Node
|
node(id: ID!): Node
|
||||||
onlyNode(id: ID!): MyNode
|
onlyNode(id: ID!): MyNode
|
||||||
|
onlyNodeLazy(id: ID!): MyNode
|
||||||
}
|
}
|
||||||
""".lstrip()
|
""".lstrip()
|
||||||
|
|
39
graphene/test/__init__.py
Normal file
39
graphene/test/__init__.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import six
|
||||||
|
from graphql.error import format_error as format_graphql_error
|
||||||
|
from graphql.error import GraphQLError
|
||||||
|
|
||||||
|
from graphene.types.schema import Schema
|
||||||
|
|
||||||
|
|
||||||
|
def default_format_error(error):
|
||||||
|
if isinstance(error, GraphQLError):
|
||||||
|
return format_graphql_error(error)
|
||||||
|
|
||||||
|
return {'message': six.text_type(error)}
|
||||||
|
|
||||||
|
|
||||||
|
def format_execution_result(execution_result, format_error):
|
||||||
|
if execution_result:
|
||||||
|
response = {}
|
||||||
|
|
||||||
|
if execution_result.errors:
|
||||||
|
response['errors'] = [format_error(e) for e in execution_result.errors]
|
||||||
|
|
||||||
|
if not execution_result.invalid:
|
||||||
|
response['data'] = execution_result.data
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class Client(object):
|
||||||
|
def __init__(self, schema, format_error=None, **execute_options):
|
||||||
|
assert isinstance(schema, Schema)
|
||||||
|
self.schema = schema
|
||||||
|
self.execute_options = execute_options
|
||||||
|
self.format_error = format_error or default_format_error
|
||||||
|
|
||||||
|
def execute(self, *args, **kwargs):
|
||||||
|
return format_execution_result(
|
||||||
|
self.schema.execute(*args, **dict(self.execute_options, **kwargs)),
|
||||||
|
self.format_error
|
||||||
|
)
|
|
@ -3,6 +3,9 @@
|
||||||
import graphene
|
import graphene
|
||||||
from graphene import resolve_only_args
|
from graphene import resolve_only_args
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
rand = graphene.String()
|
||||||
|
|
||||||
class Success(graphene.ObjectType):
|
class Success(graphene.ObjectType):
|
||||||
yeah = graphene.String()
|
yeah = graphene.String()
|
||||||
|
|
||||||
|
@ -45,7 +48,7 @@ def test_create_post():
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
schema = graphene.Schema(mutation=Mutations)
|
schema = graphene.Schema(query=Query, mutation=Mutations)
|
||||||
result = schema.execute(query_string)
|
result = schema.execute(query_string)
|
||||||
|
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
|
|
53
graphene/tests/issues/test_425.py
Normal file
53
graphene/tests/issues/test_425.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# https://github.com/graphql-python/graphene/issues/425
|
||||||
|
import six
|
||||||
|
|
||||||
|
from graphene.utils.is_base_type import is_base_type
|
||||||
|
|
||||||
|
from graphene.types.objecttype import ObjectTypeMeta, ObjectType
|
||||||
|
from graphene.types.options import Options
|
||||||
|
|
||||||
|
class SpecialObjectTypeMeta(ObjectTypeMeta):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __new__(cls, name, bases, attrs):
|
||||||
|
# Also ensure initialization is only performed for subclasses of
|
||||||
|
# DjangoObjectType
|
||||||
|
if not is_base_type(bases, SpecialObjectTypeMeta):
|
||||||
|
return type.__new__(cls, name, bases, attrs)
|
||||||
|
|
||||||
|
options = Options(
|
||||||
|
attrs.pop('Meta', None),
|
||||||
|
other_attr='default',
|
||||||
|
)
|
||||||
|
|
||||||
|
cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options))
|
||||||
|
assert cls._meta is options
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialObjectType(six.with_metaclass(SpecialObjectTypeMeta, ObjectType)):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_special_objecttype_could_be_subclassed():
|
||||||
|
class MyType(SpecialObjectType):
|
||||||
|
class Meta:
|
||||||
|
other_attr = 'yeah!'
|
||||||
|
|
||||||
|
assert MyType._meta.other_attr == 'yeah!'
|
||||||
|
|
||||||
|
|
||||||
|
def test_special_objecttype_could_be_subclassed_default():
|
||||||
|
class MyType(SpecialObjectType):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert MyType._meta.other_attr == 'default'
|
||||||
|
|
||||||
|
|
||||||
|
def test_special_objecttype_inherit_meta_options():
|
||||||
|
class MyType(SpecialObjectType):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert MyType._meta.name == 'MyType'
|
||||||
|
assert MyType._meta.default_resolver == None
|
||||||
|
assert MyType._meta.interfaces == ()
|
|
@ -4,6 +4,7 @@ from itertools import chain
|
||||||
from .mountedtype import MountedType
|
from .mountedtype import MountedType
|
||||||
from .structures import NonNull
|
from .structures import NonNull
|
||||||
from .dynamic import Dynamic
|
from .dynamic import Dynamic
|
||||||
|
from .utils import get_type
|
||||||
|
|
||||||
|
|
||||||
class Argument(MountedType):
|
class Argument(MountedType):
|
||||||
|
@ -15,10 +16,14 @@ class Argument(MountedType):
|
||||||
type = NonNull(type)
|
type = NonNull(type)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.type = type
|
self._type = type
|
||||||
self.default_value = default_value
|
self.default_value = default_value
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
return get_type(self._type)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return isinstance(other, Argument) and (
|
return isinstance(other, Argument) and (
|
||||||
self.name == other.name,
|
self.name == other.name,
|
||||||
|
|
|
@ -9,10 +9,13 @@ class Dynamic(MountedType):
|
||||||
the schema. So we can have lazy fields.
|
the schema. So we can have lazy fields.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, type, _creation_counter=None):
|
def __init__(self, type, with_schema=False, _creation_counter=None):
|
||||||
super(Dynamic, self).__init__(_creation_counter=_creation_counter)
|
super(Dynamic, self).__init__(_creation_counter=_creation_counter)
|
||||||
assert inspect.isfunction(type)
|
assert inspect.isfunction(type)
|
||||||
self.type = type
|
self.type = type
|
||||||
|
self.with_schema = with_schema
|
||||||
|
|
||||||
def get_type(self):
|
def get_type(self, schema=None):
|
||||||
|
if schema and self.with_schema:
|
||||||
|
return self.type(schema=schema)
|
||||||
return self.type()
|
return self.type()
|
||||||
|
|
|
@ -3,6 +3,7 @@ from collections import OrderedDict
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ..utils.is_base_type import is_base_type
|
from ..utils.is_base_type import is_base_type
|
||||||
|
from ..utils.trim_docstring import trim_docstring
|
||||||
from .options import Options
|
from .options import Options
|
||||||
from .unmountedtype import UnmountedType
|
from .unmountedtype import UnmountedType
|
||||||
|
|
||||||
|
@ -12,6 +13,12 @@ except ImportError:
|
||||||
from ..pyutils.enum import Enum as PyEnum
|
from ..pyutils.enum import Enum as PyEnum
|
||||||
|
|
||||||
|
|
||||||
|
def eq_enum(self, other):
|
||||||
|
if isinstance(other, self.__class__):
|
||||||
|
return self is other
|
||||||
|
return self.value is other
|
||||||
|
|
||||||
|
|
||||||
class EnumTypeMeta(type):
|
class EnumTypeMeta(type):
|
||||||
|
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(cls, name, bases, attrs):
|
||||||
|
@ -23,10 +30,11 @@ class EnumTypeMeta(type):
|
||||||
options = Options(
|
options = Options(
|
||||||
attrs.pop('Meta', None),
|
attrs.pop('Meta', None),
|
||||||
name=name,
|
name=name,
|
||||||
description=attrs.get('__doc__'),
|
description=trim_docstring(attrs.get('__doc__')),
|
||||||
enum=None,
|
enum=None,
|
||||||
)
|
)
|
||||||
if not options.enum:
|
if not options.enum:
|
||||||
|
attrs['__eq__'] = eq_enum
|
||||||
options.enum = PyEnum(cls.__name__, attrs)
|
options.enum = PyEnum(cls.__name__, attrs)
|
||||||
|
|
||||||
new_attrs = OrderedDict(attrs, _meta=options, **options.enum.__members__)
|
new_attrs = OrderedDict(attrs, _meta=options, **options.enum.__members__)
|
||||||
|
@ -35,11 +43,18 @@ class EnumTypeMeta(type):
|
||||||
def __prepare__(name, bases, **kwargs): # noqa: N805
|
def __prepare__(name, bases, **kwargs): # noqa: N805
|
||||||
return OrderedDict()
|
return OrderedDict()
|
||||||
|
|
||||||
|
def get(cls, value):
|
||||||
|
return cls._meta.enum(value)
|
||||||
|
|
||||||
|
def __getitem__(cls, value):
|
||||||
|
return cls._meta.enum[value]
|
||||||
|
|
||||||
def __call__(cls, *args, **kwargs): # noqa: N805
|
def __call__(cls, *args, **kwargs): # noqa: N805
|
||||||
if cls is Enum:
|
if cls is Enum:
|
||||||
description = kwargs.pop('description', None)
|
description = kwargs.pop('description', None)
|
||||||
return cls.from_enum(PyEnum(*args, **kwargs), description=description)
|
return cls.from_enum(PyEnum(*args, **kwargs), description=description)
|
||||||
return super(EnumTypeMeta, cls).__call__(*args, **kwargs)
|
return super(EnumTypeMeta, cls).__call__(*args, **kwargs)
|
||||||
|
# return cls._meta.enum(*args, **kwargs)
|
||||||
|
|
||||||
def from_enum(cls, enum, description=None): # noqa: N805
|
def from_enum(cls, enum, description=None): # noqa: N805
|
||||||
meta_class = type('Meta', (object,), {'enum': enum, 'description': description})
|
meta_class = type('Meta', (object,), {'enum': enum, 'description': description})
|
||||||
|
|
|
@ -6,6 +6,7 @@ from .argument import Argument, to_arguments
|
||||||
from .mountedtype import MountedType
|
from .mountedtype import MountedType
|
||||||
from .structures import NonNull
|
from .structures import NonNull
|
||||||
from .unmountedtype import UnmountedType
|
from .unmountedtype import UnmountedType
|
||||||
|
from .utils import get_type
|
||||||
|
|
||||||
|
|
||||||
base_type = type
|
base_type = type
|
||||||
|
@ -60,9 +61,7 @@ class Field(MountedType):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
if inspect.isfunction(self._type) or type(self._type) is partial:
|
return get_type(self._type)
|
||||||
return self._type()
|
|
||||||
return self._type
|
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
def get_resolver(self, parent_resolver):
|
||||||
return self.resolver or parent_resolver
|
return self.resolver or parent_resolver
|
||||||
|
|
39
graphene/types/generic.py
Normal file
39
graphene/types/generic.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from graphql.language.ast import (BooleanValue, FloatValue, IntValue,
|
||||||
|
StringValue, ListValue, ObjectValue)
|
||||||
|
|
||||||
|
from graphene.types.scalars import MIN_INT, MAX_INT
|
||||||
|
from .scalars import Scalar
|
||||||
|
|
||||||
|
|
||||||
|
class GenericScalar(Scalar):
|
||||||
|
"""
|
||||||
|
The `GenericScalar` scalar type represents a generic
|
||||||
|
GraphQL scalar value that could be:
|
||||||
|
String, Boolean, Int, Float, List or Object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def identity(value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
serialize = identity
|
||||||
|
parse_value = identity
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_literal(ast):
|
||||||
|
if isinstance(ast, (StringValue, BooleanValue)):
|
||||||
|
return ast.value
|
||||||
|
elif isinstance(ast, IntValue):
|
||||||
|
num = int(ast.value)
|
||||||
|
if MIN_INT <= num <= MAX_INT:
|
||||||
|
return num
|
||||||
|
elif isinstance(ast, FloatValue):
|
||||||
|
return float(ast.value)
|
||||||
|
elif isinstance(ast, ListValue):
|
||||||
|
return [GenericScalar.parse_literal(value) for value in ast.values]
|
||||||
|
elif isinstance(ast, ObjectValue):
|
||||||
|
return {field.name.value: GenericScalar.parse_literal(field.value) for field in ast.fields}
|
||||||
|
else:
|
||||||
|
return None
|
|
@ -1,5 +1,6 @@
|
||||||
from .mountedtype import MountedType
|
from .mountedtype import MountedType
|
||||||
from .structures import NonNull
|
from .structures import NonNull
|
||||||
|
from .utils import get_type
|
||||||
|
|
||||||
|
|
||||||
class InputField(MountedType):
|
class InputField(MountedType):
|
||||||
|
@ -11,7 +12,11 @@ class InputField(MountedType):
|
||||||
self.name = name
|
self.name = name
|
||||||
if required:
|
if required:
|
||||||
type = NonNull(type)
|
type = NonNull(type)
|
||||||
self.type = type
|
self._type = type
|
||||||
self.deprecation_reason = deprecation_reason
|
self.deprecation_reason = deprecation_reason
|
||||||
self.default_value = default_value
|
self.default_value = default_value
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
return get_type(self._type)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ..utils.is_base_type import is_base_type
|
from ..utils.is_base_type import is_base_type
|
||||||
|
from ..utils.trim_docstring import trim_docstring
|
||||||
from .abstracttype import AbstractTypeMeta
|
from .abstracttype import AbstractTypeMeta
|
||||||
from .inputfield import InputField
|
from .inputfield import InputField
|
||||||
from .options import Options
|
from .options import Options
|
||||||
|
@ -19,7 +20,7 @@ class InputObjectTypeMeta(AbstractTypeMeta):
|
||||||
options = Options(
|
options = Options(
|
||||||
attrs.pop('Meta', None),
|
attrs.pop('Meta', None),
|
||||||
name=name,
|
name=name,
|
||||||
description=attrs.get('__doc__'),
|
description=trim_docstring(attrs.get('__doc__')),
|
||||||
local_fields=None,
|
local_fields=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ..utils.is_base_type import is_base_type
|
from ..utils.is_base_type import is_base_type
|
||||||
|
from ..utils.trim_docstring import trim_docstring
|
||||||
from .abstracttype import AbstractTypeMeta
|
from .abstracttype import AbstractTypeMeta
|
||||||
from .field import Field
|
from .field import Field
|
||||||
from .options import Options
|
from .options import Options
|
||||||
|
@ -18,7 +19,7 @@ class InterfaceMeta(AbstractTypeMeta):
|
||||||
options = Options(
|
options = Options(
|
||||||
attrs.pop('Meta', None),
|
attrs.pop('Meta', None),
|
||||||
name=name,
|
name=name,
|
||||||
description=attrs.get('__doc__'),
|
description=trim_docstring(attrs.get('__doc__')),
|
||||||
local_fields=None,
|
local_fields=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from collections import OrderedDict
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ..utils.is_base_type import is_base_type
|
from ..utils.is_base_type import is_base_type
|
||||||
|
from ..utils.trim_docstring import trim_docstring
|
||||||
from .abstracttype import AbstractTypeMeta
|
from .abstracttype import AbstractTypeMeta
|
||||||
from .field import Field
|
from .field import Field
|
||||||
from .interface import Interface
|
from .interface import Interface
|
||||||
|
@ -19,13 +20,22 @@ class ObjectTypeMeta(AbstractTypeMeta):
|
||||||
return type.__new__(cls, name, bases, attrs)
|
return type.__new__(cls, name, bases, attrs)
|
||||||
|
|
||||||
_meta = attrs.pop('_meta', None)
|
_meta = attrs.pop('_meta', None)
|
||||||
options = _meta or Options(
|
defaults = dict(
|
||||||
attrs.pop('Meta', None),
|
|
||||||
name=name,
|
name=name,
|
||||||
description=attrs.get('__doc__'),
|
description=trim_docstring(attrs.get('__doc__')),
|
||||||
interfaces=(),
|
interfaces=(),
|
||||||
|
possible_types=(),
|
||||||
|
default_resolver=None,
|
||||||
local_fields=OrderedDict(),
|
local_fields=OrderedDict(),
|
||||||
)
|
)
|
||||||
|
if not _meta:
|
||||||
|
options = Options(
|
||||||
|
attrs.pop('Meta', None),
|
||||||
|
**defaults
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
options = _meta.extend_with_defaults(defaults)
|
||||||
|
|
||||||
options.base_fields = get_base_fields(bases, _as=Field)
|
options.base_fields = get_base_fields(bases, _as=Field)
|
||||||
|
|
||||||
if not options.local_fields:
|
if not options.local_fields:
|
||||||
|
@ -46,6 +56,11 @@ class ObjectTypeMeta(AbstractTypeMeta):
|
||||||
|
|
||||||
cls = type.__new__(cls, name, bases, dict(attrs, _meta=options))
|
cls = type.__new__(cls, name, bases, dict(attrs, _meta=options))
|
||||||
|
|
||||||
|
assert not (options.possible_types and cls.is_type_of), (
|
||||||
|
'{}.Meta.possible_types will cause type collision with {}.is_type_of. '
|
||||||
|
'Please use one or other.'
|
||||||
|
).format(name, name)
|
||||||
|
|
||||||
for interface in options.interfaces:
|
for interface in options.interfaces:
|
||||||
interface.implements(cls)
|
interface.implements(cls)
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,12 @@ class Options(object):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def extend_with_defaults(self, defaults):
|
||||||
|
for attr_name, value in defaults.items():
|
||||||
|
if not hasattr(self, attr_name):
|
||||||
|
setattr(self, attr_name, value)
|
||||||
|
return self
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
options_props = props(self)
|
options_props = props(self)
|
||||||
props_as_attrs = ' '.join(['{}={}'.format(key, value) for key, value in options_props.items()])
|
props_as_attrs = ' '.join(['{}={}'.format(key, value) for key, value in options_props.items()])
|
||||||
|
|
19
graphene/types/resolver.py
Normal file
19
graphene/types/resolver.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
def attr_resolver(attname, default_value, root, args, context, info):
|
||||||
|
return getattr(root, attname, default_value)
|
||||||
|
|
||||||
|
|
||||||
|
def dict_resolver(attname, default_value, root, args, context, info):
|
||||||
|
return root.get(attname, default_value)
|
||||||
|
|
||||||
|
|
||||||
|
default_resolver = attr_resolver
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_resolver(resolver):
|
||||||
|
global default_resolver
|
||||||
|
assert callable(resolver), 'Received non-callable resolver.'
|
||||||
|
default_resolver = resolver
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_resolver():
|
||||||
|
return default_resolver
|
|
@ -1,9 +1,9 @@
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from graphql.language.ast import (BooleanValue, FloatValue, IntValue,
|
from graphql.language.ast import (BooleanValue, FloatValue, IntValue,
|
||||||
StringValue)
|
StringValue)
|
||||||
|
|
||||||
from ..utils.is_base_type import is_base_type
|
from ..utils.is_base_type import is_base_type
|
||||||
|
from ..utils.trim_docstring import trim_docstring
|
||||||
from .options import Options
|
from .options import Options
|
||||||
from .unmountedtype import UnmountedType
|
from .unmountedtype import UnmountedType
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class ScalarTypeMeta(type):
|
||||||
options = Options(
|
options = Options(
|
||||||
attrs.pop('Meta', None),
|
attrs.pop('Meta', None),
|
||||||
name=name,
|
name=name,
|
||||||
description=attrs.get('__doc__'),
|
description=trim_docstring(attrs.get('__doc__')),
|
||||||
)
|
)
|
||||||
|
|
||||||
return type.__new__(cls, name, bases, dict(attrs, _meta=options))
|
return type.__new__(cls, name, bases, dict(attrs, _meta=options))
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import inspect
|
||||||
|
|
||||||
from graphql import GraphQLSchema, graphql, is_type
|
from graphql import GraphQLSchema, graphql, is_type
|
||||||
from graphql.type.directives import (GraphQLDirective, GraphQLIncludeDirective,
|
from graphql.type.directives import (GraphQLDirective, GraphQLIncludeDirective,
|
||||||
|
@ -7,6 +8,7 @@ from graphql.utils.introspection_query import introspection_query
|
||||||
from graphql.utils.schema_printer import print_schema
|
from graphql.utils.schema_printer import print_schema
|
||||||
|
|
||||||
from .definitions import GrapheneGraphQLType
|
from .definitions import GrapheneGraphQLType
|
||||||
|
from .objecttype import ObjectType
|
||||||
from .typemap import TypeMap, is_graphene_type
|
from .typemap import TypeMap, is_graphene_type
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +22,9 @@ class Schema(GraphQLSchema):
|
||||||
|
|
||||||
def __init__(self, query=None, mutation=None, subscription=None,
|
def __init__(self, query=None, mutation=None, subscription=None,
|
||||||
directives=None, types=None, auto_camelcase=True):
|
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)
|
||||||
self._query = query
|
self._query = query
|
||||||
self._mutation = mutation
|
self._mutation = mutation
|
||||||
self._subscription = subscription
|
self._subscription = subscription
|
||||||
|
@ -77,7 +82,10 @@ class Schema(GraphQLSchema):
|
||||||
return graphql(self, *args, **kwargs)
|
return graphql(self, *args, **kwargs)
|
||||||
|
|
||||||
def introspect(self):
|
def introspect(self):
|
||||||
return self.execute(introspection_query).data
|
instrospection = self.execute(introspection_query)
|
||||||
|
if instrospection.errors:
|
||||||
|
raise instrospection.errors[0]
|
||||||
|
return instrospection.data
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return print_schema(self)
|
return print_schema(self)
|
||||||
|
@ -94,4 +102,4 @@ 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)
|
self._type_map = TypeMap(initial_types, auto_camelcase=self.auto_camelcase, schema=self)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from .unmountedtype import UnmountedType
|
from .unmountedtype import UnmountedType
|
||||||
|
from .utils import get_type
|
||||||
|
|
||||||
|
|
||||||
class Structure(UnmountedType):
|
class Structure(UnmountedType):
|
||||||
|
@ -18,7 +19,11 @@ class Structure(UnmountedType):
|
||||||
cls_name,
|
cls_name,
|
||||||
of_type_name,
|
of_type_name,
|
||||||
))
|
))
|
||||||
self.of_type = of_type
|
self._of_type = of_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def of_type(self):
|
||||||
|
return get_type(self._of_type)
|
||||||
|
|
||||||
def get_type(self):
|
def get_type(self):
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from ..argument import Argument, to_arguments
|
from ..argument import Argument, to_arguments
|
||||||
from ..field import Field
|
from ..field import Field
|
||||||
|
@ -48,7 +49,7 @@ def test_to_arguments_raises_if_field():
|
||||||
|
|
||||||
with pytest.raises(ValueError) as exc_info:
|
with pytest.raises(ValueError) as exc_info:
|
||||||
to_arguments(args)
|
to_arguments(args)
|
||||||
|
|
||||||
assert str(exc_info.value) == 'Expected arg_string to be Argument, but received Field. Try using Argument(String).'
|
assert str(exc_info.value) == 'Expected arg_string to be Argument, but received Field. Try using Argument(String).'
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,5 +60,17 @@ def test_to_arguments_raises_if_inputfield():
|
||||||
|
|
||||||
with pytest.raises(ValueError) as exc_info:
|
with pytest.raises(ValueError) as exc_info:
|
||||||
to_arguments(args)
|
to_arguments(args)
|
||||||
|
|
||||||
assert str(exc_info.value) == 'Expected arg_string to be Argument, but received InputField. Try using Argument(String).'
|
assert str(exc_info.value) == 'Expected arg_string to be Argument, but received InputField. Try using Argument(String).'
|
||||||
|
|
||||||
|
|
||||||
|
def test_argument_with_lazy_type():
|
||||||
|
MyType = object()
|
||||||
|
arg = Argument(lambda: MyType)
|
||||||
|
assert arg.type == MyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_argument_with_lazy_partial_type():
|
||||||
|
MyType = object()
|
||||||
|
arg = Argument(partial(lambda: MyType))
|
||||||
|
assert arg.type == MyType
|
|
@ -111,3 +111,52 @@ def test_enum_value_as_unmounted_argument():
|
||||||
unmounted_field = unmounted.Argument()
|
unmounted_field = unmounted.Argument()
|
||||||
assert isinstance(unmounted_field, Argument)
|
assert isinstance(unmounted_field, Argument)
|
||||||
assert unmounted_field.type == RGB
|
assert unmounted_field.type == RGB
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_can_be_compared():
|
||||||
|
class RGB(Enum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
assert RGB.RED == 1
|
||||||
|
assert RGB.GREEN == 2
|
||||||
|
assert RGB.BLUE == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_can_be_initialzied():
|
||||||
|
class RGB(Enum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
assert RGB.get(1) == RGB.RED
|
||||||
|
assert RGB.get(2) == RGB.GREEN
|
||||||
|
assert RGB.get(3) == RGB.BLUE
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_can_retrieve_members():
|
||||||
|
class RGB(Enum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
assert RGB['RED'] == RGB.RED
|
||||||
|
assert RGB['GREEN'] == RGB.GREEN
|
||||||
|
assert RGB['BLUE'] == RGB.BLUE
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_to_enum_comparison_should_differ():
|
||||||
|
class RGB1(Enum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
class RGB2(Enum):
|
||||||
|
RED = 1
|
||||||
|
GREEN = 2
|
||||||
|
BLUE = 3
|
||||||
|
|
||||||
|
assert RGB1.RED != RGB2.RED
|
||||||
|
assert RGB1.GREEN != RGB2.GREEN
|
||||||
|
assert RGB1.BLUE != RGB2.BLUE
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from ..argument import Argument
|
from ..argument import Argument
|
||||||
from ..field import Field
|
from ..field import Field
|
||||||
from ..structures import NonNull
|
from ..structures import NonNull
|
||||||
from ..scalars import String
|
from ..scalars import String
|
||||||
|
from .utils import MyLazyType
|
||||||
|
|
||||||
|
|
||||||
class MyInstance(object):
|
class MyInstance(object):
|
||||||
|
@ -66,6 +68,17 @@ def test_field_with_lazy_type():
|
||||||
assert field.type == MyType
|
assert field.type == MyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_with_lazy_partial_type():
|
||||||
|
MyType = object()
|
||||||
|
field = Field(partial(lambda: MyType))
|
||||||
|
assert field.type == MyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_with_string_type():
|
||||||
|
field = Field("graphene.types.tests.utils.MyLazyType")
|
||||||
|
assert field.type == MyLazyType
|
||||||
|
|
||||||
|
|
||||||
def test_field_not_source_and_resolver():
|
def test_field_not_source_and_resolver():
|
||||||
MyType = object()
|
MyType = object()
|
||||||
with pytest.raises(Exception) as exc_info:
|
with pytest.raises(Exception) as exc_info:
|
||||||
|
|
96
graphene/types/tests/test_generic.py
Normal file
96
graphene/types/tests/test_generic.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
from ..generic import GenericScalar
|
||||||
|
from ..objecttype import ObjectType
|
||||||
|
from ..schema import Schema
|
||||||
|
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
generic = GenericScalar(input=GenericScalar())
|
||||||
|
|
||||||
|
def resolve_generic(self, args, context, info):
|
||||||
|
input = args.get('input')
|
||||||
|
return input
|
||||||
|
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generic_query_variable():
|
||||||
|
for generic_value in [
|
||||||
|
1,
|
||||||
|
1.1,
|
||||||
|
True,
|
||||||
|
'str',
|
||||||
|
[1, 2, 3],
|
||||||
|
[1.1, 2.2, 3.3],
|
||||||
|
[True, False],
|
||||||
|
['str1', 'str2'],
|
||||||
|
{
|
||||||
|
'key_a': 'a',
|
||||||
|
'key_b': 'b'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'int': 1,
|
||||||
|
'float': 1.1,
|
||||||
|
'boolean': True,
|
||||||
|
'string': 'str',
|
||||||
|
'int_list': [1, 2, 3],
|
||||||
|
'float_list': [1.1, 2.2, 3.3],
|
||||||
|
'boolean_list': [True, False],
|
||||||
|
'string_list': ['str1', 'str2'],
|
||||||
|
'nested_dict': {
|
||||||
|
'key_a': 'a',
|
||||||
|
'key_b': 'b'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None
|
||||||
|
]:
|
||||||
|
result = schema.execute(
|
||||||
|
'''query Test($generic: GenericScalar){ generic(input: $generic) }''',
|
||||||
|
variable_values={'generic': generic_value}
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {
|
||||||
|
'generic': generic_value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_generic_parse_literal_query():
|
||||||
|
result = schema.execute(
|
||||||
|
'''
|
||||||
|
query {
|
||||||
|
generic(input: {
|
||||||
|
int: 1,
|
||||||
|
float: 1.1
|
||||||
|
boolean: true,
|
||||||
|
string: "str",
|
||||||
|
int_list: [1, 2, 3],
|
||||||
|
float_list: [1.1, 2.2, 3.3],
|
||||||
|
boolean_list: [true, false]
|
||||||
|
string_list: ["str1", "str2"],
|
||||||
|
nested_dict: {
|
||||||
|
key_a: "a",
|
||||||
|
key_b: "b"
|
||||||
|
},
|
||||||
|
empty_key: undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {
|
||||||
|
'generic': {
|
||||||
|
'int': 1,
|
||||||
|
'float': 1.1,
|
||||||
|
'boolean': True,
|
||||||
|
'string': 'str',
|
||||||
|
'int_list': [1, 2, 3],
|
||||||
|
'float_list': [1.1, 2.2, 3.3],
|
||||||
|
'boolean_list': [True, False],
|
||||||
|
'string_list': ['str1', 'str2'],
|
||||||
|
'nested_dict': {
|
||||||
|
'key_a': 'a',
|
||||||
|
'key_b': 'b'
|
||||||
|
},
|
||||||
|
'empty_key': None
|
||||||
|
}
|
||||||
|
}
|
30
graphene/types/tests/test_inputfield.py
Normal file
30
graphene/types/tests/test_inputfield.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import pytest
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from ..inputfield import InputField
|
||||||
|
from ..structures import NonNull
|
||||||
|
from .utils import MyLazyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_inputfield_required():
|
||||||
|
MyType = object()
|
||||||
|
field = InputField(MyType, required=True)
|
||||||
|
assert isinstance(field.type, NonNull)
|
||||||
|
assert field.type.of_type == MyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_inputfield_with_lazy_type():
|
||||||
|
MyType = object()
|
||||||
|
field = InputField(lambda: MyType)
|
||||||
|
assert field.type == MyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_inputfield_with_lazy_partial_type():
|
||||||
|
MyType = object()
|
||||||
|
field = InputField(partial(lambda: MyType))
|
||||||
|
assert field.type == MyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_inputfield_with_string_type():
|
||||||
|
field = InputField("graphene.types.tests.utils.MyLazyType")
|
||||||
|
assert field.type == MyLazyType
|
|
@ -173,3 +173,38 @@ def test_objecttype_container_benchmark(benchmark):
|
||||||
@benchmark
|
@benchmark
|
||||||
def create_objecttype():
|
def create_objecttype():
|
||||||
Container(field1='field1', field2='field2')
|
Container(field1='field1', field2='field2')
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_objecttype_description():
|
||||||
|
class MyObjectType(ObjectType):
|
||||||
|
'''
|
||||||
|
Documentation
|
||||||
|
|
||||||
|
Documentation line 2
|
||||||
|
'''
|
||||||
|
|
||||||
|
assert MyObjectType._meta.description == "Documentation\n\nDocumentation line 2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_objecttype_with_possible_types():
|
||||||
|
class MyObjectType(ObjectType):
|
||||||
|
class Meta:
|
||||||
|
possible_types = (dict, )
|
||||||
|
|
||||||
|
assert MyObjectType._meta.possible_types == (dict, )
|
||||||
|
|
||||||
|
|
||||||
|
def test_objecttype_with_possible_types_and_is_type_of_should_raise():
|
||||||
|
with pytest.raises(AssertionError) as excinfo:
|
||||||
|
class MyObjectType(ObjectType):
|
||||||
|
class Meta:
|
||||||
|
possible_types = (dict, )
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_type_of(cls, root, context, info):
|
||||||
|
return False
|
||||||
|
|
||||||
|
assert str(excinfo.value) == (
|
||||||
|
'MyObjectType.Meta.possible_types will cause type collision with '
|
||||||
|
'MyObjectType.is_type_of. Please use one or other.'
|
||||||
|
)
|
||||||
|
|
48
graphene/types/tests/test_resolver.py
Normal file
48
graphene/types/tests/test_resolver.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ..resolver import attr_resolver, dict_resolver, get_default_resolver, set_default_resolver
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
context = None
|
||||||
|
info = None
|
||||||
|
|
||||||
|
demo_dict = {
|
||||||
|
'attr': 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class demo_obj(object):
|
||||||
|
attr = 'value'
|
||||||
|
|
||||||
|
|
||||||
|
def test_attr_resolver():
|
||||||
|
resolved = attr_resolver('attr', None, demo_obj, args, context, info)
|
||||||
|
assert resolved == 'value'
|
||||||
|
|
||||||
|
|
||||||
|
def test_attr_resolver_default_value():
|
||||||
|
resolved = attr_resolver('attr2', 'default', demo_obj, args, context, info)
|
||||||
|
assert resolved == 'default'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dict_resolver():
|
||||||
|
resolved = dict_resolver('attr', None, demo_dict, args, context, info)
|
||||||
|
assert resolved == 'value'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dict_resolver_default_value():
|
||||||
|
resolved = dict_resolver('attr2', 'default', demo_dict, args, context, info)
|
||||||
|
assert resolved == 'default'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_default_resolver_is_attr_resolver():
|
||||||
|
assert get_default_resolver() == attr_resolver
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_default_resolver_workd():
|
||||||
|
default_resolver = get_default_resolver()
|
||||||
|
|
||||||
|
set_default_resolver(dict_resolver)
|
||||||
|
assert get_default_resolver() == dict_resolver
|
||||||
|
|
||||||
|
set_default_resolver(default_resolver)
|
|
@ -1,7 +1,9 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from ..structures import List, NonNull
|
from ..structures import List, NonNull
|
||||||
from ..scalars import String
|
from ..scalars import String
|
||||||
|
from .utils import MyLazyType
|
||||||
|
|
||||||
|
|
||||||
def test_list():
|
def test_list():
|
||||||
|
@ -17,6 +19,23 @@ def test_list_with_unmounted_type():
|
||||||
assert str(exc_info.value) == 'List could not have a mounted String() as inner type. Try with List(String).'
|
assert str(exc_info.value) == 'List could not have a mounted String() as inner type. Try with List(String).'
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_with_lazy_type():
|
||||||
|
MyType = object()
|
||||||
|
field = List(lambda: MyType)
|
||||||
|
assert field.of_type == MyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_with_lazy_partial_type():
|
||||||
|
MyType = object()
|
||||||
|
field = List(partial(lambda: MyType))
|
||||||
|
assert field.of_type == MyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_with_string_type():
|
||||||
|
field = List("graphene.types.tests.utils.MyLazyType")
|
||||||
|
assert field.of_type == MyLazyType
|
||||||
|
|
||||||
|
|
||||||
def test_list_inherited_works_list():
|
def test_list_inherited_works_list():
|
||||||
_list = List(List(String))
|
_list = List(List(String))
|
||||||
assert isinstance(_list.of_type, List)
|
assert isinstance(_list.of_type, List)
|
||||||
|
@ -35,6 +54,23 @@ def test_nonnull():
|
||||||
assert str(nonnull) == 'String!'
|
assert str(nonnull) == 'String!'
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonnull_with_lazy_type():
|
||||||
|
MyType = object()
|
||||||
|
field = NonNull(lambda: MyType)
|
||||||
|
assert field.of_type == MyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonnull_with_lazy_partial_type():
|
||||||
|
MyType = object()
|
||||||
|
field = NonNull(partial(lambda: MyType))
|
||||||
|
assert field.of_type == MyType
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonnull_with_string_type():
|
||||||
|
field = NonNull("graphene.types.tests.utils.MyLazyType")
|
||||||
|
assert field.of_type == MyLazyType
|
||||||
|
|
||||||
|
|
||||||
def test_nonnull_inherited_works_list():
|
def test_nonnull_inherited_works_list():
|
||||||
_list = NonNull(List(String))
|
_list = NonNull(List(String))
|
||||||
assert isinstance(_list.of_type, List)
|
assert isinstance(_list.of_type, List)
|
||||||
|
|
|
@ -183,3 +183,18 @@ def test_objecttype_camelcase_disabled():
|
||||||
assert foo_field.args == {
|
assert foo_field.args == {
|
||||||
'bar_foo': GraphQLArgument(GraphQLString, out_name='bar_foo')
|
'bar_foo': GraphQLArgument(GraphQLString, out_name='bar_foo')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_objecttype_with_possible_types():
|
||||||
|
class MyObjectType(ObjectType):
|
||||||
|
'''Description'''
|
||||||
|
class Meta:
|
||||||
|
possible_types = (dict, )
|
||||||
|
|
||||||
|
foo_bar = String()
|
||||||
|
|
||||||
|
typemap = TypeMap([MyObjectType])
|
||||||
|
graphql_type = typemap['MyObjectType']
|
||||||
|
assert graphql_type.is_type_of
|
||||||
|
assert graphql_type.is_type_of({}, None, None) is True
|
||||||
|
assert graphql_type.is_type_of(MyObjectType(), None, None) is False
|
||||||
|
|
1
graphene/types/tests/utils.py
Normal file
1
graphene/types/tests/utils.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
MyLazyType = object()
|
|
@ -21,6 +21,7 @@ from .field import Field
|
||||||
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 .resolver import get_default_resolver
|
||||||
from .scalars import ID, Boolean, Float, Int, Scalar, String
|
from .scalars import ID, Boolean, Float, Int, Scalar, String
|
||||||
from .structures import List, NonNull
|
from .structures import List, NonNull
|
||||||
from .union import Union
|
from .union import Union
|
||||||
|
@ -43,16 +44,23 @@ def resolve_type(resolve_type_func, map, type_name, root, context, 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 and 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 graphql_type
|
||||||
|
|
||||||
return _type
|
return _type
|
||||||
|
|
||||||
|
|
||||||
|
def is_type_of_from_possible_types(possible_types, root, context, info):
|
||||||
|
return isinstance(root, possible_types)
|
||||||
|
|
||||||
|
|
||||||
class TypeMap(GraphQLTypeMap):
|
class TypeMap(GraphQLTypeMap):
|
||||||
|
|
||||||
def __init__(self, types, auto_camelcase=True):
|
def __init__(self, types, auto_camelcase=True, schema=None):
|
||||||
self.auto_camelcase = auto_camelcase
|
self.auto_camelcase = auto_camelcase
|
||||||
|
self.schema = schema
|
||||||
super(TypeMap, self).__init__(types)
|
super(TypeMap, self).__init__(types)
|
||||||
|
|
||||||
def reducer(self, map, type):
|
def reducer(self, map, type):
|
||||||
|
@ -70,23 +78,31 @@ class TypeMap(GraphQLTypeMap):
|
||||||
if type._meta.name in map:
|
if type._meta.name in map:
|
||||||
_type = map[type._meta.name]
|
_type = map[type._meta.name]
|
||||||
if isinstance(_type, GrapheneGraphQLType):
|
if isinstance(_type, GrapheneGraphQLType):
|
||||||
assert _type.graphene_type == type
|
assert _type.graphene_type == type, (
|
||||||
|
'Found different types with the same name in the schema: {}, {}.'
|
||||||
|
).format(_type.graphene_type, type)
|
||||||
return map
|
return map
|
||||||
|
|
||||||
if issubclass(type, ObjectType):
|
if issubclass(type, ObjectType):
|
||||||
return self.construct_objecttype(map, type)
|
internal_type = self.construct_objecttype(map, type)
|
||||||
if issubclass(type, InputObjectType):
|
elif issubclass(type, InputObjectType):
|
||||||
return self.construct_inputobjecttype(map, type)
|
internal_type = self.construct_inputobjecttype(map, type)
|
||||||
if issubclass(type, Interface):
|
elif issubclass(type, Interface):
|
||||||
return self.construct_interface(map, type)
|
internal_type = self.construct_interface(map, type)
|
||||||
if issubclass(type, Scalar):
|
elif issubclass(type, Scalar):
|
||||||
return self.construct_scalar(map, type)
|
internal_type = self.construct_scalar(map, type)
|
||||||
if issubclass(type, Enum):
|
elif issubclass(type, Enum):
|
||||||
return self.construct_enum(map, type)
|
internal_type = self.construct_enum(map, type)
|
||||||
if issubclass(type, Union):
|
elif issubclass(type, Union):
|
||||||
return self.construct_union(map, type)
|
internal_type = self.construct_union(map, type)
|
||||||
return map
|
else:
|
||||||
|
raise Exception("Expected Graphene type, but received: {}.".format(type))
|
||||||
|
|
||||||
|
return GraphQLTypeMap.reducer(map, internal_type)
|
||||||
|
|
||||||
def construct_scalar(self, map, type):
|
def construct_scalar(self, map, type):
|
||||||
|
# We have a mapping to the original GraphQL types
|
||||||
|
# so there are no collisions.
|
||||||
_scalars = {
|
_scalars = {
|
||||||
String: GraphQLString,
|
String: GraphQLString,
|
||||||
Int: GraphQLInt,
|
Int: GraphQLInt,
|
||||||
|
@ -95,18 +111,17 @@ class TypeMap(GraphQLTypeMap):
|
||||||
ID: GraphQLID
|
ID: GraphQLID
|
||||||
}
|
}
|
||||||
if type in _scalars:
|
if type in _scalars:
|
||||||
map[type._meta.name] = _scalars[type]
|
return _scalars[type]
|
||||||
else:
|
|
||||||
map[type._meta.name] = GrapheneScalarType(
|
|
||||||
graphene_type=type,
|
|
||||||
name=type._meta.name,
|
|
||||||
description=type._meta.description,
|
|
||||||
|
|
||||||
serialize=getattr(type, 'serialize', None),
|
return GrapheneScalarType(
|
||||||
parse_value=getattr(type, 'parse_value', None),
|
graphene_type=type,
|
||||||
parse_literal=getattr(type, 'parse_literal', None),
|
name=type._meta.name,
|
||||||
)
|
description=type._meta.description,
|
||||||
return map
|
|
||||||
|
serialize=getattr(type, 'serialize', None),
|
||||||
|
parse_value=getattr(type, 'parse_value', None),
|
||||||
|
parse_literal=getattr(type, 'parse_literal', None),
|
||||||
|
)
|
||||||
|
|
||||||
def construct_enum(self, map, type):
|
def construct_enum(self, map, type):
|
||||||
values = OrderedDict()
|
values = OrderedDict()
|
||||||
|
@ -117,92 +132,104 @@ class TypeMap(GraphQLTypeMap):
|
||||||
description=getattr(value, 'description', None),
|
description=getattr(value, 'description', None),
|
||||||
deprecation_reason=getattr(value, 'deprecation_reason', None)
|
deprecation_reason=getattr(value, 'deprecation_reason', None)
|
||||||
)
|
)
|
||||||
map[type._meta.name] = GrapheneEnumType(
|
return GrapheneEnumType(
|
||||||
graphene_type=type,
|
graphene_type=type,
|
||||||
values=values,
|
values=values,
|
||||||
name=type._meta.name,
|
name=type._meta.name,
|
||||||
description=type._meta.description,
|
description=type._meta.description,
|
||||||
)
|
)
|
||||||
return map
|
|
||||||
|
|
||||||
def construct_objecttype(self, map, type):
|
def construct_objecttype(self, map, type):
|
||||||
if type._meta.name in map:
|
if type._meta.name in map:
|
||||||
_type = map[type._meta.name]
|
_type = map[type._meta.name]
|
||||||
if isinstance(_type, GrapheneGraphQLType):
|
if isinstance(_type, GrapheneGraphQLType):
|
||||||
assert _type.graphene_type == type
|
assert _type.graphene_type == type, (
|
||||||
return map
|
'Found different types with the same name in the schema: {}, {}.'
|
||||||
map[type._meta.name] = GrapheneObjectType(
|
).format(_type.graphene_type, type)
|
||||||
|
return _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)
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
if type._meta.possible_types:
|
||||||
|
is_type_of = partial(is_type_of_from_possible_types, type._meta.possible_types)
|
||||||
|
else:
|
||||||
|
is_type_of = type.is_type_of
|
||||||
|
|
||||||
|
return GrapheneObjectType(
|
||||||
graphene_type=type,
|
graphene_type=type,
|
||||||
name=type._meta.name,
|
name=type._meta.name,
|
||||||
description=type._meta.description,
|
description=type._meta.description,
|
||||||
fields=None,
|
fields=partial(self.construct_fields_for_type, map, type),
|
||||||
is_type_of=type.is_type_of,
|
is_type_of=is_type_of,
|
||||||
interfaces=None
|
interfaces=interfaces
|
||||||
)
|
)
|
||||||
interfaces = []
|
|
||||||
for i in type._meta.interfaces:
|
|
||||||
map = self.reducer(map, i)
|
|
||||||
interfaces.append(map[i._meta.name])
|
|
||||||
map[type._meta.name]._provided_interfaces = interfaces
|
|
||||||
map[type._meta.name]._fields = self.construct_fields_for_type(map, type)
|
|
||||||
# self.reducer(map, map[type._meta.name])
|
|
||||||
return map
|
|
||||||
|
|
||||||
def construct_interface(self, map, type):
|
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
|
_resolve_type = None
|
||||||
if type.resolve_type:
|
if type.resolve_type:
|
||||||
_resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name)
|
_resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name)
|
||||||
map[type._meta.name] = GrapheneInterfaceType(
|
return GrapheneInterfaceType(
|
||||||
graphene_type=type,
|
graphene_type=type,
|
||||||
name=type._meta.name,
|
name=type._meta.name,
|
||||||
description=type._meta.description,
|
description=type._meta.description,
|
||||||
fields=None,
|
fields=partial(self.construct_fields_for_type, map, type),
|
||||||
resolve_type=_resolve_type,
|
resolve_type=_resolve_type,
|
||||||
)
|
)
|
||||||
map[type._meta.name]._fields = self.construct_fields_for_type(map, type)
|
|
||||||
# self.reducer(map, map[type._meta.name])
|
|
||||||
return map
|
|
||||||
|
|
||||||
def construct_inputobjecttype(self, map, type):
|
def construct_inputobjecttype(self, map, type):
|
||||||
map[type._meta.name] = GrapheneInputObjectType(
|
return GrapheneInputObjectType(
|
||||||
graphene_type=type,
|
graphene_type=type,
|
||||||
name=type._meta.name,
|
name=type._meta.name,
|
||||||
description=type._meta.description,
|
description=type._meta.description,
|
||||||
fields=None,
|
fields=partial(self.construct_fields_for_type, map, type, is_input_type=True),
|
||||||
)
|
)
|
||||||
map[type._meta.name]._fields = self.construct_fields_for_type(map, type, is_input_type=True)
|
|
||||||
return map
|
|
||||||
|
|
||||||
def construct_union(self, map, type):
|
def construct_union(self, map, type):
|
||||||
_resolve_type = None
|
_resolve_type = None
|
||||||
if type.resolve_type:
|
if type.resolve_type:
|
||||||
_resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name)
|
_resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name)
|
||||||
types = []
|
|
||||||
for i in type._meta.types:
|
def types():
|
||||||
map = self.construct_objecttype(map, i)
|
union_types = []
|
||||||
types.append(map[i._meta.name])
|
for objecttype in type._meta.types:
|
||||||
map[type._meta.name] = GrapheneUnionType(
|
self.graphene_reducer(map, objecttype)
|
||||||
|
internal_type = map[objecttype._meta.name]
|
||||||
|
assert internal_type.graphene_type == objecttype
|
||||||
|
union_types.append(internal_type)
|
||||||
|
return union_types
|
||||||
|
|
||||||
|
return GrapheneUnionType(
|
||||||
graphene_type=type,
|
graphene_type=type,
|
||||||
name=type._meta.name,
|
name=type._meta.name,
|
||||||
types=types,
|
types=types,
|
||||||
resolve_type=_resolve_type,
|
resolve_type=_resolve_type,
|
||||||
)
|
)
|
||||||
map[type._meta.name].types = types
|
|
||||||
return map
|
|
||||||
|
|
||||||
def get_name(self, name):
|
def get_name(self, name):
|
||||||
if self.auto_camelcase:
|
if self.auto_camelcase:
|
||||||
return to_camel_case(name)
|
return to_camel_case(name)
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def default_resolver(self, attname, default_value, root, *_):
|
|
||||||
return getattr(root, attname, default_value)
|
|
||||||
|
|
||||||
def construct_fields_for_type(self, map, type, is_input_type=False):
|
def construct_fields_for_type(self, map, type, is_input_type=False):
|
||||||
fields = OrderedDict()
|
fields = OrderedDict()
|
||||||
for name, field in type._meta.fields.items():
|
for name, field in type._meta.fields.items():
|
||||||
if isinstance(field, Dynamic):
|
if isinstance(field, Dynamic):
|
||||||
field = get_field_as(field.get_type(), _as=Field)
|
field = get_field_as(field.get_type(self.schema), _as=Field)
|
||||||
if not field:
|
if not field:
|
||||||
continue
|
continue
|
||||||
map = self.reducer(map, field.type)
|
map = self.reducer(map, field.type)
|
||||||
|
@ -257,13 +284,12 @@ class TypeMap(GraphQLTypeMap):
|
||||||
if resolver:
|
if resolver:
|
||||||
return get_unbound_function(resolver)
|
return get_unbound_function(resolver)
|
||||||
|
|
||||||
return partial(self.default_resolver, name, default_value)
|
default_resolver = type._meta.default_resolver or get_default_resolver()
|
||||||
|
return partial(default_resolver, name, default_value)
|
||||||
|
|
||||||
def get_field_type(self, map, type):
|
def get_field_type(self, map, type):
|
||||||
if isinstance(type, List):
|
if isinstance(type, List):
|
||||||
return GraphQLList(self.get_field_type(map, type.of_type))
|
return GraphQLList(self.get_field_type(map, type.of_type))
|
||||||
if isinstance(type, NonNull):
|
if isinstance(type, NonNull):
|
||||||
return GraphQLNonNull(self.get_field_type(map, type.of_type))
|
return GraphQLNonNull(self.get_field_type(map, type.of_type))
|
||||||
if inspect.isfunction(type):
|
|
||||||
type = type()
|
|
||||||
return map.get(type._meta.name)
|
return map.get(type._meta.name)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ..utils.is_base_type import is_base_type
|
from ..utils.is_base_type import is_base_type
|
||||||
|
from ..utils.trim_docstring import trim_docstring
|
||||||
from .options import Options
|
from .options import Options
|
||||||
from .unmountedtype import UnmountedType
|
from .unmountedtype import UnmountedType
|
||||||
|
|
||||||
|
@ -16,7 +17,7 @@ class UnionMeta(type):
|
||||||
options = Options(
|
options = Options(
|
||||||
attrs.pop('Meta', None),
|
attrs.pop('Meta', None),
|
||||||
name=name,
|
name=name,
|
||||||
description=attrs.get('__doc__'),
|
description=trim_docstring(attrs.get('__doc__')),
|
||||||
types=(),
|
types=(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
import inspect
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from functools import partial
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
|
from ..utils.module_loading import import_string
|
||||||
from .mountedtype import MountedType
|
from .mountedtype import MountedType
|
||||||
from .unmountedtype import UnmountedType
|
from .unmountedtype import UnmountedType
|
||||||
|
|
||||||
|
@ -62,3 +66,11 @@ def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True):
|
||||||
if sort:
|
if sort:
|
||||||
fields_with_names = sorted(fields_with_names, key=lambda f: f[1])
|
fields_with_names = sorted(fields_with_names, key=lambda f: f[1])
|
||||||
return OrderedDict(fields_with_names)
|
return OrderedDict(fields_with_names)
|
||||||
|
|
||||||
|
|
||||||
|
def get_type(_type):
|
||||||
|
if isinstance(_type, string_types):
|
||||||
|
return import_string(_type)
|
||||||
|
if inspect.isfunction(_type) or type(_type) is partial:
|
||||||
|
return _type()
|
||||||
|
return _type
|
||||||
|
|
44
graphene/utils/module_loading.py
Normal file
44
graphene/utils/module_loading.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from functools import partial
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
|
||||||
|
def import_string(dotted_path, dotted_attributes=None):
|
||||||
|
"""
|
||||||
|
Import a dotted module path and return the attribute/class designated by the
|
||||||
|
last name in the path. When a dotted attribute path is also provided, the
|
||||||
|
dotted attribute path would be applied to the attribute/class retrieved from
|
||||||
|
the first step, and return the corresponding value designated by the
|
||||||
|
attribute path. Raise ImportError if the import failed.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
module_path, class_name = dotted_path.rsplit('.', 1)
|
||||||
|
except ValueError:
|
||||||
|
raise ImportError("%s doesn't look like a module path" % dotted_path)
|
||||||
|
|
||||||
|
module = import_module(module_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = getattr(module, class_name)
|
||||||
|
except AttributeError:
|
||||||
|
raise ImportError('Module "%s" does not define a "%s" attribute/class' % (
|
||||||
|
module_path, class_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not dotted_attributes:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
attributes = dotted_attributes.split('.')
|
||||||
|
traveled_attributes = []
|
||||||
|
try:
|
||||||
|
for attribute in attributes:
|
||||||
|
traveled_attributes.append(attribute)
|
||||||
|
result = getattr(result, attribute)
|
||||||
|
return result
|
||||||
|
except AttributeError:
|
||||||
|
raise ImportError('Module "%s" does not define a "%s" attribute inside attribute/class "%s"' % (
|
||||||
|
module_path, '.'.join(traveled_attributes), class_name
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def lazy_import(dotted_path, dotted_attributes=None):
|
||||||
|
return partial(import_string, dotted_path, dotted_attributes)
|
57
graphene/utils/tests/test_module_loading.py
Normal file
57
graphene/utils/tests/test_module_loading.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
from graphene import String
|
||||||
|
from graphene.types.objecttype import ObjectTypeMeta
|
||||||
|
from ..module_loading import lazy_import, import_string
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_string():
|
||||||
|
MyString = import_string('graphene.String')
|
||||||
|
assert MyString == String
|
||||||
|
|
||||||
|
MyObjectTypeMeta = import_string('graphene.ObjectType', '__class__')
|
||||||
|
assert MyObjectTypeMeta == ObjectTypeMeta
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_string_module():
|
||||||
|
with raises(Exception) as exc_info:
|
||||||
|
import_string('graphenea')
|
||||||
|
|
||||||
|
assert str(exc_info.value) == 'graphenea doesn\'t look like a module path'
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_string_class():
|
||||||
|
with raises(Exception) as exc_info:
|
||||||
|
import_string('graphene.Stringa')
|
||||||
|
|
||||||
|
assert str(exc_info.value) == 'Module "graphene" does not define a "Stringa" attribute/class'
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_string_attributes():
|
||||||
|
with raises(Exception) as exc_info:
|
||||||
|
import_string('graphene.String', 'length')
|
||||||
|
|
||||||
|
assert str(exc_info.value) == 'Module "graphene" does not define a "length" attribute inside attribute/class ' \
|
||||||
|
'"String"'
|
||||||
|
|
||||||
|
with raises(Exception) as exc_info:
|
||||||
|
import_string('graphene.ObjectType', '__class__.length')
|
||||||
|
|
||||||
|
assert str(exc_info.value) == 'Module "graphene" does not define a "__class__.length" attribute inside ' \
|
||||||
|
'attribute/class "ObjectType"'
|
||||||
|
|
||||||
|
with raises(Exception) as exc_info:
|
||||||
|
import_string('graphene.ObjectType', '__classa__.__base__')
|
||||||
|
|
||||||
|
assert str(exc_info.value) == 'Module "graphene" does not define a "__classa__" attribute inside attribute/class ' \
|
||||||
|
'"ObjectType"'
|
||||||
|
|
||||||
|
|
||||||
|
def test_lazy_import():
|
||||||
|
f = lazy_import('graphene.String')
|
||||||
|
MyString = f()
|
||||||
|
assert MyString == String
|
||||||
|
|
||||||
|
f = lazy_import('graphene.ObjectType', '__class__')
|
||||||
|
MyObjectTypeMeta = f()
|
||||||
|
assert MyObjectTypeMeta == ObjectTypeMeta
|
21
graphene/utils/tests/test_trim_docstring.py
Normal file
21
graphene/utils/tests/test_trim_docstring.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from ..trim_docstring import trim_docstring
|
||||||
|
|
||||||
|
|
||||||
|
def test_trim_docstring():
|
||||||
|
class WellDocumentedObject(object):
|
||||||
|
"""
|
||||||
|
This object is very well-documented. It has multiple lines in its
|
||||||
|
description.
|
||||||
|
|
||||||
|
Multiple paragraphs too
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert (trim_docstring(WellDocumentedObject.__doc__) ==
|
||||||
|
"This object is very well-documented. It has multiple lines in its\n"
|
||||||
|
"description.\n\nMultiple paragraphs too")
|
||||||
|
|
||||||
|
class UndocumentedObject(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert trim_docstring(UndocumentedObject.__doc__) is None
|
9
graphene/utils/trim_docstring.py
Normal file
9
graphene/utils/trim_docstring.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def trim_docstring(docstring):
|
||||||
|
# Cleans up whitespaces from an indented docstring
|
||||||
|
#
|
||||||
|
# See https://www.python.org/dev/peps/pep-0257/
|
||||||
|
# and https://docs.python.org/2/library/inspect.html#inspect.cleandoc
|
||||||
|
return inspect.cleandoc(docstring) if docstring else None
|
5
setup.py
5
setup.py
|
@ -41,6 +41,7 @@ tests_require = [
|
||||||
'pytest>=2.7.2',
|
'pytest>=2.7.2',
|
||||||
'pytest-benchmark',
|
'pytest-benchmark',
|
||||||
'pytest-cov',
|
'pytest-cov',
|
||||||
|
'snapshottest',
|
||||||
'coveralls',
|
'coveralls',
|
||||||
'six',
|
'six',
|
||||||
'mock',
|
'mock',
|
||||||
|
@ -81,9 +82,9 @@ setup(
|
||||||
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'six>=1.10.0',
|
'six>=1.10.0',
|
||||||
'graphql-core>=1.0.1',
|
'graphql-core>=1.1',
|
||||||
'graphql-relay>=0.4.5',
|
'graphql-relay>=0.4.5',
|
||||||
'promise>=1.0.1',
|
'promise>=2.0',
|
||||||
],
|
],
|
||||||
tests_require=tests_require,
|
tests_require=tests_require,
|
||||||
extras_require={
|
extras_require={
|
||||||
|
|
Loading…
Reference in New Issue
Block a user