profusion / sgqlc

Simple GraphQL Client
https://sgqlc.readthedocs.io/
ISC License
513 stars 85 forks source link

Explicitly passing None as input for nullable Type definitions produces exception #163

Closed jkbak closed 3 years ago

jkbak commented 3 years ago

Even though type_field is not marked as non-null, passing None as it's value produces an exception. I think it's a bug since graphql allows explicitly passing null.

sgqlc: 14.0 python: 3.9

Code to reproduce:

import sgqlc.types
from sgqlc.operation import Operation

test_schema = sgqlc.types.Schema()

class TestType(sgqlc.types.Type):
    __schema__ = test_schema
    __field_names__ = ("type_field",)
    type_field = sgqlc.types.Field(sgqlc.types.non_null("TypeField"), graphql_name="typeField")

class TypeFieldInput(sgqlc.types.Input):
    __schema__ = test_schema
    __field_names__ = ("nested_field",)
    nested_field = sgqlc.types.Field(sgqlc.types.non_null(sgqlc.types.Int), graphql_name="nestedField")

class TestMutationInput(sgqlc.types.Input):
    __schema__ = test_schema
    __field_names__ = ("type_field",)
    type_field = sgqlc.types.Field(
        TypeFieldInput, graphql_name="typeField"
    )

class Mutation(sgqlc.types.Type):
    __schema__ = test_schema
    __field_names__ = (

        "test_mutation",

    )

    test_mutation = sgqlc.types.Field(
        sgqlc.types.non_null(TestType),
        graphql_name="testMutation",
        args=sgqlc.types.ArgDict(
            (
                (
                    "input",
                    sgqlc.types.Arg(
                        sgqlc.types.non_null(TestMutationInput),
                        graphql_name="input",
                        default=None,
                    ),
                ),
            )
        ),
    )

if __name__ == '__main__':
    op = Operation(test_schema.Mutation)
    op.test_mutation(input={"typeField": None})
    print(op)

Exception traceback:

Traceback (most recent call last):
  File "/home/jbak/sandbox-env/sgqlc_null.py", line 55, in <module>
    print(op)
  File "/home/jbak/sandbox-env/venv/lib/python3.9/site-packages/sgqlc/operation/__init__.py", line 2231, in __str__
    return self.__to_graphql__()
  File "/home/jbak/sandbox-env/venv/lib/python3.9/site-packages/sgqlc/operation/__init__.py", line 2201, in __to_graphql__
    selections = self.__selection_list.__to_graphql__(
  File "/home/jbak/sandbox-env/venv/lib/python3.9/site-packages/sgqlc/operation/__init__.py", line 1850, in __to_graphql__
    s.append(v.__to_graphql__(
  File "/home/jbak/sandbox-env/venv/lib/python3.9/site-packages/sgqlc/operation/__init__.py", line 1546, in __to_graphql__
    args = self.__field__.args.__to_graphql_input__(
  File "/home/jbak/sandbox-env/venv/lib/python3.9/site-packages/sgqlc/types/__init__.py", line 2394, in __to_graphql_input__
    args.append(p.__to_graphql_input__(v))
  File "/home/jbak/sandbox-env/venv/lib/python3.9/site-packages/sgqlc/types/__init__.py", line 2221, in __to_graphql_input__
    v = self.type.__to_graphql_input__(value, indent, indent_string)
  File "/home/jbak/sandbox-env/venv/lib/python3.9/site-packages/sgqlc/types/__init__.py", line 979, in __to_graphql_input__
    return t.__to_graphql_input__(value, indent, indent_string)
  File "/home/jbak/sandbox-env/venv/lib/python3.9/site-packages/sgqlc/types/__init__.py", line 2628, in __to_graphql_input__
    vs = f.type.__to_graphql_input__(v, indent, indent_string)
  File "/home/jbak/sandbox-env/venv/lib/python3.9/site-packages/sgqlc/types/__init__.py", line 2620, in __to_graphql_input__
    v = value[f.graphql_name]
TypeError: 'NoneType' object is not subscriptable

For now we subclassed sgqlc.types.Input and added some code (between change_begin and change_end) to __to_graphql_input__ method but maybe there's a cleaner way to fix this

class CustomInput(sgqlc.types.Input):
    # custom Input class with overloaded __to_graphql_input__ method to support explicitly setting none as field value

    @classmethod
    def __to_graphql_input__(cls: Type[sgqlc.types.Input], value, indent=0, indent_string="  "):
        args = []
        if isinstance(value, sgqlc.types.Variable):
            return sgqlc.types.Variable.__to_graphql_input__(value, indent, indent_string)
        elif isinstance(value, sgqlc.types.Input):
            value = value.__json_data__

        for f in cls:
            try:
                v = value[f.graphql_name]
            except KeyError:
                try:
                    # previous versions allowed Python name as dict keys
                    v = value[f.name]
                except KeyError:  # pragma: no cover
                    continue

            # change begin
            if v is not None:
                vs = f.type.__to_graphql_input__(v, indent, indent_string)
            else:
                vs = "null"
            # change end

            args.append("%s: %s" % (f.graphql_name, vs))

        return "{" + ", ".join(args) + "}"
barbieri commented 3 years ago

I'm on vacation this week, next week I'll take a look to see a fix, but looks like we need to handle the if value is None: *beforethefor f in cls`