graphql-python / gql

A GraphQL client in Python

Home Page:https://gql.readthedocs.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

3.5 Beta: overlapping_fields_can_be_merged: directives are None

theCapypara opened this issue · comments

Describe the bug
I upgraded to the 3.5 Beta and now queries fail in overlapping_fields_can_be_merged.py in get_stream_directive. The directives are None, if I go up the stack they are always None. They are read from node1 and node2 in lines 563 - 564, and they are already None at the nodes.

...

  File "/home/marco/dev/skytemple/skytemple/files/skytemple_files/common/spritecollab/client.py", line 671, in execute_query
    return cast(Query, await self._session.execute(dsl_gql(*fragments, query)))
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/gql/client.py", line 1629, in execute
    result = await self._execute(
             ^^^^^^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/gql/client.py", line 1817, in _execute
    return await self._execute_with_retries(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/backoff/_async.py", line 151, in retry
    ret = await target(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/gql/client.py", line 1790, in _execute_once
    answer = await super()._execute(
             ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/gql/client.py", line 1520, in _execute
    self.client.validate(document)
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/gql/client.py", line 172, in validate
    validation_errors = validate(self.schema, document)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/validation/validate.py", line 73, in validate
    visit(document_ast, TypeInfoVisitor(type_info, ParallelVisitor(visitors)))
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/language/visitor.py", line 257, in visit
    result = visit_fn(node, key, parent, path, ancestors)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/utilities/type_info.py", line 280, in enter
    result = fn(node, *args)
             ^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/language/visitor.py", line 339, in enter
    result = fn(node, *args)
             ^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/validation/rules/overlapping_fields_can_be_merged.py", line 76, in enter_selection_set
    conflicts = find_conflicts_within_selection_set(
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/validation/rules/overlapping_fields_can_be_merged.py", line 210, in find_conflicts_within_selection_set
    collect_conflicts_between_fragments(
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/validation/rules/overlapping_fields_can_be_merged.py", line 324, in collect_conflicts_between_fragments
    collect_conflicts_between(
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/validation/rules/overlapping_fields_can_be_merged.py", line 502, in collect_conflicts_between
    conflict = find_conflict(
               ^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/validation/rules/overlapping_fields_can_be_merged.py", line 565, in find_conflict
    if not same_streams(directives1, directives2):
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/validation/rules/overlapping_fields_can_be_merged.py", line 622, in same_streams
    stream1 = get_stream_directive(directives1)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marco/.virtualenvs/skytemple/lib/python3.11/site-packages/graphql/validation/rules/overlapping_fields_can_be_merged.py", line 613, in get_stream_directive
    for directive in directives:
TypeError: 'NoneType' object is not iterable

field1 and field2 are:

field1:

parent_type1.__dict__ =  {'name': 'Sprite', 'description': 'A single sprite for a single action.', 'extensions': {}, 'ast_node': None, 'extension_ast_nodes': (), '_fields': <function build_client_schema.<locals>.build_object_def.<locals>.<lambda> at 0x7fd2293791c0>, '_interfaces': <function build_client_schema.<locals>.build_object_def.<locals>.<lambda> at 0x7fd229379120>, 'is_type_of': None, 'interfaces': (), 'fields': {'action': <GraphQLField <GraphQLNonNull <GraphQLScalarType 'String'>>>, 'locked': <GraphQLField <GraphQLNonNull <GraphQLScalarType 'Boolean'>>>, 'animUrl': <GraphQLField <GraphQLNonNull <GraphQLScalarType 'String'>>>, 'offsetsUrl': <GraphQLField <GraphQLNonNull <GraphQLScalarType 'String'>>>, 'shadowsUrl': <GraphQLField <GraphQLNonNull <GraphQLScalarType 'String'>>>}}
node1.to_dict() = {'alias': None, 'arguments': [], 'directives': None, 'kind': 'field', 'name': {'kind': 'name', 'value': 'action'}, 'nullability_assertion': None, 'selection_set': None}
def1.__dict__ = {'args': {}, 'ast_node': None, 'deprecation_reason': None, 'description': 'Action of this sprite.', 'extensions': {}, 'resolve': None, 'subscribe': None, 'type': String!}

field2:

parent_type2.__dict__ =  {'name': 'CopyOf', 'description': 'A sprite, which is a copy of another sprite.', 'extensions': {}, 'ast_node': None, 'extension_ast_nodes': (), '_fields': <function build_client_schema.<locals>.build_object_def.<locals>.<lambda> at 0x7fd229378900>, '_interfaces': <function build_client_schema.<locals>.build_object_def.<locals>.<lambda> at 0x7fd229378860>, 'is_type_of': None, 'interfaces': (), 'fields': {'action': <GraphQLField <GraphQLNonNull <GraphQLScalarType 'String'>>>, 'locked': <GraphQLField <GraphQLNonNull <GraphQLScalarType 'Boolean'>>>, 'copyOf': <GraphQLField <GraphQLNonNull <GraphQLScalarType 'String'>>>}}
node2.to_dict() = {'alias': None, 'arguments': [], 'directives': None, 'kind': 'field', 'name': {'kind': 'name', 'value': 'action'}, 'nullability_assertion': None, 'selection_set': None}
def2.__dict__ = {'args': {}, 'ast_node': None, 'deprecation_reason': None, 'description': 'Action of this sprite.', 'extensions': {}, 'resolve': None, 'subscribe': None, 'type': String!}

To Reproduce
Using gql and aiohttp 3.9., query the following GraphQL server with the following document (JSON encoded, sorry I don't know any way to get the raw query and variables out of the document:

Server: https://spriteserver.pmdcollab.org
Query document: https://gist.github.com/theCapypara/2c38f3c1da9607010627a4c03979d74f

Expected behavior
The query returns the result.

System info (please complete the following information):

  • OS: Arch Linux
  • Python version: Python 3.11
  • gql version: 3.5.0b7
  • graphql-core version: 3.3.0a3

This blocks my project from upgrading to Python 3.12. I have been anticipating aiohttp finally upgrading to be 3.12 compatible and am now stuck due to this.

PS: I know the dependencies are currently locked so that aiohttp 3.8 is used for Python 3.11, however I was unable to get 3.12 compiled locally. If this error is because the combination 3.11 + aiohttp 3.9 is not supported, I'm sorry!

If the info I provided doesn't help and the example is too complicated, I'd be happy to try and get a more compact example out of this, but I hope this may already be enough to pin-point the issue.

Please use the print_ast method (print_ast(dsl_gql(*fragments, query))) to get the GraphQL query and post it here.

Ah thanks!

fragment SpriteUnionAsSprite on Sprite {
  action
  animUrl
  offsetsUrl
  shadowsUrl
}

fragment SpriteUnionAsCopyOf on CopyOf {
  action
  copyOf
}

fragment CreditFields on Credit {
  id
  name
  contact
  discordHandle
}

{
  monster(filter: [3]) {
    id
    name
    f_0: manual(path: "") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_1: manual(path: "0001") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_2: manual(path: "0003") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_3: manual(path: "0002") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_4: manual(path: "0001/0001") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_5: manual(path: "0000/0001") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_6: manual(path: "0003/0001") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_7: manual(path: "0002/0001") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_8: manual(path: "0000/0000/0002") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_9: manual(path: "0000/0001/0002") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_10: manual(path: "0003/0001/0002") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
    f_11: manual(path: "0003/0000/0002") {
      path
      fullName
      isShiny
      isFemale
      canon
      portraits {
        phase
        modifiedDate
        emotions {
          emotion
          url
        }
        emotionsFlipped {
          emotion
          url
        }
        sheetUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
      sprites {
        phase
        modifiedDate
        actions {
          ...SpriteUnionAsSprite
          ...SpriteUnionAsCopyOf
        }
        zipUrl
        creditPrimary {
          ...CreditFields
        }
        creditSecondary {
          ...CreditFields
        }
      }
    }
  }
}

I am not able to reproduce your problem on my machine with gql v3.5.0b7 and Python 3.12

I downloaded the schema using gql-cli --print-schema https://spriteserver.pmdcollab.org/graphql > schema.graphql

Then I tried to run the test.py file in attachement validating the query you provided, and it seems to work correctly.

Maybe the schema you provided is different than mine?

test.zip
schema.graphql.txt

That's very odd. I don't provide the schema, I let gql handle this automatically, but I checked and the schema it uses internally is the same you shared.

The test script you sent also works for me. I can tell it's seemingly going the same code paths but the directives are actually empty tuples there and so it doesn't crash.

I was also able to reproduce this with a condensed version of the DSL my app generates. Here is essentially your test script but with DSL:

https://gist.github.com/theCapypara/8d4ef3d7229fa03b57d392b325e5bcba

So I guess this has to be a DSL issue.

Yes, quite odd...

I was able to reproduce the problem and reduce it to:

from gql import Client, gql                                                                                             
from gql.dsl import DSLSchema, DSLFragment, dsl_gql, DSLQuery, print_ast                                                
                                                                                                                        
with open("./schema.graphql") as f:                                                                                     
    schema_str = f.read()                                                                                               
                                                                                                                        
client = Client(schema=schema_str)                                                                                      
ds = DSLSchema(client.schema)                                                                                           
                                                                                                                        
sprite = DSLFragment("SpriteUnionAsSprite")                                                                             
sprite.on(ds.Sprite)                                                                                                    
sprite.select(                                                                                                          
    ds.Sprite.action,                                                                                                   
)                                                                                                                       
copy_of = DSLFragment("SpriteUnionAsCopyOf")                                                                            
copy_of.on(ds.CopyOf)                                                                                                   
copy_of.select(                                                                                                         
    ds.CopyOf.action,                                                                                                   
)                                                                                                                       
                                                                                                                        
query = ds.Query.monster.select(                                                                                        
    ds.Monster.manual(path="").select(                                                                                  
        ds.MonsterForm.sprites.select(                                                                                  
            ds.MonsterFormSprites.actions.select(sprite, copy_of),                                                      
        ),                                                                                                              
    ),                                                                                                                  
)                                                                                                                       
                                                                                                                        
q = dsl_gql(sprite, copy_of, DSLQuery(query))                                                                           
                                                                                                                        
client.validate(q)

The PR #448 should fix your problem.

The DSL code was not initializing the directives correctly as you noticed.

Thanks for your bug report!

Thank you! I can confirm the PR fixes the issue both in the app and its tests.