Skip to content

Commit 8c053a3

Browse files
committed
Support deep copy of GraphQL schema (#100)
1 parent 826b7a1 commit 8c053a3

File tree

3 files changed

+141
-6
lines changed

3 files changed

+141
-6
lines changed

docs/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@
9696
autosummary_generate = True
9797

9898
autodoc_type_aliases = {
99-
'AwaitableOrValue': 'graphql.pyutils.AwaitableOrValue'
99+
'AwaitableOrValue': 'graphql.pyutils.AwaitableOrValue',
100+
'TypeMap': 'graphql.schema.TypeMap'
100101
}
101102

102103
# GraphQL-core top level modules with submodules that can be omitted.

src/graphql/type/schema.py

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from copy import copy, deepcopy
12
from typing import (
23
Any,
34
Collection,
@@ -9,7 +10,6 @@
910
Union,
1011
cast,
1112
)
12-
from warnings import warn
1313

1414
from ..error import GraphQLError
1515
from ..language import ast
@@ -22,11 +22,13 @@
2222
GraphQLObjectType,
2323
GraphQLUnionType,
2424
GraphQLType,
25+
GraphQLWrappingType,
2526
get_named_type,
2627
is_input_object_type,
2728
is_interface_type,
2829
is_object_type,
2930
is_union_type,
31+
is_wrapping_type,
3032
)
3133
from .directives import GraphQLDirective, specified_directives, is_directive
3234
from .introspection import introspection_types
@@ -289,9 +291,41 @@ def to_kwargs(self) -> Dict[str, Any]:
289291
def __copy__(self) -> "GraphQLSchema": # pragma: no cover
290292
return self.__class__(**self.to_kwargs())
291293

292-
def __deepcopy__(self, memo_: Dict) -> "GraphQLSchema": # pragma: no cover
293-
warn("Cannot deep copy a schema. Creating a flat copy instead.")
294-
return self.__copy__()
294+
def __deepcopy__(self, memo: Dict) -> "GraphQLSchema":
295+
from ..type import (
296+
is_introspection_type,
297+
is_specified_scalar_type,
298+
is_specified_directive,
299+
)
300+
301+
type_map = {
302+
name: copy(type_)
303+
for name, type_ in self.type_map.items()
304+
if not is_introspection_type(type_) and not is_specified_scalar_type(type_)
305+
}
306+
remapped: Set[str] = set()
307+
types = [
308+
cast(GraphQLNamedType, remap_type(type_, type_map, remapped))
309+
for type_ in type_map.values()
310+
]
311+
directives = [
312+
directive if is_specified_directive(directive) else copy(directive)
313+
for directive in self.directives
314+
]
315+
return self.__class__(
316+
self.query_type and cast(GraphQLObjectType, type_map[self.query_type.name]),
317+
self.mutation_type
318+
and cast(GraphQLObjectType, type_map[self.mutation_type.name]),
319+
self.subscription_type
320+
and cast(GraphQLObjectType, type_map[self.subscription_type.name]),
321+
types,
322+
directives,
323+
self.description,
324+
extensions=deepcopy(self.extensions, memo),
325+
ast_node=deepcopy(self.ast_node, memo),
326+
extension_ast_nodes=deepcopy(self.extension_ast_nodes, memo),
327+
assume_valid=True,
328+
)
295329

296330
def get_type(self, name: str) -> Optional[GraphQLNamedType]:
297331
return self.type_map.get(name)
@@ -406,3 +440,52 @@ def assert_schema(schema: Any) -> GraphQLSchema:
406440
if not is_schema(schema):
407441
raise TypeError(f"Expected {inspect(schema)} to be a GraphQL schema.")
408442
return cast(GraphQLSchema, schema)
443+
444+
445+
def remap_type(
446+
type_: GraphQLType, type_map: TypeMap, remapped: Set[str]
447+
) -> GraphQLType:
448+
"""Change all references in the given type for a new type map."""
449+
if is_wrapping_type(type_):
450+
wrapping_type = cast(GraphQLWrappingType, type_)
451+
return wrapping_type.__class__(
452+
remap_type(wrapping_type.of_type, type_map, remapped)
453+
)
454+
named_type = cast(GraphQLNamedType, type_)
455+
name = named_type.name
456+
remapped_type = type_map.get(name)
457+
if not remapped_type:
458+
return named_type
459+
if name in remapped:
460+
return remapped_type
461+
remapped.add(name)
462+
if is_union_type(remapped_type):
463+
named_type = cast(GraphQLUnionType, named_type)
464+
named_type.types = [
465+
remap_type(member_type, type_map, remapped)
466+
for member_type in named_type.types
467+
]
468+
elif is_object_type(remapped_type) or is_interface_type(remapped_type):
469+
named_type = cast(Union[GraphQLObjectType, GraphQLInterfaceType], named_type)
470+
named_type.interfaces = [
471+
remap_type(interface_type, type_map, remapped)
472+
for interface_type in named_type.interfaces
473+
]
474+
fields = named_type.fields
475+
for field_name, field in fields.items():
476+
field = copy(field)
477+
field.type = remap_type(field.type, type_map, remapped)
478+
args = field.args
479+
for arg_name, arg in args.items():
480+
arg = copy(arg)
481+
arg.type = remap_type(arg.type, type_map, remapped)
482+
args[arg_name] = arg
483+
fields[field_name] = field
484+
elif is_input_object_type(remapped_type):
485+
named_type = cast(GraphQLInputObjectType, named_type)
486+
fields = named_type.fields
487+
for field_name, field in fields.items():
488+
field = copy(field)
489+
field.type = remap_type(field.type, type_map, remapped)
490+
fields[field_name] = field
491+
return named_type

tests/type/test_schema.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from copy import deepcopy
2+
13
from pytest import raises # type: ignore
24

35
from graphql.language import (
@@ -26,7 +28,7 @@
2628
GraphQLType,
2729
specified_directives,
2830
)
29-
from graphql.utilities import print_schema
31+
from graphql.utilities import build_schema, lexicographic_sort_schema, print_schema
3032

3133
from ..utils import dedent
3234

@@ -439,3 +441,52 @@ def rejects_a_scalar_type_with_incorrect_extension_ast_nodes():
439441
"Schema extension AST nodes must be specified"
440442
" as a collection of SchemaExtensionNode instances."
441443
)
444+
445+
def can_deep_copy_a_schema():
446+
source = """
447+
schema {
448+
query: Farm
449+
mutation: Work
450+
}
451+
452+
type Cow {
453+
id: ID
454+
name: String
455+
moos: Boolean
456+
}
457+
458+
type Pig {
459+
id: ID
460+
name: String
461+
oink: Boolean
462+
}
463+
464+
union Animal = Cow | Pig
465+
466+
enum Food {
467+
CORN
468+
FRUIT
469+
}
470+
471+
input Feed {
472+
amount: Float
473+
type: Food
474+
}
475+
476+
type Farm {
477+
animals: [Animal]
478+
}
479+
480+
type Work {
481+
feed(feed: Feed): Boolean
482+
}
483+
"""
484+
schema = build_schema(source)
485+
schema_copy = deepcopy(schema)
486+
487+
for name in ("Cow", "Pig", "Animal", "Food", "Feed", "Farm", "Work"):
488+
assert schema.get_type(name) is not schema_copy.get_type(name)
489+
490+
assert print_schema(lexicographic_sort_schema(schema)) == print_schema(
491+
lexicographic_sort_schema(schema_copy)
492+
)

0 commit comments

Comments
 (0)