diff --git a/.gitignore b/.gitignore index 9f465556..d98ebfc3 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ target/ # Databases *.sqlite3 .vscode +.mypy_cache diff --git a/.travis.yml b/.travis.yml index 89b17c0c..2ee20a4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ language: python sudo: false python: - 2.7 -- 3.4 - 3.5 +- 3.6 - pypy before_install: - | @@ -26,6 +26,8 @@ install: python setup.py develop elif [ "$TEST_TYPE" = lint ]; then pip install flake8 + elif [ "$TEST_TYPE" = mypy ]; then + pip install mypy fi script: - | @@ -33,6 +35,10 @@ script: echo "Checking Python code lint." flake8 graphene exit + elif [ "$TEST_TYPE" = mypy ]; then + echo "Checking Python types." + mypy graphene + exit elif [ "$TEST_TYPE" = build ]; then py.test --cov=graphene graphene examples fi @@ -51,6 +57,8 @@ matrix: include: - python: '2.7' env: TEST_TYPE=lint + - python: '3.6' + env: TEST_TYPE=mypy deploy: provider: pypi user: syrusakbary @@ -58,3 +66,4 @@ deploy: tags: true password: secure: LHOp9DvYR+70vj4YVY8+JRNCKUOfYZREEUY3+4lMUpY7Zy5QwDfgEMXG64ybREH9dFldpUqVXRj53eeU3spfudSfh8NHkgqW7qihez2AhSnRc4dK6ooNfB+kLcSoJ4nUFGxdYImABc4V1hJvflGaUkTwDNYVxJF938bPaO797IvSbuI86llwqkvuK2Vegv9q/fy9sVGaF9VZIs4JgXwR5AyDR7FBArl+S84vWww4vTFD33hoE88VR4QvFY3/71BwRtQrnCMm7AOm31P9u29yi3bpzQpiOR2rHsgrsYdm597QzFKVxYwsmf9uAx2bpbSPy2WibunLePIvOFwm8xcfwnz4/J4ONBc5PSFmUytTWpzEnxb0bfUNLuYloIS24V6OZ8BfAhiYZ1AwySeJCQDM4Vk1V8IF6trTtyx5EW/uV9jsHCZ3LFsAD7UnFRTosIgN3SAK3ZWCEk5oF2IvjecsolEfkRXB3q9EjMkkuXRUeFDH2lWJLgNE27BzY6myvZVzPmfwZUsPBlPD/6w+WLSp97Rjgr9zS3T1d4ddqFM4ZYu04f2i7a/UUQqG+itzzuX5DWLPvzuNt37JB45mB9IsvxPyXZ6SkAcLl48NGyKok1f3vQnvphkfkl4lni29woKhaau8xlsuEDrcwOoeAsVcZXiItg+l+z2SlIwM0A06EvQ= + distributions: "sdist bdist_wheel" diff --git a/README.md b/README.md index e2d79edf..80c3d9c1 100644 --- a/README.md +++ b/README.md @@ -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 ```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 @@ -48,7 +48,7 @@ Here is one example for you to get started: class Query(graphene.ObjectType): hello = graphene.String(description='A typical hello world') - def resolve_hello(self, args, context, info): + def resolve_hello(self, info): return 'World' schema = graphene.Schema(query=Query) diff --git a/README.rst b/README.rst index c0fa1c28..bea6c4d4 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -Please read `UPGRADE-v1.0.md `__ to learn how to -upgrade to Graphene ``1.0``. +Please read `UPGRADE-v2.0.md `__ to learn how to +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 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 (Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe 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 - pip install "graphene>=1.0" + pip install "graphene>=2.0" -1.0 Upgrade Guide +2.0 Upgrade Guide ----------------- -Please read `UPGRADE-v1.0.md `__ to learn how to +Please read `UPGRADE-v2.0.md `__ to learn how to upgrade. Examples @@ -65,7 +65,7 @@ Here is one example for you to get started: class Query(graphene.ObjectType): hello = graphene.String(description='A typical hello world') - def resolve_hello(self, args, context, info): + def resolve_hello(self, info): return 'World' schema = graphene.Schema(query=Query) diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md new file mode 100644 index 00000000..89a0435b --- /dev/null +++ b/UPGRADE-v2.0.md @@ -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`. diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 3695fcf7..3322acfd 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -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) 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) - def resolve_friends(self, args, context, info): + def resolve_friends(self, info): return user_loader.load_many(self.friend_ids) diff --git a/docs/execution/execute.rst b/docs/execution/execute.rst index d86fdd10..327ce230 100644 --- a/docs/execution/execute.rst +++ b/docs/execution/execute.rst @@ -24,8 +24,8 @@ You can pass context to a query via ``context_value``. class Query(graphene.ObjectType): name = graphene.String() - def resolve_name(self, args, context, info): - return context.get('name') + def resolve_name(self, info): + return info.context.get('name') schema = graphene.Schema(Query) 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): user = graphene.Field(User) - def resolve_user(self, args, context, info): - return context.get('user') + def resolve_user(self, info): + return info.context.get('user') schema = graphene.Schema(Query) result = schema.execute( diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst index 3303ed41..c5e11aa7 100644 --- a/docs/execution/middleware.rst +++ b/docs/execution/middleware.rst @@ -31,10 +31,10 @@ This middleware only continues evaluation if the ``field_name`` is not ``'user'` .. code:: python class AuthorizationMiddleware(object): - def resolve(self, next, root, args, context, info): + def resolve(self, next, root, info, **args): if info.field_name == 'user': return None - return next(root, args, context, info) + return next(root, info, **args) And then execute it with: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 44a686cf..dde79c3b 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -12,15 +12,15 @@ Let’s build a basic GraphQL schema from scratch. Requirements ------------ -- Python (2.7, 3.2, 3.3, 3.4, 3.5, pypy) -- Graphene (1.0) +- Python (2.7, 3.4, 3.5, 3.6, pypy) +- Graphene (2.0) Project setup ------------- .. code:: bash - pip install "graphene>=1.0" + pip install "graphene>=2.0" 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 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): - return 'Hello ' + args['name'] + def resolve_hello(self, info, name): + return 'Hello ' + name schema = graphene.Schema(query=Query) diff --git a/docs/relay/connection.rst b/docs/relay/connection.rst index f581e3f9..c2379cbc 100644 --- a/docs/relay/connection.rst +++ b/docs/relay/connection.rst @@ -41,5 +41,5 @@ that implements ``Node`` will have a default Connection. name = graphene.String() ships = relay.ConnectionField(ShipConnection) - def resolve_ships(self, args, context, info): + def resolve_ships(self, info): return [] diff --git a/docs/relay/mutations.rst b/docs/relay/mutations.rst index 507d4a09..89bf89b3 100644 --- a/docs/relay/mutations.rst +++ b/docs/relay/mutations.rst @@ -21,9 +21,9 @@ subclass of ``relay.ClientIDMutation``. faction = graphene.Field(Faction) @classmethod - def mutate_and_get_payload(cls, input, context, info): - ship_name = input.get('ship_name') - faction_id = input.get('faction_id') + def mutate_and_get_payload(cls, root, info, **input): + ship_name = input.ship_name + faction_id = input.faction_id ship = create_ship(ship_name, faction_id) faction = get_faction(faction_id) 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() @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 files = context.FILES # Or, if used in Flask, context will be the flask global request diff --git a/docs/relay/nodes.rst b/docs/relay/nodes.rst index 5f470055..74f42094 100644 --- a/docs/relay/nodes.rst +++ b/docs/relay/nodes.rst @@ -22,7 +22,7 @@ Example usage (taken from the `Starwars Relay example`_): name = graphene.String(description='The name of the ship.') @classmethod - def get_node(cls, id, context, info): + def get_node(cls, info, id): return get_ship(id) 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) @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(':') if only_node: # We assure that the node type that we want to retrieve diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index d7f590a4..b722d41e 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -13,15 +13,14 @@ This example defines a Mutation: import graphene class CreatePerson(graphene.Mutation): - class Input: + class Arguments: name = graphene.String() ok = graphene.Boolean() person = graphene.Field(lambda: Person) - @staticmethod - def mutate(root, args, context, info): - person = Person(name=args.get('name')) + def mutate(self, name): + person = Person(name=name) ok = True 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 - - .. code:: python import graphene class PersonInput(graphene.InputObjectType): - name = graphene.String() - age = graphene.Int() + name = graphene.String(required=True) + age = graphene.Int(required=True) class CreatePerson(graphene.Mutation): - class Input: - person_data = graphene.Argument(PersonInput) + class Arguments: + person_data = PersonInput(required=True) - person = graphene.Field(lambda: Person) + person = graphene.Field(Person) @staticmethod - def mutate(root, args, context, info): - p_data = args.get('person_data') - + def mutate(root, person_data=None): name = p_data.get('name') 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) diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 2b3bce23..091617ce 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -25,7 +25,7 @@ This example model defines a Person, with a first and a last name: last_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) **first\_name** and **last\_name** are fields of the ObjectType. Each @@ -71,8 +71,7 @@ method in the class. class Query(graphene.ObjectType): reverse = graphene.String(word=graphene.String()) - def resolve_reverse(self, args, context, info): - word = args.get('word') + def resolve_reverse(self, info, word): return word[::-1] Resolvers outside the class @@ -84,8 +83,7 @@ A field can use a custom resolver from outside the class: import graphene - def reverse(root, args, context, info): - word = args.get('word') + def reverse(root, info, word): return word[::-1] class Query(graphene.ObjectType): diff --git a/docs/types/unions.rst b/docs/types/unions.rst new file mode 100644 index 00000000..f3d66e02 --- /dev/null +++ b/docs/types/unions.rst @@ -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 + diff --git a/examples/complex_example.py b/examples/complex_example.py index f6f80f81..492e6a19 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -11,10 +11,9 @@ class Address(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): - geo = args.get('geo') + def resolve_address(self, info, geo): return Address(latlng="({},{})".format(geo.get('lat'), geo.get('lng'))) diff --git a/examples/context_example.py b/examples/context_example.py index 058e578b..dfcf3548 100644 --- a/examples/context_example.py +++ b/examples/context_example.py @@ -9,8 +9,9 @@ class User(graphene.ObjectType): class Query(graphene.ObjectType): me = graphene.Field(User) - def resolve_me(self, args, context, info): - return context['user'] + def resolve_me(self, info): + return info.context['user'] + schema = graphene.Schema(query=Query) query = ''' diff --git a/examples/simple_example.py b/examples/simple_example.py index 2bc9b44f..927e0962 100644 --- a/examples/simple_example.py +++ b/examples/simple_example.py @@ -11,9 +11,10 @@ class Query(graphene.ObjectType): patron = graphene.Field(Patron) - def resolve_patron(self, args, context, info): + def resolve_patron(self, info): return Patron(id=1, name='Syrus', age=27) + schema = graphene.Schema(query=Query) query = ''' query something{ diff --git a/examples/starwars/schema.py b/examples/starwars/schema.py index 939f7253..a19a7b31 100644 --- a/examples/starwars/schema.py +++ b/examples/starwars/schema.py @@ -1,5 +1,4 @@ import graphene -from graphene import resolve_only_args from .data import get_character, get_droid, get_hero, get_human @@ -16,7 +15,7 @@ class Character(graphene.Interface): friends = graphene.List(lambda: Character) appears_in = graphene.List(Episode) - def resolve_friends(self, args, *_): + def resolve_friends(self, info): # The character friends is a list of strings return [get_character(f) for f in self.friends] @@ -46,16 +45,13 @@ class Query(graphene.ObjectType): id=graphene.String() ) - @resolve_only_args - def resolve_hero(self, episode=None): + def resolve_hero(self, info, episode=None): return get_hero(episode) - @resolve_only_args - def resolve_human(self, id): + def resolve_human(self, info, id): return get_human(id) - @resolve_only_args - def resolve_droid(self, id): + def resolve_droid(self, info, id): return get_droid(id) diff --git a/examples/starwars/tests/test_query.py b/examples/starwars/tests/test_query.py index e6a70735..d88076a5 100644 --- a/examples/starwars/tests/test_query.py +++ b/examples/starwars/tests/test_query.py @@ -1,4 +1,5 @@ from graphene.test import Client + from ..data import setup from ..schema import schema @@ -6,6 +7,7 @@ setup() client = Client(schema) + def test_hero_name_query(snapshot): query = ''' query HeroNameQuery { @@ -15,7 +17,6 @@ def test_hero_name_query(snapshot): } ''' snapshot.assert_match(client.execute(query)) - def test_hero_name_and_friends_query(snapshot): diff --git a/examples/starwars_relay/schema.py b/examples/starwars_relay/schema.py index 8ad1a643..beb291c3 100644 --- a/examples/starwars_relay/schema.py +++ b/examples/starwars_relay/schema.py @@ -1,5 +1,5 @@ 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 @@ -13,10 +13,16 @@ class Ship(graphene.ObjectType): name = graphene.String(description='The name of the ship.') @classmethod - def get_node(cls, id, context, info): + def get_node(cls, info, id): return get_ship(id) +class ShipConnection(relay.Connection): + + class Meta: + node = Ship + + class Faction(graphene.ObjectType): '''A faction in the Star Wars saga''' @@ -24,15 +30,14 @@ class Faction(graphene.ObjectType): interfaces = (relay.Node, ) 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, **args): + def resolve_ships(self, info, **args): # Transform the instance ship_ids into real instances return [get_ship(ship_id) for ship_id in self.ships] @classmethod - def get_node(cls, id, context, info): + def get_node(cls, info, id): return get_faction(id) @@ -46,9 +51,7 @@ class IntroduceShip(relay.ClientIDMutation): faction = graphene.Field(Faction) @classmethod - def mutate_and_get_payload(cls, input, context, info): - ship_name = input.get('ship_name') - faction_id = input.get('faction_id') + def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None): ship = create_ship(ship_name, faction_id) faction = get_faction(faction_id) return IntroduceShip(ship=ship, faction=faction) @@ -59,12 +62,10 @@ class Query(graphene.ObjectType): empire = graphene.Field(Faction) node = relay.Node.Field() - @resolve_only_args - def resolve_rebels(self): + def resolve_rebels(self, info): return get_rebels() - @resolve_only_args - def resolve_empire(self): + def resolve_empire(self, info): return get_empire() diff --git a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py index a6095bb0..a15d49d3 100644 --- a/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py +++ b/examples/starwars_relay/tests/snapshots/snap_test_objectidentification.py @@ -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! +} +''' diff --git a/examples/starwars_relay/tests/test_connections.py b/examples/starwars_relay/tests/test_connections.py index e3ecfa7b..bf26c0ec 100644 --- a/examples/starwars_relay/tests/test_connections.py +++ b/examples/starwars_relay/tests/test_connections.py @@ -1,4 +1,5 @@ from graphene.test import Client + from ..data import setup from ..schema import schema diff --git a/examples/starwars_relay/tests/test_mutation.py b/examples/starwars_relay/tests/test_mutation.py index 2c07f08c..fb4ab7ad 100644 --- a/examples/starwars_relay/tests/test_mutation.py +++ b/examples/starwars_relay/tests/test_mutation.py @@ -1,4 +1,5 @@ from graphene.test import Client + from ..data import setup from ..schema import schema diff --git a/examples/starwars_relay/tests/test_objectidentification.py b/examples/starwars_relay/tests/test_objectidentification.py index d1b4c529..28a5decb 100644 --- a/examples/starwars_relay/tests/test_objectidentification.py +++ b/examples/starwars_relay/tests/test_objectidentification.py @@ -1,4 +1,5 @@ from graphene.test import Client + from ..data import setup from ..schema import schema @@ -7,66 +8,8 @@ setup() client = Client(schema) -def test_str_schema(): - assert str(schema) == '''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_str_schema(snapshot): + snapshot.assert_match(str(schema)) def test_correctly_fetches_id_name_rebels(snapshot): diff --git a/graphene/__init__.py b/graphene/__init__.py index b2e166db..df0b96f1 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -1,78 +1,78 @@ 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 +from .types import ( + AbstractType, + ObjectType, + InputObjectType, + Interface, + Mutation, + Field, + InputField, + Schema, + Scalar, + String, ID, Int, Float, Boolean, + JSONString, + UUID, + List, NonNull, + Enum, + Argument, + Dynamic, + Union, + Context, + ResolveInfo +) +from .relay import ( + Node, + is_node, + GlobalID, + ClientIDMutation, + Connection, + ConnectionField, + PageInfo +) +from .utils.resolve_only_args import resolve_only_args +from .utils.module_loading import lazy_import -VERSION = (1, 4, 1, 'final', 0) +VERSION = (2, 0, 0, 'alpha', 0) __version__ = get_version(VERSION) -if not __SETUP__: +__all__ = [ + '__version__', + 'ObjectType', + 'InputObjectType', + 'Interface', + 'Mutation', + 'Field', + 'InputField', + 'Schema', + 'Scalar', + 'String', + 'ID', + 'Int', + 'Float', + 'Enum', + 'Boolean', + 'JSONString', + 'UUID', + 'List', + 'NonNull', + 'Argument', + 'Dynamic', + 'Union', + 'resolve_only_args', + 'Node', + 'is_node', + 'GlobalID', + 'ClientIDMutation', + 'Connection', + 'ConnectionField', + 'PageInfo', + 'lazy_import', + 'Context', + 'ResolveInfo', - from .types import ( - AbstractType, - ObjectType, - InputObjectType, - Interface, - Mutation, - Field, - InputField, - Schema, - Scalar, - String, ID, Int, Float, Boolean, - List, NonNull, - Enum, - Argument, - Dynamic, - Union, - ) - from .relay import ( - Node, - is_node, - GlobalID, - ClientIDMutation, - Connection, - ConnectionField, - PageInfo - ) - from .utils.resolve_only_args import resolve_only_args - from .utils.module_loading import lazy_import - - __all__ = [ - 'AbstractType', - 'ObjectType', - 'InputObjectType', - 'Interface', - 'Mutation', - 'Field', - 'InputField', - 'Schema', - 'Scalar', - 'String', - 'ID', - 'Int', - 'Float', - 'Enum', - 'Boolean', - 'List', - 'NonNull', - 'Argument', - 'Dynamic', - 'Union', - 'resolve_only_args', - 'Node', - 'is_node', - 'GlobalID', - 'ClientIDMutation', - 'Connection', - 'ConnectionField', - 'PageInfo', - 'lazy_import', - ] + # Deprecated + 'AbstractType', +] diff --git a/graphene/pyutils/compat.py b/graphene/pyutils/compat.py new file mode 100644 index 00000000..86452081 --- /dev/null +++ b/graphene/pyutils/compat.py @@ -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__ diff --git a/graphene/pyutils/init_subclass.py b/graphene/pyutils/init_subclass.py new file mode 100644 index 00000000..78e4ff19 --- /dev/null +++ b/graphene/pyutils/init_subclass.py @@ -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 diff --git a/graphene/pyutils/signature.py b/graphene/pyutils/signature.py new file mode 100644 index 00000000..1308ded9 --- /dev/null +++ b/graphene/pyutils/signature.py @@ -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 diff --git a/graphene/pyutils/tests/test_enum.py b/graphene/pyutils/tests/test_enum.py index bf15a620..8854f4c2 100644 --- a/graphene/pyutils/tests/test_enum.py +++ b/graphene/pyutils/tests/test_enum.py @@ -21,6 +21,7 @@ def test__is_dunder(): for name in non_dunder_names: assert _is_dunder(name) is False + def test__is_sunder(): sunder_names = [ '_i_', diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index be77700d..afe6ffb3 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -2,18 +2,13 @@ import re from collections import Iterable, OrderedDict from functools import partial -import six - from graphql_relay import connection_from_list from promise import Promise, is_thenable -from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, - Union) +from ..types import (Boolean, Enum, Int, Interface, List, NonNull, Scalar, + String, Union) from ..types.field import Field -from ..types.objecttype import ObjectType, ObjectTypeMeta -from ..types.options import Options -from ..utils.is_base_type import is_base_type -from ..utils.props import props +from ..types.objecttype import ObjectType, ObjectTypeOptions 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( - attrs.pop('Meta', None), - name=name, - description=None, - node=None, - ) - options.interfaces = () - options.local_fields = OrderedDict() +class Connection(ObjectType): - assert options.node, 'You have to provide a node in {}.Meta'.format(cls.__name__) - assert issubclass(options.node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)), ( + class Meta: + 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 {}.' - ).format(options.node, name) + ).format(node, cls.__name__) - base_name = re.sub('Connection$', '', options.name) or options.node._meta.name - if not options.name: - options.name = '{}Connection'.format(base_name) + base_name = re.sub('Connection$', '', name or cls.__name__) or node._meta.name + if not name: + name = '{}Connection'.format(base_name) - edge_class = attrs.pop('Edge', None) + edge_class = getattr(cls, 'Edge', None) + _node = node - class EdgeBase(AbstractType): - node = Field(options.node, description='The item at the end of the edge') + class EdgeBase(object): + node = Field(_node, description='The item at the end of the edge') cursor = String(required=True, description='A cursor for use in pagination') edge_name = '{}Edge'.format(base_name) - if edge_class and issubclass(edge_class, AbstractType): - edge = type(edge_name, (EdgeBase, edge_class, ObjectType, ), {}) + if edge_class: + edge_bases = (edge_class, EdgeBase, ObjectType,) else: - edge_attrs = props(edge_class) if edge_class else {} - edge = type(edge_name, (EdgeBase, ObjectType, ), edge_attrs) + edge_bases = (EdgeBase, ObjectType,) - class ConnectionBase(AbstractType): - page_info = Field(PageInfo, name='pageInfo', required=True) - edges = NonNull(List(edge)) + edge = type(edge_name, edge_bases, {}) + cls.Edge = edge - bases = (ConnectionBase, ) + bases - attrs = dict(attrs, _meta=options, Edge=edge) - return ObjectTypeMeta.__new__(cls, name, bases, attrs) - - -class Connection(six.with_metaclass(ConnectionMeta, ObjectType)): - pass + _meta.name = name + _meta.node = node + _meta.fields = OrderedDict([ + ('page_info', Field(PageInfo, name='pageInfo', required=True)), + ('edges', Field(NonNull(List(edge)))), + ]) + return super(Connection, cls).__init_subclass_with_meta__(_meta=_meta, **options) class IterableConnectionField(Field): @@ -109,10 +98,13 @@ class IterableConnectionField(Field): @property def type(self): type = super(IterableConnectionField, self).type + connection_type = type if is_node(type): - connection_type = type.Connection - else: - connection_type = 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), ( '{} type have to be a subclass of Connection. Received "{}".' ).format(self.__class__.__name__, connection_type) @@ -138,8 +130,8 @@ class IterableConnectionField(Field): return connection @classmethod - def connection_resolver(cls, resolver, connection_type, root, args, context, info): - resolved = resolver(root, args, context, info) + def connection_resolver(cls, resolver, connection_type, root, info, **args): + resolved = resolver(root, info, **args) on_resolve = partial(cls.resolve_connection, connection_type, args) if is_thenable(resolved): diff --git a/graphene/relay/mutation.py b/graphene/relay/mutation.py index ab1e9eb4..8c020ef0 100644 --- a/graphene/relay/mutation.py +++ b/graphene/relay/mutation.py @@ -1,68 +1,72 @@ 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, 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 +from ..types import Field, InputObjectType, String +from ..types.mutation import Mutation -class ClientIDMutationMeta(MutationMeta): - 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) +class ClientIDMutation(Mutation): - input_class = attrs.pop('Input', None) - base_name = re.sub('Payload$', '', name) - if 'client_mutation_id' not in attrs: - attrs['client_mutation_id'] = String(name='clientMutationId') - cls = ObjectTypeMeta.__new__(cls, '{}Payload'.format(base_name), bases, - attrs) + class Meta: + abstract = True + + @classmethod + def __init_subclass_with_meta__(cls, output=None, input_fields=None, + 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) if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__: assert mutate_and_get_payload, ( - "{}.mutate_and_get_payload method is required" - " in a ClientIDMutation.").format(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 + "{name}.mutate_and_get_payload method is required" + " in a ClientIDMutation.").format(name=name or cls.__name__) + 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 - def mutate(cls, root, args, context, info): - input = args.get('input') - + def mutate(cls, root, info, input): def on_resolve(payload): try: - payload.client_mutation_id = input.get('clientMutationId') + payload.client_mutation_id = input.get('client_mutation_id') except: raise Exception( ('Cannot set client_mutation_id in the payload object {}' ).format(repr(payload))) return payload - return Promise.resolve( - cls.mutate_and_get_payload(input, context, info)).then(on_resolve) + result = cls.mutate_and_get_payload(root, info, **input) + if is_thenable(result): + return Promise.resolve(result).then(on_resolve) + + return on_resolve(result) diff --git a/graphene/relay/node.py b/graphene/relay/node.py index aa6e2dc0..d762a110 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -1,12 +1,11 @@ +from collections import OrderedDict from functools import partial -import six - from graphql_relay import from_global_id, to_global_id from ..types import ID, Field, Interface, ObjectType +from ..types.interface import InterfaceOptions from ..types.utils import get_type -from ..types.interface import InterfaceMeta def is_node(objecttype): @@ -22,18 +21,6 @@ def is_node(objecttype): 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): 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 @staticmethod - def id_resolver(parent_resolver, node, root, args, context, info, parent_type_name=None): - type_id = parent_resolver(root, args, context, info) + def id_resolver(parent_resolver, node, root, info, parent_type_name=None, **args): + type_id = parent_resolver(root, info, **args) parent_type_name = parent_type_name or info.parent_type.name return node.to_global_id(parent_type_name, type_id) # root._meta.name def get_resolver(self, parent_resolver): - 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 + return partial( + self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name + ) class NodeField(Field): @@ -75,10 +56,24 @@ class NodeField(Field): ) 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''' @classmethod @@ -86,11 +81,11 @@ class Node(six.with_metaclass(NodeMeta, Interface)): return NodeField(cls, *args, **kwargs) @classmethod - def node_resolver(cls, root, args, context, info, only_type=None): - return cls.get_node_from_global_id(args.get('id'), context, info, only_type) + def node_resolver(cls, only_type, root, info, id): + return cls.get_node_from_global_id(info, id, only_type=only_type) @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: _type, _id = cls.from_global_id(global_id) 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) if get_node: - return get_node(_id, context, info) + return get_node(info, _id) @classmethod def from_global_id(cls, global_id): @@ -117,11 +112,3 @@ class Node(six.with_metaclass(NodeMeta, Interface)): @classmethod def to_global_id(cls, 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() diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index 87f937ae..b6a26df3 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -1,6 +1,7 @@ +import pytest -from ...types import AbstractType, Field, List, NonNull, ObjectType, String, Argument, Int -from ..connection import Connection, PageInfo, ConnectionField +from ...types import Argument, Field, Int, List, NonNull, ObjectType, String +from ..connection import Connection, ConnectionField, PageInfo from ..node import Node @@ -38,7 +39,7 @@ def test_connection(): def test_connection_inherit_abstracttype(): - class BaseConnection(AbstractType): + class BaseConnection(object): extra = String() class MyObjectConnection(BaseConnection, Connection): @@ -73,7 +74,7 @@ def test_edge(): def test_edge_with_bases(): - class BaseEdge(AbstractType): + class BaseEdge(object): extra = String() class MyObjectConnection(Connection): @@ -96,16 +97,6 @@ def test_edge_with_bases(): 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(): assert PageInfo._meta.name == 'PageInfo' fields = PageInfo._meta.fields @@ -114,6 +105,7 @@ def test_pageinfo(): def test_connectionfield(): class MyObjectConnection(Connection): + class Meta: 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(): class MyObjectConnection(Connection): + class Meta: node = MyObject diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index 068081d6..b8150e64 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -4,7 +4,7 @@ from graphql_relay.utils import base64 from promise import Promise from ...types import ObjectType, Schema, String -from ..connection import ConnectionField, PageInfo +from ..connection import Connection, ConnectionField, PageInfo from ..node import Node letter_chars = ['A', 'B', 'C', 'D', 'E'] @@ -18,27 +18,33 @@ class Letter(ObjectType): letter = String() +class LetterConnection(Connection): + + class Meta: + node = Letter + + class Query(ObjectType): - letters = ConnectionField(Letter) - connection_letters = ConnectionField(Letter) - promise_letters = ConnectionField(Letter) + letters = ConnectionField(LetterConnection) + connection_letters = ConnectionField(LetterConnection) + promise_letters = ConnectionField(LetterConnection) node = Node.Field() - def resolve_letters(self, args, context, info): + def resolve_letters(self, info, **args): 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())) - def resolve_connection_letters(self, args, context, info): - return Letter.Connection( + def resolve_connection_letters(self, info, **args): + return LetterConnection( page_info=PageInfo( has_next_page=True, has_previous_page=False ), edges=[ - Letter.Connection.Edge( + LetterConnection.Edge( node=Letter(id=0, letter='A'), cursor='a-cursor' ), diff --git a/graphene/relay/tests/test_global_id.py b/graphene/relay/tests/test_global_id.py index b0a6c5cb..6301f954 100644 --- a/graphene/relay/tests/test_global_id.py +++ b/graphene/relay/tests/test_global_id.py @@ -1,8 +1,8 @@ from graphql_relay import to_global_id -from ..node import Node, GlobalID -from ...types import NonNull, ID, ObjectType, String +from ...types import ID, NonNull, ObjectType, String from ...types.definitions import GrapheneObjectType +from ..node import GlobalID, Node class CustomNode(Node): @@ -48,7 +48,7 @@ def test_global_id_defaults_to_info_parent_type(): my_id = '1' gid = GlobalID() 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) @@ -56,5 +56,5 @@ def test_global_id_allows_setting_customer_parent_type(): my_id = '1' gid = GlobalID(parent_type=User) 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) diff --git a/graphene/relay/tests/test_mutation.py b/graphene/relay/tests/test_mutation.py index 8a76037a..aa5ce179 100644 --- a/graphene/relay/tests/test_mutation.py +++ b/graphene/relay/tests/test_mutation.py @@ -1,62 +1,86 @@ 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 ...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() class MyNode(ObjectType): - class Meta: - interfaces = (Node, ) - + # class Meta: + # interfaces = (Node, ) + id = ID() name = String() class SaySomething(ClientIDMutation): + class Input: what = String() phrase = String() @staticmethod - def mutate_and_get_payload(args, context, info): - what = args.get('what') + def mutate_and_get_payload(self, info, what, client_mutation_id=None): 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: what = String() phrase = String() @staticmethod - def mutate_and_get_payload(args, context, info): - what = args.get('what') + def mutate_and_get_payload(self, info, what, client_mutation_id=None): + 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))) +# MyEdge = MyNode.Connection.Edge +class MyEdge(ObjectType): + node = Field(MyNode) + cursor = String() + + class OtherMutation(ClientIDMutation): + class Input(SharedFields): additional_field = String() name = String() - my_node_edge = Field(MyNode.Connection.Edge) + my_node_edge = Field(MyEdge) - @classmethod - def mutate_and_get_payload(cls, args, context, info): - shared = args.get('shared', '') - additionalField = args.get('additionalField', '') - edge_type = MyNode.Connection.Edge + @staticmethod + def mutate_and_get_payload(self, info, shared='', additional_field='', client_mutation_id=None): + edge_type = MyEdge return OtherMutation( - name=shared + additionalField, + name=shared + additional_field, my_node_edge=edge_type(cursor='1', node=MyNode(name='name'))) @@ -66,6 +90,7 @@ class RootQuery(ObjectType): class Mutation(ObjectType): say = SaySomething.Field() + say_fixed = SaySomethingFixed.Field() say_promise = SaySomethingPromise.Field() other = OtherMutation.Field() @@ -86,6 +111,7 @@ def test_no_mutate_and_get_payload(): def test_mutation(): fields = SaySomething._meta.fields assert list(fields.keys()) == ['phrase', 'client_mutation_id'] + assert SaySomething._meta.name == "SaySomethingPayload" assert isinstance(fields['phrase'], Field) field = SaySomething.Field() assert field.type == SaySomething @@ -146,7 +172,14 @@ def test_node_query(): 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( 'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }' ) diff --git a/graphene/relay/tests/test_node.py b/graphene/relay/tests/test_node.py index 6a9c2e04..10dc5d94 100644 --- a/graphene/relay/tests/test_node.py +++ b/graphene/relay/tests/test_node.py @@ -2,12 +2,11 @@ from collections import OrderedDict from graphql_relay import to_global_id -from ...types import AbstractType, ObjectType, Schema, String -from ..connection import Connection -from ..node import Node +from ...types import ObjectType, Schema, String +from ..node import Node, is_node -class SharedNodeFields(AbstractType): +class SharedNodeFields(object): shared = String() something_else = String() @@ -23,7 +22,7 @@ class MyNode(ObjectType): name = String() @staticmethod - def get_node(id, *_): + def get_node(info, id): return MyNode(name=str(id)) @@ -37,7 +36,7 @@ class MyOtherNode(SharedNodeFields, ObjectType): return 'extra field info.' @staticmethod - def get_node(id, *_): + def get_node(info, id): return MyOtherNode(shared=str(id)) @@ -47,20 +46,14 @@ class RootQuery(ObjectType): only_node = Node.Field(MyNode) only_node_lazy = Node.Field(lambda: MyNode) + schema = Schema(query=RootQuery, types=[MyNode, MyOtherNode]) def test_node_good(): assert 'id' in MyNode._meta.fields - - -def test_node_get_connection(): - connection = MyNode.Connection - assert issubclass(connection, Connection) - - -def test_node_get_connection_dont_duplicate(): - assert MyNode.Connection == MyNode.Connection + assert is_node(MyNode) + assert not is_node(object) def test_node_query(): @@ -80,6 +73,15 @@ def test_subclassed_node_query(): [('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(): executed = schema.execute( '{ node(id:"%s") { ... on MyNode { name } } }' % "something:2" @@ -114,7 +116,7 @@ def test_node_field_only_type_wrong(): ) assert len(executed.errors) == 1 assert str(executed.errors[0]) == 'Must receive an MyOtherNode id.' - assert executed.data == { 'onlyNode': None } + assert executed.data == {'onlyNode': None} def test_node_field_only_lazy_type(): @@ -131,7 +133,7 @@ def test_node_field_only_lazy_type_wrong(): ) assert len(executed.errors) == 1 assert str(executed.errors[0]) == 'Must receive an MyOtherNode id.' - assert executed.data == { 'onlyNodeLazy': None } + assert executed.data == {'onlyNodeLazy': None} def test_str_schema(): diff --git a/graphene/relay/tests/test_node_custom.py b/graphene/relay/tests/test_node_custom.py index ba34c401..cc4e910c 100644 --- a/graphene/relay/tests/test_node_custom.py +++ b/graphene/relay/tests/test_node_custom.py @@ -15,7 +15,7 @@ class CustomNode(Node): return id @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 if id in user_data: return user_data.get(id) diff --git a/graphene/test/__init__.py b/graphene/test/__init__.py index a613d905..f7823f48 100644 --- a/graphene/test/__init__.py +++ b/graphene/test/__init__.py @@ -29,6 +29,7 @@ def format_execution_result(execution_result, format_error): class Client(object): + def __init__(self, schema, format_error=None, **execute_options): assert isinstance(schema, Schema) self.schema = schema diff --git a/graphene/tests/issues/test_313.py b/graphene/tests/issues/test_313.py index 1a67a8ec..9df6c17b 100644 --- a/graphene/tests/issues/test_313.py +++ b/graphene/tests/issues/test_313.py @@ -3,9 +3,11 @@ import graphene from graphene import resolve_only_args + class Query(graphene.ObjectType): rand = graphene.String() + class Success(graphene.ObjectType): yeah = graphene.String() @@ -15,18 +17,19 @@ class Error(graphene.ObjectType): class CreatePostResult(graphene.Union): + class Meta: types = [Success, Error] class CreatePost(graphene.Mutation): + class Input: text = graphene.String(required=True) result = graphene.Field(CreatePostResult) - @resolve_only_args - def mutate(self, text): + def mutate(self, info, text): result = Success(yeah='yeah') return CreatePost(result=result) @@ -37,6 +40,7 @@ class Mutations(graphene.ObjectType): # tests.py + def test_create_post(): query_string = ''' mutation { @@ -52,4 +56,4 @@ def test_create_post(): result = schema.execute(query_string) assert not result.errors - assert result.data['createPost']['result']['__typename'] == 'Success' \ No newline at end of file + assert result.data['createPost']['result']['__typename'] == 'Success' diff --git a/graphene/tests/issues/test_356.py b/graphene/tests/issues/test_356.py index 605594e1..8eeaed10 100644 --- a/graphene/tests/issues/test_356.py +++ b/graphene/tests/issues/test_356.py @@ -1,19 +1,25 @@ # https://github.com/graphql-python/graphene/issues/356 import pytest + import graphene from graphene import relay + class SomeTypeOne(graphene.ObjectType): pass + class SomeTypeTwo(graphene.ObjectType): pass + class MyUnion(graphene.Union): + class Meta: types = (SomeTypeOne, SomeTypeTwo) + def test_issue(): with pytest.raises(Exception) as exc_info: class Query(graphene.ObjectType): diff --git a/graphene/tests/issues/test_425.py b/graphene/tests/issues/test_425.py index 08bdde8c..7f92a75a 100644 --- a/graphene/tests/issues/test_425.py +++ b/graphene/tests/issues/test_425.py @@ -1,36 +1,25 @@ # 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 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 +from graphene.types.objecttype import ObjectType, ObjectTypeOptions -class SpecialObjectType(six.with_metaclass(SpecialObjectTypeMeta, ObjectType)): - pass +class SpecialOptions(ObjectTypeOptions): + 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(): class MyType(SpecialObjectType): + class Meta: other_attr = 'yeah!' @@ -49,5 +38,5 @@ def test_special_objecttype_inherit_meta_options(): pass assert MyType._meta.name == 'MyType' - assert MyType._meta.default_resolver == None + assert MyType._meta.default_resolver is None assert MyType._meta.interfaces == () diff --git a/graphene/tests/issues/test_490.py b/graphene/tests/issues/test_490.py index 0ecd8911..9bd00590 100644 --- a/graphene/tests/issues/test_490.py +++ b/graphene/tests/issues/test_490.py @@ -1,14 +1,13 @@ # https://github.com/graphql-python/graphene/issues/313 import graphene -from graphene import resolve_only_args class Query(graphene.ObjectType): some_field = graphene.String(from_=graphene.String(name="from")) - def resolve_some_field(_, args, context, infos): - return args.get("from_") + def resolve_some_field(self, info, from_=None): + return from_ def test_issue(): diff --git a/graphene/types/__init__.py b/graphene/types/__init__.py index fcb3fbfd..2699740a 100644 --- a/graphene/types/__init__.py +++ b/graphene/types/__init__.py @@ -1,10 +1,12 @@ # flake8: noqa +from graphql import ResolveInfo from .objecttype import ObjectType -from .abstracttype import AbstractType from .interface import Interface from .mutation import Mutation from .scalars import Scalar, String, ID, Int, Float, Boolean +from .json import JSONString +from .uuid import UUID from .schema import Schema from .structures import List, NonNull from .enum import Enum @@ -14,10 +16,13 @@ from .argument import Argument from .inputobjecttype import InputObjectType from .dynamic import Dynamic from .union import Union +from .context import Context + +# Deprecated +from .abstracttype import AbstractType __all__ = [ - 'AbstractType', 'ObjectType', 'InputObjectType', 'Interface', @@ -31,10 +36,17 @@ __all__ = [ 'ID', 'Int', 'Float', + 'JSONString', + 'UUID', 'Boolean', 'List', 'NonNull', 'Argument', 'Dynamic', 'Union', + 'Context', + 'ResolveInfo', + + # Deprecated + 'AbstractType', ] diff --git a/graphene/types/abstracttype.py b/graphene/types/abstracttype.py index a5bbb73e..3e283493 100644 --- a/graphene/types/abstracttype.py +++ b/graphene/types/abstracttype.py @@ -1,41 +1,12 @@ -import six - -from ..utils.is_base_type import is_base_type -from .options import Options -from .utils import get_base_fields, merge, yank_fields_from_attrs +from ..utils.subclass_with_meta import SubclassWithMeta +from ..utils.deprecated import warn_deprecation -class AbstractTypeMeta(type): - ''' - AbstractType Definition +class AbstractType(SubclassWithMeta): - When we want to share fields across multiple types, like a Interface, - a ObjectType and a Input ObjectType we can use AbstractTypes for defining - our fields that the other types will inherit from. - ''' - - 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) + def __init_subclass__(cls, *args, **kwargs): + warn_deprecation( + "Abstract type is deprecated, please use normal object inheritance instead.\n" + "See more: https://github.com/graphql-python/graphene/blob/2.0/UPGRADE-v2.0.md#deprecations" ) - cls = type.__new__(cls, name, bases, dict(attrs, _meta=options)) - - return cls - - -class AbstractType(six.with_metaclass(AbstractTypeMeta)): - pass + super(AbstractType, cls).__init_subclass__(*args, **kwargs) diff --git a/graphene/types/argument.py b/graphene/types/argument.py index cb28ee5e..df032510 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -1,9 +1,9 @@ from collections import OrderedDict from itertools import chain +from .dynamic import Dynamic from .mountedtype import MountedType from .structures import NonNull -from .dynamic import Dynamic from .utils import get_type diff --git a/graphene/types/base.py b/graphene/types/base.py new file mode 100644 index 00000000..d9c3bdd4 --- /dev/null +++ b/graphene/types/base.py @@ -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__() diff --git a/graphene/types/context.py b/graphene/types/context.py new file mode 100644 index 00000000..bac2073c --- /dev/null +++ b/graphene/types/context.py @@ -0,0 +1,4 @@ +class Context(object): + def __init__(self, **params): + for key, value in params.items(): + setattr(self, key, value) diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 029e6991..a328cc93 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -2,15 +2,12 @@ from collections import OrderedDict import six -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .options import Options +from graphene.utils.subclass_with_meta import SubclassWithMeta_Meta + +from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType -try: - from enum import Enum as PyEnum -except ImportError: - from ..pyutils.enum import Enum as PyEnum +from ..pyutils.compat import Enum as PyEnum def eq_enum(self, other): @@ -19,29 +16,18 @@ def eq_enum(self, 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( - attrs.pop('Meta', None), - name=name, - description=trim_docstring(attrs.get('__doc__')), - enum=None, - ) - if not options.enum: - attrs['__eq__'] = eq_enum - options.enum = PyEnum(cls.__name__, attrs) +class EnumOptions(BaseOptions): + enum = None # type: Enum - new_attrs = OrderedDict(attrs, _meta=options, **options.enum.__members__) - return type.__new__(cls, name, bases, new_attrs) - def __prepare__(name, bases, **kwargs): # noqa: N805 - return OrderedDict() +class EnumMeta(SubclassWithMeta_Meta): + + 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): return cls._meta.enum(value) @@ -49,29 +35,30 @@ class EnumTypeMeta(type): def __getitem__(cls, value): return cls._meta.enum[value] + def __prepare__(name, bases, **kwargs): # noqa: N805 + return OrderedDict() + def __call__(cls, *args, **kwargs): # noqa: N805 if cls is Enum: description = kwargs.pop('description', None) 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) def from_enum(cls, enum, description=None): # noqa: N805 meta_class = type('Meta', (object,), {'enum': enum, 'description': description}) 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)): - ''' - Enum Type Definition - - Some leaf values of requests and input values are Enums. GraphQL serializes - Enum values as strings, however internally Enums can be represented by any - kind of type, often integers. - ''' + @classmethod + def __init_subclass_with_meta__(cls, enum=None, **options): + _meta = EnumOptions(cls) + _meta.enum = enum or cls.__enum__ + for key, value in _meta.enum.__members__.items(): + setattr(cls, key, value) + super(Enum, cls).__init_subclass_with_meta__(_meta=_meta, **options) @classmethod def get_type(cls): diff --git a/graphene/types/field.py b/graphene/types/field.py index 06632d35..4a79fcac 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -8,11 +8,10 @@ from .structures import NonNull from .unmountedtype import UnmountedType from .utils import get_type - base_type = type -def source_resolver(source, root, args, context, info): +def source_resolver(source, root, info, **args): resolved = getattr(root, source, None) if inspect.isfunction(resolved) or inspect.ismethod(resolved): return resolved() diff --git a/graphene/types/generic.py b/graphene/types/generic.py index a1034bfd..3170e38d 100644 --- a/graphene/types/generic.py +++ b/graphene/types/generic.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals +from graphene.types.scalars import MAX_INT, MIN_INT 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 diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 1796988a..38173c79 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -1,45 +1,36 @@ -import six +from collections import OrderedDict -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .abstracttype import AbstractTypeMeta +from .base import BaseOptions, BaseType from .inputfield import InputField -from .options import Options 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): - - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # 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 +# For static type checking with Mypy +MYPY = False +if MYPY: + from typing import Dict, Callable # NOQA -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 @@ -49,6 +40,22 @@ class InputObjectType(six.with_metaclass(InputObjectTypeMeta, UnmountedType)): 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 def get_type(cls): ''' diff --git a/graphene/types/interface.py b/graphene/types/interface.py index f0980b6c..dbc3b476 100644 --- a/graphene/types/interface.py +++ b/graphene/types/interface.py @@ -1,45 +1,20 @@ -import six +from collections import OrderedDict -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .abstracttype import AbstractTypeMeta +from .base import BaseOptions, BaseType from .field import Field -from .options import Options -from .utils import get_base_fields, merge, yank_fields_from_attrs +from .utils import yank_fields_from_attrs + +# For static type checking with Mypy +MYPY = False +if MYPY: + from typing import Dict # NOQA -class InterfaceMeta(AbstractTypeMeta): - - 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 InterfaceOptions(BaseOptions): + fields = None # type: Dict[str, Field] -class Interface(six.with_metaclass(InterfaceMeta)): +class Interface(BaseType): ''' 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 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 - def resolve_type(cls, instance, context, info): + def resolve_type(cls, instance, info): from .objecttype import ObjectType if isinstance(instance, ObjectType): return type(instance) def __init__(self, *args, **kwargs): raise Exception("An Interface cannot be intitialized") - - @classmethod - def implements(cls, objecttype): - pass diff --git a/graphene/types/mutation.py b/graphene/types/mutation.py index 2e8e120a..25794d47 100644 --- a/graphene/types/mutation.py +++ b/graphene/types/mutation.py @@ -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.props import props 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): - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # Mutation - if not is_base_type(bases, MutationMeta): - 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 +# For static type checking with Mypy +MYPY = False +if MYPY: + from .argument import Argument # NOQA + from typing import Dict, Type, Callable # NOQA -class Mutation(six.with_metaclass(MutationMeta, ObjectType)): - pass +class MutationOptions(ObjectTypeOptions): + 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 + ) diff --git a/graphene/types/objecttype.py b/graphene/types/objecttype.py index 1f0e5b8d..fe234b09 100644 --- a/graphene/types/objecttype.py +++ b/graphene/types/objecttype.py @@ -1,82 +1,64 @@ from collections import OrderedDict -import six - -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .abstracttype import AbstractTypeMeta +from .base import BaseOptions, BaseType from .field import Field from .interface import Interface -from .options import Options -from .utils import get_base_fields, merge, yank_fields_from_attrs +from .utils import yank_fields_from_attrs + +# For static type checking with Mypy +MYPY = False +if MYPY: + from typing import Dict, Iterable, Type # NOQA -class ObjectTypeMeta(AbstractTypeMeta): - - def __new__(cls, name, bases, attrs): - # 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 ObjectTypeOptions(BaseOptions): + fields = None # type: Dict[str, Field] + interfaces = () # type: Iterable[Type[Interface]] -class ObjectType(six.with_metaclass(ObjectTypeMeta)): +class ObjectType(BaseType): ''' Object Type Definition Almost all of the GraphQL types you define will be object types. Object types 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 diff --git a/graphene/types/options.py b/graphene/types/options.py deleted file mode 100644 index 7cefbea0..00000000 --- a/graphene/types/options.py +++ /dev/null @@ -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 ''.format(props_as_attrs) diff --git a/graphene/types/resolver.py b/graphene/types/resolver.py index 1f395b50..e5652c2d 100644 --- a/graphene/types/resolver.py +++ b/graphene/types/resolver.py @@ -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) -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) diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index e1ff80d3..3b78185d 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -1,34 +1,17 @@ import six + from graphql.language.ast import (BooleanValue, FloatValue, IntValue, StringValue) -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .options import Options +from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType -class ScalarTypeMeta(type): - - 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 ScalarOptions(BaseOptions): + pass -class Scalar(six.with_metaclass(ScalarTypeMeta, UnmountedType)): +class Scalar(UnmountedType, BaseType): ''' 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 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 parse_value = None @@ -99,6 +86,7 @@ class Float(Scalar): @staticmethod def coerce_float(value): + # type: (Any) -> float try: return float(value) except ValueError: diff --git a/graphene/types/structures.py b/graphene/types/structures.py index 38fa5609..dcd9d5e3 100644 --- a/graphene/types/structures.py +++ b/graphene/types/structures.py @@ -68,9 +68,9 @@ class NonNull(Structure): def __init__(self, *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: {}.' - ).format(self.of_type) + ).format(self._of_type) def __str__(self): return '{}!'.format(self.of_type) diff --git a/graphene/types/tests/test_abstracttype.py b/graphene/types/tests/test_abstracttype.py index dabf1e68..6484deb8 100644 --- a/graphene/types/tests/test_abstracttype.py +++ b/graphene/types/tests/test_abstracttype.py @@ -1,10 +1,13 @@ +import pytest -from ..abstracttype import AbstractType -from ..field import Field +from ..objecttype import ObjectType from ..unmountedtype import UnmountedType +from ..abstracttype import AbstractType +from .. import abstracttype +from ..field import Field -class MyType(object): +class MyType(ObjectType): pass @@ -14,29 +17,24 @@ class MyScalar(UnmountedType): return MyType -def test_generate_abstracttype_with_fields(): +def test_abstract_objecttype_warn_deprecation(mocker): + mocker.patch.object(abstracttype, 'warn_deprecation') + class MyAbstractType(AbstractType): - field = Field(MyType) + field1 = MyScalar() - assert 'field' in MyAbstractType._meta.fields - assert isinstance(MyAbstractType._meta.fields['field'], Field) + assert abstracttype.warn_deprecation.called -def test_generate_abstracttype_with_unmountedfields(): +def test_generate_objecttype_inherit_abstracttype(): class MyAbstractType(AbstractType): - field = UnmountedType(MyType) + field1 = MyScalar() - assert 'field' in MyAbstractType._meta.fields - assert isinstance(MyAbstractType._meta.fields['field'], UnmountedType) + class MyObjectType(ObjectType, MyAbstractType): + field2 = MyScalar() - -def test_generate_abstracttype_inheritance(): - class MyAbstractType1(AbstractType): - field1 = UnmountedType(MyType) - - class MyAbstractType2(MyAbstractType1): - field2 = UnmountedType(MyType) - - assert list(MyAbstractType2._meta.fields.keys()) == ['field1', 'field2'] - assert not hasattr(MyAbstractType1, 'field1') - assert not hasattr(MyAbstractType2, 'field2') + assert MyObjectType._meta.description is None + assert MyObjectType._meta.interfaces == () + assert MyObjectType._meta.name == "MyObjectType" + assert list(MyObjectType._meta.fields.keys()) == ['field1', 'field2'] + assert list(map(type, MyObjectType._meta.fields.values())) == [Field, Field] diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py index e485c01b..744c9e29 100644 --- a/graphene/types/tests/test_argument.py +++ b/graphene/types/tests/test_argument.py @@ -1,11 +1,12 @@ -import pytest from functools import partial +import pytest + from ..argument import Argument, to_arguments from ..field import Field from ..inputfield import InputField -from ..structures import NonNull from ..scalars import String +from ..structures import NonNull def test_argument(): @@ -73,4 +74,4 @@ def test_argument_with_lazy_type(): def test_argument_with_lazy_partial_type(): MyType = object() arg = Argument(partial(lambda: MyType)) - assert arg.type == MyType \ No newline at end of file + assert arg.type == MyType diff --git a/graphene/types/tests/test_base.py b/graphene/types/tests/test_base.py new file mode 100644 index 00000000..2ea3dcce --- /dev/null +++ b/graphene/types/tests/test_base.py @@ -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" diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index 651850cb..9d23fee5 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -1,4 +1,5 @@ import datetime + import pytz from ..datetime import DateTime, Time @@ -10,12 +11,11 @@ class Query(ObjectType): datetime = DateTime(_in=DateTime(name='in')) time = Time(_at=Time(name='at')) - def resolve_datetime(self, args, context, info): - _in = args.get('_in') + def resolve_datetime(self, info, _in=None): return _in - def resolve_time(self, args, context, info): - return args.get('_at') + def resolve_time(self, info, _at=None): + return _at schema = Schema(query=Query) diff --git a/graphene/types/tests/test_definition.py b/graphene/types/tests/test_definition.py index b040c42d..af9168c9 100644 --- a/graphene/types/tests/test_definition.py +++ b/graphene/types/tests/test_definition.py @@ -1,6 +1,5 @@ -from ..abstracttype import AbstractType from ..argument import Argument from ..enum import Enum from ..field import Field @@ -296,7 +295,7 @@ def test_stringifies_simple_types(): def test_does_not_mutate_passed_field_definitions(): - class CommonFields(AbstractType): + class CommonFields(object): field1 = String() field2 = String(id=String()) @@ -307,12 +306,8 @@ def test_does_not_mutate_passed_field_definitions(): pass 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() field2 = String() @@ -323,8 +318,3 @@ def test_does_not_mutate_passed_field_definitions(): pass assert TestInputObject1._meta.fields == TestInputObject2._meta.fields - - assert CommonFields._meta.fields == { - 'field1': String(), - 'field2': String(), - } diff --git a/graphene/types/tests/test_dynamic.py b/graphene/types/tests/test_dynamic.py index 61dcbd81..4e72395f 100644 --- a/graphene/types/tests/test_dynamic.py +++ b/graphene/types/tests/test_dynamic.py @@ -1,6 +1,6 @@ -from ..structures import List, NonNull -from ..scalars import String from ..dynamic import Dynamic +from ..scalars import String +from ..structures import List, NonNull def test_dynamic(): diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 6cd22bd9..42f00605 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -1,7 +1,7 @@ +from ..argument import Argument from ..enum import Enum, PyEnum from ..field import Field from ..inputfield import InputField -from ..argument import Argument def test_enum_construction(): diff --git a/graphene/types/tests/test_field.py b/graphene/types/tests/test_field.py index 80a32154..d2bb8c36 100644 --- a/graphene/types/tests/test_field.py +++ b/graphene/types/tests/test_field.py @@ -1,10 +1,11 @@ -import pytest from functools import partial +import pytest + from ..argument import Argument from ..field import Field -from ..structures import NonNull from ..scalars import String +from ..structures import NonNull from .utils import MyLazyType @@ -19,10 +20,11 @@ class MyInstance(object): def test_field_basic(): MyType = object() args = {'my arg': Argument(True)} - resolver = lambda: None + + def resolver(): return None deprecation_reason = 'Deprecated now' description = 'My Field' - my_default='something' + my_default = 'something' field = Field( MyType, name='name', @@ -59,7 +61,7 @@ def test_field_default_value_not_callable(): def test_field_source(): MyType = object() 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(): @@ -83,19 +85,20 @@ def test_field_not_source_and_resolver(): MyType = object() with pytest.raises(Exception) as exc_info: 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(): MyType = object() 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(): MyType = object() 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(): diff --git a/graphene/types/tests/test_generic.py b/graphene/types/tests/test_generic.py index aede1a8b..be832763 100644 --- a/graphene/types/tests/test_generic.py +++ b/graphene/types/tests/test_generic.py @@ -6,8 +6,7 @@ from ..schema import Schema class Query(ObjectType): generic = GenericScalar(input=GenericScalar()) - def resolve_generic(self, args, context, info): - input = args.get('input') + def resolve_generic(self, info, input=None): return input diff --git a/graphene/types/tests/test_inputfield.py b/graphene/types/tests/test_inputfield.py index a0888e44..bfedfb05 100644 --- a/graphene/types/tests/test_inputfield.py +++ b/graphene/types/tests/test_inputfield.py @@ -1,4 +1,3 @@ -import pytest from functools import partial from ..inputfield import InputField diff --git a/graphene/types/tests/test_inputobjecttype.py b/graphene/types/tests/test_inputobjecttype.py index 7f8eaa7a..77b1eb0e 100644 --- a/graphene/types/tests/test_inputobjecttype.py +++ b/graphene/types/tests/test_inputobjecttype.py @@ -1,10 +1,9 @@ -from ..abstracttype import AbstractType -from ..field import Field from ..argument import Argument +from ..field import Field from ..inputfield import InputField -from ..objecttype import ObjectType from ..inputobjecttype import InputObjectType +from ..objecttype import ObjectType from ..unmountedtype import UnmountedType @@ -69,7 +68,7 @@ def test_generate_inputobjecttype_as_argument(): class MyObjectType(ObjectType): field = Field(MyType, input=MyInputObjectType()) - + assert 'field' in MyObjectType._meta.fields field = MyObjectType._meta.fields['field'] assert isinstance(field, Field) @@ -80,7 +79,7 @@ def test_generate_inputobjecttype_as_argument(): def test_generate_inputobjecttype_inherit_abstracttype(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar(MyType) class MyInputObjectType(InputObjectType, MyAbstractType): @@ -91,7 +90,7 @@ def test_generate_inputobjecttype_inherit_abstracttype(): def test_generate_inputobjecttype_inherit_abstracttype_reversed(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar(MyType) class MyInputObjectType(MyAbstractType, InputObjectType): diff --git a/graphene/types/tests/test_interface.py b/graphene/types/tests/test_interface.py index f9c12250..444ff4a6 100644 --- a/graphene/types/tests/test_interface.py +++ b/graphene/types/tests/test_interface.py @@ -1,5 +1,3 @@ - -from ..abstracttype import AbstractType from ..field import Field from ..interface import Interface from ..unmountedtype import UnmountedType @@ -61,7 +59,7 @@ def test_generate_interface_unmountedtype(): def test_generate_interface_inherit_abstracttype(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar() class MyInterface(Interface, MyAbstractType): @@ -84,7 +82,7 @@ def test_generate_interface_inherit_interface(): def test_generate_interface_inherit_abstracttype_reversed(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar() class MyInterface(MyAbstractType, Interface): diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py index ef6425a9..cadc729f 100644 --- a/graphene/types/tests/test_json.py +++ b/graphene/types/tests/test_json.py @@ -1,4 +1,3 @@ -import json from ..json import JSONString from ..objecttype import ObjectType @@ -8,8 +7,7 @@ from ..schema import Schema class Query(ObjectType): json = JSONString(input=JSONString()) - def resolve_json(self, args, context, info): - input = args.get('input') + def resolve_json(self, info, input): return input schema = Schema(query=Query) @@ -19,7 +17,7 @@ def test_jsonstring_query(): json_value = '{"key": "value"}' json_value_quoted = json_value.replace('"', '\\"') - result = schema.execute('''{ json(input: "%s") }'''%json_value_quoted) + result = schema.execute('''{ json(input: "%s") }''' % json_value_quoted) assert not result.errors assert result.data == { 'json': json_value diff --git a/graphene/types/tests/test_mountedtype.py b/graphene/types/tests/test_mountedtype.py index 0bc39a2f..9dcc11c7 100644 --- a/graphene/types/tests/test_mountedtype.py +++ b/graphene/types/tests/test_mountedtype.py @@ -1,11 +1,10 @@ -import pytest -from ..mountedtype import MountedType from ..field import Field from ..scalars import String class CustomField(Field): + def __init__(self, *args, **kwargs): self.metadata = kwargs.pop('metadata', None) super(CustomField, self).__init__(*args, **kwargs) diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 0f6d8900..b4d65dcc 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -1,40 +1,41 @@ import pytest +from ..argument import Argument +from ..dynamic import Dynamic from ..mutation import Mutation from ..objecttype import ObjectType -from ..schema import Schema -from ..argument import Argument from ..scalars import String -from ..dynamic import Dynamic +from ..schema import Schema def test_generate_mutation_no_args(): class MyMutation(Mutation): '''Documentation''' - @classmethod - def mutate(cls, *args, **kwargs): - pass + def mutate(self, info, **args): + return args assert issubclass(MyMutation, ObjectType) assert MyMutation._meta.name == "MyMutation" 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(): class MyMutation(Mutation): + class Meta: name = 'MyOtherMutation' description = 'Documentation' - @classmethod - def mutate(cls, *args, **kwargs): - pass + def mutate(self, info, **args): + return args assert MyMutation._meta.name == "MyOtherMutation" 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(): @@ -52,24 +53,26 @@ def test_mutation_custom_output_type(): name = String() class CreateUser(Mutation): + class Input: name = String() Output = User - @classmethod - def mutate(cls, args, context, info): - name = args.get('name') + def mutate(self, info, name): return User(name=name) field = CreateUser.Field() assert field.type == User 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(): class CreateUser(Mutation): + class Input: name = String() dynamic = Dynamic(lambda: String()) @@ -78,9 +81,7 @@ def test_mutation_execution(): name = String() dynamic = Dynamic(lambda: String()) - def mutate(self, args, context, info): - name = args.get('name') - dynamic = args.get('dynamic') + def mutate(self, info, name, dynamic): return CreateUser(name=name, dynamic=dynamic) class Query(ObjectType): diff --git a/graphene/types/tests/test_objecttype.py b/graphene/types/tests/test_objecttype.py index 8a1cf898..5ff972de 100644 --- a/graphene/types/tests/test_objecttype.py +++ b/graphene/types/tests/test_objecttype.py @@ -1,6 +1,5 @@ import pytest -from ..abstracttype import AbstractType from ..field import Field from ..interface import Interface from ..objecttype import ObjectType @@ -89,7 +88,7 @@ def test_ordered_fields_in_objecttype(): def test_generate_objecttype_inherit_abstracttype(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar() class MyObjectType(ObjectType, MyAbstractType): @@ -103,7 +102,7 @@ def test_generate_objecttype_inherit_abstracttype(): def test_generate_objecttype_inherit_abstracttype_reversed(): - class MyAbstractType(AbstractType): + class MyAbstractType(object): field1 = MyScalar() class MyObjectType(MyAbstractType, ObjectType): @@ -188,6 +187,7 @@ def test_generate_objecttype_description(): def test_objecttype_with_possible_types(): class MyObjectType(ObjectType): + class Meta: 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(): with pytest.raises(AssertionError) as excinfo: class MyObjectType(ObjectType): + class Meta: possible_types = (dict, ) diff --git a/graphene/types/tests/test_options.py b/graphene/types/tests/test_options.py deleted file mode 100644 index fbcba2db..00000000 --- a/graphene/types/tests/test_options.py +++ /dev/null @@ -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) == '' diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index daeb63e8..95e626ef 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -1,18 +1,19 @@ import json 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 ..interface import Interface from ..inputfield import InputField from ..inputobjecttype import InputObjectType +from ..interface import Interface from ..objecttype import ObjectType from ..scalars import Int, String from ..schema import Schema from ..structures import List from ..union import Union -from ..dynamic import Dynamic +from ..context import Context def test_query(): @@ -26,6 +27,23 @@ def test_query(): 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(): class one_object(object): pass @@ -37,24 +55,25 @@ def test_query_union(): one = String() @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): return isinstance(root, one_object) class Two(ObjectType): two = String() @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): return isinstance(root, two_object) class MyUnion(Union): + class Meta: types = (One, Two) class Query(ObjectType): unions = List(MyUnion) - def resolve_unions(self, args, context, info): + def resolve_unions(self, info): return [one_object(), two_object()] hello_schema = Schema(Query) @@ -81,29 +100,31 @@ def test_query_interface(): base = String() class One(ObjectType): + class Meta: interfaces = (MyInterface, ) one = String() @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): return isinstance(root, one_object) class Two(ObjectType): + class Meta: interfaces = (MyInterface, ) two = String() @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): return isinstance(root, two_object) class Query(ObjectType): interfaces = List(MyInterface) - def resolve_interfaces(self, args, context, info): + def resolve_interfaces(self, info): return [one_object(), two_object()] hello_schema = Schema(Query, types=[One, Two]) @@ -123,13 +144,15 @@ def test_query_dynamic(): class Query(ObjectType): hello = Dynamic(lambda: String(resolver=lambda *_: 'World')) 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) executed = hello_schema.execute('{ hello hellos helloField }') 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(): @@ -151,7 +174,7 @@ def test_query_wrong_default_value(): field = String() @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): return isinstance(root, MyType) class Query(ObjectType): @@ -161,7 +184,8 @@ def test_query_wrong_default_value(): executed = hello_schema.execute('{ hello { field } }') 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} @@ -170,7 +194,8 @@ def test_query_default_value_ignored_by_resolver(): field = String() 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) @@ -183,7 +208,7 @@ def test_query_resolve_function(): class Query(ObjectType): hello = String() - def resolve_hello(self, args, context, info): + def resolve_hello(self, info): return 'World' hello_schema = Schema(Query) @@ -197,7 +222,7 @@ def test_query_arguments(): class Query(ObjectType): 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=(',', ':')) test_schema = Schema(Query) @@ -210,7 +235,8 @@ def test_query_arguments(): assert not result.errors 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 result.data in [ {'test': '["Source!",{"a_str":"String!","a_int":-123}]'}, @@ -226,7 +252,7 @@ def test_query_input_field(): class Query(ObjectType): test = String(a_input=Input()) - def resolve_test(self, args, context, info): + def resolve_test(self, info, **args): return json.dumps([self, args], separators=(',', ':')) test_schema = Schema(Query) @@ -235,13 +261,17 @@ def test_query_input_field(): assert not result.errors 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 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 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(): @@ -249,10 +279,10 @@ def test_query_middlewares(): hello = String() other = String() - def resolve_hello(self, args, context, info): + def resolve_hello(self, info): return 'World' - def resolve_other(self, args, context, info): + def resolve_other(self, info): return 'other' def reversed_middleware(next, *args, **kwargs): @@ -261,27 +291,29 @@ def test_query_middlewares(): 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 executed.data == {'hello': 'dlroW', 'other': 'rehto'} def test_objecttype_on_instances(): class Ship: + def __init__(self, name): self.name = name class ShipType(ObjectType): 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 return self.name class Query(ObjectType): ship = Field(ShipType) - def resolve_ship(self, context, args, info): + def resolve_ship(self, info): return Ship(name='xwing') schema = Schema(query=Query) @@ -296,7 +328,7 @@ def test_big_list_query_benchmark(benchmark): class Query(ObjectType): all_ints = List(Int) - def resolve_all_ints(self, args, context, info): + def resolve_all_ints(self, info): return big_list hello_schema = Schema(Query) @@ -313,7 +345,7 @@ def test_big_list_query_compiled_query_benchmark(benchmark): class Query(ObjectType): all_ints = List(Int) - def resolve_all_ints(self, args, context, info): + def resolve_all_ints(self, info): return big_list hello_schema = Schema(Query) @@ -335,7 +367,7 @@ def test_big_list_of_containers_query_benchmark(benchmark): class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self, args, context, info): + def resolve_all_containers(self, info): return big_container_list 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 } }') result = benchmark(big_list_query) 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): @@ -358,15 +391,17 @@ def test_big_list_of_containers_multiple_fields_query_benchmark(benchmark): class Query(ObjectType): all_containers = List(Container) - def resolve_all_containers(self, args, context, info): + def resolve_all_containers(self, info): return big_container_list 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) 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): @@ -376,16 +411,16 @@ def test_big_list_of_containers_multiple_fields_custom_resolvers_query_benchmark z = Int() o = Int() - def resolve_x(self, args, context, info): + def resolve_x(self, info): return self.x - def resolve_y(self, args, context, info): + def resolve_y(self, info): return self.y - def resolve_z(self, args, context, info): + def resolve_z(self, info): return self.z - def resolve_o(self, args, context, info): + def resolve_o(self, info): return self.o 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): all_containers = List(Container) - def resolve_all_containers(self, args, context, info): + def resolve_all_containers(self, info): return big_container_list 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) 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'} diff --git a/graphene/types/tests/test_resolver.py b/graphene/types/tests/test_resolver.py index 25629979..2beb607e 100644 --- a/graphene/types/tests/test_resolver.py +++ b/graphene/types/tests/test_resolver.py @@ -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 = {} context = None @@ -16,22 +16,22 @@ class demo_obj(object): 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' 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' 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' 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' diff --git a/graphene/types/tests/test_scalar.py b/graphene/types/tests/test_scalar.py new file mode 100644 index 00000000..3c6383fa --- /dev/null +++ b/graphene/types/tests/test_scalar.py @@ -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" diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index af9bc14c..2ed5c099 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -1,9 +1,9 @@ import pytest -from ..schema import Schema +from ..field import Field from ..objecttype import ObjectType from ..scalars import String -from ..field import Field +from ..schema import Schema class MyOtherType(ObjectType): diff --git a/graphene/types/tests/test_structures.py b/graphene/types/tests/test_structures.py index 082bf097..6fb290fd 100644 --- a/graphene/types/tests/test_structures.py +++ b/graphene/types/tests/test_structures.py @@ -1,8 +1,9 @@ -import pytest from functools import partial -from ..structures import List, NonNull +import pytest + from ..scalars import String +from ..structures import List, NonNull from .utils import MyLazyType @@ -15,7 +16,7 @@ def test_list(): def test_list_with_unmounted_type(): with pytest.raises(Exception) as exc_info: List(String()) - + assert str(exc_info.value) == 'List could not have a mounted String() as inner type. Try with List(String).' @@ -80,14 +81,14 @@ def test_nonnull_inherited_works_list(): def test_nonnull_inherited_dont_work_nonnull(): with pytest.raises(Exception) as exc_info: NonNull(NonNull(String)) - + assert str(exc_info.value) == 'Can only create NonNull of a Nullable GraphQLType but got: String!.' def test_nonnull_with_unmounted_type(): with pytest.raises(Exception) as exc_info: NonNull(String()) - + assert str(exc_info.value) == 'NonNull could not have a mounted String() as inner type. Try with NonNull(String).' diff --git a/graphene/types/tests/test_typemap.py b/graphene/types/tests/test_typemap.py index 475d0905..082f25bd 100644 --- a/graphene/types/tests/test_typemap.py +++ b/graphene/types/tests/test_typemap.py @@ -49,8 +49,8 @@ def test_objecttype(): foo = String(bar=String(description='Argument description', default_value='x'), description='Field description') bar = String(name='gizmo') - def resolve_foo(self, args, info): - return args.get('bar') + def resolve_foo(self, bar): + return bar typemap = TypeMap([MyObjectType]) assert 'MyObjectType' in typemap @@ -65,7 +65,7 @@ def test_objecttype(): assert isinstance(foo_field, GraphQLField) assert foo_field.description == 'Field description' f = MyObjectType.resolve_foo - assert foo_field.resolver == getattr(f, '__func__', f) + # assert foo_field.resolver == getattr(f, '__func__', f) assert foo_field.args == { '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.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 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'] assert isinstance(foo_field, GraphQLInputObjectField) assert foo_field.description == 'Field description' @@ -196,5 +204,5 @@ def test_objecttype_with_possible_types(): typemap = TypeMap([MyObjectType]) graphql_type = typemap['MyObjectType'] assert graphql_type.is_type_of - assert graphql_type.is_type_of({}, None, None) is True - assert graphql_type.is_type_of(MyObjectType(), None, None) is False + assert graphql_type.is_type_of({}, None) is True + assert graphql_type.is_type_of(MyObjectType(), None) is False diff --git a/graphene/types/tests/test_union.py b/graphene/types/tests/test_union.py index d7ba2f31..ac2708ad 100644 --- a/graphene/types/tests/test_union.py +++ b/graphene/types/tests/test_union.py @@ -47,6 +47,7 @@ def test_generate_union_with_no_types(): def test_union_can_be_mounted(): class MyUnion(Union): + class Meta: types = (MyObjectType1, MyObjectType2) diff --git a/graphene/types/tests/test_uuid.py b/graphene/types/tests/test_uuid.py new file mode 100644 index 00000000..c1419750 --- /dev/null +++ b/graphene/types/tests/test_uuid.py @@ -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 + } diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index 02069e41..3b389cd6 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -11,10 +11,10 @@ from graphql.type.typemap import GraphQLTypeMap from ..utils.get_unbound_function import get_unbound_function from ..utils.str_converters import to_camel_case -from .definitions import (GrapheneEnumType, GrapheneInputObjectType, - GrapheneInterfaceType, GrapheneObjectType, - GrapheneScalarType, GrapheneUnionType, - GrapheneGraphQLType) +from .definitions import (GrapheneEnumType, GrapheneGraphQLType, + GrapheneInputObjectType, GrapheneInterfaceType, + GrapheneObjectType, GrapheneScalarType, + GrapheneUnionType) from .dynamic import Dynamic from .enum import Enum from .field import Field @@ -37,12 +37,12 @@ def is_graphene_type(_type): return True -def resolve_type(resolve_type_func, map, type_name, root, context, info): - _type = resolve_type_func(root, context, info) +def resolve_type(resolve_type_func, map, type_name, root, info): + _type = resolve_type_func(root, info) if not _type: 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): 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 -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) class TypeMap(GraphQLTypeMap): + def __init__(self, types, auto_camelcase=True, schema=None): self.auto_camelcase = auto_camelcase self.schema = schema @@ -194,6 +195,7 @@ class TypeMap(GraphQLTypeMap): graphene_type=type, name=type._meta.name, description=type._meta.description, + container_type=type._meta.container, fields=partial( self.construct_fields_for_type, map, type, is_input_type=True), ) @@ -237,7 +239,7 @@ class TypeMap(GraphQLTypeMap): _field = GraphQLInputObjectField( field_type, default_value=field.default_value, - out_name=field.name or name, + out_name=name, description=field.description) else: args = OrderedDict() @@ -254,8 +256,12 @@ class TypeMap(GraphQLTypeMap): field_type, args=args, resolver=field.get_resolver( - self.get_resolver_for_type(type, name, - field.default_value)), + self.get_resolver_for_type( + type, + name, + field.default_value + ) + ), deprecation_reason=field.deprecation_reason, description=field.description) field_name = field.name or self.get_name(name) diff --git a/graphene/types/union.py b/graphene/types/union.py index d4af88ed..c5925e88 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -1,38 +1,19 @@ -import six - -from ..utils.is_base_type import is_base_type -from ..utils.trim_docstring import trim_docstring -from .options import Options +from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType -class UnionMeta(type): - - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of - # Union - 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 +# For static type checking with Mypy +MYPY = False +if MYPY: + from .objecttype import ObjectType # NOQA + from typing import Iterable, Type # NOQA -class Union(six.with_metaclass(UnionMeta, UnmountedType)): +class UnionOptions(BaseOptions): + types = () # type: Iterable[Type[ObjectType]] + + +class Union(UnmountedType, BaseType): ''' 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 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 def get_type(cls): @@ -50,7 +41,7 @@ class Union(six.with_metaclass(UnionMeta, UnmountedType)): return cls @classmethod - def resolve_type(cls, instance, context, info): - from .objecttype import ObjectType + def resolve_type(cls, instance, info): + from .objecttype import ObjectType # NOQA if isinstance(instance, ObjectType): return type(instance) diff --git a/graphene/types/utils.py b/graphene/types/utils.py index 19cc06e7..c4434199 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -1,6 +1,7 @@ import inspect from collections import OrderedDict from functools import partial + from six import string_types from ..utils.module_loading import import_string @@ -8,35 +9,6 @@ from .mountedtype import MountedType 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): ''' Get type mounted @@ -49,7 +21,7 @@ def get_field_as(value, _as=None): 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) and return them ordered @@ -60,8 +32,6 @@ def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True): if not field: continue fields_with_names.append((attname, field)) - if delete: - del attrs[attname] if sort: 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): if isinstance(_type, string_types): 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 diff --git a/graphene/types/uuid.py b/graphene/types/uuid.py new file mode 100644 index 00000000..7686f682 --- /dev/null +++ b/graphene/types/uuid.py @@ -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) diff --git a/graphene/utils/annotate.py b/graphene/utils/annotate.py new file mode 100644 index 00000000..51a87a78 --- /dev/null +++ b/graphene/utils/annotate.py @@ -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 diff --git a/graphene/utils/deprecated.py b/graphene/utils/deprecated.py new file mode 100644 index 00000000..f70b2e7d --- /dev/null +++ b/graphene/utils/deprecated.py @@ -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))) diff --git a/graphene/utils/is_base_type.py b/graphene/utils/is_base_type.py deleted file mode 100644 index 8dfdfb70..00000000 --- a/graphene/utils/is_base_type.py +++ /dev/null @@ -1,3 +0,0 @@ - -def is_base_type(bases, _type): - return any(b for b in bases if isinstance(b, _type)) diff --git a/graphene/utils/resolve_only_args.py b/graphene/utils/resolve_only_args.py index 93a9ab7e..897e6223 100644 --- a/graphene/utils/resolve_only_args.py +++ b/graphene/utils/resolve_only_args.py @@ -1,8 +1,11 @@ from functools import wraps +from .deprecated import deprecated +@deprecated('This function is deprecated') def resolve_only_args(func): @wraps(func) - def inner(root, args, context, info): + def wrapped_func(root, info, **args): return func(root, **args) - return inner + + return wrapped_func diff --git a/graphene/utils/subclass_with_meta.py b/graphene/utils/subclass_with_meta.py new file mode 100644 index 00000000..9226e418 --- /dev/null +++ b/graphene/utils/subclass_with_meta.py @@ -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""" diff --git a/graphene/utils/tests/test_annotate.py b/graphene/utils/tests/test_annotate.py new file mode 100644 index 00000000..760a8bf5 --- /dev/null +++ b/graphene/utils/tests/test_annotate.py @@ -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".' diff --git a/graphene/utils/tests/test_deprecated.py b/graphene/utils/tests/test_deprecated.py new file mode 100644 index 00000000..d196744f --- /dev/null +++ b/graphene/utils/tests/test_deprecated.py @@ -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({}) diff --git a/graphene/utils/tests/test_module_loading.py b/graphene/utils/tests/test_module_loading.py index 769fde8b..dd67ffe1 100644 --- a/graphene/utils/tests/test_module_loading.py +++ b/graphene/utils/tests/test_module_loading.py @@ -1,16 +1,16 @@ from pytest import raises -from graphene import String -from graphene.types.objecttype import ObjectTypeMeta -from ..module_loading import lazy_import, import_string +from graphene import ObjectType, String + +from ..module_loading import import_string, lazy_import def test_import_string(): MyString = import_string('graphene.String') assert MyString == String - MyObjectTypeMeta = import_string('graphene.ObjectType', '__class__') - assert MyObjectTypeMeta == ObjectTypeMeta + MyObjectTypeMeta = import_string('graphene.ObjectType', '__doc__') + assert MyObjectTypeMeta == ObjectType.__doc__ def test_import_string_module(): @@ -52,6 +52,6 @@ def test_lazy_import(): MyString = f() assert MyString == String - f = lazy_import('graphene.ObjectType', '__class__') + f = lazy_import('graphene.ObjectType', '__doc__') MyObjectTypeMeta = f() - assert MyObjectTypeMeta == ObjectTypeMeta + assert MyObjectTypeMeta == ObjectType.__doc__ diff --git a/graphene/utils/tests/test_resolve_only_args.py b/graphene/utils/tests/test_resolve_only_args.py index 8c3ec248..f5b77de1 100644 --- a/graphene/utils/tests/test_resolve_only_args.py +++ b/graphene/utils/tests/test_resolve_only_args.py @@ -1,12 +1,15 @@ from ..resolve_only_args import resolve_only_args +from .. import deprecated -def test_resolve_only_args(): - - def resolver(*args, **kwargs): - return kwargs +def test_resolve_only_args(mocker): + mocker.patch.object(deprecated, 'warn_deprecation') + def resolver(root, **args): + return root, args my_data = {'one': 1, 'two': 2} - wrapped = resolve_only_args(resolver) - assert wrapped(None, my_data, None, None) == my_data + wrapped_resolver = resolve_only_args(resolver) + assert deprecated.warn_deprecation.called + result = wrapped_resolver(1, 2, a=3) + assert result == (1, {'a': 3}) diff --git a/graphene/utils/tests/test_resolver_from_annotations.py b/graphene/utils/tests/test_resolver_from_annotations.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/utils/tests/test_trim_docstring.py b/graphene/utils/tests/test_trim_docstring.py index 3aab5f11..9695fad4 100644 --- a/graphene/utils/tests/test_trim_docstring.py +++ b/graphene/utils/tests/test_trim_docstring.py @@ -9,11 +9,10 @@ def test_trim_docstring(): Multiple paragraphs too """ - pass assert (trim_docstring(WellDocumentedObject.__doc__) == - "This object is very well-documented. It has multiple lines in its\n" - "description.\n\nMultiple paragraphs too") + "This object is very well-documented. It has multiple lines in its\n" + "description.\n\nMultiple paragraphs too") class UndocumentedObject(object): pass diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..bbb37b77 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,17 @@ +[mypy] +ignore_missing_imports = True + +[mypy-graphene.pyutils.*] +ignore_errors = True + +[mypy-graphene.types.scalars] +ignore_errors = True + +[mypy-graphene.types.generic] +ignore_errors = True + +[mypy-graphene.types.tests.*] +ignore_errors = True + +[mypy-graphene.relay.tests.*] +ignore_errors = True diff --git a/setup.cfg b/setup.cfg index d83176f0..2037bc1b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,10 @@ exclude = setup.py,docs/*,*/examples/*,graphene/pyutils/*,tests max-line-length = 120 [coverage:run] -omit = graphene/pyutils/*,*/tests/* +omit = graphene/pyutils/*,*/tests/*,graphene/types/scalars.py [isort] known_first_party=graphene + +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py index 3405e0c5..013e6b2e 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,25 @@ -import sys - from setuptools import find_packages, setup from setuptools.command.test import test as TestCommand +import sys +import ast +import re -if sys.version_info[0] < 3: - import __builtin__ as builtins -else: - import builtins +_version_re = re.compile(r'VERSION\s+=\s+(.*)') -# This is a bit (!) hackish: we are setting a global variable so that the main -# graphql __init__ can detect if it is being loaded by the setup routine, to -# avoid attempting to load components that aren't built yet: -# the numpy distutils extensions that are used by scikit-learn to recursively -# build the compiled extensions in sub-packages is based on the Python import -# machinery. -builtins.__SETUP__ = True +with open('graphene/__init__.py', 'rb') as f: + version = ast.literal_eval(_version_re.search( + f.read().decode('utf-8')).group(1)) -version = __import__('graphene').get_version() +path_copy = sys.path[:] + +sys.path.append('graphene') +try: + from pyutils.version import get_version + version = get_version(version) +except Exception: + version = ".".join([str(v) for v in version]) + +sys.path[:] = path_copy class PyTest(TestCommand): @@ -41,6 +44,7 @@ tests_require = [ 'pytest>=2.7.2', 'pytest-benchmark', 'pytest-cov', + 'pytest-mock', 'snapshottest', 'coveralls', 'six', @@ -78,13 +82,13 @@ setup( keywords='api graphql protocol rest relay graphene', - packages=find_packages(exclude=['tests']), + packages=find_packages(exclude=['tests', 'tests.*']), install_requires=[ 'six>=1.10.0', - 'graphql-core>=1.1', + 'graphql-core>=2.0.dev', 'graphql-relay>=0.4.5', - 'promise>=2.0', + 'promise>=2.1.dev', ], tests_require=tests_require, extras_require={