schemathesis 3.21.2__py3-none-any.whl → 3.22.1__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 +1 -1
- schemathesis/_compat.py +2 -18
- schemathesis/_dependency_versions.py +1 -6
- schemathesis/_hypothesis.py +15 -12
- schemathesis/_lazy_import.py +3 -2
- schemathesis/_xml.py +12 -11
- schemathesis/auths.py +88 -81
- schemathesis/checks.py +4 -4
- schemathesis/cli/__init__.py +202 -171
- schemathesis/cli/callbacks.py +29 -32
- schemathesis/cli/cassettes.py +25 -25
- schemathesis/cli/context.py +18 -12
- schemathesis/cli/junitxml.py +2 -2
- schemathesis/cli/options.py +10 -11
- schemathesis/cli/output/default.py +64 -34
- schemathesis/code_samples.py +10 -10
- schemathesis/constants.py +1 -1
- schemathesis/contrib/unique_data.py +2 -2
- schemathesis/exceptions.py +55 -42
- schemathesis/extra/_aiohttp.py +2 -2
- schemathesis/extra/_flask.py +2 -2
- schemathesis/extra/_server.py +3 -2
- schemathesis/extra/pytest_plugin.py +10 -10
- schemathesis/failures.py +16 -16
- schemathesis/filters.py +40 -41
- schemathesis/fixups/__init__.py +4 -3
- schemathesis/fixups/fast_api.py +5 -4
- schemathesis/generation/__init__.py +16 -4
- schemathesis/hooks.py +25 -25
- schemathesis/internal/jsonschema.py +4 -3
- schemathesis/internal/transformation.py +3 -2
- schemathesis/lazy.py +39 -31
- schemathesis/loaders.py +8 -8
- schemathesis/models.py +128 -126
- schemathesis/parameters.py +6 -5
- schemathesis/runner/__init__.py +107 -81
- schemathesis/runner/events.py +37 -26
- schemathesis/runner/impl/core.py +86 -81
- schemathesis/runner/impl/solo.py +19 -15
- schemathesis/runner/impl/threadpool.py +40 -22
- schemathesis/runner/serialization.py +67 -40
- schemathesis/sanitization.py +18 -20
- schemathesis/schemas.py +83 -72
- schemathesis/serializers.py +39 -30
- schemathesis/service/ci.py +20 -21
- schemathesis/service/client.py +29 -9
- schemathesis/service/constants.py +1 -0
- schemathesis/service/events.py +2 -2
- schemathesis/service/hosts.py +8 -7
- schemathesis/service/metadata.py +5 -0
- schemathesis/service/models.py +22 -4
- schemathesis/service/report.py +15 -15
- schemathesis/service/serialization.py +23 -27
- schemathesis/service/usage.py +8 -7
- schemathesis/specs/graphql/loaders.py +31 -24
- schemathesis/specs/graphql/nodes.py +3 -2
- schemathesis/specs/graphql/scalars.py +26 -2
- schemathesis/specs/graphql/schemas.py +38 -34
- schemathesis/specs/openapi/_hypothesis.py +62 -44
- schemathesis/specs/openapi/checks.py +10 -10
- schemathesis/specs/openapi/converter.py +10 -9
- schemathesis/specs/openapi/definitions.py +2 -2
- schemathesis/specs/openapi/examples.py +22 -21
- schemathesis/specs/openapi/expressions/nodes.py +5 -4
- schemathesis/specs/openapi/expressions/parser.py +7 -6
- schemathesis/specs/openapi/filters.py +6 -6
- schemathesis/specs/openapi/formats.py +2 -2
- schemathesis/specs/openapi/links.py +19 -21
- schemathesis/specs/openapi/loaders.py +133 -78
- schemathesis/specs/openapi/negative/__init__.py +16 -11
- schemathesis/specs/openapi/negative/mutations.py +11 -10
- schemathesis/specs/openapi/parameters.py +20 -19
- schemathesis/specs/openapi/references.py +21 -20
- schemathesis/specs/openapi/schemas.py +97 -84
- schemathesis/specs/openapi/security.py +25 -24
- schemathesis/specs/openapi/serialization.py +20 -23
- schemathesis/specs/openapi/stateful/__init__.py +12 -11
- schemathesis/specs/openapi/stateful/links.py +7 -7
- schemathesis/specs/openapi/utils.py +4 -3
- schemathesis/specs/openapi/validation.py +3 -2
- schemathesis/stateful/__init__.py +15 -16
- schemathesis/stateful/state_machine.py +9 -9
- schemathesis/targets.py +3 -3
- schemathesis/throttling.py +2 -2
- schemathesis/transports/auth.py +2 -2
- schemathesis/transports/content_types.py +5 -0
- schemathesis/transports/headers.py +3 -2
- schemathesis/transports/responses.py +1 -1
- schemathesis/utils.py +7 -10
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
- schemathesis-3.22.1.dist-info/RECORD +130 -0
- schemathesis-3.21.2.dist-info/RECORD +0 -130
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/cli/callbacks.py
CHANGED
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import re
|
|
5
5
|
import traceback
|
|
6
6
|
from contextlib import contextmanager
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Generator, TYPE_CHECKING
|
|
8
8
|
from urllib.parse import urlparse
|
|
9
9
|
|
|
10
10
|
import click
|
|
@@ -77,7 +77,7 @@ class SchemaInputKind(enum.Enum):
|
|
|
77
77
|
NAME = 4
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def parse_schema_kind(schema: str, app:
|
|
80
|
+
def parse_schema_kind(schema: str, app: str | None) -> SchemaInputKind:
|
|
81
81
|
"""Detect what kind the input schema is."""
|
|
82
82
|
try:
|
|
83
83
|
netloc = urlparse(schema).netloc
|
|
@@ -99,21 +99,20 @@ def validate_schema(
|
|
|
99
99
|
schema: str,
|
|
100
100
|
kind: SchemaInputKind,
|
|
101
101
|
*,
|
|
102
|
-
base_url:
|
|
102
|
+
base_url: str | None,
|
|
103
103
|
dry_run: bool,
|
|
104
|
-
app:
|
|
105
|
-
api_name:
|
|
104
|
+
app: str | None,
|
|
105
|
+
api_name: str | None,
|
|
106
106
|
) -> None:
|
|
107
107
|
if kind == SchemaInputKind.URL:
|
|
108
108
|
validate_url(schema)
|
|
109
109
|
if kind == SchemaInputKind.PATH:
|
|
110
|
-
|
|
111
|
-
if app is None and base_url is None and not dry_run:
|
|
110
|
+
if app is None:
|
|
112
111
|
if not file_exists(schema):
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
raise click.UsageError(FILE_DOES_NOT_EXIST_MESSAGE)
|
|
113
|
+
# Base URL is required if it is not a dry run
|
|
114
|
+
if base_url is None and not dry_run:
|
|
115
|
+
raise click.UsageError(MISSING_BASE_URL_MESSAGE)
|
|
117
116
|
if kind == SchemaInputKind.NAME:
|
|
118
117
|
if api_name is not None:
|
|
119
118
|
raise click.UsageError(f"Got unexpected extra argument ({api_name})")
|
|
@@ -138,9 +137,7 @@ def validate_base_url(ctx: click.core.Context, param: click.core.Parameter, raw_
|
|
|
138
137
|
return raw_value
|
|
139
138
|
|
|
140
139
|
|
|
141
|
-
def validate_rate_limit(
|
|
142
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]
|
|
143
|
-
) -> Optional[str]:
|
|
140
|
+
def validate_rate_limit(ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None) -> str | None:
|
|
144
141
|
if raw_value is None:
|
|
145
142
|
return raw_value
|
|
146
143
|
try:
|
|
@@ -150,7 +147,7 @@ def validate_rate_limit(
|
|
|
150
147
|
raise click.UsageError(exc.args[0]) from exc
|
|
151
148
|
|
|
152
149
|
|
|
153
|
-
def validate_app(ctx: click.core.Context, param: click.core.Parameter, raw_value:
|
|
150
|
+
def validate_app(ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None) -> str | None:
|
|
154
151
|
if raw_value is None:
|
|
155
152
|
return raw_value
|
|
156
153
|
try:
|
|
@@ -176,8 +173,8 @@ def validate_app(ctx: click.core.Context, param: click.core.Parameter, raw_value
|
|
|
176
173
|
|
|
177
174
|
|
|
178
175
|
def validate_hypothesis_database(
|
|
179
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value:
|
|
180
|
-
) ->
|
|
176
|
+
ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
|
|
177
|
+
) -> str | None:
|
|
181
178
|
if raw_value is None:
|
|
182
179
|
return raw_value
|
|
183
180
|
if ctx.params.get("hypothesis_derandomize"):
|
|
@@ -186,8 +183,8 @@ def validate_hypothesis_database(
|
|
|
186
183
|
|
|
187
184
|
|
|
188
185
|
def validate_auth(
|
|
189
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value:
|
|
190
|
-
) ->
|
|
186
|
+
ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
|
|
187
|
+
) -> tuple[str, str] | None:
|
|
191
188
|
if raw_value is not None:
|
|
192
189
|
with reraise_format_error(raw_value):
|
|
193
190
|
user, password = tuple(raw_value.split(":"))
|
|
@@ -202,8 +199,8 @@ def validate_auth(
|
|
|
202
199
|
|
|
203
200
|
|
|
204
201
|
def validate_headers(
|
|
205
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value:
|
|
206
|
-
) ->
|
|
202
|
+
ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]
|
|
203
|
+
) -> dict[str, str]:
|
|
207
204
|
headers = {}
|
|
208
205
|
for header in raw_value:
|
|
209
206
|
with reraise_format_error(header):
|
|
@@ -222,7 +219,7 @@ def validate_headers(
|
|
|
222
219
|
return headers
|
|
223
220
|
|
|
224
221
|
|
|
225
|
-
def validate_regex(ctx: click.core.Context, param: click.core.Parameter, raw_value:
|
|
222
|
+
def validate_regex(ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]) -> tuple[str, ...]:
|
|
226
223
|
for value in raw_value:
|
|
227
224
|
try:
|
|
228
225
|
re.compile(value)
|
|
@@ -232,8 +229,8 @@ def validate_regex(ctx: click.core.Context, param: click.core.Parameter, raw_val
|
|
|
232
229
|
|
|
233
230
|
|
|
234
231
|
def validate_request_cert_key(
|
|
235
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value:
|
|
236
|
-
) ->
|
|
232
|
+
ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
|
|
233
|
+
) -> str | None:
|
|
237
234
|
if raw_value is not None and "request_cert" not in ctx.params:
|
|
238
235
|
raise click.UsageError(MISSING_REQUEST_CERT_MESSAGE)
|
|
239
236
|
return raw_value
|
|
@@ -246,8 +243,8 @@ def validate_preserve_exact_body_bytes(ctx: click.core.Context, param: click.cor
|
|
|
246
243
|
|
|
247
244
|
|
|
248
245
|
def convert_verbosity(
|
|
249
|
-
ctx: click.core.Context, param: click.core.Parameter, value:
|
|
250
|
-
) ->
|
|
246
|
+
ctx: click.core.Context, param: click.core.Parameter, value: str | None
|
|
247
|
+
) -> hypothesis.Verbosity | None:
|
|
251
248
|
import hypothesis
|
|
252
249
|
|
|
253
250
|
if value is None:
|
|
@@ -255,15 +252,15 @@ def convert_verbosity(
|
|
|
255
252
|
return hypothesis.Verbosity[value]
|
|
256
253
|
|
|
257
254
|
|
|
258
|
-
def convert_stateful(ctx: click.core.Context, param: click.core.Parameter, value: str) ->
|
|
255
|
+
def convert_stateful(ctx: click.core.Context, param: click.core.Parameter, value: str) -> Stateful | None:
|
|
259
256
|
if value == "none":
|
|
260
257
|
return None
|
|
261
258
|
return Stateful[value]
|
|
262
259
|
|
|
263
260
|
|
|
264
261
|
def convert_experimental(
|
|
265
|
-
ctx: click.core.Context, param: click.core.Parameter, value:
|
|
266
|
-
) ->
|
|
262
|
+
ctx: click.core.Context, param: click.core.Parameter, value: tuple[str, ...]
|
|
263
|
+
) -> list[experimental.Experiment]:
|
|
267
264
|
return [
|
|
268
265
|
feature
|
|
269
266
|
for feature in experimental.GLOBAL_EXPERIMENTS.available
|
|
@@ -271,7 +268,7 @@ def convert_experimental(
|
|
|
271
268
|
]
|
|
272
269
|
|
|
273
270
|
|
|
274
|
-
def convert_checks(ctx: click.core.Context, param: click.core.Parameter, value:
|
|
271
|
+
def convert_checks(ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]]) -> list[str]:
|
|
275
272
|
return sum(value, [])
|
|
276
273
|
|
|
277
274
|
|
|
@@ -281,7 +278,7 @@ def convert_code_sample_style(ctx: click.core.Context, param: click.core.Paramet
|
|
|
281
278
|
|
|
282
279
|
def convert_data_generation_method(
|
|
283
280
|
ctx: click.core.Context, param: click.core.Parameter, value: str
|
|
284
|
-
) ->
|
|
281
|
+
) -> list[DataGenerationMethod]:
|
|
285
282
|
if value == "all":
|
|
286
283
|
return DataGenerationMethod.all()
|
|
287
284
|
return [DataGenerationMethod[value]]
|
|
@@ -307,7 +304,7 @@ def convert_hosts_file(ctx: click.core.Context, param: click.core.Parameter, val
|
|
|
307
304
|
return value
|
|
308
305
|
|
|
309
306
|
|
|
310
|
-
def convert_boolean_string(ctx: click.core.Context, param: click.core.Parameter, value: str) ->
|
|
307
|
+
def convert_boolean_string(ctx: click.core.Context, param: click.core.Parameter, value: str) -> str | bool:
|
|
311
308
|
if value.lower() in TRUE_VALUES:
|
|
312
309
|
return True
|
|
313
310
|
if value.lower() in FALSE_VALUES:
|
schemathesis/cli/cassettes.py
CHANGED
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
import threading
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
from queue import Queue
|
|
9
|
-
from typing import IO, Any,
|
|
9
|
+
from typing import IO, Any, Generator, Iterator, cast, TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from ..constants import SCHEMATHESIS_VERSION
|
|
12
12
|
from ..runner import events
|
|
@@ -93,7 +93,7 @@ class Process:
|
|
|
93
93
|
correlation_id: str
|
|
94
94
|
thread_id: int
|
|
95
95
|
data_generation_method: DataGenerationMethod
|
|
96
|
-
interactions:
|
|
96
|
+
interactions: list[SerializedInteraction]
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
@dataclass
|
|
@@ -123,16 +123,16 @@ def worker(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, q
|
|
|
123
123
|
current_id = 1
|
|
124
124
|
stream = file_handle.open()
|
|
125
125
|
|
|
126
|
-
def format_header_values(values:
|
|
126
|
+
def format_header_values(values: list[str]) -> str:
|
|
127
127
|
return "\n".join(f" - {json.dumps(v)}" for v in values)
|
|
128
128
|
|
|
129
|
-
def format_headers(headers:
|
|
129
|
+
def format_headers(headers: dict[str, list[str]]) -> str:
|
|
130
130
|
return "\n".join(f' "{name}":\n{format_header_values(values)}' for name, values in headers.items())
|
|
131
131
|
|
|
132
|
-
def format_check_message(message:
|
|
132
|
+
def format_check_message(message: str | None) -> str:
|
|
133
133
|
return "~" if message is None else f"{repr(message)}"
|
|
134
134
|
|
|
135
|
-
def format_checks(checks:
|
|
135
|
+
def format_checks(checks: list[SerializedCheck]) -> str:
|
|
136
136
|
return "\n".join(
|
|
137
137
|
f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
|
|
138
138
|
for check in checks
|
|
@@ -278,18 +278,18 @@ def write_double_quoted(stream: IO, text: str) -> None:
|
|
|
278
278
|
|
|
279
279
|
@dataclass
|
|
280
280
|
class Replayed:
|
|
281
|
-
interaction:
|
|
281
|
+
interaction: dict[str, Any]
|
|
282
282
|
response: requests.Response
|
|
283
283
|
|
|
284
284
|
|
|
285
285
|
def replay(
|
|
286
|
-
cassette:
|
|
287
|
-
id_:
|
|
288
|
-
status:
|
|
289
|
-
uri:
|
|
290
|
-
method:
|
|
286
|
+
cassette: dict[str, Any],
|
|
287
|
+
id_: str | None = None,
|
|
288
|
+
status: str | None = None,
|
|
289
|
+
uri: str | None = None,
|
|
290
|
+
method: str | None = None,
|
|
291
291
|
request_tls_verify: bool = True,
|
|
292
|
-
request_cert:
|
|
292
|
+
request_cert: RequestCert | None = None,
|
|
293
293
|
) -> Generator[Replayed, None, None]:
|
|
294
294
|
"""Replay saved interactions."""
|
|
295
295
|
import requests
|
|
@@ -304,26 +304,26 @@ def replay(
|
|
|
304
304
|
|
|
305
305
|
|
|
306
306
|
def filter_cassette(
|
|
307
|
-
interactions:
|
|
308
|
-
id_:
|
|
309
|
-
status:
|
|
310
|
-
uri:
|
|
311
|
-
method:
|
|
312
|
-
) -> Iterator[
|
|
307
|
+
interactions: list[dict[str, Any]],
|
|
308
|
+
id_: str | None = None,
|
|
309
|
+
status: str | None = None,
|
|
310
|
+
uri: str | None = None,
|
|
311
|
+
method: str | None = None,
|
|
312
|
+
) -> Iterator[dict[str, Any]]:
|
|
313
313
|
filters = []
|
|
314
314
|
|
|
315
|
-
def id_filter(item:
|
|
315
|
+
def id_filter(item: dict[str, Any]) -> bool:
|
|
316
316
|
return item["id"] == id_
|
|
317
317
|
|
|
318
|
-
def status_filter(item:
|
|
318
|
+
def status_filter(item: dict[str, Any]) -> bool:
|
|
319
319
|
status_ = cast(str, status)
|
|
320
320
|
return item["status"].upper() == status_.upper()
|
|
321
321
|
|
|
322
|
-
def uri_filter(item:
|
|
322
|
+
def uri_filter(item: dict[str, Any]) -> bool:
|
|
323
323
|
uri_ = cast(str, uri)
|
|
324
324
|
return bool(re.search(uri_, item["request"]["uri"]))
|
|
325
325
|
|
|
326
|
-
def method_filter(item:
|
|
326
|
+
def method_filter(item: dict[str, Any]) -> bool:
|
|
327
327
|
method_ = cast(str, method)
|
|
328
328
|
return bool(re.search(method_, item["request"]["method"]))
|
|
329
329
|
|
|
@@ -339,13 +339,13 @@ def filter_cassette(
|
|
|
339
339
|
if method is not None:
|
|
340
340
|
filters.append(method_filter)
|
|
341
341
|
|
|
342
|
-
def is_match(interaction:
|
|
342
|
+
def is_match(interaction: dict[str, Any]) -> bool:
|
|
343
343
|
return all(filter_(interaction) for filter_ in filters)
|
|
344
344
|
|
|
345
345
|
return filter(is_match, interactions)
|
|
346
346
|
|
|
347
347
|
|
|
348
|
-
def get_prepared_request(data:
|
|
348
|
+
def get_prepared_request(data: dict[str, Any]) -> requests.PreparedRequest:
|
|
349
349
|
"""Create a `requests.PreparedRequest` from a serialized one."""
|
|
350
350
|
from requests.structures import CaseInsensitiveDict
|
|
351
351
|
from requests.cookies import RequestsCookieJar
|
schemathesis/cli/context.py
CHANGED
|
@@ -3,9 +3,10 @@ import os
|
|
|
3
3
|
import shutil
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from queue import Queue
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from ..code_samples import CodeSampleStyle
|
|
9
|
+
from ..internal.deprecation import deprecated_property
|
|
9
10
|
from ..runner.serialization import SerializedTestResult
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
@@ -21,7 +22,7 @@ class ServiceReportContext:
|
|
|
21
22
|
@dataclass
|
|
22
23
|
class FileReportContext:
|
|
23
24
|
queue: Queue
|
|
24
|
-
filename:
|
|
25
|
+
filename: str | None = None
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
@dataclass
|
|
@@ -29,21 +30,26 @@ class ExecutionContext:
|
|
|
29
30
|
"""Storage for the current context of the execution."""
|
|
30
31
|
|
|
31
32
|
hypothesis_settings: hypothesis.settings
|
|
32
|
-
hypothesis_output:
|
|
33
|
+
hypothesis_output: list[str] = field(default_factory=list)
|
|
33
34
|
workers_num: int = 1
|
|
34
|
-
rate_limit:
|
|
35
|
-
|
|
36
|
-
wait_for_schema:
|
|
35
|
+
rate_limit: str | None = None
|
|
36
|
+
show_trace: bool = False
|
|
37
|
+
wait_for_schema: float | None = None
|
|
37
38
|
validate_schema: bool = True
|
|
38
39
|
operations_processed: int = 0
|
|
39
|
-
# It is set in runtime, from
|
|
40
|
-
operations_count:
|
|
40
|
+
# It is set in runtime, from the `Initialized` event
|
|
41
|
+
operations_count: int | None = None
|
|
42
|
+
seed: int | None = None
|
|
41
43
|
current_line_length: int = 0
|
|
42
44
|
terminal_size: os.terminal_size = field(default_factory=shutil.get_terminal_size)
|
|
43
|
-
results:
|
|
44
|
-
cassette_path:
|
|
45
|
-
junit_xml_file:
|
|
45
|
+
results: list[SerializedTestResult] = field(default_factory=list)
|
|
46
|
+
cassette_path: str | None = None
|
|
47
|
+
junit_xml_file: str | None = None
|
|
46
48
|
is_interrupted: bool = False
|
|
47
49
|
verbosity: int = 0
|
|
48
50
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
|
49
|
-
report:
|
|
51
|
+
report: ServiceReportContext | FileReportContext | None = None
|
|
52
|
+
|
|
53
|
+
@deprecated_property(removed_in="4.0", replacement="show_trace")
|
|
54
|
+
def show_errors_tracebacks(self) -> bool:
|
|
55
|
+
return self.show_trace
|
schemathesis/cli/junitxml.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import platform
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
from junit_xml import TestCase, TestSuite, to_xml_report_file
|
|
7
7
|
|
|
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
|
|
|
19
19
|
@dataclass
|
|
20
20
|
class JunitXMLHandler(EventHandler):
|
|
21
21
|
file_handle: LazyFile
|
|
22
|
-
test_cases:
|
|
22
|
+
test_cases: list = field(default_factory=list)
|
|
23
23
|
|
|
24
24
|
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
25
25
|
if isinstance(event, events.AfterExecution):
|
schemathesis/cli/options.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
from enum import Enum
|
|
2
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, NoReturn
|
|
3
4
|
|
|
4
5
|
import click
|
|
5
6
|
|
|
@@ -19,12 +20,12 @@ class CustomHelpMessageChoice(click.Choice):
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class BaseCsvChoice(click.Choice):
|
|
22
|
-
def parse_value(self, value: str) ->
|
|
23
|
+
def parse_value(self, value: str) -> tuple[list[str], set[str]]:
|
|
23
24
|
selected = [item for item in value.split(",") if item]
|
|
24
25
|
invalid_options = set(selected) - set(self.choices)
|
|
25
26
|
return selected, invalid_options
|
|
26
27
|
|
|
27
|
-
def fail_on_invalid_options(self, invalid_options:
|
|
28
|
+
def fail_on_invalid_options(self, invalid_options: set[str], selected: list[str]) -> NoReturn:
|
|
28
29
|
# Sort to keep the error output consistent with the passed values
|
|
29
30
|
sorted_options = ", ".join(sorted(invalid_options, key=selected.index))
|
|
30
31
|
available_options = ", ".join(self.choices)
|
|
@@ -32,13 +33,13 @@ class BaseCsvChoice(click.Choice):
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
class CsvEnumChoice(BaseCsvChoice):
|
|
35
|
-
def __init__(self, choices:
|
|
36
|
+
def __init__(self, choices: type[Enum]):
|
|
36
37
|
self.enum = choices
|
|
37
38
|
super().__init__(tuple(el.name for el in choices))
|
|
38
39
|
|
|
39
40
|
def convert( # type: ignore[return]
|
|
40
|
-
self, value: str, param:
|
|
41
|
-
) ->
|
|
41
|
+
self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
|
|
42
|
+
) -> list[Enum]:
|
|
42
43
|
selected, invalid_options = self.parse_value(value)
|
|
43
44
|
if not invalid_options and selected:
|
|
44
45
|
return [self.enum[item] for item in selected]
|
|
@@ -46,9 +47,7 @@ class CsvEnumChoice(BaseCsvChoice):
|
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
class CsvChoice(BaseCsvChoice):
|
|
49
|
-
def convert(
|
|
50
|
-
self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]
|
|
51
|
-
) -> List[str]:
|
|
50
|
+
def convert(self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None) -> list[str]:
|
|
52
51
|
selected, invalid_options = self.parse_value(value)
|
|
53
52
|
if not invalid_options and selected:
|
|
54
53
|
return selected
|
|
@@ -57,8 +56,8 @@ class CsvChoice(BaseCsvChoice):
|
|
|
57
56
|
|
|
58
57
|
class OptionalInt(click.types.IntRange):
|
|
59
58
|
def convert( # type: ignore
|
|
60
|
-
self, value: str, param:
|
|
61
|
-
) ->
|
|
59
|
+
self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
|
|
60
|
+
) -> int | NotSet:
|
|
62
61
|
if value.lower() == "none":
|
|
63
62
|
return NOT_SET
|
|
64
63
|
try:
|