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.
- {graphql_api-1.6.9 → graphql_api-1.7.0}/.github/workflows/python-package.yml +1 -1
- {graphql_api-1.6.9 → graphql_api-1.7.0}/PKG-INFO +8 -3
- graphql_api-1.7.0/VERSION +1 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/executor.py +14 -2
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/remote.py +8 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api.egg-info/PKG-INFO +8 -3
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api.egg-info/SOURCES.txt +1 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api.egg-info/requires.txt +1 -1
- {graphql_api-1.6.9 → graphql_api-1.7.0}/pyproject.toml +7 -2
- graphql_api-1.7.0/tests/test_executor_signature.py +116 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_performance.py +16 -10
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_remote.py +12 -0
- graphql_api-1.7.0/uv.lock +1506 -0
- graphql_api-1.6.9/VERSION +0 -1
- graphql_api-1.6.9/uv.lock +0 -948
- {graphql_api-1.6.9 → graphql_api-1.7.0}/.github/workflows/pages.yml +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/.gitignore +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/.vscode/settings.json +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/AGENTS.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/CLAUDE.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/LICENSE +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/README.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/README.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/_index.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/_index.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/advanced-features/_index.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/advanced-features/directives.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/advanced-features/performance.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/advanced-features/schema-filtering.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/building-applications/_index.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/building-applications/documentation.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/building-applications/error-handling.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/distributed-systems/_index.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/distributed-systems/federation.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/distributed-systems/pagination-relay.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/distributed-systems/remote-graphql.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/_index.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/async-and-subscriptions.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/defining-schemas.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/enums-interfaces.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/field-types.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/getting-started.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/mutations.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/object-types.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/fundamentals/pydantic-and-dataclasses.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/production/_index.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/production/context-metadata.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/production/middleware.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/reference/_index.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/reference/api-reference.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/reference/contributing.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/content/docs/reference/examples.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/hugo.toml +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/docs/index.md +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/__init__.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/api.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/context.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/dataclass_mapping.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/decorators.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/directives.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/error.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/exception.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/federation/__init__.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/federation/directives.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/federation/federation.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/federation/types.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/mapper.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/middleware.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/mutation_schema.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/pydantic.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/pydantic_utils.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/reduce.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/relay.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/schema.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/schema_filter.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/type_utils.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/types.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api/utils.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api.egg-info/dependency_links.txt +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/graphql_api.egg-info/top_level.txt +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/setup.cfg +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/__init__.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_async.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_calculator.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_custom_suffixes.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_custom_types.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_dataclass.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_dataclass_relationships.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_decorator_patterns.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_directives.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_advanced.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_async_subscriptions.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_defining_schemas.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_field_types.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_getting_started.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docs_relay.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_docstrings.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_error.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_federation.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_federation_example.graphql +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_federation_example.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_filtering.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_graphql.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_middleware.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_plain_functions.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_pydantic.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_relay.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_schema.py +0 -0
- {graphql_api-1.6.9 → graphql_api-1.7.0}/tests/test_subscriptions.py +0 -0
- {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.
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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"
|
|
614
|
-
|
|
615
|
-
# Later builds should not
|
|
616
|
-
|
|
617
|
-
|
|
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"
|