graphql-api 1.6.9__tar.gz → 1.7.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 (110) hide show
  1. {graphql_api-1.6.9 → graphql_api-1.7.0}/.github/workflows/python-package.yml +1 -1
  2. {graphql_api-1.6.9 → graphql_api-1.7.0}/PKG-INFO +8 -3
  3. graphql_api-1.7.0/VERSION +1 -0
  4. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/executor.py +14 -2
  5. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/remote.py +8 -0
  6. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api.egg-info/PKG-INFO +8 -3
  7. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api.egg-info/SOURCES.txt +1 -0
  8. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api.egg-info/requires.txt +1 -1
  9. {graphql_api-1.6.9 → graphql_api-1.7.0}/pyproject.toml +7 -2
  10. graphql_api-1.7.0/tests/test_executor_signature.py +116 -0
  11. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_performance.py +16 -10
  12. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_remote.py +12 -0
  13. graphql_api-1.7.0/uv.lock +1506 -0
  14. graphql_api-1.6.9/VERSION +0 -1
  15. graphql_api-1.6.9/uv.lock +0 -948
  16. {graphql_api-1.6.9 → graphql_api-1.7.0}/.github/workflows/pages.yml +0 -0
  17. {graphql_api-1.6.9 → graphql_api-1.7.0}/.gitignore +0 -0
  18. {graphql_api-1.6.9 → graphql_api-1.7.0}/.vscode/settings.json +0 -0
  19. {graphql_api-1.6.9 → graphql_api-1.7.0}/AGENTS.md +0 -0
  20. {graphql_api-1.6.9 → graphql_api-1.7.0}/CLAUDE.md +0 -0
  21. {graphql_api-1.6.9 → graphql_api-1.7.0}/LICENSE +0 -0
  22. {graphql_api-1.6.9 → graphql_api-1.7.0}/README.md +0 -0
  23. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/README.md +0 -0
  24. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/_index.md +0 -0
  25. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/_index.md +0 -0
  26. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/advanced-features/_index.md +0 -0
  27. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/advanced-features/directives.md +0 -0
  28. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/advanced-features/performance.md +0 -0
  29. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/advanced-features/schema-filtering.md +0 -0
  30. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/building-applications/_index.md +0 -0
  31. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/building-applications/documentation.md +0 -0
  32. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/building-applications/error-handling.md +0 -0
  33. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/distributed-systems/_index.md +0 -0
  34. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/distributed-systems/federation.md +0 -0
  35. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/distributed-systems/pagination-relay.md +0 -0
  36. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/distributed-systems/remote-graphql.md +0 -0
  37. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/_index.md +0 -0
  38. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/async-and-subscriptions.md +0 -0
  39. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/defining-schemas.md +0 -0
  40. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/enums-interfaces.md +0 -0
  41. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/field-types.md +0 -0
  42. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/getting-started.md +0 -0
  43. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/mutations.md +0 -0
  44. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/object-types.md +0 -0
  45. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/pydantic-and-dataclasses.md +0 -0
  46. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/production/_index.md +0 -0
  47. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/production/context-metadata.md +0 -0
  48. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/production/middleware.md +0 -0
  49. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/reference/_index.md +0 -0
  50. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/reference/api-reference.md +0 -0
  51. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/reference/contributing.md +0 -0
  52. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/reference/examples.md +0 -0
  53. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/hugo.toml +0 -0
  54. {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/index.md +0 -0
  55. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/__init__.py +0 -0
  56. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/api.py +0 -0
  57. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/context.py +0 -0
  58. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/dataclass_mapping.py +0 -0
  59. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/decorators.py +0 -0
  60. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/directives.py +0 -0
  61. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/error.py +0 -0
  62. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/exception.py +0 -0
  63. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/federation/__init__.py +0 -0
  64. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/federation/directives.py +0 -0
  65. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/federation/federation.py +0 -0
  66. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/federation/types.py +0 -0
  67. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/mapper.py +0 -0
  68. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/middleware.py +0 -0
  69. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/mutation_schema.py +0 -0
  70. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/pydantic.py +0 -0
  71. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/pydantic_utils.py +0 -0
  72. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/reduce.py +0 -0
  73. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/relay.py +0 -0
  74. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/schema.py +0 -0
  75. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/schema_filter.py +0 -0
  76. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/type_utils.py +0 -0
  77. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/types.py +0 -0
  78. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/utils.py +0 -0
  79. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api.egg-info/dependency_links.txt +0 -0
  80. {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api.egg-info/top_level.txt +0 -0
  81. {graphql_api-1.6.9 → graphql_api-1.7.0}/setup.cfg +0 -0
  82. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/__init__.py +0 -0
  83. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_async.py +0 -0
  84. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_calculator.py +0 -0
  85. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_custom_suffixes.py +0 -0
  86. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_custom_types.py +0 -0
  87. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_dataclass.py +0 -0
  88. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_dataclass_relationships.py +0 -0
  89. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_decorator_patterns.py +0 -0
  90. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_directives.py +0 -0
  91. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_advanced.py +0 -0
  92. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_async_subscriptions.py +0 -0
  93. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_defining_schemas.py +0 -0
  94. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_field_types.py +0 -0
  95. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_getting_started.py +0 -0
  96. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_relay.py +0 -0
  97. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docstrings.py +0 -0
  98. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_error.py +0 -0
  99. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_federation.py +0 -0
  100. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_federation_example.graphql +0 -0
  101. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_federation_example.py +0 -0
  102. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_filtering.py +0 -0
  103. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_graphql.py +0 -0
  104. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_middleware.py +0 -0
  105. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_plain_functions.py +0 -0
  106. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_pydantic.py +0 -0
  107. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_relay.py +0 -0
  108. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_schema.py +0 -0
  109. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_subscriptions.py +0 -0
  110. {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_unified_vs_explicit_schema.py +0 -0
@@ -17,7 +17,7 @@ jobs:
17
17
  strategy:
18
18
  fail-fast: false
19
19
  matrix:
20
- python-version: ["3.11", "3.12"] # graphql-api requires Python 3.11+
20
+ python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] # graphql-api requires Python 3.11+
21
21
 
22
22
  steps:
23
23
  - name: Checkout repository
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql-api
3
- Version: 1.6.9
3
+ Version: 1.7.0
4
4
  Summary: A framework for building Python GraphQL APIs.
5
5
  Author-email: Robert Parker <rob@parob.com>
6
6
  License: MIT
@@ -10,12 +10,17 @@ Keywords: GraphQL,GraphQL-API,GraphQLAPI,Server
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
13
18
  Classifier: License :: OSI Approved :: MIT License
14
19
  Classifier: Operating System :: OS Independent
15
- Requires-Python: <3.13,>=3.11
20
+ Requires-Python: >=3.11
16
21
  Description-Content-Type: text/markdown
17
22
  License-File: LICENSE
18
- Requires-Dist: graphql-core
23
+ Requires-Dist: graphql-core<3.3
19
24
  Requires-Dist: requests
20
25
  Requires-Dist: typing-inspect
21
26
  Requires-Dist: aiohttp
@@ -0,0 +1 @@
1
+ 1.7.0
@@ -68,10 +68,21 @@ class ErrorProtectionExecutionContext(ExecutionContext):
68
68
  self,
69
69
  error: GraphQLError,
70
70
  return_type: GraphQLOutputType,
71
+ *args: Any,
72
+ **kwargs: Any,
71
73
  ) -> None:
72
74
  """
73
75
  Intercept field errors to decide whether to raise or suppress them based on the
74
76
  'error_protection' attribute, either on the error or its original error.
77
+
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.
75
86
  """
76
87
  error_protection = self.default_error_protection
77
88
  original_error = error.original_error
@@ -86,8 +97,9 @@ class ErrorProtectionExecutionContext(ExecutionContext):
86
97
  if not error_protection:
87
98
  raise error.original_error if error.original_error is not None else error
88
99
 
89
- # Otherwise, call the default error handler
90
- return super().handle_field_error(error=error, return_type=return_type)
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)
91
103
 
92
104
 
93
105
  class NoErrorProtectionExecutionContext(ErrorProtectionExecutionContext):
@@ -157,6 +157,14 @@ class GraphQLRemoteExecutor(GraphQLBaseExecutor, GraphQLObjectType):
157
157
  A GraphQL executor that forwards all operations to a remote GraphQL service.
158
158
  """
159
159
 
160
+ def __new__(cls, url, name="Remote", *args, **kwargs):
161
+ # graphql-core >= 3.2.7 gives GraphQLNamedType a __new__(cls, name, ...)
162
+ # that checks for reserved type names. Our first argument is the URL, so
163
+ # pass the type name explicitly (older versions have no such __new__).
164
+ if GraphQLNamedType.__new__ is object.__new__:
165
+ return super().__new__(cls)
166
+ return super().__new__(cls, name)
167
+
160
168
  def __init__(
161
169
  self,
162
170
  url,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql-api
3
- Version: 1.6.9
3
+ Version: 1.7.0
4
4
  Summary: A framework for building Python GraphQL APIs.
5
5
  Author-email: Robert Parker <rob@parob.com>
6
6
  License: MIT
@@ -10,12 +10,17 @@ Keywords: GraphQL,GraphQL-API,GraphQLAPI,Server
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
13
18
  Classifier: License :: OSI Approved :: MIT License
14
19
  Classifier: Operating System :: OS Independent
15
- Requires-Python: <3.13,>=3.11
20
+ Requires-Python: >=3.11
16
21
  Description-Content-Type: text/markdown
17
22
  License-File: LICENSE
18
- Requires-Dist: graphql-core
23
+ Requires-Dist: graphql-core<3.3
19
24
  Requires-Dist: requests
20
25
  Requires-Dist: typing-inspect
21
26
  Requires-Dist: aiohttp
@@ -89,6 +89,7 @@ tests/test_docs_getting_started.py
89
89
  tests/test_docs_relay.py
90
90
  tests/test_docstrings.py
91
91
  tests/test_error.py
92
+ tests/test_executor_signature.py
92
93
  tests/test_federation.py
93
94
  tests/test_federation_example.graphql
94
95
  tests/test_federation_example.py
@@ -1,4 +1,4 @@
1
- graphql-core
1
+ graphql-core<3.3
2
2
  requests
3
3
  typing-inspect
4
4
  aiohttp
@@ -4,7 +4,7 @@ dynamic = ["version"]
4
4
  description = "A framework for building Python GraphQL APIs."
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
7
- requires-python = ">=3.11,<3.13"
7
+ requires-python = ">=3.11"
8
8
  authors = [
9
9
  {name = "Robert Parker", email = "rob@parob.com"}
10
10
  ]
@@ -13,11 +13,16 @@ classifiers = [
13
13
  "Development Status :: 3 - Alpha",
14
14
  "Intended Audience :: Developers",
15
15
  "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Programming Language :: Python :: 3.15",
16
21
  "License :: OSI Approved :: MIT License",
17
22
  "Operating System :: OS Independent",
18
23
  ]
19
24
  dependencies = [
20
- "graphql-core",
25
+ "graphql-core<3.3",
21
26
  "requests",
22
27
  "typing-inspect",
23
28
  "aiohttp",
@@ -0,0 +1,116 @@
1
+ """The error path must survive graphql-core changing under us.
2
+
3
+ `ErrorProtectionExecutionContext` overrides `handle_field_error`, which
4
+ graphql-core calls positionally from inside the executor. graphql-core added a
5
+ `path` argument to it in 3.2.8; the override kept its three-argument shape, and
6
+ the result was that **every** field error became a `TypeError` raised from
7
+ inside the executor. A resolver raising `ValueError("title is required")` came
8
+ back to the caller as a crash with no message — the one thing an error path
9
+ must never do.
10
+
11
+ It went unnoticed because this library's own environment resolves an older
12
+ graphql-core than its consumers do, so the existing error tests all pass here
13
+ and fail there. Asserting against a pinned signature would repeat that mistake,
14
+ so these tests ask the *installed* graphql-core what it expects and check the
15
+ override against that.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import inspect
21
+
22
+ from graphql import GraphQLError, GraphQLInt
23
+ from graphql.execution.execute import ExecutionContext
24
+ from graphql.pyutils import Path
25
+
26
+ from graphql_api.api import GraphQLAPI
27
+ from graphql_api.executor import (
28
+ ErrorProtectionExecutionContext,
29
+ NoErrorProtectionExecutionContext,
30
+ )
31
+
32
+
33
+ def _accepted_arity(func) -> int:
34
+ """How many positional arguments this method takes, `self` excluded."""
35
+ parameters = [
36
+ p for name, p in inspect.signature(func).parameters.items()
37
+ if name != "self"
38
+ ]
39
+ return sum(
40
+ 1 for p in parameters
41
+ if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
42
+ )
43
+
44
+
45
+ class TestHandleFieldErrorSignature:
46
+ def test_override_accepts_everything_graphql_core_passes(self) -> None:
47
+ """The override must be callable however the installed version calls it."""
48
+ expected = _accepted_arity(ExecutionContext.handle_field_error)
49
+ override = inspect.signature(
50
+ ErrorProtectionExecutionContext.handle_field_error
51
+ )
52
+ # Either the arities match, or the override absorbs the difference.
53
+ absorbs = any(
54
+ p.kind is p.VAR_POSITIONAL for p in override.parameters.values()
55
+ )
56
+ assert absorbs or _accepted_arity(
57
+ ErrorProtectionExecutionContext.handle_field_error
58
+ ) == expected, (
59
+ f"graphql-core passes {expected} positional argument(s) to "
60
+ f"handle_field_error; the override does not accept them"
61
+ )
62
+
63
+ def test_survives_being_called_the_way_the_executor_calls_it(self) -> None:
64
+ """A direct call with graphql-core's own argument list.
65
+
66
+ This is the exact call that was raising TypeError in production.
67
+ """
68
+ context = ErrorProtectionExecutionContext.__new__(
69
+ ErrorProtectionExecutionContext
70
+ )
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 = []
80
+ try:
81
+ context.handle_field_error(*args)
82
+ except TypeError as exc: # pragma: no cover - this is the regression
83
+ raise AssertionError(
84
+ f"handle_field_error rejected graphql-core's own call: {exc}"
85
+ ) from exc
86
+ except Exception:
87
+ # Any GraphQL-level outcome is fine; a TypeError is not.
88
+ pass
89
+
90
+ def test_the_no_protection_variant_inherits_the_same_shape(self) -> None:
91
+ assert (
92
+ NoErrorProtectionExecutionContext.handle_field_error
93
+ is ErrorProtectionExecutionContext.handle_field_error
94
+ )
95
+
96
+
97
+ class TestResolverErrorsReachTheCaller:
98
+ def test_a_raising_resolver_produces_a_graphql_error_not_a_crash(self) -> None:
99
+ """End to end: the shape every consumer actually depends on.
100
+
101
+ A validating resolver — "status must be one of …" — has to come back as
102
+ a GraphQL error carrying its message, or callers cannot tell a bad
103
+ argument from a broken server.
104
+ """
105
+ api = GraphQLAPI()
106
+
107
+ @api.type(is_root_type=True)
108
+ class Root:
109
+ @api.field
110
+ def set_status(self, status: str) -> bool:
111
+ raise ValueError(f"status must be one of ['open', 'closed'], got {status!r}")
112
+
113
+ result = api.execute('query { setStatus(status: "banana") }')
114
+
115
+ assert result.errors, "the resolver's error never reached the caller"
116
+ assert "status must be one of" in str(result.errors[0])
@@ -601,23 +601,29 @@ class TestSchemaRebuilding:
601
601
  return []
602
602
 
603
603
  times = []
604
- for i in range(10):
604
+ for i in range(14):
605
605
  start_time = time.perf_counter()
606
606
  api = GraphQLAPI(root_type=API)
607
607
  schema, _ = api.build()
608
608
  end_time = time.perf_counter()
609
609
  times.append((end_time - start_time) * 1000)
610
610
 
611
- print("\n10 repeated schema builds:")
611
+ # Drop warm-up iterations (imports, first-touch caches), then compare
612
+ # the medians of the early and late halves. Comparing two single
613
+ # sub-millisecond samples is scheduler noise on shared CI runners and
614
+ # produced false failures.
615
+ warm = times[2:]
616
+ half = len(warm) // 2
617
+ early = sorted(warm[:half])[half // 2]
618
+ late = sorted(warm[half:])[half // 2]
619
+
620
+ print(f"\n{len(times)} repeated schema builds:")
612
621
  print(f" Times: {[f'{t:.2f}' for t in times]}")
613
- print(f" Average: {sum(times) / len(times):.2f}ms")
614
-
615
- # Later builds should not be significantly slower than first
616
- # Allow 50% variance
617
- first_build = times[0]
618
- last_build = times[-1]
619
- assert last_build < first_build * 1.5, \
620
- f"Performance degradation: first={first_build:.2f}ms, last={last_build:.2f}ms"
622
+ print(f" Early median: {early:.2f}ms, late median: {late:.2f}ms")
623
+
624
+ # Later builds should not degrade relative to earlier ones.
625
+ assert late < early * 2 + 1.0, \
626
+ f"Performance degradation: early median={early:.2f}ms, late median={late:.2f}ms"
621
627
 
622
628
 
623
629
  class TestDataclassNesting:
@@ -1098,3 +1098,15 @@ class TestGraphQLRemote:
1098
1098
  roll.fetch() # type: ignore[reportIncompatibleMethodOverride]
1099
1099
 
1100
1100
  assert roll.students() == ["alice", "bob"]
1101
+
1102
+ def test_remote_executor_construction(self) -> None:
1103
+ # graphql-core >= 3.2.7 routes the first constructor argument to
1104
+ # GraphQLNamedType.__new__ as the type name; the URL must not end up
1105
+ # there, and keyword-only construction must work. Fields are resolved
1106
+ # lazily, so no request is made here.
1107
+ by_keyword = GraphQLRemoteExecutor(url="http://localhost:1/graphql")
1108
+ positional = GraphQLRemoteExecutor("http://localhost:1/graphql", "Upstream")
1109
+
1110
+ assert by_keyword.name == "Remote"
1111
+ assert positional.name == "Upstream"
1112
+ assert positional.url == "http://localhost:1/graphql"