From d4a903f76b99077a47dc2c90578ed2d75144a3a7 Mon Sep 17 00:00:00 2001 From: Leszek Hanusz Date: Mon, 29 Jun 2026 16:25:16 +0200 Subject: [PATCH 1/5] Add kind to introspection query root types Fix tests for graphql-core 3.3.0b0 --- gql/utilities/build_client_schema.py | 12 ++++---- gql/utilities/get_introspection_query_ast.py | 6 ++-- gql/utilities/node_tree.py | 26 ++++++++-------- tests/starwars/test_dsl.py | 32 +++++++++++--------- tests/test_transport.py | 28 ++++++++++++++++- tests/test_transport_batch.py | 28 ++++++++++++++++- 6 files changed, 95 insertions(+), 37 deletions(-) diff --git a/gql/utilities/build_client_schema.py b/gql/utilities/build_client_schema.py index 30402868..8b8f4d67 100644 --- a/gql/utilities/build_client_schema.py +++ b/gql/utilities/build_client_schema.py @@ -23,10 +23,10 @@ "description": "Included when true.", "type": { "kind": "NON_NULL", - "name": "None", - "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": "None"}, + "name": None, + "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": None}, }, - "defaultValue": "None", + "defaultValue": None, } ], } @@ -48,10 +48,10 @@ "description": "Skipped when true.", "type": { "kind": "NON_NULL", - "name": "None", - "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": "None"}, + "name": None, + "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": None}, }, - "defaultValue": "None", + "defaultValue": None, } ], } diff --git a/gql/utilities/get_introspection_query_ast.py b/gql/utilities/get_introspection_query_ast.py index 3a59ced4..1ed07e67 100644 --- a/gql/utilities/get_introspection_query_ast.py +++ b/gql/utilities/get_introspection_query_ast.py @@ -37,9 +37,9 @@ def get_introspection_query_ast( schema.select(ds.__Schema.description) schema.select( - ds.__Schema.queryType.select(ds.__Type.name), - ds.__Schema.mutationType.select(ds.__Type.name), - ds.__Schema.subscriptionType.select(ds.__Type.name), + ds.__Schema.queryType.select(ds.__Type.name, ds.__Type.kind), + ds.__Schema.mutationType.select(ds.__Type.name, ds.__Type.kind), + ds.__Schema.subscriptionType.select(ds.__Type.name, ds.__Type.kind), ) schema.select(ds.__Schema.types.select(fragment_FullType)) diff --git a/gql/utilities/node_tree.py b/gql/utilities/node_tree.py index 5f763e48..9474c57f 100644 --- a/gql/utilities/node_tree.py +++ b/gql/utilities/node_tree.py @@ -29,22 +29,21 @@ def _node_tree_recursive( continue attr_value = getattr(obj, key, None) results.append(" " * (indent + 1) + f"{key}:") - if isinstance(attr_value, Iterable) and not isinstance( + if attr_value is None or ( + isinstance(attr_value, Sized) and len(attr_value) == 0 + ): + results.append(" " * (indent + 2) + "None") + elif isinstance(attr_value, Iterable) and not isinstance( attr_value, (str, bytes) ): - if isinstance(attr_value, Sized) and len(attr_value) == 0: + for item in attr_value: results.append( - " " * (indent + 2) + f"empty {type(attr_value).__name__}" - ) - else: - for item in attr_value: - results.append( - _node_tree_recursive( - item, - indent=indent + 2, - ignored_keys=ignored_keys, - ) + _node_tree_recursive( + item, + indent=indent + 2, + ignored_keys=ignored_keys, ) + ) else: results.append( _node_tree_recursive( @@ -92,4 +91,7 @@ def node_tree( # Ignore new field added in graphql-core 3.3.0a12 to keep output compatible ignored_keys.append("nullability_assertion") + # Ignore description field which was added to OperationDefinitionNode in graphql-core 3.3.0b0 + ignored_keys.append("description") + return _node_tree_recursive(obj, ignored_keys=ignored_keys) diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index ca9137a7..a539bb04 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -1044,6 +1044,10 @@ def test_invalid_meta_field_selection(ds): ds.Query.hero.select(DSLMetaField("__type")) +@pytest.mark.skipif( + version.parse(graphql_version) < version.parse("3.3.0b0"), + reason="Requires graphql-core >= 3.3.0b0", +) @pytest.mark.parametrize("option", [True, False]) def test_get_introspection_query_ast(option): @@ -1089,8 +1093,8 @@ def test_get_introspection_query_ast(option): @pytest.mark.skipif( - version.parse(graphql_version) < version.parse("3.3.0a7"), - reason="Requires graphql-core >= 3.3.0a7", + version.parse(graphql_version) < version.parse("3.3.0b0"), + reason="Requires graphql-core >= 3.3.0b0", ) @pytest.mark.parametrize("option", [True, False]) def test_get_introspection_query_ast_is_one_of(option): @@ -1165,7 +1169,7 @@ def test_node_tree_with_loc(ds): definitions: OperationDefinitionNode directives: - empty tuple + None loc: Location @@ -1188,9 +1192,9 @@ def test_node_tree_with_loc(ds): alias: None arguments: - empty tuple + None directives: - empty tuple + None loc: Location @@ -1213,9 +1217,9 @@ def test_node_tree_with_loc(ds): alias: None arguments: - empty tuple + None directives: - empty tuple + None loc: Location @@ -1231,7 +1235,7 @@ def test_node_tree_with_loc(ds): selection_set: None variable_definitions: - empty tuple + None loc: Location @@ -1242,7 +1246,7 @@ def test_node_tree_with_loc(ds): definitions: OperationDefinitionNode directives: - empty tuple + None loc: Location @@ -1265,9 +1269,9 @@ def test_node_tree_with_loc(ds): alias: None arguments: - empty tuple + None directives: - empty tuple + None loc: Location @@ -1288,9 +1292,9 @@ def test_node_tree_with_loc(ds): alias: None arguments: - empty tuple + None directives: - empty tuple + None loc: Location @@ -1304,7 +1308,7 @@ def test_node_tree_with_loc(ds): selection_set: None variable_definitions: - empty tuple + None loc: Location diff --git a/tests/test_transport.py b/tests/test_transport.py index 7c2a5a8f..5dea5598 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -13,14 +13,40 @@ def use_cassette(name): import vcr + import json + + # method to ignore introspection changes in graphql-core 3.3.0b0 + def graphql_body_matcher(r1, r2): + try: + b1 = json.loads(r1.body) + b2 = json.loads(r2.body) + if isinstance(b1, dict) and isinstance(b2, dict): + q1 = b1.get("query", "") + q2 = b2.get("query", "") + if "IntrospectionQuery" in q1 and "IntrospectionQuery" in q2: + return True + return b1 == b2 + elif isinstance(b1, list) and isinstance(b2, list) and len(b1) == len(b2): + for item1, item2 in zip(b1, b2): + q1 = item1.get("query", "") + q2 = item2.get("query", "") + if "IntrospectionQuery" in q1 and "IntrospectionQuery" in q2: + continue + if item1 != item2: + return False + return True + except Exception: + pass + return r1.body == r2.body query_vcr = vcr.VCR( cassette_library_dir=os.path.join( os.path.dirname(__file__), "fixtures", "vcr_cassettes" ), record_mode="new_episodes", - match_on=["uri", "method", "body"], ) + query_vcr.register_matcher("graphql_body", graphql_body_matcher) + query_vcr.match_on = ["uri", "method", "graphql_body"] return query_vcr.use_cassette(name + ".yaml") diff --git a/tests/test_transport_batch.py b/tests/test_transport_batch.py index 671858e7..8cf5edd4 100644 --- a/tests/test_transport_batch.py +++ b/tests/test_transport_batch.py @@ -13,14 +13,40 @@ def use_cassette(name): import vcr + import json + + # method to ignore introspection changes in graphql-core 3.3.0b0 + def graphql_body_matcher(r1, r2): + try: + b1 = json.loads(r1.body) + b2 = json.loads(r2.body) + if isinstance(b1, dict) and isinstance(b2, dict): + q1 = b1.get("query", "") + q2 = b2.get("query", "") + if "IntrospectionQuery" in q1 and "IntrospectionQuery" in q2: + return True + return b1 == b2 + elif isinstance(b1, list) and isinstance(b2, list) and len(b1) == len(b2): + for item1, item2 in zip(b1, b2): + q1 = item1.get("query", "") + q2 = item2.get("query", "") + if "IntrospectionQuery" in q1 and "IntrospectionQuery" in q2: + continue + if item1 != item2: + return False + return True + except Exception: + pass + return r1.body == r2.body query_vcr = vcr.VCR( cassette_library_dir=os.path.join( os.path.dirname(__file__), "fixtures", "vcr_cassettes" ), record_mode="new_episodes", - match_on=["uri", "method", "body"], ) + query_vcr.register_matcher("graphql_body", graphql_body_matcher) + query_vcr.match_on = ["uri", "method", "graphql_body"] return query_vcr.use_cassette(name + ".yaml") From 2aea2dc36444f164e06f996af46900e8d888528e Mon Sep 17 00:00:00 2001 From: Leszek Hanusz Date: Mon, 29 Jun 2026 16:29:25 +0200 Subject: [PATCH 2/5] Restrict aiohttp to <= 3.13.2 for now to avoid unrelated tests break --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f4508b86..7282bf9a 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ ] + tests_requires install_aiohttp_requires = [ - "aiohttp>=3.11.2,<4", + "aiohttp>=3.11.2,<=3.13.2", ] install_requests_requires = [ From 1866923cde2130963701c10484d384bb7c8d8646 Mon Sep 17 00:00:00 2001 From: Leszek Hanusz Date: Mon, 29 Jun 2026 16:41:06 +0200 Subject: [PATCH 3/5] Invert order or kind and name in introspection query to follow new graphql-core version --- gql/utilities/get_introspection_query_ast.py | 4 ++-- tests/starwars/test_dsl.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gql/utilities/get_introspection_query_ast.py b/gql/utilities/get_introspection_query_ast.py index 1ed07e67..6d084599 100644 --- a/gql/utilities/get_introspection_query_ast.py +++ b/gql/utilities/get_introspection_query_ast.py @@ -134,10 +134,10 @@ def get_introspection_query_ast( ) if type_recursion_level >= 1: - current_field = ds.__Type.ofType.select(ds.__Type.kind, ds.__Type.name) + current_field = ds.__Type.ofType.select(ds.__Type.name, ds.__Type.kind) for _ in repeat(None, type_recursion_level - 1): - parent_field = ds.__Type.ofType.select(ds.__Type.kind, ds.__Type.name) + parent_field = ds.__Type.ofType.select(ds.__Type.name, ds.__Type.kind) parent_field.select(current_field) current_field = parent_field diff --git a/tests/starwars/test_dsl.py b/tests/starwars/test_dsl.py index a539bb04..cf5cbe36 100644 --- a/tests/starwars/test_dsl.py +++ b/tests/starwars/test_dsl.py @@ -1045,8 +1045,8 @@ def test_invalid_meta_field_selection(ds): @pytest.mark.skipif( - version.parse(graphql_version) < version.parse("3.3.0b0"), - reason="Requires graphql-core >= 3.3.0b0", + version.parse(graphql_version) < version.parse("3.3.0rc0"), + reason="Requires graphql-core >= 3.3.0rc0", ) @pytest.mark.parametrize("option", [True, False]) def test_get_introspection_query_ast(option): @@ -1093,8 +1093,8 @@ def test_get_introspection_query_ast(option): @pytest.mark.skipif( - version.parse(graphql_version) < version.parse("3.3.0b0"), - reason="Requires graphql-core >= 3.3.0b0", + version.parse(graphql_version) < version.parse("3.3.0rc0"), + reason="Requires graphql-core >= 3.3.0rc0", ) @pytest.mark.parametrize("option", [True, False]) def test_get_introspection_query_ast_is_one_of(option): From cceb7aafa16b009415334edc246c68bf1ebd7ff8 Mon Sep 17 00:00:00 2001 From: Leszek Hanusz Date: Mon, 29 Jun 2026 16:53:31 +0200 Subject: [PATCH 4/5] make check --- gql/utilities/node_tree.py | 3 ++- tests/test_transport.py | 3 ++- tests/test_transport_batch.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/gql/utilities/node_tree.py b/gql/utilities/node_tree.py index 9474c57f..ad4fe755 100644 --- a/gql/utilities/node_tree.py +++ b/gql/utilities/node_tree.py @@ -91,7 +91,8 @@ def node_tree( # Ignore new field added in graphql-core 3.3.0a12 to keep output compatible ignored_keys.append("nullability_assertion") - # Ignore description field which was added to OperationDefinitionNode in graphql-core 3.3.0b0 + # Ignore description field which was added to OperationDefinitionNode + # in graphql-core 3.3.0b0 ignored_keys.append("description") return _node_tree_recursive(obj, ignored_keys=ignored_keys) diff --git a/tests/test_transport.py b/tests/test_transport.py index 5dea5598..132fa11f 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -12,9 +12,10 @@ def use_cassette(name): - import vcr import json + import vcr + # method to ignore introspection changes in graphql-core 3.3.0b0 def graphql_body_matcher(r1, r2): try: diff --git a/tests/test_transport_batch.py b/tests/test_transport_batch.py index 8cf5edd4..b0edf015 100644 --- a/tests/test_transport_batch.py +++ b/tests/test_transport_batch.py @@ -12,9 +12,10 @@ def use_cassette(name): - import vcr import json + import vcr + # method to ignore introspection changes in graphql-core 3.3.0b0 def graphql_body_matcher(r1, r2): try: From 38e534fcc75d79ee12cb63b357be802695a59f68 Mon Sep 17 00:00:00 2001 From: Leszek Hanusz Date: Mon, 29 Jun 2026 17:01:21 +0200 Subject: [PATCH 5/5] Fix mypy type errors with graphql-core 3.3.0rc0 --- gql/dsl.py | 6 +++--- gql/utilities/serialize_variable_values.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gql/dsl.py b/gql/dsl.py index 40c267bd..544af51d 100644 --- a/gql/dsl.py +++ b/gql/dsl.py @@ -416,7 +416,7 @@ def args(self, **kwargs: Any) -> Self: :raises graphql.error.GraphQLError: if argument doesn't exist in directive definition """ - if len(self.ast_directive.arguments) > 0: + if self.ast_directive.arguments and len(self.ast_directive.arguments) > 0: raise AttributeError(f"Arguments for directive @{self.name} already set.") errs = [] @@ -448,7 +448,7 @@ def args(self, **kwargs: Any) -> Self: def __repr__(self) -> str: args_str = ", ".join( f"{arg.name.value}={getattr(arg.value, 'value')}" - for arg in self.ast_directive.arguments + for arg in (self.ast_directive.arguments or ()) ) return f"" @@ -893,7 +893,7 @@ def default(self, default_value: Any) -> Self: def is_valid_directive(self, directive: DSLDirective) -> bool: """Check if directive is valid for Variable definitions.""" - for arg in directive.ast_directive.arguments: + for arg in directive.ast_directive.arguments or (): if isinstance(arg.value, VariableNode): raise GraphQLError( f"Directive @{directive.name} argument value has " diff --git a/gql/utilities/serialize_variable_values.py b/gql/utilities/serialize_variable_values.py index 38ad1995..87185fd5 100644 --- a/gql/utilities/serialize_variable_values.py +++ b/gql/utilities/serialize_variable_values.py @@ -115,7 +115,7 @@ def serialize_variable_values( operation = _get_document_operation(document, operation_name=operation_name) # Serialize every variable value defined for the operation - for var_def_node in operation.variable_definitions: + for var_def_node in operation.variable_definitions or (): var_name = var_def_node.variable.name.value var_type = type_from_ast(schema, var_def_node.type)