Skip to content

Low-resolution documentation and its solution #925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
BenSapiens opened this issue Mar 20, 2019 · 14 comments
Closed

Low-resolution documentation and its solution #925

BenSapiens opened this issue Mar 20, 2019 · 14 comments

Comments

@BenSapiens
Copy link

I'm trying to figure out this software and I'm having a hell of a time. The documentation just doesn't explain much of anything. If someone with domain expertise would like to walk me through this thing, I'll write you up a whole new set of docs with what I've learned, start to finish. That's a fair trade, I think.

@Cito
Copy link
Member

Cito commented Mar 20, 2019

Personally, I find the documentation a bit terse, but not so bad that it needs to be completely rewritten. Maybe you can give a few concrete example for the kind of issues you have with the documentation or questions that you feel are not answered there.

@BenSapiens
Copy link
Author

Hi, @Cito.

I've done more reading and thinking, and I think you're probably right. The skeleton is there, but it's bare — there's just no fleshy tissue, metaphorically speaking.

Take this, for instance, on the ObjectType page:

import graphene

class Person(graphene.ObjectType):
    first_name = graphene.String()
    last_name = graphene.String()
    full_name = graphene.String()

    def resolve_full_name(self, info):
        return '{} {}'.format(self.first_name, self.last_name)

There is then an example that shows the following GraphQL schema:

type Person {
  firstName: String
  lastName: String
  fullName: String
}

I understand this GraphQL schema perfectly well. What I don't understand is what exactly is the relationship between this GraphQL schema and the Graphene representation thereof.

  1. Why are there mixed schema and resolvers?
  2. If there are to be mixed schema and resolvers, why does only full_name have a resolver? All of the fields will need values. Or is the resolver in the ObjectType never more than a computed property?
  3. How do object types derived from the ObjectType class relate to the Query class (which is also derived from ObjectType)?
  4. How do nested ObjectTypes work? Can they be nested to arbitrary depths?
  5. Etc. With Javascript objects it's all perfectly clear to me. This Python, not so much.

I attempt to write an ObjectType:

class Person(ObjectType):
    name = String()
    age = Integer()
    sex = String()

    def resolve_name(self, info):
        return "Jack"
    def resolve_age(self, info):
        return 22
    def resolve_sex(self, info):
        return "male"

class Query(ObjectType):
    person = Field(Person)

This doesn't work. Why doesn't this work?

In addition, arguments can be passed to a class initialization. They are handled by __init__. Why not here?

This also doesn't work:

class Person(ObjectType):
    name = String()
    age = Integer()
    sex = String()

    def resolve(self, info):
        return Person(name="Jack", age=22, sex="male")

class Query(ObjectType):
    person = Field(Person)

Only this works:

class Person(ObjectType):
    name = String()
    age = Integer()
    sex = String()

class Query(ObjectType):
    person = Field(Person)

    def resolve_person(self, info):
        return Person(name="Jack", age=22, sex="male")

But in one of the examples of Mutations there's a mutate function definition in the resulting class! (e.g. class AddPerson(Mutation):) Why, then, doesn't the plain resolve in the code two blocks up work??

Here's an example from the Interfaces page:

class Human(graphene.ObjectType):
    class Meta:
        interfaces = (Character, )

What's this class Meta doing? There's no explanation whatsoever, as best as I can tell. Yes, I'm reasonably familiar with metaclasses. Still.

SQLAlchemy. I'm trying to move away from the clusterfuck that is NPM because I really would prefer not having hundreds of megabytes of ¯\(ツ)/¯ code from literally-no-one-knows running on my servers.

  1. What is the relationship between graphene.ObjectType and SQLAlchemyType?
  2. What's best practice for the resolvers?
class Department(SQLAlchemyObjectType):
    class Meta:
        model = DepartmentModel

...I guess the attributes are inherited somehow? More details would sure be nice.

In Javascript, objects can be composed in various manner. Object destructuring, for example, is fantastic, and Object.assign is equally effective. With destructuring assignment, Query can look like this:

import weatherResolvers from "./weatherResolvers.js";
import landmassResolvers from "./landmassResolvers.js";
import oceanResolvers from "./oceanResolvers.js";

let query = {
    ...weatherResolvers,
    ...landmassResolvers,
    ...oceanResolvers,
};

Is there a way to achieve this with Python? If so, I didn't see it in the Graphene documentation, although maybe it exists.

I have some more questions, like (from the official documentation):

class MyMutations(graphene.ObjectType):
    create_person = CreatePerson.Field()

# We must define a query for our schema
class Query(graphene.ObjectType):
    person = graphene.Field(Person)

Why is Field an attribute of the CreatePerson mutation in the first and an attribute of graphene in the second?

...But I think I've covered the big ones for the most part.

Finally, I suppose it's entirely possible that all of this is perfectly obvious to the Python metabolizing-and-respirating wizards of the world, but I am only "reasonably competent".

@jkimbo
Copy link
Member

jkimbo commented Mar 26, 2019

@BenSapiens the docs on the website are unfortunately a bit out of date but I recently re-did the documentation for ObjectTypes and you can see it here: https://github.com/graphql-python/graphene/blob/master/docs/types/objecttypes.rst

Also I'm unsure from your comment if you actually want answers to the questions or they are purely rhetorical? If you do want answers then I'm happy to try and answer them one by one.

@Cito
Copy link
Member

Cito commented Mar 26, 2019

Thanks for your concrete and detailed feedback @BenSapiens. Give me 1 or 2 days to answer, since I'm currently very busy, or maybe @jkimbo will answer in the meantime. We can then decide how to improve or amend the docs to make these issues more clear for new users.

@Cito
Copy link
Member

Cito commented Mar 27, 2019

Hi @BenSapiens. Finally found some time to answer your questions.

I understand this GraphQL schema perfectly well. What I don't understand is what exactly is the relationship between this GraphQL schema and the Graphene representation thereof.

The GraphQL schema representation is just a description of the schema in the so called GraphQL Schema Definition Language (SDL). It's a syntax that allows describing the schema independent of the used programming language or platform. That means that this schema description leaves out the functional parts (resolvers) and platform specific details like representation of Enums as server-side values. It describes only the types used in the schema, it is just text, not live and executable.

The Python object on the other hand is part of the executable schema. The ObjectType is one of the classes you need to build such a schema, which has an execute method that can be called to run queries against that schema.

Note that graphql-core (the lower level library used by Graphene) provides a utility function that can automatically create a schema made of Python objects from SDL. That schema will be like the schema created manually, but lack the language and platform specific parts mentioned above. These parts (primarily the resolvers) can be added to a schema created from SDL by defining a resolver map. The Ariadne tool takes this approach. But that's different from the approach taken by Graphene, where you build your schema using Python classes. You don't need the SDL then, it is only shown in the documentation for comparison so that you know which SDL description the Graphene objects correspond to.

Why are there mixed schema and resolvers?

As I wrote above, you can also define the resolvers separately from the SDL using a resolver map. Graphene uses a different approach, since logically the resolvers belong to their respective types. It's a bit like PHP where you put the dynamic parts directly into the page at the location where they are used, and do not define them in separate files. This approach has pros and cons of course.

why does only full_name have a resolver?

This is because most GraphQL implementations and Graphene as well provide you with "trivial resolvers" which you don't need to specify. The trivial resolvers will just look up the corresponding attribute or dictionary key in the object value that has been resolved one level above (or the root object value at the top level). This should really be mentioned in the docs.

How do object types derived from the ObjectType class relate to the Query class (which is also derived from ObjectType)?

Classes derived from ObjectType are used to describe object types in GraphQL. The Query object type is only special in that it is used at the root as the entry point for GraphQL queries. But other than that, it is just an object type like all the others.

How do nested ObjectTypes work? Can they be nested to arbitrary depths?

In the declaration of the object type, when you give another object type as the type of one of its field, you get a nested object type. Yes, there is no limitation in depth.

With Javascript objects it's all perfectly clear to me. This Python, not so much.

Are you referring to the objects that are used to describe the schema or the object values used for the queried data?

This doesn't work. Why doesn't this work?

You should always explain what exactly doesn't work (what do you expect? what happens instead)? I guess what you mean is that this schema doesn't return anything when running a query like {person {name age sex}}. The problem here (besides that you need to write Int instead of Integer) is that you don't have a resolver for the person field. As explained above, therefore Graphene will use the trivial resolver on the root object, which is None. You need to tell Graphene that there is an object further down, by providing a root object with a person object as attribute or a field resolver for the person on the Query object:

def resolve_person(self, info):
    return object()

Then schema.execute('{person {name age sex}}').data will give {'person': {'name': 'Jack', 'age': 22, 'sex': 'male'}}.

In addition, arguments can be passed to a class initialization. They are handled by __init__. Why not here?

I'm not sure what you mean here.

This also doesn't work:

Yes, because resolvers always belong to one of the fields. And the resolver for the whole object must be defined on the object type one level above.

Only this works:

Right, now you're providing a person object value, and then make use of the trivial resolvers for the fields of that object.

Btw, one thing that can be confusing is when you use the same class Person for defining both the object type and the object value. You can do that, but conceptually these are actually two different objects belonging to different domains with usually different base classes. To make that clearer, you can rename the Person object type to PersonType and reserve the Person class for your object values. This could e.g. be a model class of a database ORM, where object values correspond to table rows. But it could also be a dictionary. It just needs to have the same fields if you're relying on the trivial resolvers:

class Query(ObjectType):
    person = Field(Person)

    def resolve_person(self, info):
        return {'name': "Jack", 'age': 22, 'sex': "male"}

This is probably one point that should be better explained in the docs.

in one of the examples of Mutations there's a mutate function definition in the resulting class! (e.g. class AddPerson(Mutation):) Why, then, doesn't the plain resolve in the code two blocks up work??

This is because mutation fields are defined only once at the root level, they execute one clearly defined operation. Not so with query fields. Consider this:

class Query(ObjectType):
    best_friend = Field(Person)
    most_hated_enemy = Field(Person)

If you define the resolver for these fields at the level of Person, you would not know whether to return friend or foe. They must have different resolvers.

What's this class Meta doing? There's no explanation whatsoever, as best as I can tell. Yes, I'm reasonably familiar with metaclasses. Still.

Ok, I can see how this can be confusing because of "class" and "Meta". Actually this has nothing to do with Python metaclasses, it is just meta information (additional information) to tell Graphene more about the object type. In this case, the information which interfaces the object type implements. This syntax is used to distinguish this attribute from the normal fields attributes.

What is the relationship between graphene.ObjectType and SQLAlchemyType?

The SQLAlchemyType is a subclass of ObjectType that uses an SQLAlchemy model to resolve values. You tell it which model to use in the Meta attribute. This is not part of Graphene itself, but belongs to graphene-sqlalchemy. Again, there can be confusion between the object type classes and the model classes, so name them consistently. E.g. Department for the object type and DepartmentModel for the model class, or DepartmentType for the object type and Department for the model class.

...I guess the attributes are inherited somehow? More details would sure be nice.

Yes, they are taken from the model class by default. You can also exclude and include certain fields. This is documented separately in graphene-sqlalchemy. But yes, that project and its documentation needs more love as well. graphene-django currently gets much more attention, tough I personally also favor SQLAlchemy over the Django ORM.

Is there a way to achieve this with Python?

It looks like you're referring to resolver maps here. As explained, that's a different approach from Graphene where you structure your resolvers with your types. I think you can do something similar in Ariadne, where you pass a resolver list, which could be a concatenation of lists imported from different modules. And of course, Python has something similar to the destructuring and spread operators in JavaScript: the * and ** operators.

Why is Field an attribute of the CreatePerson mutation in the first and an attribute of graphene in the second?

In the first case, it's a special field that is provided by that particular mutation. In the second case, it's a general field of the given type.

I hope this answers your questions and will help in identifying where we can improve the documentation.

@BenSapiens
Copy link
Author

@jkimbo, Thanks for the reply. To answer your question, no, these questions are not at all rhetorical; they accurately represent my current state of intellectual development vis-à-vis Graphene (and to some extent GraphQL, having for the most part used only graphql-tag in Javascript).

I really appreciate your response, @Cito. I'm going to let it rattle around in my brain for a while, and explore graphql-core to see what I can see, if that's alright.

@changeling
Copy link

@jkimbo and @Cito, I'd like to suggest this Issue be labeled with Documentation and Enhancement so that folks addressing documentation issues can easily find it for reference. And that similar issues be reviewed and so tagged. This issue is a good example for documentation reviewers.

@changeling
Copy link

changeling commented May 13, 2019

@jkimbo and @Cito, I've also added a rudimentary beginning for an FAQ on the wiki here addressing an issue I struggled with as an example. First, is that ok, and second, is it welcome?

(Fourth, on the semantic side, I always struggle with FAQ grammatically. Whether it should be 'a fack' or 'an eff-a-q.' Feel free to toss your hat in on this. ::grin::)

@Cito
Copy link
Member

Cito commented May 19, 2019

I think using the Wiki is a good idea. Will address this at the next meeting. I hope we can also find someone who will serve particularly as a documentation maintainer for Graphene.

@dvndrsn
Copy link
Contributor

dvndrsn commented May 25, 2019

I submitted a couple of PR for updating documentation.

Adds an autodoc API reference for core Graphene and some nicer hooks for Documentation developer experience: #971

Attempt at explaining resolvers better and giving first time users of the library a smoother transition: #969

There are a couple of other open docs-related PR as well:

I'll be on the call next week, so hopefully we can figure out how to move forward with refining the documentation further!

@changeling
Copy link

changeling commented May 25, 2019

I believe there would be value in harvesting StackOverflow, issues found here and in the other components of the ecosystem, in tutorials and blogs, and elsewhere where relevant, for clear pain-points and creating a triage list of such pain points caught in the wild to address in the docs.

I'm considering where these might best be collected, either within a living issue entry, a wiki page, a TODO-style document in the source, a repo Project a la Improvements, or some other solution.

Thoughts on this, is there value, where (and how) best to collect these for review? IS this something that would be welcome or useful?

@changeling
Copy link

changeling commented May 26, 2019

Added a CORS/CSRF quickie to the graphene-django wiki FAQ.

@jkimbo
Copy link
Member

jkimbo commented Jun 9, 2019

A few PRs improving docs have been merged (thanks @dvndrsn and @changeling !) so I'm going to close this issue for now in favour of tracking this under the main #823 issue.

@jkimbo jkimbo closed this as completed Jun 9, 2019
@BenSapiens
Copy link
Author

Sorry for the radio silence, I intend to come back to this in a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants