API reference
The public API of pytest_graphql, generated from its docstrings and
annotations.
Schema-aware GraphQL test client with an optional pytest plugin.
pytest_graphql sends GraphQL operations to a server and reads back the result.
It loads the server's schema, chooses the fields to ask for, checks arguments
before anything is sent, and returns responses that read like Python objects.
Every name on this page can be imported from pytest_graphql itself. Importing
the package does not import pytest, so the client works in any Python program.
Under pytest, the gql fixture is a ready client.
Examples:
def test_user_has_a_name(gql):
user = gql.query("user", id="u1")
assert user.name == "Ada Lovelace"
Outside pytest, build the client yourself and close it when you are done:
from pytest_graphql import build_client
with build_client(url="http://localhost:8000/graphql") as gql:
user = gql.query("user", id="u1")
AUTO
module-attribute
AUTO: Final[_Auto] = _Auto()
Ask the client to choose the fields for this position.
AUTO is the default for fields= on query() and mutation(), so you
rarely write it. Use it to mix fields you wrote with fields that the client
chooses: AUTO can be the whole fields= value, or the sub-selection of one
field, as in Field("team", fields=AUTO) or {"team": AUTO}. It cannot be one
item in a list.
The client generates the selection from the schema and the selection settings of
ClientConfig. For example, max_depth bounds how deep the generated part
goes. Those limits apply to the generated part only, and never to fields you
wrote yourself.
Examples:
from pytest_graphql import AUTO
# The id and name of the user, and the fields the client chooses for the team.
response = gql.query(
"user",
id="u1",
fields=["id", "name", {"team": AUTO}],
raw=True,
)
assert "team {" in response.request.document
assert response.data.user.team.name == "Core"
CyclePolicy
module-attribute
CyclePolicy = Literal['stop', 'shallow', 'id_only']
What auto-selection does when a type appears again on its own path.
A cycle is a type that contains itself, directly or through other types, such
as User.manager, which is a User again. Auto-selection checks each path from
the top, so the same type in two sibling branches is not a cycle. The value is
one of three strings:
"stop": leave the field out."shallow": select the scalar fields of the repeated type and itsid, and nothing nested. This is the default."id_only": select only theid, or nothing when the type has none.
Examples:
from pytest_graphql import ClientConfig, SelectionPolicy
config = ClientConfig(cycle_policy="id_only")
assert config.selection_policy().cycle_policy == "id_only"
assert SelectionPolicy().cycle_policy == "shallow"
__version__
module-attribute
__version__ = '0.1.1'
The installed version of pytest-graphql, as a string.
Examples:
import pytest_graphql
assert isinstance(pytest_graphql.__version__, str)
Auth
Bases: Protocol
One request identity: something that signs each request before it is sent.
Any object with an apply method of this shape is an Auth. The client
calls apply once for every request, so a token that expires can be
refreshed inside apply and needs no other machinery. Use BearerAuth or
HeaderAuth for the common cases and write your own class for anything else.
Headers reach a request from several places. In order, lowest to highest:
ClientConfig.headers, the Auth object, GraphQLClient.with_headers(),
and a headers= option on a single call. A later source replaces an earlier
one for the same header name. Names compare without regard to case.
Examples:
A custom Auth that adds an API key header:
from dataclasses import replace
from pytest_graphql import Auth, RequestInfo
class ApiKeyAuth:
def __init__(self, key: str) -> None:
self.key = key
def apply(self, request: RequestInfo) -> RequestInfo:
headers = {**request.headers, "X-Api-Key": self.key}
return replace(request, headers=headers)
auth = ApiKeyAuth("key-123")
assert isinstance(auth, Auth)
ada = gql.with_auth(auth).query("user", id="u1")
assert ada.name == "Ada Lovelace"
apply
apply(request: RequestInfo) -> RequestInfo
Return the request to send, with this identity applied.
request is the live request and carries real header and variable
values, because signing needs them. It is immutable, so build a new one
with dataclasses.replace() and return that. Do not print or log the
request text yourself. Its repr() is already redacted, and
redacted() is the safe view if you need one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
RequestInfo
|
The request about to be sent. |
required |
Returns:
| Type | Description |
|---|---|
RequestInfo
|
The request to send in its place. |
Examples:
from pytest_graphql import BearerAuth, RequestInfo
request = RequestInfo(
operation=None,
kind="query",
document="{ __typename }",
variables={},
headers={},
url="http://localhost:8000/graphql",
)
signed = BearerAuth("tok").apply(request)
assert signed.headers["Authorization"] == "Bearer tok"
BearerAuth
BearerAuth(token: str)
Authenticate with an Authorization: Bearer <token> header.
repr() of this object never shows the token. In a request, the
Authorization header is shown as a placeholder in repr(), in a snapshot
and in a failure report, because that header name is in
ClientConfig.redact_headers by default.
The token is also removed from free text, such as a server's error message or
the query text, because it is the value of a header in redact_headers. That
removal has three conditions. authorization must stay in that list.
ClientConfig.redact_values must be on. The token must be at least
ClientConfig.min_redacted_value_length characters long, which is 8 by
default. A token that fails one of them can stay in such text. Give a test
account a token that is long enough.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The bearer token. Do not include the |
required |
Examples:
from pytest_graphql import BearerAuth
auth = BearerAuth("s3cret-token")
assert repr(auth) == "BearerAuth(token=<redacted>)"
ada = gql.with_auth(auth).query("user", id="u1")
assert ada.name == "Ada Lovelace"
# Free text: a token of 8 characters or more is removed, a shorter one is not.
from pytest_graphql import RequestInfo
request = RequestInfo(None, "query", "{ a }", {}, {}, "http://localhost/graphql")
assert "[redacted:" in BearerAuth("long-enough-token").apply(request).scrub(
"echo long-enough-token"
)
assert BearerAuth("short").apply(request).scrub("echo short") == "echo short"
# The header name must be in redact_headers, as `authorization` is by default.
from dataclasses import replace
unlisted = replace(request, redact_headers=frozenset())
token = BearerAuth("long-enough-token")
assert token.apply(unlisted).scrub("echo long-enough-token") == (
"echo long-enough-token"
)
token
instance-attribute
token = token
The token as given. Treat it as a secret.
Examples:
from pytest_graphql import BearerAuth
assert BearerAuth("s3cret-token").token == "s3cret-token"
apply
apply(request: RequestInfo) -> RequestInfo
Return request with Authorization: Bearer <token> set.
An Authorization header already on the request, in any capitalization,
is replaced.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
RequestInfo
|
The request about to be sent. |
required |
Returns:
| Type | Description |
|---|---|
RequestInfo
|
A copy of |
Examples:
from pytest_graphql import BearerAuth, RequestInfo
request = RequestInfo(
operation=None,
kind="query",
document="{ __typename }",
variables={},
headers={"authorization": "Bearer old"},
url="http://localhost:8000/graphql",
)
signed = BearerAuth("new").apply(request)
assert dict(signed.headers) == {"Authorization": "Bearer new"}
HeaderAuth
HeaderAuth(
headers: Mapping[str, str] | None = None,
/,
**named: str,
)
Authenticate with a fixed set of headers, such as an API key.
Header names that contain a hyphen cannot be written as keyword arguments, so the constructor also takes a mapping as its first positional argument. Both forms can be mixed in one call. A name given twice, in any capitalization, keeps the keyword value.
repr() of this object shows the header names and never the values.
In a request, a header value is shown as a placeholder only when its name is
in ClientConfig.redact_headers. The default list is authorization,
cookie, proxy-authorization and x-api-key. A header with another name
is shown with its value in repr() of the request, in a snapshot and in a
failure report, so add its name to that list.
The same list decides what is removed from free text, such as a server's
error message or the query text. The value of a header whose name is in
redact_headers is a known secret. The value of a header with another name
is not removed because of that header, however long it is. It is removed
only if the same text is also a known secret from another source, such as
the URL. For a listed name, the removal needs ClientConfig.redact_values
on and a value of at least ClientConfig.min_redacted_value_length
characters, which is 8 by default.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
headers
|
Mapping[str, str] | None
|
An optional mapping of header names to values. It is positional only. |
None
|
**named
|
str
|
More headers, written as keyword arguments. |
{}
|
Examples:
from pytest_graphql import HeaderAuth
auth = HeaderAuth({"X-Api-Key": "key-123"}, Authorization="Token abc")
assert repr(auth).endswith("values=<redacted>)")
ada = gql.with_auth(auth).query("user", id="u1")
assert ada.name == "Ada Lovelace"
# In a request, only a name in redact_headers is hidden.
from pytest_graphql import RequestInfo
request = RequestInfo(None, "query", "{ a }", {}, {}, "http://localhost/graphql")
shown = HeaderAuth({"X-Trace": "t-1"}).apply(request).redacted().headers
assert shown["X-Trace"] == "t-1"
hidden = HeaderAuth({"X-Api-Key": "k-1"}).apply(request).redacted().headers
assert hidden["X-Api-Key"] == "[redacted:X-Api-Key]"
# Free text: an unlisted name does not make its value a secret.
from dataclasses import replace
echo = "server echoed long-secret-value"
custom = HeaderAuth({"X-Custom-Token": "long-secret-value"})
assert custom.apply(request).scrub(echo) == echo
names = frozenset({"x-custom-token"})
listed = replace(request, redact_headers=names)
assert "long-secret-value" not in custom.apply(listed).scrub(echo)
headers
instance-attribute
headers = merge_headers(headers, named)
The headers this object sets, as a plain dict. The values are secrets.
Examples:
from pytest_graphql import HeaderAuth
auth = HeaderAuth({"X-Api-Key": "key-123"}, Accept="application/json")
expected = {"X-Api-Key": "key-123", "Accept": "application/json"}
assert auth.headers == expected
apply
apply(request: RequestInfo) -> RequestInfo
Return request with these headers set.
A header already on the request with the same name, in any capitalization, is replaced by this one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
RequestInfo
|
The request about to be sent. |
required |
Returns:
| Type | Description |
|---|---|
RequestInfo
|
A copy of |
Examples:
from pytest_graphql import HeaderAuth, RequestInfo
request = RequestInfo(
operation=None,
kind="query",
document="{ __typename }",
variables={},
headers={"x-api-key": "old"},
url="http://localhost:8000/graphql",
)
signed = HeaderAuth({"X-Api-Key": "new"}).apply(request)
assert dict(signed.headers) == {"X-Api-Key": "new"}
ClientConfig
dataclass
ClientConfig(
url: str | None = None,
headers: Mapping[str, str] = dict(),
schema_headers: Mapping[str, str] | None = None,
cookies: Mapping[str, str] = dict(),
cookie_scope: CookieScope = "none",
timeout: float | Timeout = DEFAULT_TIMEOUT_SECONDS,
retries: int = 2,
verify: bool | str | SSLContext = True,
trust_env: bool = False,
proxy: Proxy | str | None = None,
max_response_bytes: int = DEFAULT_MAX_RESPONSE_BYTES,
follow_redirects: bool = False,
http2: bool = False,
max_depth: int = 3,
cycle_policy: CyclePolicy = "shallow",
per_type_depth_cap: Mapping[str, int] = dict(),
include_deprecated: bool = False,
max_fields: int = 2000,
exclude: Sequence[str] = (),
relay_aware: bool = True,
validate: bool = True,
raise_on_error: bool = True,
raise_on_partial: bool = True,
seed: int = 0,
schema_cache_dir: str | None = None,
schema_cache_ttl: int = 0,
redact_headers: Sequence[str] = tuple(
sorted(DEFAULT_REDACT_HEADERS)
),
redact_variables: Sequence[
str
] = DEFAULT_REDACT_VARIABLES,
redact_values: bool = True,
min_redacted_value_length: int = DEFAULT_MIN_REDACTED_VALUE_LENGTH,
max_diagnostic_bytes: int = DEFAULT_MAX_DIAGNOSTIC_BYTES,
max_recorded_errors: int = DEFAULT_MAX_RECORDED_ERRORS,
max_recorded_calls: int = DEFAULT_MAX_RECORDED_CALLS,
)
The settings of a client: everything that is data and not an object.
A ClientConfig holds the endpoint, headers, timeouts, selection limits,
raising rules, seed and redaction settings. Objects that a client uses, such
as the transport, the schema, the scalar registry and the middleware, are
arguments of GraphQLClient and build_client(). The seed lives here and
not on the client.
It is a plain dataclass. Copy it with dataclasses.replace() to change a few
fields. build_client() also accepts any field as a keyword, so
build_client(url=..., max_depth=2) needs no ClientConfig of its own.
The fields that can hold a credential (headers, schema_headers,
cookies and proxy) are left out of repr(), so printing a config does not
show them.
Many of these settings can also be set for one call, with the keyword of the
same name on query() or mutation(). See GraphQLClient.query().
Examples:
from pytest_graphql import ClientConfig, build_client
config = ClientConfig(timeout=5.0, max_depth=2, headers={"X-Env": "test"})
client = build_client(
url="http://localhost:8000/graphql",
config=config,
transport=gql.transport,
schema=gql.schema,
)
assert client.config.max_depth == 2
assert client.config.url == "http://localhost:8000/graphql"
url
class-attribute
instance-attribute
url: str | None = None
The URL of the GraphQL endpoint. build_client(url=...) sets it.
Examples:
from pytest_graphql import ClientConfig
config = ClientConfig(url="http://localhost:8000/graphql")
assert config.url == "http://localhost:8000/graphql"
assert ClientConfig().url is None
headers
class-attribute
instance-attribute
headers: Mapping[str, str] = field(
default_factory=dict, repr=False
)
Headers sent with every request.
This includes the request that loads the schema, unless schema_headers is
set. Names compare without regard to case.
Examples:
from pytest_graphql import ClientConfig
config = ClientConfig(headers={"X-Env": "test"})
assert config.headers == {"X-Env": "test"}
assert "X-Env" not in repr(config)
schema_headers
class-attribute
instance-attribute
schema_headers: Mapping[str, str] | None = field(
default=None, repr=False
)
Headers for the request that loads the schema, and for nothing else.
Auth objects and per-call headers never reach it. None, the default, means
use headers.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().schema_headers is None
config = ClientConfig(schema_headers={"X-Schema": "1"})
assert config.schema_headers == {"X-Schema": "1"}
cookies
class-attribute
instance-attribute
cookies: Mapping[str, str] = field(
default_factory=dict, repr=False
)
Cookie values to treat as secrets.
In this version the client does not send these cookies. The values are only added to the set of secrets that are removed from error messages and reports.
Examples:
from pytest_graphql import ClientConfig
config = ClientConfig(cookies={"session": "abc123"})
assert config.cookies == {"session": "abc123"}
assert "abc123" not in repr(config)
cookie_scope
class-attribute
instance-attribute
cookie_scope: CookieScope = 'none'
Whether a client keeps the cookies that a server sets.
With "none", the default, every cookie is dropped after each response, so
no state carries over from one call to the next. With "client", a client
keeps its cookies and sends them back. The cookies belong to that client
only. A clone made with with_headers() or as_() starts with none.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().cookie_scope == "none"
assert ClientConfig(cookie_scope="client").cookie_scope == "client"
timeout
class-attribute
instance-attribute
timeout: float | Timeout = DEFAULT_TIMEOUT_SECONDS
The time limit of one call, in seconds. The default is 30.
A number applies to each of the four phases: connect, read, write and wait
for a connection from the pool. To set the phases apart, give an
httpx.Timeout. A phase set to None has no limit. A number must be more
than zero and at most 1,000,000, or math.inf for no limit. Anything else
raises an error before any request is sent.
Examples:
from pytest_graphql import ClientConfig
import httpx
assert ClientConfig().timeout == 30.0
assert ClientConfig(timeout=5).timeout == 5
phases = httpx.Timeout(connect=2, read=20, write=5, pool=1)
assert ClientConfig(timeout=phases).call_timeout() == 20
retries
class-attribute
instance-attribute
retries: int = 2
How many times a call is tried again after it fails to connect.
The default is 2, so a call is tried up to three times. Only a failure to
connect is retried, never a request that reached the server, and a mutation
is not retried unless the call has idempotent=True. The wait between tries
grows by steps and has a random part.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().retries == 2
assert ClientConfig(retries=0).retries == 0
verify
class-attribute
instance-attribute
verify: bool | str | SSLContext = True
How to check the server's TLS certificate.
True uses the standard certificate authorities. A string is the path of a
CA bundle file. An ssl.SSLContext is used as it is. False turns the check
off and issues a warning.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().verify is True
assert ClientConfig(verify="/etc/ssl/ca.pem").verify == "/etc/ssl/ca.pem"
trust_env
class-attribute
instance-attribute
trust_env: bool = False
Whether to read proxy settings from the environment.
Off by default, so a test run does not quietly go through a proxy that the
machine happens to have. When it is off, the variables HTTP_PROXY,
HTTPS_PROXY and NO_PROXY, the netrc file and SSLKEYLOGFILE are ignored.
A proxy that you set is used either way.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().trust_env is False
assert ClientConfig(trust_env=True).trust_env is True
proxy
class-attribute
instance-attribute
proxy: Proxy | str | None = field(default=None, repr=False)
A proxy for every request: a URL, an httpx.Proxy, or None.
An explicit proxy takes every request and outranks the environment. A URL that the transport cannot use raises an error that does not quote the URL, because a proxy password can be in it.
Examples:
from pytest_graphql import ClientConfig
config = ClientConfig(proxy="http://proxy.local:3128")
assert config.proxy == "http://proxy.local:3128"
assert "proxy.local" not in repr(config)
max_response_bytes
class-attribute
instance-attribute
max_response_bytes: int = DEFAULT_MAX_RESPONSE_BYTES
The largest response body that the client reads, in bytes. The default is 32 MiB.
The count is of bytes after decompression. A larger response raises an error, and the client does not read the rest.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().max_response_bytes == 32 * 1024 * 1024
config = ClientConfig(max_response_bytes=1_000_000)
assert config.max_response_bytes == 1_000_000
follow_redirects
class-attribute
instance-attribute
follow_redirects: bool = False
Not used in this version. The client never follows a redirect.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().follow_redirects is False
http2
class-attribute
instance-attribute
http2: bool = False
Whether to use HTTP/2. It needs the h2 package that httpx[http2] installs.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().http2 is False
assert ClientConfig(http2=True).http2 is True
max_depth
class-attribute
instance-attribute
max_depth: int = 3
How many levels of objects auto-selection selects. See SelectionPolicy.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().max_depth == 3
assert ClientConfig(max_depth=1).selection_policy().max_depth == 1
cycle_policy
class-attribute
instance-attribute
cycle_policy: CyclePolicy = 'shallow'
What auto-selection does when a type repeats on its path. See CyclePolicy.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().cycle_policy == "shallow"
assert ClientConfig(cycle_policy="stop").cycle_policy == "stop"
per_type_depth_cap
class-attribute
instance-attribute
per_type_depth_cap: Mapping[str, int] = field(
default_factory=dict
)
Stricter depth limits for named types. See SelectionPolicy.
Examples:
from pytest_graphql import ClientConfig
config = ClientConfig(per_type_depth_cap={"User": 1})
assert config.per_type_depth_cap == {"User": 1}
assert config.selection_policy().depth_cap_for("User") == 1
include_deprecated
class-attribute
instance-attribute
include_deprecated: bool = False
Whether auto-selection selects deprecated fields. Off by default.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().include_deprecated is False
assert ClientConfig(include_deprecated=True).include_deprecated is True
max_fields
class-attribute
instance-attribute
max_fields: int = 2000
The most fields that one generated query may select. See SelectionPolicy.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().max_fields == 2000
assert ClientConfig(max_fields=500).max_fields == 500
exclude
class-attribute
instance-attribute
exclude: Sequence[str] = ()
Patterns for fields that auto-selection never selects.
Each is "Type.field", "*.field" or "Type.*". Use it to leave out fields that
your test user may not read.
Examples:
from pytest_graphql import ClientConfig
config = ClientConfig(exclude=["User.balance", "*.preferences"])
assert config.exclude == ["User.balance", "*.preferences"]
policy = config.selection_policy()
assert not policy.should_include("User", "balance", ("balance",), 0)
relay_aware
class-attribute
instance-attribute
relay_aware: bool = True
Whether auto-selection recognizes Relay connections. See SelectionPolicy.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().relay_aware is True
assert ClientConfig(relay_aware=False).relay_aware is False
validate
class-attribute
instance-attribute
validate: bool = True
Whether to check a query against the schema before it is sent.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().validate is True
assert ClientConfig(validate=False).validate is False
raise_on_error
class-attribute
instance-attribute
raise_on_error: bool = True
Whether a response with errors raises GraphQLExecutionError.
With False, no error is raised, and that includes a partial-data error.
Read response.errors yourself, usually with raw=True.
Examples:
from pytest_graphql import ClientConfig, build_client
client = build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=gql.schema,
config=ClientConfig(raise_on_error=False),
)
response = client.mutation("updateUser", id="missing", fields=["id"], raw=True)
assert response.errors[0].path == ("updateUser",)
raise_on_partial
class-attribute
instance-attribute
raise_on_partial: bool = True
Whether a response with both data and errors raises GraphQLPartialDataError.
It applies only when raise_on_error is True.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().raise_on_partial is True
assert ClientConfig(raise_on_partial=False).raise_on_partial is False
seed
class-attribute
instance-attribute
seed: int = 0
The seed for gql.fake. The same seed gives the same data on every run.
Examples:
from pytest_graphql import build_client
def client_with(seed):
return build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=gql.schema,
seed=seed,
)
one, two = client_with(1), client_with(2)
assert one.fake.CreatePostInput() == one.fake.CreatePostInput()
assert one.fake.CreatePostInput() != two.fake.CreatePostInput()
schema_cache_dir
class-attribute
instance-attribute
schema_cache_dir: str | None = None
Not used in this version. The client does not cache the schema on disk.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().schema_cache_dir is None
schema_cache_ttl
class-attribute
instance-attribute
schema_cache_ttl: int = 0
Not used in this version. The client does not cache the schema on disk.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().schema_cache_ttl == 0
redact_headers
class-attribute
instance-attribute
redact_headers: Sequence[str] = tuple(
sorted(DEFAULT_REDACT_HEADERS)
)
The names of headers whose values are hidden in reports and error messages.
Names compare without regard to case. The default is authorization,
cookie, proxy-authorization and x-api-key. A list that you give here
replaces the default, so include these names when you add one.
Examples:
from pytest_graphql import ClientConfig
assert "authorization" in ClientConfig().redact_headers
config = ClientConfig(redact_headers=("authorization", "x-session"))
assert "x-session" in config.redact_headers
redact_variables
class-attribute
instance-attribute
redact_variables: Sequence[str] = DEFAULT_REDACT_VARIABLES
Patterns for variable names whose values are hidden.
A pattern is a name or a dotted path, and it can have * wildcards. It matches
at the end of a variable's path, at any depth, in any capitalization. The
default covers password, token, secret, api_key, access_token,
refresh_token, authorization, otp, pin, credit_card and ssn. The same
patterns hide values in the response data shown in a report.
Examples:
from pytest_graphql import ClientConfig
assert "password" in ClientConfig().redact_variables
config = ClientConfig(redact_variables=["password", "*.card_number"])
assert "*.card_number" in config.redact_variables
redact_values
class-attribute
instance-attribute
redact_values: bool = True
Whether to also remove known secret values from free text.
Free text is text like a server's error message or the query text. With
True, a known secret is removed from it if it is at least
min_redacted_value_length characters long. A server can echo such a value
back. See DiagnosticSnapshot for what a known secret is. In short, it is a
value of a header named in redact_headers, of a variable that matches
redact_variables, a cookie, or a part of the URL, so add the name of every
other header or variable that holds a secret. With False, no value is
removed from free text, and only headers and variables that redact_headers
and redact_variables name are replaced.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().redact_values is True
assert ClientConfig(redact_values=False).redact_values is False
min_redacted_value_length
class-attribute
instance-attribute
min_redacted_value_length: int = (
DEFAULT_MIN_REDACTED_VALUE_LENGTH
)
The shortest known secret that is removed from free text. The default is 8.
A very short value, such as 1 or on, would match ordinary words and
damage the text. So a known secret shorter than this stays in free text, for
example a 3-character token that appears in the query. Give test accounts
credentials that are at least this long. The limit applies only to known
secrets. See redact_values.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().min_redacted_value_length == 8
config = ClientConfig(min_redacted_value_length=12)
assert config.min_redacted_value_length == 12
max_diagnostic_bytes
class-attribute
instance-attribute
max_diagnostic_bytes: int = DEFAULT_MAX_DIAGNOSTIC_BYTES
The size limit of one field of a request shown in a report, in bytes.
The default is 4096. Each cut says how much it removed.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().max_diagnostic_bytes == 4096
assert ClientConfig(max_diagnostic_bytes=1024).max_diagnostic_bytes == 1024
max_recorded_errors
class-attribute
instance-attribute
max_recorded_errors: int = DEFAULT_MAX_RECORDED_ERRORS
How many of a response's errors a report or message lists. The default is 20.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().max_recorded_errors == 20
assert ClientConfig(max_recorded_errors=5).max_recorded_errors == 5
max_recorded_calls
class-attribute
instance-attribute
max_recorded_calls: int = DEFAULT_MAX_RECORDED_CALLS
How many recent calls the client remembers for a report. The default is 50.
Examples:
from pytest_graphql import ClientConfig
assert ClientConfig().max_recorded_calls == 50
assert ClientConfig(max_recorded_calls=10).max_recorded_calls == 10
call_timeout
call_timeout(override: float | None = None) -> float
Return the one time limit, in seconds, that a call hands to the transport.
The transport takes a single number for each call and applies it as a
ceiling on each of its four phases. A timeout setting that is one
number gives that number. A setting that has a limit for each phase gives
the largest of them, which cuts none of the phases short. If any phase
has no limit, the result is math.inf.
A per-call timeout is used as it is. It can tighten a phase, and it can
never make a phase longer than the configured limit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
override
|
float | None
|
A limit for this call alone, in seconds, or |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The limit in seconds. |
Raises:
| Type | Description |
|---|---|
ValueError
|
When a limit is zero, negative, NaN or above 1,000,000. |
TypeError
|
When a limit is not a number. |
Examples:
import httpx
from pytest_graphql import ClientConfig
assert ClientConfig(timeout=5).call_timeout() == 5
assert ClientConfig(timeout=5).call_timeout(2) == 2
phases = httpx.Timeout(connect=2, read=20, write=5, pool=1)
assert ClientConfig(timeout=phases).call_timeout() == 20
selection_policy
selection_policy() -> SelectionPolicy
Return the SelectionPolicy that these settings describe.
It is built from max_depth, cycle_policy, per_type_depth_cap,
include_deprecated, max_fields, exclude and relay_aware. The other
fields of a policy have their defaults. A keyword on a single call can
change some of the values for that call.
Returns:
| Type | Description |
|---|---|
SelectionPolicy
|
A new policy. |
Examples:
from pytest_graphql import ClientConfig
config = ClientConfig(max_depth=2, exclude=["User.balance"])
policy = config.selection_policy()
assert policy.max_depth == 2
assert policy.exclude == ("User.balance",)
GraphQLClient
GraphQLClient(
*,
transport: Transport,
schema: GraphQLSchema,
config: ClientConfig | None = None,
scalars: ScalarRegistry | None = None,
fake_context: FakeContext | None = None,
middleware: Sequence[Middleware] = (),
auth: Auth | None = None,
headers: Mapping[str, str] | None = None,
owns_transport: bool = False,
cleanup: list[Closable] | None = None,
recorder: DiagnosticsRecorder | None = None,
builder: SelectionBuilder | None = None,
)
A client that sends GraphQL operations to a server as one identity.
A client has a transport, a schema and a ClientConfig. Call query() or
mutation() with the name of a root field and its arguments as keywords. The
client checks the call against the schema, chooses the fields to ask for,
sends the request and returns the result as a Node, a NodeList or a
plain value.
Most code gets a client from the gql fixture or from build_client().
That function loads the schema and creates the transport. Construct a
GraphQLClient yourself when you already have both.
Identity changes make a clone. with_headers(), with_auth(), as_() and
anonymous() return a new client that shares the schema, config, scalar
registry and middleware, and has its own headers and auth. Over the default
HTTP transport a clone also gets its own connection and cookie state, shares
the connection pool, and must be closed.
The client is a context manager. Leaving the with block closes it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transport
|
Transport
|
What sends the requests. |
required |
schema
|
GraphQLSchema
|
The |
required |
config
|
ClientConfig | None
|
The settings. |
None
|
scalars
|
ScalarRegistry | None
|
The custom scalars this client knows. |
None
|
fake_context
|
FakeContext | None
|
Which test the data of |
None
|
middleware
|
Sequence[Middleware]
|
Hooks that run around every call. See |
()
|
auth
|
Auth | None
|
The identity applied to every request. See |
None
|
headers
|
Mapping[str, str] | None
|
Headers that the client adds on top of those of |
None
|
owns_transport
|
bool
|
Whether |
False
|
cleanup
|
list[Closable] | None
|
Internal. The list of what the client closes. |
None
|
recorder
|
DiagnosticsRecorder | None
|
Internal. The record of recent calls that clones share. Leave it out. |
None
|
builder
|
SelectionBuilder | None
|
Internal. The cache of generated selections that clones share. Leave it out. |
None
|
Examples:
from pytest_graphql import GraphQLClient, build_client
base = build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=gql.schema,
)
with GraphQLClient(transport=base.transport, schema=base.schema) as client:
user = client.query("user", id="u1")
assert user.name == "Ada Lovelace"
owns_transport
instance-attribute
owns_transport = owns_transport
Whether closing this client closes its transport.
Examples:
from pytest_graphql import build_client
assert gql.owns_transport is False
client = build_client(
url="http://localhost:8000/graphql", schema=gql.schema
)
assert client.owns_transport is True
client.close()
schema
property
schema: GraphQLSchema
The schema this client checks calls against, a graphql.GraphQLSchema.
Examples:
assert gql.schema.query_type.name == "Query"
assert "User" in gql.schema.type_map
expect
property
expect: ExpectNamespace
Build matchers for the types of the schema: gql.expect.Type(**fields).
gql.expect.User(name="Ada") returns a Matcher that checks only the
fields you name. The type name and the field names are checked against the
schema when you build the matcher, so a typo fails on that line and not
later. A field name works in its exact spelling and in snake_case. A type
with no fields, such as an enum, an input object or a scalar, raises an
error.
Examples:
user = gql.query("user", id="u1")
assert user == gql.expect.User(id="u1", name="Ada Lovelace")
assert user != gql.expect.User(name="Grace Hopper")
fake
property
fake: FakeNamespace
Build test input for the input types of the schema: gql.fake.Type().
gql.fake.CreatePostInput() returns a dict with a value for each field
of that input type. The values are seeded by ClientConfig.seed and by
the test, so the same test gets the same payload on every run. Give
keywords to override fields: gql.fake.CreatePostInput(title="Hello"). An
override key is the exact field name or its snake_case form, and the result
uses the exact names. An unknown key raises an error.
Rules for the values:
- A required field is always filled. An optional field is filled too,
unless you pass
_required_only=True. - Input objects nested up to
_depthlevels (default2) are filled in full. Deeper ones get their required fields only. - A list gets one to three elements.
- A custom scalar gets the value from the
fakefunction of itsScalarSpec. A scalar with no spec raisesScalarNotRegisteredErrorunless you give the field a value. - For a value that must differ on every call, use
unique().
Examples:
payload = gql.fake.CreatePostInput(title="Hello")
assert payload["title"] == "Hello"
assert set(payload) == {"title", "authorId"}
assert payload == gql.fake.CreatePostInput(title="Hello")
post = gql.mutation("createPost", input=payload, fields=["title"])
assert post.title == "Hello"
scalars
property
scalars: ScalarRegistry
The custom scalars that this client decodes, serializes and fakes.
The registry is shared with the clones of the client. A scalar that you register takes effect on the next call.
Examples:
from pytest_graphql import ScalarSpec
gql.scalars.register(ScalarSpec(name="Money", serialize=str, fake=str))
assert "Money" in gql.scalars
config
property
config: ClientConfig
The settings of this client.
Examples:
assert gql.config.max_depth == 3
assert gql.config.raise_on_error is True
transport
property
transport: Transport
The transport that this client sends its requests through.
Examples:
assert callable(gql.transport.send)
assert callable(gql.transport.close)
recorder
property
recorder: DiagnosticsRecorder
The record of this client's recent calls.
The pytest plugin reads it to add the calls of a failed test to the
report. Clones of a client share one recorder. It keeps at most
ClientConfig.max_recorded_calls calls, as redacted text, and never a
live request or response. Most code does not use it directly.
Examples:
gql.query("user", id="u1", fields=["id"])
assert len(gql.recorder.calls) >= 1
close
close() -> None
Release what this client owns.
A client from build_client() that created its own transport closes the
connection pool. A client over a transport that you gave it closes
nothing, because that transport is yours. A clone closes only the
transport that it derived for itself.
It is safe to call it more than once. If closing several things fails, the failures are reported together and none is lost.
Examples:
from pytest_graphql import build_client
client = build_client(
url="http://localhost:8000/graphql",
schema=gql.schema,
)
assert client.owns_transport
client.close()
client.close()
__enter__
__enter__() -> GraphQLClient
Use the client in a with block. It returns the client itself.
Examples:
from pytest_graphql import build_client
with build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=gql.schema,
) as client:
assert client.query("user", id="u1").name == "Ada Lovelace"
__exit__
__exit__(*exc_info: object) -> None
Close the client when the with block ends. It never hides an exception.
Examples:
from pytest_graphql import build_client
with build_client(
url="http://localhost:8000/graphql",
schema=gql.schema,
) as client:
assert client.owns_transport
with_headers
with_headers(
headers: Mapping[str, str] | None = None,
/,
**named: str,
) -> GraphQLClient
Return a clone that sends extra headers.
The new headers are added over the ones this client already sends. A
header with the same name, in any capitalization, replaces the old one.
The clone's headers rank above those of the config and of Auth, and
below a headers= option on a single call.
Header names that contain a hyphen cannot be keyword arguments, so the method also takes a mapping as its first positional argument. You can use both in one call.
A clone over the default HTTP transport has its own connection and
cookie state, and you must close it. Use it in a with block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
headers
|
Mapping[str, str] | None
|
An optional mapping of header names to values. It is positional only. |
None
|
**named
|
str
|
More headers, written as keyword arguments. |
{}
|
Returns:
| Type | Description |
|---|---|
GraphQLClient
|
A new client. |
Examples:
headers = {"X-Api-Key": "key-123"}
with gql.with_headers(headers, Authorization="Bearer abc") as client:
assert client.query("user", id="u1").name == "Ada Lovelace"
with_auth
with_auth(auth: Auth) -> GraphQLClient
Return a clone that uses another Auth object.
The clone's headers and the rest of its settings stay as they are. Only the auth changes. Over the default HTTP transport the clone has its own connection and cookie state, and you must close it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
auth
|
Auth
|
The identity for the clone. |
required |
Returns:
| Type | Description |
|---|---|
GraphQLClient
|
A new client. |
Examples:
from pytest_graphql import HeaderAuth
with gql.with_auth(HeaderAuth({"X-Api-Key": "key-123"})) as client:
assert client.query("user", id="u1").name == "Ada Lovelace"
as_
as_(auth: Auth | str) -> GraphQLClient
Return a clone that acts as another identity.
Pass an Auth object, or a plain string, which is taken as a bearer
token. gql.as_("tok") is the same as gql.with_auth(BearerAuth("tok")).
Use it to test the same call as different users.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
auth
|
Auth | str
|
An |
required |
Returns:
| Type | Description |
|---|---|
GraphQLClient
|
A new client. |
Examples:
with gql.as_("admin-token") as admin, gql.as_("guest-token") as guest:
assert admin.query("user", id="u1").name == "Ada Lovelace"
assert guest.query("user", id="u1").name == "Ada Lovelace"
anonymous
anonymous() -> GraphQLClient
Return a clone that sends no auth.
Use it to check that a call is refused without a login. The clone drops
the Auth object. Headers that were set with with_headers() stay.
Returns:
| Type | Description |
|---|---|
GraphQLClient
|
A new client. |
Examples:
with gql.as_("admin-token") as admin:
with admin.anonymous() as visitor:
assert visitor.query("users").pluck("id") == ["u1", "u2", "u3"]
query
query(name: str, /, **variables: Any) -> Any
Run a query by the name of a root field and return its result.
The client looks the field up in the schema, checks the arguments you
gave, chooses the fields to ask for (unless you list them), builds the
operation, sends it and returns the value of that one field. The result is
a Node for an object, a NodeList for a list of objects, and a plain
value for a scalar. Arguments are sent as variables, so a value never
changes the shape of the query text.
Give each argument of the field as a keyword. A keyword works in its exact
schema spelling and in snake_case. The keywords below are options, and not
arguments. An argument of the field that has one of these names can only be
given through variables=.
fields: what to select. A list or dict of names, aField, aSelection, a raw GraphQL string, orAUTO, the default.variables: a dict of variables by their exact names. Use it for an argument that has the name of an option. Giving one argument both ways is an error.raw: return the wholeGraphQLResponseand not the value of the field.validate: check the operation against the schema before it is sent.raise_on_error,raise_on_partial: seeClientConfig.max_depth,cycle_policy,per_type_depth_cap,include_deprecated,max_fields: the selection settings ofClientConfig, for this call.operation_name: the name written into the operation. The default is the field name.timeout: the time limit of this call, in seconds. It can shorten a configured limit and cannot make it longer.idempotent: allow a mutation to be tried again after a failure to connect. A query is always allowed.headers: extra headers for this call. They rank above every other source of headers.retries: reserved. It has no effect. SetClientConfig.retries.
When the operation has one argument of an input object type, you may give
the fields of that object as keywords, and they are wrapped into it. This
works only when no keyword is an argument of the operation, and at least
one is a field of the input. Writing input=payload always works.
Every variable is checked against its input type before it is sent, and a
value of a custom scalar goes through the serialize function of its
ScalarSpec. A wrong value raises an error that names the path and the
type and never repeats the value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of the root query field, such as |
required |
**variables
|
Any
|
The arguments of the field, and the options above. |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
The value of the field. With |
Raises:
| Type | Description |
|---|---|
OperationNotFoundError
|
When the schema has no query with that name. The message suggests the closest name. |
ArgumentError
|
When an argument is missing, unknown or has a wrong value. |
SelectionError
|
When |
GraphQLExecutionError
|
When the server returned errors and no data. |
GraphQLPartialDataError
|
When the server returned data and errors. |
ResponseShapeError
|
When the response does not fit the schema. |
Examples:
user = gql.query("user", id="u1")
assert user.name == "Ada Lovelace"
user = gql.query("user", id="u1", fields=["name", {"team": ["name"]}])
assert user.team.name == "Core"
response = gql.query("user", id="u1", fields=["name"], raw=True)
assert response.http.status_code == 200
assert response.data.user.name == "Ada Lovelace"
mutation
mutation(name: str, /, **variables: Any) -> Any
Run a mutation by the name of a root field and return its result.
It works as query() does, with the same arguments, options and
results, for a root field of the Mutation type. A mutation is not tried
again after a failure to connect unless you pass idempotent=True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of the root mutation field, such as |
required |
**variables
|
Any
|
The arguments of the field, and the options of |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
The value of the field. With |
Raises:
| Type | Description |
|---|---|
OperationNotFoundError
|
When the schema has no mutation with that name. |
ArgumentError
|
When an argument is missing, unknown or has a wrong value. |
GraphQLExecutionError
|
When the server returned errors and no data. |
GraphQLPartialDataError
|
When the server returned data and errors. |
Examples:
payload = gql.fake.CreatePostInput(title="Hello")
post = gql.mutation("createPost", input=payload, fields=["title"])
assert post.title == "Hello"
# The fields of the input object can be given as plain keywords.
post = gql.mutation("createPost", title="Hi", author_id="u1")
assert post.author.name == "Ada Lovelace"
execute
execute(
document: str,
variables: Mapping[str, Any] | None = None,
*,
operation_name: str | None = None,
**options: Any,
) -> GraphQLResponse[Any]
Send a GraphQL document that you wrote, and return the response.
Use it for a query that query() and mutation() cannot build, such as
one with directives or with several root fields. The document is checked
against the schema first, unless you turn that off. Values go in
variables, and each custom scalar value is serialized and checked as in
query().
It always returns the GraphQLResponse. A document may select any number
of root fields, so there is no single value to unwrap. Call
GraphQLResponse.unwrap() when it has exactly one. Errors are raised as
they are for query().
These options work here: validate, raise_on_error, raise_on_partial,
timeout, idempotent and headers. See query().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
document
|
str
|
The GraphQL text. It may have several operations. |
required |
variables
|
Mapping[str, Any] | None
|
The variables by name. Only variables that the operation declares are sent. |
None
|
operation_name
|
str | None
|
Which operation to run, when the document has more than one. It is an error to leave it out then. |
None
|
**options
|
Any
|
The options listed above. |
{}
|
Returns:
| Type | Description |
|---|---|
GraphQLResponse[Any]
|
The response. |
Raises:
| Type | Description |
|---|---|
GraphQLSyntaxError
|
When the text is not valid GraphQL. |
SelectionError
|
When the document does not pass validation against
the schema, or has several operations and no |
ArgumentError
|
When a variable has a wrong value. |
GraphQLExecutionError
|
When the server returned errors and no data. |
GraphQLPartialDataError
|
When the server returned data and errors. |
Examples:
response = gql.execute(
"query Whoami($id: ID!) { user(id: $id) { name } }",
{"id": "u1"},
)
assert response.data.user.name == "Ada Lovelace"
assert response.unwrap().name == "Ada Lovelace"
expect_error
expect_error(
*,
code: str | None = None,
path: Sequence[str | int] | None = None,
message_matches: str | Pattern[str] | None = None,
count: int | None = None,
) -> AbstractContextManager[CapturedErrors]
Check that the code in a with block fails with a GraphQL error.
The block must end with a GraphQLExecutionError. A
GraphQLPartialDataError counts, because it is one. Any other exception
passes through unchanged. If the block ends without one, or a filter
fails, it raises ExpectedErrorNotRaised. A client set to not raise, with
raise_on_error=False or raise_on_partial=False, never satisfies the
block.
Each filter you give must match at least one error of the response. The filters need not match the same error.
codeequalsextensions["code"]of the error.pathequals the whole path of the error, segment by segment. It is not a prefix.message_matchesis a regular expression search on the message.countis the exact number of errors that the server returned. It does not depend on the other filters.
A block sees the calls of every client in its context, so a call through
gql.as_(...) counts. The values are checked when you call
expect_error(), and a wrong one fails there.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
code
|
str | None
|
The error code to look for. |
None
|
path
|
Sequence[str | int] | None
|
The error path to look for, as names and list indexes. |
None
|
message_matches
|
str | Pattern[str] | None
|
A pattern, as text or as a compiled pattern. |
None
|
count
|
int | None
|
How many errors the server returned. An integer of at least 1. |
None
|
Returns:
| Type | Description |
|---|---|
AbstractContextManager[CapturedErrors]
|
A context manager. It gives a |
AbstractContextManager[CapturedErrors]
|
|
AbstractContextManager[CapturedErrors]
|
of them) and |
Raises:
| Type | Description |
|---|---|
ExpectedErrorNotRaised
|
When the block does not end in a matching error. It is raised on leaving the block. |
TypeError
|
When a filter has the wrong type. |
ValueError
|
When a filter has a wrong value, such as a |
Examples:
with gql.expect_error(path=["updateUser"], count=1) as caught:
gql.mutation("updateUser", id="missing", name="Ada", fields=["id"])
assert caught.first.path == ("updateUser",)
assert len(caught.errors) == 1
wait_until
wait_until(
name: str,
/,
*,
until: Callable[[Any], Any],
timeout: float = 30.0,
interval: float = 1.0,
backoff: float = 1.0,
ignore: type[Exception]
| tuple[type[Exception], ...] = (),
**variables: Any,
) -> Any
Run a query again and again until a condition holds, or time runs out.
Use it when a result becomes true later, for example after a background
job. At least one attempt always runs, even with timeout=0. After a failed
attempt the call sleeps interval * backoff ** (attempt - 1) seconds, and
never longer than the time that is left. The deadline is the only
limit. It is set once at the start.
The query takes the same arguments and options as query(). In this
method timeout is the deadline of the whole wait. The time limit of one
HTTP call is ClientConfig.timeout.
Only queries can be polled. A name that is a mutation raises an error before any call is made.
ignore covers one whole attempt: the call, the unwrapping of the
response, and until. It names exception classes that are swallowed and
counted as a failed attempt. Any other exception passes through unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of the root query field. |
required |
until
|
Callable[[Any], Any]
|
A function that gets what |
required |
timeout
|
float
|
The deadline of the whole wait, in seconds. A number of zero or more. Infinity is not accepted. |
30.0
|
interval
|
float
|
The first sleep between attempts, in seconds. A number of zero or more. |
1.0
|
backoff
|
float
|
The factor by which the sleep grows after each attempt. A
number of one or more. The default |
1.0
|
ignore
|
type[Exception] | tuple[type[Exception], ...]
|
An exception class, or a tuple of classes, to swallow. Each
must be a subclass of |
()
|
**variables
|
Any
|
The arguments of the field, and the options of |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
The value that |
Raises:
| Type | Description |
|---|---|
WaitTimeoutError
|
When the deadline passes before |
ArgumentError
|
When |
TypeError
|
When |
ValueError
|
When a number is out of range. |
Examples:
user = gql.wait_until(
"user",
id="u1",
until=lambda user: user.name == "Ada Lovelace",
timeout=5,
interval=0.1,
)
assert user.name == "Ada Lovelace"
DiagnosticSnapshot
dataclass
DiagnosticSnapshot(
operation: str | None,
kind: OperationKind,
document: str,
variables: Mapping[str, Any],
headers: Mapping[str, str],
method: str,
url: str,
idempotent: bool,
curl_headers: tuple[_HeaderEntry, ...] = (),
truncated: tuple[str, ...] = (),
omissions: tuple[OmissionRecord, ...] = (),
omissions_total: int = 0,
*,
_guard: _SecretGuard = _SecretGuard(),
)
A request in the form that is meant for error messages and reports.
You get a snapshot from RequestInfo.redacted(), and from
GraphQLResponse.request and the request of an exception. It is the only
form of a request that reaches an error message or a report. Every field has
been through these steps:
- Header values are replaced by markers when the header name is in
redact_headers, and variable values when their path matchesredact_variables. This depends on the name and is always done. - Free text, such as the query text, has the known secret values removed. A
known secret is one of these: the value of a header whose name is in
redact_headers, a variable value at a path that matchesredact_variables, a cookie value, the user name and password of the URL, every value in the query string of the URL, or a credential that the transport sends outside the headers, such as a proxy password. The value of a header with another name is not a known secret because of that header. It is removed only if the same text is also one of the values above, for example when it is also in the URL. A known secret is removed only whileredact_valuesis on, and only if it is at leastmin_redacted_value_lengthcharacters long. A shorter one, such as a 3-character token, stays in the query text. - Control characters are escaped, and each field is cut to a size limit.
So a snapshot is safe to show as far as those rules reach. Give test accounts credentials that are long enough, and list every header or variable that holds a secret.
truncated says which fields were cut and by how many bytes. It is empty when
nothing was cut, so a cut is never silent.
A snapshot never changes. variables and headers are read-only mappings.
Examples:
response = gql.query("user", id="u1", fields=["name"], raw=True)
snapshot = response.request
assert snapshot.kind == "query"
assert snapshot.operation == "user"
assert snapshot.method == "POST"
assert snapshot.variables == {"id": "u1"}
operation
instance-attribute
operation: str | None
The operation name, or None for an operation without a name.
Examples:
snapshot = gql.query("user", id="u1", fields=["name"], raw=True).request
assert snapshot.operation == "user"
kind
instance-attribute
kind: OperationKind
"query", "mutation" or "subscription".
Examples:
snapshot = gql.query("user", id="u1", fields=["name"], raw=True).request
assert snapshot.kind == "query"
document
instance-attribute
document: str
The GraphQL text that was sent.
Examples:
snapshot = gql.query("user", id="u1", fields=["name"], raw=True).request
assert snapshot.document.startswith("query user")
variables
instance-attribute
variables: Mapping[str, Any]
The variables that were sent.
A value whose variable path matches redact_variables is replaced by a marker.
Examples:
snapshot = gql.query("user", id="u1", fields=["name"], raw=True).request
assert snapshot.variables == {"id": "u1"}
headers
instance-attribute
headers: Mapping[str, str]
The headers that were sent.
A value whose header name is in redact_headers is replaced by a marker such
as [redacted:Authorization]. A header with another name keeps its value.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ me }", {}, {"Authorization": "Bearer abcdef123456"}, "http://localhost/graphql"
)
assert request.redacted().headers["Authorization"] == "[redacted:Authorization]"
method
instance-attribute
method: str
The HTTP method. It is "POST".
Examples:
snapshot = gql.query("user", id="u1", fields=["name"], raw=True).request
assert snapshot.method == "POST"
url
instance-attribute
url: str
The URL, without any user name, password or query string.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ me }", {}, {}, "http://ada:secret-pass@localhost/graphql?token=abc123"
)
assert "secret-pass" not in request.redacted().url
assert "abc123" not in request.redacted().url
idempotent
instance-attribute
idempotent: bool
Whether the call was marked as safe to try again after a connection failure.
Examples:
snapshot = gql.query("user", id="u1", fields=["name"], raw=True).request
assert snapshot.idempotent is False
curl_headers
class-attribute
instance-attribute
curl_headers: tuple[_HeaderEntry, ...] = ()
The headers in request order, in the form that as_curl() uses.
Examples:
snapshot = gql.query("user", id="u1", fields=["name"], raw=True).request
assert snapshot.curl_headers == ()
truncated
class-attribute
instance-attribute
truncated: tuple[str, ...] = ()
A note for each cut field, such as "variables: truncated 39 byte(s)".
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ a }", {"a": "x" * 50}, {}, "http://x/g",
max_diagnostic_bytes=20,
)
notes = request.redacted().truncated
assert any(note.startswith("variables: truncated") for note in notes)
omissions
class-attribute
instance-attribute
omissions: tuple[OmissionRecord, ...] = ()
The fields that auto-selection left out, and why. At most 50 are kept.
Each record has parent_type, path and reason. The reason is one of
"required-argument", "deprecated", "connection-page-size",
"connection-depth", "depth", "cycle", "should-include" and
"union-member-cap". A record never holds an argument value. The list is empty
when you wrote the fields yourself.
Examples:
snapshot = gql.query("user", id="u1", raw=True).request
record = snapshot.omissions[0]
assert record.reason == "cycle"
assert record.parent_type == "User"
assert record.field_path == "user.manager.manager"
omissions_total
class-attribute
instance-attribute
omissions_total: int = 0
How many fields auto-selection left out in all, including those not kept.
Examples:
snapshot = gql.query("user", id="u1", fields=["name"], raw=True).request
assert snapshot.omissions_total == 0
wide = gql.query("user", id="u1", raw=True).request
assert wide.omissions_total >= len(wide.omissions)
omissions_dropped
property
omissions_dropped: int
How many omission records were cut by the limit of 50.
Examples:
snapshot = gql.query("user", id="u1", raw=True).request
assert snapshot.omissions_dropped == (
snapshot.omissions_total - len(snapshot.omissions)
)
RequestInfo
dataclass
RequestInfo(
operation: str | None,
kind: OperationKind,
document: str,
variables: Mapping[str, Any],
headers: Mapping[str, str],
url: str,
method: str = "POST",
idempotent: bool = False,
redact_headers: frozenset[str] = DEFAULT_REDACT_HEADERS,
redact_variables: tuple[
str, ...
] = DEFAULT_REDACT_VARIABLES,
redact_values: bool = True,
min_redacted_value_length: int = DEFAULT_MIN_REDACTED_VALUE_LENGTH,
max_diagnostic_bytes: int = DEFAULT_MAX_DIAGNOSTIC_BYTES,
max_recorded_errors: int = DEFAULT_MAX_RECORDED_ERRORS,
omissions: tuple[OmissionRecord, ...] = (),
omissions_total: int = 0,
transport_credentials: tuple[tuple[str, str], ...] = (),
)
One outgoing GraphQL request, with its real header and variable values.
This is the object that Auth.apply, Middleware.before_request and
Transport.send receive. They need the real values, because they sign, change
and send the request. So the object holds them, and takes care that they do not
leak: repr() and str() show the redacted form, and redacted() and
as_curl() are the views meant for an error message or a report. Redaction
covers the headers and variables that the redaction settings name, and known
secrets in free text above a minimum length. See DiagnosticSnapshot.
A request never changes. headers and variables are read-only mappings. To
change a request, build a new one with dataclasses.replace().
You build a RequestInfo yourself only in tests of your own Auth,
Middleware or Transport. The client builds the real ones. The settings
that start with redact_ and max_ copy the fields of ClientConfig that
have the same names, with the same defaults.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
operation
|
str | None
|
The operation name, or |
required |
kind
|
OperationKind
|
|
required |
document
|
str
|
The GraphQL text. |
required |
variables
|
Mapping[str, Any]
|
The variables, by name. |
required |
headers
|
Mapping[str, str]
|
The headers. |
required |
url
|
str
|
The URL of the endpoint. |
required |
method
|
str
|
The HTTP method. |
'POST'
|
idempotent
|
bool
|
Whether the call may be tried again after a failure to connect. |
False
|
redact_headers
|
frozenset[str]
|
The names of headers whose values are secret. |
DEFAULT_REDACT_HEADERS
|
redact_variables
|
tuple[str, ...]
|
Patterns for variables whose values are secret. |
DEFAULT_REDACT_VARIABLES
|
redact_values
|
bool
|
Whether to remove known secrets from free text. See
|
True
|
min_redacted_value_length
|
int
|
The shortest known secret that is removed from free text. |
DEFAULT_MIN_REDACTED_VALUE_LENGTH
|
max_diagnostic_bytes
|
int
|
The size limit of one field of a snapshot. |
DEFAULT_MAX_DIAGNOSTIC_BYTES
|
max_recorded_errors
|
int
|
How many errors a report lists. |
DEFAULT_MAX_RECORDED_ERRORS
|
omissions
|
tuple[OmissionRecord, ...]
|
The fields that auto-selection left out. The client sets it. |
()
|
omissions_total
|
int
|
How many it left out in all. The client sets it. |
0
|
transport_credentials
|
tuple[tuple[str, str], ...]
|
Secrets that are not in |
()
|
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
operation="login",
kind="mutation",
document="mutation login($password: String!) { login }",
variables={"password": "hunter2-hunter2"},
headers={"Authorization": "Bearer abcdef123456"},
url="http://localhost:8000/graphql",
)
assert request.headers["Authorization"] == "Bearer abcdef123456"
assert "abcdef123456" not in repr(request)
assert "hunter2" not in repr(request)
operation
instance-attribute
operation: str | None
The operation name, or None for an operation without a name.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
"user", "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.operation == "user"
kind
instance-attribute
kind: OperationKind
"query", "mutation" or "subscription".
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.kind == "query"
document
instance-attribute
document: str
The GraphQL text.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.document == "{ user }"
variables
instance-attribute
variables: Mapping[str, Any]
The variables, by name, with their real values. A read-only mapping.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.variables == {"id": "u1"}
headers
instance-attribute
headers: Mapping[str, str]
The headers, with their real values. A read-only mapping.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.headers == {"X-Trace": "t1"}
url
instance-attribute
url: str
The URL of the endpoint.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.url == "http://localhost/graphql"
method
class-attribute
instance-attribute
method: str = 'POST'
The HTTP method. The default is "POST".
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.method == "POST"
idempotent
class-attribute
instance-attribute
idempotent: bool = False
Whether the call may be tried again after a failure to connect.
A query may always be tried again. A mutation may be only when this is True.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.idempotent is False
redact_headers
class-attribute
instance-attribute
redact_headers: frozenset[str] = DEFAULT_REDACT_HEADERS
The names of headers whose values are secret, in lower case.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert "authorization" in request.redact_headers
redact_variables
class-attribute
instance-attribute
redact_variables: tuple[str, ...] = DEFAULT_REDACT_VARIABLES
Patterns for variables whose values are secret.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert "password" in request.redact_variables
redact_values
class-attribute
instance-attribute
redact_values: bool = True
Whether known secret values are removed from free text.
See DiagnosticSnapshot for what a known secret is.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.redact_values is True
min_redacted_value_length
class-attribute
instance-attribute
min_redacted_value_length: int = (
DEFAULT_MIN_REDACTED_VALUE_LENGTH
)
The shortest known secret value that is removed from free text.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.min_redacted_value_length == 8
max_diagnostic_bytes
class-attribute
instance-attribute
max_diagnostic_bytes: int = DEFAULT_MAX_DIAGNOSTIC_BYTES
The size limit of one field of the snapshot, in bytes.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.max_diagnostic_bytes == 4096
max_recorded_errors
class-attribute
instance-attribute
max_recorded_errors: int = DEFAULT_MAX_RECORDED_ERRORS
How many of a response's errors a report lists.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.max_recorded_errors == 20
omissions
class-attribute
instance-attribute
omissions: tuple[OmissionRecord, ...] = ()
The fields that auto-selection left out, and why. At most 50 are kept.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.omissions == ()
omissions_total
class-attribute
instance-attribute
omissions_total: int = 0
How many fields auto-selection left out in all, including those not kept.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.omissions_total == 0
transport_credentials
class-attribute
instance-attribute
transport_credentials: tuple[tuple[str, str], ...] = ()
Secrets that are not in headers, as pairs of a label and a value.
Examples are a proxy password and the values of cookies. The client and the transport set this. They are never shown.
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
None, "query", "{ user }", {"id": "u1"}, {"X-Trace": "t1"}, "http://localhost/graphql"
)
assert request.transport_credentials == ()
scrub
scrub(text: str) -> str
Remove this request's secrets from a piece of free text.
Use it on text that you build yourself and want to show, such as a
server's error message or a body excerpt. A server can echo a secret it
was sent. Each known secret of this request is replaced by a marker such
as [redacted:Authorization]: the values of headers whose names are in
redact_headers, of variables that a redaction pattern matches, of
cookies, the user name and password of the URL, every value in its query
string, and the credentials that the transport sends outside the headers.
A value with a scheme, such as Bearer <token>, also has its part after
the scheme removed. The value of a header with another name is not
removed because of that header, however long it is. It is removed only
if the same text is also one of the values above.
Only a value of at least min_redacted_value_length characters is
removed, so a shorter secret stays in the text. This removes secrets only.
It does not escape control characters or cut the text. If redact_values
is False, it returns the text unchanged. The set of secrets is never
returned.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The text to clean. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The text with every known secret replaced. |
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
operation=None,
kind="query",
document="{ me }",
variables={},
headers={"Authorization": "Bearer abcdef123456"},
url="http://localhost:8000/graphql",
)
text = request.scrub("rejected token abcdef123456")
assert text == "rejected token [redacted:Authorization]"
# A value in the query string of the URL is a known secret too, and so
# is a transport credential. A header that is not listed is not.
from dataclasses import replace
url = "http://localhost:8000/graphql?debug=long-query-value"
other = replace(
request,
headers={"X-Trace": "long-trace-value"},
url=url,
transport_credentials=(("proxy", "long-proxy-password"),),
)
assert other.scrub("echo long-query-value") == "echo [redacted:url]"
assert other.scrub("echo long-proxy-password") == "echo [redacted:proxy]"
assert other.scrub("echo long-trace-value") == "echo long-trace-value"
redacted
redacted() -> DiagnosticSnapshot
Return the request as a snapshot for error messages and reports.
Header values and variable values are replaced when their names match the
redaction settings. Known secret values (see scrub()) are removed from
free text such as the query, while redact_values is on and for values of
at least min_redacted_value_length characters. Control characters are
escaped and every field is cut to max_diagnostic_bytes. A cut is stated in
truncated. This is the only form of a request that should reach an error
message, a log or a report. A secret shorter than the minimum, or one that
is not a known secret, can stay in the text. See
DiagnosticSnapshot.
Returns:
| Type | Description |
|---|---|
DiagnosticSnapshot
|
A |
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
operation="login",
kind="mutation",
document="mutation login($password: String!) { login }",
variables={"password": "hunter2-hunter2"},
headers={"Authorization": "Bearer abcdef123456", "X-Trace": "t1"},
url="http://localhost:8000/graphql",
)
snapshot = request.redacted()
assert snapshot.variables == {"password": "[redacted:password]"}
assert snapshot.headers["Authorization"] == "[redacted:Authorization]"
assert snapshot.headers["X-Trace"] == "t1"
as_curl
as_curl() -> str
Return a curl command that repeats this request, with secrets hidden.
The command is built from redacted(), so it follows the same rules. A
header whose name is in redact_headers is written as a shell variable,
such as ${PYTEST_GQL_HEADER_AUTHORIZATION}. Export that variable with the
real value, and the command runs as it is. Every other part is quoted for
the shell. A variable value that a redaction pattern matches shows a marker.
A secret that is not covered, for example one shorter than
min_redacted_value_length inside the query text, stays in the command.
Returns:
| Type | Description |
|---|---|
str
|
The command, as text. |
Raises:
| Type | Description |
|---|---|
DiagnosticRenderError
|
When a secret equals text that the command must contain, so that it cannot be replaced safely. |
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
operation=None,
kind="query",
document="{ me }",
variables={},
headers={"Authorization": "Bearer abcdef123456"},
url="http://localhost:8000/graphql",
)
command = request.as_curl()
assert command.startswith("curl -sS -X POST http://localhost:8000/graphql")
assert "${PYTEST_GQL_HEADER_AUTHORIZATION}" in command
assert "abcdef123456" not in command
ArgumentError
ArgumentError(
*,
kind: str,
operation_name: str,
bad_name: str,
signature: str,
candidates: Sequence[str],
input_type_name: str | None = None,
input_fields: Sequence[str] | None = None,
)
Bases: GraphQLClientError
An argument of an operation is unknown, missing, repeated or not valid.
The client raises this before it sends anything, for any of these reasons:
- The operation has no argument of that name, or an input object has no field of that name. The message shows the signature and suggests the closest name.
- A required argument was not given.
- One argument was given twice, for example as
user_idanduserId. - A value does not fit the declared type of its argument.
wait_until()was given a mutation. Only queries can be polled.- A call or
build_client()got a keyword that is neither an argument nor a configuration option.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
|
required |
operation_name
|
str
|
The name of the operation. |
required |
bad_name
|
str
|
The name that is not valid. |
required |
signature
|
str
|
The schema signature of the operation, such as
|
required |
candidates
|
Sequence[str]
|
The names that are valid in the place of |
required |
input_type_name
|
str | None
|
The input type, when |
None
|
input_fields
|
Sequence[str] | None
|
The fields of that input type. Leave it out otherwise. |
None
|
Examples:
from pytest_graphql import ArgumentError
try:
gql.query("user", idd="u1")
except ArgumentError as error:
assert error.bad_name == "idd"
assert error.signature == "user(id: ID!): User"
assert "Did you mean 'id'?" in str(error)
else:
raise AssertionError("expected an error")
kind
instance-attribute
kind = kind
The kind of the operation, "query" or "mutation".
Examples:
from pytest_graphql import ArgumentError
try:
gql.query("user", idd="u1")
except ArgumentError as error:
assert error.kind == "query"
else:
raise AssertionError("expected an error")
operation_name
instance-attribute
operation_name = operation_name
The name of the operation.
Examples:
from pytest_graphql import ArgumentError
try:
gql.query("user", idd="u1")
except ArgumentError as error:
assert error.operation_name == "user"
else:
raise AssertionError("expected an error")
bad_name
instance-attribute
bad_name = bad_name
The name that is not valid, or the name of the argument that has a problem.
Examples:
from pytest_graphql import ArgumentError
try:
gql.query("user", idd="u1")
except ArgumentError as error:
assert error.bad_name == "idd"
else:
raise AssertionError("expected an error")
signature
instance-attribute
signature = signature
The schema signature of the operation. It is empty when the error does not come from one argument name.
Examples:
from pytest_graphql import ArgumentError
try:
gql.query("user", idd="u1")
except ArgumentError as error:
assert error.signature == "user(id: ID!): User"
else:
raise AssertionError("expected an error")
candidates
instance-attribute
candidates = tuple(candidates)
The valid names that stand in the place of bad_name, as a tuple.
Examples:
from pytest_graphql import ArgumentError
try:
gql.query("user", idd="u1")
except ArgumentError as error:
assert error.candidates == ("id",)
else:
raise AssertionError("expected an error")
input_type_name
instance-attribute
input_type_name = input_type_name
The input type, when bad_name was meant as a field of an input object.
Otherwise None.
Examples:
from pytest_graphql import ArgumentError
error = ArgumentError(
kind="mutation",
operation_name="createPost",
bad_name="titel",
signature="createPost(input: CreatePostInput!): Post",
candidates=["title", "authorId"],
input_type_name="CreatePostInput",
input_fields=["title", "authorId"],
)
assert error.input_type_name == "CreatePostInput"
other = ArgumentError.not_a_query(kind="mutation", name="x")
assert other.input_type_name is None
input_fields
instance-attribute
input_fields = (
tuple(input_fields)
if input_fields is not None
else None
)
The fields of the input type, as a tuple. None when there is no input type.
Examples:
from pytest_graphql import ArgumentError
error = ArgumentError(
kind="mutation",
operation_name="createPost",
bad_name="titel",
signature="createPost(input: CreatePostInput!): Post",
candidates=["title", "authorId"],
input_type_name="CreatePostInput",
input_fields=["title", "authorId"],
)
assert error.input_fields == ("title", "authorId")
missing_argument
classmethod
missing_argument(
*,
kind: str,
operation_name: str,
arg_name: str,
signature: str,
) -> ArgumentError
Build the error for a required argument that the call did not give.
The library calls this when a required argument is missing. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
|
required |
operation_name
|
str
|
The name of the operation. |
required |
arg_name
|
str
|
The name of the missing argument. |
required |
signature
|
str
|
The schema signature of the operation. |
required |
Returns:
| Type | Description |
|---|---|
ArgumentError
|
The error, ready to raise. |
Examples:
from pytest_graphql import ArgumentError
error = ArgumentError.missing_argument(
kind="query",
operation_name="user",
arg_name="id",
signature="user(id: ID!): User",
)
assert "missing required argument 'id'" in str(error)
reserved_name_collision
classmethod
reserved_name_collision(
*,
kind: str,
operation_name: str,
arg_name: str,
signature: str,
) -> ArgumentError
Build the error for a required argument named like a call option.
Every keyword that a call accepts as an option, such as raw or
timeout, is always read as that option. A schema argument with the same
name cannot be given as a plain keyword. variables={...} is the one
way left. The library calls this when such an argument is required and
missing. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
|
required |
operation_name
|
str
|
The name of the operation. |
required |
arg_name
|
str
|
The name of the argument. |
required |
signature
|
str
|
The schema signature of the operation. |
required |
Returns:
| Type | Description |
|---|---|
ArgumentError
|
The error, ready to raise. |
Examples:
from pytest_graphql import ArgumentError
error = ArgumentError.reserved_name_collision(
kind="query",
operation_name="find",
arg_name="raw",
signature="find(raw: String!): Item",
)
assert "variables=" in str(error)
duplicate_argument
classmethod
duplicate_argument(
*,
kind: str,
operation_name: str,
resolved_name: str,
first_key: str,
second_key: str,
signature: str,
) -> ArgumentError
Build the error for one argument that the call gave twice.
The two keys can be the exact name and the snake_case name of one
argument, or a variables={...} entry next to a plain keyword. The
client refuses both, because it would have to drop one of the values
without telling you. The library calls this. You rarely need it
yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
|
required |
operation_name
|
str
|
The name of the operation. |
required |
resolved_name
|
str
|
The schema name of the argument. |
required |
first_key
|
str
|
The first key that named it. |
required |
second_key
|
str
|
The second key that named it. |
required |
signature
|
str
|
The schema signature of the operation. |
required |
Returns:
| Type | Description |
|---|---|
ArgumentError
|
The error, ready to raise. |
Examples:
from pytest_graphql import ArgumentError
error = ArgumentError.duplicate_argument(
kind="mutation",
operation_name="updateUser",
resolved_name="userId",
first_key="user_id",
second_key="userId",
signature="updateUser(userId: ID!): User",
)
assert "received argument 'userId' twice" in str(error)
invalid_value
classmethod
invalid_value(
*,
kind: str,
operation_name: str,
arg_name: str,
detail: str,
) -> ArgumentError
Build the error for a value that does not fit the type of its argument.
The library calls this when a value fails the type check. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
|
required |
operation_name
|
str
|
The name of the operation. |
required |
arg_name
|
str
|
The name of the argument. |
required |
detail
|
str
|
What was wrong with the value. |
required |
Returns:
| Type | Description |
|---|---|
ArgumentError
|
The error, ready to raise. |
Examples:
from pytest_graphql import ArgumentError
try:
gql.query("user", id=object())
except ArgumentError as error:
assert "invalid value for argument 'id'" in str(error)
else:
raise AssertionError("expected an error")
error = ArgumentError.invalid_value(
kind="query",
operation_name="user",
arg_name="id",
detail="$id is not a valid value of type 'ID!'.",
)
assert error.bad_name == "id"
not_a_query
classmethod
not_a_query(*, kind: str, name: str) -> ArgumentError
Build the error for wait_until() called on a mutation.
A poll repeats its call, so a mutation would repeat its side effect on every attempt. The library calls this. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
What the operation is, such as |
required |
name
|
str
|
The name of the operation. |
required |
Returns:
| Type | Description |
|---|---|
ArgumentError
|
The error, ready to raise. |
Examples:
from pytest_graphql import ArgumentError
try:
gql.wait_until("updateUser", until=bool)
except ArgumentError as error:
assert "polls queries only" in str(error)
else:
raise AssertionError("expected an error")
unknown_option
classmethod
unknown_option(
*,
callable_name: str,
bad_name: str,
candidates: Sequence[str],
) -> ArgumentError
Build the error for a keyword that is not a configuration option.
The client refuses an unknown keyword, because accepting it would leave the client running with a default that you believed you had replaced. The library calls this. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
callable_name
|
str
|
The name of the function that got the keyword. |
required |
bad_name
|
str
|
The keyword that is not known. |
required |
candidates
|
Sequence[str]
|
The options that the function accepts. The message lists them and suggests the closest one. |
required |
Returns:
| Type | Description |
|---|---|
ArgumentError
|
The error, ready to raise. |
Examples:
from pytest_graphql import ArgumentError, build_client
try:
build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=gql.schema,
timout=5,
)
except ArgumentError as error:
assert error.bad_name == "timout"
assert "Did you mean 'timeout'?" in str(error)
else:
raise AssertionError("expected an error")
DiagnosticRenderError
DiagnosticRenderError(renderer: str, message: str)
Bases: GraphQLClientError
A request cannot be shown as text without exposing a secret.
repr() and as_curl() of a RequestInfo replace secret values with
markers. They raise this error when a secret is equal to text that the
output must contain, such as a class name or a field name, so that no
replacement could remove the secret without breaking the output. The error
is raised instead of returning text that shows the secret.
This is rare, because a real secret is long and random. The fix is in the
message: use a different value for the colliding secret, or call
redacted() on the request and read the fields of the snapshot.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
renderer
|
str
|
The name of the output that was refused, such as |
required |
message
|
str
|
The error text. |
required |
Examples:
from pytest_graphql import DiagnosticRenderError, RequestInfo
request = RequestInfo(
operation=None,
kind="query",
document="{ ping }",
variables={},
headers={"Authorization": "RequestInfo"},
url="http://localhost:8000/graphql",
)
try:
repr(request)
except DiagnosticRenderError as error:
assert error.renderer == "repr()"
assert request.redacted().kind == "query"
else:
raise AssertionError("expected an error")
renderer
instance-attribute
renderer = renderer
The name of the output that was refused, such as "repr()".
Examples:
from pytest_graphql import DiagnosticRenderError
error = DiagnosticRenderError("as_curl()", "not safe to show")
assert error.renderer == "as_curl()"
default_message
staticmethod
default_message(renderer: str) -> str
Return the standard message for a refused output.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
renderer
|
str
|
The name of the output that was refused. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The message, which says what happened and what to do about it. |
Examples:
from pytest_graphql import DiagnosticRenderError
text = DiagnosticRenderError.default_message("repr()")
assert text.startswith("repr() could not produce a safe representation")
ExpectedErrorNotRaised
ExpectedErrorNotRaised(
message: str,
*,
response: GraphQLResponse[Any] | None,
errors: Sequence[GraphQLErrorInfo] = (),
unmatched: Sequence[str] = (),
calls: int = 0,
)
Bases: GraphQLTestError
An expect_error block did not end in the error that it expected.
GraphQLClient.expect_error() checks that the code in its with block
fails with a GraphQL error. This error says that the check failed, in one of
two ways:
- The block ended without a
GraphQLExecutionError.errorsandunmatchedare empty. A client that does not raise, because ofraise_on_error=Falseorraise_on_partial=False, never satisfies the block. - The block raised one, but a filter matched none of its errors.
unmatchednames the filters that failed, anderrorsholds every error the server returned.
The message lists the errors that the server returned, up to the limit set by
max_recorded_errors, and says how many it left out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
The error text. |
required |
response
|
GraphQLResponse[Any] | None
|
The last response that the block received, or the failed one.
|
required |
errors
|
Sequence[GraphQLErrorInfo]
|
Every error the server returned. Empty when no error was raised. |
()
|
unmatched
|
Sequence[str]
|
The names of the filters that matched no error. |
()
|
calls
|
int
|
How many responses the block received. |
0
|
Examples:
from pytest_graphql import ExpectedErrorNotRaised
try:
with gql.expect_error(code="FORBIDDEN"):
gql.query("user", id="u1")
except ExpectedErrorNotRaised as error:
assert error.calls == 1
assert error.errors == ()
else:
raise AssertionError("expected an error")
response
instance-attribute
response = response
The last response the block received, or None when it made no call.
Examples:
from pytest_graphql import ExpectedErrorNotRaised
try:
with gql.expect_error(code="FORBIDDEN"):
gql.mutation("updateUser", id="missing", fields=["id"])
except ExpectedErrorNotRaised as error:
assert error.response.data_state == "null"
else:
raise AssertionError("expected an error")
errors
instance-attribute
errors = tuple(errors)
Every error the server returned. Empty when the block did not fail.
Examples:
from pytest_graphql import ExpectedErrorNotRaised
try:
with gql.expect_error(code="FORBIDDEN"):
gql.mutation("updateUser", id="missing", fields=["id"])
except ExpectedErrorNotRaised as error:
assert len(error.errors) == 1
assert error.errors[0].path == ("updateUser",)
else:
raise AssertionError("expected an error")
unmatched
instance-attribute
unmatched = tuple(unmatched)
The names of the filters that matched no error, such as "code".
Examples:
from pytest_graphql import ExpectedErrorNotRaised
try:
with gql.expect_error(code="FORBIDDEN"):
gql.mutation("updateUser", id="missing", fields=["id"])
except ExpectedErrorNotRaised as error:
assert error.unmatched == ("code",)
else:
raise AssertionError("expected an error")
calls
instance-attribute
calls = calls
How many responses the block received.
Examples:
from pytest_graphql import ExpectedErrorNotRaised
try:
with gql.expect_error(code="FORBIDDEN"):
gql.mutation("updateUser", id="missing", fields=["id"])
except ExpectedErrorNotRaised as error:
assert error.calls == 1
else:
raise AssertionError("expected an error")
no_error
classmethod
no_error(
*, response: GraphQLResponse[Any] | None, calls: int
) -> ExpectedErrorNotRaised
Build the error for a block that ended without an execution error.
The library calls this from expect_error(). You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
response
|
GraphQLResponse[Any] | None
|
The last response the block received, or |
required |
calls
|
int
|
How many responses the block received. |
required |
Returns:
| Type | Description |
|---|---|
ExpectedErrorNotRaised
|
The error, ready to raise. |
Examples:
from pytest_graphql import ExpectedErrorNotRaised
error = ExpectedErrorNotRaised.no_error(response=None, calls=0)
assert error.calls == 0
assert "The block made no GraphQL call." in str(error)
unmatched_filters
classmethod
unmatched_filters(
*,
response: GraphQLResponse[Any],
filters: Sequence[tuple[str, str]],
calls: int,
) -> ExpectedErrorNotRaised
Build the error for a raised error that a filter did not match.
The library calls this from expect_error(). You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
response
|
GraphQLResponse[Any]
|
The response that carried the errors. |
required |
filters
|
Sequence[tuple[str, str]]
|
Each filter that failed, as its name and the text of the value you wrote. The text is checked against the request's secrets, like every other line of the message. |
required |
calls
|
int
|
How many responses the block received. |
required |
Returns:
| Type | Description |
|---|---|
ExpectedErrorNotRaised
|
The error, ready to raise. |
Examples:
from pytest_graphql import ExpectedErrorNotRaised
response = gql.mutation(
"updateUser",
id="missing",
fields=["id"],
raise_on_error=False,
raw=True,
)
error = ExpectedErrorNotRaised.unmatched_filters(
response=response, filters=[("code", "'FORBIDDEN'")], calls=1
)
assert error.unmatched == ("code",)
assert len(error.errors) == 1
GraphQLClientError
Bases: GraphQLTestError
The client could not finish a step that it does itself.
These steps are checking a call against the schema, loading the schema,
making a value for gql.fake and showing a request as text. The class
says what the client was doing. It does not say who caused the failure,
so read the subclass and the message. The message says what was wrong,
what was expected and what to do about it.
Most often the call and the schema disagree. A test that only wants a
result should let this error fail the test. Catch it to check that a
helper of your own rejects a bad call. A bad name, argument or selection
is refused before anything is sent: OperationNotFoundError,
ArgumentError, SelectionError and SelectionTooLargeError raise
before the client sends anything. SchemaError can follow a request,
because the introspection query that loads the schema goes to the server,
and a server that refuses it is the cause. DiagnosticRenderError comes
after a request was captured.
Examples:
from pytest_graphql import ArgumentError, GraphQLClientError
try:
gql.query("user", idd="u1")
except GraphQLClientError as error:
assert isinstance(error, ArgumentError)
else:
raise AssertionError("expected an error")
GraphQLConnectionError
GraphQLConnectionError(
message: str,
*,
request: DiagnosticSnapshot,
body_excerpt: str = "",
)
Bases: GraphQLTransportError
The client could not reach the server.
The connection failed, for example because the server is down, the host
name does not resolve, or a connection attempt timed out. A query is tried
again after a connection failure, up to ClientConfig.retries times, and
this error is raised when the last try fails. A mutation is not tried again,
unless the call has idempotent=True, because the server may have run it.
Catch it to test how your code behaves when the server is down. Check
request to see what was being sent.
The example uses a transport that raises this error, as the HTTP transport does when no server answers, so that it needs no network.
Examples:
from pytest_graphql import GraphQLConnectionError, build_client
class FailingTransport:
# Fails the way the HTTP transport does when no server answers.
def send(self, request, *, timeout):
raise GraphQLConnectionError(
"connection failed: connection refused",
request=request.redacted(),
)
def close(self):
pass
client = build_client(
url="http://localhost:8000/graphql",
transport=FailingTransport(),
schema=gql.schema,
)
try:
client.query("users")
except GraphQLConnectionError as error:
assert error.request.url == "http://localhost:8000/graphql"
else:
raise AssertionError("expected an error")
GraphQLExecutionError
GraphQLExecutionError(
summary: str,
*,
response: GraphQLResponse[Any] | None = None,
)
Bases: GraphQLTestError
The server answered with errors and no usable data.
query() and mutation() raise this when the response has an errors
list and no data, for example when a required field fails, so that data
is null. It is also raised for a server that breaks the protocol, by
sending neither errors nor data.
The message holds the response's repr(): the status, the number of errors
and the redacted request. It never holds a server value, so a secret that a
server echoes back does not enter your test report. Read the errors from
errors.
Set raise_on_error=False on the client or on a call, and use raw=True,
to get the response back instead of this error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
summary
|
str
|
The first line of the message. |
required |
response
|
GraphQLResponse[Any] | None
|
The response that failed. The library always passes it.
Without it, |
None
|
Examples:
from pytest_graphql import GraphQLExecutionError
try:
gql.mutation("updateUser", id="missing", name="Ada", fields=["id"])
except GraphQLExecutionError as error:
assert error.response.data_state == "null"
assert error.errors[0].path == ("updateUser",)
else:
raise AssertionError("expected an error")
response
instance-attribute
response = response
The response that failed, or None when the error was built without one.
Examples:
from pytest_graphql import GraphQLExecutionError
try:
gql.mutation("updateUser", id="missing", fields=["id"])
except GraphQLExecutionError as error:
assert error.response.data_state == "null"
assert error.response.errors == error.errors
else:
raise AssertionError("expected an error")
errors
property
errors: tuple[GraphQLErrorInfo, ...]
The errors the server returned, from response.
Each entry has message, path, locations, extensions and code.
The tuple is empty when the error has no response.
Examples:
from pytest_graphql import GraphQLExecutionError
try:
gql.mutation("updateUser", id="missing", name="Ada", fields=["id"])
except GraphQLExecutionError as error:
assert len(error.errors) == 1
assert error.errors[0] is error.response.errors[0]
GraphQLFieldError
GraphQLFieldError(message: str)
Bases: GraphQLTestError, AttributeError, KeyError
You read a field that the response does not have, or a name that is unclear.
A Node raises this when you read a field that is not in the response. The
message lists the fields that are there, suggests the closest name, and
reminds you that the field may be in the schema but missing from your
selection. It is also raised for a snake_case name that matches two
different fields. Use the exact field name then.
It is both an AttributeError and a KeyError. That keeps hasattr(),
getattr(node, "x", default) and node.get("x") working in the usual
way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
The error text. |
required |
Examples:
from pytest_graphql import GraphQLFieldError
user = gql.query("user", id="u1", fields=["id"])
try:
user.name
except GraphQLFieldError as error:
assert isinstance(error, AttributeError)
assert isinstance(error, KeyError)
else:
raise AssertionError("expected an error")
assert hasattr(user, "name") is False
unknown_field
classmethod
unknown_field(
type_name: str, bad_name: str, available: Sequence[str]
) -> GraphQLFieldError
Build the error for a field that is not in the response.
The library calls this when you read a missing field. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type_name
|
str
|
The name of the type that was read. |
required |
bad_name
|
str
|
The name that was read. |
required |
available
|
Sequence[str]
|
The field names the response has. The message lists them and suggests the closest one. |
required |
Returns:
| Type | Description |
|---|---|
GraphQLFieldError
|
The error, ready to raise. |
Examples:
from pytest_graphql import GraphQLFieldError
error = GraphQLFieldError.unknown_field("User", "nme", ["id", "name"])
assert "Did you mean 'name'?" in str(error)
ambiguous
classmethod
ambiguous(
type_name: str,
snake_key: str,
exact_names: Sequence[str],
) -> GraphQLFieldError
Build the error for a snake_case name that matches two fields.
The library calls this when you read such a name. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type_name
|
str
|
The name of the type that was read. |
required |
snake_key
|
str
|
The snake_case name that was read. |
required |
exact_names
|
Sequence[str]
|
Every exact field name that has this snake_case form. Give two or more. |
required |
Returns:
| Type | Description |
|---|---|
GraphQLFieldError
|
The error, ready to raise. |
Examples:
from pytest_graphql import GraphQLFieldError
error = GraphQLFieldError.ambiguous("User", "user_id", ["userId", "UserId"])
assert "ambiguous" in str(error)
GraphQLHTTPStatusError
GraphQLHTTPStatusError(
message: str,
*,
request: DiagnosticSnapshot,
status_code: int,
body_excerpt: str = "",
)
Bases: GraphQLTransportError
The server answered with an error status and no GraphQL result.
The status was not 2xx, for example 502 from a proxy or 401 from a gateway,
and the body was not a GraphQL response. status_code holds the status and
body_excerpt holds the start of the body, with secrets removed. A non-2xx
answer that is a valid GraphQL response is not this error. It becomes a
GraphQLRequestError or a GraphQLExecutionError, according to its body.
Catch it to test how your code behaves when something in front of the server fails.
The example uses a transport that raises this error, as the HTTP transport does for a 502 answer, so that it needs no network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
The error text. |
required |
request
|
DiagnosticSnapshot
|
The request that failed, as a |
required |
status_code
|
int
|
The HTTP status code of the answer. |
required |
body_excerpt
|
str
|
The start of the body, cut short and with secrets removed. |
''
|
Examples:
from pytest_graphql import GraphQLHTTPStatusError, build_client
class FailingTransport:
# Fails the way the HTTP transport does for a 502 answer.
def send(self, request, *, timeout):
raise GraphQLHTTPStatusError(
"request failed with status 502: Bad Gateway",
request=request.redacted(),
status_code=502,
body_excerpt="Bad Gateway",
)
def close(self):
pass
client = build_client(
url="http://localhost:8000/graphql",
transport=FailingTransport(),
schema=gql.schema,
)
try:
client.query("users")
except GraphQLHTTPStatusError as error:
assert error.status_code == 502
assert error.body_excerpt == "Bad Gateway"
else:
raise AssertionError("expected an error")
status_code
instance-attribute
status_code = status_code
The HTTP status code of the answer.
Examples:
from pytest_graphql import GraphQLHTTPStatusError
request = gql.query("users", raw=True).request
error = GraphQLHTTPStatusError(
"bad gateway", request=request, status_code=502
)
assert error.status_code == 502
GraphQLPartialDataError
GraphQLPartialDataError(
summary: str,
*,
response: GraphQLResponse[Any] | None = None,
)
Bases: GraphQLExecutionError
The server returned data and errors together.
GraphQL allows a field to fail while the rest of the operation succeeds. The
response then has both data and errors. By default the client raises this
error for it, so that an error is never ignored by accident. It is a
GraphQLExecutionError, so one except clause covers both.
The data is still available in response.data. To accept partial data,
set raise_on_partial=False on the client or on a call. To accept every
error, set raise_on_error=False.
Examples:
from dataclasses import replace
from pytest_graphql import GraphQLPartialDataError, build_client
class FailsOneField:
# Adds an error to every response of another transport.
def __init__(self, inner):
self.inner = inner
def send(self, request, *, timeout):
raw = self.inner.send(request, timeout=timeout)
error = {"message": "no access", "path": ["user", "balance"]}
return replace(raw, errors=(error,))
def close(self):
pass
client = build_client(
url="http://localhost:8000/graphql",
transport=FailsOneField(gql.transport),
schema=gql.schema,
)
try:
client.query("user", id="u1", fields=["id", "balance"])
except GraphQLPartialDataError as error:
assert error.response.has_data
assert error.response.data.user.id == "u1"
else:
raise AssertionError("expected an error")
GraphQLRequestError
GraphQLRequestError(
message: str,
*,
request: DiagnosticSnapshot,
status_code: int,
media_type: str,
errors: tuple[Mapping[str, Any], ...],
)
Bases: GraphQLTestError
The server refused the request before it ran it.
The server answered with a GraphQL response that has errors and no data
at all, whatever the status. This is what a server sends for a document it
cannot parse or validate, or for a request that it does not accept. The
operation never started, so this differs from GraphQLExecutionError,
where the operation ran and failed. A response with a data entry, even a
null one, is an execution result.
expect_error() does not catch this error, because it is not an execution
error. To test that a server refuses a request, catch this class.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
The error text. |
required |
request
|
DiagnosticSnapshot
|
The request that failed, as a |
required |
status_code
|
int
|
The HTTP status code of the answer. |
required |
media_type
|
str
|
The media type of the answer. |
required |
errors
|
tuple[Mapping[str, Any], ...]
|
The error objects of the answer, as mappings. Secrets are removed. |
required |
Examples:
The example uses a transport that raises this error, as the HTTP transport does for a rejected document, so that it needs no network.
from pytest_graphql import GraphQLRequestError, build_client
class RejectingTransport:
# Fails the way the HTTP transport does for a document the server rejects.
def send(self, request, *, timeout):
raise GraphQLRequestError(
"the server rejected the request before execution (status 400).",
request=request.redacted(),
status_code=400,
media_type="application/json",
errors=({"message": "Syntax Error: Unexpected Name 'usr'."},),
)
def close(self):
pass
client = build_client(
url="http://localhost:8000/graphql",
transport=RejectingTransport(),
schema=gql.schema,
)
try:
client.query("users")
except GraphQLRequestError as error:
assert error.status_code == 400
assert error.errors[0]["message"].startswith("Syntax Error")
else:
raise AssertionError("expected an error")
request
instance-attribute
request = request
The request that failed, with every secret removed.
Examples:
from pytest_graphql import GraphQLRequestError
error = GraphQLRequestError(
"rejected",
request=gql.query("users", raw=True).request,
status_code=400,
media_type="application/json",
errors=({"message": "Syntax Error"},),
)
assert error.request.kind == "query"
status_code
instance-attribute
status_code = status_code
The HTTP status code of the answer.
Examples:
from pytest_graphql import GraphQLRequestError
error = GraphQLRequestError(
"rejected",
request=gql.query("users", raw=True).request,
status_code=400,
media_type="application/json",
errors=({"message": "Syntax Error"},),
)
assert error.status_code == 400
media_type
instance-attribute
media_type = media_type
The media type of the answer, such as application/json.
Examples:
from pytest_graphql import GraphQLRequestError
error = GraphQLRequestError(
"rejected",
request=gql.query("users", raw=True).request,
status_code=400,
media_type="application/json",
errors=({"message": "Syntax Error"},),
)
assert error.media_type == "application/json"
errors
instance-attribute
errors = errors
The error objects of the answer, as a tuple of mappings. Secrets are removed.
Examples:
from pytest_graphql import GraphQLRequestError
error = GraphQLRequestError(
"rejected",
request=gql.query("users", raw=True).request,
status_code=400,
media_type="application/json",
errors=({"message": "Syntax Error"},),
)
assert error.errors[0]["message"] == "Syntax Error"
GraphQLTestError
Bases: Exception
The base of every exception that pytest_graphql raises on purpose.
Catch it to handle any failure of the library in one place. Every other exception of the library is a subclass of it.
The library still raises a plain TypeError or ValueError when a call is
wrong in an ordinary Python way, for example an expect_error() count
below 1. Those are not subclasses of this class.
It is a different class from graphql.GraphQLError of graphql-core, which
describes an error in a GraphQL document or result.
Examples:
from pytest_graphql import GraphQLClientError, GraphQLTestError
try:
gql.query("usr")
except GraphQLTestError as error:
assert isinstance(error, GraphQLClientError)
else:
raise AssertionError("expected an error")
GraphQLTimeoutError
GraphQLTimeoutError(
message: str,
*,
request: DiagnosticSnapshot,
body_excerpt: str = "",
)
Bases: GraphQLTransportError
The server did not answer in time.
The wait for the server, the write of the request or the read of the answer
took longer than ClientConfig.timeout. The client never tries again after
this error, because the server may already have started the work. A timeout
while connecting is a GraphQLConnectionError instead.
Catch it to test how your code behaves when the server is slow. The limit
for one call is set with timeout= on the call or in ClientConfig.
The example uses a transport that raises this error, as the HTTP transport does when a read times out, so that it needs no network.
Examples:
from pytest_graphql import GraphQLTimeoutError, build_client
class FailingTransport:
# Fails the way the HTTP transport does when a read times out.
def send(self, request, *, timeout):
raise GraphQLTimeoutError(
"request timed out: read timeout",
request=request.redacted(),
)
def close(self):
pass
client = build_client(
url="http://localhost:8000/graphql",
transport=FailingTransport(),
schema=gql.schema,
)
try:
client.query("users")
except GraphQLTimeoutError as error:
assert "timed out" in str(error)
else:
raise AssertionError("expected an error")
GraphQLTransportError
GraphQLTransportError(
message: str,
*,
request: DiagnosticSnapshot,
body_excerpt: str = "",
)
Bases: GraphQLTestError
The request did not give a usable GraphQL answer.
This is the base of the errors that come from the network layer. Catch it to handle every failure of the connection and of the HTTP answer in one place. A test that wants a result should let it fail, because the server is not reachable or is not answering as GraphQL.
The class is raised itself when:
- the status is 2xx but the body is not a GraphQL response;
- the body is larger than
max_response_bytes; - the response names a character set that Python does not know;
- the client cannot build the request, for example because the URL is not valid.
Its subclasses are GraphQLConnectionError, GraphQLTimeoutError and
GraphQLHTTPStatusError.
The error holds a copy of the request with every secret removed. It never holds the live request, so a credential cannot reach your test report through it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
The error text. |
required |
request
|
DiagnosticSnapshot
|
The request that failed, as a |
required |
body_excerpt
|
str
|
The start of the response body. It is cut to a short length and has secrets removed. It is empty when no body arrived. |
''
|
Examples:
from pytest_graphql import GraphQLTestError, GraphQLTransportError
request = gql.query("users", raw=True).request
error = GraphQLTransportError("not a GraphQL response", request=request)
assert isinstance(error, GraphQLTestError)
assert error.request.kind == "query"
request
instance-attribute
request = request
The request that failed, with every secret removed.
Examples:
from pytest_graphql import GraphQLTransportError
request = gql.query("users", raw=True).request
error = GraphQLTransportError("not a GraphQL response", request=request)
assert error.request is request
assert error.request.kind == "query"
body_excerpt
instance-attribute
body_excerpt = body_excerpt
The start of the response body, cut short and with secrets removed. It is an empty string when no body arrived.
Examples:
from pytest_graphql import GraphQLTransportError
request = gql.query("users", raw=True).request
error = GraphQLTransportError(
"not JSON", request=request, body_excerpt="<html>"
)
assert error.body_excerpt == "<html>"
assert GraphQLTransportError("no body", request=request).body_excerpt == ""
OperationNotFoundError
OperationNotFoundError(
*,
kind: str,
name: str,
available: Sequence[str],
total_count: int,
)
Bases: GraphQLClientError
The schema has no query or mutation with the name you used.
query(), mutation() and wait_until() raise this when the name matches
no operation of that kind. A name matches in its exact spelling and in
snake_case. The message suggests the closest names and says how many
operations of that kind the schema has.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
|
required |
name
|
str
|
The name that was used. |
required |
available
|
Sequence[str]
|
Every name of that kind in the schema. |
required |
total_count
|
int
|
How many names of that kind the schema has. |
required |
Examples:
from pytest_graphql import OperationNotFoundError
try:
gql.query("usr")
except OperationNotFoundError as error:
assert error.kind == "query"
assert error.name == "usr"
assert "user" in error.available
else:
raise AssertionError("expected an error")
kind
instance-attribute
kind = kind
The kind that was looked up, "query" or "mutation".
Examples:
from pytest_graphql import OperationNotFoundError
try:
gql.query("usr")
except OperationNotFoundError as error:
assert error.kind == "query"
else:
raise AssertionError("expected an error")
name
instance-attribute
name = name
The name that matched no operation.
Examples:
from pytest_graphql import OperationNotFoundError
try:
gql.query("usr")
except OperationNotFoundError as error:
assert error.name == "usr"
else:
raise AssertionError("expected an error")
available
instance-attribute
available = tuple(available)
Every name of that kind in the schema, as a tuple.
Examples:
from pytest_graphql import OperationNotFoundError
try:
gql.query("usr")
except OperationNotFoundError as error:
assert "user" in error.available
else:
raise AssertionError("expected an error")
total_count
instance-attribute
total_count = total_count
How many names of that kind the schema has.
Examples:
from pytest_graphql import OperationNotFoundError
try:
gql.query("usr")
except OperationNotFoundError as error:
assert error.total_count == len(error.available)
else:
raise AssertionError("expected an error")
ResponseShapeError
ResponseShapeError(
message: str, *, path: tuple[str | int, ...]
)
Bases: GraphQLTestError
The server sent a value that does not fit the type in the schema.
The client checks every response against the schema. This error means that
a value has the wrong shape, for example a number where the schema says
String, a list where it says an object, or a __typename that is not a
possible type. The server is broken or the schema is out of date.
The message names the place in the response and the types. It never repeats the server's value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
The error text. |
required |
path
|
tuple[str | int, ...]
|
Where in the response the value is: field names, with a list index as an integer. |
required |
Examples:
from dataclasses import replace
from pytest_graphql import ResponseShapeError, build_client
class NumberForName:
# Returns a number where the schema promises a string.
def __init__(self, inner):
self.inner = inner
def send(self, request, *, timeout):
raw = self.inner.send(request, timeout=timeout)
return replace(raw, data={"user": {"id": "u1", "name": 123}})
def close(self):
pass
client = build_client(
url="http://localhost:8000/graphql",
transport=NumberForName(gql.transport),
schema=gql.schema,
)
try:
client.query("user", id="u1", fields=["id", "name"])
except ResponseShapeError as error:
assert error.path == ("user", "name")
else:
raise AssertionError("expected an error")
path
instance-attribute
path = path
Where in the response the bad value is, as names and list indexes.
Examples:
from pytest_graphql import ResponseShapeError
error = ResponseShapeError("bad value", path=("user", "friends", 0, "name"))
assert error.path == ("user", "friends", 0, "name")
ScalarNotRegisteredError
ScalarNotRegisteredError(
scalar_name: str, *, path: Sequence[str | int] = ()
)
Bases: GraphQLClientError
gql.fake needed a value for a custom scalar that has no generator.
The factory fills a field of a custom scalar type by calling the fake
function of that scalar's ScalarSpec. This error means no spec is
registered for the scalar. The message gives the code that registers one,
and names the field to override instead. A field you give a value for in
the call, or one that the factory leaves out, never raises it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scalar_name
|
str
|
The name of the scalar. |
required |
path
|
Sequence[str | int]
|
Where the factory needed the value: the input type, then the field names, with a list index as an integer. Leave it out when the place is not known. |
()
|
Examples:
from graphql import build_schema
from pytest_graphql import ScalarNotRegisteredError, build_client
schema = build_schema(
"scalar Money type Query { ping: Boolean } input Pay { amount: Money! }"
)
client = build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=schema,
)
try:
client.fake.Pay()
except ScalarNotRegisteredError as error:
assert error.scalar_name == "Money"
assert error.location == "Pay.amount"
else:
raise AssertionError("expected an error")
scalar_name
instance-attribute
scalar_name = scalar_name
The name of the scalar that has no generator.
Examples:
from pytest_graphql import ScalarNotRegisteredError
error = ScalarNotRegisteredError("Money", path=("Pay", "lines", 0, "price"))
assert error.scalar_name == "Money"
path
instance-attribute
path = tuple(path)
Where the factory needed the value, as names and list indexes.
Examples:
from pytest_graphql import ScalarNotRegisteredError
error = ScalarNotRegisteredError("Money", path=("Pay", "lines", 0, "price"))
assert error.path == ("Pay", "lines", 0, "price")
location
instance-attribute
location = _render_path(self.path) if self.path else None
The path as text, such as Pay.amount or Pay.lines[0].price, or None.
Examples:
from pytest_graphql import ScalarNotRegisteredError
error = ScalarNotRegisteredError("Money", path=("Pay", "lines", 0, "price"))
assert error.location == "Pay.lines[0].price"
assert ScalarNotRegisteredError("Money").location is None
SchemaError
Bases: GraphQLClientError
A type that you named is not in the schema, or the schema cannot be used.
The client raises this when:
gql.fakeorgql.expectis asked for a type that the schema does not have, or that is not the right kind of type for it;- a selection names a type in a fragment that is not in the schema, or is not a possible type of the field;
- the schema cannot be loaded, because the introspection query failed, a schema file cannot be read, or the schema is not valid.
Examples:
from pytest_graphql import SchemaError
try:
gql.fake.Nope()
except SchemaError as error:
assert "no type named 'Nope'" in str(error)
else:
raise AssertionError("expected an error")
unknown_type
classmethod
unknown_type(
name: str, available: Sequence[str]
) -> SchemaError
Build the error for a type that the schema does not have.
The library calls this. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The type name that was used. |
required |
available
|
Sequence[str]
|
The type names of the schema. The message suggests the closest one. |
required |
Returns:
| Type | Description |
|---|---|
SchemaError
|
The error, ready to raise. |
Examples:
from pytest_graphql import SchemaError
error = SchemaError.unknown_type("Usr", ["User", "Team"])
assert "Did you mean 'User'?" in str(error)
not_a_possible_type
classmethod
not_a_possible_type(
name: str, parent_name: str
) -> SchemaError
Build the error for a fragment type that the field can never return.
The library calls this. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The type of the fragment. |
required |
parent_name
|
str
|
The type that the fragment is inside. |
required |
Returns:
| Type | Description |
|---|---|
SchemaError
|
The error, ready to raise. |
Examples:
from pytest_graphql import SchemaError
error = SchemaError.not_a_possible_type("Team", "User")
assert "not a possible type of 'User'" in str(error)
SelectionError
Bases: GraphQLClientError
A selection is not valid, or the finished document fails the schema check.
The client raises this before it sends anything, in these cases:
fieldsnames a field that the type does not have. The message lists the valid names and suggests the closest one.fieldsasks for sub-fields of a scalar or an enum.- A snake_case field name matches two fields of the schema.
- Auto-selection leaves no field to ask for.
- A document that you give to
execute()fails validation against the schema. The message is then the message of graphql-core.
Examples:
from pytest_graphql import SelectionError
try:
gql.query("user", id="u1", fields=["nme"])
except SelectionError as error:
assert "no field 'nme' on User" in str(error)
assert "Did you mean 'name'?" in str(error)
else:
raise AssertionError("expected an error")
unknown_field
classmethod
unknown_field(
type_name: str, bad_name: str, available: Sequence[str]
) -> SelectionError
Build the error for a field that the type does not have.
The library calls this when fields names such a field. You rarely need
it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type_name
|
str
|
The name of the type. |
required |
bad_name
|
str
|
The field name that was written. |
required |
available
|
Sequence[str]
|
The field names of the type. The message lists them and suggests the closest one. |
required |
Returns:
| Type | Description |
|---|---|
SelectionError
|
The error, ready to raise. |
Examples:
from pytest_graphql import SelectionError
error = SelectionError.unknown_field("User", "nme", ["id", "name"])
assert "Did you mean 'name'?" in str(error)
ambiguous_field
classmethod
ambiguous_field(
type_name: str, bad_name: str, spellings: Sequence[str]
) -> SelectionError
Build the error for a snake_case name that two or more fields share.
The library calls this when fields uses such a name. You rarely need
it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type_name
|
str
|
The name of the type. |
required |
bad_name
|
str
|
The snake_case name that was written. |
required |
spellings
|
Sequence[str]
|
The exact field names that have this snake_case form. |
required |
Returns:
| Type | Description |
|---|---|
SelectionError
|
The error, ready to raise. |
Examples:
from pytest_graphql import SelectionError
error = SelectionError.ambiguous_field(
"User", "user_id", ["userId", "UserId"]
)
assert "ambiguous" in str(error)
from_validation
classmethod
from_validation(message: str) -> SelectionError
Build the error for a document that fails validation.
The text of graphql-core is kept as it is. The library calls this. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
The validation message. |
required |
Returns:
| Type | Description |
|---|---|
SelectionError
|
The error, ready to raise. |
Examples:
from pytest_graphql import SelectionError
try:
gql.execute("{ nope }")
except SelectionError as error:
assert "Cannot query field 'nope'" in str(error)
else:
raise AssertionError("expected an error")
error = SelectionError.from_validation("Cannot query field 'nope'.")
assert str(error) == "Cannot query field 'nope'."
on_leaf_type
classmethod
on_leaf_type(
*, field_name: str, type_name: str
) -> SelectionError
Build the error for sub-fields that were asked for on a scalar or an enum.
The library calls this when fields writes sub-fields under such a
field. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
field_name
|
str
|
The name of the field. |
required |
type_name
|
str
|
The name of its scalar or enum type. |
required |
Returns:
| Type | Description |
|---|---|
SelectionError
|
The error, ready to raise. |
Examples:
from pytest_graphql import SelectionError
error = SelectionError.on_leaf_type(field_name="name", type_name="String")
assert "has no fields to select" in str(error)
no_selectable_fields
classmethod
no_selectable_fields(type_name: str) -> SelectionError
Build the error for an auto-selection that left no field to ask for.
An empty selection is not a valid document. The message lists the settings that widen the selection. The library calls this. You rarely need it yourself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type_name
|
str
|
The name of the type. |
required |
Returns:
| Type | Description |
|---|---|
SelectionError
|
The error, ready to raise. |
Examples:
from pytest_graphql import SelectionError
error = SelectionError.no_selectable_fields("User")
assert "produced no fields" in str(error)
SelectionTooLargeError
SelectionTooLargeError(
*, type_name: str, field_count: int, limit: int
)
Bases: GraphQLClientError
Auto-selection would ask for more fields than max_fields allows.
The client builds the selection of a call by walking the schema from the
return type of the operation. When the walk finds more fields than
ClientConfig.max_fields, which is 2000 by default, it stops and raises
this error instead of sending a very large document. The message lists the
ways to make the selection smaller.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type_name
|
str
|
The name of the type that was auto-selected. |
required |
field_count
|
int
|
How many fields the selection would have had. |
required |
limit
|
int
|
The limit that applied. |
required |
Examples:
from pytest_graphql import SelectionTooLargeError
try:
gql.query("user", id="u1", max_fields=3)
except SelectionTooLargeError as error:
assert error.type_name == "User"
assert error.field_count > error.limit == 3
else:
raise AssertionError("expected an error")
type_name
instance-attribute
type_name = type_name
The name of the type that was auto-selected.
Examples:
from pytest_graphql import SelectionTooLargeError
try:
gql.query("user", id="u1", max_fields=3)
except SelectionTooLargeError as error:
assert error.type_name == "User"
else:
raise AssertionError("expected an error")
field_count
instance-attribute
field_count = field_count
How many fields the selection would have had.
Examples:
from pytest_graphql import SelectionTooLargeError
try:
gql.query("user", id="u1", max_fields=3)
except SelectionTooLargeError as error:
assert error.field_count > error.limit
else:
raise AssertionError("expected an error")
limit
instance-attribute
limit = limit
The limit that applied, from max_fields.
Examples:
from pytest_graphql import SelectionTooLargeError
try:
gql.query("user", id="u1", max_fields=3)
except SelectionTooLargeError as error:
assert error.limit == 3
else:
raise AssertionError("expected an error")
WaitTimeoutError
WaitTimeoutError(
*,
operation: str,
attempts: int,
elapsed: float,
timeout: float,
last_response: GraphQLResponse[Any] | None,
last_exception: Exception | None,
last_exception_request: DiagnosticSnapshot
| None = None,
)
Bases: GraphQLTestError
A wait_until poll reached its deadline before until held.
GraphQLClient.wait_until() repeats a query until a condition holds. This
error says that time ran out. Its attributes tell you what the last attempts
saw: how many calls were made, how long the poll ran, the last response, and
the last exception that ignore swallowed.
The message shows the text of the last swallowed exception only when it can
check that text against the request of the attempt that raised it. Without
that request, the text is withheld to protect secrets. The exception is
still available as last_exception.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
operation
|
str
|
The name of the query that was polled. |
required |
attempts
|
int
|
How many calls were made. |
required |
elapsed
|
float
|
The seconds from the first attempt until the poll stopped. |
required |
timeout
|
float
|
The limit that the poll was given, in seconds. |
required |
last_response
|
GraphQLResponse[Any] | None
|
The most recent response that any attempt received, or
|
required |
last_exception
|
Exception | None
|
The most recent exception that |
required |
last_exception_request
|
DiagnosticSnapshot | None
|
The redacted request of the attempt that raised
|
None
|
Examples:
from pytest_graphql import WaitTimeoutError
try:
gql.wait_until(
"user",
id="u1",
until=lambda user: user.name == "Grace Hopper",
timeout=0.05,
interval=0.01,
)
except WaitTimeoutError as error:
assert error.operation == "user"
assert error.attempts >= 1
assert error.last_response.data.user.name == "Ada Lovelace"
else:
raise AssertionError("expected an error")
operation
instance-attribute
operation = operation
The name of the query that was polled.
Examples:
from pytest_graphql import WaitTimeoutError
error = WaitTimeoutError(
operation="user",
attempts=3,
elapsed=0.52,
timeout=0.5,
last_response=gql.execute("{ __typename }"),
last_exception=None,
)
assert error.operation == "user"
attempts
instance-attribute
attempts = attempts
How many calls were made. At least one call always runs.
Examples:
from pytest_graphql import WaitTimeoutError
error = WaitTimeoutError(
operation="user",
attempts=3,
elapsed=0.52,
timeout=0.5,
last_response=gql.execute("{ __typename }"),
last_exception=None,
)
assert error.attempts == 3
elapsed
instance-attribute
elapsed = elapsed
The seconds from the first attempt until the poll stopped.
Examples:
from pytest_graphql import WaitTimeoutError
error = WaitTimeoutError(
operation="user",
attempts=3,
elapsed=0.52,
timeout=0.5,
last_response=gql.execute("{ __typename }"),
last_exception=None,
)
assert error.elapsed == 0.52
timeout
instance-attribute
timeout = timeout
The limit the poll was given, in seconds.
Examples:
from pytest_graphql import WaitTimeoutError
error = WaitTimeoutError(
operation="user",
attempts=3,
elapsed=0.52,
timeout=0.5,
last_response=gql.execute("{ __typename }"),
last_exception=None,
)
assert error.timeout == 0.5
last_response
instance-attribute
last_response = last_response
The most recent response any attempt received, or None.
It includes the response that a swallowed execution error carried.
Examples:
from pytest_graphql import WaitTimeoutError
error = WaitTimeoutError(
operation="user",
attempts=3,
elapsed=0.52,
timeout=0.5,
last_response=gql.execute("{ __typename }"),
last_exception=None,
)
assert error.last_response.has_data
last_exception
instance-attribute
last_exception = last_exception
The most recent exception that ignore swallowed, or None.
Examples:
from pytest_graphql import WaitTimeoutError
error = WaitTimeoutError(
operation="user",
attempts=3,
elapsed=0.52,
timeout=0.5,
last_response=gql.execute("{ __typename }"),
last_exception=None,
)
assert error.last_exception is None
last_exception_request
instance-attribute
last_exception_request = last_exception_request
The redacted request of the attempt that raised last_exception.
It is None when that attempt failed before a request existed.
Examples:
from pytest_graphql import WaitTimeoutError
error = WaitTimeoutError(
operation="user",
attempts=3,
elapsed=0.52,
timeout=0.5,
last_response=gql.execute("{ __typename }"),
last_exception=None,
)
assert error.last_exception_request is None
safe_cause
safe_cause() -> Exception | None
Return last_exception when it is safe to chain as the cause.
Chaining prints the exception, and its notes and causes, in the traceback. So the library chains it only when it is one of this package's own errors, its attempt's request is known, and nothing linked to it shows a secret of any request that this error knows. A foreign exception is named once, in the message, and is never chained.
Returns:
| Type | Description |
|---|---|
Exception | None
|
The exception, or |
Examples:
from pytest_graphql import WaitTimeoutError
try:
gql.wait_until(
"user",
id="u1",
until=lambda user: False,
timeout=0.02,
interval=0.01,
)
except WaitTimeoutError as error:
assert error.last_exception is None
assert error.safe_cause() is None
DeterministicRandom
DeterministicRandom(seed: int, *path: str | int)
A source of random values that is the same every time for the same inputs.
A ScalarSpec.fake function receives one of these. The same seed and path
give the same values on every machine and every supported Python version, so
a test that uses seeded data repeats exactly. This is why the package does
not use random.Random for any value that reaches output: its stream is
only partly stable between Python versions.
Create one only when you test your own fake functions. Reproducible values
and unique values exclude each other. For a value that must differ on every
run, use unique().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seed
|
int
|
An integer from |
required |
*path
|
str | int
|
Names for the value the stream is for, such as a type name and a field name. Each path gets its own stream, so adding a field does not change the values of the others. A segment is a string or an integer. An integer reads as its decimal text. |
()
|
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
ValueError
|
When |
Examples:
from pytest_graphql import DeterministicRandom
name = DeterministicRandom(7, "User", "name").sample_string(8)
assert name == DeterministicRandom(7, "User", "name").sample_string(8)
email = DeterministicRandom(7, "User", "email").sample_string(8)
assert email != name
bits
bits(width: int) -> int
Return an integer of width random bits, from 0 to 2**width - 1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
width
|
int
|
The number of bits. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The integer. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
ValueError
|
When |
Examples:
from pytest_graphql import DeterministicRandom
rng = DeterministicRandom(1, "demo")
assert 0 <= rng.bits(8) < 256
assert rng.bits(0) == 0
below
below(bound: int) -> int
Return a uniform integer from 0 up to but not including bound.
Every value has the same chance. The method never uses a remainder, which would favour small values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bound
|
int
|
The exclusive upper limit. It must be at least |
required |
Returns:
| Type | Description |
|---|---|
int
|
The integer. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
ValueError
|
When |
Examples:
from pytest_graphql import DeterministicRandom
rng = DeterministicRandom(1, "demo")
assert all(0 <= rng.below(6) < 6 for _ in range(100))
assert rng.below(1) == 0
float_unit
float_unit() -> float
Return a float from 0 up to but not including 1.
The value is a multiple of 2**-53, so it is the same on every platform.
Returns:
| Type | Description |
|---|---|
float
|
The float. |
Examples:
from pytest_graphql import DeterministicRandom
rng = DeterministicRandom(1, "demo")
assert 0.0 <= rng.float_unit() < 1.0
choice
choice(items: Sequence[T]) -> T
Return one element of a sequence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
Sequence[T]
|
A sequence with a stable order, such as a list or a tuple. A set is refused, because its order can change between runs. |
required |
Returns:
| Type | Description |
|---|---|
T
|
One of the elements. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
ValueError
|
When |
Examples:
from pytest_graphql import DeterministicRandom
rng = DeterministicRandom(1, "demo")
assert rng.choice(["red", "green", "blue"]) in {"red", "green", "blue"}
sample_string
sample_string(
length: int, alphabet: str = DEFAULT_ALPHABET
) -> str
Return a string of random characters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
length
|
int
|
The number of characters. It may be |
required |
alphabet
|
str
|
The characters to draw from. The default is the lowercase letters and the digits. |
DEFAULT_ALPHABET
|
Returns:
| Type | Description |
|---|---|
str
|
The string. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
ValueError
|
When |
Examples:
from pytest_graphql import DeterministicRandom
rng = DeterministicRandom(1, "demo")
assert len(rng.sample_string(12)) == 12
assert set(rng.sample_string(20, "01")) <= {"0", "1"}
ScalarRegistry
ScalarRegistry(specs: Iterable[ScalarSpec] = ())
The custom scalars that a client knows, kept by name.
A client has one registry, available as gql.scalars. The same registry
decodes responses, serializes variables and feeds gql.fake. It is read
again for every call, so a scalar that you register at any time takes effect
on the next one. A clone made with with_headers() or as_() shares its
parent's registry.
Iteration gives the registered names in sorted order. in tests a name, and
len() counts the specs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
specs
|
Iterable[ScalarSpec]
|
Specs to register at once. A name that appears twice raises
|
()
|
Examples:
from pytest_graphql import ScalarRegistry, ScalarSpec
registry = ScalarRegistry(
[ScalarSpec(name="Money", serialize=str, fake=lambda rng: "1.00")]
)
assert "Money" in registry
assert list(registry) == ["Money"]
assert len(registry) == 1
register
register(
spec: ScalarSpec, *, replace: bool = False
) -> None
Add a scalar spec to the registry.
Two registrations of one name usually mean that two plugins disagree, so the second one is an error unless you say that you mean to replace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
ScalarSpec
|
The spec to add. |
required |
replace
|
bool
|
Replace a spec that has the same name. Without it, a name that is already registered raises an error. |
False
|
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
ValueError
|
When the name is registered and |
Examples:
from pytest_graphql import ScalarRegistry, ScalarSpec
registry = ScalarRegistry()
registry.register(ScalarSpec(name="Money", serialize=str, fake=str))
registry.register(
ScalarSpec(name="Money", serialize=repr, fake=str), replace=True
)
assert registry.get("Money").serialize is repr
get
get(name: str) -> ScalarSpec | None
Return the spec registered under a name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The scalar's name. |
required |
Returns:
| Type | Description |
|---|---|
ScalarSpec | None
|
The spec, or |
Examples:
from pytest_graphql import ScalarRegistry, ScalarSpec
spec = ScalarSpec(name="Money", serialize=str, fake=str)
registry = ScalarRegistry([spec])
assert registry.get("Money") is spec
assert registry.get("Date") is None
parsers
parsers() -> dict[str, Callable[[Any], Any]]
Return the parse function of every spec that has one, by name.
This is a snapshot. It does not change when you register more specs. The client takes a new snapshot for each response.
Returns:
| Type | Description |
|---|---|
dict[str, Callable[[Any], Any]]
|
A new |
Examples:
from pytest_graphql import ScalarRegistry, ScalarSpec
registry = ScalarRegistry(
[
ScalarSpec(name="Money", serialize=str, fake=str),
ScalarSpec(name="Count", serialize=int, fake=int, parse=int),
]
)
assert registry.parsers() == {"Count": int}
__contains__
__contains__(name: object) -> bool
Tell whether a name is registered.
Examples:
assert "DateTime" not in gql.scalars
__iter__
__iter__() -> Iterator[str]
Iterate over the registered names in sorted order.
Examples:
from pytest_graphql import ScalarRegistry, ScalarSpec
registry = ScalarRegistry()
for name in ("Money", "Date"):
registry.register(ScalarSpec(name=name, serialize=str, fake=str))
assert list(registry) == ["Date", "Money"]
__len__
__len__() -> int
Count the registered specs.
Examples:
assert len(gql.scalars) == 0
ScalarSpec
dataclass
ScalarSpec(
name: str,
serialize: Callable[[Any], Any],
fake: Callable[[DeterministicRandom], Any],
parse: Callable[[Any], Any] | None = None,
)
How to read, write and generate one custom scalar.
A custom scalar is a scalar type that your schema defines, such as
DateTime or Money. On the wire it is plain JSON. A ScalarSpec tells the
client how to turn that JSON into a Python value, how to turn a Python value
back into JSON for a variable, and how to make a test value. Register it in
the client's ScalarRegistry, which you reach as gql.scalars.
The five scalars that GraphQL itself defines (String, ID, Int,
Float and Boolean) are handled by the package. A spec cannot use one of
those names.
The spec checks itself when you build it. The name must be a valid GraphQL
name that does not start with two underscores. serialize, fake and a
given parse must be callable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The scalar's name in the schema. |
required |
serialize
|
Callable[[Any], Any]
|
Turns a Python value into the JSON value that is sent. It
runs on every variable of this type. The result must be JSON: |
required |
fake
|
Callable[[DeterministicRandom], Any]
|
Makes a test value for |
required |
parse
|
Callable[[Any], Any] | None
|
Turns the JSON value of a response into a Python value. Leave it
as |
None
|
Examples:
import datetime
from pytest_graphql import ScalarSpec
spec = ScalarSpec(
name="DateTime",
serialize=lambda value: value.isoformat(),
fake=lambda rng: datetime.datetime(
2020, 1, 1 + rng.below(28), tzinfo=datetime.timezone.utc
),
parse=datetime.datetime.fromisoformat,
)
gql.scalars.register(spec)
user = gql.query("user", id="u1")
assert user.joined_at == datetime.datetime(
2020, 1, 1, tzinfo=datetime.timezone.utc
)
name
instance-attribute
name: str
The scalar's name in the schema.
Examples:
from pytest_graphql import DeterministicRandom, ScalarSpec
spec = ScalarSpec(
name="Money", serialize=str, fake=lambda rng: "1.00", parse=int
)
assert spec.name == "Money"
serialize
instance-attribute
serialize: Callable[[Any], Any]
Turns a Python value into the JSON value that is sent in a variable.
Examples:
from pytest_graphql import DeterministicRandom, ScalarSpec
spec = ScalarSpec(
name="Money", serialize=str, fake=lambda rng: "1.00", parse=int
)
assert spec.serialize(5) == "5"
fake
instance-attribute
fake: Callable[[DeterministicRandom], Any]
Makes a test value from a DeterministicRandom.
Examples:
from pytest_graphql import DeterministicRandom, ScalarSpec
spec = ScalarSpec(
name="Money", serialize=str, fake=lambda rng: "1.00", parse=int
)
assert spec.fake(DeterministicRandom(1, "Pay", "amount")) == "1.00"
parse
class-attribute
instance-attribute
parse: Callable[[Any], Any] | None = None
Turns a JSON value from a response into a Python value, or None for none.
Examples:
from pytest_graphql import DeterministicRandom, ScalarSpec
spec = ScalarSpec(
name="Money", serialize=str, fake=lambda rng: "1.00", parse=int
)
assert spec.parse("5") == 5
assert ScalarSpec(name="Date", serialize=str, fake=str).parse is None
Matcher
Bases: ABC
Something a value can be compared with, to say whether it matches.
You rarely build a Matcher directly. You get one from gql.expect.Type(...)
or from a helper such as contains(), gt() or matches(). Helpers can
also sit inside a plain dict or list that you compare with a response.
Use a matcher in one of three ways:
value == matcherisTrueorFalse. It stops at the first difference, so it costs no more than a normal comparison. Under pytest, a failedassertprints a short list of the fields that differ.matcher.evaluate(value)compares again and keeps every difference.matcher.explain(value)returns the lines of that list as text.
A matcher is partial. It checks the fields it names and ignores the rest.
repr() of a matcher shows type and field names only, never the expected
values, so a secret in an expected value does not appear in a test report.
Matchers cannot be used as dictionary keys or set members.
Examples:
from pytest_graphql import Matcher, contains
user = gql.query("user", id="u1")
matcher = gql.expect.User(name="Ada Lovelace")
assert isinstance(matcher, Matcher)
assert user == matcher
assert gql.query("users") == contains(matcher)
describe
abstractmethod
describe(show: Show, depth: int = DESCRIBE_DEPTH) -> str
Describe what this matcher expects, in a short text.
Every expected value and every field name goes through show, so the
caller decides what text a value may become. repr(matcher) is this
method with a show that prints only the name of each value's type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
show
|
Show
|
A function that returns the text for one value. |
required |
depth
|
int
|
How many levels of nested expectations to spell out. Deeper levels are summarized by count. |
DESCRIBE_DEPTH
|
Returns:
| Type | Description |
|---|---|
str
|
The description. |
Examples:
matcher = gql.expect.User(name="Ada")
assert matcher.describe(lambda value: "?") == "User(?=?)"
assert repr(matcher) == "User(name=<str>)"
evaluate
evaluate(actual: Any) -> MatchResult
Compare actual with this matcher and keep every difference.
Unlike ==, this does not stop at the first difference. The counts in
the result are exact. The lists of differences and of matched paths
keep at most 200 entries each, so a huge response cannot use unbounded
memory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
Any
|
The value to compare, such as a |
required |
Returns:
| Type | Description |
|---|---|
MatchResult
|
A result with these attributes: |
MatchResult
|
many fields were compared), |
MatchResult
|
|
MatchResult
|
not name), |
MatchResult
|
that matched). |
Examples:
user = gql.query("user", id="u1", fields=["id", "name"])
result = gql.expect.User(name="Ada").evaluate(user)
assert not result.ok
assert (result.compared, result.differ, result.ignored) == (1, 1, 1)
assert result.mismatches[0].path == ("name",)
explain
explain(
actual: Any, options: RenderOptions | None = None
) -> list[str]
Return the lines that show how actual differs from this matcher.
The first line is a summary. The lines after it name each field that differs, with the actual value and the expected one, and then the fields that matched. A long list is cut and says how many entries it left out. Values are cleaned of control characters and cut to a length limit. When the result is empty, the value matches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
Any
|
The value to compare. |
required |
options
|
RenderOptions | None
|
How to print the lines, such as the limits on line count and value length. Leave it out for the defaults. Under pytest, the failure report passes options that also remove the secrets of the request that produced the response. |
None
|
Returns:
| Type | Description |
|---|---|
list[str]
|
The lines, or an empty list when |
Examples:
user = gql.query("user", id="u1", fields=["id", "name"])
lines = gql.expect.User(name="Ada").explain(user)
assert lines[0] == (
" User does not match "
"(1 of 1 compared field differs; 1 response field ignored)"
)
assert "'Ada Lovelace'" in lines[1]
assert gql.expect.User(name="Ada Lovelace").explain(user) == []
BaseMiddleware
A Middleware whose hooks do nothing, to subclass.
Override before_request, after_response or both. The hook you leave
alone passes the request or the response through unchanged.
Examples:
from pytest_graphql import BaseMiddleware, build_client
class CountCalls(BaseMiddleware):
def __init__(self):
self.count = 0
def before_request(self, request):
self.count += 1
counter = CountCalls()
client = build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=gql.schema,
middleware=[counter],
)
client.query("users")
client.query("user", id="u2")
assert counter.count == 2
before_request
before_request(request: RequestInfo) -> RequestInfo | None
Return None, which keeps the request as it is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
RequestInfo
|
The request about to be sent. |
required |
Returns:
| Type | Description |
|---|---|
RequestInfo | None
|
Always |
Examples:
from pytest_graphql import BaseMiddleware, RequestInfo
request = RequestInfo(
operation=None,
kind="query",
document="{ __typename }",
variables={},
headers={},
url="http://localhost:8000/graphql",
)
assert BaseMiddleware().before_request(request) is None
after_response
after_response(
response: GraphQLResponse[Any],
) -> GraphQLResponse[Any] | None
Return None, which keeps the response as it is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
response
|
GraphQLResponse[Any]
|
The response that was received. |
required |
Returns:
| Type | Description |
|---|---|
GraphQLResponse[Any] | None
|
Always |
Examples:
from pytest_graphql import BaseMiddleware
response = gql.execute("{ __typename }")
assert BaseMiddleware().after_response(response) is None
Middleware
Bases: Protocol
Code that runs around every call: one hook before it, one after it.
Any object with these two methods is a Middleware. Subclass
BaseMiddleware to write only the one you need. Pass a list of middleware
to build_client() or GraphQLClient as middleware=.
The list is ordered. before_request runs from the first item to the last,
and after_response runs from the last item to the first, so each
middleware wraps the ones after it. A hook that returns None changes
nothing. A hook that returns an object replaces the request or the response
for every hook that runs after it. A hook that raises stops the call, and
the exception reaches the caller unchanged.
Examples:
from pytest_graphql import BaseMiddleware, build_client
class Tracer:
def __init__(self):
self.events = []
def before_request(self, request):
self.events.append(f"send {request.operation}")
def after_response(self, response):
self.events.append(f"got {response.http.status_code}")
tracer = Tracer()
client = build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=gql.schema,
middleware=[tracer],
)
client.query("user", id="u1")
assert tracer.events == ["send user", "got 200"]
before_request
before_request(request: RequestInfo) -> RequestInfo | None
Look at, or replace, a request before it is sent.
request carries real header and variable values. It is immutable, so
to change it, return a new one built with dataclasses.replace().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
RequestInfo
|
The request about to be sent. |
required |
Returns:
| Type | Description |
|---|---|
RequestInfo | None
|
A request to send instead, or |
Examples:
from dataclasses import replace
from pytest_graphql import BaseMiddleware, build_client
class AddTraceHeader(BaseMiddleware):
def before_request(self, request):
headers = {**request.headers, "X-Trace-Id": "t-1"}
return replace(request, headers=headers)
client = build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=gql.schema,
middleware=[AddTraceHeader()],
)
assert client.query("user", id="u1").name == "Ada Lovelace"
after_response
after_response(
response: GraphQLResponse[Any],
) -> GraphQLResponse[Any] | None
Look at, or replace, a response after it was received.
This runs before the client decides whether to raise because of errors in the response. A hook that raises here stops the call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
response
|
GraphQLResponse[Any]
|
The response that was received. |
required |
Returns:
| Type | Description |
|---|---|
GraphQLResponse[Any] | None
|
A response to use instead, or |
Examples:
from pytest_graphql import BaseMiddleware, build_client
class SlowCalls(BaseMiddleware):
def __init__(self):
self.slow = []
def after_response(self, response):
if response.duration_ms > 1000:
self.slow.append(response.request.operation)
slow = SlowCalls()
client = build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=gql.schema,
middleware=[slow],
)
client.query("user", id="u1")
assert slow.slow == []
GraphQLResponse
dataclass
GraphQLResponse(
data: T,
data_state: DataState,
errors: tuple[GraphQLErrorInfo, ...],
extensions: Mapping[str, Any],
http: HttpInfo,
request: DiagnosticSnapshot,
raw: Mapping[str, Any],
duration_ms: float,
)
Bases: Generic[T]
Everything the server returned for one operation, and how it was sent.
query() and mutation() return the value of the one field you asked for,
so you usually do not see this object. You get it from
GraphQLClient.execute(), from a call made with raw=True, and from the
response of an error. Use it to read errors next to data, the HTTP status,
the timing and the untouched JSON.
The response is immutable. Its repr() shows the status, the state of the
data, the number of errors and the redacted request. It never shows a data
value or an error message, so a failure report does not leak them.
The type parameter is the type of data. It is Any by default. Annotate
a variable as GraphQLResponse[MyType] if you want your own type for it.
Examples:
response = gql.execute('query { user(id: "u1") { name } }')
assert response.has_data
assert response.data.user.name == "Ada Lovelace"
assert response.errors == ()
assert response.http.status_code == 200
data
instance-attribute
data: T
The result of the operation.
When data_state is "present", this is a Node that holds the top-level
fields you selected. Otherwise it is None.
Examples:
response = gql.execute('{ user(id: "u1") { name } }')
assert response.data.user.name == "Ada Lovelace"
data_state
instance-attribute
data_state: DataState
Whether the server sent data: "present", "null" or "absent".
"present":datais an object."null": the server sentdata: null, which happens when an error stopped the whole operation."absent": the response has nodataentry at all. A server response with nodataentry is a rejected request and raises an error before a response exists, so you will see"present"and"null".
Examples:
response = gql.execute('{ user(id: "u1") { name } }')
assert response.data_state == "present"
failed = gql.mutation(
"updateUser", id="missing", fields=["id"], raise_on_error=False, raw=True
)
assert failed.data_state == "null"
errors
instance-attribute
errors: tuple[GraphQLErrorInfo, ...]
The entries of the response's errors list, in the server's order.
Each entry has a message (text from the server), a path and locations
(or None), an extensions mapping, and a code, which is
extensions["code"] when that is a string and None otherwise. repr() of
an entry shows the path and locations and not the message. The tuple is
empty when the server returned no errors.
Examples:
response = gql.execute('{ user(id: "u1") { name } }')
assert response.errors == ()
failed = gql.mutation(
"updateUser", id="missing", fields=["id"], raise_on_error=False, raw=True
)
assert failed.errors[0].path == ("updateUser",)
extensions
instance-attribute
extensions: Mapping[str, Any]
The response's top-level extensions, or an empty mapping.
Examples:
response = gql.execute('{ user(id: "u1") { name } }')
assert response.extensions == {}
http
instance-attribute
http: HttpInfo
The HTTP exchange: status_code, media_type, url and headers.
Examples:
response = gql.execute('{ user(id: "u1") { name } }')
assert response.http.status_code == 200
assert response.http.url.startswith("http")
request
instance-attribute
request: DiagnosticSnapshot
The request that was sent, in its redacted form, for reports.
Redaction follows the rules in DiagnosticSnapshot: it covers the redacted
headers and variables, and known secrets in free text above a minimum length.
A header or variable that the settings do not name is not redacted by its name.
Examples:
response = gql.execute('{ user(id: "u1") { name } }')
assert response.request.kind == "query"
assert "user" in response.request.document
raw
instance-attribute
raw: Mapping[str, Any]
The response envelope, before any parsing.
It holds data, and holds errors and extensions only when the server
sent them. The values are the JSON the transport parsed. Custom scalars
are not decoded here.
Examples:
response = gql.execute('{ user(id: "u1") { name } }')
assert response.raw == {"data": {"user": {"name": "Ada Lovelace"}}}
duration_ms
instance-attribute
duration_ms: float
How long the transport call took, in milliseconds.
Examples:
response = gql.execute('{ user(id: "u1") { name } }')
assert response.duration_ms >= 0
has_data
property
has_data: bool
Whether data holds an object. False when it is None.
Examples:
response = gql.execute('{ user(id: "u1") { name } }')
assert response.has_data
assert response.data_state == "present"
unwrap
unwrap() -> Any
Return the value of the one top-level field the operation selected.
This is what query() and mutation() do for you. It is for a
document that you sent with execute(). The count is of the fields the
document selects at the top level, whether or not the server returned
them.
Returns:
| Type | Description |
|---|---|
Any
|
The value of that field. |
Raises:
| Type | Description |
|---|---|
GraphQLTestError
|
When |
Examples:
response = gql.execute('{ user(id: "u1") { name } }')
assert response.unwrap().name == "Ada Lovelace"
at
at(path: str, default: Any = MISSING) -> Any
Read a value from data by a dotted path.
A segment is a field name or a list index. Write an index as a number
(orders.0.total) or in brackets (orders[0].total). Each name works
in its exact schema spelling and in snake_case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
The path to read. |
required |
default
|
Any
|
What to return when a segment does not exist. Without it, a missing segment raises. |
MISSING
|
Returns:
| Type | Description |
|---|---|
Any
|
The value at the path. |
Raises:
| Type | Description |
|---|---|
GraphQLFieldError
|
When a segment does not exist and no |
GraphQLTestError
|
When |
Examples:
response = gql.execute('{ user(id: "u1") { name team { name } } }')
assert response.at("user.team.name") == "Core"
assert response.at("user.nickname", default=None) is None
Node
Node(
raw: dict[str, Any],
materializer: Materializer,
named: GraphQLNamedType,
selections: tuple[SelectionSetNode, ...],
)
Bases: Mapping[str, Any]
One object from a response, such as a User, read like a record.
A Node is a read-only mapping. Read a field as an attribute
(user.name) or as a key (user["name"]). Both the exact schema name and
the snake_case form work, so user.joined_at reads the field joinedAt.
Reading a field that the response does not hold raises GraphQLFieldError.
The error lists the fields that are there and suggests a close match. It also
reminds you that the field may exist in the schema and still be missing from
your selection.
Values are read lazily. A field that holds an object gives a Node, and a
field that holds a list of objects gives a NodeList. A custom scalar gives
the value its registered parse function returns, or the raw JSON value
when it has none. A key the selection did not ask for is read as raw JSON.
Iteration and len() use the server's own keys. dict(node) gives those
keys with the values wrapped as above. node == other is true for another
node or a dict with exactly the same data. For a partial comparison, use a
matcher such as gql.expect.User(name="Ada"). A Node cannot be used as a
dictionary key.
repr() shows the type name and the field names. It never shows a value, so
a secret in the data cannot leak into a test report.
You do not create a Node. The client builds them from responses.
Examples:
user = gql.query("user", id="u1")
assert user.name == "Ada Lovelace"
assert user["name"] == "Ada Lovelace"
assert user.team.name == "Core"
assert user.joined_at == user["joinedAt"]
assert "name" in user
__typename__
property
__typename__: str | None
The name of the object's type, or None when it is not known.
For a field whose schema type is an object type, the name is always
known. For an interface or a union, it is known only when the response
has __typename. The client adds __typename to every selection it
generates. In a selection you write yourself, ask for it by name.
Examples:
user = gql.query("user", id="u1", fields=["id"])
assert user.__typename__ == "User"
node = gql.query("node", id="u1", fields=["id"])
assert node.__typename__ is None
node = gql.query("node", id="u1", fields=["id", "__typename"])
assert node.__typename__ == "User"
__getitem__
__getitem__(key: str) -> Any
Read a field by its schema name or its snake_case name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The field name. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The field value. A nested object is a |
Any
|
|
Raises:
| Type | Description |
|---|---|
GraphQLFieldError
|
When the key is not a string, the response has no
such field, or the snake_case name matches two fields. The error
is also a |
Examples:
user = gql.query("user", id="u1")
assert user["name"] == "Ada Lovelace"
assert user["joined_at"] == user["joinedAt"]
__getattr__
__getattr__(name: str) -> Any
Read a field as an attribute. It works like node[name].
A name that starts with an underscore is never read as a field. Use
node["_name"] for a field whose name starts with one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The field name. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The field value. |
Raises:
| Type | Description |
|---|---|
GraphQLFieldError
|
When the response has no such field. The error is
also an |
Examples:
user = gql.query("user", id="u1")
assert user.name == "Ada Lovelace"
assert getattr(user, "nickname", "none") == "none"
__iter__
__iter__() -> Iterator[str]
Iterate over the keys exactly as the server returned them.
Examples:
user = gql.query("user", id="u1", fields=["id", "name"])
assert list(user) == ["id", "name"]
__len__
__len__() -> int
Count the keys the server returned for this object.
Examples:
user = gql.query("user", id="u1", fields=["id", "name"])
assert len(user) == 2
__contains__
__contains__(key: object) -> bool
Tell whether the object has a field, by either spelling of its name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
object
|
The field name. Anything that is not a string gives |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Examples:
user = gql.query("user", id="u1", fields=["id", "joinedAt"])
assert "joinedAt" in user
assert "joined_at" in user
assert "name" not in user
get
get(key: str, default: Any = None) -> Any
Read a field, or return default when the response does not have it.
A snake_case name that matches two fields is still an error, because returning a default would hide that.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The field name, in either spelling. |
required |
default
|
Any
|
The value to return for a missing field. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The field value, or |
Examples:
user = gql.query("user", id="u1", fields=["id"])
assert user.get("id") == "u1"
assert user.get("name") is None
assert user.get("name", "unknown") == "unknown"
at
at(path: str, default: Any = MISSING) -> Any
Read a nested value by a dotted path.
A segment is a field name or a list index. Write an index as a number
(friends.0.name) or in brackets (friends[0].name). Each name works
in its exact schema spelling and in snake_case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
The path to read. |
required |
default
|
Any
|
What to return when a segment does not exist. Without it, a missing segment raises. |
MISSING
|
Returns:
| Type | Description |
|---|---|
Any
|
The value at the path. |
Raises:
| Type | Description |
|---|---|
GraphQLFieldError
|
When a segment does not exist and no |
Examples:
user = gql.query("user", id="u2")
assert user.at("manager.name") == "Ada Lovelace"
assert user.at("friends[0].name") == "Ada Lovelace"
assert user.at("manager.nickname", default=None) is None
to_dict
to_dict() -> dict[str, Any]
Return the object as a plain dict of the data the server returned.
The keys are the server's response keys. Nested values are plain JSON values, and custom scalars are not decoded. The copy is independent, so changing it does not change the response.
Examples:
fields = ["id", {"team": ["name"]}]
user = gql.query("user", id="u1", fields=fields)
assert user.to_dict() == {"id": "u1", "team": {"name": "Core"}}
NodeList
Bases: list[Any]
A list of Node values, with helpers to filter and read them.
A field that holds a list of objects gives a NodeList. It is a normal
list, so len(), indexing, slicing and iteration work. A list of lists
keeps the nesting, and the inner lists are NodeList too.
Examples:
users = gql.query("users", fields=["id", "name"])
assert len(users) == 3
assert users[0].name == "Ada Lovelace"
assert users.pluck("name") == ["Ada Lovelace", "Grace Hopper", "Alan Turing"]
assert users.one(id="u2").name == "Grace Hopper"
where
where(*, strict: bool = False, **filters: Any) -> NodeList
Return the elements that match every filter.
Each keyword is a field name and what its value must be. The value is
a plain value or a matcher such as matches() or gt(). A field name
works in either spelling. The match is partial: an element matches when
each named field matches, and its other fields are ignored. An element
that is not a Node never matches.
The result is a new NodeList, so len(users.where(...)) is the number
of matches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
strict
|
bool
|
Also require that the element has no field besides the
named ones. Because this is a keyword of |
False
|
**filters
|
Any
|
The field filters. |
{}
|
Returns:
| Type | Description |
|---|---|
NodeList
|
A new list of the elements that match. |
Examples:
from pytest_graphql import matches
users = gql.query("users", fields=["id", "name"])
assert users.where(name=matches("^A")).pluck("id") == ["u1", "u3"]
exact = users.where(id="u1", name="Ada Lovelace", strict=True)
assert exact.ids() == ["u1"]
assert users.where(id="u1", strict=True) == []
one
one(*, strict: bool = False, **filters: Any) -> Any
Return the one element that matches the filters.
The filters work as in where().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
strict
|
bool
|
Also require that the element has no field besides the named ones. |
False
|
**filters
|
Any
|
The field filters. |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
The matching element. |
Raises:
| Type | Description |
|---|---|
GraphQLTestError
|
When zero or several elements match. The message gives the number found, the size of the list and the names of the filters. It does not print any response value. |
Examples:
users = gql.query("users", fields=["id", "name"])
assert users.one(name="Grace Hopper").id == "u2"
pluck
pluck(path: str, default: Any = MISSING) -> list[Any]
Read the same path from every element and return the values as a list.
The path works as in Node.at().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
The dotted path to read from each element. |
required |
default
|
Any
|
What to use for an element that lacks the path. Without it, a missing path raises. |
MISSING
|
Returns:
| Type | Description |
|---|---|
list[Any]
|
One value for each element, in order. |
Raises:
| Type | Description |
|---|---|
GraphQLFieldError
|
When an element lacks the path and no |
Examples:
fields = ["name", {"manager": ["name"]}]
users = gql.query("users", fields=fields)
names = ["Ada Lovelace", "Grace Hopper", "Alan Turing"]
assert users.pluck("name") == names
managers = users.pluck("manager.name", default=None)
assert managers == [None, "Ada Lovelace", "Ada Lovelace"]
ids
ids() -> list[Any]
Return the id of every element. It is pluck("id").
Examples:
users = gql.query("users", fields=["id"])
assert users.ids() == ["u1", "u2", "u3"]
at
at(path: str, default: Any = MISSING) -> Any
Read a value from the list by a path that starts with an index.
Write the index as a number (0.name) or in brackets ([0].name).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
The path to read. |
required |
default
|
Any
|
What to return when a segment does not exist. Without it, a missing segment raises. |
MISSING
|
Returns:
| Type | Description |
|---|---|
Any
|
The value at the path. |
Raises:
| Type | Description |
|---|---|
GraphQLFieldError
|
When a segment does not exist and no |
Examples:
users = gql.query("users", fields=["id", "name"])
assert users.at("0.name") == "Ada Lovelace"
assert users.at("[1].name") == "Grace Hopper"
assert users.at("[9].name", default=None) is None
SchemaSource
Bases: Protocol
Tells the client where to read the schema from.
By default the client reads the schema by asking the server, with the
standard introspection query. Give build_client() a schema_source when
the schema lives somewhere else, for example in a .graphql file in your
repository, or when the server has introspection turned off.
Any object with a load method and a fingerprint property of this shape
is a SchemaSource. The schema is a graphql.GraphQLSchema from
graphql-core.
Examples:
A source that builds the schema from SDL text:
from graphql import GraphQLSchema, build_schema
from pytest_graphql import build_client
class InlineSource:
def __init__(self, sdl: str) -> None:
self.sdl = sdl
def load(self) -> GraphQLSchema:
return build_schema(self.sdl)
@property
def fingerprint(self) -> str:
return "inline"
client = build_client(
url="http://localhost:8000/graphql",
schema_source=InlineSource("type Query { ping: Boolean! }"),
)
assert "ping" in client.schema.query_type.fields
client.close()
fingerprint
property
fingerprint: str
A stable label for this source.
Use text that does not change between runs for the same schema, such as a file path. It names the source in cache keys and in failure reports.
Examples:
class FileSource:
def __init__(self, path):
self.path = path
@property
def fingerprint(self):
return f"sdl:{self.path}"
assert FileSource("schema.graphql").fingerprint == "sdl:schema.graphql"
load
load() -> GraphQLSchema
Read the schema and return it.
Returns:
| Type | Description |
|---|---|
GraphQLSchema
|
The schema. |
Raises:
| Type | Description |
|---|---|
Exception
|
When the schema cannot be read or is not valid. The exception reaches the caller. |
Examples:
from graphql import build_schema
class OneField:
fingerprint = "one-field"
def load(self):
return build_schema("type Query { ping: Boolean! }")
schema = OneField().load()
assert schema.query_type.name == "Query"
Field
dataclass
Field(
name: str,
*,
args: Mapping[str, Any] | None = None,
alias: str | None = None,
fields: SelectionInput | None = None,
on: str | None = None,
directives: Mapping[str, Mapping[str, Any]]
| None = None,
)
One field in a selection, when a plain name is not enough.
Write a plain string for a field with no arguments. Use Field when the
field takes arguments, needs an alias, or applies only to one type of an
interface or union.
Argument values are Python values. They are never written into the query text. Each one is sent as a variable, so a value cannot change the shape of the query.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The field name in the schema. The snake_case form of a camelCase name is accepted too. |
required |
args
|
Mapping[str, Any] | None
|
Argument values for the field, by argument name. |
None
|
alias
|
str | None
|
A name for the field in the response. Use it to ask for the same field twice with different arguments. It must be a valid GraphQL name. |
None
|
fields
|
SelectionInput | None
|
What to select inside the field. A list of names, nested
fields, |
None
|
on
|
str | None
|
The name of an object type. The field is then selected only for
values of that type, as in |
None
|
directives
|
Mapping[str, Mapping[str, Any]] | None
|
Not supported in this version. A non-empty value raises an
error. To send a query with directives, use |
None
|
Examples:
from pytest_graphql import Field
response = gql.query(
"user",
id="u1",
fields=[
"name",
Field("posts", args={"first": 5}, alias="recent", fields=["title"]),
],
raw=True,
)
assert "recent: posts(first: $posts_first)" in response.request.document
assert response.data.user.recent[0].title == "Hello World"
name
instance-attribute
name: str
The field name, as written in the schema or in snake_case.
Examples:
from pytest_graphql import Field
field = Field(
"posts", args={"first": 5}, alias="recent", fields=["title"], on="User"
)
assert field.name == "posts"
args
class-attribute
instance-attribute
args: Mapping[str, Any] | None = None
Argument values by argument name, or None for no arguments.
Examples:
from pytest_graphql import Field
field = Field(
"posts", args={"first": 5}, alias="recent", fields=["title"], on="User"
)
assert field.args == {"first": 5}
assert Field("name").args is None
alias
class-attribute
instance-attribute
alias: str | None = None
The name this field has in the response, or None to use name.
Examples:
from pytest_graphql import Field
field = Field(
"posts", args={"first": 5}, alias="recent", fields=["title"], on="User"
)
assert field.alias == "recent"
assert Field("name").alias is None
fields
class-attribute
instance-attribute
fields: SelectionInput | None = None
What to select inside this field, or None when nothing is selected.
Examples:
from pytest_graphql import Field
field = Field(
"posts", args={"first": 5}, alias="recent", fields=["title"], on="User"
)
assert field.fields == ["title"]
assert Field("name").fields is None
on
class-attribute
instance-attribute
on: str | None = None
An object type that this field is limited to, or None for no limit.
Examples:
from pytest_graphql import Field
field = Field(
"posts", args={"first": 5}, alias="recent", fields=["title"], on="User"
)
assert field.on == "User"
assert Field("name").on is None
directives
class-attribute
instance-attribute
directives: Mapping[str, Mapping[str, Any]] | None = field(
default=None
)
Not supported yet. Setting a value raises an error.
Examples:
from pytest_graphql import Field
assert Field("name").directives is None
try:
Field("name", directives={"include": {"if": True}})
except Exception as error:
assert "not supported" in str(error)
else:
raise AssertionError("expected an error")
response_key
property
response_key: str
The key this field has in the response data: the alias, or else the name.
Examples:
from pytest_graphql import Field
assert Field("posts", alias="recent").response_key == "recent"
assert Field("posts").response_key == "posts"
Selection
Selection(*parts: SelectionItem)
A reusable group of fields that you define once and use in many calls.
The parts can be any of the forms that fields= accepts, except AUTO on its
own: a field name, a nested mapping, a Field, a raw GraphQL string or
another Selection. AUTO can still be the sub-selection of a part, as in
Field("team", fields=AUTO). When a field changes in your schema, you edit
the one Selection instead of every test that reads it.
A Selection never changes. Combine them with +, and remove a field with
-. Each operator returns a new Selection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*parts
|
SelectionItem
|
The items to select. |
()
|
Examples:
from pytest_graphql import Field, Selection
USER_BRIEF = Selection("id", "name")
USER_WITH_TEAM = USER_BRIEF + Selection({"team": ["name"]})
user = gql.query("user", id="u2", fields=USER_WITH_TEAM)
assert user.name == "Grace Hopper"
assert user.team.name == "Core"
parts
property
parts: tuple[SelectionItem, ...]
The items this selection was built from, as they were given.
Examples:
from pytest_graphql import Selection
assert Selection("id", "name").parts == ("id", "name")
removals
property
removals: tuple[str, ...]
The response keys or paths that - removed from this selection.
Examples:
from pytest_graphql import Selection
assert (Selection("id", "name") - "id").removals == ("id",)
assert Selection("id", "name").removals == ()
of
classmethod
of(type_name: str, *parts: SelectionItem) -> Selection
Make a selection that applies only to values of one type.
Use it on a field that returns an interface or a union, to choose
fields that exist on one implementation only. It becomes an inline
fragment, ... on TypeName { ... }. The type name is checked against
the schema when the query is built. An unknown name, or a type that the
field cannot return, raises an error with a suggestion.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type_name
|
str
|
The name of the object type. |
required |
*parts
|
SelectionItem
|
The fields to select for that type. With none, the client
chooses the fields of the type, as |
()
|
Returns:
| Type | Description |
|---|---|
Selection
|
A new selection. |
Raises:
| Type | Description |
|---|---|
SchemaError
|
When |
Examples:
from pytest_graphql import Selection
fields = ["id", Selection.of("User", "name")]
node = gql.query("node", id="u1", fields=fields)
assert node.name == "Ada Lovelace"
__add__
__add__(other: Selection) -> Selection
Join two selections into one that has the fields of both.
Two fields that share a response key must be the same request. If they
have different arguments or aliases, building the query raises an error.
A removal made with - on one side stays on that side.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Selection
|
The selection to add. |
required |
Returns:
| Type | Description |
|---|---|
Selection
|
A new selection. |
Examples:
from pytest_graphql import Selection
both = Selection("id") + Selection("name")
assert gql.query("user", id="u1", fields=both).name == "Ada Lovelace"
__sub__
__sub__(other: str) -> Selection
Remove a field from this selection.
Name a response key, such as "id", or a dotted path to a nested field,
such as "team.id". The removal applies to everything in this
selection. Removing a field that is not there raises an error when the
query is built, so a renamed field cannot silently stop being removed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
str
|
The response key or dotted path to remove. |
required |
Returns:
| Type | Description |
|---|---|
Selection
|
A new selection without that field. |
Examples:
from pytest_graphql import Selection
brief = Selection("id", "name", {"team": ["id", "name"]})
without_ids = brief - "id" - "team.id"
user = gql.query("user", id="u1", fields=without_ids)
assert "id" not in user
assert "id" not in user.team
assert user.team.name == "Core"
SelectionPolicy
dataclass
SelectionPolicy(
max_depth: int = 3,
cycle_policy: CyclePolicy = "shallow",
per_type_depth_cap: Mapping[str, int] = dict(),
include_deprecated: bool = False,
max_fields: int = 2000,
exclude: Sequence[str] = (),
relay_aware: bool = True,
connection_page_size: int = 10,
max_union_members: int = 10,
max_connection_depth: int = 1,
)
The rules the client follows when it chooses fields for you.
When you call query() or mutation() without fields=, the client walks
the schema from the returned type and selects fields. A SelectionPolicy
sets how far it goes and what it leaves out, so that a generated query
stays small enough to be fast and safe to run.
A client builds its policy from the matching fields of ClientConfig, and
ClientConfig.selection_policy() returns it. Three fields exist only here,
and always have their defaults on a client: connection_page_size,
max_union_members and max_connection_depth. In this version a client
always builds a policy of this class. It does not use a subclass that you
write.
A policy never changes after it is built. Two policies with equal fields
behave the same, and have the same fingerprint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_depth
|
int
|
How many levels of objects the client selects. The returned
object is level 1, so |
3
|
cycle_policy
|
CyclePolicy
|
What to do when a type repeats on its own path. See
|
'shallow'
|
per_type_depth_cap
|
Mapping[str, int]
|
A stricter limit for named types, as a mapping from type name to a number of levels. It counts the levels below that type, wherever the type appears. |
dict()
|
include_deprecated
|
bool
|
Select fields that the schema marks as deprecated. |
False
|
max_fields
|
int
|
The most fields one query may select. This guards the size of the query. It does not control cost. Exceeding it raises an error that names the type and the ways to reduce the selection. |
2000
|
exclude
|
Sequence[str]
|
Patterns for fields that are never selected. Each is
|
()
|
relay_aware
|
bool
|
Treat a type whose name ends in |
True
|
connection_page_size
|
int
|
The |
10
|
max_union_members
|
int
|
How many types of one interface or union are
expanded. Types past the limit collapse to |
10
|
max_connection_depth
|
int
|
How many connections may be expanded inside each
other. |
1
|
Examples:
from pytest_graphql import SelectionPolicy
policy = SelectionPolicy(max_depth=2, exclude=["User.balance", "*.preferences"])
assert policy.max_depth == 2
assert policy.should_include("User", "name", ("name",), 0)
assert not policy.should_include("User", "balance", ("balance",), 0)
max_depth
class-attribute
instance-attribute
max_depth: int = 3
How many levels of objects the client selects. 1 means scalar fields only.
Examples:
from pytest_graphql import SelectionPolicy
assert SelectionPolicy().max_depth == 3
assert SelectionPolicy(max_depth=1).max_depth == 1
cycle_policy
class-attribute
instance-attribute
cycle_policy: CyclePolicy = 'shallow'
What to do when a type repeats on its own path. The default is "shallow".
Examples:
from pytest_graphql import SelectionPolicy
assert SelectionPolicy().cycle_policy == "shallow"
assert SelectionPolicy(cycle_policy="id_only").cycle_policy == "id_only"
per_type_depth_cap
class-attribute
instance-attribute
per_type_depth_cap: Mapping[str, int] = field(
default_factory=dict
)
Stricter depth limits by type name. Stored as a read-only mapping.
Examples:
from pytest_graphql import SelectionPolicy
policy = SelectionPolicy(per_type_depth_cap={"User": 1})
assert policy.per_type_depth_cap == {"User": 1}
include_deprecated
class-attribute
instance-attribute
include_deprecated: bool = False
Whether deprecated fields are selected. Off by default.
Examples:
from pytest_graphql import SelectionPolicy
assert SelectionPolicy().include_deprecated is False
max_fields
class-attribute
instance-attribute
max_fields: int = 2000
The most fields one generated query may select.
Examples:
from pytest_graphql import SelectionPolicy
assert SelectionPolicy().max_fields == 2000
exclude
class-attribute
instance-attribute
exclude: Sequence[str] = ()
Patterns for fields that are never selected. Stored as a tuple.
Examples:
from pytest_graphql import SelectionPolicy
policy = SelectionPolicy(exclude=["User.balance"])
assert policy.exclude == ("User.balance",)
try:
SelectionPolicy(exclude=["balance"])
except ValueError:
pass
else:
raise AssertionError("expected an error")
relay_aware
class-attribute
instance-attribute
relay_aware: bool = True
Whether Relay connection types are selected in the standard page shape.
Examples:
from pytest_graphql import SelectionPolicy
assert SelectionPolicy().relay_aware is True
connection_page_size
class-attribute
instance-attribute
connection_page_size: int = 10
The first value that the client sends for a connection field.
Examples:
from pytest_graphql import SelectionPolicy
assert SelectionPolicy().connection_page_size == 10
assert SelectionPolicy(connection_page_size=25).connection_page_size == 25
max_union_members
class-attribute
instance-attribute
max_union_members: int = 10
The most types of one interface or union that are expanded.
Examples:
from pytest_graphql import SelectionPolicy
assert SelectionPolicy().max_union_members == 10
max_connection_depth
class-attribute
instance-attribute
max_connection_depth: int = 1
How many connections may be expanded inside each other. The default is 1.
Examples:
from pytest_graphql import SelectionPolicy
assert SelectionPolicy().max_connection_depth == 1
memoizable
class-attribute
instance-attribute
memoizable: bool = field(
init=False, repr=False, compare=False, default=True
)
Whether the client may reuse a selection built under this policy.
It is decided from the class, and cannot be passed in. It is False for a
subclass that overrides should_include() and not fingerprint, because
the client cannot tell when such a policy would decide differently.
Examples:
import warnings
from pytest_graphql import SelectionPolicy
class NoSecrets(SelectionPolicy):
def should_include(self, parent_type, field_name, path, depth):
return field_name != "secret"
assert SelectionPolicy().memoizable is True
with warnings.catch_warnings():
warnings.simplefilter("ignore")
assert NoSecrets().memoizable is False
fingerprint
property
fingerprint: str
A stable text key that covers every field of this policy.
The client reuses a selection it already built when the policy has the same fingerprint. Two policies with equal fields have the same fingerprint, even when they are different classes.
A subclass whose decisions depend on more than its fields, for example on state outside the object, must override this property and return a value that covers that state too.
Examples:
from pytest_graphql import SelectionPolicy
assert SelectionPolicy(max_depth=2).fingerprint == (
SelectionPolicy(max_depth=2).fingerprint
)
assert SelectionPolicy(max_depth=2).fingerprint != (
SelectionPolicy(max_depth=3).fingerprint
)
cache_key
property
cache_key: str | None
The key the client uses to reuse selections, or None for no reuse.
It is the fingerprint, except for a policy whose memoizable is
False, which is never reused.
Examples:
from pytest_graphql import SelectionPolicy
policy = SelectionPolicy()
assert policy.cache_key == policy.fingerprint
should_include
should_include(
parent_type: str,
field_name: str,
path: tuple[str, ...],
depth: int,
) -> bool
Decide whether the client may select one field.
The client asks this for each field it considers. The default answer is
False for a field that matches an exclude pattern, and True for
every other field. A subclass can override it to decide by position. The
default ignores path and depth. A client does not use a subclass yet,
so call it on a policy that you build when you test your own rules.
Positions are measured from the start of the generated part of a query, not from the start of the whole document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parent_type
|
str
|
The name of the type that has the field. |
required |
field_name
|
str
|
The name of the field. |
required |
path
|
tuple[str, ...]
|
The field names from the start of the generated part down to this field, including it. |
required |
depth
|
int
|
How many levels of objects lie above this field. Fields at
the start of the generated part have depth |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Examples:
from pytest_graphql import SelectionPolicy
policy = SelectionPolicy(exclude=["User.*"])
assert not policy.should_include("User", "name", ("name",), 0)
assert policy.should_include("Team", "name", ("name",), 0)
depth_cap_for
depth_cap_for(type_name: str) -> int | None
The depth limit set for one type, or None when it has none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
type_name
|
str
|
The name of a schema type. |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
The limit from |
Examples:
from pytest_graphql import SelectionPolicy
policy = SelectionPolicy(per_type_depth_cap={"User": 1})
assert policy.depth_cap_for("User") == 1
assert policy.depth_cap_for("Team") is None
DerivableTransportBase
Bases: ABC
The base class for a transport that keeps state for each client.
A client that you clone with as_(), with_headers() or anonymous() shares
the transport of its parent. A transport that holds state for one client, such
as cookies or a session, must give each clone a copy of its own. Inherit this
class and write derive(), which takes no argument and returns a new
transport. A clone then closes its own transport when it closes, and never the
transport of its parent.
A transport that does not inherit this class is shared, whatever methods it
has. A method named derive on such a transport is never called.
Examples:
from pytest_graphql import DerivableTransportBase, build_client
class CountingTransport(DerivableTransportBase):
def __init__(self, inner):
self.inner = inner
self.sent = 0
def send(self, request, *, timeout):
self.sent += 1
return self.inner.send(request, timeout=timeout)
def derive(self):
return CountingTransport(self.inner)
def close(self):
pass
parent = CountingTransport(gql.transport)
client = build_client(
url="http://localhost:8000/graphql", transport=parent, schema=gql.schema
)
with client.as_("token-for-ada") as clone:
clone.query("users", fields=["id"])
assert clone.transport.sent == 1
assert parent.sent == 0
derive
abstractmethod
derive() -> Transport
Return a new transport for one clone of a client.
The new transport must not share per-client state with this one. The clone closes it when the clone closes.
Returns:
| Type | Description |
|---|---|
Transport
|
A new transport. |
Examples:
from pytest_graphql import DerivableTransportBase
class Plain(DerivableTransportBase):
def send(self, request, *, timeout):
raise NotImplementedError
def derive(self):
return Plain()
def close(self):
pass
first = Plain()
assert first.derive() is not first
RawResponse
dataclass
RawResponse(
status_code: int,
media_type: str,
data: Any,
errors: tuple[Mapping[str, Any], ...],
extensions: Mapping[str, Any] | None,
headers: Mapping[str, str],
transport_credentials: tuple[tuple[str, str], ...] = (),
)
What a transport returns: one GraphQL answer, as the server sent it.
A Transport builds one RawResponse for each request. The client turns it
into a GraphQLResponse and decides whether the answer is an error. Return
one for every answer that has a data entry, whatever the HTTP status, and
also when the answer holds errors. For anything else, such as an unparsable
body, raise an exception and do not return a RawResponse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status_code
|
int
|
The HTTP status code of the answer. |
required |
media_type
|
str
|
The media type of the answer, such as
|
required |
data
|
Any
|
The |
required |
errors
|
tuple[Mapping[str, Any], ...]
|
The |
required |
extensions
|
Mapping[str, Any] | None
|
The |
required |
headers
|
Mapping[str, str]
|
The response headers. |
required |
transport_credentials
|
tuple[tuple[str, str], ...]
|
Secrets that the transport sent or received
outside the request headers, as |
()
|
Examples:
from pytest_graphql import RawResponse
raw = RawResponse(
status_code=200,
media_type="application/json",
data={"user": {"name": "Ada Lovelace"}},
errors=(),
extensions=None,
headers={},
)
assert raw.data["user"]["name"] == "Ada Lovelace"
status_code
instance-attribute
status_code: int
The HTTP status code of the answer.
Examples:
from pytest_graphql import RawResponse
raw = RawResponse(503, "application/json", {"x": 1}, (), None, {})
assert raw.status_code == 503
media_type
instance-attribute
media_type: str
The media type of the answer.
Examples:
from pytest_graphql import RawResponse
raw = RawResponse(200, "application/json", {}, (), None, {})
assert raw.media_type == "application/json"
data
instance-attribute
data: Any
The data entry of the answer, which may be None.
Examples:
from pytest_graphql import RawResponse
raw = RawResponse(200, "application/json", None, (), None, {})
assert raw.data is None
errors
instance-attribute
errors: tuple[Mapping[str, Any], ...]
The errors list of the answer, as received. An empty tuple means none.
Examples:
from pytest_graphql import RawResponse
error = {"message": "no access"}
raw = RawResponse(200, "application/json", None, (error,), None, {})
assert raw.errors[0]["message"] == "no access"
extensions
instance-attribute
extensions: Mapping[str, Any] | None
The extensions entry of the answer, or None.
Examples:
from pytest_graphql import RawResponse
raw = RawResponse(200, "application/json", {}, (), {"cost": 3}, {})
assert raw.extensions == {"cost": 3}
headers
instance-attribute
headers: Mapping[str, str]
The response headers.
Examples:
from pytest_graphql import RawResponse
raw = RawResponse(200, "application/json", {}, (), None, {"X-Id": "1"})
assert raw.headers["X-Id"] == "1"
transport_credentials
class-attribute
instance-attribute
transport_credentials: tuple[tuple[str, str], ...] = field(
default=(), repr=False
)
Secrets that the transport handled outside the request headers.
A pair is (name, value). The client hides each value in what it shows, so a
server that repeats one in an error does not leak it. It is never shown.
Examples:
from pytest_graphql import RawResponse
raw = RawResponse(200, "application/json", {}, (), None, {})
assert raw.transport_credentials == ()
Transport
Bases: Protocol
The part of a client that talks to the server.
A client sends every request through a transport. The default is an HTTP
transport built on httpx. Any object with send and close methods of
this shape can take its place, for example one that calls an in-process
schema in a unit test, or one that wraps another transport to add logging.
A transport you give to build_client() or GraphQLClient is yours. The
client does not close it unless you pass owns_transport=True to
GraphQLClient.
Examples:
A transport that wraps another one and records each URL:
from pytest_graphql import build_client
class RecordingTransport:
def __init__(self, inner):
self.inner = inner
self.urls = []
def send(self, request, *, timeout):
self.urls.append(request.url)
return self.inner.send(request, timeout=timeout)
def close(self):
self.inner.close()
recording = RecordingTransport(gql.transport)
client = build_client(
url="http://localhost:8000/graphql",
transport=recording,
schema=gql.schema,
)
client.query("users")
assert recording.urls == ["http://localhost:8000/graphql"]
send
send(
request: RequestInfo, *, timeout: float
) -> RawResponse
Send one request and return the server's answer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
RequestInfo
|
The request to send. It carries real header and variable
values, so a transport can send them. Never print it. Use
|
required |
timeout
|
float
|
The longest this call may take, in seconds. It may be
|
required |
Returns:
| Type | Description |
|---|---|
RawResponse
|
The parsed response. It holds the HTTP status code, the media type, |
RawResponse
|
the |
RawResponse
|
response headers. A response that has no GraphQL result in it, such |
RawResponse
|
as an unparsable body, is not returned. Raise an exception instead. |
Raises:
| Type | Description |
|---|---|
Exception
|
When the request cannot be sent or the reply is not a GraphQL response. The exception reaches the caller. |
Examples:
from pytest_graphql import RequestInfo
request = RequestInfo(
operation="user",
kind="query",
document='query user { user(id: "u1") { name } }',
variables={},
headers={},
url="http://localhost:8000/graphql",
)
raw = gql.transport.send(request, timeout=5.0)
assert raw.status_code == 200
assert raw.data == {"user": {"name": "Ada Lovelace"}}
close
close() -> None
Release what the transport holds, such as open connections.
A client calls this once, when the client closes and owns the
transport. A client built by build_client() without a transport
owns the one it creates.
Examples:
from pytest_graphql import build_client
client = build_client(
url="http://localhost:8000/graphql",
schema=gql.schema,
)
assert client.owns_transport
client.close()
build_client
build_client(
*,
url: str,
transport: Transport | None = None,
cleanup: list[Closable] | None = None,
config: ClientConfig | None = None,
schema: GraphQLSchema | None = None,
schema_source: SchemaSource | None = None,
scalars: ScalarRegistry | None = None,
fake_context: FakeContext | None = None,
middleware: Sequence[Middleware] = (),
auth: Auth | None = None,
**config_options: Any,
) -> GraphQLClient
Build a client, with its transport and its schema, for use outside pytest.
This is the standalone way to get a GraphQLClient. Under pytest, the gql
fixture does the same work.
Without a transport, it creates an HTTP transport and loads the schema from
the server by the standard introspection query. You can skip that request
with schema= (a schema you have) or schema_source= (a SchemaSource that
knows where to read it). The client owns the transport it created, so close
the client when you are done. Use it in a with block.
With a transport, the function creates no pool and closes nothing. What you
give is yours to close. If no schema is given, it is loaded through that
transport.
If anything fails while the client is built, everything that was created is closed before the error leaves, and no connection is left open.
Each keyword that is not named below is a field of ClientConfig, so
build_client(url=..., headers=..., max_depth=3) works without a config
object. A keyword overrides the same field of a config that you also give.
A name that is not a field raises ArgumentError and lists the valid names, so
a misspelled option cannot leave you on a default. headers given here are
what the schema request sends, which an endpoint with a login needs. To rank a
header above Auth, set it on the returned client with with_headers().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
The URL of the GraphQL endpoint. |
required |
transport
|
Transport | None
|
What sends the requests. |
None
|
cleanup
|
list[Closable] | None
|
Internal. A list that receives each resource that this function creates. Leave it out. |
None
|
config
|
ClientConfig | None
|
The settings. Keywords below override its fields. |
None
|
schema
|
GraphQLSchema | None
|
A schema to use as it is. The schema is not loaded. |
None
|
schema_source
|
SchemaSource | None
|
Where to read the schema from. It is ignored when |
None
|
scalars
|
ScalarRegistry | None
|
The custom scalars the client knows. |
None
|
fake_context
|
FakeContext | None
|
Which test the data of |
None
|
middleware
|
Sequence[Middleware]
|
Hooks that run around every call. See |
()
|
auth
|
Auth | None
|
The identity applied to every request. See |
None
|
**config_options
|
Any
|
Fields of |
{}
|
Returns:
| Type | Description |
|---|---|
GraphQLClient
|
A new client. |
Raises:
| Type | Description |
|---|---|
ArgumentError
|
When a keyword is not a field of |
SchemaError
|
When the schema cannot be loaded or is not valid. |
Examples:
from pytest_graphql import build_client
with build_client(
url="http://localhost:8000/graphql",
transport=gql.transport,
schema=gql.schema,
max_depth=2,
) as client:
assert client.config.max_depth == 2
assert client.query("user", id="u1").name == "Ada Lovelace"
Against a real server, the call is the same without transport and
schema:
from pytest_graphql import build_client
with build_client(
url="http://localhost:8000/graphql",
headers={"Authorization": "Bearer my-token"},
) as gql:
user = gql.query("user", id="u1")
absent
absent() -> Absent
Match a field that is not in the response.
This is different from a field that is in the response with the value
null. Put it on a field: gql.expect.User(deleted_at=absent()). It takes
no argument.
Returns:
| Type | Description |
|---|---|
Absent
|
A matcher to put on a field. |
Examples:
from pytest_graphql import absent
user = gql.query("user", id="u1", fields=["id"])
assert user == gql.expect.User(name=absent())
user = gql.query("user", id="u1", fields=["id", "name"])
assert user != gql.expect.User(name=absent())
any_length
any_length() -> AnyLength
Match a list of any length, including an empty one.
Returns:
| Type | Description |
|---|---|
AnyLength
|
A matcher for a list. |
Examples:
from pytest_graphql import any_length
team = gql.query("team", id="t1")
assert team == gql.expect.Team(members=any_length())
assert gql.query("users") == any_length()
any_value
any_value() -> AnyValue
Match any value, including null, as long as the field is in the response.
Use it when a test needs a field to exist but does not care what it holds. A field that the response does not contain does not match.
Returns:
| Type | Description |
|---|---|
AnyValue
|
A matcher to put on a field. |
Examples:
from pytest_graphql import any_value
user = gql.query("user", id="u1", fields=["id", "avatar"])
assert user.avatar is None
assert user == gql.expect.User(avatar=any_value())
without_avatar = gql.query("user", id="u1", fields=["id"])
assert without_avatar != gql.expect.User(avatar=any_value())
contains
contains(*items: Any) -> Contains
Match a list that holds an element for each item, in any order.
Each item is matched to a different element, and the list may have other
elements too. An item can be a plain value, a dict, another matcher or a
helper. The matching is exact. An item that fits several elements never
takes the one element that another item needs. Duplicate items need
duplicate elements.
A very large comparison, more than a million pairs of an item and an
element, raises an error that suggests NodeList.where() to narrow the
list first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*items
|
Any
|
What the list must hold. |
()
|
Returns:
| Type | Description |
|---|---|
Contains
|
A matcher for a list. |
Examples:
from pytest_graphql import contains
users = gql.query("users")
assert users == contains(
gql.expect.User(name="Grace Hopper"),
{"name": "Ada Lovelace"},
)
assert users != contains({"name": "Ada Lovelace"}, {"name": "Ada Lovelace"})
gt
gt(bound: Any) -> Comparison
Match a value that is greater than bound.
It compares numbers, dates and strings. ISO 8601 date strings sort in time
order, so they compare correctly. A boolean is not a number. A value that
cannot be ordered against bound does not match, and does not raise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bound
|
Any
|
The value to compare with. It must be a number, a date or a string. |
required |
Returns:
| Type | Description |
|---|---|
Comparison
|
A matcher for a single value. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
Examples:
from pytest_graphql import gt
user = gql.query("user", id="u1")
assert user == gql.expect.User(joined_at=gt("2019-12-31"))
assert user != gql.expect.User(joined_at=gt("2020-01-01T00:00:00+00:00"))
gte
gte(bound: Any) -> Comparison
Match a value that is greater than or equal to bound.
It compares numbers, dates and strings. A value that cannot be ordered
against bound does not match, and does not raise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bound
|
Any
|
The value to compare with. It must be a number, a date or a string. |
required |
Returns:
| Type | Description |
|---|---|
Comparison
|
A matcher for a single value. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
Examples:
from pytest_graphql import gte
[attachment] = gql.query("search", term="diagram")
assert attachment == gql.expect.Attachment(size_bytes=gte(2048))
assert attachment != gql.expect.Attachment(size_bytes=gte(2049))
length
length(count: int) -> Length
Match a list that has exactly count elements.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
count
|
int
|
The number of elements. It must be an |
required |
Returns:
| Type | Description |
|---|---|
Length
|
A matcher for a list. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
ValueError
|
When |
Examples:
from pytest_graphql import length
team = gql.query("team", id="t1")
assert team == gql.expect.Team(members=length(2))
assert team != gql.expect.Team(members=length(3))
lt
lt(bound: Any) -> Comparison
Match a value that is less than bound.
It compares numbers, dates and strings. A value that cannot be ordered
against bound does not match, and does not raise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bound
|
Any
|
The value to compare with. It must be a number, a date or a string. |
required |
Returns:
| Type | Description |
|---|---|
Comparison
|
A matcher for a single value. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
Examples:
from pytest_graphql import lt
[attachment] = gql.query("search", term="diagram")
assert attachment == gql.expect.Attachment(size_bytes=lt(4096))
assert attachment != gql.expect.Attachment(size_bytes=lt(2048))
lte
lte(bound: Any) -> Comparison
Match a value that is less than or equal to bound.
It compares numbers, dates and strings. A value that cannot be ordered
against bound does not match, and does not raise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bound
|
Any
|
The value to compare with. It must be a number, a date or a string. |
required |
Returns:
| Type | Description |
|---|---|
Comparison
|
A matcher for a single value. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
Examples:
from pytest_graphql import lte
[attachment] = gql.query("search", term="diagram")
assert attachment == gql.expect.Attachment(size_bytes=lte(2048))
assert attachment != gql.expect.Attachment(size_bytes=lte(2047))
matches
matches(pattern: str | Pattern[str]) -> Matches
Match a string in which the regular expression finds a match.
This is a search, not a full match, so write ^ and $ when you need the
whole string to match. A value that is not a string never matches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pattern
|
str | Pattern[str]
|
A regular expression, as text or as a compiled pattern. The flags of a compiled pattern apply. |
required |
Returns:
| Type | Description |
|---|---|
Matches
|
A matcher for a string. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
Examples:
import re
from pytest_graphql import matches
user = gql.query("user", id="u1")
assert user == gql.expect.User(id=matches(r"^u[0-9]+$"))
assert user == gql.expect.User(name=matches(re.compile("lovelace", re.I)))
assert user != gql.expect.User(name=matches(r"^Grace"))
one_of
one_of(*values: Any) -> OneOf
Match a value that equals any one of values.
A boolean never equals a number here, although Python says True == 1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*values
|
Any
|
The accepted values. At least one is required. |
()
|
Returns:
| Type | Description |
|---|---|
OneOf
|
A matcher for a single value. |
Raises:
| Type | Description |
|---|---|
ValueError
|
When no value is given. |
Examples:
from pytest_graphql import one_of
user = gql.query("user", id="u1")
assert user == gql.expect.User(name=one_of("Ada Lovelace", "Grace Hopper"))
assert user != gql.expect.User(name=one_of("Grace Hopper", "Alan Turing"))
unordered
unordered(*items: Any) -> Unordered
Match a list that has the same elements as the items, in any order.
The list and the items must have the same length, and each item is matched
to a different element. An item can be a plain value, a dict, another
matcher or a helper. Duplicates on either side need duplicates on the other.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*items
|
Any
|
What the list must hold, and nothing else. |
()
|
Returns:
| Type | Description |
|---|---|
Unordered
|
A matcher for a list. |
Examples:
from pytest_graphql import unordered
names = gql.query("users").pluck("name")
assert names == unordered("Alan Turing", "Ada Lovelace", "Grace Hopper")
assert names != unordered("Alan Turing", "Ada Lovelace")
unique
unique(kind: str | None = None) -> Unique
Mark a value in a test payload that must be different on every call.
gql.fake is seeded, so it gives the same payload on every run of a test.
That breaks a test that writes the payload to a database that has a unique
constraint, because the second run repeats the first one's values. Put
unique() on the field that needs to differ. The factory replaces it with a
value that differs between calls, between runs and between parallel workers.
The marker works at any depth of an override, inside a dict or a list. The
other fields of the payload keep their seeded values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
Unique
|
A marker for the factory. It is not the final value. |
Raises:
| Type | Description |
|---|---|
ValueError
|
When |
TypeError
|
When |
Examples:
from pytest_graphql import unique
first = gql.fake.CreatePostInput(title=unique())
second = gql.fake.CreatePostInput(title=unique())
assert first["title"] != second["title"]
assert first["authorId"] == second["authorId"]
email = gql.fake.CreatePostInput(title=unique("email"))["title"]
assert email.endswith("@example.com")