schemathesis 4.0.0a11__py3-none-any.whl → 4.0.0a12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. schemathesis/__init__.py +28 -25
  2. schemathesis/auths.py +65 -24
  3. schemathesis/checks.py +60 -36
  4. schemathesis/cli/commands/run/__init__.py +23 -21
  5. schemathesis/cli/commands/run/context.py +6 -1
  6. schemathesis/cli/commands/run/events.py +7 -1
  7. schemathesis/cli/commands/run/executor.py +12 -7
  8. schemathesis/cli/commands/run/handlers/output.py +175 -80
  9. schemathesis/cli/commands/run/validation.py +21 -6
  10. schemathesis/config/__init__.py +2 -1
  11. schemathesis/config/_generation.py +12 -13
  12. schemathesis/config/_operations.py +14 -0
  13. schemathesis/config/_phases.py +41 -5
  14. schemathesis/config/_projects.py +28 -0
  15. schemathesis/config/_report.py +6 -2
  16. schemathesis/config/_warnings.py +25 -0
  17. schemathesis/config/schema.json +49 -1
  18. schemathesis/core/errors.py +5 -2
  19. schemathesis/core/transport.py +36 -1
  20. schemathesis/engine/context.py +1 -0
  21. schemathesis/engine/errors.py +60 -1
  22. schemathesis/engine/events.py +10 -2
  23. schemathesis/engine/phases/probes.py +3 -0
  24. schemathesis/engine/phases/stateful/__init__.py +2 -1
  25. schemathesis/engine/phases/stateful/_executor.py +38 -5
  26. schemathesis/engine/phases/stateful/context.py +2 -2
  27. schemathesis/engine/phases/unit/_executor.py +36 -7
  28. schemathesis/generation/__init__.py +0 -3
  29. schemathesis/generation/case.py +1 -0
  30. schemathesis/generation/coverage.py +1 -1
  31. schemathesis/generation/hypothesis/builder.py +31 -7
  32. schemathesis/generation/metrics.py +93 -0
  33. schemathesis/generation/modes.py +0 -8
  34. schemathesis/generation/stateful/__init__.py +4 -0
  35. schemathesis/generation/stateful/state_machine.py +1 -0
  36. schemathesis/graphql/loaders.py +138 -4
  37. schemathesis/hooks.py +62 -35
  38. schemathesis/openapi/loaders.py +120 -4
  39. schemathesis/pytest/loaders.py +24 -0
  40. schemathesis/pytest/plugin.py +22 -0
  41. schemathesis/schemas.py +9 -6
  42. schemathesis/specs/graphql/scalars.py +37 -3
  43. schemathesis/specs/graphql/schemas.py +12 -3
  44. schemathesis/specs/openapi/_hypothesis.py +14 -20
  45. schemathesis/specs/openapi/checks.py +21 -18
  46. schemathesis/specs/openapi/formats.py +30 -3
  47. schemathesis/specs/openapi/media_types.py +44 -1
  48. schemathesis/specs/openapi/schemas.py +8 -2
  49. schemathesis/specs/openapi/stateful/__init__.py +2 -1
  50. schemathesis/transport/__init__.py +54 -16
  51. schemathesis/transport/prepare.py +31 -7
  52. schemathesis/transport/requests.py +9 -8
  53. schemathesis/transport/wsgi.py +8 -8
  54. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +44 -90
  55. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/RECORD +58 -60
  56. schemathesis/contrib/__init__.py +0 -9
  57. schemathesis/contrib/openapi/__init__.py +0 -9
  58. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  59. schemathesis/generation/targets.py +0 -69
  60. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
  61. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
  62. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -19,6 +19,24 @@ if TYPE_CHECKING:
19
19
 
20
20
 
21
21
  def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> GraphQLSchema:
22
+ """Load GraphQL schema from an ASGI application via introspection.
23
+
24
+ Args:
25
+ path: Relative URL path to the GraphQL endpoint (e.g., "/graphql")
26
+ app: ASGI application instance
27
+ config: Custom configuration. If `None`, uses auto-discovered config
28
+ **kwargs: Additional request parameters passed to the ASGI test client.
29
+
30
+ Example:
31
+ ```python
32
+ from fastapi import FastAPI
33
+ import schemathesis
34
+
35
+ app = FastAPI()
36
+ schema = schemathesis.graphql.from_asgi("/graphql", app)
37
+ ```
38
+
39
+ """
22
40
  require_relative_url(path)
23
41
  kwargs.setdefault("json", {"query": get_introspection_query()})
24
42
  client = asgi.get_client(app)
@@ -28,6 +46,24 @@ def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None,
28
46
 
29
47
 
30
48
  def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> GraphQLSchema:
49
+ """Load GraphQL schema from a WSGI application via introspection.
50
+
51
+ Args:
52
+ path: Relative URL path to the GraphQL endpoint (e.g., "/graphql")
53
+ app: WSGI application instance
54
+ config: Custom configuration. If `None`, uses auto-discovered config
55
+ **kwargs: Additional request parameters passed to the WSGI test client.
56
+
57
+ Example:
58
+ ```python
59
+ from flask import Flask
60
+ import schemathesis
61
+
62
+ app = Flask(__name__)
63
+ schema = schemathesis.graphql.from_wsgi("/graphql", app)
64
+ ```
65
+
66
+ """
31
67
  require_relative_url(path)
32
68
  prepare_request_kwargs(kwargs)
33
69
  kwargs.setdefault("json", {"query": get_introspection_query()})
@@ -41,7 +77,31 @@ def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None,
41
77
  def from_url(
42
78
  url: str, *, config: SchemathesisConfig | None = None, wait_for_schema: float | None = None, **kwargs: Any
43
79
  ) -> GraphQLSchema:
44
- """Load from URL."""
80
+ """Load GraphQL schema from a URL via introspection query.
81
+
82
+ Args:
83
+ url: Full URL to the GraphQL endpoint
84
+ config: Custom configuration. If `None`, uses auto-discovered config
85
+ wait_for_schema: Maximum time in seconds to wait for schema availability
86
+ **kwargs: Additional parameters passed to `requests.post()` (headers, timeout, auth, etc.).
87
+
88
+ Example:
89
+ ```python
90
+ import schemathesis
91
+
92
+ # Basic usage
93
+ schema = schemathesis.graphql.from_url("https://api.example.com/graphql")
94
+
95
+ # With authentication and timeout
96
+ schema = schemathesis.graphql.from_url(
97
+ "https://api.example.com/graphql",
98
+ headers={"Authorization": "Bearer token"},
99
+ timeout=30,
100
+ wait_for_schema=10.0
101
+ )
102
+ ```
103
+
104
+ """
45
105
  import requests
46
106
 
47
107
  kwargs.setdefault("json", {"query": get_introspection_query()})
@@ -53,13 +113,55 @@ def from_url(
53
113
  def from_path(
54
114
  path: PathLike | str, *, config: SchemathesisConfig | None = None, encoding: str = "utf-8"
55
115
  ) -> GraphQLSchema:
56
- """Load from a filesystem path."""
116
+ """Load GraphQL schema from a filesystem path.
117
+
118
+ Args:
119
+ path: File path to the GraphQL schema file (.graphql, .gql)
120
+ config: Custom configuration. If `None`, uses auto-discovered config
121
+ encoding: Text encoding for reading the file
122
+
123
+ Example:
124
+ ```python
125
+ import schemathesis
126
+
127
+ # Load from GraphQL SDL file
128
+ schema = schemathesis.graphql.from_path("./schema.graphql")
129
+ ```
130
+
131
+ """
57
132
  with open(path, encoding=encoding) as file:
58
133
  return from_file(file=file, config=config).configure(location=Path(path).absolute().as_uri())
59
134
 
60
135
 
61
136
  def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None) -> GraphQLSchema:
62
- """Load from file-like object or string."""
137
+ """Load GraphQL schema from a file-like object or string.
138
+
139
+ Args:
140
+ file: File-like object or raw string containing GraphQL SDL
141
+ config: Custom configuration. If `None`, uses auto-discovered config
142
+
143
+ Example:
144
+ ```python
145
+ import schemathesis
146
+
147
+ # From GraphQL SDL string
148
+ schema_sdl = '''
149
+ type Query {
150
+ user(id: ID!): User
151
+ }
152
+ type User {
153
+ id: ID!
154
+ name: String!
155
+ }
156
+ '''
157
+ schema = schemathesis.graphql.from_file(schema_sdl)
158
+
159
+ # From file object
160
+ with open("schema.graphql") as f:
161
+ schema = schemathesis.graphql.from_file(f)
162
+ ```
163
+
164
+ """
63
165
  import graphql
64
166
 
65
167
  if isinstance(file, str):
@@ -87,7 +189,39 @@ def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None)
87
189
 
88
190
 
89
191
  def from_dict(schema: dict[str, Any], *, config: SchemathesisConfig | None = None) -> GraphQLSchema:
90
- """Base loader that others build upon."""
192
+ """Load GraphQL schema from a dictionary containing introspection result.
193
+
194
+ Args:
195
+ schema: Dictionary containing GraphQL introspection result or wrapped in 'data' key
196
+ config: Custom configuration. If `None`, uses auto-discovered config
197
+
198
+ Example:
199
+ ```python
200
+ import schemathesis
201
+
202
+ # From introspection result
203
+ introspection = {
204
+ "__schema": {
205
+ "types": [...],
206
+ "queryType": {"name": "Query"},
207
+ # ... rest of introspection result
208
+ }
209
+ }
210
+ schema = schemathesis.graphql.from_dict(introspection)
211
+
212
+ # From GraphQL response format (with 'data' wrapper)
213
+ response_data = {
214
+ "data": {
215
+ "__schema": {
216
+ "types": [...],
217
+ "queryType": {"name": "Query"}
218
+ }
219
+ }
220
+ }
221
+ schema = schemathesis.graphql.from_dict(response_data)
222
+ ```
223
+
224
+ """
91
225
  from schemathesis.specs.graphql.schemas import GraphQLSchema
92
226
 
93
227
  if "data" in schema:
schemathesis/hooks.py CHANGED
@@ -37,13 +37,15 @@ class RegisteredHook:
37
37
 
38
38
  @dataclass
39
39
  class HookContext:
40
- """A context that is passed to some hook functions.
40
+ """A context that is passed to some hook functions."""
41
41
 
42
- :ivar Optional[APIOperation] operation: API operation that is currently being processed.
43
- Might be absent in some cases.
44
- """
42
+ operation: APIOperation | None
43
+ """API operation that is currently being processed."""
44
+
45
+ __slots__ = ("operation",)
45
46
 
46
- operation: APIOperation | None = None
47
+ def __init__(self, *, operation: APIOperation | None = None) -> None:
48
+ self.operation = operation
47
49
 
48
50
 
49
51
  def to_filterable_hook(dispatcher: HookDispatcher) -> Callable:
@@ -110,48 +112,29 @@ class HookDispatcher:
110
112
  _specs: ClassVar[dict[str, RegisteredHook]] = {}
111
113
 
112
114
  def __post_init__(self) -> None:
113
- self.register = to_filterable_hook(self) # type: ignore[method-assign]
114
-
115
- def register(self, hook: str | Callable) -> Callable:
116
- """Register a new hook.
117
-
118
- :param hook: Either a hook function or a string.
119
-
120
- Can be used as a decorator in two forms.
121
- Without arguments for registering hooks and autodetecting their names:
122
-
123
- .. code-block:: python
124
-
125
- @schemathesis.hook
126
- def before_generate_query(context, strategy):
127
- ...
128
-
129
- With a hook name as the first argument:
130
-
131
- .. code-block:: python
115
+ self.hook = to_filterable_hook(self) # type: ignore[method-assign]
132
116
 
133
- @schemathesis.hook("before_generate_query")
134
- def hook(context, strategy):
135
- ...
136
- """
117
+ def hook(self, hook: str | Callable) -> Callable:
137
118
  raise NotImplementedError
138
119
 
139
120
  def apply(self, hook: Callable, *, name: str | None = None) -> Callable[[Callable], Callable]:
140
121
  """Register hook to run only on one test function.
141
122
 
142
- :param hook: A hook function.
143
- :param Optional[str] name: A hook name.
144
-
145
- .. code-block:: python
123
+ Args:
124
+ hook: A hook function.
125
+ name: A hook name.
146
126
 
147
- def before_generate_query(context, strategy):
127
+ Example:
128
+ ```python
129
+ def filter_query(ctx, value):
148
130
  ...
149
131
 
150
132
 
151
- @schema.hooks.apply(before_generate_query)
133
+ @schema.hooks.apply(filter_query)
152
134
  @schema.parametrize()
153
135
  def test_api(case):
154
136
  ...
137
+ ```
155
138
 
156
139
  """
157
140
  if name is None:
@@ -391,6 +374,50 @@ def after_call(context: HookContext, case: Case, response: Response) -> None:
391
374
  GLOBAL_HOOK_DISPATCHER = HookDispatcher(scope=HookScope.GLOBAL)
392
375
  dispatch = GLOBAL_HOOK_DISPATCHER.dispatch
393
376
  get_all_by_name = GLOBAL_HOOK_DISPATCHER.get_all_by_name
394
- register = GLOBAL_HOOK_DISPATCHER.register
395
377
  unregister = GLOBAL_HOOK_DISPATCHER.unregister
396
378
  unregister_all = GLOBAL_HOOK_DISPATCHER.unregister_all
379
+
380
+
381
+ def hook(hook: str | Callable) -> Callable:
382
+ """Register a new hook.
383
+
384
+ Args:
385
+ hook: Either a hook function (autodetecting its name) or a string matching one of the supported hook names.
386
+
387
+ Example:
388
+ Can be used as a decorator in two ways:
389
+
390
+ 1. Without arguments (auto-detect the hook name from the function name):
391
+
392
+ ```python
393
+ @schemathesis.hook
394
+ def filter_query(ctx, query):
395
+ \"\"\"Skip cases where query is None or invalid\"\"\"
396
+ return query and "user_id" in query
397
+
398
+ @schemathesis.hook
399
+ def before_call(ctx, case):
400
+ \"\"\"Modify headers before sending each request\"\"\"
401
+ if case.headers is None:
402
+ case.headers = {}
403
+ case.headers["X-Test-Mode"] = "true"
404
+ return None
405
+ ```
406
+
407
+ 2. With an explicit hook name as the first argument:
408
+
409
+ ```python
410
+ @schemathesis.hook("map_headers")
411
+ def add_custom_header(ctx, headers):
412
+ \"\"\"Inject a test header into every request\"\"\"
413
+ if headers is None:
414
+ headers = {}
415
+ headers["X-Custom"] = "value"
416
+ return headers
417
+ ```
418
+
419
+ """
420
+ return GLOBAL_HOOK_DISPATCHER.hook(hook)
421
+
422
+
423
+ hook.__dict__ = GLOBAL_HOOK_DISPATCHER.hook.__dict__
@@ -20,6 +20,24 @@ if TYPE_CHECKING:
20
20
 
21
21
 
22
22
  def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
23
+ """Load OpenAPI schema from an ASGI application.
24
+
25
+ Args:
26
+ path: Relative URL path to the OpenAPI schema endpoint (e.g., "/openapi.json")
27
+ app: ASGI application instance
28
+ config: Custom configuration. If `None`, uses auto-discovered config
29
+ **kwargs: Additional request parameters passed to the ASGI test client
30
+
31
+ Example:
32
+ ```python
33
+ from fastapi import FastAPI
34
+ import schemathesis
35
+
36
+ app = FastAPI()
37
+ schema = schemathesis.openapi.from_asgi("/openapi.json", app)
38
+ ```
39
+
40
+ """
23
41
  require_relative_url(path)
24
42
  client = asgi.get_client(app)
25
43
  response = load_from_url(client.get, url=path, **kwargs)
@@ -29,6 +47,24 @@ def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None,
29
47
 
30
48
 
31
49
  def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
50
+ """Load OpenAPI schema from a WSGI application.
51
+
52
+ Args:
53
+ path: Relative URL path to the OpenAPI schema endpoint (e.g., "/openapi.json")
54
+ app: WSGI application instance
55
+ config: Custom configuration. If `None`, uses auto-discovered config
56
+ **kwargs: Additional request parameters passed to the WSGI test client
57
+
58
+ Example:
59
+ ```python
60
+ from flask import Flask
61
+ import schemathesis
62
+
63
+ app = Flask(__name__)
64
+ schema = schemathesis.openapi.from_wsgi("/openapi.json", app)
65
+ ```
66
+
67
+ """
32
68
  require_relative_url(path)
33
69
  prepare_request_kwargs(kwargs)
34
70
  client = wsgi.get_client(app)
@@ -42,7 +78,31 @@ def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None,
42
78
  def from_url(
43
79
  url: str, *, config: SchemathesisConfig | None = None, wait_for_schema: float | None = None, **kwargs: Any
44
80
  ) -> BaseOpenAPISchema:
45
- """Load from URL."""
81
+ """Load OpenAPI schema from a URL.
82
+
83
+ Args:
84
+ url: Full URL to the OpenAPI schema
85
+ config: Custom configuration. If `None`, uses auto-discovered config
86
+ wait_for_schema: Maximum time in seconds to wait for schema availability
87
+ **kwargs: Additional parameters passed to `requests.get()` (headers, timeout, auth, etc.)
88
+
89
+ Example:
90
+ ```python
91
+ import schemathesis
92
+
93
+ # Basic usage
94
+ schema = schemathesis.openapi.from_url("https://api.example.com/openapi.json")
95
+
96
+ # With authentication and timeout
97
+ schema = schemathesis.openapi.from_url(
98
+ "https://api.example.com/openapi.json",
99
+ headers={"Authorization": "Bearer token"},
100
+ timeout=30,
101
+ wait_for_schema=10.0
102
+ )
103
+ ```
104
+
105
+ """
46
106
  import requests
47
107
 
48
108
  response = load_from_url(requests.get, url=url, wait_for_schema=wait_for_schema, **kwargs)
@@ -54,7 +114,25 @@ def from_url(
54
114
  def from_path(
55
115
  path: PathLike | str, *, config: SchemathesisConfig | None = None, encoding: str = "utf-8"
56
116
  ) -> BaseOpenAPISchema:
57
- """Load from a filesystem path."""
117
+ """Load OpenAPI schema from a filesystem path.
118
+
119
+ Args:
120
+ path: File path to the OpenAPI schema (supports JSON / YAML)
121
+ config: Custom configuration. If `None`, uses auto-discovered config
122
+ encoding: Text encoding for reading the file
123
+
124
+ Example:
125
+ ```python
126
+ import schemathesis
127
+
128
+ # Load from file
129
+ schema = schemathesis.openapi.from_path("./specs/openapi.yaml")
130
+
131
+ # With custom encoding
132
+ schema = schemathesis.openapi.from_path("./specs/openapi.json", encoding="cp1252")
133
+ ```
134
+
135
+ """
58
136
  with open(path, encoding=encoding) as file:
59
137
  content_type = detect_content_type(headers=None, path=str(path))
60
138
  schema = load_content(file.read(), content_type)
@@ -62,7 +140,26 @@ def from_path(
62
140
 
63
141
 
64
142
  def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
65
- """Load from file-like object or string."""
143
+ """Load OpenAPI schema from a file-like object or string.
144
+
145
+ Args:
146
+ file: File-like object or raw string containing the OpenAPI schema
147
+ config: Custom configuration. If `None`, uses auto-discovered config
148
+
149
+ Example:
150
+ ```python
151
+ import schemathesis
152
+
153
+ # From string
154
+ schema_content = '{"openapi": "3.0.0", "info": {"title": "API"}}'
155
+ schema = schemathesis.openapi.from_file(schema_content)
156
+
157
+ # From file object
158
+ with open("openapi.yaml") as f:
159
+ schema = schemathesis.openapi.from_file(f)
160
+ ```
161
+
162
+ """
66
163
  if isinstance(file, str):
67
164
  data = file
68
165
  else:
@@ -75,7 +172,26 @@ def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None)
75
172
 
76
173
 
77
174
  def from_dict(schema: dict[str, Any], *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
78
- """Base loader that others build upon."""
175
+ """Load OpenAPI schema from a dictionary.
176
+
177
+ Args:
178
+ schema: Dictionary containing the parsed OpenAPI schema
179
+ config: Custom configuration. If `None`, uses auto-discovered config
180
+
181
+ Example:
182
+ ```python
183
+ import schemathesis
184
+
185
+ schema_dict = {
186
+ "openapi": "3.0.0",
187
+ "info": {"title": "My API", "version": "1.0.0"},
188
+ "paths": {"/users": {"get": {"responses": {"200": {"description": "OK"}}}}}
189
+ }
190
+
191
+ schema = schemathesis.openapi.from_dict(schema_dict)
192
+ ```
193
+
194
+ """
79
195
  from schemathesis.specs.openapi.schemas import OpenApi30, SwaggerV20
80
196
 
81
197
  if not isinstance(schema, dict):
@@ -7,6 +7,30 @@ if TYPE_CHECKING:
7
7
 
8
8
 
9
9
  def from_fixture(name: str) -> LazySchema:
10
+ """Create a lazy schema loader that resolves a pytest fixture at test runtime.
11
+
12
+ Args:
13
+ name: Name of the pytest fixture that returns a schema object
14
+
15
+ Example:
16
+ ```python
17
+ import pytest
18
+ import schemathesis
19
+
20
+ @pytest.fixture
21
+ def api_schema():
22
+ return schemathesis.openapi.from_url("https://api.example.com/openapi.json")
23
+
24
+ # Create lazy schema from fixture
25
+ schema = schemathesis.pytest.from_fixture("api_schema")
26
+
27
+ # Use with parametrize to generate tests
28
+ @schema.parametrize()
29
+ def test_api(case):
30
+ case.call_and_validate()
31
+ ```
32
+
33
+ """
10
34
  from schemathesis.pytest.lazy import LazySchema
11
35
 
12
36
  return LazySchema(name)
@@ -21,7 +21,9 @@ from schemathesis.core.errors import (
21
21
  InvalidRegexPattern,
22
22
  InvalidSchema,
23
23
  SerializationNotPossible,
24
+ format_exception,
24
25
  )
26
+ from schemathesis.core.failures import FailureGroup
25
27
  from schemathesis.core.marks import Mark
26
28
  from schemathesis.core.result import Ok, Result
27
29
  from schemathesis.generation.hypothesis.given import (
@@ -247,6 +249,26 @@ def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -
247
249
  outcome.get_result()
248
250
 
249
251
 
252
+ @pytest.hookimpl(tryfirst=True) # type: ignore[misc]
253
+ def pytest_exception_interact(node: Function, call: pytest.CallInfo, report: pytest.TestReport) -> None:
254
+ if call.excinfo and call.excinfo.type is FailureGroup:
255
+ tb_entries = list(call.excinfo.traceback)
256
+ total_frames = len(tb_entries)
257
+
258
+ # Keep internal Schemathesis frames + one extra one from the caller
259
+ keep_from_index = 0
260
+ for i in range(total_frames - 1, -1, -1):
261
+ entry = tb_entries[i]
262
+
263
+ if "validate_response" in str(entry):
264
+ keep_from_index = max(0, i - 1)
265
+ break
266
+
267
+ skip_frames = keep_from_index
268
+
269
+ report.longrepr = "".join(format_exception(call.excinfo.value, with_traceback=True, skip_frames=skip_frames))
270
+
271
+
250
272
  @hookimpl(wrapper=True)
251
273
  def pytest_pyfunc_call(pyfuncitem): # type:ignore
252
274
  """It is possible to have a Hypothesis exception in runtime.
schemathesis/schemas.py CHANGED
@@ -211,7 +211,7 @@ class BaseSchema(Mapping):
211
211
  return self.statistic.operations.total
212
212
 
213
213
  def hook(self, hook: str | Callable) -> Callable:
214
- return self.hooks.register(hook)
214
+ return self.hooks.hook(hook)
215
215
 
216
216
  def get_full_path(self, path: str) -> str:
217
217
  """Compute full path for the given path."""
@@ -368,7 +368,7 @@ class BaseSchema(Mapping):
368
368
  operation: APIOperation,
369
369
  hooks: HookDispatcher | None = None,
370
370
  auth_storage: AuthStorage | None = None,
371
- generation_mode: GenerationMode = GenerationMode.default(),
371
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
372
372
  **kwargs: Any,
373
373
  ) -> SearchStrategy:
374
374
  raise NotImplementedError
@@ -396,7 +396,7 @@ class BaseSchema(Mapping):
396
396
  self,
397
397
  hooks: HookDispatcher | None = None,
398
398
  auth_storage: AuthStorage | None = None,
399
- generation_mode: GenerationMode = GenerationMode.default(),
399
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
400
400
  **kwargs: Any,
401
401
  ) -> SearchStrategy:
402
402
  """Build a strategy for generating test cases for all defined API operations."""
@@ -424,6 +424,9 @@ class BaseSchema(Mapping):
424
424
  self.app = app
425
425
  return self
426
426
 
427
+ def find_operation_by_label(self, label: str) -> APIOperation | None:
428
+ raise NotImplementedError
429
+
427
430
 
428
431
  @dataclass
429
432
  class APIOperationMap(Mapping):
@@ -443,7 +446,7 @@ class APIOperationMap(Mapping):
443
446
  self,
444
447
  hooks: HookDispatcher | None = None,
445
448
  auth_storage: AuthStorage | None = None,
446
- generation_mode: GenerationMode = GenerationMode.default(),
449
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
447
450
  **kwargs: Any,
448
451
  ) -> SearchStrategy:
449
452
  """Build a strategy for generating test cases for all API operations defined in this subset."""
@@ -643,14 +646,14 @@ class APIOperation(Generic[P]):
643
646
  self,
644
647
  hooks: HookDispatcher | None = None,
645
648
  auth_storage: AuthStorage | None = None,
646
- generation_mode: GenerationMode = GenerationMode.default(),
649
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
647
650
  **kwargs: Any,
648
651
  ) -> SearchStrategy[Case]:
649
652
  """Turn this API operation into a Hypothesis strategy."""
650
653
  strategy = self.schema.get_case_strategy(self, hooks, auth_storage, generation_mode, **kwargs)
651
654
 
652
655
  def _apply_hooks(dispatcher: HookDispatcher, _strategy: SearchStrategy[Case]) -> SearchStrategy[Case]:
653
- context = HookContext(self)
656
+ context = HookContext(operation=self)
654
657
  for hook in dispatcher.get_all_by_name("before_generate_case"):
655
658
  _strategy = hook(context, _strategy)
656
659
  for hook in dispatcher.get_all_by_name("filter_case"):
@@ -13,10 +13,44 @@ CUSTOM_SCALARS: dict[str, st.SearchStrategy[graphql.ValueNode]] = {}
13
13
 
14
14
 
15
15
  def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
16
- """Register a new strategy for generating custom scalars.
16
+ r"""Register a custom Hypothesis strategy for generating GraphQL scalar values.
17
+
18
+ Args:
19
+ name: Scalar name that matches your GraphQL schema scalar definition
20
+ strategy: Hypothesis strategy that generates GraphQL AST ValueNode objects
21
+
22
+ Example:
23
+ ```python
24
+ import schemathesis
25
+ from hypothesis import strategies as st
26
+ from schemathesis.graphql import nodes
27
+
28
+ # Register email scalar
29
+ schemathesis.graphql.scalar("Email", st.emails().map(nodes.String))
30
+
31
+ # Register positive integer scalar
32
+ schemathesis.graphql.scalar(
33
+ "PositiveInt",
34
+ st.integers(min_value=1).map(nodes.Int)
35
+ )
36
+
37
+ # Register phone number scalar
38
+ schemathesis.graphql.scalar(
39
+ "Phone",
40
+ st.from_regex(r"\+1-\d{3}-\d{3}-\d{4}").map(nodes.String)
41
+ )
42
+ ```
43
+
44
+ Schema usage:
45
+ ```graphql
46
+ scalar Email
47
+ scalar PositiveInt
48
+
49
+ type Query {
50
+ getUser(email: Email!, rating: PositiveInt!): User
51
+ }
52
+ ```
17
53
 
18
- :param str name: Scalar name. It should correspond the one used in the schema.
19
- :param strategy: Hypothesis strategy you'd like to use to generate values for this scalar.
20
54
  """
21
55
  from hypothesis.strategies import SearchStrategy
22
56