From 44e9b0d0c584fde3028acdf132b4e70de7e9e7f1 Mon Sep 17 00:00:00 2001 From: Mel van Londen Date: Mon, 10 Jun 2019 11:08:41 -0700 Subject: [PATCH 1/6] Add stale bot (#661) --- .github/stale.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..dc90e5a --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false From 775d2e35233f14123cea127dbd7b79d53e8e4420 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Mon, 10 Jun 2019 20:54:30 -0700 Subject: [PATCH 2/6] Update travis and tox (#667) * Update travis and tox * Use xenial distribution * Don't install coveralls twice * Add black and flake8 tox commands * Remove Python 3.5 test for Django master * Fix indent * Ignore migrations * Remove black for now * Run black formatting (#668) * Run black format * Update makefile * Add black to travis build --- .travis.yml | 80 +++++++-------- Makefile | 4 +- .../cookbook/ingredients/admin.py | 4 +- .../cookbook/ingredients/apps.py | 6 +- .../cookbook/ingredients/models.py | 7 +- .../cookbook/ingredients/schema.py | 12 +-- .../cookbook/ingredients/tests.py | 1 - .../cookbook/ingredients/views.py | 1 - .../cookbook-plain/cookbook/recipes/apps.py | 6 +- .../cookbook-plain/cookbook/recipes/models.py | 22 +++-- .../cookbook-plain/cookbook/recipes/schema.py | 9 +- .../cookbook-plain/cookbook/recipes/tests.py | 1 - .../cookbook-plain/cookbook/recipes/views.py | 1 - examples/cookbook-plain/cookbook/schema.py | 10 +- examples/cookbook-plain/cookbook/settings.py | 95 ++++++++---------- examples/cookbook-plain/cookbook/urls.py | 4 +- .../cookbook/cookbook/ingredients/admin.py | 4 +- .../cookbook/cookbook/ingredients/apps.py | 6 +- .../cookbook/cookbook/ingredients/models.py | 2 +- .../cookbook/cookbook/ingredients/schema.py | 16 ++- .../cookbook/cookbook/ingredients/tests.py | 1 - .../cookbook/cookbook/ingredients/views.py | 1 - examples/cookbook/cookbook/recipes/apps.py | 6 +- examples/cookbook/cookbook/recipes/models.py | 19 ++-- examples/cookbook/cookbook/recipes/schema.py | 15 ++- examples/cookbook/cookbook/recipes/tests.py | 1 - examples/cookbook/cookbook/recipes/views.py | 1 - examples/cookbook/cookbook/schema.py | 10 +- examples/cookbook/cookbook/settings.py | 97 +++++++++---------- examples/cookbook/cookbook/urls.py | 4 +- examples/starwars/data.py | 73 +++----------- examples/starwars/models.py | 10 +- examples/starwars/schema.py | 17 ++-- examples/starwars/tests/test_connections.py | 35 ++----- examples/starwars/tests/test_mutation.py | 58 +++-------- .../tests/test_objectidentification.py | 51 +++------- graphene_django/converter.py | 6 +- graphene_django/filter/fields.py | 5 +- graphene_django/filter/tests/test_fields.py | 1 + .../rest_framework/tests/test_mutation.py | 8 +- graphene_django/tests/test_query.py | 4 +- graphene_django/types.py | 10 +- tox.ini | 46 +++++---- 43 files changed, 331 insertions(+), 439 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5c4725f..871d4e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,58 +1,58 @@ language: python -sudo: required +cache: pip dist: xenial -python: - - 2.7 - - 3.4 - - 3.5 - - 3.6 - - 3.7 - -env: - matrix: - - DJANGO=1.11 - - DJANGO=2.1 - - DJANGO=2.2 - - DJANGO=master - install: - - TOX_ENV=py${TRAVIS_PYTHON_VERSION}-django${DJANGO} - - pip install tox - - tox -e $TOX_ENV --notest -script: - - tox -e $TOX_ENV + - pip install tox tox-travis -after_success: - - tox -e $TOX_ENV -- pip install coveralls - - tox -e $TOX_ENV -- coveralls $COVERALLS_OPTION +script: + - tox + +after_success: + - pip install coveralls + - coveralls matrix: fast_finish: true include: - - python: 3.5 - script: tox -e lint - exclude: - python: 2.7 - env: DJANGO=2.1 - - python: 2.7 - env: DJANGO=2.2 - - python: 2.7 - env: DJANGO=master - - python: 3.4 - env: DJANGO=2.1 - - python: 3.4 - env: DJANGO=2.2 - - python: 3.4 - env: DJANGO=master + env: DJANGO=1.11 + - python: 3.5 + env: DJANGO=1.11 + - python: 3.5 + env: DJANGO=2.0 + - python: 3.5 + env: DJANGO=2.1 + - python: 3.5 + env: DJANGO=2.2 + + - python: 3.6 + env: DJANGO=1.11 + - python: 3.6 + env: DJANGO=2.0 + - python: 3.6 + env: DJANGO=2.1 + - python: 3.6 + env: DJANGO=2.2 + - python: 3.6 env: DJANGO=master - - python: 3.7 - env: DJANGO=1.10 + - python: 3.7 env: DJANGO=1.11 - allow_failures: - python: 3.7 + env: DJANGO=2.0 + - python: 3.7 + env: DJANGO=2.1 + - python: 3.7 + env: DJANGO=2.2 + - python: 3.7 + env: DJANGO=master + + - python: 3.7 + env: TOXENV=black,flake8 + + allow_failures: - env: DJANGO=master deploy: diff --git a/Makefile b/Makefile index 061ad4e..70badcb 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ tests: py.test graphene_django --cov=graphene_django -vv format: - black graphene_django + black --exclude "/migrations/" graphene_django examples lint: - flake8 graphene_django + flake8 graphene_django examples diff --git a/examples/cookbook-plain/cookbook/ingredients/admin.py b/examples/cookbook-plain/cookbook/ingredients/admin.py index b57cbc3..042682f 100644 --- a/examples/cookbook-plain/cookbook/ingredients/admin.py +++ b/examples/cookbook-plain/cookbook/ingredients/admin.py @@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient @admin.register(Ingredient) class IngredientAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'category') - list_editable = ('name', 'category') + list_display = ("id", "name", "category") + list_editable = ("name", "category") admin.site.register(Category) diff --git a/examples/cookbook-plain/cookbook/ingredients/apps.py b/examples/cookbook-plain/cookbook/ingredients/apps.py index 21b4b08..3ad0143 100644 --- a/examples/cookbook-plain/cookbook/ingredients/apps.py +++ b/examples/cookbook-plain/cookbook/ingredients/apps.py @@ -2,6 +2,6 @@ from django.apps import AppConfig class IngredientsConfig(AppConfig): - name = 'cookbook.ingredients' - label = 'ingredients' - verbose_name = 'Ingredients' + name = "cookbook.ingredients" + label = "ingredients" + verbose_name = "Ingredients" diff --git a/examples/cookbook-plain/cookbook/ingredients/models.py b/examples/cookbook-plain/cookbook/ingredients/models.py index 5836949..5d88785 100644 --- a/examples/cookbook-plain/cookbook/ingredients/models.py +++ b/examples/cookbook-plain/cookbook/ingredients/models.py @@ -3,7 +3,8 @@ from django.db import models class Category(models.Model): class Meta: - verbose_name_plural = 'Categories' + verbose_name_plural = "Categories" + name = models.CharField(max_length=100) def __str__(self): @@ -13,7 +14,9 @@ class Category(models.Model): class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE) + category = models.ForeignKey( + Category, related_name="ingredients", on_delete=models.CASCADE + ) def __str__(self): return self.name diff --git a/examples/cookbook-plain/cookbook/ingredients/schema.py b/examples/cookbook-plain/cookbook/ingredients/schema.py index e7ef688..1a54c4b 100644 --- a/examples/cookbook-plain/cookbook/ingredients/schema.py +++ b/examples/cookbook-plain/cookbook/ingredients/schema.py @@ -15,14 +15,12 @@ class IngredientType(DjangoObjectType): class Query(object): - category = graphene.Field(CategoryType, - id=graphene.Int(), - name=graphene.String()) + category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) all_categories = graphene.List(CategoryType) - ingredient = graphene.Field(IngredientType, - id=graphene.Int(), - name=graphene.String()) + ingredient = graphene.Field( + IngredientType, id=graphene.Int(), name=graphene.String() + ) all_ingredients = graphene.List(IngredientType) def resolve_all_categories(self, context): @@ -30,7 +28,7 @@ class Query(object): def resolve_all_ingredients(self, context): # We can easily optimize query count in the resolve method - return Ingredient.objects.select_related('category').all() + return Ingredient.objects.select_related("category").all() def resolve_category(self, context, id=None, name=None): if id is not None: diff --git a/examples/cookbook-plain/cookbook/ingredients/tests.py b/examples/cookbook-plain/cookbook/ingredients/tests.py index 4929020..a39b155 100644 --- a/examples/cookbook-plain/cookbook/ingredients/tests.py +++ b/examples/cookbook-plain/cookbook/ingredients/tests.py @@ -1,2 +1 @@ - # Create your tests here. diff --git a/examples/cookbook-plain/cookbook/ingredients/views.py b/examples/cookbook-plain/cookbook/ingredients/views.py index b8e4ee0..60f00ef 100644 --- a/examples/cookbook-plain/cookbook/ingredients/views.py +++ b/examples/cookbook-plain/cookbook/ingredients/views.py @@ -1,2 +1 @@ - # Create your views here. diff --git a/examples/cookbook-plain/cookbook/recipes/apps.py b/examples/cookbook-plain/cookbook/recipes/apps.py index 1f24f13..f1e4dde 100644 --- a/examples/cookbook-plain/cookbook/recipes/apps.py +++ b/examples/cookbook-plain/cookbook/recipes/apps.py @@ -2,6 +2,6 @@ from django.apps import AppConfig class RecipesConfig(AppConfig): - name = 'cookbook.recipes' - label = 'recipes' - verbose_name = 'Recipes' + name = "cookbook.recipes" + label = "recipes" + verbose_name = "Recipes" diff --git a/examples/cookbook-plain/cookbook/recipes/models.py b/examples/cookbook-plain/cookbook/recipes/models.py index 382b88e..f6e955e 100644 --- a/examples/cookbook-plain/cookbook/recipes/models.py +++ b/examples/cookbook-plain/cookbook/recipes/models.py @@ -6,17 +6,23 @@ from ..ingredients.models import Ingredient class Recipe(models.Model): title = models.CharField(max_length=100) instructions = models.TextField() + def __str__(self): return self.title class RecipeIngredient(models.Model): - recipe = models.ForeignKey(Recipe, related_name='amounts', on_delete=models.CASCADE) - ingredient = models.ForeignKey(Ingredient, related_name='used_by', on_delete=models.CASCADE) + recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE) + ingredient = models.ForeignKey( + Ingredient, related_name="used_by", on_delete=models.CASCADE + ) amount = models.FloatField() - unit = models.CharField(max_length=20, choices=( - ('unit', 'Units'), - ('kg', 'Kilograms'), - ('l', 'Litres'), - ('st', 'Shots'), - )) + unit = models.CharField( + max_length=20, + choices=( + ("unit", "Units"), + ("kg", "Kilograms"), + ("l", "Litres"), + ("st", "Shots"), + ), + ) diff --git a/examples/cookbook-plain/cookbook/recipes/schema.py b/examples/cookbook-plain/cookbook/recipes/schema.py index 74692f8..b029570 100644 --- a/examples/cookbook-plain/cookbook/recipes/schema.py +++ b/examples/cookbook-plain/cookbook/recipes/schema.py @@ -15,13 +15,10 @@ class RecipeIngredientType(DjangoObjectType): class Query(object): - recipe = graphene.Field(RecipeType, - id=graphene.Int(), - title=graphene.String()) + recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String()) all_recipes = graphene.List(RecipeType) - recipeingredient = graphene.Field(RecipeIngredientType, - id=graphene.Int()) + recipeingredient = graphene.Field(RecipeIngredientType, id=graphene.Int()) all_recipeingredients = graphene.List(RecipeIngredientType) def resolve_recipe(self, context, id=None, title=None): @@ -43,5 +40,5 @@ class Query(object): return Recipe.objects.all() def resolve_all_recipeingredients(self, context): - related = ['recipe', 'ingredient'] + related = ["recipe", "ingredient"] return RecipeIngredient.objects.select_related(*related).all() diff --git a/examples/cookbook-plain/cookbook/recipes/tests.py b/examples/cookbook-plain/cookbook/recipes/tests.py index 4929020..a39b155 100644 --- a/examples/cookbook-plain/cookbook/recipes/tests.py +++ b/examples/cookbook-plain/cookbook/recipes/tests.py @@ -1,2 +1 @@ - # Create your tests here. diff --git a/examples/cookbook-plain/cookbook/recipes/views.py b/examples/cookbook-plain/cookbook/recipes/views.py index b8e4ee0..60f00ef 100644 --- a/examples/cookbook-plain/cookbook/recipes/views.py +++ b/examples/cookbook-plain/cookbook/recipes/views.py @@ -1,2 +1 @@ - # Create your views here. diff --git a/examples/cookbook-plain/cookbook/schema.py b/examples/cookbook-plain/cookbook/schema.py index f91d62c..bde9372 100644 --- a/examples/cookbook-plain/cookbook/schema.py +++ b/examples/cookbook-plain/cookbook/schema.py @@ -5,10 +5,12 @@ import graphene from graphene_django.debug import DjangoDebug -class Query(cookbook.ingredients.schema.Query, - cookbook.recipes.schema.Query, - graphene.ObjectType): - debug = graphene.Field(DjangoDebug, name='_debug') +class Query( + cookbook.ingredients.schema.Query, + cookbook.recipes.schema.Query, + graphene.ObjectType, +): + debug = graphene.Field(DjangoDebug, name="_debug") schema = graphene.Schema(query=Query) diff --git a/examples/cookbook-plain/cookbook/settings.py b/examples/cookbook-plain/cookbook/settings.py index bce2bab..7eb9d56 100644 --- a/examples/cookbook-plain/cookbook/settings.py +++ b/examples/cookbook-plain/cookbook/settings.py @@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 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' +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 @@ -32,64 +32,61 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'graphene_django', - - 'cookbook.ingredients.apps.IngredientsConfig', - 'cookbook.recipes.apps.RecipesConfig', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "graphene_django", + "cookbook.ingredients.apps.IngredientsConfig", + "cookbook.recipes.apps.RecipesConfig", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] GRAPHENE = { - 'SCHEMA': 'cookbook.schema.schema', - 'SCHEMA_INDENT': 2, - 'MIDDLEWARE': ( - 'graphene_django.debug.DjangoDebugMiddleware', - ) + "SCHEMA": "cookbook.schema.schema", + "SCHEMA_INDENT": 2, + "MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",), } -ROOT_URLCONF = 'cookbook.urls' +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', - ], + "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' +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'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -99,26 +96,20 @@ DATABASES = { 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', + "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' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -130,4 +121,4 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" diff --git a/examples/cookbook-plain/cookbook/urls.py b/examples/cookbook-plain/cookbook/urls.py index 4f87da0..a64a875 100644 --- a/examples/cookbook-plain/cookbook/urls.py +++ b/examples/cookbook-plain/cookbook/urls.py @@ -5,6 +5,6 @@ from graphene_django.views import GraphQLView urlpatterns = [ - path('admin/', admin.site.urls), - path('graphql/', GraphQLView.as_view(graphiql=True)), + path("admin/", admin.site.urls), + path("graphql/", GraphQLView.as_view(graphiql=True)), ] diff --git a/examples/cookbook/cookbook/ingredients/admin.py b/examples/cookbook/cookbook/ingredients/admin.py index b57cbc3..042682f 100644 --- a/examples/cookbook/cookbook/ingredients/admin.py +++ b/examples/cookbook/cookbook/ingredients/admin.py @@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient @admin.register(Ingredient) class IngredientAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'category') - list_editable = ('name', 'category') + list_display = ("id", "name", "category") + list_editable = ("name", "category") admin.site.register(Category) diff --git a/examples/cookbook/cookbook/ingredients/apps.py b/examples/cookbook/cookbook/ingredients/apps.py index 21b4b08..3ad0143 100644 --- a/examples/cookbook/cookbook/ingredients/apps.py +++ b/examples/cookbook/cookbook/ingredients/apps.py @@ -2,6 +2,6 @@ from django.apps import AppConfig class IngredientsConfig(AppConfig): - name = 'cookbook.ingredients' - label = 'ingredients' - verbose_name = 'Ingredients' + name = "cookbook.ingredients" + label = "ingredients" + verbose_name = "Ingredients" diff --git a/examples/cookbook/cookbook/ingredients/models.py b/examples/cookbook/cookbook/ingredients/models.py index 2f0eba3..6426dab 100644 --- a/examples/cookbook/cookbook/ingredients/models.py +++ b/examples/cookbook/cookbook/ingredients/models.py @@ -11,7 +11,7 @@ class Category(models.Model): class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, related_name='ingredients') + category = models.ForeignKey(Category, related_name="ingredients") def __str__(self): return self.name diff --git a/examples/cookbook/cookbook/ingredients/schema.py b/examples/cookbook/cookbook/ingredients/schema.py index 5ad92e8..5e5da80 100644 --- a/examples/cookbook/cookbook/ingredients/schema.py +++ b/examples/cookbook/cookbook/ingredients/schema.py @@ -7,24 +7,22 @@ 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'] + interfaces = (Node,) + filter_fields = ["name", "ingredients"] class IngredientNode(DjangoObjectType): - class Meta: model = Ingredient # Allow for some more advanced filtering here - interfaces = (Node, ) + interfaces = (Node,) filter_fields = { - 'name': ['exact', 'icontains', 'istartswith'], - 'notes': ['exact', 'icontains'], - 'category': ['exact'], - 'category__name': ['exact'], + "name": ["exact", "icontains", "istartswith"], + "notes": ["exact", "icontains"], + "category": ["exact"], + "category__name": ["exact"], } diff --git a/examples/cookbook/cookbook/ingredients/tests.py b/examples/cookbook/cookbook/ingredients/tests.py index 4929020..a39b155 100644 --- a/examples/cookbook/cookbook/ingredients/tests.py +++ b/examples/cookbook/cookbook/ingredients/tests.py @@ -1,2 +1 @@ - # Create your tests here. diff --git a/examples/cookbook/cookbook/ingredients/views.py b/examples/cookbook/cookbook/ingredients/views.py index b8e4ee0..60f00ef 100644 --- a/examples/cookbook/cookbook/ingredients/views.py +++ b/examples/cookbook/cookbook/ingredients/views.py @@ -1,2 +1 @@ - # Create your views here. diff --git a/examples/cookbook/cookbook/recipes/apps.py b/examples/cookbook/cookbook/recipes/apps.py index 1f24f13..f1e4dde 100644 --- a/examples/cookbook/cookbook/recipes/apps.py +++ b/examples/cookbook/cookbook/recipes/apps.py @@ -2,6 +2,6 @@ from django.apps import AppConfig class RecipesConfig(AppConfig): - name = 'cookbook.recipes' - label = 'recipes' - verbose_name = 'Recipes' + name = "cookbook.recipes" + label = "recipes" + verbose_name = "Recipes" diff --git a/examples/cookbook/cookbook/recipes/models.py b/examples/cookbook/cookbook/recipes/models.py index ca12fac..b98664c 100644 --- a/examples/cookbook/cookbook/recipes/models.py +++ b/examples/cookbook/cookbook/recipes/models.py @@ -10,12 +10,15 @@ class Recipe(models.Model): class RecipeIngredient(models.Model): - recipe = models.ForeignKey(Recipe, related_name='amounts') - ingredient = models.ForeignKey(Ingredient, related_name='used_by') + recipe = models.ForeignKey(Recipe, related_name="amounts") + ingredient = models.ForeignKey(Ingredient, related_name="used_by") amount = models.FloatField() - unit = models.CharField(max_length=20, choices=( - ('unit', 'Units'), - ('kg', 'Kilograms'), - ('l', 'Litres'), - ('st', 'Shots'), - )) + unit = models.CharField( + max_length=20, + choices=( + ("unit", "Units"), + ("kg", "Kilograms"), + ("l", "Litres"), + ("st", "Shots"), + ), + ) diff --git a/examples/cookbook/cookbook/recipes/schema.py b/examples/cookbook/cookbook/recipes/schema.py index 8018322..fbbedd8 100644 --- a/examples/cookbook/cookbook/recipes/schema.py +++ b/examples/cookbook/cookbook/recipes/schema.py @@ -3,24 +3,23 @@ from graphene import Node from graphene_django.filter import DjangoFilterConnectionField from graphene_django.types import DjangoObjectType -class RecipeNode(DjangoObjectType): +class RecipeNode(DjangoObjectType): class Meta: model = Recipe - interfaces = (Node, ) - filter_fields = ['title','amounts'] + interfaces = (Node,) + filter_fields = ["title", "amounts"] class RecipeIngredientNode(DjangoObjectType): - class Meta: model = RecipeIngredient # Allow for some more advanced filtering here - interfaces = (Node, ) + interfaces = (Node,) filter_fields = { - 'ingredient__name': ['exact', 'icontains', 'istartswith'], - 'recipe': ['exact'], - 'recipe__title': ['icontains'], + "ingredient__name": ["exact", "icontains", "istartswith"], + "recipe": ["exact"], + "recipe__title": ["icontains"], } diff --git a/examples/cookbook/cookbook/recipes/tests.py b/examples/cookbook/cookbook/recipes/tests.py index 4929020..a39b155 100644 --- a/examples/cookbook/cookbook/recipes/tests.py +++ b/examples/cookbook/cookbook/recipes/tests.py @@ -1,2 +1 @@ - # Create your tests here. diff --git a/examples/cookbook/cookbook/recipes/views.py b/examples/cookbook/cookbook/recipes/views.py index b8e4ee0..60f00ef 100644 --- a/examples/cookbook/cookbook/recipes/views.py +++ b/examples/cookbook/cookbook/recipes/views.py @@ -1,2 +1 @@ - # Create your views here. diff --git a/examples/cookbook/cookbook/schema.py b/examples/cookbook/cookbook/schema.py index f91d62c..bde9372 100644 --- a/examples/cookbook/cookbook/schema.py +++ b/examples/cookbook/cookbook/schema.py @@ -5,10 +5,12 @@ import graphene from graphene_django.debug import DjangoDebug -class Query(cookbook.ingredients.schema.Query, - cookbook.recipes.schema.Query, - graphene.ObjectType): - debug = graphene.Field(DjangoDebug, name='_debug') +class Query( + cookbook.ingredients.schema.Query, + cookbook.recipes.schema.Query, + graphene.ObjectType, +): + debug = graphene.Field(DjangoDebug, name="_debug") schema = graphene.Schema(query=Query) diff --git a/examples/cookbook/cookbook/settings.py b/examples/cookbook/cookbook/settings.py index 0b3207e..ed41a65 100644 --- a/examples/cookbook/cookbook/settings.py +++ b/examples/cookbook/cookbook/settings.py @@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 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' +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 @@ -32,65 +32,62 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'graphene_django', - - 'cookbook.ingredients.apps.IngredientsConfig', - 'cookbook.recipes.apps.RecipesConfig', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "graphene_django", + "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', + "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", ] GRAPHENE = { - 'SCHEMA': 'cookbook.schema.schema', - 'SCHEMA_INDENT': 2, - 'MIDDLEWARE': ( - 'graphene_django.debug.DjangoDebugMiddleware', - ) + "SCHEMA": "cookbook.schema.schema", + "SCHEMA_INDENT": 2, + "MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",), } -ROOT_URLCONF = 'cookbook.urls' +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', - ], + "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' +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'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -100,26 +97,20 @@ DATABASES = { 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', + "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' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -131,4 +122,4 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook/cookbook/urls.py index 4bf6003..6f8a302 100644 --- a/examples/cookbook/cookbook/urls.py +++ b/examples/cookbook/cookbook/urls.py @@ -5,6 +5,6 @@ from graphene_django.views import GraphQLView urlpatterns = [ - url(r'^admin/', admin.site.urls), - url(r'^graphql$', GraphQLView.as_view(graphiql=True)), + url(r"^admin/", admin.site.urls), + url(r"^graphql$", GraphQLView.as_view(graphiql=True)), ] diff --git a/examples/starwars/data.py b/examples/starwars/data.py index 9b52006..6bdbf57 100644 --- a/examples/starwars/data.py +++ b/examples/starwars/data.py @@ -2,97 +2,50 @@ from .models import Character, Faction, Ship def initialize(): - human = Character( - name='Human' - ) + human = Character(name="Human") human.save() - droid = Character( - name='Droid' - ) + droid = Character(name="Droid") droid.save() - rebels = Faction( - id='1', - name='Alliance to Restore the Republic', - hero=human - ) + rebels = Faction(id="1", name="Alliance to Restore the Republic", hero=human) rebels.save() - empire = Faction( - id='2', - name='Galactic Empire', - hero=droid - ) + empire = Faction(id="2", name="Galactic Empire", hero=droid) empire.save() - xwing = Ship( - id='1', - name='X-Wing', - faction=rebels, - ) + xwing = Ship(id="1", name="X-Wing", faction=rebels) xwing.save() human.ship = xwing human.save() - ywing = Ship( - id='2', - name='Y-Wing', - faction=rebels, - ) + ywing = Ship(id="2", name="Y-Wing", faction=rebels) ywing.save() - awing = Ship( - id='3', - name='A-Wing', - faction=rebels, - ) + 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 = Ship(id="4", name="Millenium Falcon", faction=rebels) falcon.save() - homeOne = Ship( - id='5', - name='Home One', - faction=rebels, - ) + homeOne = Ship(id="5", name="Home One", faction=rebels) homeOne.save() - tieFighter = Ship( - id='6', - name='TIE Fighter', - faction=empire, - ) + tieFighter = Ship(id="6", name="TIE Fighter", faction=empire) tieFighter.save() - tieInterceptor = Ship( - id='7', - name='TIE Interceptor', - faction=empire, - ) + tieInterceptor = Ship(id="7", name="TIE Interceptor", faction=empire) tieInterceptor.save() - executor = Ship( - id='8', - name='Executor', - faction=empire, - ) + 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 = Ship(name=ship_name, faction_id=faction_id) new_ship.save() return new_ship diff --git a/examples/starwars/models.py b/examples/starwars/models.py index 45741da..03e06a2 100644 --- a/examples/starwars/models.py +++ b/examples/starwars/models.py @@ -5,7 +5,13 @@ from django.db import models class Character(models.Model): name = models.CharField(max_length=50) - ship = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True, related_name='characters') + ship = models.ForeignKey( + "Ship", + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="characters", + ) def __str__(self): return self.name @@ -21,7 +27,7 @@ class Faction(models.Model): class Ship(models.Model): name = models.CharField(max_length=50) - faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name='ships') + faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships") def __str__(self): return self.name diff --git a/examples/starwars/schema.py b/examples/starwars/schema.py index 492918e..fb22840 100644 --- a/examples/starwars/schema.py +++ b/examples/starwars/schema.py @@ -2,18 +2,16 @@ import graphene from graphene import Schema, relay, resolve_only_args from graphene_django import DjangoConnectionField, DjangoObjectType -from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship, - get_ships) +from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships from .models import Character as CharacterModel from .models import Faction as FactionModel from .models import Ship as ShipModel class Ship(DjangoObjectType): - class Meta: model = ShipModel - interfaces = (relay.Node, ) + interfaces = (relay.Node,) @classmethod def get_node(cls, info, id): @@ -22,16 +20,14 @@ class Ship(DjangoObjectType): class Character(DjangoObjectType): - class Meta: model = CharacterModel class Faction(DjangoObjectType): - class Meta: model = FactionModel - interfaces = (relay.Node, ) + interfaces = (relay.Node,) @classmethod def get_node(cls, info, id): @@ -39,7 +35,6 @@ class Faction(DjangoObjectType): class IntroduceShip(relay.ClientIDMutation): - class Input: ship_name = graphene.String(required=True) faction_id = graphene.String(required=True) @@ -48,7 +43,9 @@ class IntroduceShip(relay.ClientIDMutation): faction = graphene.Field(Faction) @classmethod - def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None): + def mutate_and_get_payload( + cls, root, info, ship_name, faction_id, client_mutation_id=None + ): ship = create_ship(ship_name, faction_id) faction = get_faction(faction_id) return IntroduceShip(ship=ship, faction=faction) @@ -58,7 +55,7 @@ class Query(graphene.ObjectType): rebels = graphene.Field(Faction) empire = graphene.Field(Faction) node = relay.Node.Field() - ships = DjangoConnectionField(Ship, description='All the ships.') + ships = DjangoConnectionField(Ship, description="All the ships.") @resolve_only_args def resolve_ships(self): diff --git a/examples/starwars/tests/test_connections.py b/examples/starwars/tests/test_connections.py index d266df3..425dce5 100644 --- a/examples/starwars/tests/test_connections.py +++ b/examples/starwars/tests/test_connections.py @@ -8,7 +8,7 @@ pytestmark = pytest.mark.django_db def test_correct_fetch_first_ship_rebels(): initialize() - query = ''' + query = """ query RebelsShipsQuery { rebels { name, @@ -24,22 +24,12 @@ def test_correct_fetch_first_ship_rebels(): } } } - ''' + """ expected = { - 'rebels': { - 'name': 'Alliance to Restore the Republic', - 'hero': { - 'name': 'Human' - }, - 'ships': { - 'edges': [ - { - 'node': { - 'name': 'X-Wing' - } - } - ] - } + "rebels": { + "name": "Alliance to Restore the Republic", + "hero": {"name": "Human"}, + "ships": {"edges": [{"node": {"name": "X-Wing"}}]}, } } result = schema.execute(query) @@ -49,7 +39,7 @@ def test_correct_fetch_first_ship_rebels(): def test_correct_list_characters(): initialize() - query = ''' + query = """ query RebelsShipsQuery { node(id: "U2hpcDox") { ... on Ship { @@ -60,15 +50,8 @@ def test_correct_list_characters(): } } } - ''' - expected = { - 'node': { - 'name': 'X-Wing', - 'characters': [{ - 'name': 'Human' - }], - } - } + """ + expected = {"node": {"name": "X-Wing", "characters": [{"name": "Human"}]}} result = schema.execute(query) assert not result.errors assert result.data == expected diff --git a/examples/starwars/tests/test_mutation.py b/examples/starwars/tests/test_mutation.py index aa312ff..e24bf8a 100644 --- a/examples/starwars/tests/test_mutation.py +++ b/examples/starwars/tests/test_mutation.py @@ -9,7 +9,7 @@ pytestmark = pytest.mark.django_db def test_mutations(): initialize() - query = ''' + query = """ mutation MyMutation { introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) { ship { @@ -29,49 +29,23 @@ def test_mutations(): } } } - ''' + """ 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' - } - }] + "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) diff --git a/examples/starwars/tests/test_objectidentification.py b/examples/starwars/tests/test_objectidentification.py index fad1958..6e04a7b 100644 --- a/examples/starwars/tests/test_objectidentification.py +++ b/examples/starwars/tests/test_objectidentification.py @@ -8,19 +8,16 @@ pytestmark = pytest.mark.django_db def test_correctly_fetches_id_name_rebels(): initialize() - query = ''' + query = """ query RebelsQuery { rebels { id name } } - ''' + """ expected = { - 'rebels': { - 'id': 'RmFjdGlvbjox', - 'name': 'Alliance to Restore the Republic' - } + "rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"} } result = schema.execute(query) assert not result.errors @@ -29,7 +26,7 @@ def test_correctly_fetches_id_name_rebels(): def test_correctly_refetches_rebels(): initialize() - query = ''' + query = """ query RebelsRefetchQuery { node(id: "RmFjdGlvbjox") { id @@ -38,12 +35,9 @@ def test_correctly_refetches_rebels(): } } } - ''' + """ expected = { - 'node': { - 'id': 'RmFjdGlvbjox', - 'name': 'Alliance to Restore the Republic' - } + "node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"} } result = schema.execute(query) assert not result.errors @@ -52,20 +46,15 @@ def test_correctly_refetches_rebels(): def test_correctly_fetches_id_name_empire(): initialize() - query = ''' + query = """ query EmpireQuery { empire { id name } } - ''' - expected = { - 'empire': { - 'id': 'RmFjdGlvbjoy', - 'name': 'Galactic Empire' - } - } + """ + expected = {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}} result = schema.execute(query) assert not result.errors assert result.data == expected @@ -73,7 +62,7 @@ def test_correctly_fetches_id_name_empire(): def test_correctly_refetches_empire(): initialize() - query = ''' + query = """ query EmpireRefetchQuery { node(id: "RmFjdGlvbjoy") { id @@ -82,13 +71,8 @@ def test_correctly_refetches_empire(): } } } - ''' - expected = { - 'node': { - 'id': 'RmFjdGlvbjoy', - 'name': 'Galactic Empire' - } - } + """ + expected = {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}} result = schema.execute(query) assert not result.errors assert result.data == expected @@ -96,7 +80,7 @@ def test_correctly_refetches_empire(): def test_correctly_refetches_xwing(): initialize() - query = ''' + query = """ query XWingRefetchQuery { node(id: "U2hpcDox") { id @@ -105,13 +89,8 @@ def test_correctly_refetches_xwing(): } } } - ''' - expected = { - 'node': { - 'id': 'U2hpcDox', - 'name': 'X-Wing' - } - } + """ + expected = {"node": {"id": "U2hpcDox", "name": "X-Wing"}} result = schema.execute(query) assert not result.errors assert result.data == expected diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 158355a..1bb16f4 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -177,7 +177,11 @@ def convert_field_to_list_or_connection(field, registry=None): if not _type: return - description = field.help_text if isinstance(field, models.ManyToManyField) else field.field.help_text + description = ( + field.help_text + if isinstance(field, models.ManyToManyField) + else field.field.help_text + ) # If there is a connection, we should transform the field # into a DjangoConnectionField diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index 7c85e9a..62f4b1a 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -41,10 +41,9 @@ class DjangoFilterConnectionField(DjangoConnectionField): meta.update(self._extra_filter_meta) filterset_class = self._provided_filterset_class or ( - self.node_type._meta.filterset_class) - self._filterset_class = get_filterset_class( - filterset_class, **meta + self.node_type._meta.filterset_class ) + self._filterset_class = get_filterset_class(filterset_class, **meta) return self._filterset_class diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index eb6581b..4d8d597 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -229,6 +229,7 @@ def test_filter_filterset_information_on_meta_related(): def test_filter_filterset_class_filter_fields_exception(): with pytest.raises(Exception): + class ReporterFilter(FilterSet): class Meta: model = Reporter diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index a0c861d..9621ee3 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -104,7 +104,9 @@ def test_write_only_field(): ) assert hasattr(result, "cool_name") - assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible" + assert not hasattr( + result, "password" + ), "'password' is write_only field and shouldn't be visible" @mark.django_db @@ -124,7 +126,9 @@ def test_write_only_field_using_extra_kwargs(): ) assert hasattr(result, "cool_name") - assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible" + assert not hasattr( + result, "password" + ), "'password' is write_only field and shouldn't be visible" def test_nested_model(): diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 9ef217e..484a225 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1015,13 +1015,13 @@ def test_proxy_model_support(): "edges": [ {"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}} ] - } + }, } result = schema.execute(query) assert not result.errors assert result.data == expected - + def test_should_resolve_get_queryset_connectionfields(): reporter_1 = Reporter.objects.create( diff --git a/graphene_django/types.py b/graphene_django/types.py index ded8a15..a1e17b3 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -82,10 +82,12 @@ class DjangoObjectType(ObjectType): raise Exception("Can't set both filter_fields and filterset_class") if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class): - raise Exception(( - "Can only set filter_fields or filterset_class if " - "Django-Filter is installed" - )) + raise Exception( + ( + "Can only set filter_fields or filterset_class if " + "Django-Filter is installed" + ) + ) django_fields = yank_fields_from_attrs( construct_fields(model, registry, only_fields, exclude_fields), _as=Field diff --git a/tox.ini b/tox.ini index 8e21c74..58f283a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,31 +1,39 @@ [tox] -envlist = py{2.7,3.4,3.5,3.6,3.7,pypy,pypy3}-django{1.10,1.11,2.0,2.1,2.2,master},lint +envlist = + py{27,35,36,37}-django{111,20,21,22,master}, + black,flake8 + +[travis:env] +DJANGO = + 1.11: django111 + 2.0: django20 + 2.1: django21 + 2.2: django22 + master: djangomaster [testenv] passenv = * usedevelop = True -setenv = +setenv = DJANGO_SETTINGS_MODULE=django_test_settings -basepython = - py2.7: python2.7 - py3.4: python3.4 - py3.5: python3.5 - py3.6: python3.6 - py3.7: python3.7 - pypypy: pypy - pypypy3: pypy3 deps = -e.[test] psycopg2 - django1.10: Django>=1.10,<1.11 - django1.11: Django>=1.11,<1.12 - django2.0: Django>=2.0 - django2.1: Django>=2.1 + django111: Django>=1.11,<2.0 + django20: Django>=2.0,<2.1 + django21: Django>=2.1,<2.2 + django22: Django>=2.2,<3.0 djangomaster: https://github.com/django/django/archive/master.zip commands = {posargs:py.test --cov=graphene_django graphene_django examples} -[testenv:lint] -basepython = python -deps = - prospector -commands = prospector graphene_django -0 +[testenv:black] +basepython = python3.7 +deps = black +commands = + black --exclude "/migrations/" graphene_django examples --check + +[testenv:flake8] +basepython = python3.7 +deps = flake8 +commands = + flake8 graphene_django examples From 6e8dce95ae184cc3f8c3c9202ba92f8fd92c09d1 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Fri, 14 Jun 2019 12:33:37 +0100 Subject: [PATCH 3/6] Update doc setup (#673) * Expose doc commands in root makefile and add autobuild * Fix some errors * Alias some commands and add PHONY --- Makefile | 18 ++++++++++++++++++ docs/Makefile | 8 ++++++++ docs/_static/.gitkeep | 0 docs/authorization.rst | 3 ++- docs/requirements.txt | 3 ++- docs/settings.rst | 10 +++++----- 6 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 docs/_static/.gitkeep diff --git a/Makefile b/Makefile index 70badcb..39a0f31 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,29 @@ +.PHONY: dev-setup ## Install development dependencies dev-setup: pip install -e ".[dev]" +.PHONY: install-dev +install-dev: dev-setup # Alias install-dev -> dev-setup + +.PHONY: tests tests: py.test graphene_django --cov=graphene_django -vv +.PHONY: test +test: tests # Alias test -> tests + +.PHONY: format format: black --exclude "/migrations/" graphene_django examples +.PHONY: lint lint: flake8 graphene_django examples + +.PHONY: docs ## Generate docs +docs: dev-setup + cd docs && make install && make html + +.PHONY: docs-live ## Generate docs with live reloading +docs-live: dev-setup + cd docs && make install && make livehtml diff --git a/docs/Makefile b/docs/Makefile index 7da67c3..4ae2962 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -48,12 +48,20 @@ help: clean: rm -rf $(BUILDDIR)/* +.PHONY: install ## to install all documentation related requirements +install: + pip install -r requirements.txt + .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." +.PHONY: livehtml ## to build and serve live-reloading documentation +livehtml: + sphinx-autobuild -b html --watch ../graphene_django $(ALLSPHINXOPTS) $(BUILDDIR)/html + .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/authorization.rst b/docs/authorization.rst index 3d0bb8a..2c38fa4 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -154,7 +154,8 @@ Adding Login Required To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.: .. code:: python - #views.py + + # views.py from django.contrib.auth.mixins import LoginRequiredMixin from graphene_django.views import GraphQLView diff --git a/docs/requirements.txt b/docs/requirements.txt index 220b7cf..7c89926 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ -sphinx +Sphinx==1.5.3 +sphinx-autobuild==0.7.1 # Docs template http://graphene-python.org/sphinx_graphene_theme.zip diff --git a/docs/settings.rst b/docs/settings.rst index 547e77f..4d37a99 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -30,7 +30,7 @@ Default: ``None`` ``SCHEMA_OUTPUT`` ----------- +----------------- The name of the file where the GraphQL schema output will go. @@ -44,7 +44,7 @@ Default: ``schema.json`` ``SCHEMA_INDENT`` ----------- +----------------- The indentation level of the schema output. @@ -58,7 +58,7 @@ Default: ``2`` ``MIDDLEWARE`` ----------- +-------------- A tuple of middleware that will be executed for each GraphQL query. @@ -76,7 +76,7 @@ Default: ``()`` ``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST`` ----------- +------------------------------------------ Enforces relay queries to have the ``first`` or ``last`` argument. @@ -90,7 +90,7 @@ Default: ``False`` ``RELAY_CONNECTION_MAX_LIMIT`` ----------- +------------------------------ The maximum size of objects that can be requested through a relay connection. From 6169346776854c055f6349509e4e02d64b00863e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 17 Jun 2019 17:08:51 +0100 Subject: [PATCH 4/6] Bump django from 1.11.20 to 1.11.21 in /examples/cookbook (#670) Bumps [django](https://github.com/django/django) from 1.11.20 to 1.11.21. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/1.11.20...1.11.21) Signed-off-by: dependabot[bot] --- examples/cookbook/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index fe0527a..9d13a82 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,5 +1,5 @@ graphene graphene-django graphql-core>=2.1rc1 -django==1.11.20 +django==1.11.21 django-filter>=2 From 894b1053a2bb40e7f52601f70d65e3ebd7c51fe5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 17 Jun 2019 18:48:15 +0100 Subject: [PATCH 5/6] Bump django from 2.1.6 to 2.1.9 in /examples/cookbook-plain (#669) Bumps [django](https://github.com/django/django) from 2.1.6 to 2.1.9. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/2.1.6...2.1.9) Signed-off-by: dependabot[bot] --- examples/cookbook-plain/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt index 2154fd8..ea1f4ba 100644 --- a/examples/cookbook-plain/requirements.txt +++ b/examples/cookbook-plain/requirements.txt @@ -1,4 +1,4 @@ graphene graphene-django graphql-core>=2.1rc1 -django==2.1.6 +django==2.1.9 From 612ba5a4eaea0336a5dffcba3dbe7909b9d94646 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Mon, 17 Jun 2019 18:48:29 +0100 Subject: [PATCH 6/6] Add `convert_choices_to_enum` option on DjangoObjectType Meta class (#674) * Add convert_choices_to_enum meta option * Add tests * Run black * Update documentation * Add link to Django choices documentation * Add test and documentation note That setting to an empty list is the same as setting the value as False * Fix Django warning in tests * rst is not markdown --- docs/queries.rst | 65 ++++++++++++++ graphene_django/converter.py | 6 +- graphene_django/tests/test_converter.py | 17 ++++ graphene_django/tests/test_types.py | 115 +++++++++++++++++++++++- graphene_django/types.py | 23 ++++- 5 files changed, 220 insertions(+), 6 deletions(-) diff --git a/docs/queries.rst b/docs/queries.rst index 0edd1dd..7aff572 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -92,6 +92,71 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType return 'hello!' +Choices to Enum conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default Graphene-Django will convert any Django fields that have `choices`_ +defined into a GraphQL enum type. + +.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices + +For example the following ``Model`` and ``DjangoObjectType``: + +.. code:: python + + class PetModel(models.Model): + kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog'))) + + class Pet(DjangoObjectType): + class Meta: + model = PetModel + +Results in the following GraphQL schema definition: + +.. code:: + + type Pet { + id: ID! + kind: PetModelKind! + } + + enum PetModelKind { + CAT + DOG + } + +You can disable this automatic conversion by setting +``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType`` +``Meta`` class. + +.. code:: python + + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = False + +.. code:: + + type Pet { + id: ID! + kind: String! + } + +You can also set ``convert_choices_to_enum`` to a list of fields that should be +automatically converted into enums: + +.. code:: python + + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = ['kind'] + +**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to +``False``. + + Related models -------------- diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 1bb16f4..4d0b45f 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -52,13 +52,15 @@ def get_choices(choices): yield name, value, description -def convert_django_field_with_choices(field, registry=None): +def convert_django_field_with_choices( + field, registry=None, convert_choices_to_enum=True +): if registry is not None: converted = registry.get_converted_field(field) if converted: return converted choices = getattr(field, "choices", None) - if choices: + if choices and convert_choices_to_enum: meta = field.model._meta name = to_camel_case("{}_{}".format(meta.object_name, field.name)) choices = list(get_choices(choices)) diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index bb176b3..5542c90 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -196,6 +196,23 @@ def test_field_with_choices_collision(): convert_django_field_with_choices(field) +def test_field_with_choices_convert_enum_false(): + 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, convert_choices_to_enum=False + ) + assert isinstance(graphene_type, graphene.String) + + def test_should_float_convert_float(): assert_conversion(models.FloatField, graphene.Float) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 8a8643b..c1ac6c2 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -1,6 +1,11 @@ +from collections import OrderedDict, defaultdict +from textwrap import dedent + +import pytest +from django.db import models from mock import patch -from graphene import Interface, ObjectType, Schema, Connection, String +from graphene import Connection, Field, Interface, ObjectType, Schema, String from graphene.relay import Node from .. import registry @@ -224,3 +229,111 @@ def test_django_objecttype_exclude_fields(): fields = list(Reporter._meta.fields.keys()) assert "email" not in fields + + +class TestDjangoObjectType: + @pytest.fixture + def PetModel(self): + class PetModel(models.Model): + kind = models.CharField(choices=(("cat", "Cat"), ("dog", "Dog"))) + cuteness = models.IntegerField( + choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!")) + ) + + yield PetModel + + # Clear Django model cache so we don't get warnings when creating the + # model multiple times + PetModel._meta.apps.all_models = defaultdict(OrderedDict) + + def test_django_objecttype_convert_choices_enum_false(self, PetModel): + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = False + + class Query(ObjectType): + pet = Field(Pet) + + schema = Schema(query=Query) + + assert str(schema) == dedent( + """\ + schema { + query: Query + } + + type Pet { + id: ID! + kind: String! + cuteness: Int! + } + + type Query { + pet: Pet + } + """ + ) + + def test_django_objecttype_convert_choices_enum_list(self, PetModel): + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = ["kind"] + + class Query(ObjectType): + pet = Field(Pet) + + schema = Schema(query=Query) + + assert str(schema) == dedent( + """\ + schema { + query: Query + } + + type Pet { + id: ID! + kind: PetModelKind! + cuteness: Int! + } + + enum PetModelKind { + CAT + DOG + } + + type Query { + pet: Pet + } + """ + ) + + def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel): + class Pet(DjangoObjectType): + class Meta: + model = PetModel + convert_choices_to_enum = [] + + class Query(ObjectType): + pet = Field(Pet) + + schema = Schema(query=Query) + + assert str(schema) == dedent( + """\ + schema { + query: Query + } + + type Pet { + id: ID! + kind: String! + cuteness: Int! + } + + type Query { + pet: Pet + } + """ + ) diff --git a/graphene_django/types.py b/graphene_django/types.py index a1e17b3..005300d 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -18,7 +18,9 @@ if six.PY3: from typing import Type -def construct_fields(model, registry, only_fields, exclude_fields): +def construct_fields( + model, registry, only_fields, exclude_fields, convert_choices_to_enum +): _model_fields = get_model_fields(model) fields = OrderedDict() @@ -33,7 +35,18 @@ def construct_fields(model, registry, only_fields, exclude_fields): # in there. Or when we exclude this field in exclude_fields. # Or when there is no back reference. continue - converted = convert_django_field_with_choices(field, registry) + + _convert_choices_to_enum = convert_choices_to_enum + if not isinstance(_convert_choices_to_enum, bool): + # then `convert_choices_to_enum` is a list of field names to convert + if name in _convert_choices_to_enum: + _convert_choices_to_enum = True + else: + _convert_choices_to_enum = False + + converted = convert_django_field_with_choices( + field, registry, convert_choices_to_enum=_convert_choices_to_enum + ) fields[name] = converted return fields @@ -63,6 +76,7 @@ class DjangoObjectType(ObjectType): connection_class=None, use_connection=None, interfaces=(), + convert_choices_to_enum=True, _meta=None, **options ): @@ -90,7 +104,10 @@ class DjangoObjectType(ObjectType): ) django_fields = yank_fields_from_attrs( - construct_fields(model, registry, only_fields, exclude_fields), _as=Field + construct_fields( + model, registry, only_fields, exclude_fields, convert_choices_to_enum + ), + _as=Field, ) if use_connection is None and interfaces: