diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a71f6fe0..93ab2e6d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,28 @@ repos: - repo: git://github.com/pre-commit/pre-commit-hooks - rev: v1.3.0 + rev: v2.1.0 hooks: + - id: check-merge-conflict - id: check-json - id: check-yaml - id: debug-statements - id: end-of-file-fixer exclude: ^docs/.*$ - - id: trailing-whitespace - exclude: README.md - id: pretty-format-json args: - --autofix - - id: flake8 + - id: trailing-whitespace + exclude: README.md - repo: https://github.com/asottile/pyupgrade - rev: v1.4.0 + rev: v1.12.0 hooks: - id: pyupgrade - repo: https://github.com/ambv/black - rev: 18.6b4 + rev: 18.9b0 hooks: - id: black language_version: python3 +- repo: https://github.com/PyCQA/flake8 + rev: 3.7.7 + hooks: + - id: flake8 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..26bb1aff --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +/ @syrusakbary @ekampf @dan98765 @projectcheshire diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index 32b28d8b..d9d48005 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -17,7 +17,7 @@ developer has to write to use them. **New Features!** * [`InputObjectType`](#inputobjecttype) -* [`Meta as Class arguments`](#meta-ass-class-arguments) (_only available for Python 3_) +* [`Meta as Class arguments`](#meta-as-class-arguments) (_only available for Python 3_) > The type metaclasses are now deleted as they are no longer necessary. If your code was depending @@ -276,7 +276,7 @@ If you are using Middelwares, you need to some adjustments: Before: ```python -class MyGrapheneMiddleware(object): +class MyGrapheneMiddleware(object): def resolve(self, next_mw, root, args, context, info): ## Middleware code @@ -287,7 +287,7 @@ class MyGrapheneMiddleware(object): With 2.0: ```python -class MyGrapheneMiddleware(object): +class MyGrapheneMiddleware(object): def resolve(self, next_mw, root, info, **args): context = info.context diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index ca0d6b3f..b6eb3087 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -25,8 +25,8 @@ This example model defines a Person, with a first and a last name: last_name = graphene.String() full_name = graphene.String() - def resolve_full_name(self, info): - return '{} {}'.format(self.first_name, self.last_name) + def resolve_full_name(root, info): + return '{} {}'.format(root.first_name, root.last_name) **first\_name** and **last\_name** are fields of the ObjectType. Each field is specified as a class attribute, and each attribute maps to a @@ -56,23 +56,148 @@ NOTE: The resolvers on an ``ObjectType`` are always treated as ``staticmethod``\ so the first argument to the resolver method ``self`` (or ``root``) need not be an actual instance of the ``ObjectType``. +If an explicit resolver is not defined on the ``ObjectType`` then Graphene will +attempt to use a property with the same name on the object that is passed to the +``ObjectType``. -Quick example -~~~~~~~~~~~~~ +.. code:: python -This example model defines a ``Query`` type, which has a reverse field -that reverses the given ``word`` argument using the ``resolve_reverse`` -method in the class. + import graphene + + class Person(graphene.ObjectType): + first_name = graphene.String() + last_name = graphene.String() + + class Query(graphene.ObjectType): + me = graphene.Field(Person) + + def resolve_me(_, info): + # returns an object that represents a Person + return get_human(name='Luke Skywalker') + +If you are passing a dict instead of an object to your ``ObjectType`` you can +change the default resolver in the ``Meta`` class like this: + +.. code:: python + + import graphene + from graphene.types.resolver import dict_resolver + + class Person(graphene.ObjectType): + class Meta: + default_resolver = dict_resolver + + first_name = graphene.String() + last_name = graphene.String() + + class Query(graphene.ObjectType): + me = graphene.Field(Person) + + def resolve_me(_, info): + return { + "first_name": "Luke", + "last_name": "Skywalker", + } + +Or you can change the default resolver globally by calling ``set_default_resolver`` +before executing a query. + +.. code:: python + + import graphene + from graphene.types.resolver import dict_resolver, set_default_resolver + + set_default_resolver(dict_resolver) + + schema = graphene.Schema(query=Query) + result = schema.execute(''' + query { + me { + firstName + } + } + ''') + + +Resolvers with arguments +~~~~~~~~~~~~~~~~~~~~~~~~ + +Any arguments that a field defines gets passed to the resolver function as +kwargs. For example: .. code:: python import graphene class Query(graphene.ObjectType): - reverse = graphene.String(word=graphene.String()) + human_by_name = graphene.Field(Human, name=graphene.String(required=True)) + + def resolve_human_by_name(_, info, name): + return get_human(name=name) + +You can then execute the following query: + +.. code:: + + query { + humanByName(name: "Luke Skywalker") { + firstName + lastName + } + } + +NOTE: if you define an argument for a field that is not required (and in a query +execution it is not provided as an argument) it will not be passed to the +resolver function at all. This is so that the developer can differenciate +between a ``undefined`` value for an argument and an explicit ``null`` value. + +For example, given this schema: + +.. code:: python + + import graphene + + class Query(graphene.ObjectType): + hello = graphene.String(required=True, name=graphene.String()) + + def resolve_hello(_, info, name): + return name if name else 'World' + +And this query: + +.. code:: + + query { + hello + } + +An error will be thrown: + +.. code:: + + TypeError: resolve_hello() missing 1 required positional argument: 'name' + +You can fix this error in 2 ways. Either by combining all keyword arguments +into a dict: + +.. code:: python + + class Query(graphene.ObjectType): + hello = graphene.String(required=True, name=graphene.String()) + + def resolve_hello(_, info, **args): + return args.get('name', 'World') + +Or by setting a default value for the keyword argument: + +.. code:: python + + class Query(graphene.ObjectType): + hello = graphene.String(required=True, name=graphene.String()) + + def resolve_hello(_, info, name='World'): + return name - def resolve_reverse(self, info, word): - return word[::-1] Resolvers outside the class ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -83,11 +208,13 @@ A field can use a custom resolver from outside the class: import graphene - def reverse(root, info, word): - return word[::-1] + def resolve_full_name(person, info): + return '{} {}'.format(person.first_name, person.last_name) - class Query(graphene.ObjectType): - reverse = graphene.String(word=graphene.String(), resolver=reverse) + class Person(graphene.ObjectType): + first_name = graphene.String() + last_name = graphene.String() + full_name = graphene.String(resolver=resolve_full_name) Instances as data containers diff --git a/graphene/pyutils/signature.py b/graphene/pyutils/signature.py index c66c2563..7757d9d0 100644 --- a/graphene/pyutils/signature.py +++ b/graphene/pyutils/signature.py @@ -707,7 +707,10 @@ class Signature(object): break elif param.name in kwargs: if param.kind == _POSITIONAL_ONLY: - msg = "{arg!r} parameter is positional only, " "but was passed as a keyword" + msg = ( + "{arg!r} parameter is positional only, " + "but was passed as a keyword" + ) msg = msg.format(arg=param.name) raise TypeError(msg) parameters_ex = (param,) diff --git a/graphene/types/argument.py b/graphene/types/argument.py index 9c75bcee..bf304608 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -75,9 +75,7 @@ def to_arguments(args, extra_args=None): arg_name = default_name or arg.name assert ( arg_name not in arguments - ), 'More than one Argument have same name "{}".'.format( - arg_name - ) + ), 'More than one Argument have same name "{}".'.format(arg_name) arguments[arg_name] = arg return arguments diff --git a/graphene/types/base.py b/graphene/types/base.py index aa97ed22..75685d98 100644 --- a/graphene/types/base.py +++ b/graphene/types/base.py @@ -1,5 +1,9 @@ from ..utils.subclass_with_meta import SubclassWithMeta from ..utils.trim_docstring import trim_docstring +import six + +if six.PY3: + from typing import Type class BaseOptions(object): diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index dfb63e52..c5f43787 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -4,6 +4,9 @@ from graphql.language.ast import BooleanValue, FloatValue, IntValue, StringValue from .base import BaseOptions, BaseType from .unmountedtype import UnmountedType +if six.PY3: + from typing import Any + class ScalarOptions(BaseOptions): pass diff --git a/graphene/types/tests/test_definition.py b/graphene/types/tests/test_definition.py index 347de9c9..549847d5 100644 --- a/graphene/types/tests/test_definition.py +++ b/graphene/types/tests/test_definition.py @@ -1,5 +1,3 @@ - - from ..argument import Argument from ..enum import Enum from ..field import Field diff --git a/graphene/types/tests/test_inputobjecttype.py b/graphene/types/tests/test_inputobjecttype.py index d565ff40..dc557b94 100644 --- a/graphene/types/tests/test_inputobjecttype.py +++ b/graphene/types/tests/test_inputobjecttype.py @@ -1,4 +1,3 @@ - from ..argument import Argument from ..field import Field from ..inputfield import InputField diff --git a/graphene/types/tests/test_json.py b/graphene/types/tests/test_json.py index 6b8189c4..b5537180 100644 --- a/graphene/types/tests/test_json.py +++ b/graphene/types/tests/test_json.py @@ -1,4 +1,3 @@ - from ..json import JSONString from ..objecttype import ObjectType from ..schema import Schema diff --git a/graphene/types/tests/test_mountedtype.py b/graphene/types/tests/test_mountedtype.py index 787bee56..b964233e 100644 --- a/graphene/types/tests/test_mountedtype.py +++ b/graphene/types/tests/test_mountedtype.py @@ -1,4 +1,3 @@ - from ..field import Field from ..scalars import String diff --git a/graphene/types/tests/test_resolver.py b/graphene/types/tests/test_resolver.py index 3be9a492..2a15028d 100644 --- a/graphene/types/tests/test_resolver.py +++ b/graphene/types/tests/test_resolver.py @@ -1,4 +1,3 @@ - from ..resolver import ( attr_resolver, dict_resolver, diff --git a/graphene/types/tests/test_scalar.py b/graphene/types/tests/test_scalar.py index 1ec986cd..559c0ce6 100644 --- a/graphene/types/tests/test_scalar.py +++ b/graphene/types/tests/test_scalar.py @@ -1,4 +1,3 @@ - from ..scalars import Scalar diff --git a/graphene/utils/str_converters.py b/graphene/utils/str_converters.py index d8804038..216b0547 100644 --- a/graphene/utils/str_converters.py +++ b/graphene/utils/str_converters.py @@ -18,4 +18,4 @@ def to_snake_case(name): def to_const(string): - return re.sub("[\W|^]+", "_", string).upper() + return re.sub(r"[\W|^]+", "_", string).upper() # noqa diff --git a/setup.py b/setup.py index d9f62bec..48e5be35 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ import ast +import codecs import re import sys @@ -61,7 +62,9 @@ setup( name="graphene", version=version, description="GraphQL Framework for Python", - long_description=open("README.rst").read(), + long_description=codecs.open( + "README.rst", "r", encoding="ascii", errors="replace" + ).read(), url="https://github.com/graphql-python/graphene", author="Syrus Akbary", author_email="me@syrusakbary.com",