Skip to content

Commit 712ef84

Browse files
committed
Support using lazy descriptions for GraphQL types (#58)
You can now register additional classes that will be accepted as descriptions.
1 parent f23bc2c commit 712ef84

File tree

7 files changed

+317
-11
lines changed

7 files changed

+317
-11
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The current version 3.0.0a2 of GraphQL-core is up-to-date
1616
with GraphQL.js version 14.4.2.
1717

1818
All parts of the API are covered by an extensive test suite
19-
of currently 1966 unit tests.
19+
of currently 1979 unit tests.
2020

2121

2222
## Documentation

docs/modules/pyutils.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ PyUtils
1010
.. autofunction:: cached_property
1111
.. autofunction:: dedent
1212
.. autofunction:: did_you_mean
13+
.. autofunction:: register_description
14+
.. autofunction:: unregister_description
1315
.. autoclass:: EventEmitter
1416
:members:
1517
.. autoclass:: EventEmitterAsyncIterator

src/graphql/pyutils/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
from .convert_case import camel_to_snake, snake_to_camel
1212
from .cached_property import cached_property
1313
from .dedent import dedent
14+
from .description import (
15+
Description,
16+
is_description,
17+
register_description,
18+
unregister_description,
19+
)
1420
from .did_you_mean import did_you_mean
1521
from .event_emitter import EventEmitter, EventEmitterAsyncIterator
1622
from .identity_func import identity_func
@@ -33,6 +39,10 @@
3339
"cached_property",
3440
"dedent",
3541
"did_you_mean",
42+
"Description",
43+
"is_description",
44+
"register_description",
45+
"unregister_description",
3646
"EventEmitter",
3747
"EventEmitterAsyncIterator",
3848
"identity_func",

src/graphql/pyutils/description.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import Any, Tuple, Union
2+
3+
__all__ = [
4+
"Description",
5+
"is_description",
6+
"register_description",
7+
"unregister_description",
8+
]
9+
10+
11+
class Description:
12+
"""Type checker for human readable descriptions.
13+
14+
By default, only ordinary strings are accepted as descriptions,
15+
but you can register() other classes that will also be allowed,
16+
e.g. to support lazy string objects that are evaluated only at runtime.
17+
If you register(object), any object will be allowed as description.
18+
"""
19+
20+
bases: Union[type, Tuple[type, ...]] = str
21+
22+
@classmethod
23+
def isinstance(cls, obj: Any) -> bool:
24+
return isinstance(obj, cls.bases)
25+
26+
@classmethod
27+
def register(cls, base: type) -> None:
28+
"""Register a class that shall be accepted as a description."""
29+
if not isinstance(base, type):
30+
raise TypeError("Only types can be registered.")
31+
if base is object:
32+
cls.bases = object
33+
elif cls.bases is object:
34+
if base is not object:
35+
cls.bases = base
36+
elif not isinstance(cls.bases, tuple):
37+
if base is not cls.bases:
38+
cls.bases = (cls.bases, base)
39+
elif base not in cls.bases:
40+
cls.bases += (base,)
41+
42+
@classmethod
43+
def unregister(cls, base: type) -> None:
44+
"""Unregister a class that shall no more be accepted as a description."""
45+
if not isinstance(base, type):
46+
raise TypeError("Only types can be unregistered.")
47+
if isinstance(cls.bases, tuple):
48+
if base in cls.bases:
49+
cls.bases = tuple(b for b in cls.bases if b is not base)
50+
if not cls.bases:
51+
cls.bases = object
52+
elif len(cls.bases) == 1:
53+
cls.bases = cls.bases[0]
54+
elif cls.bases is base:
55+
cls.bases = object
56+
57+
58+
is_description = Description.isinstance
59+
register_description = Description.register
60+
unregister_description = Description.unregister

src/graphql/type/definition.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,14 @@
4242
UnionTypeExtensionNode,
4343
ValueNode,
4444
)
45-
from ..pyutils import AwaitableOrValue, FrozenList, Path, cached_property, inspect
45+
from ..pyutils import (
46+
AwaitableOrValue,
47+
FrozenList,
48+
Path,
49+
cached_property,
50+
inspect,
51+
is_description,
52+
)
4653
from ..utilities.value_from_ast_untyped import value_from_ast_untyped
4754

4855
if TYPE_CHECKING: # pragma: no cover
@@ -199,7 +206,7 @@ def __init__(
199206
raise TypeError("Must provide name.")
200207
if not isinstance(name, str):
201208
raise TypeError("The name must be a string.")
202-
if description is not None and not isinstance(description, str):
209+
if description is not None and not is_description(description):
203210
raise TypeError("The description must be a string.")
204211
if extensions is not None and (
205212
not isinstance(extensions, dict)
@@ -471,9 +478,9 @@ def __init__(
471478
"Field resolver must be a function if provided, "
472479
f" but got: {inspect(resolve)}."
473480
)
474-
if description is not None and not isinstance(description, str):
481+
if description is not None and not is_description(description):
475482
raise TypeError("The description must be a string.")
476-
if deprecation_reason is not None and not isinstance(deprecation_reason, str):
483+
if deprecation_reason is not None and not is_description(deprecation_reason):
477484
raise TypeError("The deprecation reason must be a string.")
478485
if extensions is not None and (
479486
not isinstance(extensions, dict)
@@ -589,7 +596,7 @@ def __init__(
589596
) -> None:
590597
if not is_input_type(type_):
591598
raise TypeError(f"Argument type must be a GraphQL input type.")
592-
if description is not None and not isinstance(description, str):
599+
if description is not None and not is_description(description):
593600
raise TypeError("Argument description must be a string.")
594601
if out_name is not None and not isinstance(out_name, str):
595602
raise TypeError("Argument out name must be a string.")
@@ -1131,9 +1138,9 @@ def __init__(
11311138
extensions: Dict[str, Any] = None,
11321139
ast_node: EnumValueDefinitionNode = None,
11331140
) -> None:
1134-
if description is not None and not isinstance(description, str):
1141+
if description is not None and not is_description(description):
11351142
raise TypeError("The description of the enum value must be a string.")
1136-
if deprecation_reason is not None and not isinstance(deprecation_reason, str):
1143+
if deprecation_reason is not None and not is_description(deprecation_reason):
11371144
raise TypeError(
11381145
"The deprecation reason for the enum value must be a string."
11391146
)
@@ -1320,7 +1327,7 @@ def __init__(
13201327
) -> None:
13211328
if not is_input_type(type_):
13221329
raise TypeError(f"Input field type must be a GraphQL input type.")
1323-
if description is not None and not isinstance(description, str):
1330+
if description is not None and not is_description(description):
13241331
raise TypeError("Input field description must be a string.")
13251332
if out_name is not None and not isinstance(out_name, str):
13261333
raise TypeError("Input field out name must be a string.")

src/graphql/type/directives.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, Dict, List, Optional, Sequence, cast
22

33
from ..language import ast, DirectiveLocation
4-
from ..pyutils import inspect, FrozenList
4+
from ..pyutils import inspect, is_description, FrozenList
55
from .definition import GraphQLArgument, GraphQLInputType, GraphQLNonNull, is_input_type
66
from .scalars import GraphQLBoolean, GraphQLString
77

@@ -84,7 +84,7 @@ def __init__(
8484
raise TypeError(f"{name} is_repeatable flag must be True or False.")
8585
if ast_node and not isinstance(ast_node, ast.DirectiveDefinitionNode):
8686
raise TypeError(f"{name} AST node must be a DirectiveDefinitionNode.")
87-
if description is not None and not isinstance(description, str):
87+
if description is not None and not is_description(description):
8888
raise TypeError(f"{name} description must be a string.")
8989
if extensions is not None and (
9090
not isinstance(extensions, dict)

0 commit comments

Comments
 (0)