graphql-python / gql-next

A Python GraphQL Client library providing ability to validate and make type-safe GraphQL calls
77 stars 7 forks source link

WIP - Feature/custom codec #16

Open ekampf opened 5 years ago

ekampf commented 5 years ago

Allow embedding GraphQL queries in Python code. For example:

GetFilm = gql'''
    query GetFilm {
      film(id: "1") {
        title
        director
      }
    }
'''

result = GetFilm.execute()
film = result.data.film

Should render all the relevant Operation class and result dataclasses and set GetFilm to be the operation class:

# AUTOGENERATED file. Do not Change!
from functools import partial
from typing import Any, Callable, Mapping, List
from enum import Enum
from dataclasses import dataclass, field
from dataclasses_json import dataclass_json
from gql.clients import Client, AsyncIOClient
from types import SimpleNamespace

class GetFilmNamespace(SimpleNamespace):
    from datetime import datetime
    from marshmallow import fields as marshmallow_fields
    DATETIME_FIELD = field(metadata={
        'dataclasses_json': {'encoder': datetime.isoformat, 'decoder': datetime.fromisoformat,
                             'mm_field': marshmallow_fields.DateTime(format='iso')}})

    @dataclass_json
    @dataclass
    class GetFilm:
        __QUERY__ = """

    query GetFilm {
      film(id: "1") {
        title
        director
      }
    }

        """

        @dataclass_json
        @dataclass
        class GetFilmData():
            @dataclass_json
            @dataclass
            class Film():
                title: str
                director: str

            film: Film = None

        data: GetFilmData = None
        errors: Any = None

        @classmethod
        def execute(cls, on_before_callback: Callable[[Mapping[str, str], Mapping[str, str]], None] = None):
            client = Client('schemaurl')
            variables = None
            response_text = client.call(cls.__QUERY__, variables=variables, on_before_callback=on_before_callback)
            return cls.from_json(response_text)

        @classmethod
        async def execute_async(cls, on_before_callback: Callable[[Mapping[str, str], Mapping[str, str]], None] = None):
            client = AsyncIOClient('schemaurl')
            variables = None
            response_text = await client.call(cls.__QUERY__, variables=variables, on_before_callback=on_before_callback)
            return cls.from_json(response_text)

GetFilm = GetFilmNamespace.GetFilm

result = GetFilm.execute()
film = result.data.film
aviv-ebates commented 5 years ago

pls use a syntax that looks like python, so that various tools that highlight/parse python don't break:

Foo = gql(''' .... ''')

some parsers/tools/highlighters might freak out over gql''' pattern.

(In practice, both my primary tools - Notepad++ and Pygmentize don't have any issues with gql''').

ekampf commented 5 years ago

@aviv-ebates I thought about this too and checked the IDEs I have:

The con of using a function syntax is that it can be confused by the reader as a real function call. Having an "invalid" python code there immediately strikes the eye as out of the ordinary - something special happens here that you need to understand.

As far as IDEs go, I looked at how pyxl sole it and they simply provide an extension to IDEs: https://github.com/christoffer/pycharm-pyxl https://github.com/yyjhao/sublime-pyxl

Out syntax is way simpler so it shouldn't be a problem to do the same.

As for non IDE tools - like pylint, mypy etc - when they do open(filename) codec should run automatically and they should get the generated code and never see the gql syntax. (This poses other problems - how do we make this generated code not break the users build on pylint\flake8\whatever he's using)

WDYT?

ekampf commented 5 years ago

Another problem with the gql( ... ) syntax is that if your code has import gql said IDEs will definitely not like it...

aviv-ebates commented 5 years ago

I would like for all tools - esp lints - to run on source code (i.e., what the user actually types) rather then generated code; mypy only works on the AST, but other tools (pylint) check for code formatting.

I don't understand the issue with import gql - it's a real name, isn't it?

As far as the user being confused with gql() looking like a function:

Also, making this section valid python would allow us to provide a good error message if the user somehow didn't load our custom encoder, rather than having the python loader error out.

aviv-ebates commented 5 years ago

If mypy sees the generated code, it might show up errors that don't make sense to the user; I'm sort-of guessing the generated code should end up being just GetFilm = from .generated_gql_stuff import GetFilm, and all the rest goes in the new file.

ekampf commented 5 years ago

@aviv-ebates isn't the whole point of strong typing the response objects is for these tools to be able to recognize them? For example:

GetFilm = gql'''
    query GetFilm {
      film(id: "1") { title, director }
    }
'''

result = GetFilm.execute()
film = result.data.starship

I would like this code to throw some lint\mypy error that data doesn't have a starship property

aviv-ebates commented 5 years ago

Like we talked offline - for some things (like "exactly 1 space before =") we want the tool to see the original code, and for others (static typing) to see the generated stuff (but we need to make sure the error messages make sense). I'm a little surprised by open() considering the encoding directive, but the tool might not have the codec loaded, and it might not even be written in Python anyway.