Merge pull request #500 from graphql-python/2.0

[WIP] Road to 2.0
This commit is contained in:
Syrus Akbary 2017-08-27 13:27:12 -07:00 committed by GitHub
commit 0b92d3dba6
103 changed files with 2707 additions and 1061 deletions

1
.gitignore vendored
View File

@ -80,3 +80,4 @@ target/
# Databases # Databases
*.sqlite3 *.sqlite3
.vscode .vscode
.mypy_cache

View File

@ -2,8 +2,8 @@ language: python
sudo: false sudo: false
python: python:
- 2.7 - 2.7
- 3.4
- 3.5 - 3.5
- 3.6
- pypy - pypy
before_install: before_install:
- | - |
@ -26,6 +26,8 @@ install:
python setup.py develop python setup.py develop
elif [ "$TEST_TYPE" = lint ]; then elif [ "$TEST_TYPE" = lint ]; then
pip install flake8 pip install flake8
elif [ "$TEST_TYPE" = mypy ]; then
pip install mypy
fi fi
script: script:
- | - |
@ -33,6 +35,10 @@ script:
echo "Checking Python code lint." echo "Checking Python code lint."
flake8 graphene flake8 graphene
exit exit
elif [ "$TEST_TYPE" = mypy ]; then
echo "Checking Python types."
mypy graphene
exit
elif [ "$TEST_TYPE" = build ]; then elif [ "$TEST_TYPE" = build ]; then
py.test --cov=graphene graphene examples py.test --cov=graphene graphene examples
fi fi
@ -51,6 +57,8 @@ matrix:
include: include:
- python: '2.7' - python: '2.7'
env: TEST_TYPE=lint env: TEST_TYPE=lint
- python: '3.6'
env: TEST_TYPE=mypy
deploy: deploy:
provider: pypi provider: pypi
user: syrusakbary user: syrusakbary
@ -58,3 +66,4 @@ deploy:
tags: true tags: true
password: password:
secure: LHOp9DvYR+70vj4YVY8+JRNCKUOfYZREEUY3+4lMUpY7Zy5QwDfgEMXG64ybREH9dFldpUqVXRj53eeU3spfudSfh8NHkgqW7qihez2AhSnRc4dK6ooNfB+kLcSoJ4nUFGxdYImABc4V1hJvflGaUkTwDNYVxJF938bPaO797IvSbuI86llwqkvuK2Vegv9q/fy9sVGaF9VZIs4JgXwR5AyDR7FBArl+S84vWww4vTFD33hoE88VR4QvFY3/71BwRtQrnCMm7AOm31P9u29yi3bpzQpiOR2rHsgrsYdm597QzFKVxYwsmf9uAx2bpbSPy2WibunLePIvOFwm8xcfwnz4/J4ONBc5PSFmUytTWpzEnxb0bfUNLuYloIS24V6OZ8BfAhiYZ1AwySeJCQDM4Vk1V8IF6trTtyx5EW/uV9jsHCZ3LFsAD7UnFRTosIgN3SAK3ZWCEk5oF2IvjecsolEfkRXB3q9EjMkkuXRUeFDH2lWJLgNE27BzY6myvZVzPmfwZUsPBlPD/6w+WLSp97Rjgr9zS3T1d4ddqFM4ZYu04f2i7a/UUQqG+itzzuX5DWLPvzuNt37JB45mB9IsvxPyXZ6SkAcLl48NGyKok1f3vQnvphkfkl4lni29woKhaau8xlsuEDrcwOoeAsVcZXiItg+l+z2SlIwM0A06EvQ= secure: LHOp9DvYR+70vj4YVY8+JRNCKUOfYZREEUY3+4lMUpY7Zy5QwDfgEMXG64ybREH9dFldpUqVXRj53eeU3spfudSfh8NHkgqW7qihez2AhSnRc4dK6ooNfB+kLcSoJ4nUFGxdYImABc4V1hJvflGaUkTwDNYVxJF938bPaO797IvSbuI86llwqkvuK2Vegv9q/fy9sVGaF9VZIs4JgXwR5AyDR7FBArl+S84vWww4vTFD33hoE88VR4QvFY3/71BwRtQrnCMm7AOm31P9u29yi3bpzQpiOR2rHsgrsYdm597QzFKVxYwsmf9uAx2bpbSPy2WibunLePIvOFwm8xcfwnz4/J4ONBc5PSFmUytTWpzEnxb0bfUNLuYloIS24V6OZ8BfAhiYZ1AwySeJCQDM4Vk1V8IF6trTtyx5EW/uV9jsHCZ3LFsAD7UnFRTosIgN3SAK3ZWCEk5oF2IvjecsolEfkRXB3q9EjMkkuXRUeFDH2lWJLgNE27BzY6myvZVzPmfwZUsPBlPD/6w+WLSp97Rjgr9zS3T1d4ddqFM4ZYu04f2i7a/UUQqG+itzzuX5DWLPvzuNt37JB45mB9IsvxPyXZ6SkAcLl48NGyKok1f3vQnvphkfkl4lni29woKhaau8xlsuEDrcwOoeAsVcZXiItg+l+z2SlIwM0A06EvQ=
distributions: "sdist bdist_wheel"

View File

@ -1,4 +1,4 @@
Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade to Graphene `1.0`. Please read [UPGRADE-v2.0.md](/UPGRADE-v2.0.md) to learn how to upgrade to Graphene `2.0`.
--- ---
@ -32,12 +32,12 @@ Also, Graphene is fully compatible with the GraphQL spec, working seamlessly wit
For instaling graphene, just run this command in your shell For instaling graphene, just run this command in your shell
```bash ```bash
pip install "graphene>=1.0" pip install "graphene>=2.0.dev"
``` ```
## 1.0 Upgrade Guide ## 2.0 Upgrade Guide
Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade. Please read [UPGRADE-v2.0.md](/UPGRADE-v2.0.md) to learn how to upgrade.
## Examples ## Examples
@ -48,7 +48,7 @@ Here is one example for you to get started:
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
hello = graphene.String(description='A typical hello world') hello = graphene.String(description='A typical hello world')
def resolve_hello(self, args, context, info): def resolve_hello(self, info):
return 'World' return 'World'
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)

View File

@ -1,5 +1,5 @@
Please read `UPGRADE-v1.0.md </UPGRADE-v1.0.md>`__ to learn how to Please read `UPGRADE-v2.0.md </UPGRADE-v2.0.md>`__ to learn how to
upgrade to Graphene ``1.0``. upgrade to Graphene ``2.0``.
-------------- --------------
@ -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 both Relay. - **Relay:** Graphene has builtin support for 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
@ -47,12 +47,12 @@ For instaling graphene, just run this command in your shell
.. code:: bash .. code:: bash
pip install "graphene>=1.0" pip install "graphene>=2.0"
1.0 Upgrade Guide 2.0 Upgrade Guide
----------------- -----------------
Please read `UPGRADE-v1.0.md </UPGRADE-v1.0.md>`__ to learn how to Please read `UPGRADE-v2.0.md </UPGRADE-v2.0.md>`__ to learn how to
upgrade. upgrade.
Examples Examples
@ -65,7 +65,7 @@ Here is one example for you to get started:
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
hello = graphene.String(description='A typical hello world') hello = graphene.String(description='A typical hello world')
def resolve_hello(self, args, context, info): def resolve_hello(self, info):
return 'World' return 'World'
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)

293
UPGRADE-v2.0.md Normal file
View File

@ -0,0 +1,293 @@
# v2.0 Upgrade Guide
`ObjectType`, `Interface`, `InputObjectType`, `Scalar` and `Enum` implementations
have been quite simplified, without the need to define a explicit Metaclass for each subtype.
It also improves the field resolvers, [simplifying the code](#simpler-resolvers) the
developer has to write to use them.
**Deprecations:**
* [`AbstractType`](#abstracttype-deprecated)
* [`resolve_only_args`](#resolve_only_args)
* [`Mutation.Input`](#mutationinput)
**Breaking changes:**
* [`Simpler Resolvers`](#simpler-resolvers)
* [`Node Connections`](#node-connections)
**New Features!**
* [`InputObjectType`](#inputobjecttype)
* [`Meta as Class arguments`](#meta-ass-class-arguments) (_only available for Python 3_)
> The type metaclasses are now deleted as they are no longer necessary. If your code was depending
> on this strategy for creating custom attrs, see an [example on how to do it in 2.0](https://github.com/graphql-python/graphene/blob/2.0/graphene/tests/issues/test_425.py).
## Deprecations
### AbstractType deprecated
AbstractType is deprecated in graphene 2.0, you can now use normal inheritance instead.
Before:
```python
class CommonFields(AbstractType):
name = String()
class Pet(CommonFields, Interface):
pass
```
With 2.0:
```python
class CommonFields(object):
name = String()
class Pet(CommonFields, Interface):
pass
```
### resolve\_only\_args
`resolve_only_args` is now deprecated as the resolver API has been simplified.
Before:
```python
class User(ObjectType):
name = String()
@resolve_only_args
def resolve_name(self):
return self.name
```
With 2.0:
```python
class User(ObjectType):
name = String()
def resolve_name(self, info):
return self.name
```
### Mutation.Input
`Mutation.Input` is now deprecated in favor of using `Mutation.Arguments` (`ClientIDMutation` still uses `Input`).
Before:
```python
class User(Mutation):
class Input:
name = String()
```
With 2.0:
```python
class User(Mutation):
class Arguments:
name = String()
```
## Breaking Changes
### Simpler resolvers
All the resolvers in graphene have been simplified.
Prior to Graphene `2.0`, all resolvers required four arguments: `(root, args, context, info)`.
Now, resolver `args` are passed as keyword arguments to the function, and `context` argument dissapeared in favor of `info.context`.
Before:
```python
my_field = graphene.String(my_arg=graphene.String())
def resolve_my_field(self, args, context, info):
my_arg = args.get('my_arg')
return ...
```
With 2.0:
```python
my_field = graphene.String(my_arg=graphene.String())
def resolve_my_field(self, info, my_arg):
return ...
```
And, if you need the context in the resolver, you can use `info.context`:
```python
my_field = graphene.String(my_arg=graphene.String())
def resolve_my_field(self, info, my_arg):
context = info.context
return ...
```
### Node Connections
Node types no longer have a `Connection` by default.
In 2.0 and onwards `Connection`s should be defined explicitly.
Before:
```python
class User(ObjectType):
class Meta:
interfaces = [relay.Node]
name = String()
class Query(ObjectType):
user_connection = relay.ConnectionField(User)
```
With 2.0:
```python
class User(ObjectType):
class Meta:
interfaces = [relay.Node]
name = String()
class UserConnection(relay.Connection):
class Meta:
node = User
class Query(ObjectType):
user_connection = relay.ConnectionField(UserConnection)
```
## Node.get_node
The method `get_node` in `ObjectTypes` that have `Node` as interface, changes its API.
From `def get_node(cls, id, context, info)` to `def get_node(cls, info, id)`.
```python
class MyObject(ObjectType):
class Meta:
interfaces = (Node, )
@classmethod
def get_node(cls, id, context, info):
return ...
```
To:
```python
class MyObject(ObjectType):
class Meta:
interfaces = (Node, )
@classmethod
def get_node(cls, info, id):
return ...
```
## Mutation.mutate
Now only receives (`root`, `info`, `**args`)
## ClientIDMutation.mutate_and_get_payload
Now only receives (`root`, `info`, `**input`)
## New Features
### InputObjectType
If you are using `InputObjectType`, you now can access
its fields via `getattr` (`my_input.myattr`) when resolving, instead of
the classic way `my_input['myattr']`.
And also use custom defined properties on your input class.
Example. Before:
```python
class UserInput(InputObjectType):
id = ID(required=True)
def is_valid_input(input):
return input.get('id').startswith('userid_')
class Query(ObjectType):
user = graphene.Field(User, input=UserInput())
@resolve_only_args
def resolve_user(self, input):
user_id = input.get('id')
if is_valid_input(user_id):
return get_user(user_id)
```
With 2.0:
```python
class UserInput(InputObjectType):
id = ID(required=True)
@property
def is_valid(self):
return self.id.startswith('userid_')
class Query(ObjectType):
user = graphene.Field(User, input=UserInput())
def resolve_user(self, info, input):
if input.is_valid:
return get_user(input.id)
```
### Meta as Class arguments
Now you can use the meta options as class arguments (**ONLY PYTHON 3**).
Before:
```python
class Dog(ObjectType):
class Meta:
interfaces = [Pet]
name = String()
```
With 2.0:
```python
class Dog(ObjectType, interfaces=[Pet]):
name = String()
```
### Abstract types
Now you can create abstact types super easily, without the need of subclassing the meta.
```python
class Base(ObjectType):
class Meta:
abstract = True
id = ID()
def resolve_id(self, info):
return "{type}_{id}".format(
type=self.__class__.__name__,
id=self.id
)
```
### UUID Scalar
In Graphene 2.0 there is a new dedicated scalar for UUIDs, `UUID`.

View File

@ -99,8 +99,8 @@ leaner code and at most 4 database requests, and possibly fewer if there are cac
best_friend = graphene.Field(lambda: User) best_friend = graphene.Field(lambda: User)
friends = graphene.List(lambda: User) friends = graphene.List(lambda: User)
def resolve_best_friend(self, args, context, info): def resolve_best_friend(self, info):
return user_loader.load(self.best_friend_id) return user_loader.load(self.best_friend_id)
def resolve_friends(self, args, context, info): def resolve_friends(self, info):
return user_loader.load_many(self.friend_ids) return user_loader.load_many(self.friend_ids)

View File

@ -24,8 +24,8 @@ You can pass context to a query via ``context_value``.
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
name = graphene.String() name = graphene.String()
def resolve_name(self, args, context, info): def resolve_name(self, info):
return context.get('name') return info.context.get('name')
schema = graphene.Schema(Query) schema = graphene.Schema(Query)
result = schema.execute('{ name }', context_value={'name': 'Syrus'}) result = schema.execute('{ name }', context_value={'name': 'Syrus'})
@ -43,8 +43,8 @@ You can pass variables to a query via ``variable_values``.
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
user = graphene.Field(User) user = graphene.Field(User)
def resolve_user(self, args, context, info): def resolve_user(self, info):
return context.get('user') return info.context.get('user')
schema = graphene.Schema(Query) schema = graphene.Schema(Query)
result = schema.execute( result = schema.execute(

View File

@ -31,10 +31,10 @@ 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, info, **args):
if info.field_name == 'user': if info.field_name == 'user':
return None return None
return next(root, args, context, info) return next(root, info, **args)
And then execute it with: And then execute it with:

View File

@ -12,15 +12,15 @@ Lets build a basic GraphQL schema from scratch.
Requirements Requirements
------------ ------------
- Python (2.7, 3.2, 3.3, 3.4, 3.5, pypy) - Python (2.7, 3.4, 3.5, 3.6, pypy)
- Graphene (1.0) - Graphene (2.0)
Project setup Project setup
------------- -------------
.. code:: bash .. code:: bash
pip install "graphene>=1.0" pip install "graphene>=2.0"
Creating a basic Schema Creating a basic Schema
----------------------- -----------------------
@ -37,10 +37,10 @@ one field: ``hello`` and an input name. And when we query it, it should return `
import graphene import graphene
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.Argument(graphene.String, default_value="stranger")) hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, args, context, info): def resolve_hello(self, info, name):
return 'Hello ' + args['name'] return 'Hello ' + name
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)

View File

@ -41,5 +41,5 @@ that implements ``Node`` will have a default Connection.
name = graphene.String() name = graphene.String()
ships = relay.ConnectionField(ShipConnection) ships = relay.ConnectionField(ShipConnection)
def resolve_ships(self, args, context, info): def resolve_ships(self, info):
return [] return []

View File

@ -21,9 +21,9 @@ subclass of ``relay.ClientIDMutation``.
faction = graphene.Field(Faction) faction = graphene.Field(Faction)
@classmethod @classmethod
def mutate_and_get_payload(cls, input, context, info): def mutate_and_get_payload(cls, root, info, **input):
ship_name = input.get('ship_name') ship_name = input.ship_name
faction_id = input.get('faction_id') faction_id = input.faction_id
ship = create_ship(ship_name, faction_id) ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id) faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction) return IntroduceShip(ship=ship, faction=faction)
@ -46,7 +46,7 @@ Mutations can also accept files, that's how it will work with different integrat
success = graphene.String() success = graphene.String()
@classmethod @classmethod
def mutate_and_get_payload(cls, input, context, info): def mutate_and_get_payload(cls, root, info, **input):
# When using it in Django, context will be the request # When using it in Django, context will be the request
files = context.FILES files = context.FILES
# Or, if used in Flask, context will be the flask global request # Or, if used in Flask, context will be the flask global request

View File

@ -22,7 +22,7 @@ Example usage (taken from the `Starwars Relay example`_):
name = graphene.String(description='The name of the ship.') name = graphene.String(description='The name of the ship.')
@classmethod @classmethod
def get_node(cls, id, context, info): def get_node(cls, info, id):
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
@ -55,7 +55,7 @@ Example of a custom node:
return '{}:{}'.format(type, id) return '{}:{}'.format(type, id)
@staticmethod @staticmethod
def get_node_from_global_id(global_id, context, info, only_type=None): def get_node_from_global_id(info global_id, only_type=None):
type, id = global_id.split(':') type, id = global_id.split(':')
if only_node: if only_node:
# We assure that the node type that we want to retrieve # We assure that the node type that we want to retrieve

View File

@ -13,15 +13,14 @@ This example defines a Mutation:
import graphene import graphene
class CreatePerson(graphene.Mutation): class CreatePerson(graphene.Mutation):
class Input: class Arguments:
name = graphene.String() name = graphene.String()
ok = graphene.Boolean() ok = graphene.Boolean()
person = graphene.Field(lambda: Person) person = graphene.Field(lambda: Person)
@staticmethod def mutate(self, name):
def mutate(root, args, context, info): person = Person(name=name)
person = Person(name=args.get('name'))
ok = True ok = True
return CreatePerson(person=person, ok=ok) return CreatePerson(person=person, ok=ok)
@ -90,30 +89,29 @@ InputFields are used in mutations to allow nested input data for mutations
To use an InputField you define an InputObjectType that specifies the structure of your input data To use an InputField you define an InputObjectType that specifies the structure of your input data
.. code:: python .. code:: python
import graphene import graphene
class PersonInput(graphene.InputObjectType): class PersonInput(graphene.InputObjectType):
name = graphene.String() name = graphene.String(required=True)
age = graphene.Int() age = graphene.Int(required=True)
class CreatePerson(graphene.Mutation): class CreatePerson(graphene.Mutation):
class Input: class Arguments:
person_data = graphene.Argument(PersonInput) person_data = PersonInput(required=True)
person = graphene.Field(lambda: Person) person = graphene.Field(Person)
@staticmethod @staticmethod
def mutate(root, args, context, info): def mutate(root, person_data=None):
p_data = args.get('person_data')
name = p_data.get('name') name = p_data.get('name')
age = p_data.get('age') age = p_data.get('age')
person = Person(name=name, age=age) person = Person(
name=person_data.name,
age=person_data.age
)
return CreatePerson(person=person) return CreatePerson(person=person)

View File

@ -25,7 +25,7 @@ This example model defines a Person, with a first and a last name:
last_name = graphene.String() last_name = graphene.String()
full_name = graphene.String() full_name = graphene.String()
def resolve_full_name(self, args, context, info): def resolve_full_name(self, info):
return '{} {}'.format(self.first_name, self.last_name) return '{} {}'.format(self.first_name, self.last_name)
**first\_name** and **last\_name** are fields of the ObjectType. Each **first\_name** and **last\_name** are fields of the ObjectType. Each
@ -71,8 +71,7 @@ method in the class.
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
reverse = graphene.String(word=graphene.String()) reverse = graphene.String(word=graphene.String())
def resolve_reverse(self, args, context, info): def resolve_reverse(self, info, word):
word = args.get('word')
return word[::-1] return word[::-1]
Resolvers outside the class Resolvers outside the class
@ -84,8 +83,7 @@ A field can use a custom resolver from outside the class:
import graphene import graphene
def reverse(root, args, context, info): def reverse(root, info, word):
word = args.get('word')
return word[::-1] return word[::-1]
class Query(graphene.ObjectType): class Query(graphene.ObjectType):

63
docs/types/unions.rst Normal file
View File

@ -0,0 +1,63 @@
Unions
======
Union types are very similar to interfaces, but they don't get
to specify any common fields between the types.
The basics:
- Each Union is a Python class that inherits from ``graphene.Union``.
- Unions don't have any fields on it, just links to the possible objecttypes.
Quick example
-------------
This example model defines a ``Character`` interface with a name. ``Human``
and ``Droid`` are two implementations of that interface.
.. code:: python
import graphene
class Human(graphene.ObjectType):
name = graphene.String()
born_in = graphene.String()
class Droid(graphene.ObjectType):
name = graphene.String()
primary_function = graphene.String()
class Starship(graphene.ObjectType):
name = graphene.String()
length = graphene.Int()
class SearchResult(graphene.Union):
class Meta:
types = (Human, Droid, Starship)
Wherever we return a SearchResult type in our schema, we might get a Human, a Droid, or a Starship.
Note that members of a union type need to be concrete object types;
you can't create a union type out of interfaces or other unions.
The above types have the following representation in a schema:
.. code::
type Droid {
name: String
primaryFunction: String
}
type Human {
name: String
bornIn: String
}
type Ship {
name: String
length: Int
}
union SearchResult = Human | Droid | Starship

View File

@ -11,10 +11,9 @@ class Address(graphene.ObjectType):
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
address = graphene.Field(Address, geo=GeoInput()) address = graphene.Field(Address, geo=GeoInput(required=True))
def resolve_address(self, args, context, info): def resolve_address(self, info, geo):
geo = args.get('geo')
return Address(latlng="({},{})".format(geo.get('lat'), geo.get('lng'))) return Address(latlng="({},{})".format(geo.get('lat'), geo.get('lng')))

View File

@ -9,8 +9,9 @@ class User(graphene.ObjectType):
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
me = graphene.Field(User) me = graphene.Field(User)
def resolve_me(self, args, context, info): def resolve_me(self, info):
return context['user'] return info.context['user']
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
query = ''' query = '''

View File

@ -11,9 +11,10 @@ class Query(graphene.ObjectType):
patron = graphene.Field(Patron) patron = graphene.Field(Patron)
def resolve_patron(self, args, context, info): def resolve_patron(self, info):
return Patron(id=1, name='Syrus', age=27) return Patron(id=1, name='Syrus', age=27)
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
query = ''' query = '''
query something{ query something{

View File

@ -1,5 +1,4 @@
import graphene import graphene
from graphene import resolve_only_args
from .data import get_character, get_droid, get_hero, get_human from .data import get_character, get_droid, get_hero, get_human
@ -16,7 +15,7 @@ class Character(graphene.Interface):
friends = graphene.List(lambda: Character) friends = graphene.List(lambda: Character)
appears_in = graphene.List(Episode) appears_in = graphene.List(Episode)
def resolve_friends(self, args, *_): def resolve_friends(self, info):
# The character friends is a list of strings # The character friends is a list of strings
return [get_character(f) for f in self.friends] return [get_character(f) for f in self.friends]
@ -46,16 +45,13 @@ class Query(graphene.ObjectType):
id=graphene.String() id=graphene.String()
) )
@resolve_only_args def resolve_hero(self, info, episode=None):
def resolve_hero(self, episode=None):
return get_hero(episode) return get_hero(episode)
@resolve_only_args def resolve_human(self, info, id):
def resolve_human(self, id):
return get_human(id) return get_human(id)
@resolve_only_args def resolve_droid(self, info, id):
def resolve_droid(self, id):
return get_droid(id) return get_droid(id)

View File

@ -1,4 +1,5 @@
from graphene.test import Client from graphene.test import Client
from ..data import setup from ..data import setup
from ..schema import schema from ..schema import schema
@ -6,6 +7,7 @@ setup()
client = Client(schema) client = Client(schema)
def test_hero_name_query(snapshot): def test_hero_name_query(snapshot):
query = ''' query = '''
query HeroNameQuery { query HeroNameQuery {
@ -17,7 +19,6 @@ def test_hero_name_query(snapshot):
snapshot.assert_match(client.execute(query)) snapshot.assert_match(client.execute(query))
def test_hero_name_and_friends_query(snapshot): def test_hero_name_and_friends_query(snapshot):
query = ''' query = '''
query HeroNameAndFriendsQuery { query HeroNameAndFriendsQuery {

View File

@ -1,5 +1,5 @@
import graphene import graphene
from graphene import relay, resolve_only_args from graphene import relay
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship from .data import create_ship, get_empire, get_faction, get_rebels, get_ship
@ -13,10 +13,16 @@ class Ship(graphene.ObjectType):
name = graphene.String(description='The name of the ship.') name = graphene.String(description='The name of the ship.')
@classmethod @classmethod
def get_node(cls, id, context, info): def get_node(cls, info, id):
return get_ship(id) return get_ship(id)
class ShipConnection(relay.Connection):
class Meta:
node = Ship
class Faction(graphene.ObjectType): class Faction(graphene.ObjectType):
'''A faction in the Star Wars saga''' '''A faction in the Star Wars saga'''
@ -24,15 +30,14 @@ class Faction(graphene.ObjectType):
interfaces = (relay.Node, ) interfaces = (relay.Node, )
name = graphene.String(description='The name of the faction.') name = graphene.String(description='The name of the faction.')
ships = relay.ConnectionField(Ship, description='The ships used by the faction.') ships = relay.ConnectionField(ShipConnection, description='The ships used by the faction.')
@resolve_only_args def resolve_ships(self, info, **args):
def resolve_ships(self, **args):
# Transform the instance ship_ids into real instances # Transform the instance ship_ids into real instances
return [get_ship(ship_id) for ship_id in self.ships] return [get_ship(ship_id) for ship_id in self.ships]
@classmethod @classmethod
def get_node(cls, id, context, info): def get_node(cls, info, id):
return get_faction(id) return get_faction(id)
@ -46,9 +51,7 @@ class IntroduceShip(relay.ClientIDMutation):
faction = graphene.Field(Faction) faction = graphene.Field(Faction)
@classmethod @classmethod
def mutate_and_get_payload(cls, input, context, info): def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None):
ship_name = input.get('ship_name')
faction_id = input.get('faction_id')
ship = create_ship(ship_name, faction_id) ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id) faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction) return IntroduceShip(ship=ship, faction=faction)
@ -59,12 +62,10 @@ class Query(graphene.ObjectType):
empire = graphene.Field(Faction) empire = graphene.Field(Faction)
node = relay.Node.Field() node = relay.Node.Field()
@resolve_only_args def resolve_rebels(self, info):
def resolve_rebels(self):
return get_rebels() return get_rebels()
@resolve_only_args def resolve_empire(self, info):
def resolve_empire(self):
return get_empire() return get_empire()

View File

@ -51,3 +51,63 @@ snapshots['test_correctly_refetches_xwing 1'] = {
} }
} }
} }
snapshots['test_str_schema 1'] = '''schema {
query: Query
mutation: Mutation
}
type Faction implements Node {
id: ID!
name: String
ships(before: String, after: String, first: Int, last: Int): ShipConnection
}
input IntroduceShipInput {
shipName: String!
factionId: String!
clientMutationId: String
}
type IntroduceShipPayload {
ship: Ship
faction: Faction
clientMutationId: String
}
type Mutation {
introduceShip(input: IntroduceShipInput!): IntroduceShipPayload
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
rebels: Faction
empire: Faction
node(id: ID!): Node
}
type Ship implements Node {
id: ID!
name: String
}
type ShipConnection {
pageInfo: PageInfo!
edges: [ShipEdge]!
}
type ShipEdge {
node: Ship
cursor: String!
}
'''

View File

@ -1,4 +1,5 @@
from graphene.test import Client from graphene.test import Client
from ..data import setup from ..data import setup
from ..schema import schema from ..schema import schema

View File

@ -1,4 +1,5 @@
from graphene.test import Client from graphene.test import Client
from ..data import setup from ..data import setup
from ..schema import schema from ..schema import schema

View File

@ -1,4 +1,5 @@
from graphene.test import Client from graphene.test import Client
from ..data import setup from ..data import setup
from ..schema import schema from ..schema import schema
@ -7,66 +8,8 @@ setup()
client = Client(schema) client = Client(schema)
def test_str_schema(): def test_str_schema(snapshot):
assert str(schema) == '''schema { snapshot.assert_match(str(schema))
query: Query
mutation: Mutation
}
type Faction implements Node {
id: ID!
name: String
ships(before: String, after: String, first: Int, last: Int): ShipConnection
}
input IntroduceShipInput {
shipName: String!
factionId: String!
clientMutationId: String
}
type IntroduceShipPayload {
ship: Ship
faction: Faction
clientMutationId: String
}
type Mutation {
introduceShip(input: IntroduceShipInput!): IntroduceShipPayload
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
rebels: Faction
empire: Faction
node(id: ID!): Node
}
type Ship implements Node {
id: ID!
name: String
}
type ShipConnection {
pageInfo: PageInfo!
edges: [ShipEdge]!
}
type ShipEdge {
node: Ship
cursor: String!
}
'''
def test_correctly_fetches_id_name_rebels(snapshot): def test_correctly_fetches_id_name_rebels(snapshot):

View File

@ -1,21 +1,5 @@
from .pyutils.version import get_version from .pyutils.version import get_version
try:
# This variable is injected in the __builtins__ by the build
# process. It used to enable importing subpackages when
# the required packages are not installed
__SETUP__
except NameError:
__SETUP__ = False
VERSION = (1, 4, 1, 'final', 0)
__version__ = get_version(VERSION)
if not __SETUP__:
from .types import ( from .types import (
AbstractType, AbstractType,
ObjectType, ObjectType,
@ -27,11 +11,15 @@ if not __SETUP__:
Schema, Schema,
Scalar, Scalar,
String, ID, Int, Float, Boolean, String, ID, Int, Float, Boolean,
JSONString,
UUID,
List, NonNull, List, NonNull,
Enum, Enum,
Argument, Argument,
Dynamic, Dynamic,
Union, Union,
Context,
ResolveInfo
) )
from .relay import ( from .relay import (
Node, Node,
@ -45,8 +33,13 @@ if not __SETUP__:
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 from .utils.module_loading import lazy_import
VERSION = (2, 0, 0, 'alpha', 0)
__version__ = get_version(VERSION)
__all__ = [ __all__ = [
'AbstractType', '__version__',
'ObjectType', 'ObjectType',
'InputObjectType', 'InputObjectType',
'Interface', 'Interface',
@ -61,6 +54,8 @@ if not __SETUP__:
'Float', 'Float',
'Enum', 'Enum',
'Boolean', 'Boolean',
'JSONString',
'UUID',
'List', 'List',
'NonNull', 'NonNull',
'Argument', 'Argument',
@ -75,4 +70,9 @@ if not __SETUP__:
'ConnectionField', 'ConnectionField',
'PageInfo', 'PageInfo',
'lazy_import', 'lazy_import',
'Context',
'ResolveInfo',
# Deprecated
'AbstractType',
] ]

View File

@ -0,0 +1,19 @@
from __future__ import absolute_import
import six
try:
from enum import Enum
except ImportError:
from .enum import Enum
try:
from inspect import signature
except ImportError:
from .signature import signature
if six.PY2:
def func_name(func):
return func.func_name
else:
def func_name(func):
return func.__name__

View File

@ -0,0 +1,19 @@
is_init_subclass_available = hasattr(object, '__init_subclass__')
if not is_init_subclass_available:
class InitSubclassMeta(type):
"""Metaclass that implements PEP 487 protocol"""
def __new__(cls, name, bases, ns, **kwargs):
__init_subclass__ = ns.pop('__init_subclass__', None)
if __init_subclass__:
__init_subclass__ = classmethod(__init_subclass__)
ns['__init_subclass__'] = __init_subclass__
return super(InitSubclassMeta, cls).__new__(cls, name, bases, ns, **kwargs)
def __init__(cls, name, bases, ns, **kwargs):
super(InitSubclassMeta, cls).__init__(name, bases, ns)
super_class = super(cls, cls)
if hasattr(super_class, '__init_subclass__'):
super_class.__init_subclass__.__func__(cls, **kwargs)
else:
InitSubclassMeta = type # type: ignore

View File

@ -0,0 +1,808 @@
# Copyright 2001-2013 Python Software Foundation; All Rights Reserved
"""Function signature objects for callables
Back port of Python 3.3's function signature tools from the inspect module,
modified to be compatible with Python 2.7 and 3.2+.
"""
from __future__ import absolute_import, division, print_function
import itertools
import functools
import re
import types
from collections import OrderedDict
__version__ = "0.4"
__all__ = ['BoundArguments', 'Parameter', 'Signature', 'signature']
_WrapperDescriptor = type(type.__call__)
_MethodWrapper = type(all.__call__)
_NonUserDefinedCallables = (_WrapperDescriptor,
_MethodWrapper,
types.BuiltinFunctionType)
def formatannotation(annotation, base_module=None):
if isinstance(annotation, type):
if annotation.__module__ in ('builtins', '__builtin__', base_module):
return annotation.__name__
return annotation.__module__+'.'+annotation.__name__
return repr(annotation)
def _get_user_defined_method(cls, method_name, *nested):
try:
if cls is type:
return
meth = getattr(cls, method_name)
for name in nested:
meth = getattr(meth, name, meth)
except AttributeError:
return
else:
if not isinstance(meth, _NonUserDefinedCallables):
# Once '__signature__' will be added to 'C'-level
# callables, this check won't be necessary
return meth
def signature(obj):
'''Get a signature object for the passed callable.'''
if not callable(obj):
raise TypeError('{0!r} is not a callable object'.format(obj))
if isinstance(obj, types.MethodType):
sig = signature(obj.__func__)
if obj.__self__ is None:
# Unbound method: the first parameter becomes positional-only
if sig.parameters:
first = sig.parameters.values()[0].replace(
kind=_POSITIONAL_ONLY)
return sig.replace(
parameters=(first,) + tuple(sig.parameters.values())[1:])
else:
return sig
else:
# In this case we skip the first parameter of the underlying
# function (usually `self` or `cls`).
return sig.replace(parameters=tuple(sig.parameters.values())[1:])
try:
sig = obj.__signature__
except AttributeError:
pass
else:
if sig is not None:
return sig
try:
# Was this function wrapped by a decorator?
wrapped = obj.__wrapped__
except AttributeError:
pass
else:
return signature(wrapped)
if isinstance(obj, types.FunctionType):
return Signature.from_function(obj)
if isinstance(obj, functools.partial):
sig = signature(obj.func)
new_params = OrderedDict(sig.parameters.items())
partial_args = obj.args or ()
partial_keywords = obj.keywords or {}
try:
ba = sig.bind_partial(*partial_args, **partial_keywords)
except TypeError as ex:
msg = 'partial object {0!r} has incorrect arguments'.format(obj)
raise ValueError(msg)
for arg_name, arg_value in ba.arguments.items():
param = new_params[arg_name]
if arg_name in partial_keywords:
# We set a new default value, because the following code
# is correct:
#
# >>> def foo(a): print(a)
# >>> print(partial(partial(foo, a=10), a=20)())
# 20
# >>> print(partial(partial(foo, a=10), a=20)(a=30))
# 30
#
# So, with 'partial' objects, passing a keyword argument is
# like setting a new default value for the corresponding
# parameter
#
# We also mark this parameter with '_partial_kwarg'
# flag. Later, in '_bind', the 'default' value of this
# parameter will be added to 'kwargs', to simulate
# the 'functools.partial' real call.
new_params[arg_name] = param.replace(default=arg_value,
_partial_kwarg=True)
elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and
not param._partial_kwarg):
new_params.pop(arg_name)
return sig.replace(parameters=new_params.values())
sig = None
if isinstance(obj, type):
# obj is a class or a metaclass
# First, let's see if it has an overloaded __call__ defined
# in its metaclass
call = _get_user_defined_method(type(obj), '__call__')
if call is not None:
sig = signature(call)
else:
# Now we check if the 'obj' class has a '__new__' method
new = _get_user_defined_method(obj, '__new__')
if new is not None:
sig = signature(new)
else:
# Finally, we should have at least __init__ implemented
init = _get_user_defined_method(obj, '__init__')
if init is not None:
sig = signature(init)
elif not isinstance(obj, _NonUserDefinedCallables):
# An object with __call__
# We also check that the 'obj' is not an instance of
# _WrapperDescriptor or _MethodWrapper to avoid
# infinite recursion (and even potential segfault)
call = _get_user_defined_method(type(obj), '__call__', 'im_func')
if call is not None:
sig = signature(call)
if sig is not None:
# For classes and objects we skip the first parameter of their
# __call__, __new__, or __init__ methods
return sig.replace(parameters=tuple(sig.parameters.values())[1:])
if isinstance(obj, types.BuiltinFunctionType):
# Raise a nicer error message for builtins
msg = 'no signature found for builtin function {0!r}'.format(obj)
raise ValueError(msg)
raise ValueError('callable {0!r} is not supported by signature'.format(obj))
class _void(object):
'''A private marker - used in Parameter & Signature'''
class _empty(object):
pass
class _ParameterKind(int):
def __new__(self, *args, **kwargs):
obj = int.__new__(self, *args)
obj._name = kwargs['name']
return obj
def __str__(self):
return self._name
def __repr__(self):
return '<_ParameterKind: {0!r}>'.format(self._name)
_POSITIONAL_ONLY = _ParameterKind(0, name='POSITIONAL_ONLY')
_POSITIONAL_OR_KEYWORD = _ParameterKind(1, name='POSITIONAL_OR_KEYWORD')
_VAR_POSITIONAL = _ParameterKind(2, name='VAR_POSITIONAL')
_KEYWORD_ONLY = _ParameterKind(3, name='KEYWORD_ONLY')
_VAR_KEYWORD = _ParameterKind(4, name='VAR_KEYWORD')
class Parameter(object):
'''Represents a parameter in a function signature.
Has the following public attributes:
* name : str
The name of the parameter as a string.
* default : object
The default value for the parameter if specified. If the
parameter has no default value, this attribute is not set.
* annotation
The annotation for the parameter if specified. If the
parameter has no annotation, this attribute is not set.
* kind : str
Describes how argument values are bound to the parameter.
Possible values: `Parameter.POSITIONAL_ONLY`,
`Parameter.POSITIONAL_OR_KEYWORD`, `Parameter.VAR_POSITIONAL`,
`Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`.
'''
__slots__ = ('_name', '_kind', '_default', '_annotation', '_partial_kwarg')
POSITIONAL_ONLY = _POSITIONAL_ONLY
POSITIONAL_OR_KEYWORD = _POSITIONAL_OR_KEYWORD
VAR_POSITIONAL = _VAR_POSITIONAL
KEYWORD_ONLY = _KEYWORD_ONLY
VAR_KEYWORD = _VAR_KEYWORD
empty = _empty
def __init__(self, name, kind, default=_empty, annotation=_empty,
_partial_kwarg=False):
if kind not in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD,
_VAR_POSITIONAL, _KEYWORD_ONLY, _VAR_KEYWORD):
raise ValueError("invalid value for 'Parameter.kind' attribute")
self._kind = kind
if default is not _empty:
if kind in (_VAR_POSITIONAL, _VAR_KEYWORD):
msg = '{0} parameters cannot have default values'.format(kind)
raise ValueError(msg)
self._default = default
self._annotation = annotation
if name is None:
if kind != _POSITIONAL_ONLY:
raise ValueError("None is not a valid name for a "
"non-positional-only parameter")
self._name = name
else:
name = str(name)
if kind != _POSITIONAL_ONLY and not re.match(r'[a-z_]\w*$', name, re.I):
msg = '{0!r} is not a valid parameter name'.format(name)
raise ValueError(msg)
self._name = name
self._partial_kwarg = _partial_kwarg
@property
def name(self):
return self._name
@property
def default(self):
return self._default
@property
def annotation(self):
return self._annotation
@property
def kind(self):
return self._kind
def replace(self, name=_void, kind=_void, annotation=_void,
default=_void, _partial_kwarg=_void):
'''Creates a customized copy of the Parameter.'''
if name is _void:
name = self._name
if kind is _void:
kind = self._kind
if annotation is _void:
annotation = self._annotation
if default is _void:
default = self._default
if _partial_kwarg is _void:
_partial_kwarg = self._partial_kwarg
return type(self)(name, kind, default=default, annotation=annotation,
_partial_kwarg=_partial_kwarg)
def __str__(self):
kind = self.kind
formatted = self._name
if kind == _POSITIONAL_ONLY:
if formatted is None:
formatted = ''
formatted = '<{0}>'.format(formatted)
# Add annotation and default value
if self._annotation is not _empty:
formatted = '{0}:{1}'.format(formatted,
formatannotation(self._annotation))
if self._default is not _empty:
formatted = '{0}={1}'.format(formatted, repr(self._default))
if kind == _VAR_POSITIONAL:
formatted = '*' + formatted
elif kind == _VAR_KEYWORD:
formatted = '**' + formatted
return formatted
def __repr__(self):
return '<{0} at {1:#x} {2!r}>'.format(self.__class__.__name__,
id(self), self.name)
def __hash__(self):
msg = "unhashable type: '{0}'".format(self.__class__.__name__)
raise TypeError(msg)
def __eq__(self, other):
return (issubclass(other.__class__, Parameter) and
self._name == other._name and
self._kind == other._kind and
self._default == other._default and
self._annotation == other._annotation)
def __ne__(self, other):
return not self.__eq__(other)
class BoundArguments(object):
'''Result of `Signature.bind` call. Holds the mapping of arguments
to the function's parameters.
Has the following public attributes:
* arguments : OrderedDict
An ordered mutable mapping of parameters' names to arguments' values.
Does not contain arguments' default values.
* signature : Signature
The Signature object that created this instance.
* args : tuple
Tuple of positional arguments values.
* kwargs : dict
Dict of keyword arguments values.
'''
def __init__(self, signature, arguments):
self.arguments = arguments
self._signature = signature
@property
def signature(self):
return self._signature
@property
def args(self):
args = []
for param_name, param in self._signature.parameters.items():
if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
param._partial_kwarg):
# Keyword arguments mapped by 'functools.partial'
# (Parameter._partial_kwarg is True) are mapped
# in 'BoundArguments.kwargs', along with VAR_KEYWORD &
# KEYWORD_ONLY
break
try:
arg = self.arguments[param_name]
except KeyError:
# We're done here. Other arguments
# will be mapped in 'BoundArguments.kwargs'
break
else:
if param.kind == _VAR_POSITIONAL:
# *args
args.extend(arg)
else:
# plain argument
args.append(arg)
return tuple(args)
@property
def kwargs(self):
kwargs = {}
kwargs_started = False
for param_name, param in self._signature.parameters.items():
if not kwargs_started:
if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
param._partial_kwarg):
kwargs_started = True
else:
if param_name not in self.arguments:
kwargs_started = True
continue
if not kwargs_started:
continue
try:
arg = self.arguments[param_name]
except KeyError:
pass
else:
if param.kind == _VAR_KEYWORD:
# **kwargs
kwargs.update(arg)
else:
# plain keyword argument
kwargs[param_name] = arg
return kwargs
def __hash__(self):
msg = "unhashable type: '{0}'".format(self.__class__.__name__)
raise TypeError(msg)
def __eq__(self, other):
return (issubclass(other.__class__, BoundArguments) and
self.signature == other.signature and
self.arguments == other.arguments)
def __ne__(self, other):
return not self.__eq__(other)
class Signature(object):
'''A Signature object represents the overall signature of a function.
It stores a Parameter object for each parameter accepted by the
function, as well as information specific to the function itself.
A Signature object has the following public attributes and methods:
* parameters : OrderedDict
An ordered mapping of parameters' names to the corresponding
Parameter objects (keyword-only arguments are in the same order
as listed in `code.co_varnames`).
* return_annotation : object
The annotation for the return type of the function if specified.
If the function has no annotation for its return type, this
attribute is not set.
* bind(*args, **kwargs) -> BoundArguments
Creates a mapping from positional and keyword arguments to
parameters.
* bind_partial(*args, **kwargs) -> BoundArguments
Creates a partial mapping from positional and keyword arguments
to parameters (simulating 'functools.partial' behavior.)
'''
__slots__ = ('_return_annotation', '_parameters')
_parameter_cls = Parameter
_bound_arguments_cls = BoundArguments
empty = _empty
def __init__(self, parameters=None, return_annotation=_empty,
__validate_parameters__=True):
'''Constructs Signature from the given list of Parameter
objects and 'return_annotation'. All arguments are optional.
'''
if parameters is None:
params = OrderedDict()
else:
if __validate_parameters__:
params = OrderedDict()
top_kind = _POSITIONAL_ONLY
for idx, param in enumerate(parameters):
kind = param.kind
if kind < top_kind:
msg = 'wrong parameter order: {0} before {1}'
msg = msg.format(top_kind, param.kind)
raise ValueError(msg)
else:
top_kind = kind
name = param.name
if name is None:
name = str(idx)
param = param.replace(name=name)
if name in params:
msg = 'duplicate parameter name: {0!r}'.format(name)
raise ValueError(msg)
params[name] = param
else:
params = OrderedDict(((param.name, param)
for param in parameters))
self._parameters = params
self._return_annotation = return_annotation
@classmethod
def from_function(cls, func):
'''Constructs Signature for the given python function'''
if not isinstance(func, types.FunctionType):
raise TypeError('{0!r} is not a Python function'.format(func))
Parameter = cls._parameter_cls
# Parameter information.
func_code = func.__code__
pos_count = func_code.co_argcount
arg_names = func_code.co_varnames
positional = tuple(arg_names[:pos_count])
keyword_only_count = getattr(func_code, 'co_kwonlyargcount', 0)
keyword_only = arg_names[pos_count:(pos_count + keyword_only_count)]
annotations = getattr(func, '__annotations__', {})
defaults = func.__defaults__
kwdefaults = getattr(func, '__kwdefaults__', None)
if defaults:
pos_default_count = len(defaults)
else:
pos_default_count = 0
parameters = []
# Non-keyword-only parameters w/o defaults.
non_default_count = pos_count - pos_default_count
for name in positional[:non_default_count]:
annotation = annotations.get(name, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_POSITIONAL_OR_KEYWORD))
# ... w/ defaults.
for offset, name in enumerate(positional[non_default_count:]):
annotation = annotations.get(name, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_POSITIONAL_OR_KEYWORD,
default=defaults[offset]))
# *args
if func_code.co_flags & 0x04:
name = arg_names[pos_count + keyword_only_count]
annotation = annotations.get(name, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_VAR_POSITIONAL))
# Keyword-only parameters.
for name in keyword_only:
default = _empty
if kwdefaults is not None:
default = kwdefaults.get(name, _empty)
annotation = annotations.get(name, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_KEYWORD_ONLY,
default=default))
# **kwargs
if func_code.co_flags & 0x08:
index = pos_count + keyword_only_count
if func_code.co_flags & 0x04:
index += 1
name = arg_names[index]
annotation = annotations.get(name, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_VAR_KEYWORD))
return cls(parameters,
return_annotation=annotations.get('return', _empty),
__validate_parameters__=False)
@property
def parameters(self):
try:
return types.MappingProxyType(self._parameters)
except AttributeError:
return OrderedDict(self._parameters.items())
@property
def return_annotation(self):
return self._return_annotation
def replace(self, parameters=_void, return_annotation=_void):
'''Creates a customized copy of the Signature.
Pass 'parameters' and/or 'return_annotation' arguments
to override them in the new copy.
'''
if parameters is _void:
parameters = self.parameters.values()
if return_annotation is _void:
return_annotation = self._return_annotation
return type(self)(parameters,
return_annotation=return_annotation)
def __hash__(self):
msg = "unhashable type: '{0}'".format(self.__class__.__name__)
raise TypeError(msg)
def __eq__(self, other):
if (not issubclass(type(other), Signature) or
self.return_annotation != other.return_annotation or
len(self.parameters) != len(other.parameters)):
return False
other_positions = dict((param, idx)
for idx, param in enumerate(other.parameters.keys()))
for idx, (param_name, param) in enumerate(self.parameters.items()):
if param.kind == _KEYWORD_ONLY:
try:
other_param = other.parameters[param_name]
except KeyError:
return False
else:
if param != other_param:
return False
else:
try:
other_idx = other_positions[param_name]
except KeyError:
return False
else:
if (idx != other_idx or
param != other.parameters[param_name]):
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
def _bind(self, args, kwargs, partial=False):
'''Private method. Don't use directly.'''
arguments = OrderedDict()
parameters = iter(self.parameters.values())
parameters_ex = ()
arg_vals = iter(args)
if partial:
# Support for binding arguments to 'functools.partial' objects.
# See 'functools.partial' case in 'signature()' implementation
# for details.
for param_name, param in self.parameters.items():
if (param._partial_kwarg and param_name not in kwargs):
# Simulating 'functools.partial' behavior
kwargs[param_name] = param.default
while True:
# Let's iterate through the positional arguments and corresponding
# parameters
try:
arg_val = next(arg_vals)
except StopIteration:
# No more positional arguments
try:
param = next(parameters)
except StopIteration:
# No more parameters. That's it. Just need to check that
# we have no `kwargs` after this while loop
break
else:
if param.kind == _VAR_POSITIONAL:
# That's OK, just empty *args. Let's start parsing
# kwargs
break
elif param.name in kwargs:
if param.kind == _POSITIONAL_ONLY:
msg = '{arg!r} parameter is positional only, ' \
'but was passed as a keyword'
msg = msg.format(arg=param.name)
raise TypeError(msg)
parameters_ex = (param,)
break
elif (param.kind == _VAR_KEYWORD or
param.default is not _empty):
# That's fine too - we have a default value for this
# parameter. So, lets start parsing `kwargs`, starting
# with the current parameter
parameters_ex = (param,)
break
else:
if partial:
parameters_ex = (param,)
break
else:
msg = '{arg!r} parameter lacking default value'
msg = msg.format(arg=param.name)
raise TypeError(msg)
else:
# We have a positional argument to process
try:
param = next(parameters)
except StopIteration:
raise TypeError('too many positional arguments')
else:
if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY):
# Looks like we have no parameter for this positional
# argument
raise TypeError('too many positional arguments')
if param.kind == _VAR_POSITIONAL:
# We have an '*args'-like argument, let's fill it with
# all positional arguments we have left and move on to
# the next phase
values = [arg_val]
values.extend(arg_vals)
arguments[param.name] = tuple(values)
break
if param.name in kwargs:
raise TypeError('multiple values for argument '
'{arg!r}'.format(arg=param.name))
arguments[param.name] = arg_val
# Now, we iterate through the remaining parameters to process
# keyword arguments
kwargs_param = None
for param in itertools.chain(parameters_ex, parameters):
if param.kind == _POSITIONAL_ONLY:
# This should never happen in case of a properly built
# Signature object (but let's have this check here
# to ensure correct behaviour just in case)
raise TypeError('{arg!r} parameter is positional only, '
'but was passed as a keyword'. \
format(arg=param.name))
if param.kind == _VAR_KEYWORD:
# Memorize that we have a '**kwargs'-like parameter
kwargs_param = param
continue
param_name = param.name
try:
arg_val = kwargs.pop(param_name)
except KeyError:
# We have no value for this parameter. It's fine though,
# if it has a default value, or it is an '*args'-like
# parameter, left alone by the processing of positional
# arguments.
if (not partial and param.kind != _VAR_POSITIONAL and
param.default is _empty):
raise TypeError('{arg!r} parameter lacking default value'. \
format(arg=param_name))
else:
arguments[param_name] = arg_val
if kwargs:
if kwargs_param is not None:
# Process our '**kwargs'-like parameter
arguments[kwargs_param.name] = kwargs
else:
raise TypeError('too many keyword arguments')
return self._bound_arguments_cls(self, arguments)
def bind(self, *args, **kwargs):
'''Get a BoundArguments object, that maps the passed `args`
and `kwargs` to the function's signature. Raises `TypeError`
if the passed arguments can not be bound.
'''
return self._bind(args, kwargs)
def bind_partial(self, *args, **kwargs):
'''Get a BoundArguments object, that partially maps the
passed `args` and `kwargs` to the function's signature.
Raises `TypeError` if the passed arguments can not be bound.
'''
return self._bind(args, kwargs, partial=True)
def __str__(self):
result = []
render_kw_only_separator = True
for idx, param in enumerate(self.parameters.values()):
formatted = str(param)
kind = param.kind
if kind == _VAR_POSITIONAL:
# OK, we have an '*args'-like parameter, so we won't need
# a '*' to separate keyword-only arguments
render_kw_only_separator = False
elif kind == _KEYWORD_ONLY and render_kw_only_separator:
# We have a keyword-only parameter to render and we haven't
# rendered an '*args'-like parameter before, so add a '*'
# separator to the parameters list ("foo(arg1, *, arg2)" case)
result.append('*')
# This condition should be only triggered once, so
# reset the flag
render_kw_only_separator = False
result.append(formatted)
rendered = '({0})'.format(', '.join(result))
if self.return_annotation is not _empty:
anno = formatannotation(self.return_annotation)
rendered += ' -> {0}'.format(anno)
return rendered

View File

@ -21,6 +21,7 @@ def test__is_dunder():
for name in non_dunder_names: for name in non_dunder_names:
assert _is_dunder(name) is False assert _is_dunder(name) is False
def test__is_sunder(): def test__is_sunder():
sunder_names = [ sunder_names = [
'_i_', '_i_',

View File

@ -2,18 +2,13 @@ import re
from collections import Iterable, OrderedDict from collections import Iterable, OrderedDict
from functools import partial from functools import partial
import six
from graphql_relay import connection_from_list from graphql_relay import connection_from_list
from promise import Promise, is_thenable from promise import Promise, is_thenable
from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar,
Union) String, Union)
from ..types.field import Field from ..types.field import Field
from ..types.objecttype import ObjectType, ObjectTypeMeta from ..types.objecttype import ObjectType, ObjectTypeOptions
from ..types.options import Options
from ..utils.is_base_type import is_base_type
from ..utils.props import props
from .node import is_node from .node import is_node
@ -41,56 +36,50 @@ class PageInfo(ObjectType):
) )
class ConnectionMeta(ObjectTypeMeta): class ConnectionOptions(ObjectTypeOptions):
node = None
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of Model
# (excluding Model class itself).
if not is_base_type(bases, ConnectionMeta):
return type.__new__(cls, name, bases, attrs)
options = Options( class Connection(ObjectType):
attrs.pop('Meta', None),
name=name,
description=None,
node=None,
)
options.interfaces = ()
options.local_fields = OrderedDict()
assert options.node, 'You have to provide a node in {}.Meta'.format(cls.__name__) class Meta:
assert issubclass(options.node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)), ( abstract = True
@classmethod
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
_meta = ConnectionOptions(cls)
assert node, 'You have to provide a node in {}.Meta'.format(cls.__name__)
assert issubclass(node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)), (
'Received incompatible node "{}" for Connection {}.' 'Received incompatible node "{}" for Connection {}.'
).format(options.node, name) ).format(node, cls.__name__)
base_name = re.sub('Connection$', '', options.name) or options.node._meta.name base_name = re.sub('Connection$', '', name or cls.__name__) or node._meta.name
if not options.name: if not name:
options.name = '{}Connection'.format(base_name) name = '{}Connection'.format(base_name)
edge_class = attrs.pop('Edge', None) edge_class = getattr(cls, 'Edge', None)
_node = node
class EdgeBase(AbstractType): class EdgeBase(object):
node = Field(options.node, description='The item at the end of the edge') node = Field(_node, description='The item at the end of the edge')
cursor = String(required=True, description='A cursor for use in pagination') cursor = String(required=True, description='A cursor for use in pagination')
edge_name = '{}Edge'.format(base_name) edge_name = '{}Edge'.format(base_name)
if edge_class and issubclass(edge_class, AbstractType): if edge_class:
edge = type(edge_name, (EdgeBase, edge_class, ObjectType, ), {}) edge_bases = (edge_class, EdgeBase, ObjectType,)
else: else:
edge_attrs = props(edge_class) if edge_class else {} edge_bases = (EdgeBase, ObjectType,)
edge = type(edge_name, (EdgeBase, ObjectType, ), edge_attrs)
class ConnectionBase(AbstractType): edge = type(edge_name, edge_bases, {})
page_info = Field(PageInfo, name='pageInfo', required=True) cls.Edge = edge
edges = NonNull(List(edge))
bases = (ConnectionBase, ) + bases _meta.name = name
attrs = dict(attrs, _meta=options, Edge=edge) _meta.node = node
return ObjectTypeMeta.__new__(cls, name, bases, attrs) _meta.fields = OrderedDict([
('page_info', Field(PageInfo, name='pageInfo', required=True)),
('edges', Field(NonNull(List(edge)))),
class Connection(six.with_metaclass(ConnectionMeta, ObjectType)): ])
pass return super(Connection, cls).__init_subclass_with_meta__(_meta=_meta, **options)
class IterableConnectionField(Field): class IterableConnectionField(Field):
@ -109,10 +98,13 @@ class IterableConnectionField(Field):
@property @property
def type(self): def type(self):
type = super(IterableConnectionField, self).type type = super(IterableConnectionField, self).type
if is_node(type):
connection_type = type.Connection
else:
connection_type = type connection_type = type
if is_node(type):
raise Exception(
"ConnectionField's now need a explicit ConnectionType for Nodes.\n"
"Read more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#node-connections"
)
assert issubclass(connection_type, Connection), ( assert issubclass(connection_type, Connection), (
'{} type have to be a subclass of Connection. Received "{}".' '{} type have to be a subclass of Connection. Received "{}".'
).format(self.__class__.__name__, connection_type) ).format(self.__class__.__name__, connection_type)
@ -138,8 +130,8 @@ class IterableConnectionField(Field):
return connection return connection
@classmethod @classmethod
def connection_resolver(cls, resolver, connection_type, root, args, context, info): def connection_resolver(cls, resolver, connection_type, root, info, **args):
resolved = resolver(root, args, context, info) resolved = resolver(root, info, **args)
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):

View File

@ -1,68 +1,72 @@
import re import re
from functools import partial from collections import OrderedDict
import six from promise import Promise, is_thenable
from promise import Promise from ..types import Field, InputObjectType, String
from ..types.mutation import Mutation
from ..types import Field, AbstractType, Argument, InputObjectType, String
from ..types.mutation import Mutation, MutationMeta
from ..types.objecttype import ObjectTypeMeta
from ..utils.is_base_type import is_base_type
from ..utils.props import props
class ClientIDMutationMeta(MutationMeta): class ClientIDMutation(Mutation):
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of
# Mutation
if not is_base_type(bases, ClientIDMutationMeta):
return type.__new__(cls, name, bases, attrs)
input_class = attrs.pop('Input', None) class Meta:
base_name = re.sub('Payload$', '', name) abstract = True
if 'client_mutation_id' not in attrs:
attrs['client_mutation_id'] = String(name='clientMutationId') @classmethod
cls = ObjectTypeMeta.__new__(cls, '{}Payload'.format(base_name), bases, def __init_subclass_with_meta__(cls, output=None, input_fields=None,
attrs) arguments=None, name=None, **options):
input_class = getattr(cls, 'Input', None)
base_name = re.sub('Payload$', '', name or cls.__name__)
assert not output, "Can't specify any output"
assert not arguments, "Can't specify any arguments"
bases = (InputObjectType, )
if input_class:
bases += (input_class, )
if not input_fields:
input_fields = {}
cls.Input = type(
'{}Input'.format(base_name),
bases,
OrderedDict(input_fields, client_mutation_id=String(
name='clientMutationId'))
)
arguments = OrderedDict(
input=cls.Input(required=True)
# 'client_mutation_id': String(name='clientMutationId')
)
mutate_and_get_payload = getattr(cls, 'mutate_and_get_payload', None) mutate_and_get_payload = getattr(cls, 'mutate_and_get_payload', None)
if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__: if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__:
assert mutate_and_get_payload, ( assert mutate_and_get_payload, (
"{}.mutate_and_get_payload method is required" "{name}.mutate_and_get_payload method is required"
" in a ClientIDMutation.").format(name) " in a ClientIDMutation.").format(name=name or cls.__name__)
input_attrs = {}
bases = ()
if not input_class:
input_attrs = {}
elif not issubclass(input_class, AbstractType):
input_attrs = props(input_class)
else:
bases += (input_class, )
input_attrs['client_mutation_id'] = String(name='clientMutationId')
cls.Input = type('{}Input'.format(base_name),
bases + (InputObjectType, ), input_attrs)
output_class = getattr(cls, 'Output', cls)
cls.Field = partial(
Field,
output_class,
resolver=cls.mutate,
input=Argument(cls.Input, required=True))
return cls
if not name:
name = '{}Payload'.format(base_name)
super(ClientIDMutation, cls).__init_subclass_with_meta__(
output=None, arguments=arguments, name=name, **options)
cls._meta.fields['client_mutation_id'] = (
Field(String, name='clientMutationId')
)
class ClientIDMutation(six.with_metaclass(ClientIDMutationMeta, Mutation)):
@classmethod @classmethod
def mutate(cls, root, args, context, info): def mutate(cls, root, info, input):
input = args.get('input')
def on_resolve(payload): def on_resolve(payload):
try: try:
payload.client_mutation_id = input.get('clientMutationId') payload.client_mutation_id = input.get('client_mutation_id')
except: except:
raise Exception( raise Exception(
('Cannot set client_mutation_id in the payload object {}' ('Cannot set client_mutation_id in the payload object {}'
).format(repr(payload))) ).format(repr(payload)))
return payload return payload
return Promise.resolve( result = cls.mutate_and_get_payload(root, info, **input)
cls.mutate_and_get_payload(input, context, info)).then(on_resolve) if is_thenable(result):
return Promise.resolve(result).then(on_resolve)
return on_resolve(result)

View File

@ -1,12 +1,11 @@
from collections import OrderedDict
from functools import partial from functools import partial
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.interface import InterfaceOptions
from ..types.utils import get_type from ..types.utils import get_type
from ..types.interface import InterfaceMeta
def is_node(objecttype): def is_node(objecttype):
@ -22,18 +21,6 @@ def is_node(objecttype):
return False return False
def get_default_connection(cls):
from .connection import Connection
assert issubclass(cls, ObjectType), (
'Can only get connection type on implemented Nodes.'
)
class Meta:
node = cls
return type('{}Connection'.format(cls.__name__), (Connection,), {'Meta': Meta})
class GlobalID(Field): class GlobalID(Field):
def __init__(self, node=None, parent_type=None, required=True, *args, **kwargs): def __init__(self, node=None, parent_type=None, required=True, *args, **kwargs):
@ -42,21 +29,15 @@ class GlobalID(Field):
self.parent_type_name = parent_type._meta.name if parent_type else None self.parent_type_name = parent_type._meta.name if parent_type else None
@staticmethod @staticmethod
def id_resolver(parent_resolver, node, root, args, context, info, parent_type_name=None): def id_resolver(parent_resolver, node, root, info, parent_type_name=None, **args):
type_id = parent_resolver(root, args, context, info) type_id = parent_resolver(root, info, **args)
parent_type_name = parent_type_name or info.parent_type.name parent_type_name = parent_type_name or info.parent_type.name
return node.to_global_id(parent_type_name, type_id) # root._meta.name return node.to_global_id(parent_type_name, type_id) # root._meta.name
def get_resolver(self, parent_resolver): def get_resolver(self, parent_resolver):
return partial(self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name) return partial(
self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name
)
class NodeMeta(InterfaceMeta):
def __new__(cls, name, bases, attrs):
cls = InterfaceMeta.__new__(cls, name, bases, attrs)
cls._meta.fields['id'] = GlobalID(cls, description='The ID of the object.')
return cls
class NodeField(Field): class NodeField(Field):
@ -75,10 +56,24 @@ class NodeField(Field):
) )
def get_resolver(self, parent_resolver): def get_resolver(self, parent_resolver):
return partial(self.node_type.node_resolver, only_type=get_type(self.field_type)) return partial(self.node_type.node_resolver, get_type(self.field_type))
class Node(six.with_metaclass(NodeMeta, Interface)): class AbstractNode(Interface):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(cls, **options):
_meta = InterfaceOptions(cls)
_meta.fields = OrderedDict(
id=GlobalID(cls, description='The ID of the object.')
)
super(AbstractNode, cls).__init_subclass_with_meta__(_meta=_meta, **options)
class Node(AbstractNode):
'''An object with an ID''' '''An object with an ID'''
@classmethod @classmethod
@ -86,11 +81,11 @@ class Node(six.with_metaclass(NodeMeta, Interface)):
return NodeField(cls, *args, **kwargs) return NodeField(cls, *args, **kwargs)
@classmethod @classmethod
def node_resolver(cls, root, args, context, info, only_type=None): def node_resolver(cls, only_type, root, info, id):
return cls.get_node_from_global_id(args.get('id'), context, info, only_type) return cls.get_node_from_global_id(info, id, only_type=only_type)
@classmethod @classmethod
def get_node_from_global_id(cls, global_id, context, info, only_type=None): def get_node_from_global_id(cls, info, global_id, only_type=None):
try: try:
_type, _id = cls.from_global_id(global_id) _type, _id = cls.from_global_id(global_id)
graphene_type = info.schema.get_type(_type).graphene_type graphene_type = info.schema.get_type(_type).graphene_type
@ -108,7 +103,7 @@ class Node(six.with_metaclass(NodeMeta, Interface)):
get_node = getattr(graphene_type, 'get_node', None) get_node = getattr(graphene_type, 'get_node', None)
if get_node: if get_node:
return get_node(_id, context, info) return get_node(info, _id)
@classmethod @classmethod
def from_global_id(cls, global_id): def from_global_id(cls, global_id):
@ -117,11 +112,3 @@ class Node(six.with_metaclass(NodeMeta, Interface)):
@classmethod @classmethod
def to_global_id(cls, type, id): def to_global_id(cls, type, id):
return to_global_id(type, id) return to_global_id(type, id)
@classmethod
def implements(cls, objecttype):
get_connection = getattr(objecttype, 'get_connection', None)
if not get_connection:
get_connection = partial(get_default_connection, objecttype)
objecttype.Connection = get_connection()

View File

@ -1,6 +1,7 @@
import pytest
from ...types import AbstractType, Field, List, NonNull, ObjectType, String, Argument, Int from ...types import Argument, Field, Int, List, NonNull, ObjectType, String
from ..connection import Connection, PageInfo, ConnectionField from ..connection import Connection, ConnectionField, PageInfo
from ..node import Node from ..node import Node
@ -38,7 +39,7 @@ def test_connection():
def test_connection_inherit_abstracttype(): def test_connection_inherit_abstracttype():
class BaseConnection(AbstractType): class BaseConnection(object):
extra = String() extra = String()
class MyObjectConnection(BaseConnection, Connection): class MyObjectConnection(BaseConnection, Connection):
@ -73,7 +74,7 @@ def test_edge():
def test_edge_with_bases(): def test_edge_with_bases():
class BaseEdge(AbstractType): class BaseEdge(object):
extra = String() extra = String()
class MyObjectConnection(Connection): class MyObjectConnection(Connection):
@ -96,16 +97,6 @@ def test_edge_with_bases():
assert edge_fields['other'].type == String assert edge_fields['other'].type == String
def test_edge_on_node():
Edge = MyObject.Connection.Edge
assert Edge._meta.name == 'MyObjectEdge'
edge_fields = Edge._meta.fields
assert list(edge_fields.keys()) == ['node', 'cursor']
assert isinstance(edge_fields['node'], Field)
assert edge_fields['node'].type == MyObject
def test_pageinfo(): def test_pageinfo():
assert PageInfo._meta.name == 'PageInfo' assert PageInfo._meta.name == 'PageInfo'
fields = PageInfo._meta.fields fields = PageInfo._meta.fields
@ -114,6 +105,7 @@ def test_pageinfo():
def test_connectionfield(): def test_connectionfield():
class MyObjectConnection(Connection): class MyObjectConnection(Connection):
class Meta: class Meta:
node = MyObject node = MyObject
@ -126,8 +118,16 @@ def test_connectionfield():
} }
def test_connectionfield_node_deprecated():
field = ConnectionField(MyObject)
with pytest.raises(Exception) as exc_info:
field.type
assert "ConnectionField's now need a explicit ConnectionType for Nodes." in str(exc_info.value)
def test_connectionfield_custom_args(): def test_connectionfield_custom_args():
class MyObjectConnection(Connection): class MyObjectConnection(Connection):
class Meta: class Meta:
node = MyObject node = MyObject

View File

@ -4,7 +4,7 @@ from graphql_relay.utils import base64
from promise import Promise from promise import Promise
from ...types import ObjectType, Schema, String from ...types import ObjectType, Schema, String
from ..connection import ConnectionField, PageInfo from ..connection import Connection, ConnectionField, PageInfo
from ..node import Node from ..node import Node
letter_chars = ['A', 'B', 'C', 'D', 'E'] letter_chars = ['A', 'B', 'C', 'D', 'E']
@ -18,27 +18,33 @@ class Letter(ObjectType):
letter = String() letter = String()
class LetterConnection(Connection):
class Meta:
node = Letter
class Query(ObjectType): class Query(ObjectType):
letters = ConnectionField(Letter) letters = ConnectionField(LetterConnection)
connection_letters = ConnectionField(Letter) connection_letters = ConnectionField(LetterConnection)
promise_letters = ConnectionField(Letter) promise_letters = ConnectionField(LetterConnection)
node = Node.Field() node = Node.Field()
def resolve_letters(self, args, context, info): def resolve_letters(self, info, **args):
return list(letters.values()) return list(letters.values())
def resolve_promise_letters(self, args, context, info): def resolve_promise_letters(self, info, **args):
return Promise.resolve(list(letters.values())) return Promise.resolve(list(letters.values()))
def resolve_connection_letters(self, args, context, info): def resolve_connection_letters(self, info, **args):
return Letter.Connection( return LetterConnection(
page_info=PageInfo( page_info=PageInfo(
has_next_page=True, has_next_page=True,
has_previous_page=False has_previous_page=False
), ),
edges=[ edges=[
Letter.Connection.Edge( LetterConnection.Edge(
node=Letter(id=0, letter='A'), node=Letter(id=0, letter='A'),
cursor='a-cursor' cursor='a-cursor'
), ),

View File

@ -1,8 +1,8 @@
from graphql_relay import to_global_id from graphql_relay import to_global_id
from ..node import Node, GlobalID from ...types import ID, NonNull, ObjectType, String
from ...types import NonNull, ID, ObjectType, String
from ...types.definitions import GrapheneObjectType from ...types.definitions import GrapheneObjectType
from ..node import GlobalID, Node
class CustomNode(Node): class CustomNode(Node):
@ -48,7 +48,7 @@ def test_global_id_defaults_to_info_parent_type():
my_id = '1' my_id = '1'
gid = GlobalID() gid = GlobalID()
id_resolver = gid.get_resolver(lambda *_: my_id) id_resolver = gid.get_resolver(lambda *_: my_id)
my_global_id = id_resolver(None, None, None, Info(User)) my_global_id = id_resolver(None, Info(User))
assert my_global_id == to_global_id(User._meta.name, my_id) assert my_global_id == to_global_id(User._meta.name, my_id)
@ -56,5 +56,5 @@ def test_global_id_allows_setting_customer_parent_type():
my_id = '1' my_id = '1'
gid = GlobalID(parent_type=User) gid = GlobalID(parent_type=User)
id_resolver = gid.get_resolver(lambda *_: my_id) id_resolver = gid.get_resolver(lambda *_: my_id)
my_global_id = id_resolver(None, None, None, None) my_global_id = id_resolver(None, None)
assert my_global_id == to_global_id(User._meta.name, my_id) assert my_global_id == to_global_id(User._meta.name, my_id)

View File

@ -1,62 +1,86 @@
import pytest import pytest
from ...types import (AbstractType, Argument, Field, InputField,
InputObjectType, NonNull, ObjectType, Schema)
from ...types.scalars import String
from ..mutation import ClientIDMutation
from ..node import Node
from promise import Promise from promise import Promise
from ...types import (ID, Argument, Field, InputField, InputObjectType,
NonNull, ObjectType, Schema)
from ...types.scalars import String
from ..mutation import ClientIDMutation
class SharedFields(AbstractType):
class SharedFields(object):
shared = String() shared = String()
class MyNode(ObjectType): class MyNode(ObjectType):
class Meta: # class Meta:
interfaces = (Node, ) # interfaces = (Node, )
id = ID()
name = String() name = String()
class SaySomething(ClientIDMutation): class SaySomething(ClientIDMutation):
class Input: class Input:
what = String() what = String()
phrase = String() phrase = String()
@staticmethod @staticmethod
def mutate_and_get_payload(args, context, info): def mutate_and_get_payload(self, info, what, client_mutation_id=None):
what = args.get('what')
return SaySomething(phrase=str(what)) return SaySomething(phrase=str(what))
class SaySomethingPromise(ClientIDMutation): class FixedSaySomething(object):
__slots__ = 'phrase',
def __init__(self, phrase):
self.phrase = phrase
class SaySomethingFixed(ClientIDMutation):
class Input: class Input:
what = String() what = String()
phrase = String() phrase = String()
@staticmethod @staticmethod
def mutate_and_get_payload(args, context, info): def mutate_and_get_payload(self, info, what, client_mutation_id=None):
what = args.get('what') return FixedSaySomething(phrase=str(what))
class SaySomethingPromise(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return Promise.resolve(SaySomething(phrase=str(what))) return Promise.resolve(SaySomething(phrase=str(what)))
# MyEdge = MyNode.Connection.Edge
class MyEdge(ObjectType):
node = Field(MyNode)
cursor = String()
class OtherMutation(ClientIDMutation): class OtherMutation(ClientIDMutation):
class Input(SharedFields): class Input(SharedFields):
additional_field = String() additional_field = String()
name = String() name = String()
my_node_edge = Field(MyNode.Connection.Edge) my_node_edge = Field(MyEdge)
@classmethod @staticmethod
def mutate_and_get_payload(cls, args, context, info): def mutate_and_get_payload(self, info, shared='', additional_field='', client_mutation_id=None):
shared = args.get('shared', '') edge_type = MyEdge
additionalField = args.get('additionalField', '')
edge_type = MyNode.Connection.Edge
return OtherMutation( return OtherMutation(
name=shared + additionalField, name=shared + additional_field,
my_node_edge=edge_type(cursor='1', node=MyNode(name='name'))) my_node_edge=edge_type(cursor='1', node=MyNode(name='name')))
@ -66,6 +90,7 @@ class RootQuery(ObjectType):
class Mutation(ObjectType): class Mutation(ObjectType):
say = SaySomething.Field() say = SaySomething.Field()
say_fixed = SaySomethingFixed.Field()
say_promise = SaySomethingPromise.Field() say_promise = SaySomethingPromise.Field()
other = OtherMutation.Field() other = OtherMutation.Field()
@ -86,6 +111,7 @@ def test_no_mutate_and_get_payload():
def test_mutation(): def test_mutation():
fields = SaySomething._meta.fields fields = SaySomething._meta.fields
assert list(fields.keys()) == ['phrase', 'client_mutation_id'] assert list(fields.keys()) == ['phrase', 'client_mutation_id']
assert SaySomething._meta.name == "SaySomethingPayload"
assert isinstance(fields['phrase'], Field) assert isinstance(fields['phrase'], Field)
field = SaySomething.Field() field = SaySomething.Field()
assert field.type == SaySomething assert field.type == SaySomething
@ -146,7 +172,14 @@ def test_node_query():
assert executed.data == {'say': {'phrase': 'hello'}} assert executed.data == {'say': {'phrase': 'hello'}}
def test_node_query(): def test_node_query_fixed():
executed = schema.execute(
'mutation a { sayFixed(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert "Cannot set client_mutation_id in the payload object" in str(executed.errors[0])
def test_node_query_promise():
executed = schema.execute( executed = schema.execute(
'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }' 'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
) )

View File

@ -2,12 +2,11 @@ from collections import OrderedDict
from graphql_relay import to_global_id from graphql_relay import to_global_id
from ...types import AbstractType, ObjectType, Schema, String from ...types import ObjectType, Schema, String
from ..connection import Connection from ..node import Node, is_node
from ..node import Node
class SharedNodeFields(AbstractType): class SharedNodeFields(object):
shared = String() shared = String()
something_else = String() something_else = String()
@ -23,7 +22,7 @@ class MyNode(ObjectType):
name = String() name = String()
@staticmethod @staticmethod
def get_node(id, *_): def get_node(info, id):
return MyNode(name=str(id)) return MyNode(name=str(id))
@ -37,7 +36,7 @@ class MyOtherNode(SharedNodeFields, ObjectType):
return 'extra field info.' return 'extra field info.'
@staticmethod @staticmethod
def get_node(id, *_): def get_node(info, id):
return MyOtherNode(shared=str(id)) return MyOtherNode(shared=str(id))
@ -47,20 +46,14 @@ class RootQuery(ObjectType):
only_node = Node.Field(MyNode) only_node = Node.Field(MyNode)
only_node_lazy = Node.Field(lambda: MyNode) only_node_lazy = Node.Field(lambda: MyNode)
schema = Schema(query=RootQuery, types=[MyNode, MyOtherNode]) schema = Schema(query=RootQuery, types=[MyNode, MyOtherNode])
def test_node_good(): def test_node_good():
assert 'id' in MyNode._meta.fields assert 'id' in MyNode._meta.fields
assert is_node(MyNode)
assert not is_node(object)
def test_node_get_connection():
connection = MyNode.Connection
assert issubclass(connection, Connection)
def test_node_get_connection_dont_duplicate():
assert MyNode.Connection == MyNode.Connection
def test_node_query(): def test_node_query():
@ -80,6 +73,15 @@ def test_subclassed_node_query():
[('shared', '1'), ('extraField', 'extra field info.'), ('somethingElse', '----')])}) [('shared', '1'), ('extraField', 'extra field info.'), ('somethingElse', '----')])})
def test_node_requesting_non_node():
executed = schema.execute(
'{ node(id:"%s") { __typename } } ' % Node.to_global_id("RootQuery", 1)
)
assert executed.data == {
'node': None
}
def test_node_query_incorrect_id(): def test_node_query_incorrect_id():
executed = schema.execute( executed = schema.execute(
'{ node(id:"%s") { ... on MyNode { name } } }' % "something:2" '{ node(id:"%s") { ... on MyNode { name } } }' % "something:2"

View File

@ -15,7 +15,7 @@ class CustomNode(Node):
return id return id
@staticmethod @staticmethod
def get_node_from_global_id(id, context, info, only_type=None): def get_node_from_global_id(info, id, only_type=None):
assert info.schema == schema assert info.schema == schema
if id in user_data: if id in user_data:
return user_data.get(id) return user_data.get(id)

View File

@ -29,6 +29,7 @@ def format_execution_result(execution_result, format_error):
class Client(object): class Client(object):
def __init__(self, schema, format_error=None, **execute_options): def __init__(self, schema, format_error=None, **execute_options):
assert isinstance(schema, Schema) assert isinstance(schema, Schema)
self.schema = schema self.schema = schema

View File

@ -3,9 +3,11 @@
import graphene import graphene
from graphene import resolve_only_args from graphene import resolve_only_args
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
rand = graphene.String() rand = graphene.String()
class Success(graphene.ObjectType): class Success(graphene.ObjectType):
yeah = graphene.String() yeah = graphene.String()
@ -15,18 +17,19 @@ class Error(graphene.ObjectType):
class CreatePostResult(graphene.Union): class CreatePostResult(graphene.Union):
class Meta: class Meta:
types = [Success, Error] types = [Success, Error]
class CreatePost(graphene.Mutation): class CreatePost(graphene.Mutation):
class Input: class Input:
text = graphene.String(required=True) text = graphene.String(required=True)
result = graphene.Field(CreatePostResult) result = graphene.Field(CreatePostResult)
@resolve_only_args def mutate(self, info, text):
def mutate(self, text):
result = Success(yeah='yeah') result = Success(yeah='yeah')
return CreatePost(result=result) return CreatePost(result=result)
@ -37,6 +40,7 @@ class Mutations(graphene.ObjectType):
# tests.py # tests.py
def test_create_post(): def test_create_post():
query_string = ''' query_string = '''
mutation { mutation {

View File

@ -1,19 +1,25 @@
# https://github.com/graphql-python/graphene/issues/356 # https://github.com/graphql-python/graphene/issues/356
import pytest import pytest
import graphene import graphene
from graphene import relay from graphene import relay
class SomeTypeOne(graphene.ObjectType): class SomeTypeOne(graphene.ObjectType):
pass pass
class SomeTypeTwo(graphene.ObjectType): class SomeTypeTwo(graphene.ObjectType):
pass pass
class MyUnion(graphene.Union): class MyUnion(graphene.Union):
class Meta: class Meta:
types = (SomeTypeOne, SomeTypeTwo) types = (SomeTypeOne, SomeTypeTwo)
def test_issue(): def test_issue():
with pytest.raises(Exception) as exc_info: with pytest.raises(Exception) as exc_info:
class Query(graphene.ObjectType): class Query(graphene.ObjectType):

View File

@ -1,36 +1,25 @@
# https://github.com/graphql-python/graphene/issues/425 # https://github.com/graphql-python/graphene/issues/425
import six # Adapted for Graphene 2.0
from graphene.utils.is_base_type import is_base_type from graphene.types.objecttype import ObjectType, ObjectTypeOptions
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)): class SpecialOptions(ObjectTypeOptions):
pass other_attr = None
class SpecialObjectType(ObjectType):
@classmethod
def __init_subclass_with_meta__(cls, other_attr='default', **options):
_meta = SpecialOptions(cls)
_meta.other_attr = other_attr
super(SpecialObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options)
def test_special_objecttype_could_be_subclassed(): def test_special_objecttype_could_be_subclassed():
class MyType(SpecialObjectType): class MyType(SpecialObjectType):
class Meta: class Meta:
other_attr = 'yeah!' other_attr = 'yeah!'
@ -49,5 +38,5 @@ def test_special_objecttype_inherit_meta_options():
pass pass
assert MyType._meta.name == 'MyType' assert MyType._meta.name == 'MyType'
assert MyType._meta.default_resolver == None assert MyType._meta.default_resolver is None
assert MyType._meta.interfaces == () assert MyType._meta.interfaces == ()

View File

@ -1,14 +1,13 @@
# https://github.com/graphql-python/graphene/issues/313 # https://github.com/graphql-python/graphene/issues/313
import graphene import graphene
from graphene import resolve_only_args
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
some_field = graphene.String(from_=graphene.String(name="from")) some_field = graphene.String(from_=graphene.String(name="from"))
def resolve_some_field(_, args, context, infos): def resolve_some_field(self, info, from_=None):
return args.get("from_") return from_
def test_issue(): def test_issue():

View File

@ -1,10 +1,12 @@
# flake8: noqa # flake8: noqa
from graphql import ResolveInfo
from .objecttype import ObjectType from .objecttype import ObjectType
from .abstracttype import AbstractType
from .interface import Interface from .interface import Interface
from .mutation import Mutation from .mutation import Mutation
from .scalars import Scalar, String, ID, Int, Float, Boolean from .scalars import Scalar, String, ID, Int, Float, Boolean
from .json import JSONString
from .uuid import UUID
from .schema import Schema from .schema import Schema
from .structures import List, NonNull from .structures import List, NonNull
from .enum import Enum from .enum import Enum
@ -14,10 +16,13 @@ from .argument import Argument
from .inputobjecttype import InputObjectType from .inputobjecttype import InputObjectType
from .dynamic import Dynamic from .dynamic import Dynamic
from .union import Union from .union import Union
from .context import Context
# Deprecated
from .abstracttype import AbstractType
__all__ = [ __all__ = [
'AbstractType',
'ObjectType', 'ObjectType',
'InputObjectType', 'InputObjectType',
'Interface', 'Interface',
@ -31,10 +36,17 @@ __all__ = [
'ID', 'ID',
'Int', 'Int',
'Float', 'Float',
'JSONString',
'UUID',
'Boolean', 'Boolean',
'List', 'List',
'NonNull', 'NonNull',
'Argument', 'Argument',
'Dynamic', 'Dynamic',
'Union', 'Union',
'Context',
'ResolveInfo',
# Deprecated
'AbstractType',
] ]

View File

@ -1,41 +1,12 @@
import six from ..utils.subclass_with_meta import SubclassWithMeta
from ..utils.deprecated import warn_deprecation
from ..utils.is_base_type import is_base_type
from .options import Options
from .utils import get_base_fields, merge, yank_fields_from_attrs
class AbstractTypeMeta(type): class AbstractType(SubclassWithMeta):
'''
AbstractType Definition
When we want to share fields across multiple types, like a Interface, def __init_subclass__(cls, *args, **kwargs):
a ObjectType and a Input ObjectType we can use AbstractTypes for defining warn_deprecation(
our fields that the other types will inherit from. "Abstract type is deprecated, please use normal object inheritance instead.\n"
''' "See more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#deprecations"
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of
# AbstractType
if not is_base_type(bases, AbstractTypeMeta):
return type.__new__(cls, name, bases, attrs)
for base in bases:
if not issubclass(base, AbstractType) and issubclass(type(base), AbstractTypeMeta):
# raise Exception('You can only extend AbstractTypes after the base definition.')
return type.__new__(cls, name, bases, attrs)
base_fields = get_base_fields(bases, _as=None)
fields = yank_fields_from_attrs(attrs, _as=None)
options = Options(
fields=merge(base_fields, fields)
) )
cls = type.__new__(cls, name, bases, dict(attrs, _meta=options)) super(AbstractType, cls).__init_subclass__(*args, **kwargs)
return cls
class AbstractType(six.with_metaclass(AbstractTypeMeta)):
pass

View File

@ -1,9 +1,9 @@
from collections import OrderedDict from collections import OrderedDict
from itertools import chain from itertools import chain
from .dynamic import Dynamic
from .mountedtype import MountedType from .mountedtype import MountedType
from .structures import NonNull from .structures import NonNull
from .dynamic import Dynamic
from .utils import get_type from .utils import get_type

42
graphene/types/base.py Normal file
View File

@ -0,0 +1,42 @@
from ..utils.subclass_with_meta import SubclassWithMeta
from ..utils.trim_docstring import trim_docstring
class BaseOptions(object):
name = None # type: str
description = None # type: str
_frozen = False # type: bool
def __init__(self, class_type):
self.class_type = class_type # type: Type
def freeze(self):
self._frozen = True
def __setattr__(self, name, value):
if not self._frozen:
super(BaseOptions, self).__setattr__(name, value)
else:
raise Exception("Can't modify frozen Options {0}".format(self))
def __repr__(self):
return "<{} type={}>".format(self.__class__.__name__, self.class_type.__name__)
class BaseType(SubclassWithMeta):
@classmethod
def create_type(cls, class_name, **options):
return type(class_name, (cls, ), {'Meta': options})
@classmethod
def __init_subclass_with_meta__(cls, name=None, description=None, _meta=None):
assert "_meta" not in cls.__dict__, "Can't assign directly meta"
if not _meta:
return
_meta.name = name or cls.__name__
_meta.description = description or trim_docstring(cls.__doc__)
_meta.freeze()
cls._meta = _meta
super(BaseType, cls).__init_subclass_with_meta__()

View File

@ -0,0 +1,4 @@
class Context(object):
def __init__(self, **params):
for key, value in params.items():
setattr(self, key, value)

View File

@ -2,15 +2,12 @@ from collections import OrderedDict
import six import six
from ..utils.is_base_type import is_base_type from graphene.utils.subclass_with_meta import SubclassWithMeta_Meta
from ..utils.trim_docstring import trim_docstring
from .options import Options from .base import BaseOptions, BaseType
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
try: from ..pyutils.compat import Enum as PyEnum
from enum import Enum as PyEnum
except ImportError:
from ..pyutils.enum import Enum as PyEnum
def eq_enum(self, other): def eq_enum(self, other):
@ -19,29 +16,18 @@ def eq_enum(self, other):
return self.value is other return self.value is other
class EnumTypeMeta(type): EnumType = type(PyEnum)
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of Model
# (excluding Model class itself).
if not is_base_type(bases, EnumTypeMeta):
return type.__new__(cls, name, bases, attrs)
options = Options( class EnumOptions(BaseOptions):
attrs.pop('Meta', None), enum = None # type: Enum
name=name,
description=trim_docstring(attrs.get('__doc__')),
enum=None,
)
if not options.enum:
attrs['__eq__'] = eq_enum
options.enum = PyEnum(cls.__name__, attrs)
new_attrs = OrderedDict(attrs, _meta=options, **options.enum.__members__)
return type.__new__(cls, name, bases, new_attrs)
def __prepare__(name, bases, **kwargs): # noqa: N805 class EnumMeta(SubclassWithMeta_Meta):
return OrderedDict()
def __new__(cls, name, bases, classdict, **options):
enum = PyEnum(cls.__name__, OrderedDict(classdict, __eq__=eq_enum))
return SubclassWithMeta_Meta.__new__(cls, name, bases, OrderedDict(classdict, __enum__=enum), **options)
def get(cls, value): def get(cls, value):
return cls._meta.enum(value) return cls._meta.enum(value)
@ -49,29 +35,30 @@ class EnumTypeMeta(type):
def __getitem__(cls, value): def __getitem__(cls, value):
return cls._meta.enum[value] return cls._meta.enum[value]
def __prepare__(name, bases, **kwargs): # noqa: N805
return OrderedDict()
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(EnumMeta, cls).__call__(*args, **kwargs)
# return cls._meta.enum(*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})
return type(meta_class.enum.__name__, (Enum,), {'Meta': meta_class}) return type(meta_class.enum.__name__, (Enum,), {'Meta': meta_class})
def __str__(cls): # noqa: N805
return cls._meta.name
class Enum(six.with_metaclass(EnumMeta, UnmountedType, BaseType)):
class Enum(six.with_metaclass(EnumTypeMeta, UnmountedType)): @classmethod
''' def __init_subclass_with_meta__(cls, enum=None, **options):
Enum Type Definition _meta = EnumOptions(cls)
_meta.enum = enum or cls.__enum__
Some leaf values of requests and input values are Enums. GraphQL serializes for key, value in _meta.enum.__members__.items():
Enum values as strings, however internally Enums can be represented by any setattr(cls, key, value)
kind of type, often integers. super(Enum, cls).__init_subclass_with_meta__(_meta=_meta, **options)
'''
@classmethod @classmethod
def get_type(cls): def get_type(cls):

View File

@ -8,11 +8,10 @@ from .structures import NonNull
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
from .utils import get_type from .utils import get_type
base_type = type base_type = type
def source_resolver(source, root, args, context, info): def source_resolver(source, root, info, **args):
resolved = getattr(root, source, None) resolved = getattr(root, source, None)
if inspect.isfunction(resolved) or inspect.ismethod(resolved): if inspect.isfunction(resolved) or inspect.ismethod(resolved):
return resolved() return resolved()

View File

@ -1,9 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from graphene.types.scalars import MAX_INT, MIN_INT
from graphql.language.ast import (BooleanValue, FloatValue, IntValue, from graphql.language.ast import (BooleanValue, FloatValue, IntValue,
StringValue, ListValue, ObjectValue) ListValue, ObjectValue, StringValue)
from graphene.types.scalars import MIN_INT, MAX_INT
from .scalars import Scalar from .scalars import Scalar

View File

@ -1,45 +1,36 @@
import six from collections import OrderedDict
from ..utils.is_base_type import is_base_type from .base import BaseOptions, BaseType
from ..utils.trim_docstring import trim_docstring
from .abstracttype import AbstractTypeMeta
from .inputfield import InputField from .inputfield import InputField
from .options import Options
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
from .utils import get_base_fields, merge, yank_fields_from_attrs from .utils import yank_fields_from_attrs
class InputObjectTypeMeta(AbstractTypeMeta): # For static type checking with Mypy
MYPY = False
def __new__(cls, name, bases, attrs): if MYPY:
# Also ensure initialization is only performed for subclasses of from typing import Dict, Callable # NOQA
# InputObjectType
if not is_base_type(bases, InputObjectTypeMeta):
return type.__new__(cls, name, bases, attrs)
options = Options(
attrs.pop('Meta', None),
name=name,
description=trim_docstring(attrs.get('__doc__')),
local_fields=None,
)
options.base_fields = get_base_fields(bases, _as=InputField)
if not options.local_fields:
options.local_fields = yank_fields_from_attrs(attrs, _as=InputField)
options.fields = merge(
options.base_fields,
options.local_fields
)
return type.__new__(cls, name, bases, dict(attrs, _meta=options))
def __str__(cls): # noqa: N802
return cls._meta.name
class InputObjectType(six.with_metaclass(InputObjectTypeMeta, UnmountedType)): class InputObjectTypeOptions(BaseOptions):
fields = None # type: Dict[str, InputField]
create_container = None # type: Callable
class InputObjectTypeContainer(dict, BaseType):
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
for key, value in self.items():
setattr(self, key, value)
def __init_subclass__(cls, *args, **kwargs):
pass
class InputObjectType(UnmountedType, BaseType):
''' '''
Input Object Type Definition Input Object Type Definition
@ -49,6 +40,22 @@ class InputObjectType(six.with_metaclass(InputObjectTypeMeta, UnmountedType)):
Using `NonNull` will ensure that a value must be provided by the query Using `NonNull` will ensure that a value must be provided by the query
''' '''
@classmethod
def __init_subclass_with_meta__(cls, container=None, **options):
_meta = InputObjectTypeOptions(cls)
fields = OrderedDict()
for base in reversed(cls.__mro__):
fields.update(
yank_fields_from_attrs(base.__dict__, _as=InputField)
)
_meta.fields = fields
if container is None:
container = type(cls.__name__, (InputObjectTypeContainer, cls), {})
_meta.container = container
super(InputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options)
@classmethod @classmethod
def get_type(cls): def get_type(cls):
''' '''

View File

@ -1,45 +1,20 @@
import six from collections import OrderedDict
from ..utils.is_base_type import is_base_type from .base import BaseOptions, BaseType
from ..utils.trim_docstring import trim_docstring
from .abstracttype import AbstractTypeMeta
from .field import Field from .field import Field
from .options import Options from .utils import yank_fields_from_attrs
from .utils import get_base_fields, merge, yank_fields_from_attrs
# For static type checking with Mypy
MYPY = False
if MYPY:
from typing import Dict # NOQA
class InterfaceMeta(AbstractTypeMeta): class InterfaceOptions(BaseOptions):
fields = None # type: Dict[str, Field]
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of
# Interface
if not is_base_type(bases, InterfaceMeta):
return type.__new__(cls, name, bases, attrs)
options = Options(
attrs.pop('Meta', None),
name=name,
description=trim_docstring(attrs.get('__doc__')),
local_fields=None,
)
options.base_fields = get_base_fields(bases, _as=Field)
if not options.local_fields:
options.local_fields = yank_fields_from_attrs(attrs, _as=Field)
options.fields = merge(
options.base_fields,
options.local_fields
)
return type.__new__(cls, name, bases, dict(attrs, _meta=options))
def __str__(cls): # noqa: N802
return cls._meta.name
class Interface(six.with_metaclass(InterfaceMeta)): class Interface(BaseType):
''' '''
Interface Type Definition Interface Type Definition
@ -48,16 +23,29 @@ class Interface(six.with_metaclass(InterfaceMeta)):
all types, as well as a function to determine which type is actually used all types, as well as a function to determine which type is actually used
when the field is resolved. when the field is resolved.
''' '''
@classmethod
def __init_subclass_with_meta__(cls, _meta=None, **options):
if not _meta:
_meta = InterfaceOptions(cls)
fields = OrderedDict()
for base in reversed(cls.__mro__):
fields.update(
yank_fields_from_attrs(base.__dict__, _as=Field)
)
if _meta.fields:
_meta.fields.update(fields)
else:
_meta.fields = fields
super(Interface, cls).__init_subclass_with_meta__(_meta=_meta, **options)
@classmethod @classmethod
def resolve_type(cls, instance, context, info): def resolve_type(cls, instance, info):
from .objecttype import ObjectType from .objecttype import ObjectType
if isinstance(instance, ObjectType): if isinstance(instance, ObjectType):
return type(instance) return type(instance)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
raise Exception("An Interface cannot be intitialized") raise Exception("An Interface cannot be intitialized")
@classmethod
def implements(cls, objecttype):
pass

View File

@ -1,33 +1,81 @@
from functools import partial from collections import OrderedDict
import six
from ..utils.is_base_type import is_base_type
from ..utils.get_unbound_function import get_unbound_function from ..utils.get_unbound_function import get_unbound_function
from ..utils.props import props from ..utils.props import props
from .field import Field from .field import Field
from .objecttype import ObjectType, ObjectTypeMeta from .objecttype import ObjectType, ObjectTypeOptions
from .utils import yank_fields_from_attrs
from ..utils.deprecated import warn_deprecation
class MutationMeta(ObjectTypeMeta): # For static type checking with Mypy
def __new__(cls, name, bases, attrs): MYPY = False
# Also ensure initialization is only performed for subclasses of if MYPY:
# Mutation from .argument import Argument # NOQA
if not is_base_type(bases, MutationMeta): from typing import Dict, Type, Callable # NOQA
return type.__new__(cls, name, bases, attrs)
input_class = attrs.pop('Input', None)
cls = ObjectTypeMeta.__new__(cls, name, bases, attrs)
field_args = props(input_class) if input_class else {}
output_class = getattr(cls, 'Output', cls)
resolver = getattr(cls, 'mutate', None)
assert resolver, 'All mutations must define a mutate method in it'
resolver = get_unbound_function(resolver)
cls.Field = partial(
Field, output_class, args=field_args, resolver=resolver)
return cls
class Mutation(six.with_metaclass(MutationMeta, ObjectType)): class MutationOptions(ObjectTypeOptions):
pass arguments = None # type: Dict[str, Argument]
output = None # type: Type[ObjectType]
resolver = None # type: Callable
class Mutation(ObjectType):
'''
Mutation Type Definition
'''
@classmethod
def __init_subclass_with_meta__(cls, resolver=None, output=None, arguments=None,
_meta=None, **options):
if not _meta:
_meta = MutationOptions(cls)
output = output or getattr(cls, 'Output', None)
fields = {}
if not output:
# If output is defined, we don't need to get the fields
fields = OrderedDict()
for base in reversed(cls.__mro__):
fields.update(
yank_fields_from_attrs(base.__dict__, _as=Field)
)
output = cls
if not arguments:
input_class = getattr(cls, 'Arguments', None)
if not input_class:
input_class = getattr(cls, 'Input', None)
if input_class:
warn_deprecation((
"Please use {name}.Arguments instead of {name}.Input."
"Input is now only used in ClientMutationID.\n"
"Read more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#mutation-input"
).format(name=cls.__name__))
if input_class:
arguments = props(input_class)
else:
arguments = {}
if not resolver:
mutate = getattr(cls, 'mutate', None)
assert mutate, 'All mutations must define a mutate method in it'
resolver = get_unbound_function(mutate)
if _meta.fields:
_meta.fields.update(fields)
else:
_meta.fields = fields
_meta.output = output
_meta.resolver = resolver
_meta.arguments = arguments
super(Mutation, cls).__init_subclass_with_meta__(_meta=_meta, **options)
@classmethod
def Field(cls, *args, **kwargs):
return Field(
cls._meta.output, args=cls._meta.arguments, resolver=cls._meta.resolver
)

View File

@ -1,82 +1,64 @@
from collections import OrderedDict from collections import OrderedDict
import six from .base import BaseOptions, BaseType
from ..utils.is_base_type import is_base_type
from ..utils.trim_docstring import trim_docstring
from .abstracttype import AbstractTypeMeta
from .field import Field from .field import Field
from .interface import Interface from .interface import Interface
from .options import Options from .utils import yank_fields_from_attrs
from .utils import get_base_fields, merge, yank_fields_from_attrs
# For static type checking with Mypy
MYPY = False
if MYPY:
from typing import Dict, Iterable, Type # NOQA
class ObjectTypeMeta(AbstractTypeMeta): class ObjectTypeOptions(BaseOptions):
fields = None # type: Dict[str, Field]
def __new__(cls, name, bases, attrs): interfaces = () # type: Iterable[Type[Interface]]
# Also ensure initialization is only performed for subclasses of
# ObjectType
if not is_base_type(bases, ObjectTypeMeta):
return type.__new__(cls, name, bases, attrs)
_meta = attrs.pop('_meta', None)
defaults = dict(
name=name,
description=trim_docstring(attrs.get('__doc__')),
interfaces=(),
possible_types=(),
default_resolver=None,
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)
if not options.local_fields:
options.local_fields = yank_fields_from_attrs(attrs=attrs, _as=Field)
options.interface_fields = OrderedDict()
for interface in options.interfaces:
assert issubclass(interface, Interface), (
'All interfaces of {} must be a subclass of Interface. Received "{}".'
).format(name, interface)
options.interface_fields.update(interface._meta.fields)
options.fields = merge(
options.interface_fields,
options.base_fields,
options.local_fields
)
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:
interface.implements(cls)
return cls
def __str__(cls): # noqa: N802
return cls._meta.name
class ObjectType(six.with_metaclass(ObjectTypeMeta)): class ObjectType(BaseType):
''' '''
Object Type Definition Object Type Definition
Almost all of the GraphQL types you define will be object types. Object types Almost all of the GraphQL types you define will be object types. Object types
have a name, but most importantly describe their fields. have a name, but most importantly describe their fields.
''' '''
@classmethod
def __init_subclass_with_meta__(
cls, interfaces=(),
possible_types=(),
default_resolver=None, _meta=None, **options):
if not _meta:
_meta = ObjectTypeOptions(cls)
fields = OrderedDict()
for interface in interfaces:
assert issubclass(interface, Interface), (
'All interfaces of {} must be a subclass of Interface. Received "{}".'
).format(cls.__name__, interface)
fields.update(interface._meta.fields)
for base in reversed(cls.__mro__):
fields.update(
yank_fields_from_attrs(base.__dict__, _as=Field)
)
assert not (possible_types and cls.is_type_of), (
'{name}.Meta.possible_types will cause type collision with {name}.is_type_of. '
'Please use one or other.'
).format(name=cls.__name__)
if _meta.fields:
_meta.fields.update(fields)
else:
_meta.fields = fields
_meta.interfaces = interfaces
_meta.possible_types = possible_types
_meta.default_resolver = default_resolver
super(ObjectType, cls).__init_subclass_with_meta__(_meta=_meta, **options)
is_type_of = None is_type_of = None

View File

@ -1,42 +0,0 @@
import inspect
from ..utils.props import props
class Options(object):
'''
This is the class wrapper around Meta.
It helps to validate and cointain the attributes inside
'''
def __init__(self, meta=None, **defaults):
if meta:
assert inspect.isclass(meta), (
'Meta have to be a class, received "{}".'.format(repr(meta))
)
meta_attrs = props(meta) if meta else {}
for attr_name, value in defaults.items():
if attr_name in meta_attrs:
value = meta_attrs.pop(attr_name)
setattr(self, attr_name, value)
# If meta_attrs is not empty, it implicitly means
# it received invalid attributes
if meta_attrs:
raise TypeError(
"Invalid attributes: {}".format(
', '.join(sorted(meta_attrs.keys()))
)
)
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):
options_props = props(self)
props_as_attrs = ' '.join(['{}={}'.format(key, value) for key, value in options_props.items()])
return '<Options {}>'.format(props_as_attrs)

View File

@ -1,8 +1,8 @@
def attr_resolver(attname, default_value, root, args, context, info): def attr_resolver(attname, default_value, root, info, **args):
return getattr(root, attname, default_value) return getattr(root, attname, default_value)
def dict_resolver(attname, default_value, root, args, context, info): def dict_resolver(attname, default_value, root, info, **args):
return root.get(attname, default_value) return root.get(attname, default_value)

View File

@ -1,34 +1,17 @@
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 .base import BaseOptions, BaseType
from ..utils.trim_docstring import trim_docstring
from .options import Options
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
class ScalarTypeMeta(type): class ScalarOptions(BaseOptions):
pass
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of Model
# (excluding Model class itself).
if not is_base_type(bases, ScalarTypeMeta):
return type.__new__(cls, name, bases, attrs)
options = Options(
attrs.pop('Meta', None),
name=name,
description=trim_docstring(attrs.get('__doc__')),
)
return type.__new__(cls, name, bases, dict(attrs, _meta=options))
def __str__(cls): # noqa: N802
return cls._meta.name
class Scalar(six.with_metaclass(ScalarTypeMeta, UnmountedType)): class Scalar(UnmountedType, BaseType):
''' '''
Scalar Type Definition Scalar Type Definition
@ -36,6 +19,10 @@ class Scalar(six.with_metaclass(ScalarTypeMeta, UnmountedType)):
Scalars (or Enums) and are defined with a name and a series of functions Scalars (or Enums) and are defined with a name and a series of functions
used to parse input from ast or variables and to ensure validity. used to parse input from ast or variables and to ensure validity.
''' '''
@classmethod
def __init_subclass_with_meta__(cls, **options):
_meta = ScalarOptions(cls)
super(Scalar, cls).__init_subclass_with_meta__(_meta=_meta, **options)
serialize = None serialize = None
parse_value = None parse_value = None
@ -99,6 +86,7 @@ class Float(Scalar):
@staticmethod @staticmethod
def coerce_float(value): def coerce_float(value):
# type: (Any) -> float
try: try:
return float(value) return float(value)
except ValueError: except ValueError:

View File

@ -68,9 +68,9 @@ class NonNull(Structure):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(NonNull, self).__init__(*args, **kwargs) super(NonNull, self).__init__(*args, **kwargs)
assert not isinstance(self.of_type, NonNull), ( assert not isinstance(self._of_type, NonNull), (
'Can only create NonNull of a Nullable GraphQLType but got: {}.' 'Can only create NonNull of a Nullable GraphQLType but got: {}.'
).format(self.of_type) ).format(self._of_type)
def __str__(self): def __str__(self):
return '{}!'.format(self.of_type) return '{}!'.format(self.of_type)

View File

@ -1,10 +1,13 @@
import pytest
from ..abstracttype import AbstractType from ..objecttype import ObjectType
from ..field import Field
from ..unmountedtype import UnmountedType from ..unmountedtype import UnmountedType
from ..abstracttype import AbstractType
from .. import abstracttype
from ..field import Field
class MyType(object): class MyType(ObjectType):
pass pass
@ -14,29 +17,24 @@ class MyScalar(UnmountedType):
return MyType return MyType
def test_generate_abstracttype_with_fields(): def test_abstract_objecttype_warn_deprecation(mocker):
mocker.patch.object(abstracttype, 'warn_deprecation')
class MyAbstractType(AbstractType): class MyAbstractType(AbstractType):
field = Field(MyType) field1 = MyScalar()
assert 'field' in MyAbstractType._meta.fields assert abstracttype.warn_deprecation.called
assert isinstance(MyAbstractType._meta.fields['field'], Field)
def test_generate_abstracttype_with_unmountedfields(): def test_generate_objecttype_inherit_abstracttype():
class MyAbstractType(AbstractType): class MyAbstractType(AbstractType):
field = UnmountedType(MyType) field1 = MyScalar()
assert 'field' in MyAbstractType._meta.fields class MyObjectType(ObjectType, MyAbstractType):
assert isinstance(MyAbstractType._meta.fields['field'], UnmountedType) field2 = MyScalar()
assert MyObjectType._meta.description is None
def test_generate_abstracttype_inheritance(): assert MyObjectType._meta.interfaces == ()
class MyAbstractType1(AbstractType): assert MyObjectType._meta.name == "MyObjectType"
field1 = UnmountedType(MyType) assert list(MyObjectType._meta.fields.keys()) == ['field1', 'field2']
assert list(map(type, MyObjectType._meta.fields.values())) == [Field, Field]
class MyAbstractType2(MyAbstractType1):
field2 = UnmountedType(MyType)
assert list(MyAbstractType2._meta.fields.keys()) == ['field1', 'field2']
assert not hasattr(MyAbstractType1, 'field1')
assert not hasattr(MyAbstractType2, 'field2')

View File

@ -1,11 +1,12 @@
import pytest
from functools import partial from functools import partial
import pytest
from ..argument import Argument, to_arguments from ..argument import Argument, to_arguments
from ..field import Field from ..field import Field
from ..inputfield import InputField from ..inputfield import InputField
from ..structures import NonNull
from ..scalars import String from ..scalars import String
from ..structures import NonNull
def test_argument(): def test_argument():

View File

@ -0,0 +1,63 @@
import pytest
from ..base import BaseType, BaseOptions
class CustomOptions(BaseOptions):
pass
class CustomType(BaseType):
@classmethod
def __init_subclass_with_meta__(cls, **options):
_meta = CustomOptions(cls)
super(CustomType, cls).__init_subclass_with_meta__(_meta=_meta, **options)
def test_basetype():
class MyBaseType(CustomType):
pass
assert isinstance(MyBaseType._meta, CustomOptions)
assert MyBaseType._meta.name == "MyBaseType"
assert MyBaseType._meta.description is None
def test_basetype_nones():
class MyBaseType(CustomType):
'''Documentation'''
class Meta:
name = None
description = None
assert isinstance(MyBaseType._meta, CustomOptions)
assert MyBaseType._meta.name == "MyBaseType"
assert MyBaseType._meta.description == "Documentation"
def test_basetype_custom():
class MyBaseType(CustomType):
'''Documentation'''
class Meta:
name = 'Base'
description = 'Desc'
assert isinstance(MyBaseType._meta, CustomOptions)
assert MyBaseType._meta.name == "Base"
assert MyBaseType._meta.description == "Desc"
def test_basetype_create():
MyBaseType = CustomType.create_type('MyBaseType')
assert isinstance(MyBaseType._meta, CustomOptions)
assert MyBaseType._meta.name == "MyBaseType"
assert MyBaseType._meta.description is None
def test_basetype_create_extra():
MyBaseType = CustomType.create_type('MyBaseType', name='Base', description='Desc')
assert isinstance(MyBaseType._meta, CustomOptions)
assert MyBaseType._meta.name == "Base"
assert MyBaseType._meta.description == "Desc"

View File

@ -1,4 +1,5 @@
import datetime import datetime
import pytz import pytz
from ..datetime import DateTime, Time from ..datetime import DateTime, Time
@ -10,12 +11,11 @@ class Query(ObjectType):
datetime = DateTime(_in=DateTime(name='in')) datetime = DateTime(_in=DateTime(name='in'))
time = Time(_at=Time(name='at')) time = Time(_at=Time(name='at'))
def resolve_datetime(self, args, context, info): def resolve_datetime(self, info, _in=None):
_in = args.get('_in')
return _in return _in
def resolve_time(self, args, context, info): def resolve_time(self, info, _at=None):
return args.get('_at') return _at
schema = Schema(query=Query) schema = Schema(query=Query)

View File

@ -1,6 +1,5 @@
from ..abstracttype import AbstractType
from ..argument import Argument from ..argument import Argument
from ..enum import Enum from ..enum import Enum
from ..field import Field from ..field import Field
@ -296,7 +295,7 @@ def test_stringifies_simple_types():
def test_does_not_mutate_passed_field_definitions(): def test_does_not_mutate_passed_field_definitions():
class CommonFields(AbstractType): class CommonFields(object):
field1 = String() field1 = String()
field2 = String(id=String()) field2 = String(id=String())
@ -307,12 +306,8 @@ def test_does_not_mutate_passed_field_definitions():
pass pass
assert TestObject1._meta.fields == TestObject2._meta.fields assert TestObject1._meta.fields == TestObject2._meta.fields
assert CommonFields._meta.fields == {
'field1': String(),
'field2': String(id=String()),
}
class CommonFields(AbstractType): class CommonFields(object):
field1 = String() field1 = String()
field2 = String() field2 = String()
@ -323,8 +318,3 @@ def test_does_not_mutate_passed_field_definitions():
pass pass
assert TestInputObject1._meta.fields == TestInputObject2._meta.fields assert TestInputObject1._meta.fields == TestInputObject2._meta.fields
assert CommonFields._meta.fields == {
'field1': String(),
'field2': String(),
}

View File

@ -1,6 +1,6 @@
from ..structures import List, NonNull
from ..scalars import String
from ..dynamic import Dynamic from ..dynamic import Dynamic
from ..scalars import String
from ..structures import List, NonNull
def test_dynamic(): def test_dynamic():

View File

@ -1,7 +1,7 @@
from ..argument import Argument
from ..enum import Enum, PyEnum from ..enum import Enum, PyEnum
from ..field import Field from ..field import Field
from ..inputfield import InputField from ..inputfield import InputField
from ..argument import Argument
def test_enum_construction(): def test_enum_construction():

View File

@ -1,10 +1,11 @@
import pytest
from functools import partial from functools import partial
import pytest
from ..argument import Argument from ..argument import Argument
from ..field import Field from ..field import Field
from ..structures import NonNull
from ..scalars import String from ..scalars import String
from ..structures import NonNull
from .utils import MyLazyType from .utils import MyLazyType
@ -19,7 +20,8 @@ class MyInstance(object):
def test_field_basic(): def test_field_basic():
MyType = object() MyType = object()
args = {'my arg': Argument(True)} args = {'my arg': Argument(True)}
resolver = lambda: None
def resolver(): return None
deprecation_reason = 'Deprecated now' deprecation_reason = 'Deprecated now'
description = 'My Field' description = 'My Field'
my_default = 'something' my_default = 'something'
@ -59,7 +61,7 @@ def test_field_default_value_not_callable():
def test_field_source(): def test_field_source():
MyType = object() MyType = object()
field = Field(MyType, source='value') field = Field(MyType, source='value')
assert field.resolver(MyInstance, {}, None, None) == MyInstance.value assert field.resolver(MyInstance(), None) == MyInstance.value
def test_field_with_lazy_type(): def test_field_with_lazy_type():
@ -83,19 +85,20 @@ 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:
Field(MyType, source='value', resolver=lambda: None) Field(MyType, source='value', resolver=lambda: None)
assert str(exc_info.value) == 'A Field cannot have a source and a resolver in at the same time.' assert str(
exc_info.value) == 'A Field cannot have a source and a resolver in at the same time.'
def test_field_source_func(): def test_field_source_func():
MyType = object() MyType = object()
field = Field(MyType, source='value_func') field = Field(MyType, source='value_func')
assert field.resolver(MyInstance(), {}, None, None) == MyInstance.value_func() assert field.resolver(MyInstance(), None) == MyInstance.value_func()
def test_field_source_method(): def test_field_source_method():
MyType = object() MyType = object()
field = Field(MyType, source='value_method') field = Field(MyType, source='value_method')
assert field.resolver(MyInstance(), {}, None, None) == MyInstance().value_method() assert field.resolver(MyInstance(), None) == MyInstance().value_method()
def test_field_source_as_argument(): def test_field_source_as_argument():

View File

@ -6,8 +6,7 @@ from ..schema import Schema
class Query(ObjectType): class Query(ObjectType):
generic = GenericScalar(input=GenericScalar()) generic = GenericScalar(input=GenericScalar())
def resolve_generic(self, args, context, info): def resolve_generic(self, info, input=None):
input = args.get('input')
return input return input

View File

@ -1,4 +1,3 @@
import pytest
from functools import partial from functools import partial
from ..inputfield import InputField from ..inputfield import InputField

View File

@ -1,10 +1,9 @@
from ..abstracttype import AbstractType
from ..field import Field
from ..argument import Argument from ..argument import Argument
from ..field import Field
from ..inputfield import InputField from ..inputfield import InputField
from ..objecttype import ObjectType
from ..inputobjecttype import InputObjectType from ..inputobjecttype import InputObjectType
from ..objecttype import ObjectType
from ..unmountedtype import UnmountedType from ..unmountedtype import UnmountedType
@ -80,7 +79,7 @@ def test_generate_inputobjecttype_as_argument():
def test_generate_inputobjecttype_inherit_abstracttype(): def test_generate_inputobjecttype_inherit_abstracttype():
class MyAbstractType(AbstractType): class MyAbstractType(object):
field1 = MyScalar(MyType) field1 = MyScalar(MyType)
class MyInputObjectType(InputObjectType, MyAbstractType): class MyInputObjectType(InputObjectType, MyAbstractType):
@ -91,7 +90,7 @@ def test_generate_inputobjecttype_inherit_abstracttype():
def test_generate_inputobjecttype_inherit_abstracttype_reversed(): def test_generate_inputobjecttype_inherit_abstracttype_reversed():
class MyAbstractType(AbstractType): class MyAbstractType(object):
field1 = MyScalar(MyType) field1 = MyScalar(MyType)
class MyInputObjectType(MyAbstractType, InputObjectType): class MyInputObjectType(MyAbstractType, InputObjectType):

View File

@ -1,5 +1,3 @@
from ..abstracttype import AbstractType
from ..field import Field from ..field import Field
from ..interface import Interface from ..interface import Interface
from ..unmountedtype import UnmountedType from ..unmountedtype import UnmountedType
@ -61,7 +59,7 @@ def test_generate_interface_unmountedtype():
def test_generate_interface_inherit_abstracttype(): def test_generate_interface_inherit_abstracttype():
class MyAbstractType(AbstractType): class MyAbstractType(object):
field1 = MyScalar() field1 = MyScalar()
class MyInterface(Interface, MyAbstractType): class MyInterface(Interface, MyAbstractType):
@ -84,7 +82,7 @@ def test_generate_interface_inherit_interface():
def test_generate_interface_inherit_abstracttype_reversed(): def test_generate_interface_inherit_abstracttype_reversed():
class MyAbstractType(AbstractType): class MyAbstractType(object):
field1 = MyScalar() field1 = MyScalar()
class MyInterface(MyAbstractType, Interface): class MyInterface(MyAbstractType, Interface):

View File

@ -1,4 +1,3 @@
import json
from ..json import JSONString from ..json import JSONString
from ..objecttype import ObjectType from ..objecttype import ObjectType
@ -8,8 +7,7 @@ from ..schema import Schema
class Query(ObjectType): class Query(ObjectType):
json = JSONString(input=JSONString()) json = JSONString(input=JSONString())
def resolve_json(self, args, context, info): def resolve_json(self, info, input):
input = args.get('input')
return input return input
schema = Schema(query=Query) schema = Schema(query=Query)

View File

@ -1,11 +1,10 @@
import pytest
from ..mountedtype import MountedType
from ..field import Field from ..field import Field
from ..scalars import String from ..scalars import String
class CustomField(Field): class CustomField(Field):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.metadata = kwargs.pop('metadata', None) self.metadata = kwargs.pop('metadata', None)
super(CustomField, self).__init__(*args, **kwargs) super(CustomField, self).__init__(*args, **kwargs)

View File

@ -1,40 +1,41 @@
import pytest import pytest
from ..argument import Argument
from ..dynamic import Dynamic
from ..mutation import Mutation from ..mutation import Mutation
from ..objecttype import ObjectType from ..objecttype import ObjectType
from ..schema import Schema
from ..argument import Argument
from ..scalars import String from ..scalars import String
from ..dynamic import Dynamic from ..schema import Schema
def test_generate_mutation_no_args(): def test_generate_mutation_no_args():
class MyMutation(Mutation): class MyMutation(Mutation):
'''Documentation''' '''Documentation'''
@classmethod def mutate(self, info, **args):
def mutate(cls, *args, **kwargs): return args
pass
assert issubclass(MyMutation, ObjectType) assert issubclass(MyMutation, ObjectType)
assert MyMutation._meta.name == "MyMutation" assert MyMutation._meta.name == "MyMutation"
assert MyMutation._meta.description == "Documentation" assert MyMutation._meta.description == "Documentation"
assert MyMutation.Field().resolver == MyMutation.mutate resolved = MyMutation.Field().resolver(None, None, name='Peter')
assert resolved == {'name': 'Peter'}
def test_generate_mutation_with_meta(): def test_generate_mutation_with_meta():
class MyMutation(Mutation): class MyMutation(Mutation):
class Meta: class Meta:
name = 'MyOtherMutation' name = 'MyOtherMutation'
description = 'Documentation' description = 'Documentation'
@classmethod def mutate(self, info, **args):
def mutate(cls, *args, **kwargs): return args
pass
assert MyMutation._meta.name == "MyOtherMutation" assert MyMutation._meta.name == "MyOtherMutation"
assert MyMutation._meta.description == "Documentation" assert MyMutation._meta.description == "Documentation"
assert MyMutation.Field().resolver == MyMutation.mutate resolved = MyMutation.Field().resolver(None, None, name='Peter')
assert resolved == {'name': 'Peter'}
def test_mutation_raises_exception_if_no_mutate(): def test_mutation_raises_exception_if_no_mutate():
@ -52,24 +53,26 @@ def test_mutation_custom_output_type():
name = String() name = String()
class CreateUser(Mutation): class CreateUser(Mutation):
class Input: class Input:
name = String() name = String()
Output = User Output = User
@classmethod def mutate(self, info, name):
def mutate(cls, args, context, info):
name = args.get('name')
return User(name=name) return User(name=name)
field = CreateUser.Field() field = CreateUser.Field()
assert field.type == User assert field.type == User
assert field.args == {'name': Argument(String)} assert field.args == {'name': Argument(String)}
assert field.resolver == CreateUser.mutate resolved = field.resolver(None, None, name='Peter')
assert isinstance(resolved, User)
assert resolved.name == 'Peter'
def test_mutation_execution(): def test_mutation_execution():
class CreateUser(Mutation): class CreateUser(Mutation):
class Input: class Input:
name = String() name = String()
dynamic = Dynamic(lambda: String()) dynamic = Dynamic(lambda: String())
@ -78,9 +81,7 @@ def test_mutation_execution():
name = String() name = String()
dynamic = Dynamic(lambda: String()) dynamic = Dynamic(lambda: String())
def mutate(self, args, context, info): def mutate(self, info, name, dynamic):
name = args.get('name')
dynamic = args.get('dynamic')
return CreateUser(name=name, dynamic=dynamic) return CreateUser(name=name, dynamic=dynamic)
class Query(ObjectType): class Query(ObjectType):

View File

@ -1,6 +1,5 @@
import pytest import pytest
from ..abstracttype import AbstractType
from ..field import Field from ..field import Field
from ..interface import Interface from ..interface import Interface
from ..objecttype import ObjectType from ..objecttype import ObjectType
@ -89,7 +88,7 @@ def test_ordered_fields_in_objecttype():
def test_generate_objecttype_inherit_abstracttype(): def test_generate_objecttype_inherit_abstracttype():
class MyAbstractType(AbstractType): class MyAbstractType(object):
field1 = MyScalar() field1 = MyScalar()
class MyObjectType(ObjectType, MyAbstractType): class MyObjectType(ObjectType, MyAbstractType):
@ -103,7 +102,7 @@ def test_generate_objecttype_inherit_abstracttype():
def test_generate_objecttype_inherit_abstracttype_reversed(): def test_generate_objecttype_inherit_abstracttype_reversed():
class MyAbstractType(AbstractType): class MyAbstractType(object):
field1 = MyScalar() field1 = MyScalar()
class MyObjectType(MyAbstractType, ObjectType): class MyObjectType(MyAbstractType, ObjectType):
@ -188,6 +187,7 @@ def test_generate_objecttype_description():
def test_objecttype_with_possible_types(): def test_objecttype_with_possible_types():
class MyObjectType(ObjectType): class MyObjectType(ObjectType):
class Meta: class Meta:
possible_types = (dict, ) possible_types = (dict, )
@ -197,6 +197,7 @@ def test_objecttype_with_possible_types():
def test_objecttype_with_possible_types_and_is_type_of_should_raise(): def test_objecttype_with_possible_types_and_is_type_of_should_raise():
with pytest.raises(AssertionError) as excinfo: with pytest.raises(AssertionError) as excinfo:
class MyObjectType(ObjectType): class MyObjectType(ObjectType):
class Meta: class Meta:
possible_types = (dict, ) possible_types = (dict, )

View File

@ -1,30 +0,0 @@
import pytest
from ..options import Options
def test_options():
class BaseOptions:
option_1 = False
name = True
meta = Options(BaseOptions, name=False, option_1=False)
assert meta.name == True
assert meta.option_1 == False
def test_options_extra_attrs():
class BaseOptions:
name = True
type = True
with pytest.raises(Exception) as exc_info:
meta = Options(BaseOptions)
assert str(exc_info.value) == 'Invalid attributes: name, type'
def test_options_repr():
class BaseOptions:
name = True
meta = Options(BaseOptions, name=False)
assert repr(meta) == '<Options name=True>'

View File

@ -1,18 +1,19 @@
import json import json
from functools import partial from functools import partial
from graphql import Source, execute, parse, GraphQLError from graphql import GraphQLError, Source, execute, parse, ResolveInfo
from ..dynamic import Dynamic
from ..field import Field from ..field import Field
from ..interface import Interface
from ..inputfield import InputField from ..inputfield import InputField
from ..inputobjecttype import InputObjectType from ..inputobjecttype import InputObjectType
from ..interface import Interface
from ..objecttype import ObjectType from ..objecttype import ObjectType
from ..scalars import Int, String from ..scalars import Int, String
from ..schema import Schema from ..schema import Schema
from ..structures import List from ..structures import List
from ..union import Union from ..union import Union
from ..dynamic import Dynamic from ..context import Context
def test_query(): def test_query():
@ -26,6 +27,23 @@ def test_query():
assert executed.data == {'hello': 'World'} assert executed.data == {'hello': 'World'}
def test_query_source():
class Root(object):
_hello = "World"
def hello(self):
return self._hello
class Query(ObjectType):
hello = String(source="hello")
hello_schema = Schema(Query)
executed = hello_schema.execute('{ hello }', Root())
assert not executed.errors
assert executed.data == {'hello': 'World'}
def test_query_union(): def test_query_union():
class one_object(object): class one_object(object):
pass pass
@ -37,24 +55,25 @@ def test_query_union():
one = String() one = String()
@classmethod @classmethod
def is_type_of(cls, root, context, info): def is_type_of(cls, root, info):
return isinstance(root, one_object) return isinstance(root, one_object)
class Two(ObjectType): class Two(ObjectType):
two = String() two = String()
@classmethod @classmethod
def is_type_of(cls, root, context, info): def is_type_of(cls, root, info):
return isinstance(root, two_object) return isinstance(root, two_object)
class MyUnion(Union): class MyUnion(Union):
class Meta: class Meta:
types = (One, Two) types = (One, Two)
class Query(ObjectType): class Query(ObjectType):
unions = List(MyUnion) unions = List(MyUnion)
def resolve_unions(self, args, context, info): def resolve_unions(self, info):
return [one_object(), two_object()] return [one_object(), two_object()]
hello_schema = Schema(Query) hello_schema = Schema(Query)
@ -81,29 +100,31 @@ def test_query_interface():
base = String() base = String()
class One(ObjectType): class One(ObjectType):
class Meta: class Meta:
interfaces = (MyInterface, ) interfaces = (MyInterface, )
one = String() one = String()
@classmethod @classmethod
def is_type_of(cls, root, context, info): def is_type_of(cls, root, info):
return isinstance(root, one_object) return isinstance(root, one_object)
class Two(ObjectType): class Two(ObjectType):
class Meta: class Meta:
interfaces = (MyInterface, ) interfaces = (MyInterface, )
two = String() two = String()
@classmethod @classmethod
def is_type_of(cls, root, context, info): def is_type_of(cls, root, info):
return isinstance(root, two_object) return isinstance(root, two_object)
class Query(ObjectType): class Query(ObjectType):
interfaces = List(MyInterface) interfaces = List(MyInterface)
def resolve_interfaces(self, args, context, info): def resolve_interfaces(self, info):
return [one_object(), two_object()] return [one_object(), two_object()]
hello_schema = Schema(Query, types=[One, Two]) hello_schema = Schema(Query, types=[One, Two])
@ -123,13 +144,15 @@ def test_query_dynamic():
class Query(ObjectType): class Query(ObjectType):
hello = Dynamic(lambda: String(resolver=lambda *_: 'World')) hello = Dynamic(lambda: String(resolver=lambda *_: 'World'))
hellos = Dynamic(lambda: List(String, resolver=lambda *_: ['Worlds'])) hellos = Dynamic(lambda: List(String, resolver=lambda *_: ['Worlds']))
hello_field = Dynamic(lambda: Field(String, resolver=lambda *_: 'Field World')) hello_field = Dynamic(lambda: Field(
String, resolver=lambda *_: 'Field World'))
hello_schema = Schema(Query) hello_schema = Schema(Query)
executed = hello_schema.execute('{ hello hellos helloField }') executed = hello_schema.execute('{ hello hellos helloField }')
assert not executed.errors assert not executed.errors
assert executed.data == {'hello': 'World', 'hellos': ['Worlds'], 'helloField': 'Field World'} assert executed.data == {'hello': 'World', 'hellos': [
'Worlds'], 'helloField': 'Field World'}
def test_query_default_value(): def test_query_default_value():
@ -151,7 +174,7 @@ def test_query_wrong_default_value():
field = String() field = String()
@classmethod @classmethod
def is_type_of(cls, root, context, info): def is_type_of(cls, root, info):
return isinstance(root, MyType) return isinstance(root, MyType)
class Query(ObjectType): class Query(ObjectType):
@ -161,7 +184,8 @@ def test_query_wrong_default_value():
executed = hello_schema.execute('{ hello { field } }') executed = hello_schema.execute('{ hello { field } }')
assert len(executed.errors) == 1 assert len(executed.errors) == 1
assert executed.errors[0].message == GraphQLError('Expected value of type "MyType" but got: str.').message assert executed.errors[0].message == GraphQLError(
'Expected value of type "MyType" but got: str.').message
assert executed.data == {'hello': None} assert executed.data == {'hello': None}
@ -170,7 +194,8 @@ def test_query_default_value_ignored_by_resolver():
field = String() field = String()
class Query(ObjectType): class Query(ObjectType):
hello = Field(MyType, default_value='hello', resolver=lambda *_: MyType(field='no default.')) hello = Field(MyType, default_value='hello',
resolver=lambda *_: MyType(field='no default.'))
hello_schema = Schema(Query) hello_schema = Schema(Query)
@ -183,7 +208,7 @@ def test_query_resolve_function():
class Query(ObjectType): class Query(ObjectType):
hello = String() hello = String()
def resolve_hello(self, args, context, info): def resolve_hello(self, info):
return 'World' return 'World'
hello_schema = Schema(Query) hello_schema = Schema(Query)
@ -197,7 +222,7 @@ def test_query_arguments():
class Query(ObjectType): class Query(ObjectType):
test = String(a_str=String(), a_int=Int()) test = String(a_str=String(), a_int=Int())
def resolve_test(self, args, context, info): def resolve_test(self, info, **args):
return json.dumps([self, args], separators=(',', ':')) return json.dumps([self, args], separators=(',', ':'))
test_schema = Schema(Query) test_schema = Schema(Query)
@ -210,7 +235,8 @@ def test_query_arguments():
assert not result.errors assert not result.errors
assert result.data == {'test': '["Source!",{"a_str":"String!"}]'} assert result.data == {'test': '["Source!",{"a_str":"String!"}]'}
result = test_schema.execute('{ test(aInt: -123, aStr: "String!") }', 'Source!') result = test_schema.execute(
'{ test(aInt: -123, aStr: "String!") }', 'Source!')
assert not result.errors assert not result.errors
assert result.data in [ assert result.data in [
{'test': '["Source!",{"a_str":"String!","a_int":-123}]'}, {'test': '["Source!",{"a_str":"String!","a_int":-123}]'},
@ -226,7 +252,7 @@ def test_query_input_field():
class Query(ObjectType): class Query(ObjectType):
test = String(a_input=Input()) test = String(a_input=Input())
def resolve_test(self, args, context, info): def resolve_test(self, info, **args):
return json.dumps([self, args], separators=(',', ':')) return json.dumps([self, args], separators=(',', ':'))
test_schema = Schema(Query) test_schema = Schema(Query)
@ -235,13 +261,17 @@ def test_query_input_field():
assert not result.errors assert not result.errors
assert result.data == {'test': '[null,{}]'} assert result.data == {'test': '[null,{}]'}
result = test_schema.execute('{ test(aInput: {aField: "String!"} ) }', 'Source!') result = test_schema.execute(
'{ test(aInput: {aField: "String!"} ) }', 'Source!')
assert not result.errors assert not result.errors
assert result.data == {'test': '["Source!",{"a_input":{"a_field":"String!"}}]'} assert result.data == {
'test': '["Source!",{"a_input":{"a_field":"String!"}}]'}
result = test_schema.execute('{ test(aInput: {recursiveField: {aField: "String!"}}) }', 'Source!') result = test_schema.execute(
'{ test(aInput: {recursiveField: {aField: "String!"}}) }', 'Source!')
assert not result.errors assert not result.errors
assert result.data == {'test': '["Source!",{"a_input":{"recursive_field":{"a_field":"String!"}}}]'} assert result.data == {
'test': '["Source!",{"a_input":{"recursive_field":{"a_field":"String!"}}}]'}
def test_query_middlewares(): def test_query_middlewares():
@ -249,10 +279,10 @@ def test_query_middlewares():
hello = String() hello = String()
other = String() other = String()
def resolve_hello(self, args, context, info): def resolve_hello(self, info):
return 'World' return 'World'
def resolve_other(self, args, context, info): def resolve_other(self, info):
return 'other' return 'other'
def reversed_middleware(next, *args, **kwargs): def reversed_middleware(next, *args, **kwargs):
@ -261,27 +291,29 @@ def test_query_middlewares():
hello_schema = Schema(Query) hello_schema = Schema(Query)
executed = hello_schema.execute('{ hello, other }', middleware=[reversed_middleware]) executed = hello_schema.execute(
'{ hello, other }', middleware=[reversed_middleware])
assert not executed.errors assert not executed.errors
assert executed.data == {'hello': 'dlroW', 'other': 'rehto'} assert executed.data == {'hello': 'dlroW', 'other': 'rehto'}
def test_objecttype_on_instances(): def test_objecttype_on_instances():
class Ship: class Ship:
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
class ShipType(ObjectType): class ShipType(ObjectType):
name = String(description="Ship name", required=True) name = String(description="Ship name", required=True)
def resolve_name(self, context, args, info): def resolve_name(self, info):
# Here self will be the Ship instance returned in resolve_ship # Here self will be the Ship instance returned in resolve_ship
return self.name return self.name
class Query(ObjectType): class Query(ObjectType):
ship = Field(ShipType) ship = Field(ShipType)
def resolve_ship(self, context, args, info): def resolve_ship(self, info):
return Ship(name='xwing') return Ship(name='xwing')
schema = Schema(query=Query) schema = Schema(query=Query)
@ -296,7 +328,7 @@ def test_big_list_query_benchmark(benchmark):
class Query(ObjectType): class Query(ObjectType):
all_ints = List(Int) all_ints = List(Int)
def resolve_all_ints(self, args, context, info): def resolve_all_ints(self, info):
return big_list return big_list
hello_schema = Schema(Query) hello_schema = Schema(Query)
@ -313,7 +345,7 @@ def test_big_list_query_compiled_query_benchmark(benchmark):
class Query(ObjectType): class Query(ObjectType):
all_ints = List(Int) all_ints = List(Int)
def resolve_all_ints(self, args, context, info): def resolve_all_ints(self, info):
return big_list return big_list
hello_schema = Schema(Query) hello_schema = Schema(Query)
@ -335,7 +367,7 @@ def test_big_list_of_containers_query_benchmark(benchmark):
class Query(ObjectType): class Query(ObjectType):
all_containers = List(Container) all_containers = List(Container)
def resolve_all_containers(self, args, context, info): def resolve_all_containers(self, info):
return big_container_list return big_container_list
hello_schema = Schema(Query) hello_schema = Schema(Query)
@ -343,7 +375,8 @@ def test_big_list_of_containers_query_benchmark(benchmark):
big_list_query = partial(hello_schema.execute, '{ allContainers { x } }') big_list_query = partial(hello_schema.execute, '{ allContainers { x } }')
result = benchmark(big_list_query) result = benchmark(big_list_query)
assert not result.errors assert not result.errors
assert result.data == {'allContainers': [{'x': c.x} for c in big_container_list]} assert result.data == {'allContainers': [
{'x': c.x} for c in big_container_list]}
def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark): def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark):
@ -358,15 +391,17 @@ def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark):
class Query(ObjectType): class Query(ObjectType):
all_containers = List(Container) all_containers = List(Container)
def resolve_all_containers(self, args, context, info): def resolve_all_containers(self, info):
return big_container_list return big_container_list
hello_schema = Schema(Query) hello_schema = Schema(Query)
big_list_query = partial(hello_schema.execute, '{ allContainers { x, y, z, o } }') big_list_query = partial(hello_schema.execute,
'{ allContainers { x, y, z, o } }')
result = benchmark(big_list_query) result = benchmark(big_list_query)
assert not result.errors assert not result.errors
assert result.data == {'allContainers': [{'x': c.x, 'y': c.y, 'z': c.z, 'o': c.o} for c in big_container_list]} assert result.data == {'allContainers': [
{'x': c.x, 'y': c.y, 'z': c.z, 'o': c.o} for c in big_container_list]}
def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark(benchmark): def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark(benchmark):
@ -376,16 +411,16 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark
z = Int() z = Int()
o = Int() o = Int()
def resolve_x(self, args, context, info): def resolve_x(self, info):
return self.x return self.x
def resolve_y(self, args, context, info): def resolve_y(self, info):
return self.y return self.y
def resolve_z(self, args, context, info): def resolve_z(self, info):
return self.z return self.z
def resolve_o(self, args, context, info): def resolve_o(self, info):
return self.o return self.o
big_container_list = [Container(x=x, y=x, z=x, o=x) for x in range(1000)] big_container_list = [Container(x=x, y=x, z=x, o=x) for x in range(1000)]
@ -393,12 +428,50 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark
class Query(ObjectType): class Query(ObjectType):
all_containers = List(Container) all_containers = List(Container)
def resolve_all_containers(self, args, context, info): def resolve_all_containers(self, info):
return big_container_list return big_container_list
hello_schema = Schema(Query) hello_schema = Schema(Query)
big_list_query = partial(hello_schema.execute, '{ allContainers { x, y, z, o } }') big_list_query = partial(hello_schema.execute,
'{ allContainers { x, y, z, o } }')
result = benchmark(big_list_query) result = benchmark(big_list_query)
assert not result.errors assert not result.errors
assert result.data == {'allContainers': [{'x': c.x, 'y': c.y, 'z': c.z, 'o': c.o} for c in big_container_list]} assert result.data == {'allContainers': [
{'x': c.x, 'y': c.y, 'z': c.z, 'o': c.o} for c in big_container_list]}
def test_query_annotated_resolvers():
import json
context = Context(key="context")
class Query(ObjectType):
annotated = String(id=String())
context = String()
info = String()
def resolve_annotated(self, info, id):
return "{}-{}".format(self, id)
def resolve_context(self, info):
assert isinstance(info.context, Context)
return "{}-{}".format(self, info.context.key)
def resolve_info(self, info):
assert isinstance(info, ResolveInfo)
return "{}-{}".format(self, info.field_name)
test_schema = Schema(Query)
result = test_schema.execute('{ annotated(id:"self") }', "base")
assert not result.errors
assert result.data == {'annotated': 'base-self'}
result = test_schema.execute('{ context }', "base", context_value=context)
assert not result.errors
assert result.data == {'context': 'base-context'}
result = test_schema.execute('{ info }', "base")
assert not result.errors
assert result.data == {'info': 'base-info'}

View File

@ -1,6 +1,6 @@
import pytest
from ..resolver import attr_resolver, dict_resolver, get_default_resolver, set_default_resolver from ..resolver import (attr_resolver, dict_resolver, get_default_resolver,
set_default_resolver)
args = {} args = {}
context = None context = None
@ -16,22 +16,22 @@ class demo_obj(object):
def test_attr_resolver(): def test_attr_resolver():
resolved = attr_resolver('attr', None, demo_obj, args, context, info) resolved = attr_resolver('attr', None, demo_obj, info, **args)
assert resolved == 'value' assert resolved == 'value'
def test_attr_resolver_default_value(): def test_attr_resolver_default_value():
resolved = attr_resolver('attr2', 'default', demo_obj, args, context, info) resolved = attr_resolver('attr2', 'default', demo_obj, info, **args)
assert resolved == 'default' assert resolved == 'default'
def test_dict_resolver(): def test_dict_resolver():
resolved = dict_resolver('attr', None, demo_dict, args, context, info) resolved = dict_resolver('attr', None, demo_dict, info, **args)
assert resolved == 'value' assert resolved == 'value'
def test_dict_resolver_default_value(): def test_dict_resolver_default_value():
resolved = dict_resolver('attr2', 'default', demo_dict, args, context, info) resolved = dict_resolver('attr2', 'default', demo_dict, info, **args)
assert resolved == 'default' assert resolved == 'default'

View File

@ -0,0 +1,10 @@
from ..scalars import Scalar
def test_scalar():
class JSONScalar(Scalar):
'''Documentation'''
assert JSONScalar._meta.name == "JSONScalar"
assert JSONScalar._meta.description == "Documentation"

View File

@ -1,9 +1,9 @@
import pytest import pytest
from ..schema import Schema from ..field import Field
from ..objecttype import ObjectType from ..objecttype import ObjectType
from ..scalars import String from ..scalars import String
from ..field import Field from ..schema import Schema
class MyOtherType(ObjectType): class MyOtherType(ObjectType):

View File

@ -1,8 +1,9 @@
import pytest
from functools import partial from functools import partial
from ..structures import List, NonNull import pytest
from ..scalars import String from ..scalars import String
from ..structures import List, NonNull
from .utils import MyLazyType from .utils import MyLazyType

View File

@ -49,8 +49,8 @@ def test_objecttype():
foo = String(bar=String(description='Argument description', default_value='x'), description='Field description') foo = String(bar=String(description='Argument description', default_value='x'), description='Field description')
bar = String(name='gizmo') bar = String(name='gizmo')
def resolve_foo(self, args, info): def resolve_foo(self, bar):
return args.get('bar') return bar
typemap = TypeMap([MyObjectType]) typemap = TypeMap([MyObjectType])
assert 'MyObjectType' in typemap assert 'MyObjectType' in typemap
@ -65,7 +65,7 @@ def test_objecttype():
assert isinstance(foo_field, GraphQLField) assert isinstance(foo_field, GraphQLField)
assert foo_field.description == 'Field description' assert foo_field.description == 'Field description'
f = MyObjectType.resolve_foo f = MyObjectType.resolve_foo
assert foo_field.resolver == getattr(f, '__func__', f) # assert foo_field.resolver == getattr(f, '__func__', f)
assert foo_field.args == { assert foo_field.args == {
'bar': GraphQLArgument(GraphQLString, description='Argument description', default_value='x', out_name='bar') 'bar': GraphQLArgument(GraphQLString, description='Argument description', default_value='x', out_name='bar')
} }
@ -135,9 +135,17 @@ def test_inputobject():
assert graphql_type.name == 'MyInputObjectType' assert graphql_type.name == 'MyInputObjectType'
assert graphql_type.description == 'Description' assert graphql_type.description == 'Description'
# Container
container = graphql_type.create_container({'bar': 'oh!'})
assert isinstance(container, MyInputObjectType)
assert 'bar' in container
assert container.bar == 'oh!'
assert 'foo_bar' not in container
fields = graphql_type.fields fields = graphql_type.fields
assert list(fields.keys()) == ['fooBar', 'gizmo', 'own'] assert list(fields.keys()) == ['fooBar', 'gizmo', 'own']
assert fields['own'].type == graphql_type own_field = fields['own']
assert own_field.type == graphql_type
foo_field = fields['fooBar'] foo_field = fields['fooBar']
assert isinstance(foo_field, GraphQLInputObjectField) assert isinstance(foo_field, GraphQLInputObjectField)
assert foo_field.description == 'Field description' assert foo_field.description == 'Field description'
@ -196,5 +204,5 @@ def test_objecttype_with_possible_types():
typemap = TypeMap([MyObjectType]) typemap = TypeMap([MyObjectType])
graphql_type = typemap['MyObjectType'] graphql_type = typemap['MyObjectType']
assert graphql_type.is_type_of assert graphql_type.is_type_of
assert graphql_type.is_type_of({}, None, None) is True assert graphql_type.is_type_of({}, None) is True
assert graphql_type.is_type_of(MyObjectType(), None, None) is False assert graphql_type.is_type_of(MyObjectType(), None) is False

View File

@ -47,6 +47,7 @@ def test_generate_union_with_no_types():
def test_union_can_be_mounted(): def test_union_can_be_mounted():
class MyUnion(Union): class MyUnion(Union):
class Meta: class Meta:
types = (MyObjectType1, MyObjectType2) types = (MyObjectType1, MyObjectType2)

View File

@ -0,0 +1,34 @@
from ..uuid import UUID
from ..objecttype import ObjectType
from ..schema import Schema
class Query(ObjectType):
uuid = UUID(input=UUID())
def resolve_uuid(self, info, input):
return input
schema = Schema(query=Query)
def test_uuidstring_query():
uuid_value = 'dfeb3bcf-70fd-11e7-a61a-6003088f8204'
result = schema.execute('''{ uuid(input: "%s") }''' % uuid_value)
assert not result.errors
assert result.data == {
'uuid': uuid_value
}
def test_uuidstring_query_variable():
uuid_value = 'dfeb3bcf-70fd-11e7-a61a-6003088f8204'
result = schema.execute(
'''query Test($uuid: UUID){ uuid(input: $uuid) }''',
variable_values={'uuid': uuid_value}
)
assert not result.errors
assert result.data == {
'uuid': uuid_value
}

View File

@ -11,10 +11,10 @@ from graphql.type.typemap import GraphQLTypeMap
from ..utils.get_unbound_function import get_unbound_function from ..utils.get_unbound_function import get_unbound_function
from ..utils.str_converters import to_camel_case from ..utils.str_converters import to_camel_case
from .definitions import (GrapheneEnumType, GrapheneInputObjectType, from .definitions import (GrapheneEnumType, GrapheneGraphQLType,
GrapheneInterfaceType, GrapheneObjectType, GrapheneInputObjectType, GrapheneInterfaceType,
GrapheneScalarType, GrapheneUnionType, GrapheneObjectType, GrapheneScalarType,
GrapheneGraphQLType) GrapheneUnionType)
from .dynamic import Dynamic from .dynamic import Dynamic
from .enum import Enum from .enum import Enum
from .field import Field from .field import Field
@ -37,12 +37,12 @@ def is_graphene_type(_type):
return True return True
def resolve_type(resolve_type_func, map, type_name, root, context, info): def resolve_type(resolve_type_func, map, type_name, root, info):
_type = resolve_type_func(root, context, info) _type = resolve_type_func(root, info)
if not _type: if not _type:
return_type = map[type_name] return_type = map[type_name]
return get_default_resolve_type_fn(root, context, info, return_type) return get_default_resolve_type_fn(root, info, return_type)
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)
@ -54,11 +54,12 @@ def resolve_type(resolve_type_func, map, type_name, root, context, info):
return _type return _type
def is_type_of_from_possible_types(possible_types, root, context, info): def is_type_of_from_possible_types(possible_types, root, info):
return isinstance(root, possible_types) return isinstance(root, possible_types)
class TypeMap(GraphQLTypeMap): class TypeMap(GraphQLTypeMap):
def __init__(self, types, auto_camelcase=True, schema=None): def __init__(self, types, auto_camelcase=True, schema=None):
self.auto_camelcase = auto_camelcase self.auto_camelcase = auto_camelcase
self.schema = schema self.schema = schema
@ -194,6 +195,7 @@ class TypeMap(GraphQLTypeMap):
graphene_type=type, graphene_type=type,
name=type._meta.name, name=type._meta.name,
description=type._meta.description, description=type._meta.description,
container_type=type._meta.container,
fields=partial( fields=partial(
self.construct_fields_for_type, map, type, is_input_type=True), self.construct_fields_for_type, map, type, is_input_type=True),
) )
@ -237,7 +239,7 @@ class TypeMap(GraphQLTypeMap):
_field = GraphQLInputObjectField( _field = GraphQLInputObjectField(
field_type, field_type,
default_value=field.default_value, default_value=field.default_value,
out_name=field.name or name, out_name=name,
description=field.description) description=field.description)
else: else:
args = OrderedDict() args = OrderedDict()
@ -254,8 +256,12 @@ class TypeMap(GraphQLTypeMap):
field_type, field_type,
args=args, args=args,
resolver=field.get_resolver( resolver=field.get_resolver(
self.get_resolver_for_type(type, name, self.get_resolver_for_type(
field.default_value)), type,
name,
field.default_value
)
),
deprecation_reason=field.deprecation_reason, deprecation_reason=field.deprecation_reason,
description=field.description) description=field.description)
field_name = field.name or self.get_name(name) field_name = field.name or self.get_name(name)

View File

@ -1,38 +1,19 @@
import six from .base import BaseOptions, BaseType
from ..utils.is_base_type import is_base_type
from ..utils.trim_docstring import trim_docstring
from .options import Options
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
class UnionMeta(type): # For static type checking with Mypy
MYPY = False
def __new__(cls, name, bases, attrs): if MYPY:
# Also ensure initialization is only performed for subclasses of from .objecttype import ObjectType # NOQA
# Union from typing import Iterable, Type # NOQA
if not is_base_type(bases, UnionMeta):
return type.__new__(cls, name, bases, attrs)
options = Options(
attrs.pop('Meta', None),
name=name,
description=trim_docstring(attrs.get('__doc__')),
types=(),
)
assert (
isinstance(options.types, (list, tuple)) and
len(options.types) > 0
), 'Must provide types for Union {}.'.format(options.name)
return type.__new__(cls, name, bases, dict(attrs, _meta=options))
def __str__(cls): # noqa: N805
return cls._meta.name
class Union(six.with_metaclass(UnionMeta, UnmountedType)): class UnionOptions(BaseOptions):
types = () # type: Iterable[Type[ObjectType]]
class Union(UnmountedType, BaseType):
''' '''
Union Type Definition Union Type Definition
@ -40,6 +21,16 @@ class Union(six.with_metaclass(UnionMeta, UnmountedType)):
is used to describe what types are possible as well as providing a function is used to describe what types are possible as well as providing a function
to determine which type is actually used when the field is resolved. to determine which type is actually used when the field is resolved.
''' '''
@classmethod
def __init_subclass_with_meta__(cls, types=None, **options):
assert (
isinstance(types, (list, tuple)) and
len(types) > 0
), 'Must provide types for Union {name}.'.format(name=cls.__name__)
_meta = UnionOptions(cls)
_meta.types = types
super(Union, cls).__init_subclass_with_meta__(_meta=_meta, **options)
@classmethod @classmethod
def get_type(cls): def get_type(cls):
@ -50,7 +41,7 @@ class Union(six.with_metaclass(UnionMeta, UnmountedType)):
return cls return cls
@classmethod @classmethod
def resolve_type(cls, instance, context, info): def resolve_type(cls, instance, info):
from .objecttype import ObjectType from .objecttype import ObjectType # NOQA
if isinstance(instance, ObjectType): if isinstance(instance, ObjectType):
return type(instance) return type(instance)

View File

@ -1,6 +1,7 @@
import inspect import inspect
from collections import OrderedDict from collections import OrderedDict
from functools import partial from functools import partial
from six import string_types from six import string_types
from ..utils.module_loading import import_string from ..utils.module_loading import import_string
@ -8,35 +9,6 @@ from .mountedtype import MountedType
from .unmountedtype import UnmountedType from .unmountedtype import UnmountedType
def merge(*dicts):
'''
Merge the dicts into one
'''
merged = OrderedDict()
for _dict in dicts:
merged.update(_dict)
return merged
def get_base_fields(bases, _as=None):
'''
Get all the fields in the given bases
'''
fields = OrderedDict()
from ..types import AbstractType, Interface
# We allow inheritance in AbstractTypes and Interfaces but not ObjectTypes
inherited_bases = (AbstractType, Interface)
for base in bases:
if base in inherited_bases or not issubclass(base, inherited_bases):
continue
for name, field in base._meta.fields.items():
if name in fields:
continue
fields[name] = get_field_as(field, _as=_as)
return fields
def get_field_as(value, _as=None): def get_field_as(value, _as=None):
''' '''
Get type mounted Get type mounted
@ -49,7 +21,7 @@ def get_field_as(value, _as=None):
return _as.mounted(value) return _as.mounted(value)
def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True): def yank_fields_from_attrs(attrs, _as=None, sort=True):
''' '''
Extract all the fields in given attributes (dict) Extract all the fields in given attributes (dict)
and return them ordered and return them ordered
@ -60,8 +32,6 @@ def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True):
if not field: if not field:
continue continue
fields_with_names.append((attname, field)) fields_with_names.append((attname, field))
if delete:
del attrs[attname]
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])
@ -71,6 +41,6 @@ def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True):
def get_type(_type): def get_type(_type):
if isinstance(_type, string_types): if isinstance(_type, string_types):
return import_string(_type) return import_string(_type)
if inspect.isfunction(_type) or type(_type) is partial: if inspect.isfunction(_type) or isinstance(_type, partial):
return _type() return _type()
return _type return _type

27
graphene/types/uuid.py Normal file
View File

@ -0,0 +1,27 @@
from __future__ import absolute_import
from uuid import UUID as _UUID
from graphql.language import ast
from .scalars import Scalar
class UUID(Scalar):
'''UUID'''
@staticmethod
def serialize(uuid):
if isinstance(uuid, str):
uuid = _UUID(uuid)
assert isinstance(uuid, _UUID), "Expected UUID instance, received {}".format(uuid)
return str(uuid)
@staticmethod
def parse_literal(node):
if isinstance(node, ast.StringValue):
return _UUID(node.value)
@staticmethod
def parse_value(value):
return _UUID(value)

View File

@ -0,0 +1,36 @@
import six
from ..pyutils.compat import signature, func_name
from .deprecated import warn_deprecation
def annotate(_func=None, _trigger_warning=True, **annotations):
if not six.PY2 and _trigger_warning:
warn_deprecation(
"annotate is intended for use in Python 2 only, as you can use type annotations Python 3.\n"
"Read more in https://docs.python.org/3/library/typing.html"
)
if not _func:
def _func(f):
return annotate(f, **annotations)
return _func
func_signature = signature(_func)
# We make sure the annotations are valid
for key, value in annotations.items():
assert key in func_signature.parameters, (
'The key {key} is not a function parameter in the function "{func_name}".'
).format(
key=key,
func_name=func_name(_func)
)
func_annotations = getattr(_func, '__annotations__', None)
if func_annotations is None:
_func.__annotations__ = annotations
else:
_func.__annotations__.update(annotations)
return _func

View File

@ -0,0 +1,80 @@
import functools
import inspect
import warnings
string_types = (type(b''), type(u''))
def warn_deprecation(text):
warnings.simplefilter('always', DeprecationWarning)
warnings.warn(
text,
category=DeprecationWarning,
stacklevel=2
)
warnings.simplefilter('default', DeprecationWarning)
def deprecated(reason):
"""
This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
when the function is used.
"""
if isinstance(reason, string_types):
# The @deprecated is used with a 'reason'.
#
# .. code-block:: python
#
# @deprecated("please, use another function")
# def old_function(x, y):
# pass
def decorator(func1):
if inspect.isclass(func1):
fmt1 = "Call to deprecated class {name} ({reason})."
else:
fmt1 = "Call to deprecated function {name} ({reason})."
@functools.wraps(func1)
def new_func1(*args, **kwargs):
warn_deprecation(
fmt1.format(name=func1.__name__, reason=reason),
)
return func1(*args, **kwargs)
return new_func1
return decorator
elif inspect.isclass(reason) or inspect.isfunction(reason):
# The @deprecated is used without any 'reason'.
#
# .. code-block:: python
#
# @deprecated
# def old_function(x, y):
# pass
func2 = reason
if inspect.isclass(func2):
fmt2 = "Call to deprecated class {name}."
else:
fmt2 = "Call to deprecated function {name}."
@functools.wraps(func2)
def new_func2(*args, **kwargs):
warn_deprecation(
fmt2.format(name=func2.__name__),
)
return func2(*args, **kwargs)
return new_func2
else:
raise TypeError(repr(type(reason)))

View File

@ -1,3 +0,0 @@
def is_base_type(bases, _type):
return any(b for b in bases if isinstance(b, _type))

View File

@ -1,8 +1,11 @@
from functools import wraps from functools import wraps
from .deprecated import deprecated
@deprecated('This function is deprecated')
def resolve_only_args(func): def resolve_only_args(func):
@wraps(func) @wraps(func)
def inner(root, args, context, info): def wrapped_func(root, info, **args):
return func(root, **args) return func(root, **args)
return inner
return wrapped_func

View File

@ -0,0 +1,44 @@
import six
from inspect import isclass
from ..pyutils.init_subclass import InitSubclassMeta
from .props import props
class SubclassWithMeta_Meta(InitSubclassMeta):
def __repr__(cls):
return cls._meta.name
class SubclassWithMeta(six.with_metaclass(SubclassWithMeta_Meta)):
"""This class improves __init_subclass__ to receive automatically the options from meta"""
# We will only have the metaclass in Python 2
def __init_subclass__(cls, **meta_options):
"""This method just terminates the super() chain"""
_Meta = getattr(cls, "Meta", None)
_meta_props = {}
if _Meta:
if isinstance(_Meta, dict):
_meta_props = _Meta
elif isclass(_Meta):
_meta_props = props(_Meta)
else:
raise Exception("Meta have to be either a class or a dict. Received {}".format(_Meta))
delattr(cls, "Meta")
options = dict(meta_options, **_meta_props)
abstract = options.pop('abstract', False)
if abstract:
assert not options, (
"Abstract types can only contain the abstract attribute. "
"Received: abstract, {option_keys}"
).format(option_keys=', '.join(options.keys()))
else:
super_class = super(cls, cls)
if hasattr(super_class, '__init_subclass_with_meta__'):
super_class.__init_subclass_with_meta__(**options)
@classmethod
def __init_subclass_with_meta__(cls, **meta_options):
"""This method just terminates the super() chain"""

View File

@ -0,0 +1,33 @@
import pytest
from ..annotate import annotate
def func(a, b, *c, **d):
pass
annotations = {
'a': int,
'b': str,
'c': list,
'd': dict
}
def func_with_annotations(a, b, *c, **d):
pass
func_with_annotations.__annotations__ = annotations
def test_annotate_with_no_params():
annotated_func = annotate(func, _trigger_warning=False)
assert annotated_func.__annotations__ == {}
def test_annotate_with_params():
annotated_func = annotate(_trigger_warning=False, **annotations)(func)
assert annotated_func.__annotations__ == annotations
def test_annotate_with_wront_params():
with pytest.raises(Exception) as exc_info:
annotated_func = annotate(p=int, _trigger_warning=False)(func)
assert str(exc_info.value) == 'The key p is not a function parameter in the function "func".'

View File

@ -0,0 +1,65 @@
import pytest
from .. import deprecated
from ..deprecated import deprecated as deprecated_decorator, warn_deprecation
def test_warn_deprecation(mocker):
mocker.patch.object(deprecated.warnings, 'warn')
warn_deprecation("OH!")
deprecated.warnings.warn.assert_called_with('OH!', stacklevel=2, category=DeprecationWarning)
def test_deprecated_decorator(mocker):
mocker.patch.object(deprecated, 'warn_deprecation')
@deprecated_decorator
def my_func():
return True
result = my_func()
assert result
deprecated.warn_deprecation.assert_called_with("Call to deprecated function my_func.")
def test_deprecated_class(mocker):
mocker.patch.object(deprecated, 'warn_deprecation')
@deprecated_decorator
class X:
pass
result = X()
assert result
deprecated.warn_deprecation.assert_called_with("Call to deprecated class X.")
def test_deprecated_decorator_text(mocker):
mocker.patch.object(deprecated, 'warn_deprecation')
@deprecated_decorator("Deprecation text")
def my_func():
return True
result = my_func()
assert result
deprecated.warn_deprecation.assert_called_with("Call to deprecated function my_func (Deprecation text).")
def test_deprecated_class_text(mocker):
mocker.patch.object(deprecated, 'warn_deprecation')
@deprecated_decorator("Deprecation text")
class X:
pass
result = X()
assert result
deprecated.warn_deprecation.assert_called_with("Call to deprecated class X (Deprecation text).")
def test_deprecated_other_object(mocker):
mocker.patch.object(deprecated, 'warn_deprecation')
with pytest.raises(TypeError) as exc_info:
deprecated_decorator({})

View File

@ -1,16 +1,16 @@
from pytest import raises from pytest import raises
from graphene import String from graphene import ObjectType, String
from graphene.types.objecttype import ObjectTypeMeta
from ..module_loading import lazy_import, import_string from ..module_loading import import_string, lazy_import
def test_import_string(): def test_import_string():
MyString = import_string('graphene.String') MyString = import_string('graphene.String')
assert MyString == String assert MyString == String
MyObjectTypeMeta = import_string('graphene.ObjectType', '__class__') MyObjectTypeMeta = import_string('graphene.ObjectType', '__doc__')
assert MyObjectTypeMeta == ObjectTypeMeta assert MyObjectTypeMeta == ObjectType.__doc__
def test_import_string_module(): def test_import_string_module():
@ -52,6 +52,6 @@ def test_lazy_import():
MyString = f() MyString = f()
assert MyString == String assert MyString == String
f = lazy_import('graphene.ObjectType', '__class__') f = lazy_import('graphene.ObjectType', '__doc__')
MyObjectTypeMeta = f() MyObjectTypeMeta = f()
assert MyObjectTypeMeta == ObjectTypeMeta assert MyObjectTypeMeta == ObjectType.__doc__

View File

@ -1,12 +1,15 @@
from ..resolve_only_args import resolve_only_args from ..resolve_only_args import resolve_only_args
from .. import deprecated
def test_resolve_only_args(): def test_resolve_only_args(mocker):
mocker.patch.object(deprecated, 'warn_deprecation')
def resolver(*args, **kwargs): def resolver(root, **args):
return kwargs return root, args
my_data = {'one': 1, 'two': 2} my_data = {'one': 1, 'two': 2}
wrapped = resolve_only_args(resolver) wrapped_resolver = resolve_only_args(resolver)
assert wrapped(None, my_data, None, None) == my_data assert deprecated.warn_deprecation.called
result = wrapped_resolver(1, 2, a=3)
assert result == (1, {'a': 3})

View File

@ -9,7 +9,6 @@ def test_trim_docstring():
Multiple paragraphs too Multiple paragraphs too
""" """
pass
assert (trim_docstring(WellDocumentedObject.__doc__) == assert (trim_docstring(WellDocumentedObject.__doc__) ==
"This object is very well-documented. It has multiple lines in its\n" "This object is very well-documented. It has multiple lines in its\n"

Some files were not shown because too many files have changed in this diff Show More