schemathesis 3.34.3__py3-none-any.whl → 3.35.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.
@@ -0,0 +1,487 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from contextlib import contextmanager, suppress
5
+ from dataclasses import dataclass, field
6
+ from functools import lru_cache
7
+ from typing import Any, Generator, Set, Type, TypeVar, cast
8
+
9
+ import jsonschema
10
+ from hypothesis import strategies as st
11
+ from hypothesis.errors import InvalidArgument, Unsatisfiable
12
+ from hypothesis_jsonschema import from_schema
13
+ from hypothesis_jsonschema._canonicalise import canonicalish
14
+
15
+ from schemathesis.constants import NOT_SET
16
+
17
+ from ._hypothesis import combine_strategies, get_single_example
18
+ from ._methods import DataGenerationMethod
19
+
20
+ BUFFER_SIZE = 8 * 1024
21
+ FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(lambda x: x or 0.0)
22
+ NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
23
+ JSON_STRATEGY: st.SearchStrategy = st.recursive(
24
+ st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(),
25
+ lambda strategy: st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3),
26
+ )
27
+ ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY)
28
+ OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
29
+
30
+ UNKNOWN_PROPERTY_KEY = "x-schemathesis-unknown-property"
31
+ UNKNOWN_PROPERTY_VALUE = 42
32
+
33
+
34
+ @dataclass
35
+ class GeneratedValue:
36
+ value: Any
37
+ data_generation_method: DataGenerationMethod
38
+
39
+ __slots__ = ("value", "data_generation_method")
40
+
41
+ @classmethod
42
+ def with_positive(cls, value: Any) -> GeneratedValue:
43
+ return cls(value, DataGenerationMethod.positive)
44
+
45
+ @classmethod
46
+ def with_negative(cls, value: Any) -> GeneratedValue:
47
+ return cls(value, DataGenerationMethod.negative)
48
+
49
+
50
+ PositiveValue = GeneratedValue.with_positive
51
+ NegativeValue = GeneratedValue.with_negative
52
+
53
+
54
+ @lru_cache(maxsize=128)
55
+ def cached_draw(strategy: st.SearchStrategy) -> Any:
56
+ return get_single_example(strategy)
57
+
58
+
59
+ @dataclass
60
+ class CoverageContext:
61
+ data_generation_methods: list[DataGenerationMethod] = field(default_factory=DataGenerationMethod.all)
62
+
63
+ @classmethod
64
+ def with_positive(cls) -> CoverageContext:
65
+ return CoverageContext(data_generation_methods=[DataGenerationMethod.positive])
66
+
67
+ @classmethod
68
+ def with_negative(cls) -> CoverageContext:
69
+ return CoverageContext(data_generation_methods=[DataGenerationMethod.negative])
70
+
71
+ def generate_from(self, strategy: st.SearchStrategy, cached: bool = False) -> Any:
72
+ if cached:
73
+ value = cached_draw(strategy)
74
+ else:
75
+ value = get_single_example(strategy)
76
+ return value
77
+
78
+ def generate_from_schema(self, schema: dict) -> Any:
79
+ return self.generate_from(from_schema(schema))
80
+
81
+
82
+ T = TypeVar("T")
83
+
84
+
85
+ def _to_hashable_key(value: T) -> T | tuple[type, str]:
86
+ if isinstance(value, (dict, list)):
87
+ serialized = json.dumps(value, sort_keys=True)
88
+ return (type(value), serialized)
89
+ return value
90
+
91
+
92
+ def _cover_positive_for_type(
93
+ ctx: CoverageContext, schema: dict, ty: str | None
94
+ ) -> Generator[GeneratedValue, None, None]:
95
+ if ty == "object" or ty == "array":
96
+ template_schema = _get_template_schema(schema, ty)
97
+ template = ctx.generate_from_schema(template_schema)
98
+ else:
99
+ template = None
100
+ if DataGenerationMethod.positive in ctx.data_generation_methods:
101
+ ctx = ctx.with_positive()
102
+ enum = schema.get("enum", NOT_SET)
103
+ const = schema.get("const", NOT_SET)
104
+ for key in ("anyOf", "oneOf"):
105
+ sub_schemas = schema.get(key)
106
+ if sub_schemas is not None:
107
+ for sub_schema in sub_schemas:
108
+ yield from cover_schema_iter(ctx, sub_schema)
109
+ all_of = schema.get("allOf")
110
+ if all_of is not None:
111
+ if len(all_of) == 1:
112
+ yield from cover_schema_iter(ctx, all_of[0])
113
+ else:
114
+ with suppress(jsonschema.SchemaError):
115
+ canonical = canonicalish(schema)
116
+ yield from cover_schema_iter(ctx, canonical)
117
+ if enum is not NOT_SET:
118
+ for value in enum:
119
+ yield PositiveValue(value)
120
+ elif const is not NOT_SET:
121
+ yield PositiveValue(const)
122
+ elif ty is not None:
123
+ if ty == "null":
124
+ yield PositiveValue(None)
125
+ elif ty == "boolean":
126
+ yield PositiveValue(True)
127
+ yield PositiveValue(False)
128
+ elif ty == "string":
129
+ yield from _positive_string(ctx, schema)
130
+ elif ty == "integer" or ty == "number":
131
+ yield from _positive_number(ctx, schema)
132
+ elif ty == "array":
133
+ yield from _positive_array(ctx, schema, cast(list, template))
134
+ elif ty == "object":
135
+ yield from _positive_object(ctx, schema, cast(dict, template))
136
+
137
+
138
+ @contextmanager
139
+ def _ignore_unfixable(
140
+ *,
141
+ # Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
142
+ # and it may cause errors during the interpreter shutdown
143
+ ref_error: Type[Exception] = jsonschema.RefResolutionError,
144
+ schema_error: Type[Exception] = jsonschema.SchemaError,
145
+ ) -> Generator:
146
+ try:
147
+ yield
148
+ except (Unsatisfiable, ref_error, schema_error):
149
+ pass
150
+ except InvalidArgument as exc:
151
+ message = str(exc)
152
+ if "Cannot create non-empty" not in message and "is not in the specified alphabet" not in message:
153
+ raise
154
+ except TypeError as exc:
155
+ if "first argument must be string or compiled pattern" not in str(exc):
156
+ raise
157
+
158
+
159
+ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[GeneratedValue, None, None]:
160
+ if isinstance(schema, bool):
161
+ types = ["null", "boolean", "string", "number", "array", "object"]
162
+ schema = {}
163
+ else:
164
+ types = schema.get("type", [])
165
+ if not isinstance(types, list):
166
+ types = [types] # type: ignore[unreachable]
167
+ if not types:
168
+ with _ignore_unfixable():
169
+ yield from _cover_positive_for_type(ctx, schema, None)
170
+ for ty in types:
171
+ with _ignore_unfixable():
172
+ yield from _cover_positive_for_type(ctx, schema, ty)
173
+ if DataGenerationMethod.negative in ctx.data_generation_methods:
174
+ template = None
175
+ seen: Set[Any | tuple[type, str]] = set()
176
+ for key, value in schema.items():
177
+ with _ignore_unfixable():
178
+ if key == "enum":
179
+ yield from _negative_enum(ctx, value)
180
+ elif key == "const":
181
+ yield from _negative_enum(ctx, [value])
182
+ elif key == "type":
183
+ yield from _negative_type(ctx, seen, value)
184
+ elif key == "properties":
185
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
186
+ yield from _negative_properties(ctx, template, value)
187
+ elif key == "pattern":
188
+ yield from _negative_pattern(ctx, value)
189
+ elif key == "format" and ("string" in types or not types):
190
+ yield from _negative_format(ctx, schema, value)
191
+ elif key == "maximum":
192
+ next = value + 1
193
+ yield NegativeValue(next)
194
+ seen.add(next)
195
+ elif key == "minimum":
196
+ next = value - 1
197
+ yield NegativeValue(next)
198
+ seen.add(next)
199
+ elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
200
+ yield NegativeValue(value)
201
+ seen.add(value)
202
+ elif key == "multipleOf":
203
+ yield from _negative_multiple_of(ctx, schema, value)
204
+ elif key == "minLength" and 0 < value < BUFFER_SIZE and "pattern" not in schema:
205
+ with suppress(InvalidArgument):
206
+ yield NegativeValue(
207
+ ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
208
+ )
209
+ elif key == "maxLength" and value < BUFFER_SIZE and "pattern" not in schema:
210
+ with suppress(InvalidArgument):
211
+ yield NegativeValue(
212
+ ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
213
+ )
214
+ elif key == "uniqueItems" and value:
215
+ yield from _negative_unique_items(ctx, schema)
216
+ elif key == "required":
217
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
218
+ yield from _negative_required(ctx, template, value)
219
+ elif key == "additionalProperties" and not value:
220
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
221
+ yield NegativeValue({**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE})
222
+ elif key == "allOf":
223
+ nctx = ctx.with_negative()
224
+ if len(value) == 1:
225
+ yield from cover_schema_iter(nctx, value[0])
226
+ else:
227
+ with _ignore_unfixable():
228
+ canonical = canonicalish(schema)
229
+ yield from cover_schema_iter(nctx, canonical)
230
+ elif key == "anyOf" or key == "oneOf":
231
+ nctx = ctx.with_negative()
232
+ # NOTE: Other sub-schemas are not filtered out
233
+ for sub_schema in value:
234
+ yield from cover_schema_iter(nctx, sub_schema)
235
+
236
+
237
+ def _get_template_schema(schema: dict, ty: str) -> dict:
238
+ if ty == "object":
239
+ properties = schema.get("properties")
240
+ if properties is not None:
241
+ return {
242
+ **schema,
243
+ "required": list(properties),
244
+ "type": ty,
245
+ "properties": {
246
+ k: _get_template_schema(v, "object") if isinstance(v, dict) and v.get("type") == "object" else v
247
+ for k, v in properties.items()
248
+ },
249
+ }
250
+ return {**schema, "type": ty}
251
+
252
+
253
+ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
254
+ """Generate positive string values."""
255
+ # Boundary and near boundary values
256
+ min_length = schema.get("minLength")
257
+ max_length = schema.get("maxLength")
258
+
259
+ if not min_length and not max_length:
260
+ # Default positive value
261
+ yield PositiveValue(ctx.generate_from_schema(schema))
262
+
263
+ seen = set()
264
+
265
+ if min_length is not None and min_length < BUFFER_SIZE and "pattern" not in schema:
266
+ # Exactly the minimum length
267
+ yield PositiveValue(ctx.generate_from_schema({**schema, "maxLength": min_length}))
268
+ seen.add(min_length)
269
+
270
+ # One character more than minimum if possible
271
+ larger = min_length + 1
272
+ if larger < BUFFER_SIZE and larger not in seen and (not max_length or larger <= max_length):
273
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger}))
274
+ seen.add(larger)
275
+
276
+ if max_length is not None and "pattern" not in schema:
277
+ # Exactly the maximum length
278
+ if max_length < BUFFER_SIZE and max_length not in seen:
279
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": max_length}))
280
+ seen.add(max_length)
281
+
282
+ # One character less than maximum if possible
283
+ smaller = max_length - 1
284
+ if (
285
+ smaller < BUFFER_SIZE
286
+ and smaller not in seen
287
+ and (smaller > 0 and (min_length is None or smaller >= min_length))
288
+ ):
289
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller}))
290
+ seen.add(smaller)
291
+
292
+
293
+ def closest_multiple_greater_than(y: int, x: int) -> int:
294
+ """Find the closest multiple of X that is greater than Y."""
295
+ quotient, remainder = divmod(y, x)
296
+ if remainder == 0:
297
+ return y
298
+ return x * (quotient + 1)
299
+
300
+
301
+ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
302
+ """Generate positive integer values."""
303
+ # Boundary and near boundary values
304
+ minimum = schema.get("minimum")
305
+ maximum = schema.get("maximum")
306
+ exclusive_minimum = schema.get("exclusiveMinimum")
307
+ exclusive_maximum = schema.get("exclusiveMaximum")
308
+ if exclusive_minimum is not None:
309
+ minimum = exclusive_minimum + 1
310
+ if exclusive_maximum is not None:
311
+ maximum = exclusive_maximum - 1
312
+ multiple_of = schema.get("multipleOf")
313
+
314
+ if not minimum and not maximum:
315
+ # Default positive value
316
+ yield PositiveValue(ctx.generate_from_schema(schema))
317
+
318
+ seen = set()
319
+
320
+ if minimum is not None:
321
+ # Exactly the minimum
322
+ if multiple_of is not None:
323
+ smallest = closest_multiple_greater_than(minimum, multiple_of)
324
+ else:
325
+ smallest = minimum
326
+ seen.add(smallest)
327
+ yield PositiveValue(smallest)
328
+
329
+ # One more than minimum if possible
330
+ if multiple_of is not None:
331
+ larger = smallest + multiple_of
332
+ else:
333
+ larger = minimum + 1
334
+ if larger not in seen and (not maximum or larger <= maximum):
335
+ seen.add(larger)
336
+ yield PositiveValue(larger)
337
+
338
+ if maximum is not None:
339
+ # Exactly the maximum
340
+ if multiple_of is not None:
341
+ largest = maximum - (maximum % multiple_of)
342
+ else:
343
+ largest = maximum
344
+ if largest not in seen:
345
+ seen.add(largest)
346
+ yield PositiveValue(largest)
347
+
348
+ # One less than maximum if possible
349
+ if multiple_of is not None:
350
+ smaller = largest - multiple_of
351
+ else:
352
+ smaller = maximum - 1
353
+ if smaller not in seen and (smaller > 0 and (minimum is None or smaller >= minimum)):
354
+ seen.add(smaller)
355
+ yield PositiveValue(smaller)
356
+
357
+
358
+ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
359
+ seen = set()
360
+ yield PositiveValue(template)
361
+ seen.add(len(template))
362
+
363
+ # Boundary and near-boundary sizes
364
+ min_items = schema.get("minItems")
365
+ max_items = schema.get("maxItems")
366
+ if min_items is not None:
367
+ # Do not generate an array with `minItems` length, because it is already covered by `template`
368
+
369
+ # One item more than minimum if possible
370
+ larger = min_items + 1
371
+ if larger not in seen and (max_items is None or larger <= max_items):
372
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger}))
373
+ seen.add(larger)
374
+
375
+ if max_items is not None:
376
+ if max_items < BUFFER_SIZE and max_items not in seen:
377
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minItems": max_items}))
378
+ seen.add(max_items)
379
+
380
+ # One item smaller than maximum if possible
381
+ smaller = max_items - 1
382
+ if (
383
+ smaller < BUFFER_SIZE
384
+ and smaller > 0
385
+ and smaller not in seen
386
+ and (min_items is None or smaller >= min_items)
387
+ ):
388
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller}))
389
+ seen.add(smaller)
390
+
391
+
392
+ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
393
+ yield PositiveValue(template)
394
+ # Only required properties
395
+ properties = schema.get("properties", {})
396
+ if set(properties) != set(schema.get("required", {})):
397
+ only_required = {k: v for k, v in template.items() if k in schema.get("required", [])}
398
+ yield PositiveValue(only_required)
399
+ seen = set()
400
+ for name, sub_schema in properties.items():
401
+ seen.add(_to_hashable_key(template.get(name)))
402
+ for new in cover_schema_iter(ctx, sub_schema):
403
+ key = _to_hashable_key(new.value)
404
+ if key not in seen:
405
+ yield PositiveValue({**template, name: new.value})
406
+ seen.add(key)
407
+ seen.clear()
408
+
409
+
410
+ def _negative_enum(ctx: CoverageContext, value: list) -> Generator[GeneratedValue, None, None]:
411
+ strategy = JSON_STRATEGY.filter(lambda x: x not in value)
412
+ # The exact negative value is not important here
413
+ yield NegativeValue(ctx.generate_from(strategy, cached=True))
414
+
415
+
416
+ def _negative_properties(
417
+ ctx: CoverageContext, template: dict, properties: dict
418
+ ) -> Generator[GeneratedValue, None, None]:
419
+ nctx = ctx.with_negative()
420
+ for key, sub_schema in properties.items():
421
+ for value in cover_schema_iter(nctx, sub_schema):
422
+ yield NegativeValue({**template, key: value.value})
423
+
424
+
425
+ def _negative_pattern(ctx: CoverageContext, pattern: str) -> Generator[GeneratedValue, None, None]:
426
+ yield NegativeValue(ctx.generate_from(st.text().filter(lambda x: x != pattern), cached=True))
427
+
428
+
429
+ def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
430
+ return {"allOf": [{k: v for k, v in schema.items() if k != key}, {"not": {key: value}}]}
431
+
432
+
433
+ def _negative_multiple_of(
434
+ ctx: CoverageContext, schema: dict, multiple_of: int | float
435
+ ) -> Generator[GeneratedValue, None, None]:
436
+ yield NegativeValue(ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)))
437
+
438
+
439
+ def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
440
+ unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
441
+ yield NegativeValue(unique + unique)
442
+
443
+
444
+ def _negative_required(
445
+ ctx: CoverageContext, template: dict, required: list[str]
446
+ ) -> Generator[GeneratedValue, None, None]:
447
+ for key in required:
448
+ yield NegativeValue({k: v for k, v in template.items() if k != key})
449
+
450
+
451
+ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator[GeneratedValue, None, None]:
452
+ # Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
453
+ without_format = {k: v for k, v in schema.items() if k != "format"}
454
+ without_format.setdefault("type", "string")
455
+ strategy = from_schema(without_format)
456
+ if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
457
+ strategy = strategy.filter(
458
+ lambda v: (format == "hostname" and v == "")
459
+ or not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format)
460
+ )
461
+ yield NegativeValue(ctx.generate_from(strategy))
462
+
463
+
464
+ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator[GeneratedValue, None, None]:
465
+ strategies = {
466
+ "integer": st.integers(),
467
+ "number": NUMERIC_STRATEGY,
468
+ "boolean": st.booleans(),
469
+ "null": st.none(),
470
+ "string": st.text(),
471
+ "array": ARRAY_STRATEGY,
472
+ "object": OBJECT_STRATEGY,
473
+ }
474
+ if isinstance(ty, str):
475
+ types = [ty]
476
+ else:
477
+ types = ty
478
+ for ty_ in types:
479
+ strategies.pop(ty_)
480
+ if "number" in types:
481
+ del strategies["integer"]
482
+ if "integer" in types:
483
+ strategies["number"] = FLOAT_STRATEGY.filter(lambda x: x != int(x))
484
+ negative_strategy = combine_strategies(tuple(strategies.values())).filter(lambda x: _to_hashable_key(x) not in seen)
485
+ value = ctx.generate_from(negative_strategy, cached=True)
486
+ yield NegativeValue(value)
487
+ seen.add(_to_hashable_key(value))
schemathesis/models.py CHANGED
@@ -125,6 +125,15 @@ def prepare_request_data(kwargs: dict[str, Any]) -> PreparedRequestData:
125
125
  )
126
126
 
127
127
 
128
+ @dataclass
129
+ class TestPhase(Enum):
130
+ __test__ = False
131
+
132
+ EXPLICIT = "explicit"
133
+ COVERAGE = "coverage"
134
+ GENERATE = "generate"
135
+
136
+
128
137
  @dataclass
129
138
  class GenerationMetadata:
130
139
  """Stores various information about how data is generated."""
@@ -134,8 +143,9 @@ class GenerationMetadata:
134
143
  headers: DataGenerationMethod | None
135
144
  cookies: DataGenerationMethod | None
136
145
  body: DataGenerationMethod | None
146
+ phase: TestPhase
137
147
 
138
- __slots__ = ("query", "path_parameters", "headers", "cookies", "body")
148
+ __slots__ = ("query", "path_parameters", "headers", "cookies", "body", "phase")
139
149
 
140
150
 
141
151
  @dataclass(repr=False)
@@ -968,6 +978,7 @@ class Interaction:
968
978
  checks: list[Check]
969
979
  status: Status
970
980
  data_generation_method: DataGenerationMethod
981
+ phase: TestPhase | None
971
982
  recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
972
983
 
973
984
  @classmethod
@@ -978,6 +989,7 @@ class Interaction:
978
989
  status=status,
979
990
  checks=checks,
980
991
  data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
992
+ phase=case.meta.phase if case.meta is not None else None,
981
993
  )
982
994
 
983
995
  @classmethod
@@ -1000,6 +1012,7 @@ class Interaction:
1000
1012
  status=status,
1001
1013
  checks=checks,
1002
1014
  data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
1015
+ phase=case.meta.phase if case.meta is not None else None,
1003
1016
  )
1004
1017
 
1005
1018
 
@@ -28,7 +28,7 @@ from ..exceptions import (
28
28
  make_unique_by_key,
29
29
  )
30
30
  from ..generation import DataGenerationMethod
31
- from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
31
+ from ..models import Case, Check, Interaction, Request, Response, Status, TestPhase, TestResult
32
32
  from ..transports import deserialize_payload, serialize_payload
33
33
 
34
34
  if TYPE_CHECKING:
@@ -385,6 +385,7 @@ class SerializedInteraction:
385
385
  checks: list[SerializedCheck]
386
386
  status: Status
387
387
  data_generation_method: DataGenerationMethod
388
+ phase: TestPhase | None
388
389
  recorded_at: str
389
390
 
390
391
  @classmethod
@@ -395,6 +396,7 @@ class SerializedInteraction:
395
396
  checks=[SerializedCheck.from_check(check) for check in interaction.checks],
396
397
  status=interaction.status,
397
398
  data_generation_method=interaction.data_generation_method,
399
+ phase=interaction.phase,
398
400
  recorded_at=interaction.recorded_at,
399
401
  )
400
402
 
schemathesis/schemas.py CHANGED
@@ -50,6 +50,7 @@ from .generation import (
50
50
  DataGenerationMethod,
51
51
  DataGenerationMethodInput,
52
52
  GenerationConfig,
53
+ combine_strategies,
53
54
  )
54
55
  from .hooks import HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
55
56
  from .internal.deprecation import warn_filtration_arguments
@@ -69,7 +70,7 @@ from .types import (
69
70
  PathParameters,
70
71
  Query,
71
72
  )
72
- from .utils import PARAMETRIZE_MARKER, GivenInput, combine_strategies, given_proxy
73
+ from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy
73
74
 
74
75
  if TYPE_CHECKING:
75
76
  from .transports import Transport
@@ -128,7 +128,7 @@ def _apply_schema_patches_extension(extension: SchemaPatchesExtension, schema: B
128
128
 
129
129
 
130
130
  def strategy_from_definitions(definitions: list[StrategyDefinition]) -> Result[st.SearchStrategy, Exception]:
131
- from ..utils import combine_strategies
131
+ from ..generation import combine_strategies
132
132
 
133
133
  strategies = []
134
134
  for definition in definitions:
@@ -25,7 +25,7 @@ from ...generation import DataGenerationMethod, GenerationConfig
25
25
  from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
26
26
  from ...internal.copy import fast_deepcopy
27
27
  from ...internal.validation import is_illegal_surrogate
28
- from ...models import APIOperation, Case, GenerationMetadata, cant_serialize
28
+ from ...models import APIOperation, Case, GenerationMetadata, TestPhase, cant_serialize
29
29
  from ...serializers import Binary
30
30
  from ...transports.content_types import parse_content_type
31
31
  from ...transports.headers import has_invalid_characters, is_latin_1_encodable
@@ -123,6 +123,7 @@ def get_case_strategy(
123
123
  body: Any = NOT_SET,
124
124
  media_type: str | None = None,
125
125
  skip_on_not_negated: bool = True,
126
+ phase: TestPhase = TestPhase.GENERATE,
126
127
  ) -> Any:
127
128
  """A strategy that creates `Case` instances.
128
129
 
@@ -213,6 +214,7 @@ def get_case_strategy(
213
214
  headers=headers_.generator,
214
215
  cookies=cookies_.generator,
215
216
  body=body_.generator,
217
+ phase=phase,
216
218
  ),
217
219
  )
218
220
  auth_context = auths.AuthContext(
@@ -10,10 +10,10 @@ import requests
10
10
  from hypothesis.strategies import SearchStrategy
11
11
  from hypothesis_jsonschema import from_schema
12
12
 
13
- from ..._hypothesis import get_single_example
14
13
  from ...constants import DEFAULT_RESPONSE_TIMEOUT
14
+ from ...generation import get_single_example
15
15
  from ...internal.copy import fast_deepcopy
16
- from ...models import APIOperation, Case
16
+ from ...models import APIOperation, Case, TestPhase
17
17
  from ._hypothesis import get_case_strategy, get_default_format_strategies
18
18
  from .constants import LOCATION_TO_CONTAINER
19
19
  from .formats import STRING_FORMATS
@@ -67,6 +67,8 @@ def get_strategies_from_examples(
67
67
  examples = list(extract_top_level(operation))
68
68
  # Add examples from parameter's schemas
69
69
  examples.extend(extract_from_schemas(operation))
70
+ as_strategy_kwargs = as_strategy_kwargs or {}
71
+ as_strategy_kwargs["phase"] = TestPhase.EXPLICIT
70
72
  return [
71
73
  get_case_strategy(operation=operation, **{**parameters, **(as_strategy_kwargs or {})}).map(serialize_components)
72
74
  for parameters in produce_combinations(examples)
@@ -1032,6 +1032,7 @@ class SwaggerV20(BaseOpenAPISchema):
1032
1032
  query: Query | None = None,
1033
1033
  body: Body | NotSet = NOT_SET,
1034
1034
  media_type: str | None = None,
1035
+ generation_time: float = 0.0,
1035
1036
  ) -> C:
1036
1037
  if body is not NOT_SET and media_type is None:
1037
1038
  media_type = operation._get_default_media_type()
@@ -1043,7 +1044,7 @@ class SwaggerV20(BaseOpenAPISchema):
1043
1044
  query=query,
1044
1045
  body=body,
1045
1046
  media_type=media_type,
1046
- generation_time=0.0,
1047
+ generation_time=generation_time,
1047
1048
  )
1048
1049
 
1049
1050
  def _get_consumes_for_operation(self, definition: dict[str, Any]) -> list[str]:
@@ -8,11 +8,10 @@ from hypothesis import strategies as st
8
8
  from hypothesis.stateful import Bundle, Rule, precondition, rule
9
9
 
10
10
  from ....constants import NOT_SET
11
- from ....generation import DataGenerationMethod
11
+ from ....generation import DataGenerationMethod, combine_strategies
12
12
  from ....internal.result import Ok
13
13
  from ....stateful.state_machine import APIStateMachine, Direction, StepResult
14
14
  from ....types import NotSet
15
- from ....utils import combine_strategies
16
15
  from .. import expressions
17
16
  from ..links import get_all_links
18
17
  from ..utils import expand_status_code