diff --git a/README.md b/README.md index 3aa1dcc1..1376c733 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) +# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![PyPI version](https://badge.fury.io/py/graphene.svg)](https://badge.fury.io/py/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) Graphene is a Python library for building GraphQL schemas/types fast and easily. * **Easy to use:** It maps the models/fields to internal GraphQL objects without effort. * **Relay:** Graphene has builtin support for Relay -* **Django:** Automatic [Django models](#djangorelay-schema) conversion. *See an [example Django](http://github.com/graphql-python/swapi-graphene) implementation* +* **Django:** Automatic *Django model* mapping to Graphene Types. *See an [example Django](http://github.com/graphql-python/swapi-graphene) implementation* ## Installation @@ -16,26 +16,21 @@ pip install graphene ``` -## Usage +## Examples -Example code of a GraphQL schema using Graphene: - -### Schema definition +Here is one example for get you started: ```python -class Character(graphene.Interface): - id = graphene.IDField() - name = graphene.StringField() - friends = graphene.ListField('self') - - def resolve_friends(self, args, *_): - return [Human(f) for f in self.instance.friends] - -class Human(Character): - homePlanet = graphene.StringField() - class Query(graphene.ObjectType): - human = graphene.Field(Human) + hello = graphene.StringField(description='A typical hello world') + ping = graphene.StringField(description='Ping someone', + to=graphene.Argument(graphene.String)) + + def resolve_hello(self, args, info): + return 'World' + + def resolve_ping(self, args, info): + return 'Pinging {}'.format(args.get('to')) schema = graphene.Schema(query=Query) ``` @@ -44,48 +39,19 @@ Then Querying `graphene.Schema` is as simple as: ```python query = ''' - query HeroNameQuery { - hero { - name - } + query SayHello { + hello + ping(to:'peter') } ''' result = schema.execute(query) ``` -### Relay Schema +If you want to learn even more, you can also check the following examples: -Graphene also supports Relay, check the [Starwars Relay example](tests/starwars_relay)! +* Relay Schema: [Starwars Relay example](tests/starwars_relay) +* Django: [Starwars Django example](tests/starwars_django) -```python -class Ship(relay.Node): - name = graphene.StringField() - - @classmethod - def get_node(cls, id): - return Ship(your_ship_instance) - - -class Query(graphene.ObjectType): - ships = relay.ConnectionField(Ship) - node = relay.NodeField() - -``` - -### Django+Relay Schema - -If you want to use graphene with your Django Models check the [Starwars Django example](tests/starwars_django)! - -```python -class Ship(DjangoNode): - class Meta: - model = YourDjangoModelHere - # only_fields = ('id', 'name') # Only map this fields from the model - # exclude_fields ('field_to_exclude', ) # Exclude mapping this fields from the model - -class Query(graphene.ObjectType): - node = relay.NodeField() -``` ## Contributing diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index fbc770d5..7675fdaa 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -37,11 +37,20 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): cls.add_to_class(field.name, converted_field) -class DjangoObjectType(six.with_metaclass(DjangoObjectTypeMeta, BaseObjectType)): +class InstanceObjectType(BaseObjectType): + def __init__(self, instance=None): + self.instance = instance + super(InstanceObjectType, self).__init__() + + def __getattr__(self, attr): + return getattr(self.instance, attr) + + +class DjangoObjectType(six.with_metaclass(DjangoObjectTypeMeta, InstanceObjectType)): pass -class DjangoInterface(six.with_metaclass(DjangoObjectTypeMeta, BaseObjectType)): +class DjangoInterface(six.with_metaclass(DjangoObjectTypeMeta, InstanceObjectType)): pass diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 2b64cf7f..684a61cf 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -28,7 +28,7 @@ class Field(object): creation_counter = 0 required = False - def __init__(self, field_type, name=None, resolve=None, required=False, args=None, description='', **extra_args): + def __init__(self, field_type, name=None, resolve=None, required=False, args=None, description='', default=None, **extra_args): self.field_type = field_type self.resolve_fn = resolve self.required = self.required or required @@ -38,9 +38,13 @@ class Field(object): self.name = name self.description = description or self.__doc__ self.object_type = None + self.default = default self.creation_counter = Field.creation_counter Field.creation_counter += 1 + def get_default(self): + return self.default + def contribute_to_class(self, cls, name, add=True): if not self.name: self.name = to_camel_case(name) @@ -57,7 +61,7 @@ class Field(object): if resolve_fn: return resolve_fn(instance, args, info) else: - return getattr(instance, self.field_name, None) + return getattr(instance, self.field_name, self.get_default()) def get_resolve_fn(self, schema): object_type = self.get_object_type(schema) diff --git a/graphene/core/types.py b/graphene/core/types.py index 6be7921b..93b0d0fa 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -129,29 +129,48 @@ class ObjectTypeMeta(type): class BaseObjectType(object): - def __new__(cls, instance=None, **kwargs): + def __new__(cls, *args, **kwargs): if cls._meta.is_interface: raise Exception("An interface cannot be initialized") - if instance is None: - if not kwargs: - return None - elif type(instance) is cls: - return instance - + if not args and not kwargs: + return None return super(BaseObjectType, cls).__new__(cls) - def __init__(self, instance=None, **kwargs): - signals.pre_init.send(self.__class__, instance=instance) - assert instance or kwargs - if not instance: - init_kwargs = dict({k: None for k in self._meta.fields_map.keys()}, **kwargs) - instance = self._meta.object(**init_kwargs) - self.instance = instance - signals.post_init.send(self.__class__, instance=self) + def __init__(self, *args, **kwargs): + signals.pre_init.send(self.__class__, args=args, kwargs=kwargs) + args_len = len(args) + fields = self._meta.fields + if args_len > len(fields): + # Daft, but matches old exception sans the err msg. + raise IndexError("Number of args exceeds number of fields") + fields_iter = iter(fields) - def __getattr__(self, name): - if self.instance: - return getattr(self.instance, name) + if not kwargs: + for val, field in zip(args, fields_iter): + setattr(self, field.field_name, val) + else: + for val, field in zip(args, fields_iter): + setattr(self, field.field_name, val) + kwargs.pop(field.field_name, None) + + for field in fields_iter: + try: + val = kwargs.pop(field.field_name) + setattr(self, field.field_name, val) + except KeyError: + pass + + if kwargs: + for prop in list(kwargs): + try: + if isinstance(getattr(self.__class__, prop), property): + setattr(self, prop, kwargs.pop(prop)) + except AttributeError: + pass + if kwargs: + raise TypeError("'%s' is an invalid keyword argument for this function" % list(kwargs)[0]) + + signals.post_init.send(self.__class__, instance=self) @classmethod def fields_as_arguments(cls, schema): diff --git a/tests/relay/test_relay.py b/tests/relay/test_relay.py index 5ad456ca..94c2cad3 100644 --- a/tests/relay/test_relay.py +++ b/tests/relay/test_relay.py @@ -32,21 +32,3 @@ def test_node_should_have_same_connection_always(): def test_node_should_have_id_field(): assert 'id' in OtherNode._meta.fields_map - - -# def test_field_no_contributed_raises_error(): -# with raises(Exception) as excinfo: -# class Ship(graphene.ObjectType): -# name = graphene.StringField() -# class Meta: -# schema = schema - -# class Faction(relay.Node): -# name = graphene.StringField() -# ships = relay.ConnectionField(Ship) -# @classmethod -# def get_node(cls): -# pass -# class Meta: -# schema = schema -# assert 'same type_name' in str(excinfo.value) diff --git a/tests/relay/test_relayfields.py b/tests/relay/test_relayfields.py index 17be4152..9ae32d5a 100644 --- a/tests/relay/test_relayfields.py +++ b/tests/relay/test_relayfields.py @@ -1,4 +1,3 @@ -from pytest import raises from graphql.core.type import ( GraphQLNonNull, GraphQLID @@ -10,11 +9,6 @@ from graphene import relay schema = graphene.Schema() -class MyType(object): - name = 'my' - arg = None - - class MyConnection(relay.Connection): my_custom_field = graphene.StringField(resolve=lambda instance, *_: 'Custom') @@ -24,7 +18,7 @@ class MyNode(relay.Node): @classmethod def get_node(cls, id): - return MyNode(MyType()) + return MyNode(name='mo') class Query(graphene.ObjectType): @@ -32,10 +26,9 @@ class Query(graphene.ObjectType): all_my_nodes = relay.ConnectionField(MyNode, connection_type=MyConnection, customArg=graphene.Argument(graphene.String)) def resolve_all_my_nodes(self, args, info): - t = MyType() custom_arg = args.get('customArg') assert custom_arg == "1" - return [MyNode(t)] + return [MyNode(name='my')] schema.query = Query @@ -61,7 +54,7 @@ def test_nodefield_query(): ''' expected = { 'myNode': { - 'name': 'my' + 'name': 'mo' }, 'allMyNodes': { 'edges': [{ diff --git a/tests/starwars/data.py b/tests/starwars/data.py index 51f29a59..856c9e50 100644 --- a/tests/starwars/data.py +++ b/tests/starwars/data.py @@ -1,77 +1,78 @@ -from collections import namedtuple +humanData = {} +droidData = {} -Human = namedtuple('Human', 'id name friends appearsIn homePlanet') -luke = Human( - id='1000', - name='Luke Skywalker', - friends=['1002', '1003', '2000', '2001'], - appearsIn=[4, 5, 6], - homePlanet='Tatooine', -) +def setup(): + from .schema import Human, Droid + global humanData, droidData + luke = Human( + id='1000', + name='Luke Skywalker', + friends=['1002', '1003', '2000', '2001'], + appears_in=[4, 5, 6], + home_planet='Tatooine', + ) -vader = Human( - id='1001', - name='Darth Vader', - friends=['1004'], - appearsIn=[4, 5, 6], - homePlanet='Tatooine', -) + vader = Human( + id='1001', + name='Darth Vader', + friends=['1004'], + appears_in=[4, 5, 6], + home_planet='Tatooine', + ) -han = Human( - id='1002', - name='Han Solo', - friends=['1000', '1003', '2001'], - appearsIn=[4, 5, 6], - homePlanet=None, -) + han = Human( + id='1002', + name='Han Solo', + friends=['1000', '1003', '2001'], + appears_in=[4, 5, 6], + home_planet=None, + ) -leia = Human( - id='1003', - name='Leia Organa', - friends=['1000', '1002', '2000', '2001'], - appearsIn=[4, 5, 6], - homePlanet='Alderaan', -) + leia = Human( + id='1003', + name='Leia Organa', + friends=['1000', '1002', '2000', '2001'], + appears_in=[4, 5, 6], + home_planet='Alderaan', + ) -tarkin = Human( - id='1004', - name='Wilhuff Tarkin', - friends=['1001'], - appearsIn=[4], - homePlanet=None, -) + tarkin = Human( + id='1004', + name='Wilhuff Tarkin', + friends=['1001'], + appears_in=[4], + home_planet=None, + ) -humanData = { - '1000': luke, - '1001': vader, - '1002': han, - '1003': leia, - '1004': tarkin, -} + humanData = { + '1000': luke, + '1001': vader, + '1002': han, + '1003': leia, + '1004': tarkin, + } -Droid = namedtuple('Droid', 'id name friends appearsIn primaryFunction') + threepio = Droid( + id='2000', + name='C-3PO', + friends=['1000', '1002', '1003', '2001'], + appears_in=[4, 5, 6], + primary_function='Protocol', + ) -threepio = Droid( - id='2000', - name='C-3PO', - friends=['1000', '1002', '1003', '2001'], - appearsIn=[4, 5, 6], - primaryFunction='Protocol', -) + artoo = Droid( + id='2001', + name='R2-D2', + friends=['1000', '1002', '1003'], + appears_in=[4, 5, 6], + primary_function='Astromech', + ) -artoo = Droid( - id='2001', - name='R2-D2', - friends=['1000', '1002', '1003'], - appearsIn=[4, 5, 6], - primaryFunction='Astromech', -) - -droidData = { - '2000': threepio, - '2001': artoo, -} + droidData = { + '2000': threepio, + '2001': artoo, + } def getCharacter(id): @@ -84,8 +85,8 @@ def getFriends(character): def getHero(episode): if episode == 5: - return luke - return artoo + return humanData['1000'] + return droidData['2001'] def getHuman(id): diff --git a/tests/starwars/schema.py b/tests/starwars/schema.py index e6981c0a..bca27b76 100644 --- a/tests/starwars/schema.py +++ b/tests/starwars/schema.py @@ -2,7 +2,7 @@ from graphql.core.type import GraphQLEnumValue import graphene from graphene import resolve_only_args -from .data import getHero, getHuman, getCharacter, getDroid, Human as _Human, Droid as _Droid +from .data import getHero, getHuman, getCharacter, getDroid Episode = graphene.Enum('Episode', dict( NEWHOPE=GraphQLEnumValue(4), @@ -11,29 +11,23 @@ Episode = graphene.Enum('Episode', dict( )) -def wrap_character(character): - if isinstance(character, _Human): - return Human(character) - elif isinstance(character, _Droid): - return Droid(character) - - class Character(graphene.Interface): id = graphene.IDField() name = graphene.StringField() friends = graphene.ListField('self') - appearsIn = graphene.ListField(Episode) + appears_in = graphene.ListField(Episode) def resolve_friends(self, args, *_): - return [wrap_character(getCharacter(f)) for f in self.instance.friends] + # The character friends is a list of strings + return [getCharacter(f) for f in self.friends] class Human(Character): - homePlanet = graphene.StringField() + home_planet = graphene.StringField() class Droid(Character): - primaryFunction = graphene.StringField() + primary_function = graphene.StringField() class Query(graphene.ObjectType): @@ -52,15 +46,15 @@ class Query(graphene.ObjectType): @resolve_only_args def resolve_hero(self, episode=None): - return wrap_character(getHero(episode)) + return getHero(episode) @resolve_only_args def resolve_human(self, id): - return wrap_character(getHuman(id)) + return getHuman(id) @resolve_only_args def resolve_droid(self, id): - return wrap_character(getDroid(id)) + return getDroid(id) Schema = graphene.Schema(query=Query) diff --git a/tests/starwars/test_query.py b/tests/starwars/test_query.py index 214030ef..c0d94357 100644 --- a/tests/starwars/test_query.py +++ b/tests/starwars/test_query.py @@ -1,6 +1,8 @@ from .schema import Schema, Query from graphql.core import graphql +from .data import setup +setup() def test_hero_name_query(): query = ''' diff --git a/tests/starwars_django/schema.py b/tests/starwars_django/schema.py index 1949da6a..c6839a14 100644 --- a/tests/starwars_django/schema.py +++ b/tests/starwars_django/schema.py @@ -27,7 +27,7 @@ class Ship(DjangoNode): @schema.register -class CharacterModel(DjangoObjectType): +class Character(DjangoObjectType): class Meta: model = CharacterModel diff --git a/tests/starwars_relay/data.py b/tests/starwars_relay/data.py index a706dd53..2e49854f 100644 --- a/tests/starwars_relay/data.py +++ b/tests/starwars_relay/data.py @@ -1,78 +1,80 @@ -from collections import namedtuple +data = {} -Ship = namedtuple('Ship', ['id', 'name']) -Faction = namedtuple('Faction', ['id', 'name', 'ships']) -xwing = Ship( - id='1', - name='X-Wing', -) +def setup(): + global data + + from .schema import Ship, Faction + xwing = Ship( + id='1', + name='X-Wing', + ) -ywing = Ship( - id='2', - name='Y-Wing', -) + ywing = Ship( + id='2', + name='Y-Wing', + ) -awing = Ship( - id='3', - name='A-Wing', -) + awing = Ship( + id='3', + name='A-Wing', + ) -# Yeah, technically it's Corellian. But it flew in the service of the rebels, -# so for the purposes of this demo it's a rebel ship. -falcon = Ship( - id='4', - name='Millenium Falcon', -) + # Yeah, technically it's Corellian. But it flew in the service of the rebels, + # so for the purposes of this demo it's a rebel ship. + falcon = Ship( + id='4', + name='Millenium Falcon', + ) -homeOne = Ship( - id='5', - name='Home One', -) + homeOne = Ship( + id='5', + name='Home One', + ) -tieFighter = Ship( - id='6', - name='TIE Fighter', -) + tieFighter = Ship( + id='6', + name='TIE Fighter', + ) -tieInterceptor = Ship( - id='7', - name='TIE Interceptor', -) + tieInterceptor = Ship( + id='7', + name='TIE Interceptor', + ) -executor = Ship( - id='8', - name='Executor', -) + executor = Ship( + id='8', + name='Executor', + ) -rebels = Faction( - id='1', - name='Alliance to Restore the Republic', - ships=['1', '2', '3', '4', '5'] -) + rebels = Faction( + id='1', + name='Alliance to Restore the Republic', + ships=['1', '2', '3', '4', '5'] + ) -empire = Faction( - id='2', - name='Galactic Empire', - ships=['6', '7', '8'] -) + empire = Faction( + id='2', + name='Galactic Empire', + ships=['6', '7', '8'] + ) -data = { - 'Faction': { - '1': rebels, - '2': empire - }, - 'Ship': { - '1': xwing, - '2': ywing, - '3': awing, - '4': falcon, - '5': homeOne, - '6': tieFighter, - '7': tieInterceptor, - '8': executor + data = { + 'Faction': { + '1': rebels, + '2': empire + }, + 'Ship': { + '1': xwing, + '2': ywing, + '3': awing, + '4': falcon, + '5': homeOne, + '6': tieFighter, + '7': tieInterceptor, + '8': executor + } } -} def createShip(shipName, factionId): @@ -95,8 +97,8 @@ def getFaction(_id): def getRebels(): - return rebels + return getFaction('1') def getEmpire(): - return empire + return getFaction('2') diff --git a/tests/starwars_relay/schema.py b/tests/starwars_relay/schema.py index 83aa673b..5e01f48b 100644 --- a/tests/starwars_relay/schema.py +++ b/tests/starwars_relay/schema.py @@ -12,29 +12,28 @@ schema = graphene.Schema(name='Starwars Relay Schema') class Ship(relay.Node): - '''A ship in the Star Wars saga''' name = graphene.StringField(description='The name of the ship.') @classmethod def get_node(cls, id): - return Ship(getShip(id)) + return getShip(id) class Faction(relay.Node): - '''A faction in the Star Wars saga''' name = graphene.StringField(description='The name of the faction.') ships = relay.ConnectionField( Ship, description='The ships used by the faction.') @resolve_only_args - def resolve_ships(self, **kwargs): - return [Ship(getShip(ship)) for ship in self.instance.ships] + def resolve_ships(self, **args): + # Transform the instance ship_ids into real instances + return [getShip(ship_id) for ship_id in self.ships] @classmethod def get_node(cls, id): - return Faction(getFaction(id)) + return getFaction(id) class Query(graphene.ObjectType): @@ -44,11 +43,11 @@ class Query(graphene.ObjectType): @resolve_only_args def resolve_rebels(self): - return Faction(getRebels()) + return getRebels() @resolve_only_args def resolve_empire(self): - return Faction(getEmpire()) + return getEmpire() schema.query = Query diff --git a/tests/starwars_relay/schema_other.py b/tests/starwars_relay/schema_other.py deleted file mode 100644 index 5cf4f0a9..00000000 --- a/tests/starwars_relay/schema_other.py +++ /dev/null @@ -1,61 +0,0 @@ -import graphene -from graphene import resolve_only_args, relay - -from .data import ( - getHero, getHuman, getCharacter, getDroid, - Human as _Human, Droid as _Droid) - -Episode = graphene.Enum('Episode', dict( - NEWHOPE=4, - EMPIRE=5, - JEDI=6 -)) - - -def wrap_character(character): - if isinstance(character, _Human): - return Human(character) - elif isinstance(character, _Droid): - return Droid(character) - - -class Character(graphene.Interface): - name = graphene.StringField() - friends = relay.Connection('Character') - appearsIn = graphene.ListField(Episode) - - def resolve_friends(self, args, *_): - return [wrap_character(getCharacter(f)) for f in self.instance.friends] - - -class Human(relay.Node, Character): - homePlanet = graphene.StringField() - - -class Droid(relay.Node, Character): - primaryFunction = graphene.StringField() - - -class Query(graphene.ObjectType): - hero = graphene.Field(Character, - episode=graphene.Argument(Episode)) - human = graphene.Field(Human, - id=graphene.Argument(graphene.String)) - droid = graphene.Field(Droid, - id=graphene.Argument(graphene.String)) - node = relay.NodeField() - - @resolve_only_args - def resolve_hero(self, episode): - return wrap_character(getHero(episode)) - - @resolve_only_args - def resolve_human(self, id): - return wrap_character(getHuman(id)) - - @resolve_only_args - def resolve_droid(self, id): - return wrap_character(getDroid(id)) - - -Schema = graphene.Schema(query=Query) diff --git a/tests/starwars_relay/test_connections.py b/tests/starwars_relay/test_connections.py index 303f5230..ad635e84 100644 --- a/tests/starwars_relay/test_connections.py +++ b/tests/starwars_relay/test_connections.py @@ -1,7 +1,7 @@ -from pytest import raises -from graphql.core import graphql - from .schema import schema +from .data import setup + +setup() def test_correct_fetch_first_ship_rebels(): diff --git a/tests/starwars_relay/test_objectidentification.py b/tests/starwars_relay/test_objectidentification.py index eabaa785..b6331455 100644 --- a/tests/starwars_relay/test_objectidentification.py +++ b/tests/starwars_relay/test_objectidentification.py @@ -1,7 +1,7 @@ -from pytest import raises -from graphql.core import graphql - from .schema import schema +from .data import setup + +setup() def test_correctly_fetches_id_name_rebels():