graphql-api 1.7.0__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. {graphql_api-1.7.0 → graphql_api-2.0.0}/.github/workflows/python-package.yml +2 -0
  2. {graphql_api-1.7.0 → graphql_api-2.0.0}/PKG-INFO +2 -2
  3. graphql_api-2.0.0/VERSION +1 -0
  4. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/fundamentals/field-types.md +13 -10
  5. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/executor.py +29 -28
  6. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/mapper.py +2 -25
  7. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/remote.py +9 -3
  8. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/types.py +49 -39
  9. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api.egg-info/PKG-INFO +2 -2
  10. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api.egg-info/requires.txt +1 -1
  11. {graphql_api-1.7.0 → graphql_api-2.0.0}/pyproject.toml +1 -1
  12. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_custom_types.py +7 -6
  13. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_docs_field_types.py +8 -7
  14. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_executor_signature.py +36 -17
  15. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_remote.py +6 -2
  16. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_subscriptions.py +9 -4
  17. {graphql_api-1.7.0 → graphql_api-2.0.0}/uv.lock +4 -4
  18. graphql_api-1.7.0/VERSION +0 -1
  19. {graphql_api-1.7.0 → graphql_api-2.0.0}/.github/workflows/pages.yml +0 -0
  20. {graphql_api-1.7.0 → graphql_api-2.0.0}/.gitignore +0 -0
  21. {graphql_api-1.7.0 → graphql_api-2.0.0}/.vscode/settings.json +0 -0
  22. {graphql_api-1.7.0 → graphql_api-2.0.0}/AGENTS.md +0 -0
  23. {graphql_api-1.7.0 → graphql_api-2.0.0}/CLAUDE.md +0 -0
  24. {graphql_api-1.7.0 → graphql_api-2.0.0}/LICENSE +0 -0
  25. {graphql_api-1.7.0 → graphql_api-2.0.0}/README.md +0 -0
  26. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/README.md +0 -0
  27. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/_index.md +0 -0
  28. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/_index.md +0 -0
  29. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/advanced-features/_index.md +0 -0
  30. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/advanced-features/directives.md +0 -0
  31. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/advanced-features/performance.md +0 -0
  32. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/advanced-features/schema-filtering.md +0 -0
  33. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/building-applications/_index.md +0 -0
  34. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/building-applications/documentation.md +0 -0
  35. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/building-applications/error-handling.md +0 -0
  36. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/_index.md +0 -0
  37. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/federation.md +0 -0
  38. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/pagination-relay.md +0 -0
  39. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/remote-graphql.md +0 -0
  40. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/fundamentals/_index.md +0 -0
  41. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/fundamentals/async-and-subscriptions.md +0 -0
  42. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/fundamentals/defining-schemas.md +0 -0
  43. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/fundamentals/enums-interfaces.md +0 -0
  44. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/fundamentals/getting-started.md +0 -0
  45. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/fundamentals/mutations.md +0 -0
  46. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/fundamentals/object-types.md +0 -0
  47. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/fundamentals/pydantic-and-dataclasses.md +0 -0
  48. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/production/_index.md +0 -0
  49. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/production/context-metadata.md +0 -0
  50. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/production/middleware.md +0 -0
  51. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/reference/_index.md +0 -0
  52. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/reference/api-reference.md +0 -0
  53. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/reference/contributing.md +0 -0
  54. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/content/docs/reference/examples.md +0 -0
  55. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/hugo.toml +0 -0
  56. {graphql_api-1.7.0 → graphql_api-2.0.0}/docs/index.md +0 -0
  57. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/__init__.py +0 -0
  58. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/api.py +0 -0
  59. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/context.py +0 -0
  60. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/dataclass_mapping.py +0 -0
  61. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/decorators.py +0 -0
  62. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/directives.py +0 -0
  63. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/error.py +0 -0
  64. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/exception.py +0 -0
  65. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/federation/__init__.py +0 -0
  66. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/federation/directives.py +0 -0
  67. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/federation/federation.py +0 -0
  68. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/federation/types.py +0 -0
  69. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/middleware.py +0 -0
  70. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/mutation_schema.py +0 -0
  71. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/pydantic.py +0 -0
  72. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/pydantic_utils.py +0 -0
  73. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/reduce.py +0 -0
  74. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/relay.py +0 -0
  75. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/schema.py +0 -0
  76. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/schema_filter.py +0 -0
  77. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/type_utils.py +0 -0
  78. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api/utils.py +0 -0
  79. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api.egg-info/SOURCES.txt +0 -0
  80. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api.egg-info/dependency_links.txt +0 -0
  81. {graphql_api-1.7.0 → graphql_api-2.0.0}/graphql_api.egg-info/top_level.txt +0 -0
  82. {graphql_api-1.7.0 → graphql_api-2.0.0}/setup.cfg +0 -0
  83. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/__init__.py +0 -0
  84. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_async.py +0 -0
  85. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_calculator.py +0 -0
  86. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_custom_suffixes.py +0 -0
  87. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_dataclass.py +0 -0
  88. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_dataclass_relationships.py +0 -0
  89. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_decorator_patterns.py +0 -0
  90. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_directives.py +0 -0
  91. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_docs_advanced.py +0 -0
  92. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_docs_async_subscriptions.py +0 -0
  93. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_docs_defining_schemas.py +0 -0
  94. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_docs_getting_started.py +0 -0
  95. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_docs_relay.py +0 -0
  96. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_docstrings.py +0 -0
  97. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_error.py +0 -0
  98. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_federation.py +0 -0
  99. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_federation_example.graphql +0 -0
  100. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_federation_example.py +0 -0
  101. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_filtering.py +0 -0
  102. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_graphql.py +0 -0
  103. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_middleware.py +0 -0
  104. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_performance.py +0 -0
  105. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_plain_functions.py +0 -0
  106. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_pydantic.py +0 -0
  107. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_relay.py +0 -0
  108. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_schema.py +0 -0
  109. {graphql_api-1.7.0 → graphql_api-2.0.0}/tests/test_unified_vs_explicit_schema.py +0 -0
@@ -30,6 +30,8 @@ jobs:
30
30
 
31
31
  - name: Install UV
32
32
  uses: astral-sh/setup-uv@v3
33
+ with:
34
+ enable-cache: true
33
35
 
34
36
  - name: Install dependencies
35
37
  run: uv sync --dev
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql-api
3
- Version: 1.7.0
3
+ Version: 2.0.0
4
4
  Summary: A framework for building Python GraphQL APIs.
5
5
  Author-email: Robert Parker <rob@parob.com>
6
6
  License: MIT
@@ -20,7 +20,7 @@ Classifier: Operating System :: OS Independent
20
20
  Requires-Python: >=3.11
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: graphql-core<3.3
23
+ Requires-Dist: graphql-core<3.4,>=3.3.1
24
24
  Requires-Dist: requests
25
25
  Requires-Dist: typing-inspect
26
26
  Requires-Dist: aiohttp
@@ -0,0 +1 @@
1
+ 2.0.0
@@ -180,22 +180,23 @@ You can define custom GraphQL scalar types for specialized data handling:
180
180
  from graphql import GraphQLScalarType, StringValueNode
181
181
 
182
182
  # Define a custom scalar
183
- def parse_value(value):
183
+ def coerce_input_value(value):
184
184
  return str(value) + "_parsed"
185
185
 
186
- def parse_literal(node):
186
+ def coerce_input_literal(node):
187
187
  if isinstance(node, StringValueNode):
188
- return parse_value(node.value)
188
+ return coerce_input_value(node.value)
189
+ raise TypeError("A Key literal must be a string")
189
190
 
190
- def serialize(value):
191
+ def coerce_output_value(value):
191
192
  return str(value) + "_serialized"
192
193
 
193
194
  GraphQLKey = GraphQLScalarType(
194
195
  name="Key",
195
196
  description="The `Key` scalar type represents a unique key.",
196
- serialize=serialize,
197
- parse_value=parse_value,
198
- parse_literal=parse_literal,
197
+ coerce_output_value=coerce_output_value,
198
+ coerce_input_value=coerce_input_value,
199
+ coerce_input_literal=coerce_input_literal,
199
200
  )
200
201
 
201
202
  @api.type(is_root_type=True)
@@ -206,9 +207,11 @@ class Root:
206
207
  ```
207
208
 
208
209
  **Key functions:**
209
- - `serialize`: Converts Python value to GraphQL response format
210
- - `parse_value`: Parses variable values from GraphQL requests
211
- - `parse_literal`: Parses literal values from GraphQL queries
210
+ - `coerce_output_value`: Converts a Python value to the GraphQL response format
211
+ - `coerce_input_value`: Parses variable values from GraphQL requests
212
+ - `coerce_input_literal`: Parses literal values written in GraphQL queries
213
+
214
+ These are the graphql-core 3.3 names. The older `serialize`, `parse_value` and `parse_literal` still work until graphql-core 3.4, but `parse_literal` is now always called with two arguments, `(node, variables)`.
212
215
 
213
216
  ## GraphQL ID Type
214
217
 
@@ -3,7 +3,6 @@ from inspect import isawaitable
3
3
  from typing import Any, Dict, Optional, List, Callable, AsyncIterator, Tuple
4
4
 
5
5
  from graphql import (
6
- ExecutionContext,
7
6
  GraphQLError,
8
7
  GraphQLOutputType,
9
8
  execute,
@@ -14,7 +13,9 @@ from graphql import (
14
13
  validate_schema,
15
14
  )
16
15
  from graphql.execution import ExecutionResult
16
+ from graphql.execution.executor import Executor
17
17
  from graphql.execution.middleware import MiddlewareManager
18
+ from graphql.pyutils import Path
18
19
  from graphql.language import DocumentNode
19
20
  from graphql.type.schema import GraphQLSchema
20
21
 
@@ -54,10 +55,10 @@ class GraphQLBaseExecutor:
54
55
  raise NotImplementedError
55
56
 
56
57
 
57
- class ErrorProtectionExecutionContext(ExecutionContext):
58
+ class ErrorProtectionExecutionContext(Executor):
58
59
  """
59
- A custom GraphQL ExecutionContext that can selectively suppress or raise errors
60
- based on an 'error_protection' attribute in the GraphQLError or its original error.
60
+ A custom graphql-core Executor that can selectively suppress or raise errors
61
+ based on an 'error_protection' attribute in the error or its original error.
61
62
  """
62
63
 
63
64
  # Default to True, meaning errors are protected by default (not raised directly).
@@ -66,40 +67,38 @@ class ErrorProtectionExecutionContext(ExecutionContext):
66
67
 
67
68
  def handle_field_error(
68
69
  self,
69
- error: GraphQLError,
70
+ raw_error: Exception,
70
71
  return_type: GraphQLOutputType,
71
- *args: Any,
72
- **kwargs: Any,
72
+ field_details_list: Any,
73
+ path: Path,
73
74
  ) -> None:
74
75
  """
75
76
  Intercept field errors to decide whether to raise or suppress them based on the
76
77
  'error_protection' attribute, either on the error or its original error.
77
78
 
78
- Anything after ``return_type`` is forwarded to graphql-core untouched.
79
- graphql-core added a ``path`` argument to this method in 3.2.8, and an
80
- override with a fixed arity turns every field error into a TypeError
81
- raised from inside the executor — so the resolver's real error never
82
- reaches the caller and a validation failure reads as a crash. Since
83
- this class exists precisely to sit in the error path, it must not be
84
- the thing that breaks it: accepting whatever the installed version
85
- passes keeps it working across both signatures.
79
+ graphql-core passes the error as raised. That is usually the resolver's own
80
+ exception, but it can be a GraphQLError that wraps one (e.g. from a nested
81
+ executor), so the flag is looked up on both.
86
82
  """
87
83
  error_protection = self.default_error_protection
88
- original_error = error.original_error
84
+ original_error = (
85
+ raw_error.original_error if isinstance(raw_error, GraphQLError) else None
86
+ )
89
87
 
90
88
  # Check if this error or its original error has a custom error_protection flag
91
- if hasattr(error, self.error_protection):
92
- error_protection = getattr(error, self.error_protection)
89
+ if hasattr(raw_error, self.error_protection):
90
+ error_protection = getattr(raw_error, self.error_protection)
93
91
  elif hasattr(original_error, self.error_protection):
94
92
  error_protection = getattr(original_error, self.error_protection)
95
93
 
96
94
  # If error protection is disabled, re-raise the original error
97
95
  if not error_protection:
98
- raise error.original_error if error.original_error is not None else error
96
+ raise original_error if original_error is not None else raw_error
99
97
 
100
- # Otherwise, call the default error handler. Positionally, because the
101
- # trailing arguments differ by version and only the caller knows them.
102
- return super().handle_field_error(error, return_type, *args, **kwargs)
98
+ # Otherwise, let graphql-core locate and collect the error.
99
+ return super().handle_field_error(
100
+ raw_error, return_type, field_details_list, path
101
+ )
103
102
 
104
103
 
105
104
  class NoErrorProtectionExecutionContext(ErrorProtectionExecutionContext):
@@ -153,8 +152,8 @@ class GraphQLExecutor(GraphQLBaseExecutor):
153
152
  # can serve every execution.
154
153
  self._middleware_manager = MiddlewareManager(*self.middleware)
155
154
 
156
- # Set the custom ExecutionContext class to handle error protection
157
- self.execution_context_class = (
155
+ # Set the custom Executor class to handle error protection
156
+ self.executor_class = (
158
157
  ErrorProtectionExecutionContext
159
158
  if error_protection
160
159
  else NoErrorProtectionExecutionContext
@@ -221,7 +220,7 @@ class GraphQLExecutor(GraphQLBaseExecutor):
221
220
  operation_name=operation_name,
222
221
  middleware=self._middleware_manager,
223
222
  root_value=root_value,
224
- execution_context_class=self.execution_context_class,
223
+ executor_class=self.executor_class,
225
224
  )
226
225
  return result
227
226
 
@@ -248,7 +247,7 @@ class GraphQLExecutor(GraphQLBaseExecutor):
248
247
  operation_name=operation_name,
249
248
  middleware=self._middleware_manager,
250
249
  root_value=root_value,
251
- execution_context_class=self.execution_context_class,
250
+ executor_class=self.executor_class,
252
251
  )
253
252
  if isawaitable(result):
254
253
  result = await result
@@ -267,13 +266,15 @@ class GraphQLExecutor(GraphQLBaseExecutor):
267
266
  # graphql-core subscribe expects a parsed document
268
267
  document = parse(query)
269
268
  # Use the same middleware and execution context where applicable
270
- async_iter = await subscribe(
269
+ async_iter = subscribe(
271
270
  schema=self.schema,
272
271
  document=document,
273
272
  variable_values=variables,
274
273
  operation_name=operation_name,
275
274
  context_value=context,
276
275
  root_value=root_value,
277
- # execution_context_class is not a parameter for subscribe in graphql-core v3
278
276
  )
277
+ # graphql-core >= 3.3 returns the result directly when it can.
278
+ if isawaitable(async_iter):
279
+ async_iter = await async_iter
279
280
  return async_iter # type: ignore
@@ -1,7 +1,5 @@
1
- import collections.abc
2
1
  import enum
3
2
  import inspect
4
- import types
5
3
  import typing
6
4
  from datetime import date, datetime
7
5
  from typing import Any, Annotated, Callable, List, Optional, Set, Tuple, Type, Union, cast, get_origin, get_args
@@ -21,7 +19,7 @@ from graphql import (
21
19
  is_union_type,
22
20
  GraphQLWrappingType,
23
21
  )
24
- from graphql.pyutils import Undefined, UndefinedType
22
+ from graphql.pyutils import Undefined
25
23
  from graphql.type.definition import (
26
24
  GraphQLArgument,
27
25
  GraphQLEnumType,
@@ -659,27 +657,6 @@ class GraphQLTypeMapper:
659
657
 
660
658
  enum_type.enum_type = type_
661
659
 
662
- def serialize(_self, value) -> Union[str, None, UndefinedType]:
663
- if value and isinstance(value, collections.abc.Hashable):
664
- # For enums, always return the enum name (which matches the GraphQL enum value)
665
- if isinstance(value, enum.Enum):
666
- return value.name
667
- else:
668
- # For non-enum values, try to look them up in the value lookup
669
- # This handles cases where the value might be a string representation
670
- lookup_key = value
671
- # noinspection PyProtectedMember
672
- lookup_value = _self._value_lookup.get(lookup_key)
673
- if lookup_value:
674
- return lookup_value
675
- else:
676
- # If lookup fails, the value might be invalid
677
- return Undefined
678
-
679
- return None
680
-
681
- enum_type.serialize = types.MethodType(serialize, enum_type)
682
-
683
660
  self.add_applied_directives(enum_type, name, type_)
684
661
 
685
662
  return enum_type
@@ -831,7 +808,7 @@ class GraphQLTypeMapper:
831
808
 
832
809
  # Use the raw Python default for input coercion.
833
810
  # Do not convert to GraphQL query literals here.
834
- default_value = local_default_args.get(key, None)
811
+ default_value = local_default_args.get(key, Undefined)
835
812
 
836
813
  arguments[to_camel_case(key)] = GraphQLInputField(
837
814
  type_=input_arg_type, default_value=default_value # type: ignore
@@ -602,9 +602,15 @@ class GraphQLRemoteObject:
602
602
  ast_val = to_ast_value(val, unwrapped_field_type)
603
603
  if isinstance(
604
604
  unwrapped_field_type, (GraphQLScalarType, GraphQLEnumType)
605
- ) and hasattr(unwrapped_field_type, "parse_literal"):
606
- parsed_val = unwrapped_field_type.parse_literal(
607
- ast_val, None) # type: ignore
605
+ ):
606
+ # Scalars defined with the pre-3.3 hooks have no
607
+ # coerce_input_literal; they still provide parse_literal.
608
+ coerce_literal = unwrapped_field_type.coerce_input_literal
609
+ if coerce_literal is not None:
610
+ parsed_val = coerce_literal(ast_val) # type: ignore
611
+ else:
612
+ parsed_val = unwrapped_field_type.parse_literal(
613
+ ast_val, None) # type: ignore
608
614
  # If the field is an enum, convert to the Python enum if available
609
615
  if isinstance(unwrapped_field_type, GraphQLEnumType) and hasattr(
610
616
  unwrapped_field_type, "enum_type"
@@ -1,10 +1,11 @@
1
1
  import binascii
2
+ import collections.abc
2
3
  import datetime
3
4
  import json
4
5
  import uuid
5
6
  import enum
6
7
 
7
- from typing import Any, Dict, List, Optional, Type, Union, cast
8
+ from typing import Any, Dict, List, Optional, Type, Union
8
9
 
9
10
  from graphql import (
10
11
  GraphQLEnumType,
@@ -13,45 +14,54 @@ from graphql import (
13
14
  Undefined,
14
15
  ValueNode,
15
16
  )
17
+ from graphql.pyutils import UndefinedType
16
18
  from graphql.language import ast
17
19
 
18
20
 
19
21
  class GraphQLMappedEnumType(GraphQLEnumType):
22
+ """A GraphQL enum backed by a Python Enum.
23
+
24
+ Inputs (inline literals and variables) are coerced to the Python Enum member,
25
+ and outputs accept either the member or its value.
26
+ """
20
27
 
21
28
  def __init__(self, *args, **kwargs):
22
29
  super().__init__(*args, **kwargs)
23
30
  self.enum_type: Optional[Type[enum.Enum]] = None
24
31
 
25
- def parse_literal(self, *args, **kwargs):
26
- result = super().parse_literal(*args, **kwargs)
27
- if result is not Undefined and result is not None and getattr(self, "enum_type", None):
32
+ def _to_enum_member(self, result):
33
+ if result is not Undefined and result is not None and self.enum_type is not None:
28
34
  try:
29
- enum_type: Optional[Type[enum.Enum]] = self.enum_type
30
- if enum_type is None:
31
- return result
32
- return cast(Type[enum.Enum], enum_type)(result)
35
+ return self.enum_type(result)
33
36
  except (ValueError, KeyError, TypeError):
34
37
  # Invalid enum value or type casting error
35
38
  return Undefined
36
39
  return result
37
40
 
38
- def parse_value(self, value):
39
- """Coerce incoming variable values to the mapped Python Enum instance.
41
+ def coerce_input_literal(self, value_node, hide_suggestions=False):
42
+ return self._to_enum_member(
43
+ super().coerce_input_literal(value_node, hide_suggestions)
44
+ )
45
+
46
+ def coerce_input_value(self, input_value, hide_suggestions=False):
47
+ return self._to_enum_member(
48
+ super().coerce_input_value(input_value, hide_suggestions)
49
+ )
50
+
51
+ def coerce_output_value(self, output_value) -> Union[str, None, UndefinedType]:
52
+ if output_value and isinstance(output_value, collections.abc.Hashable):
53
+ # For enums, always return the enum name (which matches the GraphQL enum value)
54
+ if isinstance(output_value, enum.Enum):
55
+ return output_value.name
56
+ # For non-enum values, try to look them up in the value lookup.
57
+ # This handles cases where the value might be a string representation.
58
+ lookup_value = self._value_lookup.get(output_value)
59
+ if lookup_value:
60
+ return lookup_value
61
+ # If lookup fails, the value is invalid
62
+ return Undefined
40
63
 
41
- GraphQL variables use `parse_value`, while inline literals use
42
- `parse_literal`. Ensure both paths produce the Python Enum.
43
- """
44
- result = super().parse_value(value)
45
- if result is not Undefined and result is not None and getattr(self, "enum_type", None):
46
- try:
47
- enum_type: Optional[Type[enum.Enum]] = self.enum_type
48
- if enum_type is None:
49
- return result
50
- return cast(Type[enum.Enum], enum_type)(result)
51
- except (ValueError, KeyError, TypeError):
52
- # Invalid enum value or type casting error
53
- return Undefined
54
- return result
64
+ return None
55
65
 
56
66
 
57
67
  def parse_uuid_literal(
@@ -85,9 +95,9 @@ def parse_uuid_value(value: Any) -> uuid.UUID:
85
95
  GraphQLUUID = GraphQLScalarType(
86
96
  name="UUID",
87
97
  description="The `UUID` scalar type represents a unique identifer.",
88
- serialize=str,
89
- parse_value=parse_uuid_value,
90
- parse_literal=parse_uuid_literal,
98
+ coerce_output_value=str,
99
+ coerce_input_value=parse_uuid_value,
100
+ coerce_input_literal=parse_uuid_literal,
91
101
  )
92
102
 
93
103
 
@@ -134,9 +144,9 @@ GraphQLDateTime = GraphQLScalarType(
134
144
  name="DateTime",
135
145
  description="The `DateTime` scalar type represents a datetime, "
136
146
  "the datetime should be in the format `2018-01-22 17:46:32`",
137
- serialize=serialize_datetime,
138
- parse_value=parse_datetime_value,
139
- parse_literal=parse_datetime_literal,
147
+ coerce_output_value=serialize_datetime,
148
+ coerce_input_value=parse_datetime_value,
149
+ coerce_input_literal=parse_datetime_literal,
140
150
  )
141
151
 
142
152
 
@@ -166,9 +176,9 @@ GraphQLDate = GraphQLScalarType(
166
176
  name="Date",
167
177
  description="The `Date` scalar type represents a datetime, "
168
178
  "the datetime should be in the format `2018-01-22`",
169
- serialize=serialize_date,
170
- parse_value=parse_date_value,
171
- parse_literal=parse_date_literal,
179
+ coerce_output_value=serialize_date,
180
+ coerce_input_value=parse_date_value,
181
+ coerce_input_literal=parse_date_literal,
172
182
  )
173
183
 
174
184
 
@@ -204,9 +214,9 @@ GraphQLJSON = GraphQLScalarType(
204
214
  description="The `JSON` scalar type represents JSON values as specified by"
205
215
  " [ECMA-404](http://www.ecma-international.org/"
206
216
  "publications/files/ECMA-ST/ECMA-404.pdf).",
207
- serialize=serialize_json,
208
- parse_value=parse_json_value,
209
- parse_literal=parse_json_literal,
217
+ coerce_output_value=serialize_json,
218
+ coerce_input_value=parse_json_value,
219
+ coerce_input_literal=parse_json_literal,
210
220
  )
211
221
 
212
222
 
@@ -234,7 +244,7 @@ GraphQLBytes = GraphQLScalarType(
234
244
  description="The `Bytes` scalar type expects and returns a "
235
245
  "Byte array in UTF-8 string format that represents the Bytes. "
236
246
  "If the data is not UTF-encodable the errors will be ignored",
237
- serialize=serialize_bytes,
238
- parse_value=parse_bytes_value,
239
- parse_literal=parse_bytes_literal,
247
+ coerce_output_value=serialize_bytes,
248
+ coerce_input_value=parse_bytes_value,
249
+ coerce_input_literal=parse_bytes_literal,
240
250
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql-api
3
- Version: 1.7.0
3
+ Version: 2.0.0
4
4
  Summary: A framework for building Python GraphQL APIs.
5
5
  Author-email: Robert Parker <rob@parob.com>
6
6
  License: MIT
@@ -20,7 +20,7 @@ Classifier: Operating System :: OS Independent
20
20
  Requires-Python: >=3.11
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: graphql-core<3.3
23
+ Requires-Dist: graphql-core<3.4,>=3.3.1
24
24
  Requires-Dist: requests
25
25
  Requires-Dist: typing-inspect
26
26
  Requires-Dist: aiohttp
@@ -1,4 +1,4 @@
1
- graphql-core<3.3
1
+ graphql-core<3.4,>=3.3.1
2
2
  requests
3
3
  typing-inspect
4
4
  aiohttp
@@ -22,7 +22,7 @@ classifiers = [
22
22
  "Operating System :: OS Independent",
23
23
  ]
24
24
  dependencies = [
25
- "graphql-core<3.3",
25
+ "graphql-core>=3.3.1,<3.4",
26
26
  "requests",
27
27
  "typing-inspect",
28
28
  "aiohttp",
@@ -371,19 +371,20 @@ class TestCustomTypes:
371
371
  def test_custom_scalar_type(self) -> None:
372
372
  api = GraphQLAPI()
373
373
 
374
- def parse_value(value):
374
+ def coerce_input_value(value):
375
375
  return str(value) + "_parsed_value"
376
376
 
377
- def parse_literal(node):
377
+ def coerce_input_literal(node):
378
378
  if isinstance(node, StringValueNode):
379
- return parse_value(node.value)
379
+ return coerce_input_value(node.value)
380
+ raise TypeError("A Key literal must be a string")
380
381
 
381
382
  GraphQLKey = GraphQLScalarType(
382
383
  name="Key",
383
384
  description="The `Key` scalar type represents a key.",
384
- serialize=lambda value: str(value) + "_serialized",
385
- parse_value=parse_value,
386
- parse_literal=parse_literal, # type: ignore[reportIncompatibleMethodOverride]
385
+ coerce_output_value=lambda value: str(value) + "_serialized",
386
+ coerce_input_value=coerce_input_value,
387
+ coerce_input_literal=coerce_input_literal,
387
388
  )
388
389
 
389
390
  @api.type(is_root_type=True)
@@ -189,22 +189,23 @@ class TestFieldTypesExamples:
189
189
  api = GraphQLAPI()
190
190
 
191
191
  # Define a custom scalar
192
- def parse_value(value):
192
+ def coerce_input_value(value):
193
193
  return str(value) + "_parsed"
194
194
 
195
- def parse_literal(node):
195
+ def coerce_input_literal(node):
196
196
  if isinstance(node, StringValueNode):
197
- return parse_value(node.value)
197
+ return coerce_input_value(node.value)
198
+ raise TypeError("A Key literal must be a string")
198
199
 
199
- def serialize(value):
200
+ def coerce_output_value(value):
200
201
  return str(value) + "_serialized"
201
202
 
202
203
  GraphQLKey = GraphQLScalarType(
203
204
  name="Key",
204
205
  description="The `Key` scalar type represents a unique key.",
205
- serialize=serialize,
206
- parse_value=parse_value,
207
- parse_literal=parse_literal,
206
+ coerce_output_value=coerce_output_value,
207
+ coerce_input_value=coerce_input_value,
208
+ coerce_input_literal=coerce_input_literal,
208
209
  )
209
210
 
210
211
  @api.type(is_root_type=True)
@@ -12,7 +12,8 @@ It went unnoticed because this library's own environment resolves an older
12
12
  graphql-core than its consumers do, so the existing error tests all pass here
13
13
  and fail there. Asserting against a pinned signature would repeat that mistake,
14
14
  so these tests ask the *installed* graphql-core what it expects and check the
15
- override against that.
15
+ override against that. (graphql-core 3.3 renamed `ExecutionContext` to
16
+ `Executor`; the hook kept its name and now receives the raw error.)
16
17
  """
17
18
 
18
19
  from __future__ import annotations
@@ -20,7 +21,7 @@ from __future__ import annotations
20
21
  import inspect
21
22
 
22
23
  from graphql import GraphQLError, GraphQLInt
23
- from graphql.execution.execute import ExecutionContext
24
+ from graphql.execution.executor import CollectedErrors, Executor
24
25
  from graphql.pyutils import Path
25
26
 
26
27
  from graphql_api.api import GraphQLAPI
@@ -45,7 +46,7 @@ def _accepted_arity(func) -> int:
45
46
  class TestHandleFieldErrorSignature:
46
47
  def test_override_accepts_everything_graphql_core_passes(self) -> None:
47
48
  """The override must be callable however the installed version calls it."""
48
- expected = _accepted_arity(ExecutionContext.handle_field_error)
49
+ expected = _accepted_arity(Executor.handle_field_error)
49
50
  override = inspect.signature(
50
51
  ErrorProtectionExecutionContext.handle_field_error
51
52
  )
@@ -65,27 +66,45 @@ class TestHandleFieldErrorSignature:
65
66
 
66
67
  This is the exact call that was raising TypeError in production.
67
68
  """
68
- context = ErrorProtectionExecutionContext.__new__(
69
+ executor = ErrorProtectionExecutionContext.__new__(
69
70
  ErrorProtectionExecutionContext
70
71
  )
71
- error = GraphQLError("boom", original_error=ValueError("boom"))
72
- args = [error, GraphQLInt]
73
- if _accepted_arity(ExecutionContext.handle_field_error) > 2:
74
- # graphql-core >= 3.2.8 also passes the field's path.
75
- args.append(Path(None, "field", None))
76
-
77
- # Nullable return type, so the error is collected rather than re-raised;
78
- # the point is only that the call itself is well-formed.
79
- context.errors = []
72
+ executor.collected_errors = CollectedErrors()
73
+ executor.error_propagation = True
74
+ # raw error, return type, field details, path: as many as the
75
+ # installed graphql-core passes.
76
+ args = [ValueError("boom"), GraphQLInt, [], Path(None, "field", None)]
77
+ args = args[:_accepted_arity(Executor.handle_field_error)]
78
+
79
+ # Nullable return type, so the error is collected rather than re-raised.
80
80
  try:
81
- context.handle_field_error(*args)
81
+ executor.handle_field_error(*args)
82
82
  except TypeError as exc: # pragma: no cover - this is the regression
83
83
  raise AssertionError(
84
84
  f"handle_field_error rejected graphql-core's own call: {exc}"
85
85
  ) from exc
86
- except Exception:
87
- # Any GraphQL-level outcome is fine; a TypeError is not.
88
- pass
86
+
87
+ assert [e.message for e in executor.collected_errors.errors] == ["boom"]
88
+
89
+ def test_a_wrapped_error_keeps_its_protection_flag(self) -> None:
90
+ """A GraphQLError wrapping the resolver's error (e.g. from a nested
91
+ executor) still honours the flag set on the original error."""
92
+ executor = NoErrorProtectionExecutionContext.__new__(
93
+ NoErrorProtectionExecutionContext
94
+ )
95
+ executor.collected_errors = CollectedErrors()
96
+ executor.error_propagation = True
97
+ original = ValueError("boom")
98
+ wrapped = GraphQLError("boom", original_error=original)
99
+
100
+ try:
101
+ executor.handle_field_error(
102
+ wrapped, GraphQLInt, [], Path(None, "field", None)
103
+ )
104
+ except ValueError as exc:
105
+ assert exc is original
106
+ else: # pragma: no cover
107
+ raise AssertionError("the unprotected error was swallowed")
89
108
 
90
109
  def test_the_no_protection_variant_inherits_the_same_shape(self) -> None:
91
110
  assert (
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  import enum
3
- import random
3
+ import itertools
4
4
  import time
5
5
  import uuid
6
6
  from dataclasses import dataclass
@@ -491,6 +491,10 @@ class TestGraphQLRemote:
491
491
 
492
492
  def test_remote_return_object_cache(self) -> None:
493
493
  api = GraphQLAPI()
494
+ # Every real resolver call returns a new number, so getting the same
495
+ # number back means the cache answered. (random.randint here made
496
+ # the "not cached" assertions fail whenever two draws collided.)
497
+ calls = itertools.count()
494
498
 
495
499
  @dataclass
496
500
  class Door:
@@ -498,7 +502,7 @@ class TestGraphQLRemote:
498
502
 
499
503
  @api.field
500
504
  def rand(self, max: int = 100) -> int:
501
- return random.randint(0, max)
505
+ return next(calls)
502
506
 
503
507
  @api.type(is_root_type=True)
504
508
  class House:
@@ -1,4 +1,6 @@
1
1
  from asyncio import create_task, sleep, wait
2
+ from collections.abc import AsyncIterator
3
+ from inspect import isawaitable
2
4
  from dataclasses import dataclass
3
5
  from typing import Any, AsyncGenerator
4
6
  import enum
@@ -9,7 +11,6 @@ from graphql import (
9
11
  GraphQLInt,
10
12
  GraphQLObjectType,
11
13
  GraphQLSchema,
12
- MapAsyncIterator,
13
14
  graphql,
14
15
  parse,
15
16
  subscribe,
@@ -69,8 +70,10 @@ class TestSubscriptions:
69
70
 
70
71
  query = "subscription {count}"
71
72
 
72
- subscription = await subscribe(schema, parse(query))
73
- assert isinstance(subscription, MapAsyncIterator)
73
+ subscription = subscribe(schema, parse(query))
74
+ if isawaitable(subscription):
75
+ subscription = await subscription
76
+ assert isinstance(subscription, AsyncIterator)
74
77
 
75
78
  assert a and b and c
76
79
 
@@ -86,7 +89,9 @@ class TestSubscriptions:
86
89
  await sleep(0.1)
87
90
  await graphql(schema, "mutation {increaseCount}")
88
91
 
89
- subscription = await subscribe(schema, parse(query)) # type: ignore
92
+ subscription = subscribe(schema, parse(query))
93
+ if isawaitable(subscription):
94
+ subscription = await subscription
90
95
 
91
96
  async def receive_count():
92
97
  async for result in subscription: # type: ignore
@@ -546,7 +546,7 @@ dev = [
546
546
  requires-dist = [
547
547
  { name = "aiohttp" },
548
548
  { name = "docstring-parser" },
549
- { name = "graphql-core", specifier = "<3.3" },
549
+ { name = "graphql-core", specifier = ">=3.3.1,<3.4" },
550
550
  { name = "pydantic" },
551
551
  { name = "requests" },
552
552
  { name = "typing-inspect" },
@@ -569,11 +569,11 @@ dev = [
569
569
 
570
570
  [[package]]
571
571
  name = "graphql-core"
572
- version = "3.2.13"
572
+ version = "3.3.1"
573
573
  source = { registry = "https://pypi.org/simple" }
574
- sdist = { url = "https://files.pythonhosted.org/packages/4e/5e/aa0d4e701b50db0bab71b125dd19ddb0f98c638d008d6f7e3d8ce9cbc92e/graphql_core-3.2.13.tar.gz", hash = "sha256:bb81dd266d4ab7b591bd976f1b23639d97776cb9ac1a896b4a93c271e11ed618", size = 577471, upload-time = "2026-09-27T12:22:57.136Z" }
574
+ sdist = { url = "https://files.pythonhosted.org/packages/a4/91/223fbace7c8648690d7fb77855e29bd248c9ed1e0833ed3002ab29bc1441/graphql_core-3.3.1.tar.gz", hash = "sha256:8d9b6f95fb66b58b260904cf9e191f422782620533ec8fa0c96ab941b6659a2a", size = 751402, upload-time = "2026-10-25T09:34:09.764Z" }
575
575
  wheels = [
576
- { url = "https://files.pythonhosted.org/packages/1f/b4/b52a324b0b297ddc49b8ea31a51d39fae63132f694a267b9381d94e2e0af/graphql_core-3.2.13-py3-none-any.whl", hash = "sha256:b0eb04f2c31556b2310a77c8fb53c74e8b56570f8fea594d89c6ef7827dbb497", size = 268191, upload-time = "2026-09-27T12:22:55.661Z" },
576
+ { url = "https://files.pythonhosted.org/packages/57/0e/e51b006267e7fa785abf5d1f86a7d60b84f7f893e3ba4fe23df9be2000cf/graphql_core-3.3.1-py3-none-any.whl", hash = "sha256:43ebcd0d9a0662a71b1a2f6562ba863d253057ec11cbc45835b2e2683a0a3770", size = 349047, upload-time = "2026-10-25T09:34:07.725Z" },
577
577
  ]
578
578
 
579
579
  [[package]]
graphql_api-1.7.0/VERSION DELETED
@@ -1 +0,0 @@
1
- 1.7.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes