-
Notifications
You must be signed in to change notification settings - Fork 50
/
Copy pathrename_schema.py
1049 lines (943 loc) · 50.9 KB
/
rename_schema.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2019-present Kensho Technologies, LLC.
"""Implement renaming and suppressing parts of a GraphQL schema.
There are two ways to rename a part of a schema: 1-1 renaming and 1-many renaming.
1-1 renaming replaces the name of a type, field, or enum value in the schema. For instance, given
the following part of a schema:
type Dog {
name: String
}
type Human {
pet: Dog
}
1-1 renaming "Dog" to "NewDog" on a schema containing this object type (but not containing a
type named "NewDog") would produce a schema almost identical to the original schema except
with the NewDog object type replacing Dog everywhere it appears.
type NewDog {
name: String
}
type Human {
pet: NewDog
}
If "Dog" also appeared as a field in the schema's root type, it would be renamed to "NewDog" there
as well.
1-many renaming is an operation intended specifically for any of the following:
- fields in object types
- fields in interface types
- enum values
In 1-many renaming, the same field or enum value is mapped to multiple names. For instance, given
the following type in a schema:
type Dog {
name: String
}
1-many renaming the "Dog" type's "name" field to "name" and "secondname" would produce a schema
almost identical to the original schema except with both fields representing the same underlying
data.
type Dog {
name: String
secondname: String
}
Suppressing part of the schema removes it altogether. For instance, given the following part of a
schema:
type Dog {
name: String
}
suppressing "Dog" would produce an otherwise-identical schema but with that type (and therefore all
its fields) removed. If "Dog" also appeared as a field in the schema's root type, it would be
removed there as well.
For both 1-1 or 1-many renaming, renamings only apply to types, fields, and enum values that exist
in the original schema. For example, if a schema contains a type named "Foo" and a type named "Bar"
but not a type named "Baz" and type_renamings maps "Foo" to "Bar" and "Bar" to "Baz", then the
renamed schema will contain a type named "Bar" (corresponding to the original schema's type "Foo")
and a type named "Baz" (corresponding to the original schema's type "Bar"), instead of containing
two types both named "Baz".
Field renamings may produce "illegal" schema states in the process of renaming, but they are legal
as long as the end result is a legal schema. For example, if a schema contains a type named
type_name that contains just two fields named "foo" and "bar", and
field_renamings == {"type_name": {"foo": {"foo", "bar"}, "bar": {"baz", "quux"}}}
this would be a legal renaming. Even though applying the renaming for "foo" first would produce an
intermediate state with two fields named "bar", the end result has no naming collisions.
Field renaming operations take place before type renamings, so all field renamings should be
specified in terms of the name of the type in the original schema. For example, if a schema
contains a single type named "Foo" that contains a field named "bar", then to produce a schema
with a single type named "Baz" containing a field named "quux", the renamings could be as
follows:
type_renamings == {"Foo": "Baz"}
field_renamings == {"Foo": {"bar": "quux"}}
Note that field_renamings == {"Baz": {"bar": "quux"}} would not produce the desired
result because "Baz" is not a type in the original schema.
Operations that are already supported:
- 1-1 renaming of object types, unions, enums, and interfaces.
- Suppressing types that don't implement an interface.
- Suppressing unions.
- 1-1 and 1-many renamings for fields belonging to object types.
- Suppressions for fields belonging to object types.
- Renamings and suppressions for scalar types.
Operations that are not yet supported but will be implemented:
- Suppressions for enums, interfaces, and object types that implement interfaces.
- Renamings and suppressions for fields that belong to either interface types or object types that
implement interfaces.
- Renamings and suppressions for enum values.
Renaming constraints:
- If you suppress all member types in a union, you must also suppress the union.
- If you suppress a type Foo, no other type Bar may keep fields of type Foo (those fields must be
suppressed). However, if type Foo has a field of that type Foo, it is legal to suppress type Foo
without explicitly suppressing that particular field.
- If you suppress all the fields of a type Foo, then the type Foo must also be suppressed in
type_renamings.
- You may not suppress all types in the schema's root type.
- All names must be valid GraphQL names.
- Names may not conflict with each other. For instance, you may not rename both "Foo" and "Bar" to
"Baz". You also may not rename anything to "Baz" if a type "Baz" already exists and is not also
being renamed or suppressed. The same rules apply for fields that belong to the same type, since
they share a namespace as well.
- No-op renamings are not allowed meaning that:
- A string type_name may be in type_renamings only if there exists a type in the original schema
named type_name (since otherwise that entry would not affect any type in the schema).
- If string type_name is in type_renamings, then type_renamings[type_name] != type_name (since
if they were the same, then applying the renaming would not change the type named type_name).
- A string type_name may be in field_renamings only if there exists a type in the original
schema named type_name and that type wouldn't get suppressed by type_renamings (since
otherwise that entry would not affect any type in the schema).
- If type_name is in field_renamings, a string field_name may be in field_renamings[type_name]
only if the type named type_name in the original schema contains a field named field_name in the
original schema (since otherwise that entry would not affect any field in the schema).
- If type_name is in field_renamings and field_name is in field_renamings[type_name], then
field_renamings[type_name][field_name] != {field_name} (since if this were the case, then
applying the renaming would not change the field named field_name).
"""
from collections import namedtuple
from copy import copy
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union, cast
from graphql import (
DocumentNode,
EnumTypeDefinitionNode,
FieldDefinitionNode,
InterfaceTypeDefinitionNode,
Node,
ObjectTypeDefinitionNode,
UnionTypeDefinitionNode,
build_ast_schema,
)
from graphql.language.visitor import IDLE, REMOVE, Visitor, VisitorAction, visit
from graphql.pyutils import FrozenList
import six
from ..ast_manipulation import get_ast_with_non_null_and_list_stripped
from .utils import (
CascadingSuppressionError,
InvalidNameError,
NoOpRenamingError,
RenameTypes,
RenameTypesT,
SchemaRenameNameConflictError,
SchemaTransformError,
builtin_scalar_type_names,
check_ast_schema_is_valid,
get_copy_of_node_with_new_name,
get_query_type_name,
is_valid_nonreserved_name,
)
RenamedSchemaDescriptor = namedtuple(
"RenamedSchemaDescriptor",
(
"schema_ast", # Document, AST representing the renamed schema
"schema", # GraphQLSchema, representing the same schema as schema_ast
"reverse_name_map", # Dict[str, str], renamed type/query type field name to original name
# reverse_name_map only contains names that were changed
"reverse_field_name_map", # Dict[str, Dict[str, str]], mappings type names in the original
# schema to dicts mapping renamed field names to their original names. It contains entries
# solely for fields whose names were renamed.
),
)
# AST visitor functions can return a number of different things, such as returning a Node (to update
# that node) or returning a special value specified in graphql.visitor's VisitorAction.
VisitorReturnType = Union[Node, VisitorAction]
def rename_schema(
schema_ast: DocumentNode,
type_renamings: Mapping[str, Optional[str]],
field_renamings: Mapping[str, Mapping[str, Set[str]]],
) -> RenamedSchemaDescriptor:
"""Create a RenamedSchemaDescriptor; rename/suppress types and fields.
Any object type, interface type, enum type, or field of the root type/query type has a name. Let
the name be called type_name. If type_renamings.get(type_name, type_name) is not None, the type
or field of the root type/query type will be renamed to the returned value. If the value is
None, it will be suppressed in the renamed schema and queries will not be able to access it.
Fields may also be renamed or suppressed if they belong to object types that don't implement an
interface. For an object type named type_name, field_renamings.get(type_name, {}) contains
the renamings for the fields belonging to that type.
If a type or field doesn't appear in the renamings arguments, it will be unchanged. Directives
will never be renamed.
In addition, some operations have not been implemented yet (see module-level docstring for more
details).
Args:
schema_ast: represents a valid schema that does not contain extensions, input object
definitions, mutations, or subscriptions, whose fields of the query type share
the same name as the types they query. Not modified by this function
type_renamings: maps original type name to renamed name or None (for type suppression). A
type named "Foo" will be unchanged iff type_renamings does not map "Foo" to
anything, i.e. "Foo" not in type_renamings
field_renamings: maps type names to the field renamings for that type. The renamings map
field names belonging to the type to a set of field names for the
renamed schema
Returns:
RenamedSchemaDescriptor containing the AST of the renamed schema, and the maps of renamed
type/field names to original names. Only renamed names will be included in the maps.
Raises:
- CascadingSuppressionError if type/field suppression would require further suppressions
- SchemaTransformError if type_renamings suppressed every type. Note that this is a
superclass of CascadingSuppressionError, InvalidNameError, SchemaStructureError, and
SchemaRenameNameConflictError, so handling exceptions of type SchemaTransformError will
also catch all of its subclasses. This will change after the error classes are modified so
that errors can be fixed programmatically, at which point it will make sense for the user
to attempt to treat different errors differently
- NotImplementedError if type_renamings attempts to suppress an enum, an interface, or a
type implementing an interface
- InvalidNameError if the schema contains an invalid type name, or if the user attempts
to rename a type to an invalid name. A name is considered invalid if it does not consist
of alphanumeric characters and underscores, if it starts with a numeric character, or
if it starts with double underscores
- SchemaStructureError if the schema does not have the expected form; in particular, if
the AST does not represent a valid schema, if any query type field does not have the
same name as the type that it queries, if the schema contains type extensions or
input object definitions, or if the schema contains mutations or subscriptions
- SchemaRenameNameConflictError if there are name conflicts between the renamed types or
fields
- NoOpRenamingError if the renamings contain no-op renamings
"""
# Check input schema satisfies various structural requirements
check_ast_schema_is_valid(schema_ast)
schema = build_ast_schema(schema_ast)
query_type = get_query_type_name(schema)
_ensure_no_unsupported_suppressions(schema_ast, type_renamings)
# Rename types, interfaces, enums, unions and suppress types, unions
schema_ast, reverse_name_map, reverse_field_name_map = _rename_and_suppress_types_and_fields(
schema_ast, type_renamings, field_renamings, query_type
)
schema_ast = _rename_and_suppress_query_type_fields(schema_ast, type_renamings, query_type)
return RenamedSchemaDescriptor(
schema_ast=schema_ast,
schema=build_ast_schema(schema_ast),
reverse_name_map=reverse_name_map,
reverse_field_name_map=reverse_field_name_map,
)
def _ensure_no_unsupported_suppressions(
schema_ast: DocumentNode, type_renamings: Mapping[str, Optional[str]]
) -> None:
"""Confirm type_renamings has no enums, interfaces, or interface implementation suppressions."""
visitor = SuppressionNotImplementedVisitor(type_renamings)
visit(schema_ast, visitor)
if (
not visitor.unsupported_enum_suppressions
and not visitor.unsupported_interface_suppressions
and not visitor.unsupported_interface_implementation_suppressions
):
return
# Otherwise, attempted to suppress something we shouldn't suppress.
error_message_components = [
f"Type renamings {type_renamings} attempted to suppress parts of the schema for which "
f"suppression is not implemented yet."
]
if visitor.unsupported_enum_suppressions:
error_message_components.append(
f"Type renamings mapped these schema enums to None: "
f"{visitor.unsupported_enum_suppressions}, attempting to suppress them. However, "
f"type renaming has not implemented enum suppression yet."
)
if visitor.unsupported_interface_suppressions:
error_message_components.append(
f"Type renamings mapped these schema interfaces to None: "
f"{visitor.unsupported_interface_suppressions}, attempting to suppress them. However, "
f"type renaming has not implemented interface suppression yet."
)
if visitor.unsupported_interface_implementation_suppressions:
error_message_components.append(
f"Type renamings mapped these object types to None: "
f"{visitor.unsupported_interface_implementation_suppressions}, attempting to suppress "
f"them. Normally, this would be fine. However, these types each implement at least one "
f"interface and type renaming has not implemented this particular suppression yet."
)
error_message_components.append(
"To avoid these suppressions, remove the mappings from the type_renamings argument."
)
raise NotImplementedError("\n".join(error_message_components))
def _rename_and_suppress_types_and_fields(
schema_ast: DocumentNode,
type_renamings: Mapping[str, Optional[str]],
field_renamings: Mapping[str, Mapping[str, Set[str]]],
query_type: str,
) -> Tuple[DocumentNode, Dict[str, str], Dict[str, Dict[str, str]]]:
"""Rename and suppress types, enums, interfaces, fields using renamings.
The query type will not be renamed.
The input schema AST will not be modified.
Args:
schema_ast: schema that we're returning a modified version of
type_renamings: maps original type name to renamed name or None (for type suppression). A
type named "Foo" will be unchanged iff type_renamings does not map "Foo" to
anything, i.e. "Foo" not in type_renamings
field_renamings: maps type names to the field renamings for that type. The renamings map
field names belonging to the type to a set of field names for the
renamed schema
query_type: name of the query type, e.g. 'RootSchemaQuery'
Returns:
Tuple containing the modified version of the schema AST, the renamed type name to original
type name map, and the renamed field name to original field name map. The maps contain
entries for all non-suppressed types/ fields that were changed.
Raises:
- InvalidNameError if the user attempts to rename a type or field to an invalid name
- SchemaRenameNameConflictError if the rename causes name conflicts
- NoOpRenamingError if renamings contains no-op renamings
"""
visitor = RenameSchemaTypesVisitor(type_renamings, field_renamings, query_type)
renamed_schema_ast = visit(schema_ast, visitor)
if (
visitor.object_types_to_suppress
or visitor.union_types_to_suppress
or visitor.fields_to_suppress
):
error_message_components = [
"Renamings would require further suppressions to produce a valid renamed schema."
]
if visitor.object_types_to_suppress:
error_message_components.append(
f"The following object types have no non-suppressed fields, which is invalid: "
f"{sorted(visitor.object_types_to_suppress)}. To fix this, suppress the "
f"previously-mentioned types using the type_renamings argument of rename_schema."
)
if visitor.union_types_to_suppress:
error_message_components.append(
f"The following union types have no non-suppressed types, which is invalid: "
f"{sorted(visitor.union_types_to_suppress)}. To fix this, suppress the "
"previously-mentioned unions using the type_renamings argument of rename_schema."
)
if visitor.fields_to_suppress:
for object_type in visitor.fields_to_suppress:
error_message_components.append(f"Object type {object_type} contains: ")
error_message_components.extend(
(
f"field {field} of suppressed type "
f"{visitor.fields_to_suppress[object_type][field]}, "
for field in visitor.fields_to_suppress[object_type]
)
)
error_message_components.append(
"A schema containing a field that is of a nonexistent type is invalid. To fix "
"this, suppress the previously-mentioned fields using the field_renamings argument "
"of rename_schema."
)
error_message_components.append(
"Note that adding suppressions may lead to other types, fields, etc. requiring "
"suppression so you may need to iterate on this before getting a legal schema."
)
raise CascadingSuppressionError("\n".join(error_message_components))
if visitor.invalid_type_names or visitor.invalid_field_names:
error_message_components = [
"Applying the renaming would involve names that are not valid, non-reserved "
"GraphQL names. Valid, non-reserved GraphQL names must consist of only alphanumeric "
"characters and underscores, must not start with a numeric character, and must not "
"start with double underscores."
]
if visitor.invalid_type_names:
sorted_invalid_type_names = sorted(visitor.invalid_type_names.items())
error_message_components.append(
f"The following is a list of tuples that describes what needs to be fixed for type "
f"renamings. Each tuple is of the form (original_name, invalid_new_name) where "
f"original_name is the name in the original schema and invalid_new_name is what "
f"original_name would be renamed to: {sorted_invalid_type_names}"
)
if visitor.invalid_field_names:
sorted_invalid_field_names = [
(type_name, sorted(field_renamings.items()))
for type_name, field_renamings in sorted(visitor.invalid_field_names.items())
]
error_message_components.append(
f"The following is a list of tuples that describes what needs to be fixed for "
f"field renamings. Each tuple is of the form (type_name, field_renamings) "
f"where type_name is the name of the type in the original schema and "
f"field_renamings is a list of tuples mapping the original field name to the "
f"invalid GraphQL name it would be renamed to: {sorted_invalid_field_names}"
)
raise InvalidNameError("\n".join([i for i in error_message_components if i is not None]))
if (
visitor.type_name_conflicts
or visitor.type_renamed_to_builtin_scalar_conflicts
or visitor.field_name_conflicts
):
raise SchemaRenameNameConflictError(
visitor.type_name_conflicts,
visitor.type_renamed_to_builtin_scalar_conflicts,
visitor.field_name_conflicts,
)
if visitor.types_involving_interfaces_with_field_renamings:
raise NotImplementedError(
f"Field renaming for interfaces or types implementing interfaces is not supported, but "
f"they exist for the following types and should be removed: "
f"{visitor.types_involving_interfaces_with_field_renamings}"
)
if visitor.illegal_builtin_scalar_renamings:
raise NotImplementedError(
f"Type_renamings contained renamings for the following built-in scalar types: "
f"{visitor.illegal_builtin_scalar_renamings}. To fix this, remove them from "
f"type_renamings."
)
for type_name in visitor.suppressed_type_names:
if type_name not in type_renamings:
raise AssertionError(
f"suppressed_type_names should be a subset of the set of keys in "
f"type_renamings, but found {type_name} in suppressed_type_names that is not a "
f"key in type_renamings. This is a bug."
)
renamed_types = {
visitor.reverse_name_map[type_name]
for type_name in visitor.reverse_name_map
if type_name != visitor.reverse_name_map[type_name]
}
no_op_type_renames = set(type_renamings) - renamed_types - set(visitor.suppressed_type_names)
# nonexistent_types_with_field_renamings is the set of all object type names that aren't in the
# original schema but appeared in field_renamings anyways.
nonexistent_types_with_field_renamings = (
set(field_renamings) - visitor.types_with_field_renamings_processed
)
if (
no_op_type_renames
or visitor.no_op_field_renamings
or nonexistent_types_with_field_renamings
):
raise NoOpRenamingError(
no_op_type_renames,
visitor.no_op_field_renamings,
nonexistent_types_with_field_renamings,
)
reverse_name_map_changed_names_only = {
renamed_name: original_name
for renamed_name, original_name in six.iteritems(visitor.reverse_name_map)
if renamed_name != original_name
}
reverse_field_name_map_changed_names_only = {}
for type_name, reverse_field_name_mapping in visitor.reverse_field_name_map.items():
current_type_reverse_field_name_map_changed_names_only = {
renamed_field_name: original_field_name
for renamed_field_name, original_field_name in reverse_field_name_mapping.items()
if renamed_field_name != original_field_name
}
if current_type_reverse_field_name_map_changed_names_only:
reverse_field_name_map_changed_names_only[
type_name
] = current_type_reverse_field_name_map_changed_names_only
return (
renamed_schema_ast,
reverse_name_map_changed_names_only,
reverse_field_name_map_changed_names_only,
)
def _rename_and_suppress_query_type_fields(
schema_ast: DocumentNode, type_renamings: Mapping[str, Optional[str]], query_type: str
) -> DocumentNode:
"""Rename or suppress all fields of the query type.
The input schema AST will not be modified.
Args:
schema_ast: schema that we're returning a modified version of
type_renamings: maps original type name to renamed name or None (for type suppression). A
type named "Foo" will be unchanged iff type_renamings does not map "Foo" to
anything, i.e. "Foo" not in type_renamings
query_type: name of the query type, e.g. 'RootSchemaQuery'
Returns:
modified version of the input schema AST
Raises:
- SchemaTransformError if type_renamings suppressed every type
"""
visitor = RenameQueryTypeFieldsVisitor(type_renamings, query_type)
renamed_schema_ast = visit(schema_ast, visitor)
return renamed_schema_ast
class RenameSchemaTypesVisitor(Visitor):
"""Traverse a Document AST, editing the names of nodes."""
noop_types = frozenset(
{
"ArgumentNode",
"BooleanValueNode",
"DirectiveNode",
"DirectiveDefinitionNode",
"DocumentNode",
"EnumValueNode",
"EnumValueDefinitionNode",
"FieldNode",
"FieldDefinitionNode",
"FloatValueNode",
"FragmentDefinitionNode",
"FragmentSpreadNode",
"InlineFragmentNode",
"InputObjectTypeDefinitionNode",
"InputValueDefinitionNode",
"IntValueNode",
"ListTypeNode",
"ListValueNode",
"NameNode",
"NonNullTypeNode",
"ObjectFieldNode",
"ObjectValueNode",
"OperationDefinitionNode",
"OperationTypeDefinitionNode",
"SchemaDefinitionNode",
"SelectionSetNode",
"StringValueNode",
"VariableNode",
"VariableDefinitionNode",
"SchemaExtensionNode",
"InterfaceTypeExtensionNode",
"UnionTypeExtensionNode",
"EnumTypeExtensionNode",
"ObjectTypeExtensionNode",
"InputObjectTypeExtensionNode",
"ScalarTypeExtensionNode",
}
)
# rename_types must be a set of strings corresponding to the names of the classes in
# RenameTypes. The duplication exists because introspection for Unions via typing.get_args()
# doesn't exist until Python 3.8. In Python 3.8, this would be a valid way to define
# rename_types:
# rename_types = frozenset(cls.__name__ for cls in get_args(RenameTypes)) # type: ignore
# Note: even with Python 3.8, the mypy version at the time of writing (version 0.770) doesn't
# allow for introspection for Unions. mypy's maintainers recently merged a PR
# (https://github.com/python/mypy/pull/8779) that permits this line of code, but did so after
# the mypy 0.770 release. If we do end up removing the duplication at a later point but not
# update the mypy version, we'd need to ignore it (as shown in the in-line comment).
rename_types = frozenset(
{
"EnumTypeDefinitionNode",
"InterfaceTypeDefinitionNode",
"NamedTypeNode",
"ObjectTypeDefinitionNode",
"ScalarTypeDefinitionNode",
"UnionTypeDefinitionNode",
}
)
# Collects naming conflict errors involving types that are not built-in scalar types. If
# type_renamings would result in multiple types being named "Foo", type_name_conflicts will map
# "Foo" to a set containing the name of each such type
type_name_conflicts: Dict[str, Set[str]]
# Collects naming conflict errors involving built-in scalar types. If
# type_renamings["Foo"] == "String", type_renamed_to_builtin_scalar_conflicts will map
# "Foo" to "String"
type_renamed_to_builtin_scalar_conflicts: Dict[str, str]
# Collects naming errors that arise from attempting to rename a builtin scalar. If
# type_renamings["String"] == "Foo" schema, illegal_builtin_scalar_renamings will contain
# "String"
illegal_builtin_scalar_renamings: Set[str]
# reverse_name_map maps renamed type name to original type name, containing all non-suppressed
# types, including those that were unchanged. Must contain unchanged names to prevent type
# renaming conflicts and raise SchemaRenameNameConflictError when they arise
reverse_name_map: Dict[str, str]
# Collects invalid type names in type_renamings. If type_renamings["Foo"] is a string that is
# not a valid, non-reserved GraphQL type name (valid, non-reserved names consist only of
# alphanumeric characters and underscores, do not start with a number, and do not start with two
# underscores), invalid_type_names will map "Foo" to the invalid type name.
invalid_type_names: Dict[str, str]
# Collects the type names for types that get suppressed. If type_renamings["Foo"] == None,
# suppressed_type_names will contain "Foo".
suppressed_type_names: Set[str]
# reverse_field_name_map maps type name to a dict, which in turn maps the name of a field in the
# renamed schema to the name of the field in the original schema, if the field has different
# names in the original schema and the new schema. If field_renamings would rename a field named
# "foo" (in a type named "Baz") to "bar", then reverse_field_name_map["Baz"] will map "bar" to
# "foo".
reverse_field_name_map: Dict[str, Dict[str, str]]
# Collects no-op renamings for fields, mapping the type name that contains the field to the set
# of field names for which field_renamings contained no-op renamings. For a type named "Bar", if
# field_renaming
# - renames a field named "foo" to "foo", or
# - attempts to rename a field named "foo" when such a field does not exist
# no_op_field_renamings will map "Bar" to a set containing "foo".
no_op_field_renamings: Dict[str, Set[str]]
# Collects type names for each object type that has field renamings that have been applied.
# After every renaming is done, this is used to ensure that field_renamings contains no unused
# field renamings for a particular type.
types_with_field_renamings_processed: Set[str]
# Collects invalid field names in field_renamings. If field_renamings would rename a field named
# "foo" (in a type named "Bar") to a string that is not a valid, non-reserved GraphQL type name
# (valid, non-reserved names consist only of alphanumeric characters and underscores, do not
# start with a number, and do not start with two underscores), invalid_field_names will map
# "Bar" to a dict that maps "foo" to the invalid field name.
invalid_field_names: Dict[str, Dict[str, str]]
# Collects naming conflict errors involving fields. If field_renamings would rename multiple
# fields (in a type named "Bar" in the original schema) to "foo", field_name_conflicts will map
# "Bar" to a dict that maps "foo" to a set containing the names of the fields in the original
# schema that would be renamed to "foo".
field_name_conflicts: Dict[str, Dict[str, Set[str]]]
# Collects names of types who have entries in field_renamings if the type is an interface
# or if the type is an object type implementing an interface because field renamings involving
# interfaces haven't been implemented yet. If field renamings has field renamings for such a
# type named "Foo", types_involving_interfaces_with_field_renamings will contain "Foo".
types_involving_interfaces_with_field_renamings: Set[str]
# Collects cascading suppression errors involving types. If every field in a type gets
# suppressed but the type itself is not explicitly supppressed, object_types_to_suppress will
# contain that type's name.
object_types_to_suppress: Set[str]
# Collects cascading suppression errors involving unions. If every type in a union gets
# suppressed but the union itself is not explicitly supppressed, union_types_to_suppress will
# contain that union's name.
union_types_to_suppress: Set[str]
# Collects cascading suppression errors involving fields. If a field's type (named "V") gets
# suppressed but the field (named "F") or the type containing that field (named "T") is not
# explicitly supppressed, fields_to_suppress will map "T" to a dict which maps "F" to "V".
fields_to_suppress: Dict[str, Dict[str, str]]
def __init__(
self,
type_renamings: Mapping[str, Optional[str]],
field_renamings: Mapping[str, Mapping[str, Set[str]]],
query_type: str,
) -> None:
"""Create a visitor for renaming types in a schema AST.
Args:
type_renamings: maps original type name to renamed name or None (for type suppression).
A type named "Foo" will be unchanged iff type_renamings does not map
"Foo" to anything, i.e. "Foo" not in type_renamings
field_renamings: maps type names to the field renamings for that type. The renamings map
field names belonging to the type to a set of field names for the
renamed schema
query_type: name of the query type (e.g. RootSchemaQuery), which will not be renamed
"""
self.type_renamings = type_renamings
self.reverse_name_map = {}
self.type_name_conflicts = {}
self.type_renamed_to_builtin_scalar_conflicts = {}
self.illegal_builtin_scalar_renamings = {
scalar_name
for scalar_name in builtin_scalar_type_names
if scalar_name in type_renamings
}
self.invalid_type_names = {}
self.query_type = query_type
self.suppressed_type_names = set()
self.field_renamings = field_renamings
self.reverse_field_name_map = {}
self.no_op_field_renamings = {}
self.types_with_field_renamings_processed = set()
self.invalid_field_names = {}
self.field_name_conflicts = {}
self.types_involving_interfaces_with_field_renamings = set()
self.object_types_to_suppress = set()
self.union_types_to_suppress = set()
self.fields_to_suppress = {}
def _rename_or_suppress_or_ignore_name_and_add_to_record(
self, node: RenameTypesT
) -> Union[RenameTypesT, VisitorAction]:
"""Change node based on renamings and update reverse mapping data structures.
Don't rename if the type is the query type or a builtin type.
The input node will not be modified. reverse_name_map may be modified.
Args:
node: object representing an AST component, containing a .name attribute
corresponding to an AST node of type NameNode.
Returns:
Node object, REMOVE, or IDLE. The GraphQL library defines special return values REMOVE
and IDLE to delete or do nothing with the node a visitor is currently at, respectively.
If the current node is to be renamed, this function returns a Node object identical to
the input node except with a new name. If it is to be suppressed, this function returns
REMOVE. If neither of these are the case, this function returns IDLE.
"""
type_name = node.name.value
if type_name == self.query_type or type_name in builtin_scalar_type_names:
return IDLE
desired_type_name = self.type_renamings.get(type_name, type_name) # Default use original
if desired_type_name is None:
# Suppress the type
self.suppressed_type_names.add(type_name)
return REMOVE
if not is_valid_nonreserved_name(desired_type_name):
self.invalid_type_names[type_name] = desired_type_name
# Renaming conflict arises when two types with different names in the original schema have
# the same name in the new schema.
if self.reverse_name_map.get(desired_type_name, type_name) != type_name:
conflictingly_renamed_type_name = self.reverse_name_map[desired_type_name]
# Collect all types in the original schema that would be named desired_type_name in the
# new schema
if desired_type_name not in self.type_name_conflicts:
self.type_name_conflicts[desired_type_name] = {conflictingly_renamed_type_name}
self.type_name_conflicts[desired_type_name].add(type_name)
if desired_type_name in builtin_scalar_type_names:
self.type_renamed_to_builtin_scalar_conflicts[type_name] = desired_type_name
# Any potential type suppressions will have taken place by this point, so this current node
# will appear in the renamed schema, so it's safe to apply field renamings to this type.
fields_renamed_node = node # If no field renaming happens, fields_renamed_node will just be
# the current node, unchanged.
if isinstance(fields_renamed_node, ObjectTypeDefinitionNode):
# mypy is unable to detect that fields_renamed_node is an ObjectTypeDefinitionNode if
# the code enters this block, so disabling it for this line.
# https://github.com/python/mypy/issues/2885#issuecomment-287928126
fields_renamed_node = self._rename_fields(fields_renamed_node) # type: ignore
elif (
isinstance(fields_renamed_node, InterfaceTypeDefinitionNode)
and fields_renamed_node.name.value in self.field_renamings
):
self.types_involving_interfaces_with_field_renamings.add(fields_renamed_node.name.value)
self.reverse_name_map[desired_type_name] = type_name
if desired_type_name == type_name:
return fields_renamed_node
else: # Make copy of node with the changed name, return the copy
node_with_new_name = get_copy_of_node_with_new_name(
fields_renamed_node, desired_type_name
)
return node_with_new_name
def _rename_fields(self, node: ObjectTypeDefinitionNode) -> ObjectTypeDefinitionNode:
"""Rename node's fields if applicable and return new node with updated fields.
This method only gets called for type nodes that are not to be suppressed, since if the type
is to be suppressed then there's no need to check its fields for CascadingSuppressionError.
If a field F is of type T where node is not of type T, and T is suppressed, then F must also
be suppressed. If this is not the case, it will lead to a CascadingSuppressionError and this
method will collect the information necessary to raise a CascadingSuppressionError in the
visitor object.
Args:
node: type node with fields to be renamed
Returns:
node with fields renamed if there are no cascading suppression issues.
"""
# Field renaming takes place in three steps:
# Step 1: detect fields that depend on types that were suppressed.
# For each field foo of type Bar: if Bar is going to be suppressed but foo isn't going to
# be suppressed, the renamings are invalid. In that case, we should do the bare minimum
# necessary to continue the schema renaming and collect any other errors that occur
# independently while recording the problem. Once done, we return from the current type
# because there is no more to be done for this particular type's fields.
# Step 2: return immediately if we're not going to be renaming any fields.
# If the current type has no field renamings at all, we return immediately after doing
# nothing.
# Step 3: apply the field renamings to each field.
# If the current type does have field renamings, apply those field renamings.
type_name = node.name.value
current_type_fields_to_suppress = {}
# Step 1: detect fields that depend on types that were suppressed.
for field_node in node.fields:
field_name = field_node.name.value
field_type_name = get_ast_with_non_null_and_list_stripped(field_node.type).name.value
field_type_suppressed = (
self.type_renamings.get(field_type_name, field_type_name) is None
)
field_node_suppressed = (
type_name in self.field_renamings
and self.field_renamings[type_name].get(field_name, {field_name}) == set()
)
if field_type_suppressed and not field_node_suppressed:
# If the type of the field is suppressed but the field itself is not, it's invalid.
current_type_fields_to_suppress[field_name] = field_type_name
if current_type_fields_to_suppress != {}:
self.fields_to_suppress[type_name] = current_type_fields_to_suppress
new_type_node = copy(node)
new_type_node.fields = FrozenList(
[
field_node
for field_node in new_type_node.fields
if field_node.name.value not in current_type_fields_to_suppress
]
)
return new_type_node
# Step 2: return immediately if we're not going to be renaming any fields.
if type_name not in self.field_renamings:
return node
if node.interfaces:
self.types_involving_interfaces_with_field_renamings.add(type_name)
# Step 3: apply the field renamings to each field.
current_type_field_renamings = self.field_renamings[type_name]
self.types_with_field_renamings_processed.add(type_name)
# Need to create a set of field nodes that the type will have after the field renamings,
# instead of just modifying them in place. This is to support 1-many renaming.
new_field_nodes: Set[FieldDefinitionNode] = set()
for field_node in node.fields:
original_field_name = field_node.name.value
if original_field_name in current_type_field_renamings and current_type_field_renamings[
original_field_name
] == {original_field_name}:
# Check for no-op 1-1 renamings when the renamings would rename a field to itself.
self.no_op_field_renamings.setdefault(type_name, set()).add(original_field_name)
new_field_names = current_type_field_renamings.get(
original_field_name, {original_field_name}
)
for new_field_name in new_field_names:
# Validate the new field name before adding it to the schema.
if new_field_name in self.reverse_field_name_map.setdefault(type_name, {}):
if new_field_name not in self.field_name_conflicts.setdefault(type_name, {}):
conflictingly_renamed_field_name = self.reverse_field_name_map[type_name][
new_field_name
]
self.field_name_conflicts[type_name][new_field_name] = {
conflictingly_renamed_field_name
}
self.field_name_conflicts[type_name][new_field_name].add(original_field_name)
if not is_valid_nonreserved_name(new_field_name):
self.invalid_field_names.setdefault(type_name, {})[
original_field_name
] = new_field_name
self.reverse_field_name_map[type_name][new_field_name] = original_field_name
new_field_nodes.update(
get_copy_of_node_with_new_name(field_node, new_field_name)
for new_field_name in new_field_names
)
# Check that, having applied field renamings for every single field in node.fields, that
# there are no leftover field_renamings that would be illegal noops.
unused_field_renamings = set(current_type_field_renamings) - {
field.name.value for field in node.fields
}
if unused_field_renamings:
# Need this condition because if all the renamings are used, calling update() will
# materialize an empty set, making it seem like there are no-op field renamings even
# when there aren't.
self.no_op_field_renamings.setdefault(type_name, set()).update(unused_field_renamings)
if len(new_field_nodes) == 0:
self.object_types_to_suppress.add(type_name)
new_type_node = copy(node)
new_type_node.fields = FrozenList(new_field_nodes)
return new_type_node
def leave_union_type_definition(
self,
node: UnionTypeDefinitionNode,
key: Any,
parent: Any,
path: List[Any],
ancestors: List[Any],
) -> None:
"""Check that each union still has at least one non-suppressed member."""
if len(node.types) == 0:
self.union_types_to_suppress.add(node.name.value)
def enter(
self,
node: Node,
key: Any,
parent: Any,
path: List[Any],
ancestors: List[Any],
) -> VisitorReturnType:
"""Upon entering a node, operate depending on node type."""
node_type = type(node).__name__
if node_type in self.noop_types:
# Do nothing, continue traversal
return IDLE
elif node_type in self.rename_types:
# Process the node by either renaming, suppressing, or not doing anything with it
# (depending on what type_renamings specifies)
return self._rename_or_suppress_or_ignore_name_and_add_to_record(
cast(RenameTypes, node)
)
else:
# All Node types should've been taken care of, this line should never be reached
raise AssertionError('Unreachable code reached. Missed type: "{}"'.format(node_type))
class RenameQueryTypeFieldsVisitor(Visitor):
def __init__(self, type_renamings: Mapping[str, Optional[str]], query_type: str) -> None:
"""Create a visitor for renaming or suppressing fields of the query type in a schema AST.
Args:
type_renamings: maps original type name to renamed name or None (for type suppression).
A type named "Foo" will be unchanged iff type_renamings does not map
"Foo" to anything, i.e. "Foo" not in type_renamings
query_type: name of the query type (e.g. RootSchemaQuery)
Raises:
- SchemaTransformError if every field in the query type was suppressed
"""
# Note that as field names and type names have been confirmed to match up, any renamed
# query type field already has a corresponding renamed type.
self.in_query_type = False
self.type_renamings = type_renamings
self.query_type = query_type
def enter_object_type_definition(
self,
node: ObjectTypeDefinitionNode,
key: Any,
parent: Any,
path: List[Any],
ancestors: List[Any],
) -> None:
"""If the node's name matches the query type, record that we entered the query type."""
if node.name.value == self.query_type:
self.in_query_type = True
def leave_object_type_definition(
self,
node: ObjectTypeDefinitionNode,
key: Any,
parent: Any,
path: List[Any],
ancestors: List[Any],
) -> None:
"""If the node's name matches the query type, record that we left the query type."""
if not node.fields:
raise SchemaTransformError(
f"Type renamings {self.type_renamings} suppressed every type in the schema so it "
f"will be impossible to query for anything. To fix this, check why the "
f"type_renamings argument of rename_schema mapped every type to None."
)
if node.name.value == self.query_type:
self.in_query_type = False
def enter_field_definition(
self,
node: FieldDefinitionNode,
key: Any,
parent: Any,
path: List[Any],
ancestors: List[Any],
) -> VisitorReturnType:
"""If inside query type, rename or remove field as specified by type_renamings."""
if self.in_query_type:
field_name = node.name.value
new_field_name = self.type_renamings.get(field_name, field_name) # Default use original
if new_field_name == field_name:
return IDLE
if new_field_name is None:
# Suppress the type
return REMOVE
else: # Make copy of node with the changed name, return the copy
field_node_with_new_name = get_copy_of_node_with_new_name(node, new_field_name)
return field_node_with_new_name
return IDLE
class SuppressionNotImplementedVisitor(Visitor):
"""Traverse the schema to check for suppressions that are not yet implemented.
Each attribute that mentions an unsupported suppression records the types that type_renamings
attempts to suppress.
After calling visit() on the schema using this visitor, if any of these attributes are non-empty
then some suppressions specified by type_renamings are unsupported, so the code should then
raise a NotImplementedError.
"""
unsupported_enum_suppressions: Set[str]
unsupported_interface_suppressions: Set[str]
unsupported_interface_implementation_suppressions: Set[str]
def __init__(self, type_renamings: Mapping[str, Optional[str]]) -> None:
"""Confirm type_renamings doesn't try to suppress enum/interface/interface implementation.
Args:
type_renamings: maps original type name to renamed name or None (for type suppression).