From 5cb7d91aaad028ad186f9453c6d7cc7c1e63e8a1 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Sun, 9 Jun 2019 19:49:56 -0400 Subject: [PATCH] Revise documentation (#969) * Revise documentation - Add missing reference to `flask-graphql` in integrations - align documentation for resolver arguments (use root for 1st argument instead of self) - explore use of `parent` instead of `root` for first argument - clarify resolvers and object type documentation - add documentation for Meta class options for ObjectType - expand quickstart documentation for first time users - streamline order of documentation for first time users (broad -> specific) - document resolver quirks * explict imports from graphene * rename doc refs for resolvers * suggestions typos and graphene import --- UPGRADE-v1.0.md | 30 ++- UPGRADE-v2.0.md | 72 ++++--- docs/execution/dataloader.rst | 8 +- docs/execution/execute.rst | 94 ++++++++- docs/index.rst | 3 + docs/quickstart.rst | 135 ++++++++++--- docs/relay/connection.rst | 2 +- docs/types/index.rst | 6 +- docs/types/interfaces.rst | 4 +- docs/types/mutations.rst | 7 +- docs/types/objecttypes.rst | 358 ++++++++++++++++++++++++++-------- docs/types/scalars.rst | 2 + docs/types/schema.rst | 42 ++-- 13 files changed, 573 insertions(+), 190 deletions(-) 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..74300a82 100644 --- a/docs/execution/execute.rst +++ b/docs/execution/execute.rst @@ -1,3 +1,5 @@ +.. _SchemaExecute: + Executing a query ================= @@ -7,12 +9,16 @@ For executing a query a schema, you can directly call the ``execute`` method on .. code:: python - schema = graphene.Schema(...) + from graphene import Schema + + schema = Schema(...) result = schema.execute('{ name }') ``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 _______ @@ -21,15 +27,17 @@ You can pass context to a query via ``context``. .. code:: python - class Query(graphene.ObjectType): - name = graphene.String() + from graphene import ObjectType, String, Schema + + class Query(ObjectType): + name = String() def resolve_name(root, info): return info.context.get('name') - schema = graphene.Schema(Query) + schema = Schema(Query) result = schema.execute('{ name }', context={'name': 'Syrus'}) - + assert result.data['name'] == 'Syrus' Variables @@ -40,13 +48,15 @@ You can pass variables to a query via ``variables``. .. code:: python - class Query(graphene.ObjectType): - user = graphene.Field(User, id=graphene.ID(required=True)) + from graphene import ObjectType, Field, ID, Schema + + class Query(ObjectType): + user = Field(User, id=ID(required=True)) def resolve_user(root, info, id): return get_user_by_id(id) - schema = graphene.Schema(Query) + schema = Schema(Query) result = schema.execute( ''' query getUser($id: ID) { @@ -59,3 +69,71 @@ You can pass variables to a query via ``variables``. ''', variables={'id': 12}, ) + +Root Value +__________ + +Value used for :ref:`ResolverParamParent` in root queries and mutations can be overridden using ``root`` parameter. + +.. code:: python + + from graphene import ObjectType, Field, Schema + + class Query(ObjectType): + me = Field(User) + + def resolve_user(root, info): + return {'id': root.id, 'firstName': root.name} + + schema = Schema(Query) + user_root = User(id=12, name='bob'} + result = schema.execute( + ''' + query getUser { + user { + id + firstName + lastName + } + } + ''', + root=user_root + ) + assert result.data['user']['id'] == user_root.id + +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 + + from graphene import ObjectType, Field, Schema + + class Query(ObjectType): + me = Field(User) + + def resolve_user(root, info): + return get_user_by_id(12) + + schema = Schema(Query) + query_string = ''' + query getUserWithFirstName { + user { + id + firstName + lastName + } + } + query getUserWithFullName { + user { + id + fullName + } + } + ''' + result = schema.execute( + query_string, + operation_name='getUserWithFullName' + ) + assert result.data['user']['fullName'] diff --git a/docs/index.rst b/docs/index.rst index eb78f902..8db02a6e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,10 +13,13 @@ Contents: testing/index api/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..7be910b4 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 ORMs. Graphene produces schemas tha are fully compliant with the GraphQL spec and provides tools and patterns for building a Relay-Compliant API as well. + +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 + from graphene import ObjectType, String, Schema - class Query(graphene.ObjectType): - hello = graphene.String(argument=graphene.String(default_value="stranger")) + class Query(ObjectType): + # this defines a Field `hello` in our Schema with a single Argument `name` + hello = String(name=String(default_value="stranger")) + goodbye = 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}!' - schema = graphene.Schema(query=Query) + def resolve_goodbye(root, info): + return 'See ya!' + + schema = Schema(query=Query) + + +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: +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..5f7272a6 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,17 @@ This example model defines a Person, with a first and a last name: .. code:: python - import graphene + from graphene import ObjectType, String - class Person(graphene.ObjectType): - first_name = graphene.String() - last_name = graphene.String() - full_name = graphene.String() + class Person(ObjectType): + first_name = String() + last_name = String() + full_name = 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 field **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,61 +40,111 @@ 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:`ResolverParamParent` for the value object use to resolve most fields +* :ref:`ResolverParamInfo` for query and schema meta information and per-request context +* :ref:`ResolverParamGraphQLArguments` as defined on the **Field**. + +.. _ResolverArguments: + +Resolver Parameters +~~~~~~~~~~~~~~~~~~~ + +.. _ResolverParamParent: + +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 + from graphene import ObjectType, String, Field - class Person(graphene.ObjectType): - first_name = graphene.String() - last_name = graphene.String() + class Person(ObjectType): + full_name = String() - class Query(graphene.ObjectType): - me = graphene.Field(Person) - best_friend = graphene.Field(Person) + def resolve_full_name(parent, info): + return f"{parent.first_name} {parent.last_name}" - def resolve_me(_, info): + class Query(ObjectType): + me = Field(Person) + + def resolve_me(parent, info): # returns an object that represents a Person - return get_human(name='Luke Skywalker') + 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:`ResolverParamParent` 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:`ResolverParamParent`. + +Naming convention +^^^^^^^^^^^^^^^^^ + +This :ref:`ResolverParamParent` 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. + +.. _ResolverParamInfo: + +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. + +.. _ResolverParamGraphQLArguments: + +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 - import graphene + from graphene import ObjectType, Field, String - class Query(graphene.ObjectType): - human_by_name = graphene.Field(Human, name=graphene.String(required=True)) + class Query(ObjectType): + human_by_name = Field(Human, name=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 +158,98 @@ 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:`ResolverParamParent`. 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 + + from graphene import ObjectType, String + + class Person(ObjectType): + first_name = String() + last_name = 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:`ResolverParamParent` 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 + + from graphene import ObjectType, String, Field, Schema + + PersonValueObject = namedtuple('Person', 'first_name', 'last_name') + + class Person(ObjectType): + first_name = String() + last_name = String() + + class Query(ObjectType): + me = Field(Person) + my_best_friend = 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 = 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. @@ -119,12 +258,12 @@ For example, given this schema: .. code:: python - import graphene + from graphene import ObjectType, String - class Query(graphene.ObjectType): - hello = graphene.String(required=True, name=graphene.String()) + class Query(ObjectType): + hello = String(required=True, name=String()) - def resolve_hello(_, info, name): + def resolve_hello(parent, info, name): return name if name else 'World' And this query: @@ -141,61 +280,90 @@ 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 - class Query(graphene.ObjectType): - hello = graphene.String(required=True, name=graphene.String()) + from graphene import ObjectType, String - def resolve_hello(_, info, **args): - return args.get('name', 'World') + class Query(ObjectType): + hello = String(required=True, name=String()) + + 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: .. code:: python - class Query(graphene.ObjectType): - hello = graphene.String(required=True, name=graphene.String()) + from graphene import ObjectType, String - def resolve_hello(_, info, name='World'): - return name + class Query(ObjectType): + hello = String(required=True, name=String()) + 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 + + from graphene import ObjectType, String + + class Query(ObjectType): + hello = String( + required=True, + name=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: .. code:: python - import graphene + from graphene import ObjectType, String def resolve_full_name(person, info): return '{} {}'.format(person.first_name, person.last_name) - class Person(graphene.ObjectType): - first_name = graphene.String() - last_name = graphene.String() - full_name = graphene.String(resolver=resolve_full_name) + class Person(ObjectType): + first_name = String() + last_name = String() + full_name = 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 peter = Person(first_name='Peter', last_name='Griffin') - peter.first_name # prints "Peter" - peter.last_name # prints "Griffin" + 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`` @@ -203,8 +371,44 @@ property on the ``Meta`` class: .. code:: python - class MyGraphQlSong(graphene.ObjectType): + from graphene import ObjectType + + class MyGraphQlSong(ObjectType): 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 + + from graphene import ObjectType + + class MyGraphQlSong(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 + + from graphene import ObjectType, Node + + Song = namedtuple('Song', ('title', 'artist')) + + class MyGraphQlSong(ObjectType): + class Meta: + interfaces = (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 --------------------------