schemathesis 3.35.4__py3-none-any.whl → 3.36.0__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 (85) hide show
  1. schemathesis/__init__.py +5 -5
  2. schemathesis/_hypothesis.py +12 -6
  3. schemathesis/_override.py +4 -4
  4. schemathesis/auths.py +1 -1
  5. schemathesis/checks.py +8 -5
  6. schemathesis/cli/__init__.py +23 -26
  7. schemathesis/cli/callbacks.py +6 -4
  8. schemathesis/cli/cassettes.py +67 -41
  9. schemathesis/cli/context.py +7 -6
  10. schemathesis/cli/junitxml.py +1 -1
  11. schemathesis/cli/options.py +7 -4
  12. schemathesis/cli/output/default.py +5 -5
  13. schemathesis/cli/reporting.py +4 -2
  14. schemathesis/code_samples.py +4 -3
  15. schemathesis/contrib/unique_data.py +1 -2
  16. schemathesis/exceptions.py +4 -3
  17. schemathesis/extra/_flask.py +4 -1
  18. schemathesis/extra/pytest_plugin.py +6 -3
  19. schemathesis/failures.py +2 -1
  20. schemathesis/filters.py +2 -2
  21. schemathesis/generation/__init__.py +2 -2
  22. schemathesis/generation/_hypothesis.py +1 -1
  23. schemathesis/generation/coverage.py +53 -12
  24. schemathesis/graphql.py +0 -1
  25. schemathesis/hooks.py +3 -3
  26. schemathesis/internal/checks.py +53 -0
  27. schemathesis/lazy.py +10 -7
  28. schemathesis/loaders.py +3 -3
  29. schemathesis/models.py +59 -23
  30. schemathesis/runner/__init__.py +12 -6
  31. schemathesis/runner/events.py +1 -1
  32. schemathesis/runner/impl/context.py +72 -0
  33. schemathesis/runner/impl/core.py +105 -67
  34. schemathesis/runner/impl/solo.py +17 -20
  35. schemathesis/runner/impl/threadpool.py +65 -72
  36. schemathesis/runner/serialization.py +4 -3
  37. schemathesis/sanitization.py +2 -1
  38. schemathesis/schemas.py +20 -22
  39. schemathesis/serializers.py +2 -0
  40. schemathesis/service/client.py +1 -1
  41. schemathesis/service/events.py +4 -1
  42. schemathesis/service/extensions.py +2 -2
  43. schemathesis/service/hosts.py +4 -2
  44. schemathesis/service/models.py +3 -3
  45. schemathesis/service/report.py +3 -3
  46. schemathesis/service/serialization.py +4 -2
  47. schemathesis/specs/graphql/loaders.py +5 -4
  48. schemathesis/specs/graphql/schemas.py +13 -8
  49. schemathesis/specs/openapi/checks.py +76 -27
  50. schemathesis/specs/openapi/definitions.py +1 -5
  51. schemathesis/specs/openapi/examples.py +92 -2
  52. schemathesis/specs/openapi/expressions/__init__.py +7 -0
  53. schemathesis/specs/openapi/expressions/extractors.py +4 -1
  54. schemathesis/specs/openapi/expressions/nodes.py +5 -3
  55. schemathesis/specs/openapi/links.py +4 -4
  56. schemathesis/specs/openapi/loaders.py +6 -5
  57. schemathesis/specs/openapi/negative/__init__.py +5 -3
  58. schemathesis/specs/openapi/negative/mutations.py +5 -4
  59. schemathesis/specs/openapi/parameters.py +4 -2
  60. schemathesis/specs/openapi/schemas.py +28 -13
  61. schemathesis/specs/openapi/security.py +6 -4
  62. schemathesis/specs/openapi/stateful/__init__.py +2 -2
  63. schemathesis/specs/openapi/stateful/statistic.py +3 -3
  64. schemathesis/specs/openapi/stateful/types.py +3 -2
  65. schemathesis/stateful/__init__.py +3 -3
  66. schemathesis/stateful/config.py +2 -1
  67. schemathesis/stateful/context.py +13 -3
  68. schemathesis/stateful/events.py +3 -3
  69. schemathesis/stateful/runner.py +24 -6
  70. schemathesis/stateful/sink.py +1 -1
  71. schemathesis/stateful/state_machine.py +7 -6
  72. schemathesis/stateful/statistic.py +3 -1
  73. schemathesis/stateful/validation.py +10 -5
  74. schemathesis/transports/__init__.py +2 -2
  75. schemathesis/transports/asgi.py +7 -0
  76. schemathesis/transports/auth.py +2 -1
  77. schemathesis/transports/content_types.py +1 -1
  78. schemathesis/transports/responses.py +2 -1
  79. schemathesis/utils.py +4 -2
  80. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/METADATA +1 -1
  81. schemathesis-3.36.0.dist-info/RECORD +157 -0
  82. schemathesis-3.35.4.dist-info/RECORD +0 -154
  83. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/WHEEL +0 -0
  84. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/entry_points.txt +0 -0
  85. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py CHANGED
@@ -2,12 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
- from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
5
+ from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets
6
6
  from ._lazy_import import lazy_import
7
- from .constants import SCHEMATHESIS_VERSION # noqa: E402
8
- from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig # noqa: E402
9
- from .models import Case # noqa: E402
10
- from .specs import openapi # noqa: E402
7
+ from .constants import SCHEMATHESIS_VERSION
8
+ from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig
9
+ from .models import Case
10
+ from .specs import openapi
11
11
 
12
12
  __version__ = SCHEMATHESIS_VERSION
13
13
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import warnings
8
- from typing import Any, Callable, Generator, Mapping, Optional, Tuple
8
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
9
9
 
10
10
  import hypothesis
11
11
  from hypothesis import Phase
@@ -24,7 +24,9 @@ from .models import APIOperation, Case, GenerationMetadata, TestPhase
24
24
  from .transports.content_types import parse_content_type
25
25
  from .transports.headers import has_invalid_characters, is_latin_1_encodable
26
26
  from .types import NotSet
27
- from .utils import GivenInput
27
+
28
+ if TYPE_CHECKING:
29
+ from .utils import GivenInput
28
30
 
29
31
  # Forcefully initializes Hypothesis' global PRNG to avoid races that initilize it
30
32
  # if e.g. Schemathesis CLI is used with multiple workers
@@ -215,6 +217,7 @@ def _iter_coverage_cases(
215
217
  operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
216
218
  ) -> Generator[Case, None, None]:
217
219
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
220
+ from .specs.openapi.examples import find_in_responses, find_matching_in_responses
218
221
 
219
222
  ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
220
223
  meta = GenerationMetadata(
@@ -222,8 +225,11 @@ def _iter_coverage_cases(
222
225
  )
223
226
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
224
227
  template: dict[str, Any] = {}
228
+ responses = find_in_responses(operation)
225
229
  for parameter in operation.iter_parameters():
226
230
  schema = parameter.as_json_schema(operation)
231
+ for value in find_matching_in_responses(responses, parameter.name):
232
+ schema.setdefault("examples", []).append(value)
227
233
  gen = coverage.cover_schema_iter(ctx, schema)
228
234
  value = next(gen, NOT_SET)
229
235
  if isinstance(value, NotSet):
@@ -274,7 +280,7 @@ def _iter_coverage_cases(
274
280
  yield case
275
281
 
276
282
 
277
- def find_invalid_headers(headers: Mapping) -> Generator[Tuple[str, str], None, None]:
283
+ def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
278
284
  for name, value in headers.items():
279
285
  if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
280
286
  yield name, value
@@ -305,11 +311,11 @@ def add_non_serializable_mark(test: Callable, exc: SerializationNotPossible) ->
305
311
  test._schemathesis_non_serializable = exc # type: ignore
306
312
 
307
313
 
308
- def get_non_serializable_mark(test: Callable) -> Optional[SerializationNotPossible]:
314
+ def get_non_serializable_mark(test: Callable) -> SerializationNotPossible | None:
309
315
  return getattr(test, "_schemathesis_non_serializable", None)
310
316
 
311
317
 
312
- def get_invalid_regex_mark(test: Callable) -> Optional[SchemaError]:
318
+ def get_invalid_regex_mark(test: Callable) -> SchemaError | None:
313
319
  return getattr(test, "_schemathesis_invalid_regex", None)
314
320
 
315
321
 
@@ -317,7 +323,7 @@ def add_invalid_regex_mark(test: Callable, exc: SchemaError) -> None:
317
323
  test._schemathesis_invalid_regex = exc # type: ignore
318
324
 
319
325
 
320
- def get_invalid_example_headers_mark(test: Callable) -> Optional[dict[str, str]]:
326
+ def get_invalid_example_headers_mark(test: Callable) -> dict[str, str] | None:
321
327
  return getattr(test, "_schemathesis_invalid_example_headers", None)
322
328
 
323
329
 
schemathesis/_override.py CHANGED
@@ -1,14 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import TYPE_CHECKING, Optional
4
+ from typing import TYPE_CHECKING
5
5
 
6
6
  from .exceptions import UsageError
7
- from .parameters import ParameterSet
8
- from .types import GenericTest
9
7
 
10
8
  if TYPE_CHECKING:
11
9
  from .models import APIOperation
10
+ from .parameters import ParameterSet
11
+ from .types import GenericTest
12
12
 
13
13
 
14
14
  @dataclass
@@ -37,7 +37,7 @@ def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[s
37
37
  return output
38
38
 
39
39
 
40
- def get_override_from_mark(test: GenericTest) -> Optional[CaseOverride]:
40
+ def get_override_from_mark(test: GenericTest) -> CaseOverride | None:
41
41
  return getattr(test, "_schemathesis_override", None)
42
42
 
43
43
 
schemathesis/auths.py CHANGED
@@ -21,12 +21,12 @@ from typing import (
21
21
 
22
22
  from .exceptions import UsageError
23
23
  from .filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
24
- from .types import GenericTest
25
24
 
26
25
  if TYPE_CHECKING:
27
26
  import requests.auth
28
27
 
29
28
  from .models import APIOperation, Case
29
+ from .types import GenericTest
30
30
 
31
31
  DEFAULT_REFRESH_INTERVAL = 300
32
32
  AUTH_STORAGE_ATTRIBUTE_NAME = "_schemathesis_auth"
schemathesis/checks.py CHANGED
@@ -15,11 +15,12 @@ from .specs.openapi.checks import (
15
15
  )
16
16
 
17
17
  if TYPE_CHECKING:
18
- from .models import Case, CheckFunction
18
+ from .internal.checks import CheckContext, CheckFunction
19
+ from .models import Case
19
20
  from .transports.responses import GenericResponse
20
21
 
21
22
 
22
- def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
23
+ def not_a_server_error(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
23
24
  """A check to verify that the response is not a server-side error."""
24
25
  from .specs.graphql.schemas import GraphQLCase
25
26
  from .specs.graphql.validation import validate_graphql_response
@@ -64,14 +65,16 @@ def register(check: CheckFunction) -> CheckFunction:
64
65
  .. code-block:: python
65
66
 
66
67
  @schemathesis.check
67
- def new_check(response, case):
68
+ def new_check(ctx, response, case):
68
69
  # some awesome assertions!
69
70
  ...
70
71
  """
71
72
  from . import cli
73
+ from .internal.checks import wrap_check
72
74
 
75
+ _check = wrap_check(check)
73
76
  global ALL_CHECKS
74
77
 
75
- ALL_CHECKS += (check,)
76
- cli.CHECKS_TYPE.choices += (check.__name__,) # type: ignore
78
+ ALL_CHECKS += (_check,)
79
+ cli.CHECKS_TYPE.choices += (_check.__name__,) # type: ignore
77
80
  return check
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import base64
4
- import io
5
4
  import os
6
5
  import sys
7
6
  import traceback
@@ -41,38 +40,45 @@ from ..internal.datetime import current_datetime
41
40
  from ..internal.output import OutputConfig
42
41
  from ..internal.validation import file_exists
43
42
  from ..loaders import load_app, load_yaml
44
- from ..models import Case, CheckFunction
45
43
  from ..runner import events, prepare_hypothesis_settings, probes
46
44
  from ..specs.graphql import loaders as gql_loaders
47
45
  from ..specs.openapi import loaders as oas_loaders
48
46
  from ..stateful import Stateful
49
- from ..targets import Target
50
47
  from ..transports import RequestConfig
51
48
  from ..transports.auth import get_requests_auth
52
- from ..types import PathLike, RequestCert
53
49
  from . import callbacks, cassettes, output
54
50
  from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
55
51
  from .context import ExecutionContext, FileReportContext, ServiceReportContext
56
52
  from .debug import DebugOutputHandler
57
53
  from .handlers import EventHandler
58
54
  from .junitxml import JunitXMLHandler
59
- from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, NotSet, OptionalInt
55
+ from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, OptionalInt
60
56
  from .sanitization import SanitizationHandler
61
57
 
62
58
  if TYPE_CHECKING:
59
+ import io
60
+
63
61
  import hypothesis
64
62
  import requests
65
63
 
64
+ from ..models import Case, CheckFunction
66
65
  from ..schemas import BaseSchema
67
66
  from ..service.client import ServiceClient
68
67
  from ..specs.graphql.schemas import GraphQLSchema
68
+ from ..targets import Target
69
+ from ..types import NotSet, PathLike, RequestCert
70
+
71
+
72
+ __all__ = [
73
+ "EventHandler",
74
+ ]
69
75
 
70
76
 
71
77
  def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
72
78
  return tuple(item.__name__ for item in items)
73
79
 
74
80
 
75
- CUSTOM_HANDLERS: list[Type[EventHandler]] = []
81
+ CUSTOM_HANDLERS: list[type[EventHandler]] = []
76
82
  CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
77
83
 
78
84
  DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
@@ -98,13 +104,6 @@ DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
98
104
  "Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
99
105
  "Use `--show-trace` instead"
100
106
  )
101
- DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING = (
102
- "The `--contrib-unique-data` CLI option and the corresponding `schemathesis.contrib.unique_data` hook "
103
- "are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
104
- "strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
105
- "This leads to cryptic error messages about external state and flaky test runs, "
106
- "therefore it will be removed in Schemathesis 4.0"
107
- )
108
107
  CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
109
108
  COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
110
109
  PHASES_INVALID_USAGE_MESSAGE = "Can't use `--hypothesis-phases` and `--hypothesis-no-phases` simultaneously"
@@ -114,14 +113,14 @@ def reset_checks() -> None:
114
113
  """Get checks list to their default state."""
115
114
  # Useful in tests
116
115
  checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
117
- CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS) + ("all",)
116
+ CHECKS_TYPE.choices = (*_get_callable_names(checks_module.ALL_CHECKS), "all")
118
117
 
119
118
 
120
119
  def reset_targets() -> None:
121
120
  """Get targets list to their default state."""
122
121
  # Useful in tests
123
122
  targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
124
- TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS) + ("all",)
123
+ TARGETS_TYPE.choices = (*_get_callable_names(targets_module.ALL_TARGETS), "all")
125
124
 
126
125
 
127
126
  @click.group(context_settings=CONTEXT_SETTINGS)
@@ -282,7 +281,7 @@ REPORT_TO_SERVICE = ReportToService()
282
281
  "workers_num",
283
282
  help="Number of concurrent workers for testing. Auto-adjusts if 'auto' is specified",
284
283
  type=CustomHelpMessageChoice(
285
- ["auto"] + list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1))),
284
+ ["auto", *list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1)))],
286
285
  choices_repr=f"[auto, {MIN_WORKERS}-{MAX_WORKERS}]",
287
286
  ),
288
287
  default=str(DEFAULT_WORKERS),
@@ -318,7 +317,7 @@ REPORT_TO_SERVICE = ReportToService()
318
317
  "--fixups",
319
318
  help="Apply compatibility adjustments",
320
319
  multiple=True,
321
- type=click.Choice(list(ALL_FIXUPS) + ["all"]),
320
+ type=click.Choice([*ALL_FIXUPS, "all"]),
322
321
  metavar="",
323
322
  )
324
323
  @group("API validation options")
@@ -433,7 +432,7 @@ REPORT_TO_SERVICE = ReportToService()
433
432
  "-A",
434
433
  type=click.Choice(["basic", "digest"], case_sensitive=False),
435
434
  default="basic",
436
- help="Specify the authentication method",
435
+ help="Specify the authentication method. For custom authentication methods, see our Authentication documentation: https://schemathesis.readthedocs.io/en/stable/auth.html#custom-auth",
437
436
  show_default=True,
438
437
  metavar="",
439
438
  )
@@ -943,9 +942,6 @@ def run(
943
942
  entry for health_check in hypothesis_suppress_health_check for entry in health_check.as_hypothesis()
944
943
  ]
945
944
 
946
- if contrib_unique_data:
947
- click.secho(DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING, fg="yellow")
948
-
949
945
  if show_errors_tracebacks:
950
946
  click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
951
947
  show_trace = show_errors_tracebacks
@@ -1154,8 +1150,6 @@ def run(
1154
1150
  else:
1155
1151
  _fixups.install(fixups)
1156
1152
 
1157
- if contrib_unique_data:
1158
- contrib.unique_data.install()
1159
1153
  if contrib_openapi_formats_uuid:
1160
1154
  contrib.openapi.formats.uuid.install()
1161
1155
  if contrib_openapi_fill_missing_examples:
@@ -1191,6 +1185,7 @@ def run(
1191
1185
  seed=hypothesis_seed,
1192
1186
  exit_first=exit_first,
1193
1187
  max_failures=max_failures,
1188
+ unique_data=contrib_unique_data,
1194
1189
  dry_run=dry_run,
1195
1190
  store_interactions=cassette_path is not None,
1196
1191
  checks=selected_checks,
@@ -1315,6 +1310,7 @@ def into_event_stream(
1315
1310
  exit_first: bool,
1316
1311
  max_failures: int | None,
1317
1312
  rate_limit: str | None,
1313
+ unique_data: bool,
1318
1314
  dry_run: bool,
1319
1315
  store_interactions: bool,
1320
1316
  stateful: Stateful | None,
@@ -1358,6 +1354,7 @@ def into_event_stream(
1358
1354
  exit_first=exit_first,
1359
1355
  max_failures=max_failures,
1360
1356
  started_at=started_at,
1357
+ unique_data=unique_data,
1361
1358
  dry_run=dry_run,
1362
1359
  store_interactions=store_interactions,
1363
1360
  checks=checks,
@@ -2000,7 +1997,7 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
2000
1997
  ctx.color = False
2001
1998
 
2002
1999
 
2003
- def add_option(*args: Any, cls: Type = click.Option, **kwargs: Any) -> None:
2000
+ def add_option(*args: Any, cls: type = click.Option, **kwargs: Any) -> None:
2004
2001
  """Add a new CLI option to `st run`."""
2005
2002
  run.params.append(cls(args, **kwargs))
2006
2003
 
@@ -2024,10 +2021,10 @@ def add_group(name: str, *, index: int | None = None) -> Group:
2024
2021
  return Group(name)
2025
2022
 
2026
2023
 
2027
- def handler() -> Callable[[Type], None]:
2024
+ def handler() -> Callable[[type], None]:
2028
2025
  """Register a new CLI event handler."""
2029
2026
 
2030
- def _wrapper(cls: Type) -> None:
2027
+ def _wrapper(cls: type) -> None:
2031
2028
  CUSTOM_HANDLERS.append(cls)
2032
2029
 
2033
2030
  return _wrapper
@@ -2,16 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import codecs
4
4
  import enum
5
+ import operator
5
6
  import os
6
7
  import re
7
8
  import traceback
8
9
  from contextlib import contextmanager
9
- from functools import partial
10
+ from functools import partial, reduce
10
11
  from typing import TYPE_CHECKING, Callable, Generator
11
12
  from urllib.parse import urlparse
12
13
 
13
14
  import click
14
- from click.types import LazyFile # type: ignore
15
15
 
16
16
  from .. import exceptions, experimental, throttling
17
17
  from ..code_samples import CodeSampleStyle
@@ -24,12 +24,14 @@ from ..loaders import load_app
24
24
  from ..service.hosts import get_temporary_hosts_file
25
25
  from ..stateful import Stateful
26
26
  from ..transports.headers import has_invalid_characters, is_latin_1_encodable
27
- from ..types import PathLike
28
27
  from .cassettes import CassetteFormat
29
28
  from .constants import DEFAULT_WORKERS
30
29
 
31
30
  if TYPE_CHECKING:
32
31
  import hypothesis
32
+ from click.types import LazyFile # type: ignore[attr-defined]
33
+
34
+ from ..types import PathLike
33
35
 
34
36
  INVALID_DERANDOMIZE_MESSAGE = (
35
37
  "`--hypothesis-derandomize` implies no database, so passing `--hypothesis-database` too is invalid."
@@ -339,7 +341,7 @@ def convert_experimental(
339
341
 
340
342
 
341
343
  def convert_checks(ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]]) -> list[str]:
342
- return sum(value, [])
344
+ return reduce(operator.iadd, value, [])
343
345
 
344
346
 
345
347
  def convert_code_sample_style(ctx: click.core.Context, param: click.core.Parameter, value: str) -> CodeSampleStyle:
@@ -16,7 +16,6 @@ import harfile
16
16
 
17
17
  from ..constants import SCHEMATHESIS_VERSION
18
18
  from ..runner import events
19
- from ..types import RequestCert
20
19
  from .handlers import EventHandler
21
20
 
22
21
  if TYPE_CHECKING:
@@ -25,6 +24,7 @@ if TYPE_CHECKING:
25
24
 
26
25
  from ..models import Request, Response
27
26
  from ..runner.serialization import SerializedCheck, SerializedInteraction
27
+ from ..types import RequestCert
28
28
  from .context import ExecutionContext
29
29
 
30
30
  # Wait until the worker terminates
@@ -163,13 +163,18 @@ def vcr_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
163
163
  return "\n".join(f' "{name}":\n{format_header_values(values)}' for name, values in headers.items())
164
164
 
165
165
  def format_check_message(message: str | None) -> str:
166
- return "~" if message is None else f"{repr(message)}"
166
+ return "~" if message is None else f"{message!r}"
167
167
 
168
168
  def format_checks(checks: list[SerializedCheck]) -> str:
169
- return "\n".join(
169
+ if not checks:
170
+ return " checks: []"
171
+ items = "\n".join(
170
172
  f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
171
173
  for check in checks
172
174
  )
175
+ return f"""
176
+ checks:
177
+ {items}"""
173
178
 
174
179
  if preserve_exact_body_bytes:
175
180
 
@@ -235,9 +240,8 @@ http_interactions:"""
235
240
  correlation_id: '{item.correlation_id}'
236
241
  data_generation_method: '{interaction.data_generation_method.value}'
237
242
  phase: {phase}
238
- elapsed: '{interaction.response.elapsed}'
243
+ elapsed: '{interaction.response.elapsed if interaction.response else 0}'
239
244
  recorded_at: '{interaction.recorded_at}'
240
- checks:
241
245
  {format_checks(interaction.checks)}
242
246
  request:
243
247
  uri: '{interaction.request.uri}'
@@ -246,8 +250,9 @@ http_interactions:"""
246
250
  {format_headers(interaction.request.headers)}"""
247
251
  )
248
252
  format_request_body(stream, interaction.request)
249
- stream.write(
250
- f"""
253
+ if interaction.response is not None:
254
+ stream.write(
255
+ f"""
251
256
  response:
252
257
  status:
253
258
  code: '{interaction.response.status_code}'
@@ -255,12 +260,16 @@ http_interactions:"""
255
260
  headers:
256
261
  {format_headers(interaction.response.headers)}
257
262
  """
258
- )
259
- format_response_body(stream, interaction.response)
260
- stream.write(
261
- f"""
263
+ )
264
+ format_response_body(stream, interaction.response)
265
+ stream.write(
266
+ f"""
262
267
  http_version: '{interaction.response.http_version}'"""
263
- )
268
+ )
269
+ else:
270
+ stream.write("""
271
+ response: null
272
+ """)
264
273
  current_id += 1
265
274
  else:
266
275
  break
@@ -300,11 +309,11 @@ def write_double_quoted(stream: IO, text: str) -> None:
300
309
  if ch in Emitter.ESCAPE_REPLACEMENTS:
301
310
  data = "\\" + Emitter.ESCAPE_REPLACEMENTS[ch]
302
311
  elif ch <= "\xff":
303
- data = "\\x%02X" % ord(ch)
312
+ data = f"\\x{ord(ch):02X}"
304
313
  elif ch <= "\uffff":
305
- data = "\\u%04X" % ord(ch)
314
+ data = f"\\u{ord(ch):04X}"
306
315
  else:
307
- data = "\\U%08X" % ord(ch)
316
+ data = f"\\U{ord(ch):08X}"
308
317
  stream.write(data)
309
318
  start = end + 1
310
319
  end += 1
@@ -326,25 +335,45 @@ def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
326
335
  item = queue.get()
327
336
  if isinstance(item, Process):
328
337
  for interaction in item.interactions:
329
- time = round(interaction.response.elapsed * 1000, 2)
330
- content_type = interaction.response.headers.get("Content-Type", [""])[0]
331
- content = harfile.Content(
332
- size=interaction.response.body_size or 0,
333
- mimeType=content_type,
334
- text=get_body(interaction.response.body) if interaction.response.body is not None else None,
335
- encoding="base64"
336
- if interaction.response.body is not None and preserve_exact_body_bytes
337
- else None,
338
- )
339
- http_version = f"HTTP/{interaction.response.http_version}"
340
338
  query_params = urlparse(interaction.request.uri).query
341
339
  if interaction.request.body is not None:
342
340
  post_data = harfile.PostData(
343
- mimeType=content_type,
341
+ mimeType=interaction.request.headers.get("Content-Type", [""])[0],
344
342
  text=get_body(interaction.request.body),
345
343
  )
346
344
  else:
347
345
  post_data = None
346
+ if interaction.response is not None:
347
+ content_type = interaction.response.headers.get("Content-Type", [""])[0]
348
+ content = harfile.Content(
349
+ size=interaction.response.body_size or 0,
350
+ mimeType=content_type,
351
+ text=get_body(interaction.response.body) if interaction.response.body is not None else None,
352
+ encoding="base64"
353
+ if interaction.response.body is not None and preserve_exact_body_bytes
354
+ else None,
355
+ )
356
+ http_version = f"HTTP/{interaction.response.http_version}"
357
+ response = harfile.Response(
358
+ status=interaction.response.status_code,
359
+ httpVersion=http_version,
360
+ statusText=interaction.response.message,
361
+ headers=[
362
+ harfile.Record(name=name, value=values[0])
363
+ for name, values in interaction.response.headers.items()
364
+ ],
365
+ cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
366
+ content=content,
367
+ headersSize=_headers_size(interaction.response.headers),
368
+ bodySize=interaction.response.body_size or 0,
369
+ redirectURL=interaction.response.headers.get("Location", [""])[0],
370
+ )
371
+ time = round(interaction.response.elapsed * 1000, 2)
372
+ else:
373
+ response = HARFILE_NO_RESPONSE
374
+ time = 0
375
+ http_version = ""
376
+
348
377
  har.add_entry(
349
378
  startedDateTime=interaction.recorded_at,
350
379
  time=time,
@@ -365,26 +394,23 @@ def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
365
394
  bodySize=interaction.request.body_size or 0,
366
395
  postData=post_data,
367
396
  ),
368
- response=harfile.Response(
369
- status=interaction.response.status_code,
370
- httpVersion=http_version,
371
- statusText=interaction.response.message,
372
- headers=[
373
- harfile.Record(name=name, value=values[0])
374
- for name, values in interaction.response.headers.items()
375
- ],
376
- cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
377
- content=content,
378
- headersSize=_headers_size(interaction.response.headers),
379
- bodySize=interaction.response.body_size or 0,
380
- redirectURL=interaction.response.headers.get("Location", [""])[0],
381
- ),
397
+ response=response,
382
398
  timings=harfile.Timings(send=0, wait=0, receive=time, blocked=0, dns=0, connect=0, ssl=0),
383
399
  )
384
400
  elif isinstance(item, Finalize):
385
401
  break
386
402
 
387
403
 
404
+ HARFILE_NO_RESPONSE = harfile.Response(
405
+ status=0,
406
+ httpVersion="",
407
+ statusText="",
408
+ headers=[],
409
+ cookies=[],
410
+ content=harfile.Content(),
411
+ )
412
+
413
+
388
414
  def _headers_size(headers: dict[str, list[str]]) -> int:
389
415
  size = 0
390
416
  for name, values in headers.items():
@@ -1,22 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
3
  import shutil
5
4
  from dataclasses import dataclass, field
6
- from queue import Queue
7
5
  from typing import TYPE_CHECKING, Generator
8
6
 
9
7
  from ..code_samples import CodeSampleStyle
10
8
  from ..internal.deprecation import deprecated_property
11
9
  from ..internal.output import OutputConfig
12
- from ..internal.result import Result
13
- from ..runner.probes import ProbeRun
14
- from ..runner.serialization import SerializedTestResult
15
- from ..service.models import AnalysisResult
16
10
 
17
11
  if TYPE_CHECKING:
12
+ import os
13
+ from queue import Queue
14
+
18
15
  import hypothesis
19
16
 
17
+ from ..internal.result import Result
18
+ from ..runner.probes import ProbeRun
19
+ from ..runner.serialization import SerializedTestResult
20
+ from ..service.models import AnalysisResult
20
21
  from ..stateful.sink import StateMachineSink
21
22
 
22
23
 
@@ -11,13 +11,13 @@ from ..exceptions import RuntimeErrorType
11
11
  from ..internal.output import prepare_response_payload
12
12
  from ..models import Status
13
13
  from ..runner import events
14
- from ..runner.serialization import SerializedCheck, SerializedError
15
14
  from .handlers import EventHandler
16
15
  from .reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
17
16
 
18
17
  if TYPE_CHECKING:
19
18
  from click.utils import LazyFile
20
19
 
20
+ from ..runner.serialization import SerializedCheck, SerializedError
21
21
  from .context import ExecutionContext
22
22
 
23
23
 
@@ -1,12 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- from enum import Enum
4
- from typing import Any, NoReturn
3
+ from typing import TYPE_CHECKING, Any, NoReturn
5
4
 
6
5
  import click
7
6
 
8
7
  from ..constants import NOT_SET
9
- from ..types import NotSet
8
+
9
+ if TYPE_CHECKING:
10
+ from enum import Enum
11
+
12
+ from ..types import NotSet
10
13
 
11
14
 
12
15
  class CustomHelpMessageChoice(click.Choice):
@@ -65,4 +68,4 @@ class OptionalInt(click.types.IntRange):
65
68
  int(value)
66
69
  return super().convert(value, param, ctx)
67
70
  except ValueError:
68
- self.fail("%s is not a valid integer or None." % value, param, ctx)
71
+ self.fail(f"{value} is not a valid integer or None.", param, ctx)