Isolated Graphene Django in a new package

This commit is contained in:
Syrus Akbary 2016-09-17 16:29:00 -07:00
commit 0434899b4e
82 changed files with 3891 additions and 0 deletions

81
.gitignore vendored Normal file
View File

@ -0,0 +1,81 @@
# Created by https://www.gitignore.io
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
/tests/django.sqlite
/graphene/index.json
/graphene/meta.json
/meta.json
/index.json
/docs/playground/graphene-js/pypyjs-release-nojit/
/docs/static/playground/lib
/docs/static/playground
# PyCharm
.idea
# Databases
*.sqlite3
.vscode

49
.travis.yml Normal file
View File

@ -0,0 +1,49 @@
language: python
sudo: false
python:
- 2.7
- 3.4
- 3.5
- pypy
before_install:
- |
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
export PYENV_ROOT="$HOME/.pyenv"
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
cd "$PYENV_ROOT" && git pull
else
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
fi
export PYPY_VERSION="4.0.1"
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION"
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
fi
install:
- |
if [ "$TEST_TYPE" = build ]; then
pip install pytest pytest-cov pytest-benchmark coveralls six pytest-django mock django-filter
pip install -e .
python setup.py develop
elif [ "$TEST_TYPE" = lint ]; then
pip install flake8
fi
script:
- |
if [ "$TEST_TYPE" = lint ]; then
echo "Checking Python code lint."
flake8 graphene_django
exit
elif [ "$TEST_TYPE" = build ]; then
py.test --cov=graphene_django graphene_django examples
fi
after_success:
- |
if [ "$TEST_TYPE" = build ]; then
coveralls
fi
matrix:
fast_finish: true
include:
- python: '2.7'
env: TEST_TYPE=lint

72
README.md Normal file
View File

@ -0,0 +1,72 @@
You are in the `next` unreleased version of Graphene-Django (`1.0.dev`).
Please read [UPGRADE-v1.0.md](https://github.com/graphql-python/graphene/blob/master/UPGRADE-v1.0.md) to learn how to upgrade.
---
# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene-Django](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene-django.svg?branch=master)](https://travis-ci.org/graphql-python/graphene-django) [![PyPI version](https://badge.fury.io/py/graphene-django.svg)](https://badge.fury.io/py/graphene-django) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene-django/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene-django?branch=master)
[Graphene](http://graphene-python.org) is a Python library for building GraphQL schemas/types fast and easily.
- **Easy to use:** Graphene helps you use GraphQL in Python without effort.
- **Relay:** Graphene has builtin support for Relay
- **Django:** Automatic *Django model* mapping to Graphene Types. Check a fully working [Django](http://github.com/graphql-python/swapi-graphene) implementation
Graphene also supports *SQLAlchemy*!
*What is supported in this Python version?* **Everything**: Interfaces, ObjectTypes, Scalars, Unions and Relay (Nodes, Connections), in addition to queries, mutations and subscriptions.
**NEW**!: [Try graphene online](http://graphene-python.org/playground/)
## Installation
For instaling graphene, just run this command in your shell
```bash
pip install "graphene-django>=1.0.dev"
```
## Examples
Here is one example for get you started:
```python
from django.db import models
from graphene_django import DjangoObjectType
class UserModel(models.Model):
name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
class User(DjangoObjectType):
class Meta:
# This type will transform all the UserModel fields
# into Graphene fields automatically
model = UserModel
# An extra field in the User Type
full_name = graphene.String()
def resolve_full_name(self, args, context, info):
return "{} {}".format(self.name, self.last_name)
```
If you want to learn even more, you can also check the following [examples](examples/):
* **Schema with Filtering**: [Cookbook example](examples/cookbook)
* **Relay Schema**: [Starwars Relay example](examples/starwars)
## Contributing
After cloning this repo, ensure dependencies are installed by running:
```sh
python setup.py install
```
After developing, the full test suite can be evaluated by running:
```sh
python setup.py test # Use --pytest-args="-v -s" for verbose mode
```

7
bin/autolinter Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
# Install the required scripts with
# pip install autoflake autopep8 isort
autoflake ./examples/ ./graphene_django/ -r --remove-unused-variables --remove-all-unused-imports --in-place
autopep8 ./examples/ ./graphene_django/ -r --in-place --experimental --aggressive --max-line-length 120
isort -rc ./examples/ ./graphene_django/

3
bin/convert_documentation Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
pandoc README.md --from markdown --to rst -s -o README.rst

18
django_test_settings.py Normal file
View File

@ -0,0 +1,18 @@
import sys, os
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT_PATH + '/examples/')
SECRET_KEY = 1
INSTALLED_APPS = [
'graphene_django',
'graphene_django.tests',
'starwars',
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'django_test.sqlite',
}
}

View File

@ -0,0 +1,64 @@
Cookbook Example Django Project
===============================
This example project demos integration between Graphene and Django.
The project contains two apps, one named `ingredients` and another
named `recepies`.
Getting started
---------------
First you'll need to get the source of the project. Do this by cloning the
whole Graphene repository:
```bash
# Get the example project code
git clone https://github.com/graphql-python/graphene.git
cd graphene/examples/cookbook
```
It is good idea (but not required) to create a virtual environment
for this project. We'll do this using
[virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/)
to keep things simple,
but you may also find something like
[virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/)
to be useful:
```bash
# Create a virtualenv in which we can install the dependencies
virtualenv env
source env/bin/activate
```
Now we can install our dependencies:
```bash
pip install -r requirements.txt
```
Now setup our database:
```bash
# Setup the database
./manage.py migrate
# Load some example data
./manage.py loaddata ingredients
# Create an admin user (useful for logging into the admin UI
# at http://127.0.0.1:8000/admin)
./manage.py createsuperuser
```
Now you should be ready to start the server:
```bash
./manage.py runserver
```
Now head on over to
[http://127.0.0.1:8000/graphiql](http://127.0.0.1:8000/graphiql)
and run some queries!
(See the [Django quickstart guide](http://graphene-python.org/docs/quickstart-django/)
for some example queries)

View File

View File

@ -0,0 +1,6 @@
from django.contrib import admin
from cookbook.ingredients.models import Category, Ingredient
admin.site.register(Ingredient)
admin.site.register(Category)

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class IngredientsConfig(AppConfig):
name = 'cookbook.ingredients'
label = 'ingredients'
verbose_name = 'Ingredients'

View File

@ -0,0 +1 @@
[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}]

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:15
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Ingredient',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('notes', models.TextField()),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')),
],
),
]

View File

@ -0,0 +1,17 @@
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField()
category = models.ForeignKey(Category, related_name='ingredients')
def __str__(self):
return self.name

View File

@ -0,0 +1,38 @@
from cookbook.ingredients.models import Category, Ingredient
from graphene import ObjectType, Field, AbstractType, Node
from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType
# Graphene will automatically map the Category model's fields onto the CategoryNode.
# This is configured in the CategoryNode's Meta class (as you can see below)
class CategoryNode(DjangoObjectType):
class Meta:
model = Category
interfaces = (Node, )
filter_fields = ['name', 'ingredients']
filter_order_by = ['name']
class IngredientNode(DjangoObjectType):
class Meta:
model = Ingredient
# Allow for some more advanced filtering here
interfaces = (Node, )
filter_fields = {
'name': ['exact', 'icontains', 'istartswith'],
'notes': ['exact', 'icontains'],
'category': ['exact'],
'category__name': ['exact'],
}
filter_order_by = ['name', 'category__name']
class Query(AbstractType):
category = Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode)
ingredient = Field(IngredientNode)
all_ingredients = DjangoFilterConnectionField(IngredientNode)

View File

@ -0,0 +1,2 @@
# Create your tests here.

View File

@ -0,0 +1,2 @@
# Create your views here.

View File

@ -0,0 +1,6 @@
from django.contrib import admin
from cookbook.recipes.models import Recipe, RecipeIngredient
admin.site.register(Recipe)
admin.site.register(RecipeIngredient)

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class RecipesConfig(AppConfig):
name = 'cookbook.recipes'
label = 'recipes'
verbose_name = 'Recipes'

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:20
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('ingredients', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Recipe',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)),
('instructions', models.TextField()),
],
),
migrations.CreateModel(
name='RecipeIngredient',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.FloatField()),
('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)),
('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')),
('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')),
],
),
]

View File

@ -0,0 +1,19 @@
from django.db import models
from cookbook.ingredients.models import Ingredient
class Recipe(models.Model):
title = models.CharField(max_length=100)
instructions = models.TextField()
class RecipeIngredient(models.Model):
recipes = models.ForeignKey(Recipe, related_name='amounts')
ingredient = models.ForeignKey(Ingredient, related_name='used_by')
amount = models.FloatField()
unit = models.CharField(max_length=20, choices=(
('kg', 'Kilograms'),
('l', 'Litres'),
('', 'Units'),
))

View File

@ -0,0 +1,2 @@
# Create your tests here.

View File

@ -0,0 +1,2 @@
# Create your views here.

View File

@ -0,0 +1,9 @@
import graphene
import cookbook.ingredients.schema
# print cookbook.ingredients.schema.Query._meta.graphql_type.get_fields()['allIngredients'].args
class Query(cookbook.ingredients.schema.Query, graphene.ObjectType):
pass
schema = graphene.Schema(query=Query)

View File

@ -0,0 +1,125 @@
"""
Django settings for cookbook project.
Generated by 'django-admin startproject' using Django 1.9.
For more information on this file, see
https://docs.djangoproject.com/en/1.9/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.9/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_graphiql',
'cookbook.ingredients.apps.IngredientsConfig',
'cookbook.recipes.apps.RecipesConfig',
]
MIDDLEWARE_CLASSES = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'cookbook.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'cookbook.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = '/static/'

View File

@ -0,0 +1,12 @@
from django.conf.urls import include, url
from django.contrib import admin
from django.views.decorators.csrf import csrf_exempt
from cookbook.schema import schema
from graphene_django.views import GraphQLView
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^graphql', csrf_exempt(GraphQLView.as_view(schema=schema))),
url(r'^graphiql', include('django_graphiql.urls')),
]

View File

@ -0,0 +1,16 @@
"""
WSGI config for cookbook project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings")
application = get_wsgi_application()

10
examples/cookbook/manage.py Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

View File

@ -0,0 +1,5 @@
graphene[django]
django_graphiql
graphql-core
django==1.9
django-filter==0.11.0

View File

114
examples/starwars/data.py Normal file
View File

@ -0,0 +1,114 @@
from .models import Character, Faction, Ship
def initialize():
human = Character(
name='Human'
)
human.save()
droid = Character(
name='Droid'
)
droid.save()
rebels = Faction(
id='1',
name='Alliance to Restore the Republic',
hero=human
)
rebels.save()
empire = Faction(
id='2',
name='Galactic Empire',
hero=droid
)
empire.save()
xwing = Ship(
id='1',
name='X-Wing',
faction=rebels,
)
xwing.save()
ywing = Ship(
id='2',
name='Y-Wing',
faction=rebels,
)
ywing.save()
awing = Ship(
id='3',
name='A-Wing',
faction=rebels,
)
awing.save()
# 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',
faction=rebels,
)
falcon.save()
homeOne = Ship(
id='5',
name='Home One',
faction=rebels,
)
homeOne.save()
tieFighter = Ship(
id='6',
name='TIE Fighter',
faction=empire,
)
tieFighter.save()
tieInterceptor = Ship(
id='7',
name='TIE Interceptor',
faction=empire,
)
tieInterceptor.save()
executor = Ship(
id='8',
name='Executor',
faction=empire,
)
executor.save()
def create_ship(ship_name, faction_id):
new_ship = Ship(
name=ship_name,
faction_id=faction_id
)
new_ship.save()
return new_ship
def get_ship(_id):
return Ship.objects.get(id=_id)
def get_ships():
return Ship.objects.all()
def get_faction(_id):
return Faction.objects.get(id=_id)
def get_rebels():
return get_faction(1)
def get_empire():
return get_faction(2)

View File

@ -0,0 +1,26 @@
from __future__ import absolute_import
from django.db import models
class Character(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class Faction(models.Model):
name = models.CharField(max_length=50)
hero = models.ForeignKey(Character)
def __str__(self):
return self.name
class Ship(models.Model):
name = models.CharField(max_length=50)
faction = models.ForeignKey(Faction, related_name='ships')
def __str__(self):
return self.name

View File

@ -0,0 +1,87 @@
import graphene
from graphene import relay, resolve_only_args, Schema
from graphene_django import DjangoObjectType
from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship,
get_ships)
from .models import (
Character as CharacterModel,
Faction as FactionModel,
Ship as ShipModel
)
class Ship(DjangoObjectType):
class Meta:
model = ShipModel
interfaces = (relay.Node, )
@classmethod
def get_node(cls, id, context, info):
node = get_ship(id)
print(node)
return node
class Character(DjangoObjectType):
class Meta:
model = CharacterModel
class Faction(DjangoObjectType):
class Meta:
model = FactionModel
interfaces = (relay.Node, )
@classmethod
def get_node(cls, id, context, info):
return get_faction(id)
class IntroduceShip(relay.ClientIDMutation):
class Input:
ship_name = graphene.String(required=True)
faction_id = graphene.String(required=True)
ship = graphene.Field(Ship)
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')
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction)
class Query(graphene.ObjectType):
rebels = graphene.Field(Faction)
empire = graphene.Field(Faction)
node = relay.Node.Field()
ships = relay.ConnectionField(Ship, description='All the ships.')
@resolve_only_args
def resolve_ships(self):
return get_ships()
@resolve_only_args
def resolve_rebels(self):
return get_rebels()
@resolve_only_args
def resolve_empire(self):
return get_empire()
class Mutation(graphene.ObjectType):
introduce_ship = IntroduceShip.Field()
# We register the Character Model because if not would be
# inaccessible for the schema
schema = Schema(query=Query, mutation=Mutation, types=[Ship, Character])

View File

View File

@ -0,0 +1,47 @@
import pytest
from ..data import initialize
from ..schema import schema
pytestmark = pytest.mark.django_db
def test_correct_fetch_first_ship_rebels():
initialize()
query = '''
query RebelsShipsQuery {
rebels {
name,
hero {
name
}
ships(first: 1) {
edges {
node {
name
}
}
}
}
}
'''
expected = {
'rebels': {
'name': 'Alliance to Restore the Republic',
'hero': {
'name': 'Human'
},
'ships': {
'edges': [
{
'node': {
'name': 'X-Wing'
}
}
]
}
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

@ -0,0 +1,79 @@
import pytest
from ..data import initialize
from ..schema import schema
pytestmark = pytest.mark.django_db
def test_mutations():
initialize()
query = '''
mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
ship {
id
name
}
faction {
name
ships {
edges {
node {
id
name
}
}
}
}
}
}
'''
expected = {
'introduceShip': {
'ship': {
'id': 'U2hpcDo5',
'name': 'Peter'
},
'faction': {
'name': 'Alliance to Restore the Republic',
'ships': {
'edges': [{
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}, {
'node': {
'id': 'U2hpcDoy',
'name': 'Y-Wing'
}
}, {
'node': {
'id': 'U2hpcDoz',
'name': 'A-Wing'
}
}, {
'node': {
'id': 'U2hpcDo0',
'name': 'Millenium Falcon'
}
}, {
'node': {
'id': 'U2hpcDo1',
'name': 'Home One'
}
}, {
'node': {
'id': 'U2hpcDo5',
'name': 'Peter'
}
}]
},
}
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

@ -0,0 +1,117 @@
import pytest
from ..data import initialize
from ..schema import schema
pytestmark = pytest.mark.django_db
def test_correctly_fetches_id_name_rebels():
initialize()
query = '''
query RebelsQuery {
rebels {
id
name
}
}
'''
expected = {
'rebels': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_correctly_refetches_rebels():
initialize()
query = '''
query RebelsRefetchQuery {
node(id: "RmFjdGlvbjox") {
id
... on Faction {
name
}
}
}
'''
expected = {
'node': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_correctly_fetches_id_name_empire():
initialize()
query = '''
query EmpireQuery {
empire {
id
name
}
}
'''
expected = {
'empire': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_correctly_refetches_empire():
initialize()
query = '''
query EmpireRefetchQuery {
node(id: "RmFjdGlvbjoy") {
id
... on Faction {
name
}
}
}
'''
expected = {
'node': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_correctly_refetches_xwing():
initialize()
query = '''
query XWingRefetchQuery {
node(id: "U2hpcDox") {
id
... on Ship {
name
}
}
}
'''
expected = {
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

@ -0,0 +1,9 @@
from .types import (
DjangoObjectType,
)
from .fields import (
DjangoConnectionField,
)
__all__ = ['DjangoObjectType',
'DjangoConnectionField']

24
graphene_django/compat.py Normal file
View File

@ -0,0 +1,24 @@
from django.db import models
class MissingType(object):
pass
try:
UUIDField = models.UUIDField
except AttributeError:
# Improved compatibility for Django 1.6
UUIDField = MissingType
try:
from django.db.models.related import RelatedObject
except:
# Improved compatibility for Django 1.6
RelatedObject = MissingType
try:
# Postgres fields are only available in Django 1.8+
from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField, RangeField
except ImportError:
ArrayField, HStoreField, JSONField, RangeField = (MissingType, ) * 4

View File

@ -0,0 +1,189 @@
from django.db import models
from django.utils.encoding import force_text
from graphene import Enum, List, ID, Boolean, Float, Int, String, Field, NonNull, Field, Dynamic
from graphene.types.json import JSONString
from graphene.types.datetime import DateTime
from graphene.utils.str_converters import to_const
from graphene.relay import is_node
from .compat import (ArrayField, HStoreField, JSONField, RangeField,
RelatedObject, UUIDField)
from .utils import get_related_model, import_single_dispatch
from .fields import get_connection_field
singledispatch = import_single_dispatch()
def convert_choice_name(name):
return to_const(force_text(name))
def get_choices(choices):
for value, help_text in choices:
if isinstance(help_text, (tuple, list)):
for choice in get_choices(help_text):
yield choice
else:
name = convert_choice_name(help_text)
description = help_text
yield name, value, description
def convert_django_field_with_choices(field, registry=None):
choices = getattr(field, 'choices', None)
if choices:
meta = field.model._meta
name = '{}{}'.format(meta.object_name, field.name.capitalize())
choices = list(get_choices(choices))
named_choices = [(c[0], c[1]) for c in choices]
named_choices_descriptions = {c[0]:c[2] for c in choices}
class EnumWithDescriptionsType(object):
@property
def description(self):
return named_choices_descriptions[self.name]
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
return enum(description=field.help_text)
return convert_django_field(field, registry)
@singledispatch
def convert_django_field(field, registry=None):
raise Exception(
"Don't know how to convert the Django field %s (%s)" %
(field, field.__class__))
@convert_django_field.register(models.CharField)
@convert_django_field.register(models.TextField)
@convert_django_field.register(models.EmailField)
@convert_django_field.register(models.SlugField)
@convert_django_field.register(models.URLField)
@convert_django_field.register(models.GenericIPAddressField)
@convert_django_field.register(models.FileField)
@convert_django_field.register(UUIDField)
def convert_field_to_string(field, registry=None):
return String(description=field.help_text)
@convert_django_field.register(models.AutoField)
def convert_field_to_id(field, registry=None):
return ID(description=field.help_text)
@convert_django_field.register(models.PositiveIntegerField)
@convert_django_field.register(models.PositiveSmallIntegerField)
@convert_django_field.register(models.SmallIntegerField)
@convert_django_field.register(models.BigIntegerField)
@convert_django_field.register(models.IntegerField)
def convert_field_to_int(field, registry=None):
return Int(description=field.help_text)
@convert_django_field.register(models.BooleanField)
def convert_field_to_boolean(field, registry=None):
return NonNull(Boolean, description=field.help_text)
@convert_django_field.register(models.NullBooleanField)
def convert_field_to_nullboolean(field, registry=None):
return Boolean(description=field.help_text)
@convert_django_field.register(models.DecimalField)
@convert_django_field.register(models.FloatField)
def convert_field_to_float(field, registry=None):
return Float(description=field.help_text)
@convert_django_field.register(models.DateField)
def convert_date_to_string(field, registry=None):
return DateTime(description=field.help_text)
@convert_django_field.register(models.OneToOneRel)
def convert_onetoone_field_to_djangomodel(field, registry=None):
model = get_related_model(field)
def dynamic_type():
_type = registry.get_type_for_model(model)
if not _type:
return
return Field(_type)
return Dynamic(dynamic_type)
@convert_django_field.register(models.ManyToManyField)
@convert_django_field.register(models.ManyToManyRel)
@convert_django_field.register(models.ManyToOneRel)
def convert_field_to_list_or_connection(field, registry=None):
model = get_related_model(field)
def dynamic_type():
_type = registry.get_type_for_model(model)
if not _type:
return
if is_node(_type):
return get_connection_field(_type)
return Field(List(_type))
return Dynamic(dynamic_type)
# For Django 1.6
@convert_django_field.register(RelatedObject)
def convert_relatedfield_to_djangomodel(field, registry=None):
model = field.model
def dynamic_type():
_type = registry.get_type_for_model(model)
if not _type:
return
if is_node(_type):
return get_connection_field(_type)
return Field(List(_type))
return Dynamic(dynamic_type)
@convert_django_field.register(models.OneToOneField)
@convert_django_field.register(models.ForeignKey)
def convert_field_to_djangomodel(field, registry=None):
model = get_related_model(field)
def dynamic_type():
_type = registry.get_type_for_model(model)
if not _type:
return
return Field(_type, description=field.help_text)
return Dynamic(dynamic_type)
@convert_django_field.register(ArrayField)
def convert_postgres_array_to_list(field, registry=None):
base_type = convert_django_field(field.base_field)
if not isinstance(base_type, (List, NonNull)):
base_type = type(base_type)
return List(base_type, description=field.help_text)
@convert_django_field.register(HStoreField)
@convert_django_field.register(JSONField)
def convert_posgres_field_to_string(field, registry=None):
return JSONString(description=field.help_text)
@convert_django_field.register(RangeField)
def convert_posgres_range_to_string(field, registry=None):
inner_type = convert_django_field(field.base_field)
if not isinstance(inner_type, (List, NonNull)):
inner_type = type(inner_type)
return List(inner_type, description=field.help_text)

View File

@ -0,0 +1,4 @@
from .middleware import DjangoDebugMiddleware
from .types import DjangoDebug
__all__ = ['DjangoDebugMiddleware', 'DjangoDebug']

View File

@ -0,0 +1,56 @@
from promise import Promise
from django.db import connections
from .sql.tracking import unwrap_cursor, wrap_cursor
from .types import DjangoDebug
class DjangoDebugContext(object):
def __init__(self):
self.debug_promise = None
self.promises = []
self.enable_instrumentation()
self.object = DjangoDebug(sql=[])
def get_debug_promise(self):
if not self.debug_promise:
self.debug_promise = Promise.all(self.promises)
return self.debug_promise.then(self.on_resolve_all_promises)
def on_resolve_all_promises(self, values):
self.disable_instrumentation()
return self.object
def add_promise(self, promise):
if self.debug_promise and not self.debug_promise.is_fulfilled:
self.promises.append(promise)
def enable_instrumentation(self):
# This is thread-safe because database connections are thread-local.
for connection in connections.all():
wrap_cursor(connection, self)
def disable_instrumentation(self):
for connection in connections.all():
unwrap_cursor(connection)
class DjangoDebugMiddleware(object):
def resolve(self, next, root, args, context, info):
django_debug = getattr(context, 'django_debug', None)
if not django_debug:
if context is None:
raise Exception('DjangoDebug cannot be executed in None contexts')
try:
context.django_debug = DjangoDebugContext()
except Exception:
raise Exception('DjangoDebug need the context to be writable, context received: {}.'.format(
context.__class__.__name__
))
if info.schema.get_type('DjangoDebug') == info.return_type:
return context.django_debug.get_debug_promise()
promise = next(root, args, context, info)
context.django_debug.add_promise(promise)
return promise

View File

View File

@ -0,0 +1,169 @@
# Code obtained from django-debug-toolbar sql panel tracking
from __future__ import absolute_import, unicode_literals
import json
from threading import local
from time import time
from django.utils import six
from django.utils.encoding import force_text
from .types import DjangoDebugSQL
class SQLQueryTriggered(Exception):
"""Thrown when template panel triggers a query"""
class ThreadLocalState(local):
def __init__(self):
self.enabled = True
@property
def Wrapper(self):
if self.enabled:
return NormalCursorWrapper
return ExceptionCursorWrapper
def recording(self, v):
self.enabled = v
state = ThreadLocalState()
recording = state.recording # export function
def wrap_cursor(connection, panel):
if not hasattr(connection, '_graphene_cursor'):
connection._graphene_cursor = connection.cursor
def cursor():
return state.Wrapper(connection._graphene_cursor(), connection, panel)
connection.cursor = cursor
return cursor
def unwrap_cursor(connection):
if hasattr(connection, '_graphene_cursor'):
previous_cursor = connection._graphene_cursor
connection.cursor = previous_cursor
del connection._graphene_cursor
class ExceptionCursorWrapper(object):
"""
Wraps a cursor and raises an exception on any operation.
Used in Templates panel.
"""
def __init__(self, cursor, db, logger):
pass
def __getattr__(self, attr):
raise SQLQueryTriggered()
class NormalCursorWrapper(object):
"""
Wraps a cursor and logs queries.
"""
def __init__(self, cursor, db, logger):
self.cursor = cursor
# Instance of a BaseDatabaseWrapper subclass
self.db = db
# logger must implement a ``record`` method
self.logger = logger
def _quote_expr(self, element):
if isinstance(element, six.string_types):
return "'%s'" % force_text(element).replace("'", "''")
else:
return repr(element)
def _quote_params(self, params):
if not params:
return params
if isinstance(params, dict):
return dict((key, self._quote_expr(value))
for key, value in params.items())
return list(map(self._quote_expr, params))
def _decode(self, param):
try:
return force_text(param, strings_only=True)
except UnicodeDecodeError:
return '(encoded string)'
def _record(self, method, sql, params):
start_time = time()
try:
return method(sql, params)
finally:
stop_time = time()
duration = (stop_time - start_time)
_params = ''
try:
_params = json.dumps(list(map(self._decode, params)))
except Exception:
pass # object not JSON serializable
alias = getattr(self.db, 'alias', 'default')
conn = self.db.connection
vendor = getattr(conn, 'vendor', 'unknown')
params = {
'vendor': vendor,
'alias': alias,
'sql': self.db.ops.last_executed_query(
self.cursor, sql, self._quote_params(params)),
'duration': duration,
'raw_sql': sql,
'params': _params,
'start_time': start_time,
'stop_time': stop_time,
'is_slow': duration > 10,
'is_select': sql.lower().strip().startswith('select'),
}
if vendor == 'postgresql':
# If an erroneous query was ran on the connection, it might
# be in a state where checking isolation_level raises an
# exception.
try:
iso_level = conn.isolation_level
except conn.InternalError:
iso_level = 'unknown'
params.update({
'trans_id': self.logger.get_transaction_id(alias),
'trans_status': conn.get_transaction_status(),
'iso_level': iso_level,
'encoding': conn.encoding,
})
_sql = DjangoDebugSQL(**params)
# We keep `sql` to maintain backwards compatibility
self.logger.object.sql.append(_sql)
def callproc(self, procname, params=()):
return self._record(self.cursor.callproc, procname, params)
def execute(self, sql, params=()):
return self._record(self.cursor.execute, sql, params)
def executemany(self, sql, param_list):
return self._record(self.cursor.executemany, sql, param_list)
def __getattr__(self, attr):
return getattr(self.cursor, attr)
def __iter__(self):
return iter(self.cursor)
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()

View File

@ -0,0 +1,20 @@
from graphene import Boolean, Float, ObjectType, String
class DjangoDebugSQL(ObjectType):
vendor = String()
alias = String()
sql = String()
duration = Float()
raw_sql = String()
params = String()
start_time = Float()
stop_time = Float()
is_slow = Boolean()
is_select = Boolean()
# Postgres
trans_id = String()
trans_status = String()
iso_level = String()
encoding = String()

View File

View File

@ -0,0 +1,225 @@
import pytest
import graphene
from graphene.relay import Node
from graphene_django import DjangoConnectionField, DjangoObjectType
from graphene_django.utils import DJANGO_FILTER_INSTALLED
from ...tests.models import Reporter
from ..middleware import DjangoDebugMiddleware
from ..types import DjangoDebug
class context(object):
pass
# from examples.starwars_django.models import Character
pytestmark = pytest.mark.django_db
def test_should_query_field():
r1 = Reporter(last_name='ABA')
r1.save()
r2 = Reporter(last_name='Griffin')
r2.save()
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType)
debug = graphene.Field(DjangoDebug, name='__debug')
def resolve_reporter(self, *args, **kwargs):
return Reporter.objects.first()
query = '''
query ReporterQuery {
reporter {
lastName
}
__debug {
sql {
rawSql
}
}
}
'''
expected = {
'reporter': {
'lastName': 'ABA',
},
'__debug': {
'sql': [{
'rawSql': str(Reporter.objects.order_by('pk')[:1].query)
}]
}
}
schema = graphene.Schema(query=Query, middlewares=[DjangoDebugMiddleware()])
result = schema.execute(query, context_value=context())
assert not result.errors
assert result.data == expected
def test_should_query_list():
r1 = Reporter(last_name='ABA')
r1.save()
r2 = Reporter(last_name='Griffin')
r2.save()
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
class Query(graphene.ObjectType):
all_reporters = graphene.List(ReporterType)
debug = graphene.Field(DjangoDebug, name='__debug')
def resolve_all_reporters(self, *args, **kwargs):
return Reporter.objects.all()
query = '''
query ReporterQuery {
allReporters {
lastName
}
__debug {
sql {
rawSql
}
}
}
'''
expected = {
'allReporters': [{
'lastName': 'ABA',
}, {
'lastName': 'Griffin',
}],
'__debug': {
'sql': [{
'rawSql': str(Reporter.objects.all().query)
}]
}
}
schema = graphene.Schema(query=Query, middlewares=[DjangoDebugMiddleware()])
result = schema.execute(query, context_value=context())
assert not result.errors
assert result.data == expected
def test_should_query_connection():
r1 = Reporter(last_name='ABA')
r1.save()
r2 = Reporter(last_name='Griffin')
r2.save()
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
debug = graphene.Field(DjangoDebug, name='__debug')
def resolve_all_reporters(self, *args, **kwargs):
return Reporter.objects.all()
query = '''
query ReporterQuery {
allReporters(first:1) {
edges {
node {
lastName
}
}
}
__debug {
sql {
rawSql
}
}
}
'''
expected = {
'allReporters': {
'edges': [{
'node': {
'lastName': 'ABA',
}
}]
},
}
schema = graphene.Schema(query=Query, middlewares=[DjangoDebugMiddleware()])
result = schema.execute(query, context_value=context())
assert not result.errors
assert result.data['allReporters'] == expected['allReporters']
assert 'COUNT' in result.data['__debug']['sql'][0]['rawSql']
query = str(Reporter.objects.all()[:1].query)
assert result.data['__debug']['sql'][1]['rawSql'] == query
@pytest.mark.skipif(not DJANGO_FILTER_INSTALLED,
reason="requires django-filter")
def test_should_query_connectionfilter():
from ...filter import DjangoFilterConnectionField
r1 = Reporter(last_name='ABA')
r1.save()
r2 = Reporter(last_name='Griffin')
r2.save()
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
class Query(graphene.ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterType)
s = graphene.String(resolver=lambda *_: "S")
debug = graphene.Field(DjangoDebug, name='__debug')
def resolve_all_reporters(self, *args, **kwargs):
return Reporter.objects.all()
query = '''
query ReporterQuery {
allReporters(first:1) {
edges {
node {
lastName
}
}
}
__debug {
sql {
rawSql
}
}
}
'''
expected = {
'allReporters': {
'edges': [{
'node': {
'lastName': 'ABA',
}
}]
},
}
schema = graphene.Schema(query=Query, middlewares=[DjangoDebugMiddleware()])
result = schema.execute(query, context_value=context())
assert not result.errors
assert result.data['allReporters'] == expected['allReporters']
assert 'COUNT' in result.data['__debug']['sql'][0]['rawSql']
query = str(Reporter.objects.all()[:1].query)
assert result.data['__debug']['sql'][1]['rawSql'] == query

View File

@ -0,0 +1,6 @@
from graphene import ObjectType, List
from .sql.types import DjangoDebugSQL
class DjangoDebug(ObjectType):
sql = List(DjangoDebugSQL)

57
graphene_django/fields.py Normal file
View File

@ -0,0 +1,57 @@
from functools import partial
from django.db.models.query import QuerySet
from graphene.relay import ConnectionField, PageInfo
from graphql_relay.connection.arrayconnection import connection_from_list_slice
from .utils import maybe_queryset, DJANGO_FILTER_INSTALLED
class DjangoConnectionField(ConnectionField):
def __init__(self, *args, **kwargs):
self.on = kwargs.pop('on', False)
return super(DjangoConnectionField, self).__init__(*args, **kwargs)
@property
def model(self):
return self.type._meta.node._meta.model
def get_manager(self):
if self.on:
return getattr(self.model, self.on)
else:
return self.model._default_manager
@staticmethod
def connection_resolver(resolver, connection, default_manager, root, args, context, info):
iterable = resolver(root, args, context, info)
if iterable is None:
iterable = default_manager
iterable = maybe_queryset(iterable)
if isinstance(iterable, QuerySet):
_len = iterable.count()
else:
_len = len(iterable)
connection = connection_from_list_slice(
iterable,
args,
slice_start=0,
list_length=_len,
list_slice_length=_len,
connection_type=connection,
edge_type=connection.Edge,
pageinfo_type=PageInfo,
)
connection.iterable = iterable
connection.length = _len
return connection
def get_resolver(self, parent_resolver):
return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager())
def get_connection_field(*args, **kwargs):
if DJANGO_FILTER_INSTALLED:
from .filter.fields import DjangoFilterConnectionField
return DjangoFilterConnectionField(*args, **kwargs)
return ConnectionField(*args, **kwargs)

View File

@ -0,0 +1,14 @@
import warnings
from ..utils import DJANGO_FILTER_INSTALLED
if not DJANGO_FILTER_INSTALLED:
warnings.warn(
"Use of django filtering requires the django-filter package "
"be installed. You can do so using `pip install django-filter`", ImportWarning
)
else:
from .fields import DjangoFilterConnectionField
from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter
__all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet',
'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter']

View File

@ -0,0 +1,39 @@
from functools import partial
from ..fields import DjangoConnectionField
from .utils import get_filtering_args_from_filterset, get_filterset_class
class DjangoFilterConnectionField(DjangoConnectionField):
def __init__(self, type, fields=None, order_by=None,
extra_filter_meta=None, filterset_class=None,
*args, **kwargs):
self.order_by = order_by or type._meta.filter_order_by
self.fields = fields or type._meta.filter_fields
meta = dict(model=type._meta.model,
fields=self.fields,
order_by=self.order_by)
if extra_filter_meta:
meta.update(extra_filter_meta)
self.filterset_class = get_filterset_class(filterset_class, **meta)
self.filtering_args = get_filtering_args_from_filterset(self.filterset_class, type)
kwargs.setdefault('args', {})
kwargs['args'].update(self.filtering_args)
super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs)
@staticmethod
def connection_resolver(resolver, connection, default_manager, filterset_class, filtering_args,
root, args, context, info):
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
order = args.get('order_by', None)
qs = default_manager.get_queryset()
if order:
qs = qs.order_by(order)
qs = filterset_class(data=filter_kwargs, queryset=qs)
return DjangoConnectionField.connection_resolver(resolver, connection, qs, root, args, context, info)
def get_resolver(self, parent_resolver):
return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager(),
self.filterset_class, self.filtering_args)

View File

@ -0,0 +1,115 @@
import six
from django.conf import settings
from django.db import models
from django.utils.text import capfirst
from django_filters import Filter, MultipleChoiceFilter
from django_filters.filterset import FilterSet, FilterSetMetaclass
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField
from graphql_relay.node.node import from_global_id
class GlobalIDFilter(Filter):
field_class = GlobalIDFormField
def filter(self, qs, value):
_type, _id = from_global_id(value)
return super(GlobalIDFilter, self).filter(qs, _id)
class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
field_class = GlobalIDMultipleChoiceField
def filter(self, qs, value):
gids = [from_global_id(v)[1] for v in value]
return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids)
ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order_by')
GRAPHENE_FILTER_SET_OVERRIDES = {
models.AutoField: {
'filter_class': GlobalIDFilter,
},
models.OneToOneField: {
'filter_class': GlobalIDFilter,
},
models.ForeignKey: {
'filter_class': GlobalIDFilter,
},
models.ManyToManyField: {
'filter_class': GlobalIDMultipleChoiceFilter,
}
}
class GrapheneFilterSetMetaclass(FilterSetMetaclass):
def __new__(cls, name, bases, attrs):
new_class = super(GrapheneFilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
# Customise the filter_overrides for Graphene
for k, v in GRAPHENE_FILTER_SET_OVERRIDES.items():
new_class.filter_overrides.setdefault(k, v)
return new_class
class GrapheneFilterSetMixin(object):
order_by_field = ORDER_BY_FIELD
@classmethod
def filter_for_reverse_field(cls, f, name):
"""Handles retrieving filters for reverse relationships
We override the default implementation so that we can handle
Global IDs (the default implementation expects database
primary keys)
"""
rel = f.field.rel
default = {
'name': name,
'label': capfirst(rel.related_name)
}
if rel.multiple:
# For to-many relationships
return GlobalIDMultipleChoiceFilter(**default)
else:
# For to-one relationships
return GlobalIDFilter(**default)
class GrapheneFilterSet(six.with_metaclass(GrapheneFilterSetMetaclass, GrapheneFilterSetMixin, FilterSet)):
""" Base class for FilterSets used by Graphene
You shouldn't usually need to use this class. The
DjangoFilterConnectionField will wrap FilterSets with this class as
necessary
"""
def setup_filterset(filterset_class):
""" Wrap a provided filterset in Graphene-specific functionality
"""
return type(
'Graphene{}'.format(filterset_class.__name__),
(six.with_metaclass(GrapheneFilterSetMetaclass, GrapheneFilterSetMixin, filterset_class),),
{},
)
def custom_filterset_factory(model, filterset_base_class=GrapheneFilterSet,
**meta):
""" Create a filterset for the given model using the provided meta data
"""
meta.update({
'model': model,
})
meta_class = type(str('Meta'), (object,), meta)
filterset = type(
str('%sFilterSet' % model._meta.object_name),
(filterset_base_class,),
{
'Meta': meta_class
}
)
return filterset

View File

View File

@ -0,0 +1,31 @@
import django_filters
from graphene_django.tests.models import Article, Pet, Reporter
class ArticleFilter(django_filters.FilterSet):
class Meta:
model = Article
fields = {
'headline': ['exact', 'icontains'],
'pub_date': ['gt', 'lt', 'exact'],
'reporter': ['exact'],
}
order_by = True
class ReporterFilter(django_filters.FilterSet):
class Meta:
model = Reporter
fields = ['first_name', 'last_name', 'email', 'pets']
order_by = False
class PetFilter(django_filters.FilterSet):
class Meta:
model = Pet
fields = ['name']
order_by = False

View File

@ -0,0 +1,339 @@
from datetime import datetime
import pytest
from graphene import ObjectType, Schema, Field
from graphene.relay import Node
from graphene_django import DjangoObjectType
from graphene_django.forms import (GlobalIDFormField,
GlobalIDMultipleChoiceField)
from graphene_django.tests.models import Article, Pet, Reporter
from graphene_django.utils import DJANGO_FILTER_INSTALLED
pytestmark = []
if DJANGO_FILTER_INSTALLED:
import django_filters
from graphene_django.filter import (GlobalIDFilter, DjangoFilterConnectionField,
GlobalIDMultipleChoiceFilter)
from graphene_django.filter.tests.filters import ArticleFilter, PetFilter
else:
pytestmark.append(pytest.mark.skipif(True, reason='django_filters not installed'))
pytestmark.append(pytest.mark.django_db)
class ArticleNode(DjangoObjectType):
class Meta:
model = Article
interfaces = (Node, )
class ReporterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
class PetNode(DjangoObjectType):
class Meta:
model = Pet
interfaces = (Node, )
# schema = Schema()
def get_args(field):
if isinstance(field.args, list):
return {arg.name: arg for arg in field.args}
else:
return field.args
def assert_arguments(field, *arguments):
ignore = ('after', 'before', 'first', 'last', 'order_by')
args = get_args(field)
actual = [
name
for name in args
if name not in ignore and not name.startswith('_')
]
assert set(arguments) == set(actual), \
'Expected arguments ({}) did not match actual ({})'.format(
arguments,
actual
)
def assert_orderable(field):
args = get_args(field)
assert 'order_by' in args, \
'Field cannot be ordered'
def assert_not_orderable(field):
args = get_args(field)
assert 'order_by' not in args, \
'Field can be ordered'
def test_filter_explicit_filterset_arguments():
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter)
assert_arguments(field,
'headline', 'headline__icontains',
'pub_date', 'pub_date__gt', 'pub_date__lt',
'reporter',
)
def test_filter_shortcut_filterset_arguments_list():
field = DjangoFilterConnectionField(ArticleNode, fields=['pub_date', 'reporter'])
assert_arguments(field,
'pub_date',
'reporter',
)
def test_filter_shortcut_filterset_arguments_dict():
field = DjangoFilterConnectionField(ArticleNode, fields={
'headline': ['exact', 'icontains'],
'reporter': ['exact'],
})
assert_arguments(field,
'headline', 'headline__icontains',
'reporter',
)
def test_filter_explicit_filterset_orderable():
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter)
assert_orderable(field)
def test_filter_shortcut_filterset_orderable_true():
field = DjangoFilterConnectionField(ArticleNode, order_by=True)
assert_orderable(field)
def test_filter_shortcut_filterset_orderable_headline():
field = DjangoFilterConnectionField(ArticleNode, order_by=['headline'])
assert_orderable(field)
def test_filter_explicit_filterset_not_orderable():
field = DjangoFilterConnectionField(PetNode, filterset_class=PetFilter)
assert_not_orderable(field)
def test_filter_shortcut_filterset_extra_meta():
field = DjangoFilterConnectionField(ArticleNode, extra_filter_meta={
'order_by': True
})
assert_orderable(field)
def test_filter_filterset_information_on_meta():
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
filter_fields = ['first_name', 'articles']
filter_order_by = True
field = DjangoFilterConnectionField(ReporterFilterNode)
assert_arguments(field, 'first_name', 'articles')
assert_orderable(field)
def test_filter_filterset_information_on_meta_related():
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
filter_fields = ['first_name', 'articles']
filter_order_by = True
class ArticleFilterNode(DjangoObjectType):
class Meta:
model = Article
interfaces = (Node, )
filter_fields = ['headline', 'reporter']
filter_order_by = True
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
all_articles = DjangoFilterConnectionField(ArticleFilterNode)
reporter = Field(ReporterFilterNode)
article = Field(ArticleFilterNode)
schema = Schema(query=Query)
articles_field = ReporterFilterNode._meta.fields['articles'].get_type()
assert_arguments(articles_field, 'headline', 'reporter')
assert_orderable(articles_field)
def test_filter_filterset_related_results():
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
filter_fields = ['first_name', 'articles']
filter_order_by = True
class ArticleFilterNode(DjangoObjectType):
class Meta:
interfaces = (Node, )
model = Article
filter_fields = ['headline', 'reporter']
filter_order_by = True
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
all_articles = DjangoFilterConnectionField(ArticleFilterNode)
reporter = Field(ReporterFilterNode)
article = Field(ArticleFilterNode)
r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com')
r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com')
Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1)
Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2)
query = '''
query {
allReporters {
edges {
node {
articles {
edges {
node {
headline
}
}
}
}
}
}
}
'''
schema = Schema(query=Query)
result = schema.execute(query)
assert not result.errors
# We should only get back a single article for each reporter
assert len(result.data['allReporters']['edges'][0]['node']['articles']['edges']) == 1
assert len(result.data['allReporters']['edges'][1]['node']['articles']['edges']) == 1
def test_global_id_field_implicit():
field = DjangoFilterConnectionField(ArticleNode, fields=['id'])
filterset_class = field.filterset_class
id_filter = filterset_class.base_filters['id']
assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField
def test_global_id_field_explicit():
class ArticleIdFilter(django_filters.FilterSet):
class Meta:
model = Article
fields = ['id']
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter)
filterset_class = field.filterset_class
id_filter = filterset_class.base_filters['id']
assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField
def test_global_id_field_relation():
field = DjangoFilterConnectionField(ArticleNode, fields=['reporter'])
filterset_class = field.filterset_class
id_filter = filterset_class.base_filters['reporter']
assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField
def test_global_id_multiple_field_implicit():
field = DjangoFilterConnectionField(ReporterNode, fields=['pets'])
filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['pets']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_global_id_multiple_field_explicit():
class ReporterPetsFilter(django_filters.FilterSet):
class Meta:
model = Reporter
fields = ['pets']
field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter)
filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['pets']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_global_id_multiple_field_implicit_reverse():
field = DjangoFilterConnectionField(ReporterNode, fields=['articles'])
filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['articles']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_global_id_multiple_field_explicit_reverse():
class ReporterPetsFilter(django_filters.FilterSet):
class Meta:
model = Reporter
fields = ['articles']
field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter)
filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['articles']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_filter_filterset_related_results():
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
filter_fields = {
'first_name': ['icontains']
}
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
r1 = Reporter.objects.create(first_name='A test user', last_name='Last Name', email='test1@test.com')
r2 = Reporter.objects.create(first_name='Other test user', last_name='Other Last Name', email='test2@test.com')
r3 = Reporter.objects.create(first_name='Random', last_name='RandomLast', email='random@test.com')
query = '''
query {
allReporters(firstName_Icontains: "test") {
edges {
node {
id
}
}
}
}
'''
schema = Schema(query=Query)
result = schema.execute(query)
assert not result.errors
# We should only get two reporters
assert len(result.data['allReporters']['edges']) == 2

View File

@ -0,0 +1,31 @@
import six
from graphene import Argument, String
from .filterset import custom_filterset_factory, setup_filterset
def get_filtering_args_from_filterset(filterset_class, type):
""" Inspect a FilterSet and produce the arguments to pass to
a Graphene Field. These arguments will be available to
filter against in the GraphQL
"""
from ..form_converter import convert_form_field
args = {}
for name, filter_field in six.iteritems(filterset_class.base_filters):
field_type = convert_form_field(filter_field.field)
args[name] = field_type
# Also add the 'order_by' field
if filterset_class._meta.order_by:
args[filterset_class.order_by_field] = String()
return args
def get_filterset_class(filterset_class, **meta):
"""Get the class to be used as the FilterSet"""
if filterset_class:
# If were given a FilterSet class, then set it up and
# return it
return setup_filterset(filterset_class)
return custom_filterset_factory(**meta)

View File

@ -0,0 +1,70 @@
from django import forms
from django.forms.fields import BaseTemporalField
from graphene import ID, Boolean, Float, Int, String, List
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
from .utils import import_single_dispatch
singledispatch = import_single_dispatch()
try:
UUIDField = forms.UUIDField
except AttributeError:
class UUIDField(object):
pass
@singledispatch
def convert_form_field(field):
raise Exception(
"Don't know how to convert the Django form field %s (%s) "
"to Graphene type" %
(field, field.__class__)
)
@convert_form_field.register(BaseTemporalField)
@convert_form_field.register(forms.CharField)
@convert_form_field.register(forms.EmailField)
@convert_form_field.register(forms.SlugField)
@convert_form_field.register(forms.URLField)
@convert_form_field.register(forms.ChoiceField)
@convert_form_field.register(forms.RegexField)
@convert_form_field.register(forms.Field)
@convert_form_field.register(UUIDField)
def convert_form_field_to_string(field):
return String(description=field.help_text)
@convert_form_field.register(forms.IntegerField)
@convert_form_field.register(forms.NumberInput)
def convert_form_field_to_int(field):
return Int(description=field.help_text)
@convert_form_field.register(forms.BooleanField)
def convert_form_field_to_boolean(field):
return Boolean(description=field.help_text, required=True)
@convert_form_field.register(forms.NullBooleanField)
def convert_form_field_to_nullboolean(field):
return Boolean(description=field.help_text)
@convert_form_field.register(forms.DecimalField)
@convert_form_field.register(forms.FloatField)
def convert_form_field_to_float(field):
return Float(description=field.help_text)
@convert_form_field.register(forms.ModelMultipleChoiceField)
@convert_form_field.register(GlobalIDMultipleChoiceField)
def convert_form_field_to_list(field):
return List(ID)
@convert_form_field.register(forms.ModelChoiceField)
@convert_form_field.register(GlobalIDFormField)
def convert_form_field_to_id(field):
return ID()

42
graphene_django/forms.py Normal file
View File

@ -0,0 +1,42 @@
import binascii
from django.core.exceptions import ValidationError
from django.forms import CharField, Field, MultipleChoiceField
from django.utils.translation import ugettext_lazy as _
from graphql_relay import from_global_id
class GlobalIDFormField(Field):
default_error_messages = {
'invalid': _('Invalid ID specified.'),
}
def clean(self, value):
if not value and not self.required:
return None
try:
_type, _id = from_global_id(value)
except (TypeError, ValueError, UnicodeDecodeError, binascii.Error):
raise ValidationError(self.error_messages['invalid'])
try:
CharField().clean(_id)
CharField().clean(_type)
except ValidationError:
raise ValidationError(self.error_messages['invalid'])
return value
class GlobalIDMultipleChoiceField(MultipleChoiceField):
default_error_messages = {
'invalid_choice': _('One of the specified IDs was invalid (%(value)s).'),
'invalid_list': _('Enter a list of values.'),
}
def valid_value(self, value):
# Clean will raise a validation error if there is a problem
GlobalIDFormField().clean(value)
return True

View File

View File

@ -0,0 +1,72 @@
import importlib
import json
from distutils.version import StrictVersion
from optparse import make_option
from django import get_version as get_django_version
from django.core.management.base import BaseCommand, CommandError
LT_DJANGO_1_8 = StrictVersion(get_django_version()) < StrictVersion('1.8')
if LT_DJANGO_1_8:
class CommandArguments(BaseCommand):
option_list = BaseCommand.option_list + (
make_option(
'--schema',
type=str,
dest='schema',
default='',
help='Django app containing schema to dump, e.g. myproject.core.schema',
),
make_option(
'--out',
type=str,
dest='out',
default='',
help='Output file (default: schema.json)'
),
)
else:
class CommandArguments(BaseCommand):
def add_arguments(self, parser):
from django.conf import settings
parser.add_argument(
'--schema',
type=str,
dest='schema',
default=getattr(settings, 'GRAPHENE_SCHEMA', ''),
help='Django app containing schema to dump, e.g. myproject.core.schema')
parser.add_argument(
'--out',
type=str,
dest='out',
default=getattr(settings, 'GRAPHENE_SCHEMA_OUTPUT', 'schema.json'),
help='Output file (default: schema.json)')
class Command(CommandArguments):
help = 'Dump Graphene schema JSON to file'
can_import_settings = True
def save_file(self, out, schema_dict):
with open(out, 'w') as outfile:
json.dump(schema_dict, outfile)
def handle(self, *args, **options):
from django.conf import settings
schema = options.get('schema') or getattr(settings, 'GRAPHENE_SCHEMA', '')
out = options.get('out') or getattr(settings, 'GRAPHENE_SCHEMA_OUTPUT', 'schema.json')
if schema == '':
raise CommandError('Specify schema on GRAPHENE_SCHEMA setting or by using --schema')
i = importlib.import_module(schema)
schema_dict = {'data': i.schema.introspect()}
self.save_file(out, schema_dict)
style = getattr(self, 'style', None)
SUCCESS = getattr(style, 'SUCCESS', lambda x: x)
self.stdout.write(SUCCESS('Successfully dumped GraphQL schema to %s' % out))

View File

@ -0,0 +1,29 @@
class Registry(object):
def __init__(self):
self._registry = {}
self._registry_models = {}
def register(self, cls):
from .types import DjangoObjectType
assert issubclass(cls, DjangoObjectType), 'Only DjangoObjectTypes can be registered, received "{}"'.format(cls.__name__)
assert cls._meta.registry == self, 'Registry for a Model have to match.'
# assert self.get_type_for_model(cls._meta.model) == cls, 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model)
self._registry[cls._meta.model] = cls
def get_type_for_model(self, model):
return self._registry.get(model)
registry = None
def get_global_registry():
global registry
if not registry:
registry = Registry()
return registry
def reset_global_registry():
global registry
registry = None

View File

View File

@ -0,0 +1,52 @@
from __future__ import absolute_import
from django.db import models
from django.utils.translation import ugettext_lazy as _
CHOICES = (
(1, 'this'),
(2, _('that'))
)
class Pet(models.Model):
name = models.CharField(max_length=30)
class FilmDetails(models.Model):
location = models.CharField(max_length=30)
film = models.OneToOneField('Film', related_name='details')
class Film(models.Model):
reporters = models.ManyToManyField('Reporter',
related_name='films')
class Reporter(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email = models.EmailField()
pets = models.ManyToManyField('self')
a_choice = models.CharField(max_length=30, choices=CHOICES)
def __str__(self): # __unicode__ on Python 2
return "%s %s" % (self.first_name, self.last_name)
class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
reporter = models.ForeignKey(Reporter, related_name='articles')
lang = models.CharField(max_length=2, help_text='Language', choices=[
('es', 'Spanish'),
('en', 'English')
], default='es')
importance = models.IntegerField('Importance', null=True, blank=True,
choices=[(1, u'Very important'), (2, u'Not as important')])
def __str__(self): # __unicode__ on Python 2
return self.headline
class Meta:
ordering = ('headline',)

View File

@ -0,0 +1,39 @@
import graphene
from graphene import Schema, relay
from ..types import DjangoObjectType
from .models import Article, Reporter
class Character(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (relay.Node, )
def get_node(self, id, context, info):
pass
class Human(DjangoObjectType):
raises = graphene.String()
class Meta:
model = Article
interfaces = (relay.Node, )
def resolve_raises(self, *args):
raise Exception("This field should raise exception")
def get_node(self, id):
pass
class Query(graphene.ObjectType):
human = graphene.Field(Human)
def resolve_human(self, args, context, info):
return Human()
schema = Schema(query=Query)

View File

@ -0,0 +1,11 @@
from django.core import management
from mock import patch
from six import StringIO
@patch('graphene_django.management.commands.graphql_schema.Command.save_file')
def test_generate_file_on_call_graphql_schema(savefile_mock, settings):
settings.GRAPHENE_SCHEMA = 'graphene_django.tests.schema'
out = StringIO()
management.call_command('graphql_schema', schema='', stdout=out)
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()

View File

@ -0,0 +1,261 @@
import pytest
from django.db import models
from django.utils.translation import ugettext_lazy as _
from py.test import raises
import graphene
from graphene.relay import Node, ConnectionField
from graphene.types.datetime import DateTime
from graphene.types.json import JSONString
# from graphene.core.types.custom_scalars import DateTime, JSONString
from ..compat import (ArrayField, HStoreField, JSONField, MissingType,
RangeField)
from ..converter import convert_django_field, convert_django_field_with_choices
from ..registry import Registry
from .models import Article, Reporter, Film, FilmDetails, Pet
from ..types import DjangoObjectType
def assert_conversion(django_field, graphene_field, *args, **kwargs):
field = django_field(help_text='Custom Help Text', *args, **kwargs)
graphene_type = convert_django_field(field)
assert isinstance(graphene_type, graphene_field)
field = graphene_type.Field()
assert field.description == 'Custom Help Text'
return field
def test_should_unknown_django_field_raise_exception():
with raises(Exception) as excinfo:
convert_django_field(None)
assert 'Don\'t know how to convert the Django field' in str(excinfo.value)
def test_should_date_convert_string():
assert_conversion(models.DateField, DateTime)
def test_should_char_convert_string():
assert_conversion(models.CharField, graphene.String)
def test_should_text_convert_string():
assert_conversion(models.TextField, graphene.String)
def test_should_email_convert_string():
assert_conversion(models.EmailField, graphene.String)
def test_should_slug_convert_string():
assert_conversion(models.SlugField, graphene.String)
def test_should_url_convert_string():
assert_conversion(models.URLField, graphene.String)
def test_should_ipaddress_convert_string():
assert_conversion(models.GenericIPAddressField, graphene.String)
def test_should_file_convert_string():
assert_conversion(models.FileField, graphene.String)
def test_should_image_convert_string():
assert_conversion(models.ImageField, graphene.String)
def test_should_auto_convert_id():
assert_conversion(models.AutoField, graphene.ID, primary_key=True)
def test_should_positive_integer_convert_int():
assert_conversion(models.PositiveIntegerField, graphene.Int)
def test_should_positive_small_convert_int():
assert_conversion(models.PositiveSmallIntegerField, graphene.Int)
def test_should_small_integer_convert_int():
assert_conversion(models.SmallIntegerField, graphene.Int)
def test_should_big_integer_convert_int():
assert_conversion(models.BigIntegerField, graphene.Int)
def test_should_integer_convert_int():
assert_conversion(models.IntegerField, graphene.Int)
def test_should_boolean_convert_boolean():
field = assert_conversion(models.BooleanField, graphene.NonNull)
assert field.type.of_type == graphene.Boolean
def test_should_nullboolean_convert_boolean():
assert_conversion(models.NullBooleanField, graphene.Boolean)
def test_field_with_choices_convert_enum():
field = models.CharField(help_text='Language', choices=(
('es', 'Spanish'),
('en', 'English')
))
class TranslatedModel(models.Model):
language = field
class Meta:
app_label = 'test'
graphene_type = convert_django_field_with_choices(field)
assert isinstance(graphene_type, graphene.Enum)
assert graphene_type._meta.name == 'TranslatedModelLanguage'
assert graphene_type._meta.enum.__members__['SPANISH'].value == 'es'
assert graphene_type._meta.enum.__members__['SPANISH'].description == 'Spanish'
assert graphene_type._meta.enum.__members__['ENGLISH'].value == 'en'
assert graphene_type._meta.enum.__members__['ENGLISH'].description == 'English'
def test_field_with_grouped_choices():
field = models.CharField(help_text='Language', choices=(
('Europe', (
('es', 'Spanish'),
('en', 'English'),
)),
))
class GroupedChoicesModel(models.Model):
language = field
class Meta:
app_label = 'test'
convert_django_field_with_choices(field)
def test_field_with_choices_gettext():
field = models.CharField(help_text='Language', choices=(
('es', _('Spanish')),
('en', _('English'))
))
class TranslatedChoicesModel(models.Model):
language = field
class Meta:
app_label = 'test'
convert_django_field_with_choices(field)
def test_should_float_convert_float():
assert_conversion(models.FloatField, graphene.Float)
def test_should_manytomany_convert_connectionorlist():
registry = Registry()
dynamic_field = convert_django_field(Reporter._meta.local_many_to_many[0], registry)
assert not dynamic_field.get_type()
def test_should_manytomany_convert_connectionorlist_list():
class A(DjangoObjectType):
class Meta:
model = Reporter
graphene_field = convert_django_field(Reporter._meta.local_many_to_many[0], A._meta.registry)
assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field)
assert isinstance(dynamic_field.type, graphene.List)
assert dynamic_field.type.of_type == A
def test_should_manytomany_convert_connectionorlist_connection():
class A(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
graphene_field = convert_django_field(Reporter._meta.local_many_to_many[0], A._meta.registry)
assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, ConnectionField)
assert dynamic_field.type == A.Connection
def test_should_manytoone_convert_connectionorlist():
# Django 1.9 uses 'rel', <1.9 uses 'related
related = getattr(Reporter.articles, 'rel', None) or \
getattr(Reporter.articles, 'related')
class A(DjangoObjectType):
class Meta:
model = Article
graphene_field = convert_django_field(related, A._meta.registry)
assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field)
assert isinstance(dynamic_field.type, graphene.List)
assert dynamic_field.type.of_type == A
def test_should_onetoone_reverse_convert_model():
# Django 1.9 uses 'rel', <1.9 uses 'related
related = getattr(Film.details, 'rel', None) or \
getattr(Film.details, 'related')
class A(DjangoObjectType):
class Meta:
model = FilmDetails
graphene_field = convert_django_field(related, A._meta.registry)
assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field)
assert dynamic_field.type == A
@pytest.mark.skipif(ArrayField is MissingType,
reason="ArrayField should exist")
def test_should_postgres_array_convert_list():
field = assert_conversion(ArrayField, graphene.List, models.CharField(max_length=100))
assert isinstance(field.type, graphene.List)
assert field.type.of_type == graphene.String
@pytest.mark.skipif(ArrayField is MissingType,
reason="ArrayField should exist")
def test_should_postgres_array_multiple_convert_list():
field = assert_conversion(ArrayField, graphene.List, ArrayField(models.CharField(max_length=100)))
assert isinstance(field.type, graphene.List)
assert isinstance(field.type.of_type, graphene.List)
assert field.type.of_type.of_type == graphene.String
@pytest.mark.skipif(HStoreField is MissingType,
reason="HStoreField should exist")
def test_should_postgres_hstore_convert_string():
assert_conversion(HStoreField, JSONString)
@pytest.mark.skipif(JSONField is MissingType,
reason="JSONField should exist")
def test_should_postgres_json_convert_string():
assert_conversion(JSONField, JSONString)
@pytest.mark.skipif(RangeField is MissingType,
reason="RangeField should exist")
def test_should_postgres_range_convert_list():
from django.contrib.postgres.fields import IntegerRangeField
field = assert_conversion(IntegerRangeField, graphene.List)
assert isinstance(field.type, graphene.List)
assert field.type.of_type == graphene.Int

View File

@ -0,0 +1,103 @@
from django import forms
from py.test import raises
import graphene
from ..form_converter import convert_form_field
from graphene import ID, List, NonNull
from .models import Reporter
def assert_conversion(django_field, graphene_field, *args):
field = django_field(*args, help_text='Custom Help Text')
graphene_type = convert_form_field(field)
assert isinstance(graphene_type, graphene_field)
field = graphene_type.Field()
assert field.description == 'Custom Help Text'
return field
def test_should_unknown_django_field_raise_exception():
with raises(Exception) as excinfo:
convert_form_field(None)
assert 'Don\'t know how to convert the Django form field' in str(excinfo.value)
def test_should_date_convert_string():
assert_conversion(forms.DateField, graphene.String)
def test_should_time_convert_string():
assert_conversion(forms.TimeField, graphene.String)
def test_should_date_time_convert_string():
assert_conversion(forms.DateTimeField, graphene.String)
def test_should_char_convert_string():
assert_conversion(forms.CharField, graphene.String)
def test_should_email_convert_string():
assert_conversion(forms.EmailField, graphene.String)
def test_should_slug_convert_string():
assert_conversion(forms.SlugField, graphene.String)
def test_should_url_convert_string():
assert_conversion(forms.URLField, graphene.String)
def test_should_choice_convert_string():
assert_conversion(forms.ChoiceField, graphene.String)
def test_should_base_field_convert_string():
assert_conversion(forms.Field, graphene.String)
def test_should_regex_convert_string():
assert_conversion(forms.RegexField, graphene.String, '[0-9]+')
def test_should_uuid_convert_string():
if hasattr(forms, 'UUIDField'):
assert_conversion(forms.UUIDField, graphene.String)
def test_should_integer_convert_int():
assert_conversion(forms.IntegerField, graphene.Int)
def test_should_boolean_convert_boolean():
field = assert_conversion(forms.BooleanField, graphene.Boolean)
assert isinstance(field.type, NonNull)
def test_should_nullboolean_convert_boolean():
field = assert_conversion(forms.NullBooleanField, graphene.Boolean)
assert not isinstance(field.type, NonNull)
def test_should_float_convert_float():
assert_conversion(forms.FloatField, graphene.Float)
def test_should_decimal_convert_float():
assert_conversion(forms.DecimalField, graphene.Float)
def test_should_multiple_choice_convert_connectionorlist():
field = forms.ModelMultipleChoiceField(Reporter.objects.all())
graphene_type = convert_form_field(field)
assert isinstance(graphene_type, List)
assert graphene_type.of_type == ID
def test_should_manytoone_convert_connectionorlist():
field = forms.ModelChoiceField(Reporter.objects.all())
graphene_type = convert_form_field(field)
assert isinstance(graphene_type, graphene.ID)

View File

@ -0,0 +1,29 @@
from django.core.exceptions import ValidationError
from py.test import raises
from ..forms import GlobalIDFormField
# 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc'
def test_global_id_valid():
field = GlobalIDFormField()
field.clean('TXlUeXBlOmFiYw==')
def test_global_id_invalid():
field = GlobalIDFormField()
with raises(ValidationError):
field.clean('badvalue')
def test_global_id_none():
field = GlobalIDFormField()
with raises(ValidationError):
field.clean(None)
def test_global_id_none_optional():
field = GlobalIDFormField(required=False)
field.clean(None)

View File

@ -0,0 +1,252 @@
import datetime
import pytest
from django.db import models
from py.test import raises
import graphene
from graphene.relay import Node
from ..compat import MissingType, RangeField
from ..types import DjangoObjectType
from ..fields import DjangoConnectionField
from ..registry import reset_global_registry, get_global_registry
from .models import Article, Reporter
pytestmark = pytest.mark.django_db
def test_should_query_only_fields():
with raises(Exception):
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
only_fields = ('articles', )
schema = graphene.Schema(query=ReporterType)
query = '''
query ReporterQuery {
articles
}
'''
result = schema.execute(query)
assert not result.errors
def test_should_query_well():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType)
def resolve_reporter(self, *args, **kwargs):
return Reporter(first_name='ABA', last_name='X')
query = '''
query ReporterQuery {
reporter {
firstName,
lastName,
email
}
}
'''
expected = {
'reporter': {
'firstName': 'ABA',
'lastName': 'X',
'email': ''
}
}
schema = graphene.Schema(query=Query)
result = schema.execute(query)
assert not result.errors
assert result.data == expected
@pytest.mark.skipif(RangeField is MissingType,
reason="RangeField should exist")
def test_should_query_postgres_fields():
from django.contrib.postgres.fields import IntegerRangeField, ArrayField, JSONField, HStoreField
class Event(models.Model):
ages = IntegerRangeField(help_text='The age ranges')
data = JSONField(help_text='Data')
store = HStoreField()
tags = ArrayField(models.CharField(max_length=50))
class EventType(DjangoObjectType):
class Meta:
model = Event
class Query(graphene.ObjectType):
event = graphene.Field(EventType)
def resolve_event(self, *args, **kwargs):
return Event(
ages=(0, 10),
data={'angry_babies': True},
store={'h': 'store'},
tags=['child', 'angry', 'babies']
)
schema = graphene.Schema(query=Query)
query = '''
query myQuery {
event {
ages
tags
data
store
}
}
'''
expected = {
'event': {
'ages': [0, 10],
'tags': ['child', 'angry', 'babies'],
'data': '{"angry_babies": true}',
'store': '{"h": "store"}',
},
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_should_node():
# reset_global_registry()
# Node._meta.registry = get_global_registry()
class ReporterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
@classmethod
def get_node(cls, id, context, info):
return Reporter(id=2, first_name='Cookie Monster')
def resolve_articles(self, *args, **kwargs):
return [Article(headline='Hi!')]
class ArticleNode(DjangoObjectType):
class Meta:
model = Article
interfaces = (Node, )
@classmethod
def get_node(cls, id, context, info):
return Article(id=1, headline='Article node', pub_date=datetime.date(2002, 3, 11))
class Query(graphene.ObjectType):
node = Node.Field()
reporter = graphene.Field(ReporterNode)
article = graphene.Field(ArticleNode)
def resolve_reporter(self, *args, **kwargs):
return Reporter(id=1, first_name='ABA', last_name='X')
query = '''
query ReporterQuery {
reporter {
id,
firstName,
articles {
edges {
node {
headline
}
}
}
lastName,
email
}
myArticle: node(id:"QXJ0aWNsZU5vZGU6MQ==") {
id
... on ReporterNode {
firstName
}
... on ArticleNode {
headline
pubDate
}
}
}
'''
expected = {
'reporter': {
'id': 'UmVwb3J0ZXJOb2RlOjE=',
'firstName': 'ABA',
'lastName': 'X',
'email': '',
'articles': {
'edges': [{
'node': {
'headline': 'Hi!'
}
}]
},
},
'myArticle': {
'id': 'QXJ0aWNsZU5vZGU6MQ==',
'headline': 'Article node',
'pubDate': '2002-03-11',
}
}
schema = graphene.Schema(query=Query)
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_should_query_connectionfields():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
only_fields = ('articles', )
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
def resolve_all_reporters(self, args, context, info):
return [Reporter(id=1)]
schema = graphene.Schema(query=Query)
query = '''
query ReporterConnectionQuery {
allReporters {
pageInfo {
hasNextPage
}
edges {
node {
id
}
}
}
}
'''
result = schema.execute(query)
assert not result.errors
assert result.data == {
'allReporters': {
'pageInfo': {
'hasNextPage': False,
},
'edges': [{
'node': {
'id': 'UmVwb3J0ZXJUeXBlOjE='
}
}]
}
}

View File

@ -0,0 +1,40 @@
from py.test import raises
from ..types import DjangoObjectType
from ..registry import Registry
from .models import Reporter
def test_should_raise_if_no_model():
with raises(Exception) as excinfo:
class Character1(DjangoObjectType):
pass
assert 'valid Django Model' in str(excinfo.value)
def test_should_raise_if_model_is_invalid():
with raises(Exception) as excinfo:
class Character2(DjangoObjectType):
class Meta:
model = 1
assert 'valid Django Model' in str(excinfo.value)
def test_should_map_fields_correctly():
class ReporterType2(DjangoObjectType):
class Meta:
model = Reporter
registry = Registry()
assert list(ReporterType2._meta.fields.keys()) == ['id', 'first_name', 'last_name', 'email', 'pets', 'a_choice', 'articles', 'films']
def test_should_map_only_few_fields():
class Reporter2(DjangoObjectType):
class Meta:
model = Reporter
only_fields = ('id', 'email')
assert list(Reporter2._meta.fields.keys()) == ['id', 'email']

View File

@ -0,0 +1,123 @@
from graphql.type import GraphQLObjectType
from mock import patch
from graphene import ObjectType, Field, Int, ID, Schema, Interface
from graphene.relay import Node, ConnectionField
from ..types import DjangoObjectType
from .models import Article as ArticleModel, Reporter as ReporterModel
from ..registry import reset_global_registry, Registry
reset_global_registry()
class Reporter(DjangoObjectType):
'''Reporter description'''
class Meta:
model = ReporterModel
class Article(DjangoObjectType):
'''Article description'''
class Meta:
model = ArticleModel
interfaces = (Node, )
class RootQuery(ObjectType):
node = Node.Field()
schema = Schema(query=RootQuery, types=[Article, Reporter])
def test_django_interface():
assert issubclass(Node, Interface)
assert issubclass(Node, Node)
@patch('graphene_django.tests.models.Article.objects.get', return_value=Article(id=1))
def test_django_get_node(get):
article = Article.get_node(1, None, None)
get.assert_called_with(id=1)
assert article.id == 1
def test_django_objecttype_map_correct_fields():
fields = Reporter._meta.fields
assert list(fields.keys()) == ['id', 'first_name', 'last_name', 'email', 'pets', 'a_choice', 'articles', 'films']
def test_django_objecttype_with_node_have_correct_fields():
fields = Article._meta.fields
assert list(fields.keys()) == ['id', 'headline', 'pub_date', 'reporter', 'lang', 'importance']
def test_schema_representation():
expected = """
schema {
query: RootQuery
}
type Article implements Node {
id: ID!
headline: String
pubDate: DateTime
reporter: Reporter
lang: ArticleLang
importance: ArticleImportance
}
type ArticleConnection {
pageInfo: PageInfo!
edges: [ArticleEdge]
}
type ArticleEdge {
node: Article
cursor: String!
}
enum ArticleImportance {
VERY_IMPORTANT
NOT_AS_IMPORTANT
}
enum ArticleLang {
SPANISH
ENGLISH
}
scalar DateTime
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Reporter {
id: ID
firstName: String
lastName: String
email: String
pets: [Reporter]
aChoice: ReporterA_choice
articles(before: String, after: String, first: Int, last: Int): ArticleConnection
}
enum ReporterA_choice {
THIS
THAT
}
type RootQuery {
node(id: ID!): Node
}
""".lstrip()
assert str(schema) == expected

View File

@ -0,0 +1,57 @@
import json
def format_response(response):
return json.loads(response.content.decode())
def test_client_get_good_query(settings, client):
settings.ROOT_URLCONF = 'graphene_django.tests.urls'
response = client.get('/graphql', {'query': '{ human { headline } }'})
json_response = format_response(response)
expected_json = {
'data': {
'human': {
'headline': None
}
}
}
assert json_response == expected_json
def test_client_get_good_query_with_raise(settings, client):
settings.ROOT_URLCONF = 'graphene_django.tests.urls'
response = client.get('/graphql', {'query': '{ human { raises } }'})
json_response = format_response(response)
assert json_response['errors'][0]['message'] == 'This field should raise exception'
assert json_response['data']['human']['raises'] is None
def test_client_post_good_query_json(settings, client):
settings.ROOT_URLCONF = 'graphene_django.tests.urls'
response = client.post(
'/graphql', json.dumps({'query': '{ human { headline } }'}), 'application/json')
json_response = format_response(response)
expected_json = {
'data': {
'human': {
'headline': None
}
}
}
assert json_response == expected_json
def test_client_post_good_query_graphql(settings, client):
settings.ROOT_URLCONF = 'graphene_django.tests.urls'
response = client.post(
'/graphql', '{ human { headline } }', 'application/graphql')
json_response = format_response(response)
expected_json = {
'data': {
'human': {
'headline': None
}
}
}
assert json_response == expected_json

View File

@ -0,0 +1,8 @@
from django.conf.urls import url
from ..views import GraphQLView
from .schema import schema
urlpatterns = [
url(r'^graphql', GraphQLView.as_view(schema=schema)),
]

114
graphene_django/types.py Normal file
View File

@ -0,0 +1,114 @@
from collections import OrderedDict
import six
from graphene import ObjectType, Field
from graphene.types.objecttype import ObjectTypeMeta
from .converter import convert_django_field_with_choices
from graphene.types.options import Options
from .utils import get_model_fields, is_valid_django_model, DJANGO_FILTER_INSTALLED
from .registry import Registry, get_global_registry
from graphene.utils.is_base_type import is_base_type
from graphene.types.utils import yank_fields_from_attrs, merge
def construct_fields(options):
_model_fields = get_model_fields(options.model)
only_fields = options.only_fields
exclude_fields = options.exclude_fields
fields = OrderedDict()
for field in _model_fields:
name = field.name
is_not_in_only = only_fields and name not in options.only_fields
is_already_created = name in options.fields
is_excluded = name in exclude_fields or is_already_created
if is_not_in_only or is_excluded:
# We skip this field if we specify only_fields and is not
# in there. Or when we exclude this field in exclude_fields
continue
converted = convert_django_field_with_choices(field, options.registry)
if not converted:
continue
fields[name] = converted
return fields
class DjangoObjectTypeMeta(ObjectTypeMeta):
@staticmethod
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of
# DjangoObjectType
if not is_base_type(bases, DjangoObjectTypeMeta):
return type.__new__(cls, name, bases, attrs)
defaults = dict(
name=name,
description=attrs.pop('__doc__', None),
model=None,
local_fields=None,
only_fields=(),
exclude_fields=(),
interfaces=(),
registry=None
)
if DJANGO_FILTER_INSTALLED:
# In case Django filter is available, then
# we allow more attributes in Meta
defaults.update(
filter_fields=(),
filter_order_by=(),
)
options = Options(
attrs.pop('Meta', None),
**defaults
)
if not options.registry:
options.registry = get_global_registry()
assert isinstance(options.registry, Registry), (
'The attribute registry in {}.Meta needs to be an instance of '
'Registry, received "{}".'
).format(name, options.registry)
assert is_valid_django_model(options.model), (
'You need to pass a valid Django Model in {}.Meta, received "{}".'
).format(name, options.model)
cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options))
options.registry.register(cls)
options.django_fields = yank_fields_from_attrs(
construct_fields(options),
_as=Field,
)
options.fields = merge(
options.interface_fields,
options.django_fields,
options.base_fields,
options.local_fields
)
return cls
class DjangoObjectType(six.with_metaclass(DjangoObjectTypeMeta, ObjectType)):
@classmethod
def is_type_of(cls, root, context, info):
if isinstance(root, cls):
return True
if not is_valid_django_model(type(root)):
raise Exception((
'Received incompatible instance "{}".'
).format(root))
model = root._meta.model
return model == cls._meta.model
@classmethod
def get_node(cls, id, context, info):
try:
return cls._meta.model.objects.get(id=id)
except cls._meta.model.DoesNotExist:
return None

81
graphene_django/utils.py Normal file
View File

@ -0,0 +1,81 @@
import inspect
from django.db import models
from django.db.models.manager import Manager
# from graphene.utils import LazyList
class LazyList(object):
pass
from .compat import RelatedObject
try:
import django_filters # noqa
DJANGO_FILTER_INSTALLED = True
except (ImportError, AttributeError):
# AtributeError raised if DjangoFilters installed with a incompatible Django Version
DJANGO_FILTER_INSTALLED = False
def get_reverse_fields(model):
for name, attr in model.__dict__.items():
# Django =>1.9 uses 'rel', django <1.9 uses 'related'
related = getattr(attr, 'rel', None) or \
getattr(attr, 'related', None)
if isinstance(related, RelatedObject):
# Hack for making it compatible with Django 1.6
new_related = RelatedObject(related.parent_model, related.model, related.field)
new_related.name = name
yield new_related
elif isinstance(related, models.ManyToOneRel):
yield related
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
yield related
def maybe_queryset(value):
if isinstance(value, Manager):
value = value.get_queryset()
return value
def get_model_fields(model):
reverse_fields = get_reverse_fields(model)
all_fields = sorted(list(model._meta.fields) +
list(model._meta.local_many_to_many))
all_fields += list(reverse_fields)
return all_fields
def get_related_model(field):
if hasattr(field, 'rel'):
# Django 1.6, 1.7
return field.rel.to
return field.related_model
def is_valid_django_model(model):
return inspect.isclass(model) and issubclass(model, models.Model)
def import_single_dispatch():
try:
from functools import singledispatch
except ImportError:
singledispatch = None
if not singledispatch:
try:
from singledispatch import singledispatch
except ImportError:
pass
if not singledispatch:
raise Exception(
"It seems your python version does not include "
"functools.singledispatch. Please install the 'singledispatch' "
"package. More information here: "
"https://pypi.python.org/pypi/singledispatch"
)
return singledispatch

9
graphene_django/views.py Normal file
View File

@ -0,0 +1,9 @@
from graphql_django_view import GraphQLView as BaseGraphQLView
class GraphQLView(BaseGraphQLView):
def __init__(self, schema, **kwargs):
super(GraphQLView, self).__init__(
schema=schema,
**kwargs
)

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[tool:pytest]
DJANGO_SETTINGS_MODULE = django_test_settings

50
setup.py Normal file
View File

@ -0,0 +1,50 @@
from setuptools import find_packages, setup
setup(
name='graphene-django',
version='1.0.dev20160910000001',
description='Graphene Django integration',
# long_description=open('README.rst').read(),
url='https://github.com/graphql-python/graphene-django',
author='Syrus Akbary',
author_email='me@syrusakbary.com',
license='MIT',
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: Implementation :: PyPy',
],
keywords='api graphql protocol rest relay graphene',
packages=find_packages(exclude=['tests']),
install_requires=[
'six>=1.10.0',
'graphene>=1.0.dev',
'Django>=1.6.0',
'iso8601',
'singledispatch>=3.4.0.3',
'graphql-django-view>=1.3',
],
tests_require=[
'django-filter>=0.10.0',
'pytest>=3.0',
'pytest-django',
'mock',
# Required for Django postgres fields testing
'psycopg2',
],
)