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.
- schemathesis/__init__.py +28 -25
- schemathesis/auths.py +65 -24
- schemathesis/checks.py +60 -36
- schemathesis/cli/commands/run/__init__.py +23 -21
- schemathesis/cli/commands/run/context.py +6 -1
- schemathesis/cli/commands/run/events.py +7 -1
- schemathesis/cli/commands/run/executor.py +12 -7
- schemathesis/cli/commands/run/handlers/output.py +175 -80
- schemathesis/cli/commands/run/validation.py +21 -6
- schemathesis/config/__init__.py +2 -1
- schemathesis/config/_generation.py +12 -13
- schemathesis/config/_operations.py +14 -0
- schemathesis/config/_phases.py +41 -5
- schemathesis/config/_projects.py +28 -0
- schemathesis/config/_report.py +6 -2
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +49 -1
- schemathesis/core/errors.py +5 -2
- schemathesis/core/transport.py +36 -1
- schemathesis/engine/context.py +1 -0
- schemathesis/engine/errors.py +60 -1
- schemathesis/engine/events.py +10 -2
- schemathesis/engine/phases/probes.py +3 -0
- schemathesis/engine/phases/stateful/__init__.py +2 -1
- schemathesis/engine/phases/stateful/_executor.py +38 -5
- schemathesis/engine/phases/stateful/context.py +2 -2
- schemathesis/engine/phases/unit/_executor.py +36 -7
- schemathesis/generation/__init__.py +0 -3
- schemathesis/generation/case.py +1 -0
- schemathesis/generation/coverage.py +1 -1
- schemathesis/generation/hypothesis/builder.py +31 -7
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +0 -8
- schemathesis/generation/stateful/__init__.py +4 -0
- schemathesis/generation/stateful/state_machine.py +1 -0
- schemathesis/graphql/loaders.py +138 -4
- schemathesis/hooks.py +62 -35
- schemathesis/openapi/loaders.py +120 -4
- schemathesis/pytest/loaders.py +24 -0
- schemathesis/pytest/plugin.py +22 -0
- schemathesis/schemas.py +9 -6
- schemathesis/specs/graphql/scalars.py +37 -3
- schemathesis/specs/graphql/schemas.py +12 -3
- schemathesis/specs/openapi/_hypothesis.py +14 -20
- schemathesis/specs/openapi/checks.py +21 -18
- schemathesis/specs/openapi/formats.py +30 -3
- schemathesis/specs/openapi/media_types.py +44 -1
- schemathesis/specs/openapi/schemas.py +8 -2
- schemathesis/specs/openapi/stateful/__init__.py +2 -1
- schemathesis/transport/__init__.py +54 -16
- schemathesis/transport/prepare.py +31 -7
- schemathesis/transport/requests.py +9 -8
- schemathesis/transport/wsgi.py +8 -8
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +44 -90
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/RECORD +58 -60
- schemathesis/contrib/__init__.py +0 -9
- schemathesis/contrib/openapi/__init__.py +0 -9
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
- schemathesis/generation/targets.py +0 -69
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
schemathesis/graphql/loaders.py
CHANGED
@@ -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
|
-
"""
|
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
|
-
:
|
43
|
-
|
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.
|
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
|
-
|
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
|
-
:
|
143
|
-
|
144
|
-
|
145
|
-
.. code-block:: python
|
123
|
+
Args:
|
124
|
+
hook: A hook function.
|
125
|
+
name: A hook name.
|
146
126
|
|
147
|
-
|
127
|
+
Example:
|
128
|
+
```python
|
129
|
+
def filter_query(ctx, value):
|
148
130
|
...
|
149
131
|
|
150
132
|
|
151
|
-
@schema.hooks.apply(
|
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__
|
schemathesis/openapi/loaders.py
CHANGED
@@ -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
|
-
"""
|
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):
|
schemathesis/pytest/loaders.py
CHANGED
@@ -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)
|
schemathesis/pytest/plugin.py
CHANGED
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
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
|
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
|
|