|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -from copy import copy |
4 | 3 | from functools import partial |
5 | 4 | from typing import Any, cast |
6 | 5 |
|
|
10 | 9 | BREAK, |
11 | 10 | REMOVE, |
12 | 11 | SKIP, |
| 12 | + DocumentNode, |
13 | 13 | FieldNode, |
14 | 14 | NameNode, |
15 | 15 | Node, |
| 16 | + OperationDefinitionNode, |
16 | 17 | ParallelVisitor, |
17 | 18 | SelectionNode, |
18 | 19 | SelectionSetNode, |
@@ -311,20 +312,34 @@ class TestVisitor(Visitor): |
311 | 312 |
|
312 | 313 | def enter_operation_definition(self, *args): |
313 | 314 | check_visitor_fn_args(ast, *args) |
314 | | - node = copy(args[0]) |
| 315 | + node = args[0] |
315 | 316 | assert len(node.selection_set.selections) == 3 |
316 | 317 | self.selection_set = node.selection_set |
317 | | - node.selection_set = SelectionSetNode(selections=[]) |
| 318 | + # Create new node with empty selection set (immutable pattern) |
| 319 | + new_node = OperationDefinitionNode( |
| 320 | + operation=node.operation, |
| 321 | + name=node.name, |
| 322 | + variable_definitions=node.variable_definitions, |
| 323 | + directives=node.directives, |
| 324 | + selection_set=SelectionSetNode(selections=()), |
| 325 | + ) |
318 | 326 | visited.append("enter") |
319 | | - return node |
| 327 | + return new_node |
320 | 328 |
|
321 | 329 | def leave_operation_definition(self, *args): |
322 | 330 | check_visitor_fn_args_edited(ast, *args) |
323 | | - node = copy(args[0]) |
| 331 | + node = args[0] |
324 | 332 | assert not node.selection_set.selections |
325 | | - node.selection_set = self.selection_set |
| 333 | + # Create new node with original selection set (immutable pattern) |
| 334 | + new_node = OperationDefinitionNode( |
| 335 | + operation=node.operation, |
| 336 | + name=node.name, |
| 337 | + variable_definitions=node.variable_definitions, |
| 338 | + directives=node.directives, |
| 339 | + selection_set=self.selection_set, |
| 340 | + ) |
326 | 341 | visited.append("leave") |
327 | | - return node |
| 342 | + return new_node |
328 | 343 |
|
329 | 344 | edited_ast = visit(ast, TestVisitor()) |
330 | 345 | assert edited_ast == ast |
@@ -391,13 +406,19 @@ def enter(self, *args): |
391 | 406 | check_visitor_fn_args_edited(ast, *args) |
392 | 407 | node = args[0] |
393 | 408 | if isinstance(node, FieldNode) and node.name.value == "a": |
394 | | - node = copy(node) |
395 | 409 | assert node.selection_set |
396 | | - node.selection_set.selections = ( |
397 | | - added_field, |
398 | | - *node.selection_set.selections, |
| 410 | + # Create new selection set with added field (immutable pattern) |
| 411 | + new_selection_set = SelectionSetNode( |
| 412 | + selections=(added_field, *node.selection_set.selections) |
| 413 | + ) |
| 414 | + return FieldNode( |
| 415 | + alias=node.alias, |
| 416 | + name=node.name, |
| 417 | + arguments=node.arguments, |
| 418 | + directives=node.directives, |
| 419 | + nullability_assertion=node.nullability_assertion, |
| 420 | + selection_set=new_selection_set, |
399 | 421 | ) |
400 | | - return node |
401 | 422 | if node == added_field: |
402 | 423 | self.did_visit_added_field = True |
403 | 424 | return None |
@@ -571,30 +592,42 @@ def visit_nodes_with_custom_kinds_but_does_not_traverse_deeper(): |
571 | 592 | # GraphQL.js removed support for unknown node types, |
572 | 593 | # but it is easy for us to add and support custom node types, |
573 | 594 | # so we keep allowing this and test this feature here. |
574 | | - custom_ast = parse("{ a }") |
| 595 | + parsed_ast = parse("{ a }") |
575 | 596 |
|
576 | 597 | class CustomFieldNode(SelectionNode): |
577 | 598 | __slots__ = "name", "selection_set" |
578 | 599 |
|
579 | 600 | name: NameNode |
580 | 601 | selection_set: SelectionSetNode | None |
581 | 602 |
|
582 | | - custom_selection_set = cast( |
583 | | - "FieldNode", custom_ast.definitions[0] |
584 | | - ).selection_set |
585 | | - assert custom_selection_set is not None |
586 | | - custom_selection_set.selections = ( |
587 | | - *custom_selection_set.selections, |
588 | | - CustomFieldNode( |
589 | | - name=NameNode(value="NameNodeToBeSkipped"), |
590 | | - selection_set=SelectionSetNode( |
591 | | - selections=CustomFieldNode( |
592 | | - name=NameNode(value="NameNodeToBeSkipped") |
593 | | - ) |
594 | | - ), |
| 603 | + # Build custom AST immutably |
| 604 | + op_def = cast("OperationDefinitionNode", parsed_ast.definitions[0]) |
| 605 | + assert op_def.selection_set is not None |
| 606 | + original_selection_set = op_def.selection_set |
| 607 | + |
| 608 | + # Create custom field with nested selection |
| 609 | + custom_field = CustomFieldNode( |
| 610 | + name=NameNode(value="NameNodeToBeSkipped"), |
| 611 | + selection_set=SelectionSetNode( |
| 612 | + selections=( |
| 613 | + CustomFieldNode(name=NameNode(value="NameNodeToBeSkipped")), |
| 614 | + ) |
595 | 615 | ), |
596 | 616 | ) |
597 | 617 |
|
| 618 | + # Build new nodes immutably (copy-on-write pattern) |
| 619 | + new_selection_set = SelectionSetNode( |
| 620 | + selections=(*original_selection_set.selections, custom_field) |
| 621 | + ) |
| 622 | + new_op_def = OperationDefinitionNode( |
| 623 | + operation=op_def.operation, |
| 624 | + name=op_def.name, |
| 625 | + variable_definitions=op_def.variable_definitions, |
| 626 | + directives=op_def.directives, |
| 627 | + selection_set=new_selection_set, |
| 628 | + ) |
| 629 | + custom_ast = DocumentNode(definitions=(new_op_def,)) |
| 630 | + |
598 | 631 | visited = [] |
599 | 632 |
|
600 | 633 | class TestVisitor(Visitor): |
|
0 commit comments