diff --git a/UPGRADE-v1.0.md b/UPGRADE-v1.0.md index a1ab5bd4..8ace8756 100644 --- a/UPGRADE-v1.0.md +++ b/UPGRADE-v1.0.md @@ -2,30 +2,29 @@ Big changes from v0.10.x to 1.0. While on the surface a lot of this just looks like shuffling around API, the entire codebase has been rewritten to handle some really great use cases and improved performance. - ## Backwards Compatibility and Deprecation Warnings This has been a community project from the start, we need your help making the upgrade as smooth as possible for everybody! We have done our best to provide backwards compatibility with deprecated APIs. - ## Deprecations -* `with_context` is no longer needed. Resolvers now always take the context argument. +- `with_context` is no longer needed. Resolvers now always take the context argument. Before: ```python - def resolve_xxx(self, args, info): + def resolve_xxx(root, args, info): # ... ``` With 1.0: + ```python - def resolve_xxx(self, args, context, info): + def resolve_xxx(root, args, context, info): # ... ``` -* `ObjectType` and `Interface` no longer accept the `abstract` option in the `Meta`. +- `ObjectType` and `Interface` no longer accept the `abstract` option in the `Meta`. Inheriting fields should be now achieved using `AbstractType` inheritance. Before: @@ -42,6 +41,7 @@ We have done our best to provide backwards compatibility with deprecated APIs. ``` With 1.0: + ```python class MyBaseQuery(graphene.AbstractType): my_field = String() @@ -50,9 +50,9 @@ We have done our best to provide backwards compatibility with deprecated APIs. pass ``` -* The `type_name` option in the Meta in types is now `name` +- The `type_name` option in the Meta in types is now `name` -* Type references no longer work with strings, but with functions. +- Type references no longer work with strings, but with functions. Before: @@ -70,7 +70,6 @@ We have done our best to provide backwards compatibility with deprecated APIs. users = graphene.List(lambda: User) ``` - ## Schema Schemas in graphene `1.0` are `Immutable`, that means that once you create a `graphene.Schema` any @@ -80,7 +79,6 @@ The `name` argument is removed from the Schema. The arguments `executor` and `middlewares` are also removed from the `Schema` definition. You can still use them, but by calling explicitly in the `execute` method in `graphql`. - ```python # Old way schema = graphene.Schema(name='My Schema') @@ -94,7 +92,6 @@ schema = graphene.Schema( ) ``` - ## Interfaces For implementing an Interface in an ObjectType, you have to add it onto `Meta.interfaces`. @@ -131,7 +128,7 @@ class ReverseString(Mutation): reversed = String() - def mutate(self, args, context, info): + def mutate(root, args, context, info): reversed = args.get('input')[::-1] return ReverseString(reversed=reversed) @@ -158,14 +155,13 @@ class Query(ObjectType): Also, if you wanted to create an `ObjectType` that implements `Node`, you have to do it explicity. - ## Django The Django integration with Graphene now has an independent package: `graphene-django`. For installing, you have to replace the old `graphene[django]` with `graphene-django`. -* As the package is now independent, you now have to import from `graphene_django`. -* **DjangoNode no longer exists**, please use `relay.Node` instead: +- As the package is now independent, you now have to import from `graphene_django`. +- **DjangoNode no longer exists**, please use `relay.Node` instead: ```python from graphene.relay import Node @@ -181,8 +177,8 @@ For installing, you have to replace the old `graphene[django]` with `graphene-dj The SQLAlchemy integration with Graphene now has an independent package: `graphene-sqlalchemy`. For installing, you have to replace the old `graphene[sqlalchemy]` with `graphene-sqlalchemy`. -* As the package is now independent, you have to import now from `graphene_sqlalchemy`. -* **SQLAlchemyNode no longer exists**, please use `relay.Node` instead: +- As the package is now independent, you have to import now from `graphene_sqlalchemy`. +- **SQLAlchemyNode no longer exists**, please use `relay.Node` instead: ```python from graphene.relay import Node diff --git a/UPGRADE-v2.0.md b/UPGRADE-v2.0.md index d9d48005..fed15923 100644 --- a/UPGRADE-v2.0.md +++ b/UPGRADE-v2.0.md @@ -7,20 +7,22 @@ It also improves the field resolvers, [simplifying the code](#simpler-resolvers) developer has to write to use them. **Deprecations:** -* [`AbstractType`](#abstracttype-deprecated) -* [`resolve_only_args`](#resolve_only_args) -* [`Mutation.Input`](#mutationinput) + +- [`AbstractType`](#abstracttype-deprecated) +- [`resolve_only_args`](#resolve_only_args) +- [`Mutation.Input`](#mutationinput) **Breaking changes:** -* [`Simpler Resolvers`](#simpler-resolvers) -* [`Node Connections`](#node-connections) + +- [`Simpler Resolvers`](#simpler-resolvers) +- [`Node Connections`](#node-connections) **New Features!** -* [`InputObjectType`](#inputobjecttype) -* [`Meta as Class arguments`](#meta-as-class-arguments) (_only available for Python 3_) +- [`InputObjectType`](#inputobjecttype) +- [`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 +> The type metaclasses are now deleted as they are no longer necessary. If your code was depending > on this strategy for creating custom attrs, see an [example on how to do it in 2.0](https://github.com/graphql-python/graphene/blob/v2.0.0/graphene/tests/issues/test_425.py). ## Deprecations @@ -49,7 +51,7 @@ class Pet(CommonFields, Interface): pass ``` -### resolve\_only\_args +### resolve_only_args `resolve_only_args` is now deprecated as the resolver API has been simplified. @@ -60,8 +62,8 @@ class User(ObjectType): name = String() @resolve_only_args - def resolve_name(self): - return self.name + def resolve_name(root): + return root.name ``` With 2.0: @@ -70,8 +72,8 @@ With 2.0: class User(ObjectType): name = String() - def resolve_name(self, info): - return self.name + def resolve_name(root, info): + return root.name ``` ### Mutation.Input @@ -94,7 +96,6 @@ class User(Mutation): name = String() ``` - ## Breaking Changes ### Simpler resolvers @@ -108,7 +109,7 @@ Before: ```python my_field = graphene.String(my_arg=graphene.String()) -def resolve_my_field(self, args, context, info): +def resolve_my_field(root, args, context, info): my_arg = args.get('my_arg') return ... ``` @@ -118,7 +119,7 @@ With 2.0: ```python my_field = graphene.String(my_arg=graphene.String()) -def resolve_my_field(self, info, my_arg): +def resolve_my_field(root, info, my_arg): return ... ``` @@ -126,7 +127,7 @@ def resolve_my_field(self, info, my_arg): You may need something like this: ```python -def resolve_my_field(self, info, known_field1, known_field2, **args): ## get other args with: args.get('arg_key') +def resolve_my_field(root, info, known_field1, known_field2, **args): ## get other args with: args.get('arg_key') ``` And, if you need the context in the resolver, you can use `info.context`: @@ -134,7 +135,7 @@ And, if you need the context in the resolver, you can use `info.context`: ```python my_field = graphene.String(my_arg=graphene.String()) -def resolve_my_field(self, info, my_arg): +def resolve_my_field(root, info, my_arg): context = info.context return ... ``` @@ -188,6 +189,7 @@ class MyObject(ObjectType): ``` To: + ```python class MyObject(ObjectType): class Meta: @@ -203,30 +205,32 @@ class MyObject(ObjectType): The parameters' order of `get_node_from_global_id` method has changed. You may need to adjust your [Node Root Field](http://docs.graphene-python.org/en/latest/relay/nodes/#node-root-field) and maybe other places that uses this method to obtain an object. Before: + ```python class RootQuery(object): ... node = Field(relay.Node, id=ID(required=True)) - def resolve_node(self, args, context, info): + def resolve_node(root, args, context, info): node = relay.Node.get_node_from_global_id(args['id'], context, info) return node ``` Now: + ```python class RootQuery(object): ... node = Field(relay.Node, id=ID(required=True)) - def resolve_node(self, info, id): + def resolve_node(root, info, id): node = relay.Node.get_node_from_global_id(info, id) return node ``` ## Mutation.mutate -Now only receives (`self`, `info`, `**args`) and is not a @classmethod +Now only receives (`root`, `info`, `**kwargs`) and is not a @classmethod Before: @@ -245,7 +249,7 @@ With 2.0: class SomeMutation(Mutation): ... - def mutate(self, info, **args): + def mutate(root, info, **args): ... ``` @@ -258,17 +262,14 @@ class SomeMutation(Mutation): last_name = String(required=True) ... - def mutate(self, info, first_name, last_name): + def mutate(root, info, first_name, last_name): ... ``` - - ## ClientIDMutation.mutate_and_get_payload Now only receives (`root`, `info`, `**input`) - ### Middlewares If you are using Middelwares, you need to some adjustments: @@ -294,10 +295,9 @@ class MyGrapheneMiddleware(object): ## Middleware code info.context = context -        return next_mw(root, info, **args)``` +        return next_mw(root, info, **args) ``` - ## New Features ### InputObjectType @@ -321,7 +321,7 @@ class Query(ObjectType): user = graphene.Field(User, input=UserInput()) @resolve_only_args - def resolve_user(self, input): + def resolve_user(root, input): user_id = input.get('id') if is_valid_input(user_id): return get_user(user_id) @@ -334,18 +334,17 @@ class UserInput(InputObjectType): id = ID(required=True) @property - def is_valid(self): - return self.id.startswith('userid_') + def is_valid(root): + return root.id.startswith('userid_') class Query(ObjectType): user = graphene.Field(User, input=UserInput()) - def resolve_user(self, info, input): + def resolve_user(root, info, input): if input.is_valid: return get_user(input.id) ``` - ### Meta as Class arguments Now you can use the meta options as class arguments (**ONLY PYTHON 3**). @@ -366,7 +365,6 @@ class Dog(ObjectType, interfaces=[Pet]): name = String() ``` - ### Abstract types Now you can create abstact types super easily, without the need of subclassing the meta. @@ -378,10 +376,10 @@ class Base(ObjectType): id = ID() - def resolve_id(self, info): + def resolve_id(root, info): return "{type}_{id}".format( - type=self.__class__.__name__, - id=self.id + type=root.__class__.__name__, + id=root.id ) ``` diff --git a/docs/execution/dataloader.rst b/docs/execution/dataloader.rst index 3cd36fcc..3f693075 100644 --- a/docs/execution/dataloader.rst +++ b/docs/execution/dataloader.rst @@ -111,8 +111,8 @@ leaner code and at most 4 database requests, and possibly fewer if there are cac best_friend = graphene.Field(lambda: User) friends = graphene.List(lambda: User) - def resolve_best_friend(self, info): - return user_loader.load(self.best_friend_id) + def resolve_best_friend(root, info): + return user_loader.load(root.best_friend_id) - def resolve_friends(self, info): - return user_loader.load_many(self.friend_ids) + def resolve_friends(root, info): + return user_loader.load_many(root.friend_ids) diff --git a/docs/execution/execute.rst b/docs/execution/execute.rst index f4a1e759..20d4b07a 100644 --- a/docs/execution/execute.rst +++ b/docs/execution/execute.rst @@ -1,3 +1,5 @@ +.. _SchemaExecute: + Executing a query ================= @@ -13,6 +15,8 @@ For executing a query a schema, you can directly call the ``execute`` method on ``result`` represents the result of execution. ``result.data`` is the result of executing the query, ``result.errors`` is ``None`` if no errors occurred, and is a non-empty list if an error occurred. +.. _SchemaExecuteContext: + Context _______ @@ -59,3 +63,66 @@ You can pass variables to a query via ``variables``. ''', variables={'id': 12}, ) + +Root Value +__________ + +Value used for :ref:`ResolverRootArgument` in root queries and mutations can be overridden using ``root`` parameter. + +.. code:: python + + class Query(graphene.ObjectType): + me = graphene.Field(User) + + def resolve_user(root, info): + return get_user_by_id(root.id) + + schema = graphene.Schema(Query) + user_root = User(id=12, name='bob'} + result = schema.execute( + ''' + query getUser { + user { + id + firstName + lastName + } + } + ''', + root=user_root + ) + +Operation Name +______________ + +If there are multiple operations defined in a query string, ``operation_name`` should be used to indicate which should be executed. + +.. code:: python + + class Query(graphene.ObjectType): + me = graphene.Field(User) + + def resolve_user(root, info): + return get_user_by_id(12) + + schema = graphene.Schema(Query) + query_string = ''' + query getUserWithFirstName { + user { + id + firstName + lastName + } + } + query getUserWithFullName { + user { + id + firstName + lastName + } + } + ''' + result = schema.execute( + query_string, + operation_name='getUserWithFullName' + ) diff --git a/docs/index.rst b/docs/index.rst index aff3960f..4d624777 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,10 +12,13 @@ Contents: relay/index testing/index +.. _Integrations: + Integrations ------ +------------ * `Graphene-Django `_ (`source `_) +* Flask-Graphql (`source `_) * `Graphene-SQLAlchemy `_ (`source `_) * `Graphene-GAE `_ (`source `_) * `Graphene-Mongo `_ (`source `_) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index fc333fae..c23f09f9 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,62 +1,143 @@ Getting started =============== +Introduction +------------ + What is GraphQL? ----------------- +~~~~~~~~~~~~~~~~ -For an introduction to GraphQL and an overview of its concepts, please refer -to `the official introduction `_. +GraphQL is a query language for your API. + +It provides a standard way to: + +* *describe data provided by a server* in a statically typed **Schema** +* *request data* in a **Query** which exactly describes your data requirements and +* *receive data* in a **Response** containing only the data you requested. + +For an introduction to GraphQL and an overview of its concepts, please refer to `the official GraphQL documentation`_. + +.. _the official GraphQL documentation: http://graphql.org/learn/ + +What is Graphene? +~~~~~~~~~~~~~~~~~ + +Graphene is a library that provides tools to implement a GraphQL API in Python using a *code-first* approach. + +Compare Graphene's *code-first* approach to building a GraphQL API with *schema-first* approaches like `Apollo Server`_ (JavaScript) or Ariadne_ (Python). Instead of writing GraphQL **Schema Definition Langauge (SDL)**, we write Python code to describe the data provided by your server. + +.. _Apollo Server: https://www.apollographql.com/docs/apollo-server/ + +.. _Ariadne: https://ariadne.readthedocs.io + +Graphene is fully featured with integrations for the most popular web frameworks and ORM and easy Relay Compliance. + +An example in Graphene +---------------------- + +Let’s build a basic GraphQL schema to say "hello" and "goodbye" in Graphene. + +When we send a **Query** requesting only one **Field**, ``hello``, and specify a value for the ``name`` **Argument**... + +.. code:: + + { + hello(name: "friend") + } + +...we would expect the following Response containing only the data requested (the ``goodbye`` field is not resolved). + +.. code:: + + { + "data": { + "hello": "Hello friend!" + } + } -Let’s build a basic GraphQL schema from scratch. Requirements ------------- +~~~~~~~~~~~~ - Python (2.7, 3.4, 3.5, 3.6, pypy) - Graphene (2.0) Project setup -------------- +~~~~~~~~~~~~~ .. code:: bash pip install "graphene>=2.0" Creating a basic Schema ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ -A GraphQL schema describes your data model, and provides a GraphQL -server with an associated set of resolve methods that know how to fetch -data. - -We are going to create a very simple schema, with a ``Query`` with only -one field: ``hello`` and an input name. And when we query it, it should return ``"Hello -{argument}"``. +In Graphene, we can define a simple schema using the following code: .. code:: python import graphene class Query(graphene.ObjectType): - hello = graphene.String(argument=graphene.String(default_value="stranger")) + # this defines a Field `hello` in our Schema with a single Argument `name` + hello = graphene.String(name=graphene.String(default_value="stranger")) + goodbye = graphene.String() - def resolve_hello(self, info, argument): - return 'Hello ' + argument + # our Resolver method takes the GraphQL context (root, info) as well as + # Argument (name) for the Field and returns data for the query Response + def resolve_hello(root, info, name): + return f'Hello {name}!' + + def resolve_goodbye(root, info): + return 'See ya!' schema = graphene.Schema(query=Query) -Querying --------- -Then we can start querying our schema: +A GraphQL **Schema** describes each **Field** in the data model provided by the server using scalar types like *String*, *Int* and *Enum* and compound types like *List* and *Object*. For more details refer to the Graphene :ref:`TypesReference`. + +Our schema can also define any number of **Arguments** for our **Fields**. This is a powerful way for a **Query** to describe the exact data requirements for each **Field**. + +For each **Field** in our **Schema**, we write a **Resolver** method to fetch data requested by a client's **Query** using the current context and **Arguments**. For more details, refer to this section on :ref:`Resolvers`. + +Schema Definition Language (SDL) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the `GraphQL Schema Definition Language`_, we could describe the feilds defined by our example code as show below. + +.. _GraphQL Schema Definition Language: https://graphql.org/learn/schema/ + +.. code:: + + type Query { + hello(name: String = "stranger"): String + goodbye: String + } + +Further examples in this documentation will use SDL to describe schema created by ObjectTypes and other fields. + +Querying +~~~~~~~~ + +Then we can start querying our **Schema** by passing a GraphQL query string to ``execute``: .. code:: python - result = schema.execute('{ hello }') - print(result.data['hello']) # "Hello stranger" + # we can query for our field (with the default argument) + query_string = '{ hello }' + result = schema.execute(query_string) + print(result.data['hello']) + # "Hello stranger" # or passing the argument in the query - result = schema.execute('{ hello (argument: "graph") }') - print(result.data['hello']) # "Hello graph" + query_string_with_argument = '{ hello (name: "GraphQL") }' + result = schema.execute(query_with_argument) + print(result.data['hello']) + # "Hello GraphQL!" -Congrats! You got your first graphene schema working! +Next steps +~~~~~~~~~~ + +Congrats! You got your first Graphene schema working! + +Normally, we don't need to directly execute a query string against our schema as Graphene provides many useful Integrations with popular web frameworks like Flask and Django. Check out :ref:`Integrations` for more information on how to get started serving your GraphQL API. diff --git a/docs/relay/connection.rst b/docs/relay/connection.rst index c2379cbc..07d81ada 100644 --- a/docs/relay/connection.rst +++ b/docs/relay/connection.rst @@ -41,5 +41,5 @@ that implements ``Node`` will have a default Connection. name = graphene.String() ships = relay.ConnectionField(ShipConnection) - def resolve_ships(self, info): + def resolve_ships(root, info): return [] diff --git a/docs/types/index.rst b/docs/types/index.rst index acbdb8e8..d44894af 100644 --- a/docs/types/index.rst +++ b/docs/types/index.rst @@ -1,3 +1,5 @@ +.. _TypesReference: + =============== Types Reference =============== @@ -5,12 +7,12 @@ Types Reference .. toctree:: :maxdepth: 1 - enums + schema scalars list-and-nonnull objecttypes + enums interfaces unions - schema mutations abstracttypes diff --git a/docs/types/interfaces.rst b/docs/types/interfaces.rst index f2073d6a..eb7172e9 100644 --- a/docs/types/interfaces.rst +++ b/docs/types/interfaces.rst @@ -1,3 +1,5 @@ +.. _Interfaces: + Interfaces ========== @@ -82,7 +84,7 @@ For example, you can define a field ``hero`` that resolves to any episode=graphene.Int(required=True) ) - def resolve_hero(_, info, episode): + def resolve_hero(root, info, episode): # Luke is the hero of Episode V if episode == 5: return get_human(name='Luke Skywalker') diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index 8532595e..d63ada3e 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -19,7 +19,7 @@ This example defines a Mutation: ok = graphene.Boolean() person = graphene.Field(lambda: Person) - def mutate(self, info, name): + def mutate(root, info, name): person = Person(name=name) ok = True return CreatePerson(person=person, ok=ok) @@ -32,7 +32,8 @@ resolved. only argument for the mutation. **mutate** is the function that will be applied once the mutation is -called. +called. This method is just a special resolver that we can change +data within. It takes the same arguments as the standard query :ref:`ResolverArguments`. So, we can finish our schema like this: @@ -157,7 +158,7 @@ To return an existing ObjectType instead of a mutation-specific type, set the ** Output = Person - def mutate(self, info, name): + def mutate(root, info, name): return Person(name=name) Then, if we query (``schema.execute(query_str)``) the following: diff --git a/docs/types/objecttypes.rst b/docs/types/objecttypes.rst index 1579258d..30d29307 100644 --- a/docs/types/objecttypes.rst +++ b/docs/types/objecttypes.rst @@ -1,15 +1,15 @@ -ObjectTypes -=========== +.. _ObjectType: -An ObjectType is the single, definitive source of information about your -data. It contains the essential fields and behaviors of the data you’re -querying. +ObjectType +========== + +A Graphene *ObjectType* is the building block used to define the relationship between **Fields** in your **Schema** and how their data is retrieved. The basics: -- Each ObjectType is a Python class that inherits from - ``graphene.ObjectType``. +- Each ObjectType is a Python class that inherits from ``graphene.ObjectType``. - Each attribute of the ObjectType represents a ``Field``. +- Each ``Field`` has a :ref:`resolver method` to fetch data (or :ref:`DefaultResolver`). Quick example ------------- @@ -18,19 +18,15 @@ This example model defines a Person, with a first and a last name: .. code:: python - import graphene - class Person(graphene.ObjectType): first_name = graphene.String() last_name = graphene.String() full_name = graphene.String() - def resolve_full_name(root, info): - return '{} {}'.format(root.first_name, root.last_name) + def resolve_full_name(parent, info): + return f'{parent.first_name} {parent.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 -Field. +This *ObjectType* defines the feild **first\_name**, **last\_name**, and **full\_name**. Each field is specified as a class attribute, and each attribute maps to a Field. Data is fetched by our ``resolve_full_name`` :ref:`resolver method` for ``full_name`` field and the :ref:`DefaultResolver` for other fields. The above ``Person`` ObjectType has the following schema representation: @@ -42,52 +38,102 @@ The above ``Person`` ObjectType has the following schema representation: fullName: String } +.. _Resolvers: Resolvers --------- -A resolver is a method that resolves certain fields within an -``ObjectType``. If not specified otherwise, the resolver of a -field is the ``resolve_{field_name}`` method on the ``ObjectType``. +A **Resolver** is a method that helps us answer **Queries** by fetching data for a **Field** in our **Schema**. -By default resolvers take the arguments ``info`` and ``*args``. +Resolvers are lazily executed, so if a field is not included in a query, its resolver will not be executed. -NOTE: The resolvers on an ``ObjectType`` are always treated as ``staticmethod``\ s, -so the first argument to the resolver method ``self`` (or ``root``) need -not be an actual instance of the ``ObjectType``. +Each field on an *ObjectType* in Graphene should have a corresponding resolver method to fetch data. This resolver method should match the field name. For example, in the ``Person`` type above, the ``full_name`` field is resolved by the method ``resolve_full_name``. -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 or dict that is -passed to the ``ObjectType``. +Each resolver method takes the parameters: +* :ref:`ResolverRootArgument` for the value object use to resolve most fields +* :ref:`ResolverInfoArgument` for query and schema meta information and per-request context +* :ref:`ResolverGraphQLArguments` as defined on the **Field**. + +.. _ResolverArguments: + +Resolver Arguments +~~~~~~~~~~~~~~~~~~ + +.. _ResolverRootArgument: + +Parent Value Object (*parent*) +****************************** + +This parameter is typically used to derive the values for most fields on an *ObjectType*. + +The first parameter of a resolver method (*parent*) is the value object returned from the resolver of the parent field. If there is no parent field, such as a root Query field, then the value for *parent* is set to the ``root_value`` configured while executing the query (default ``None``). See :ref:`SchemaExecute` for more details on executing queries. + +Resolver example +^^^^^^^^^^^^^^^^ + +If we have a schema with Person type and one field on the root query. .. code:: python import graphene class Person(graphene.ObjectType): - first_name = graphene.String() - last_name = graphene.String() + full_name = graphene.String() + + def resolve_full_name(parent, info): + return f'{parent.first_name} {parent.last_name}' class Query(graphene.ObjectType): me = graphene.Field(Person) - best_friend = graphene.Field(Person) - def resolve_me(_, info): + def resolve_me(parent, info): # returns an object that represents a Person return get_human(name='Luke Skywalker') - def resolve_best_friend(_, info): - return { - "first_name": "R2", - "last_name": "D2", - } +When we execute a query against that schema. +.. code:: python -Resolvers with arguments -~~~~~~~~~~~~~~~~~~~~~~~~ + schema = Schema(query=Query) + + query_string = "{ me { fullName } }" + result = schema.execute(query_string) + + assert result["data"]["me"] == {"fullName": "Luke Skywalker") + +Then we go through the following steps to resolve this query: + +* ``parent`` is set with the root_value from query execution (None). +* ``Query.resolve_me`` called with ``parent`` None which returns a value object ``Person("Luke", "Skywalker")``. +* This value object is then used as ``parent`` while calling ``Person.resolve_full_name`` to resolve the scalar String value "Luke Skywalker". +* The scalar value is serialized and sent back in the query response. + +Each resolver returns the next :ref:`ResolverRootArgument` to be used in executing the following resolver in the chain. If the Field is a Scalar type, that value will be serialized and sent in the **Response**. Otherwise, while resolving Compound types like *ObjectType*, the value be passed forward as the next :ref:`ResolverRootArgument`. + +Naming convention +^^^^^^^^^^^^^^^^^ + +This :ref:`ResolverRootArgument` is sometimes named ``obj``, ``parent``, or ``source`` in other GraphQL documentation. It can also be named after the value object being resolved (ex. ``root`` for a root Query or Mutation, and ``person`` for a Person value object). Sometimes this argument will be named ``self`` in Graphene code, but this can be misleading due to :ref:`ResolverImplicitStaticMethod` while executing queries in Graphene. + +.. _ResolverInfoArgument: + +GraphQL Execution Info (*info*) +******************************* + +The second parameter provides two things: + +* reference to meta information about the execution of the current GraphQL Query (fields, schema, parsed query, etc.) +* access to per-request ``context`` which can be used to store user authentication, data loader instances or anything else useful for resolving the query. + +Only context will be required for most applications. See :ref:`SchemaExecuteContext` for more information about setting context. + +.. _ResolverGraphQLArguments: + +GraphQL Arguments (*\*\*kwargs*) +******************************** Any arguments that a field defines gets passed to the resolver function as -kwargs. For example: +keyword arguments. For example: .. code:: python @@ -96,7 +142,7 @@ kwargs. For example: class Query(graphene.ObjectType): human_by_name = graphene.Field(Human, name=graphene.String(required=True)) - def resolve_human_by_name(_, info, name): + def resolve_human_by_name(parent, info, name): return get_human(name=name) You can then execute the following query: @@ -110,7 +156,94 @@ You can then execute the following query: } } -NOTE: if you define an argument for a field that is not required (and in a query +Convenience Features of Graphene Resolvers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _ResolverImplicitStaticMethod: + +Implicit staticmethod +********************* + +One surprising feature of Graphene is that all resolver methods are treated implicitly as staticmethods. This means that, unlike other methods in Python, the first argument of a resolver is *never* ``self`` while it is being executed by Graphene. Instead, the first argument is always :ref:`ResolverRootArgument`. In practice, this is very convenient as, in GraphQL, we are almost always more concerned with the using the parent value object to resolve queries than attributes on the Python object itself. + +The two resolvers in this example are effectively the same. + +.. code:: python + + class Person(graphene.ObjectType): + first_name = graphene.String() + last_name = graphene.String() + + @staticmethod + def resolve_first_name(parent, info): + ''' + Decorating a Python method with `staticmethod` ensures that `self` will not be provided as an + argument. However, Graphene does not need this decorator for this behavior. + ''' + return parent.first_name + + def resolve_last_name(parent, info): + ''' + Normally the first argument for this method would be `self`, but Graphene executes this as + a staticmethod implicitly. + ''' + return parent.last_name + + # ... + +If you prefer your code to be more explict, feel free to use ``@staticmethod`` decorators. Otherwise, your code may be cleaner without them! + +.. _DefaultResolver: + +Default Resolver +**************** + +If a resolver method is not defined for a **Field** attribute on our *ObjectType*, Graphene supplies a default resolver. + +If the :ref:`ResolverRootArgument` is a dictionary, the resolver will look for a dictionary key matching the field name. Otherwise, the resolver will get the attribute from the parent value object matching the field name. + +.. code:: python + + from collections import namedtuple + + PersonValueObject = namedtuple('Person', 'first_name', 'last_name') + + class Person(graphene.ObjectType): + first_name = graphene.String() + last_name = graphene.String() + + class Query(graphene.Object): + me = graphene.Field(Person) + my_best_friend = graphene.Field(Person) + + def resolve_me(parent, info): + # always pass an object for `me` field + return PersonValueObject(first_name='Luke', last_name='Skywalker') + + def resolve_my_best_friend(parent, info): + # always pass a dictionary for `my_best_fiend_field` + return {"first_name": "R2", "last_name": "D2"} + + schema = graphene.Schema(query=Query) + result = schema.execute(''' + { + me { firstName lastName } + myBestFriend { firstName lastName } + } + ''') + # With default resolvers we can resolve attributes from an object.. + assert result['data']['me'] == {"firstName": "Luke", "lastName": "Skywalker"} + + # With default resolvers, we can also resolve keys from a dictionary.. + assert result['data']['my_best_friend'] == {"firstName": "R2", "lastName": "D2"} + +Advanced +~~~~~~~~ + +GraphQL Argument defaults +************************* + +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. @@ -124,7 +257,7 @@ For example, given this schema: class Query(graphene.ObjectType): hello = graphene.String(required=True, name=graphene.String()) - def resolve_hello(_, info, name): + def resolve_hello(parent, info, name): return name if name else 'World' And this query: @@ -141,7 +274,7 @@ An error will be thrown: TypeError: resolve_hello() missing 1 required positional argument: 'name' -You can fix this error in 2 ways. Either by combining all keyword arguments +You can fix this error in serveral ways. Either by combining all keyword arguments into a dict: .. code:: python @@ -149,8 +282,9 @@ into a dict: class Query(graphene.ObjectType): hello = graphene.String(required=True, name=graphene.String()) - def resolve_hello(_, info, **args): - return args.get('name', 'World') + def resolve_hello(parent, info, **kwargs): + name = kwargs.get('name', 'World') + return f'Hello, {name}!' Or by setting a default value for the keyword argument: @@ -159,12 +293,24 @@ Or by setting a default value for the keyword argument: class Query(graphene.ObjectType): hello = graphene.String(required=True, name=graphene.String()) - def resolve_hello(_, info, name='World'): - return name + def resolve_hello(parent, info, name='World'): + return f'Hello, {name}!' +One can also set a default value for an Argument in the GraphQL schema itself using Graphene! + +.. code:: python + + class Query(graphene.ObjectType): + hello = graphene.String( + required=True, + name=graphene.Argument(graphene.String, default_value='World') + ) + + def resolve_hello(parent, info, name): + return f'Hello, {name}!' Resolvers outside the class -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*************************** A field can use a custom resolver from outside the class: @@ -181,11 +327,11 @@ A field can use a custom resolver from outside the class: full_name = graphene.String(resolver=resolve_full_name) -Instances as data containers ----------------------------- +Instances as value objects +************************** -Graphene ``ObjectType``\ s can act as containers too. So with the -previous example you could do: +Graphene ``ObjectType``\ s can act as value objects too. So with the +previous example you could use ``Person`` to capture data for each of the *ObjectType*'s fields. .. code:: python @@ -194,8 +340,18 @@ previous example you could do: peter.first_name # prints "Peter" peter.last_name # prints "Griffin" -Changing the name ------------------ +Field camelcasing +***************** + +Graphene automatically camelcases fields on *ObjectType* from ``field_name`` to ``fieldName`` to conform with GraphQL standards. See :ref:`SchemaAutoCamelCase` for more information. + +*ObjectType* Configuration - Meta class +--------------------------------------- + +Graphene uses a Meta inner class on *ObjectType* to set different options. + +GraphQL type name +~~~~~~~~~~~~~~~~~ By default the type name in the GraphQL schema will be the same as the class name that defines the ``ObjectType``. This can be changed by setting the ``name`` @@ -207,4 +363,34 @@ property on the ``Meta`` class: class Meta: name = 'Song' +GraphQL Description +~~~~~~~~~~~~~~~~~~~ + +The schema description of an *ObjectType* can be set as a docstring on the Python object or on the Meta inner class. + +.. code:: python + + class MyGraphQlSong(graphene.ObjectType): + ''' We can set the schema description for an Object Type here on a docstring ''' + class Meta: + description = 'But if we set the description in Meta, this value is used instead' + +Interfaces & Possible Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Setting ``interfaces`` in Meta inner class specifies the GraphQL Interfaces that this Object implements. + +Providing ``possible_types`` helps Graphene resolve ambiguous types such as interfaces or Unions. + +See :ref:`Interfaces` for more information. + +.. code:: python + + Song = namedtuple('Song', ('title', 'artist')) + + class MyGraphQlSong(graphene.ObjectType): + class Meta: + interfaces = (graphene.Node, ) + possible_types = (Song, ) + .. _Interface: /docs/interfaces/ diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index acbd534b..74002483 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -1,3 +1,5 @@ +.. _Scalars: + Scalars ======= diff --git a/docs/types/schema.rst b/docs/types/schema.rst index 3832b6f9..1af5c294 100644 --- a/docs/types/schema.rst +++ b/docs/types/schema.rst @@ -1,16 +1,42 @@ Schema ====== -A Schema is created by supplying the root types of each type of operation, query and mutation (optional). -A schema definition is then supplied to the validator and executor. +A GraphQL **Schema** defines the types and relationship between **Fields** in your API. + +A Schema is created by supplying the root :ref:`ObjectType` of each operation, query (mandatory), mutation and subscription. + +Schema will collect all type definitions related to the root operations and then supplied to the validator and executor. .. code:: python my_schema = Schema( query=MyRootQuery, mutation=MyRootMutation, + subscription=MyRootSubscription ) +A Root Query is just a special :ref:`ObjectType` that :ref:`defines the fields ` that are the entrypoint for your API. Root Mutation and Root Subscription are similar to Root Query, but for different operation types: + +* Query fetches data +* Mutation to changes data and retrieve the changes +* Subscription to sends changes to clients in real time + +Review the `GraphQL documentation on Schema`_ for a brief overview of fields, schema and operations. + +.. _GraphQL documentation on Schema: https://graphql.org/learn/schema/ + + +Querying +-------- + +To query a schema, call the ``execute`` method on it. See :ref:`SchemaExecute` for more details. + + +.. code:: python + + query_string = 'query whoIsMyBestFriend { myBestFriend { lastName } }' + my_schema.execute(query_string) + Types ----- @@ -28,17 +54,7 @@ In this case, we need to use the ``types`` argument when creating the Schema. types=[SomeExtraObjectType, ] ) - -Querying --------- - -To query a schema, call the ``execute`` method on it. - - -.. code:: python - - my_schema.execute('{ lastName }') - +.. _SchemaAutoCamelCase: Auto CamelCase field names --------------------------