schemathesis 3.35.0__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.
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import json
6
7
  import warnings
7
8
  from typing import Any, Callable, Generator, Mapping, Optional, Tuple
8
9
 
@@ -19,9 +20,10 @@ from .exceptions import OperationSchemaError, SerializationNotPossible
19
20
  from .experimental import COVERAGE_PHASE
20
21
  from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
21
22
  from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
22
- from .models import APIOperation, Case
23
+ from .models import APIOperation, Case, GenerationMetadata, TestPhase
23
24
  from .transports.content_types import parse_content_type
24
25
  from .transports.headers import has_invalid_characters, is_latin_1_encodable
26
+ from .types import NotSet
25
27
  from .utils import GivenInput
26
28
 
27
29
  # Forcefully initializes Hypothesis' global PRNG to avoid races that initilize it
@@ -215,39 +217,63 @@ def _iter_coverage_cases(
215
217
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
216
218
 
217
219
  ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
218
- generators: dict[tuple[str, str], Generator] = {}
220
+ meta = GenerationMetadata(
221
+ query=None, path_parameters=None, headers=None, cookies=None, body=None, phase=TestPhase.COVERAGE
222
+ )
223
+ generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
219
224
  template: dict[str, Any] = {}
225
+ template_generation_method = DataGenerationMethod.positive
220
226
  for parameter in operation.iter_parameters():
221
227
  schema = parameter.as_json_schema(operation)
222
228
  gen = coverage.cover_schema_iter(ctx, schema)
223
229
  value = next(gen, NOT_SET)
224
- if value is NOT_SET:
230
+ if isinstance(value, NotSet):
225
231
  continue
226
232
  location = parameter.location
227
233
  name = parameter.name
228
234
  container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
229
- container[name] = value
235
+ if location in ("header", "cookie") and not isinstance(value.value, str):
236
+ container[name] = json.dumps(value.value)
237
+ else:
238
+ container[name] = value.value
239
+ template_generation_method = value.data_generation_method
230
240
  generators[(location, name)] = gen
231
241
  if operation.body:
232
242
  for body in operation.body:
233
243
  schema = body.as_json_schema(operation)
234
244
  gen = coverage.cover_schema_iter(ctx, schema)
235
245
  value = next(gen, NOT_SET)
236
- if value is NOT_SET:
246
+ if isinstance(value, NotSet):
237
247
  continue
238
248
  if "body" not in template:
239
- template["body"] = value
249
+ template["body"] = value.value
240
250
  template["media_type"] = body.media_type
241
- yield operation.make_case(**{**template, "body": value, "media_type": body.media_type})
251
+ case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
252
+ case.data_generation_method = value.data_generation_method
253
+ case.meta = meta
254
+ yield case
242
255
  for next_value in gen:
243
- yield operation.make_case(**{**template, "body": next_value, "media_type": body.media_type})
256
+ case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
257
+ case.data_generation_method = next_value.data_generation_method
258
+ case.meta = meta
259
+ yield case
244
260
  else:
245
- yield operation.make_case(**template)
261
+ case = operation.make_case(**template)
262
+ case.data_generation_method = template_generation_method
263
+ case.meta = meta
264
+ yield case
246
265
  for (location, name), gen in generators.items():
247
266
  container_name = LOCATION_TO_CONTAINER[location]
248
267
  container = template[container_name]
249
268
  for value in gen:
250
- yield operation.make_case(**{**template, container_name: {**container, name: value}})
269
+ if location in ("header", "cookie") and not isinstance(value.value, str):
270
+ generated = json.dumps(value.value)
271
+ else:
272
+ generated = value.value
273
+ case = operation.make_case(**{**template, container_name: {**container, name: generated}})
274
+ case.data_generation_method = value.data_generation_method
275
+ case.meta = meta
276
+ yield case
251
277
 
252
278
 
253
279
  def find_invalid_headers(headers: Mapping) -> Generator[Tuple[str, str], None, None]:
@@ -226,6 +226,7 @@ http_interactions:"""
226
226
  for interaction in item.interactions:
227
227
  status = interaction.status.name.upper()
228
228
  # Body payloads are handled via separate `stream.write` calls to avoid some allocations
229
+ phase = f"'{interaction.phase.value}'" if interaction.phase is not None else "null"
229
230
  stream.write(
230
231
  f"""\n- id: '{current_id}'
231
232
  status: '{status}'
@@ -233,6 +234,7 @@ http_interactions:"""
233
234
  thread_id: {item.thread_id}
234
235
  correlation_id: '{item.correlation_id}'
235
236
  data_generation_method: '{interaction.data_generation_method.value}'
237
+ phase: {phase}
236
238
  elapsed: '{interaction.response.elapsed}'
237
239
  recorded_at: '{interaction.recorded_at}'
238
240
  checks:
@@ -1,17 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from contextlib import suppress
4
+ from contextlib import contextmanager, suppress
5
5
  from dataclasses import dataclass, field
6
6
  from functools import lru_cache
7
- from typing import Any, Generator, Set, TypeVar, cast
7
+ from typing import Any, Generator, Set, Type, TypeVar, cast
8
8
 
9
9
  import jsonschema
10
10
  from hypothesis import strategies as st
11
- from hypothesis.errors import Unsatisfiable
11
+ from hypothesis.errors import InvalidArgument, Unsatisfiable
12
12
  from hypothesis_jsonschema import from_schema
13
13
  from hypothesis_jsonschema._canonicalise import canonicalish
14
14
 
15
+ from schemathesis.constants import NOT_SET
16
+
15
17
  from ._hypothesis import combine_strategies, get_single_example
16
18
  from ._methods import DataGenerationMethod
17
19
 
@@ -29,6 +31,26 @@ UNKNOWN_PROPERTY_KEY = "x-schemathesis-unknown-property"
29
31
  UNKNOWN_PROPERTY_VALUE = 42
30
32
 
31
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
+
32
54
  @lru_cache(maxsize=128)
33
55
  def cached_draw(strategy: st.SearchStrategy) -> Any:
34
56
  return get_single_example(strategy)
@@ -67,11 +89,9 @@ def _to_hashable_key(value: T) -> T | tuple[type, str]:
67
89
  return value
68
90
 
69
91
 
70
- def cover_schema(ctx: CoverageContext, schema: dict) -> list:
71
- return list(cover_schema_iter(ctx, schema))
72
-
73
-
74
- def _cover_positive_for_type(ctx: CoverageContext, schema: dict, ty: str | None) -> Generator:
92
+ def _cover_positive_for_type(
93
+ ctx: CoverageContext, schema: dict, ty: str | None
94
+ ) -> Generator[GeneratedValue, None, None]:
75
95
  if ty == "object" or ty == "array":
76
96
  template_schema = _get_template_schema(schema, ty)
77
97
  template = ctx.generate_from_schema(template_schema)
@@ -79,8 +99,8 @@ def _cover_positive_for_type(ctx: CoverageContext, schema: dict, ty: str | None)
79
99
  template = None
80
100
  if DataGenerationMethod.positive in ctx.data_generation_methods:
81
101
  ctx = ctx.with_positive()
82
- enum = schema.get("enum")
83
- const = schema.get("const")
102
+ enum = schema.get("enum", NOT_SET)
103
+ const = schema.get("const", NOT_SET)
84
104
  for key in ("anyOf", "oneOf"):
85
105
  sub_schemas = schema.get(key)
86
106
  if sub_schemas is not None:
@@ -91,43 +111,70 @@ def _cover_positive_for_type(ctx: CoverageContext, schema: dict, ty: str | None)
91
111
  if len(all_of) == 1:
92
112
  yield from cover_schema_iter(ctx, all_of[0])
93
113
  else:
94
- canonical = canonicalish(schema)
95
- yield from cover_schema_iter(ctx, canonical)
96
- if enum is not None:
97
- yield from enum
98
- elif const is not None:
99
- yield const
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)
100
122
  elif ty is not None:
101
123
  if ty == "null":
102
- yield None
103
- if ty == "boolean":
104
- yield True
105
- yield False
106
- if ty == "string":
124
+ yield PositiveValue(None)
125
+ elif ty == "boolean":
126
+ yield PositiveValue(True)
127
+ yield PositiveValue(False)
128
+ elif ty == "string":
107
129
  yield from _positive_string(ctx, schema)
108
- if ty == "integer" or ty == "number":
130
+ elif ty == "integer" or ty == "number":
109
131
  yield from _positive_number(ctx, schema)
110
- if ty == "array":
132
+ elif ty == "array":
111
133
  yield from _positive_array(ctx, schema, cast(list, template))
112
- if ty == "object":
134
+ elif ty == "object":
113
135
  yield from _positive_object(ctx, schema, cast(dict, template))
114
136
 
115
137
 
116
- def cover_schema_iter(ctx: CoverageContext, schema: dict) -> Generator:
117
- types = schema.get("type", [])
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", [])
118
165
  if not isinstance(types, list):
119
- types = [types]
166
+ types = [types] # type: ignore[unreachable]
120
167
  if not types:
121
- with suppress(Unsatisfiable, jsonschema.RefResolutionError):
168
+ with _ignore_unfixable():
122
169
  yield from _cover_positive_for_type(ctx, schema, None)
123
170
  for ty in types:
124
- with suppress(Unsatisfiable, jsonschema.RefResolutionError):
171
+ with _ignore_unfixable():
125
172
  yield from _cover_positive_for_type(ctx, schema, ty)
126
173
  if DataGenerationMethod.negative in ctx.data_generation_methods:
127
174
  template = None
128
175
  seen: Set[Any | tuple[type, str]] = set()
129
176
  for key, value in schema.items():
130
- with suppress(Unsatisfiable, jsonschema.RefResolutionError):
177
+ with _ignore_unfixable():
131
178
  if key == "enum":
132
179
  yield from _negative_enum(ctx, value)
133
180
  elif key == "const":
@@ -143,21 +190,27 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict) -> Generator:
143
190
  yield from _negative_format(ctx, schema, value)
144
191
  elif key == "maximum":
145
192
  next = value + 1
146
- yield next
193
+ yield NegativeValue(next)
147
194
  seen.add(next)
148
195
  elif key == "minimum":
149
196
  next = value - 1
150
- yield next
197
+ yield NegativeValue(next)
151
198
  seen.add(next)
152
199
  elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
153
- yield value
200
+ yield NegativeValue(value)
154
201
  seen.add(value)
155
202
  elif key == "multipleOf":
156
203
  yield from _negative_multiple_of(ctx, schema, value)
157
204
  elif key == "minLength" and 0 < value < BUFFER_SIZE and "pattern" not in schema:
158
- yield ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
205
+ with suppress(InvalidArgument):
206
+ yield NegativeValue(
207
+ ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
208
+ )
159
209
  elif key == "maxLength" and value < BUFFER_SIZE and "pattern" not in schema:
160
- yield ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
210
+ with suppress(InvalidArgument):
211
+ yield NegativeValue(
212
+ ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
213
+ )
161
214
  elif key == "uniqueItems" and value:
162
215
  yield from _negative_unique_items(ctx, schema)
163
216
  elif key == "required":
@@ -165,14 +218,15 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict) -> Generator:
165
218
  yield from _negative_required(ctx, template, value)
166
219
  elif key == "additionalProperties" and not value:
167
220
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
168
- yield {**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE}
221
+ yield NegativeValue({**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE})
169
222
  elif key == "allOf":
170
223
  nctx = ctx.with_negative()
171
224
  if len(value) == 1:
172
225
  yield from cover_schema_iter(nctx, value[0])
173
226
  else:
174
- canonical = canonicalish(schema)
175
- yield from cover_schema_iter(nctx, canonical)
227
+ with _ignore_unfixable():
228
+ canonical = canonicalish(schema)
229
+ yield from cover_schema_iter(nctx, canonical)
176
230
  elif key == "anyOf" or key == "oneOf":
177
231
  nctx = ctx.with_negative()
178
232
  # NOTE: Other sub-schemas are not filtered out
@@ -189,14 +243,14 @@ def _get_template_schema(schema: dict, ty: str) -> dict:
189
243
  "required": list(properties),
190
244
  "type": ty,
191
245
  "properties": {
192
- k: _get_template_schema(v, "object") if v.get("type") == "object" else v
246
+ k: _get_template_schema(v, "object") if isinstance(v, dict) and v.get("type") == "object" else v
193
247
  for k, v in properties.items()
194
248
  },
195
249
  }
196
250
  return {**schema, "type": ty}
197
251
 
198
252
 
199
- def _positive_string(ctx: CoverageContext, schema: dict) -> Generator:
253
+ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
200
254
  """Generate positive string values."""
201
255
  # Boundary and near boundary values
202
256
  min_length = schema.get("minLength")
@@ -204,25 +258,25 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator:
204
258
 
205
259
  if not min_length and not max_length:
206
260
  # Default positive value
207
- yield ctx.generate_from_schema(schema)
261
+ yield PositiveValue(ctx.generate_from_schema(schema))
208
262
 
209
263
  seen = set()
210
264
 
211
265
  if min_length is not None and min_length < BUFFER_SIZE and "pattern" not in schema:
212
266
  # Exactly the minimum length
213
- yield ctx.generate_from_schema({**schema, "maxLength": min_length})
267
+ yield PositiveValue(ctx.generate_from_schema({**schema, "maxLength": min_length}))
214
268
  seen.add(min_length)
215
269
 
216
270
  # One character more than minimum if possible
217
271
  larger = min_length + 1
218
272
  if larger < BUFFER_SIZE and larger not in seen and (not max_length or larger <= max_length):
219
- yield ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger})
273
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger}))
220
274
  seen.add(larger)
221
275
 
222
276
  if max_length is not None and "pattern" not in schema:
223
277
  # Exactly the maximum length
224
278
  if max_length < BUFFER_SIZE and max_length not in seen:
225
- yield ctx.generate_from_schema({**schema, "minLength": max_length})
279
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": max_length}))
226
280
  seen.add(max_length)
227
281
 
228
282
  # One character less than maximum if possible
@@ -232,7 +286,7 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator:
232
286
  and smaller not in seen
233
287
  and (smaller > 0 and (min_length is None or smaller >= min_length))
234
288
  ):
235
- yield ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller})
289
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller}))
236
290
  seen.add(smaller)
237
291
 
238
292
 
@@ -244,7 +298,7 @@ def closest_multiple_greater_than(y: int, x: int) -> int:
244
298
  return x * (quotient + 1)
245
299
 
246
300
 
247
- def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
301
+ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
248
302
  """Generate positive integer values."""
249
303
  # Boundary and near boundary values
250
304
  minimum = schema.get("minimum")
@@ -259,7 +313,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
259
313
 
260
314
  if not minimum and not maximum:
261
315
  # Default positive value
262
- yield ctx.generate_from_schema(schema)
316
+ yield PositiveValue(ctx.generate_from_schema(schema))
263
317
 
264
318
  seen = set()
265
319
 
@@ -270,7 +324,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
270
324
  else:
271
325
  smallest = minimum
272
326
  seen.add(smallest)
273
- yield smallest
327
+ yield PositiveValue(smallest)
274
328
 
275
329
  # One more than minimum if possible
276
330
  if multiple_of is not None:
@@ -279,7 +333,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
279
333
  larger = minimum + 1
280
334
  if larger not in seen and (not maximum or larger <= maximum):
281
335
  seen.add(larger)
282
- yield larger
336
+ yield PositiveValue(larger)
283
337
 
284
338
  if maximum is not None:
285
339
  # Exactly the maximum
@@ -289,7 +343,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
289
343
  largest = maximum
290
344
  if largest not in seen:
291
345
  seen.add(largest)
292
- yield largest
346
+ yield PositiveValue(largest)
293
347
 
294
348
  # One less than maximum if possible
295
349
  if multiple_of is not None:
@@ -298,12 +352,12 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
298
352
  smaller = maximum - 1
299
353
  if smaller not in seen and (smaller > 0 and (minimum is None or smaller >= minimum)):
300
354
  seen.add(smaller)
301
- yield smaller
355
+ yield PositiveValue(smaller)
302
356
 
303
357
 
304
- def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator:
358
+ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
305
359
  seen = set()
306
- yield template
360
+ yield PositiveValue(template)
307
361
  seen.add(len(template))
308
362
 
309
363
  # Boundary and near-boundary sizes
@@ -315,99 +369,99 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
315
369
  # One item more than minimum if possible
316
370
  larger = min_items + 1
317
371
  if larger not in seen and (max_items is None or larger <= max_items):
318
- yield ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
372
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger}))
319
373
  seen.add(larger)
320
374
 
321
375
  if max_items is not None:
322
- if max_items not in seen:
323
- yield ctx.generate_from_schema({**schema, "minItems": max_items})
376
+ if max_items < BUFFER_SIZE and max_items not in seen:
377
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minItems": max_items}))
324
378
  seen.add(max_items)
325
379
 
326
380
  # One item smaller than maximum if possible
327
381
  smaller = max_items - 1
328
- if smaller > 0 and smaller not in seen and (min_items is None or smaller >= min_items):
329
- yield ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
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}))
330
389
  seen.add(smaller)
331
390
 
332
391
 
333
- def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator:
334
- yield template
392
+ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
393
+ yield PositiveValue(template)
335
394
  # Only required properties
336
395
  properties = schema.get("properties", {})
337
396
  if set(properties) != set(schema.get("required", {})):
338
397
  only_required = {k: v for k, v in template.items() if k in schema.get("required", [])}
339
- yield only_required
398
+ yield PositiveValue(only_required)
340
399
  seen = set()
341
400
  for name, sub_schema in properties.items():
342
401
  seen.add(_to_hashable_key(template.get(name)))
343
402
  for new in cover_schema_iter(ctx, sub_schema):
344
- key = _to_hashable_key(new)
403
+ key = _to_hashable_key(new.value)
345
404
  if key not in seen:
346
- yield {**template, name: new}
405
+ yield PositiveValue({**template, name: new.value})
347
406
  seen.add(key)
348
407
  seen.clear()
349
408
 
350
409
 
351
- @lru_cache(maxsize=128)
352
- def _get_negative_enum_strategy(value: tuple) -> st.SearchStrategy:
353
- return JSON_STRATEGY.filter(lambda x: x not in value)
354
-
355
-
356
- def _negative_enum(ctx: CoverageContext, value: list) -> Generator:
357
- try:
358
- strategy = _get_negative_enum_strategy(tuple(value))
359
- except TypeError:
360
- # The value is not hashable
361
- strategy = JSON_STRATEGY.filter(lambda x: x not in value)
410
+ def _negative_enum(ctx: CoverageContext, value: list) -> Generator[GeneratedValue, None, None]:
411
+ strategy = JSON_STRATEGY.filter(lambda x: x not in value)
362
412
  # The exact negative value is not important here
363
- yield ctx.generate_from(strategy, cached=True)
413
+ yield NegativeValue(ctx.generate_from(strategy, cached=True))
364
414
 
365
415
 
366
- def _negative_properties(ctx: CoverageContext, template: dict, properties: dict) -> Generator:
416
+ def _negative_properties(
417
+ ctx: CoverageContext, template: dict, properties: dict
418
+ ) -> Generator[GeneratedValue, None, None]:
367
419
  nctx = ctx.with_negative()
368
420
  for key, sub_schema in properties.items():
369
421
  for value in cover_schema_iter(nctx, sub_schema):
370
- yield {**template, key: value}
371
-
372
-
373
- @lru_cache(maxsize=128)
374
- def _get_negative_pattern_strategy(value: str) -> st.SearchStrategy:
375
- return st.text().filter(lambda x: x != value)
422
+ yield NegativeValue({**template, key: value.value})
376
423
 
377
424
 
378
- def _negative_pattern(ctx: CoverageContext, pattern: str) -> Generator:
379
- yield ctx.generate_from(_get_negative_pattern_strategy(pattern), cached=True)
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))
380
427
 
381
428
 
382
429
  def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
383
430
  return {"allOf": [{k: v for k, v in schema.items() if k != key}, {"not": {key: value}}]}
384
431
 
385
432
 
386
- def _negative_multiple_of(ctx: CoverageContext, schema: dict, multiple_of: int | float) -> Generator:
387
- yield ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of))
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)))
388
437
 
389
438
 
390
- def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator:
439
+ def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
391
440
  unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
392
- yield unique + unique
441
+ yield NegativeValue(unique + unique)
393
442
 
394
443
 
395
- def _negative_required(ctx: CoverageContext, template: dict, required: list[str]) -> Generator:
444
+ def _negative_required(
445
+ ctx: CoverageContext, template: dict, required: list[str]
446
+ ) -> Generator[GeneratedValue, None, None]:
396
447
  for key in required:
397
- yield {k: v for k, v in template.items() if k != key}
448
+ yield NegativeValue({k: v for k, v in template.items() if k != key})
398
449
 
399
450
 
400
- def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator:
451
+ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator[GeneratedValue, None, None]:
401
452
  # Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
402
453
  without_format = {k: v for k, v in schema.items() if k != "format"}
403
454
  without_format.setdefault("type", "string")
404
455
  strategy = from_schema(without_format)
405
456
  if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
406
- strategy = strategy.filter(lambda v: not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format))
407
- yield ctx.generate_from(strategy)
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))
408
462
 
409
463
 
410
- def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator:
464
+ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator[GeneratedValue, None, None]:
411
465
  strategies = {
412
466
  "integer": st.integers(),
413
467
  "number": NUMERIC_STRATEGY,
@@ -429,5 +483,5 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
429
483
  strategies["number"] = FLOAT_STRATEGY.filter(lambda x: x != int(x))
430
484
  negative_strategy = combine_strategies(tuple(strategies.values())).filter(lambda x: _to_hashable_key(x) not in seen)
431
485
  value = ctx.generate_from(negative_strategy, cached=True)
432
- yield value
486
+ yield NegativeValue(value)
433
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
 
@@ -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(
@@ -13,7 +13,7 @@ from hypothesis_jsonschema import from_schema
13
13
  from ...constants import DEFAULT_RESPONSE_TIMEOUT
14
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: schemathesis
3
- Version: 3.35.0
3
+ Version: 3.35.1
4
4
  Summary: Property-based testing framework for Open API and GraphQL based apps
5
5
  Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
6
6
  Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
@@ -1,7 +1,7 @@
1
1
  schemathesis/__init__.py,sha256=pNaTfaC3NSdediNQuH9QAcuIx3U-MmSydvhS65FZrxw,1984
2
2
  schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
3
3
  schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
4
- schemathesis/_hypothesis.py,sha256=eLDbqhG2fBJ6DuTxXlRfadnHhFc3JE2P5zf0YS9UonQ,12782
4
+ schemathesis/_hypothesis.py,sha256=eRbPWW9WwFJBEsb3nkhZGSYisKfwTJscNlh6x5s5Yvo,14055
5
5
  schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
6
6
  schemathesis/_override.py,sha256=3CbA7P9Q89W3ymaYxiOV5Xpv1yhoBqroLK4YRpYMjX4,1630
7
7
  schemathesis/_rate_limiter.py,sha256=q_XWst5hzuAyXQRiZc4s_bx7-JlPYZM_yKDmeavt3oo,242
@@ -17,7 +17,7 @@ schemathesis/graphql.py,sha256=YkoKWY5K8lxp7H3ikAs-IsoDbiPwJvChG7O8p3DgwtI,229
17
17
  schemathesis/hooks.py,sha256=Uv9rZHqM2bpb_uYBjf4kqsMeu7XdLOHpRWSaN43xIgw,14774
18
18
  schemathesis/lazy.py,sha256=hGwSuWe5tDaGpjZTV4Mj8zqdrHDYHxR22N2p5h2yh1g,18897
19
19
  schemathesis/loaders.py,sha256=OtCD1o0TVmSNAUF7dgHpouoAXtY6w9vEtsRVGv4lE0g,4588
20
- schemathesis/models.py,sha256=WpF-W592_MVWnMZ8t7wt_A2fr3YNLEwPX3r-jwW8GpY,44266
20
+ schemathesis/models.py,sha256=ZA-neOIY3Q6-BLvX9B3TLUO6E-coHCkqcz4S8uDoSAM,44600
21
21
  schemathesis/parameters.py,sha256=PndmqQRlEYsCt1kWjSShPsFf6vj7X_7FRdz_-A95eNg,2258
22
22
  schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  schemathesis/sanitization.py,sha256=_qSt04f_XcHrgguyUnowvdfj-b6u409Ubu07i0ivQUQ,9011
@@ -30,7 +30,7 @@ schemathesis/utils.py,sha256=bYvB3l1iMxiUNHu7_1qhOj5gJf_8QUssL4Uoqgrux9A,4878
30
30
  schemathesis/cli/__init__.py,sha256=O2VIpE9EWFrklWlVTTFUQGShxN8Ij9nbaOJFslDEmHY,73976
31
31
  schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
32
32
  schemathesis/cli/callbacks.py,sha256=PJs64n6qGrGC5Yv_yl3fGm797cvN6pp2enFLmSljGKs,15127
33
- schemathesis/cli/cassettes.py,sha256=lKKKKLjoVT8VnZXjCOCmDYhvLKn1XSrCXCMF9EeftHA,18498
33
+ schemathesis/cli/cassettes.py,sha256=KyODNf0r1ieLe3A-CGcU2EEkQYr0fgVut27AOl54MOM,18615
34
34
  schemathesis/cli/constants.py,sha256=wk-0GsoJIel8wFFerQ6Kf_6eAYUtIWkwMFwyAqv3yj4,1635
35
35
  schemathesis/cli/context.py,sha256=6OYpSbeRobkdyDGvSj_2_zwEelM9K6fXpUCUjVC2sQM,2243
36
36
  schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
@@ -60,7 +60,7 @@ schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZE
60
60
  schemathesis/generation/__init__.py,sha256=IzldWIswXBjCUaVInvXDoaXIrUbtZIU5cisnWcg2IX8,1609
61
61
  schemathesis/generation/_hypothesis.py,sha256=Qel0mBsZV6tOEspRGfbJKFZevaMgHJjzY1F0Oo1bP_Y,1408
62
62
  schemathesis/generation/_methods.py,sha256=jCK09f4sedDfePrS-6BIiE-CcEE8fJ4ZHxq1BHoTltQ,1101
63
- schemathesis/generation/coverage.py,sha256=_g4jcuNTZ7emudQklcHcnkwNZeP1QBo1smNkGZHeCMo,17176
63
+ schemathesis/generation/coverage.py,sha256=A-U4qHT6y9oBjuLxDvRZd7l227zLh-c15ai14pifUxo,19642
64
64
  schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
65
65
  schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
66
66
  schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
@@ -74,7 +74,7 @@ schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QA
74
74
  schemathesis/runner/__init__.py,sha256=-aedUaRBCiTiaooC0OsBbdi4XP8APFOpj6eOzwt5nQ8,21366
75
75
  schemathesis/runner/events.py,sha256=F3TizErDI490cDakpyeCbyK9IOecWpuToQ1HGXvBXzo,11531
76
76
  schemathesis/runner/probes.py,sha256=no5AfO3kse25qvHevjeUfB0Q3C860V2AYzschUW3QMQ,5688
77
- schemathesis/runner/serialization.py,sha256=erbXEHyI8rIlkQ42AwgmlH7aAbh313EPqCEfrGKxUls,20040
77
+ schemathesis/runner/serialization.py,sha256=C8F_2KiszQx16DkWm37viN_AJqLi-MF3YyvkdaFSmCs,20116
78
78
  schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
79
79
  schemathesis/runner/impl/core.py,sha256=H4sYY59kMXQPeOD4BH3j47Ymvix71FSgyQh36flrQxM,45605
80
80
  schemathesis/runner/impl/solo.py,sha256=N7-pUL6nWGiSRUC4Zqy1T4h99vbeQowP6b6cMnobOow,3042
@@ -102,12 +102,12 @@ schemathesis/specs/graphql/schemas.py,sha256=L7u73YXnmqypghWhmj5FaGUAmU57IporT9N
102
102
  schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzMSpnVoRWvxy0,1635
103
103
  schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
104
104
  schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
105
- schemathesis/specs/openapi/_hypothesis.py,sha256=XtC-rYiH-GHvWykdSSzPFVK7jHNNL7NtibjSEeIZDQw,24122
105
+ schemathesis/specs/openapi/_hypothesis.py,sha256=XgKq36ONJIWM-8ASnDpzOgcCcVz-uUQw74bOxcUC3n8,24201
106
106
  schemathesis/specs/openapi/checks.py,sha256=LiwoL5W_qK40j-JFSc9hfM8IGSszMBUWe71YZJ5FBzw,19931
107
107
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
108
108
  schemathesis/specs/openapi/converter.py,sha256=TaYgc5BBHPdkN-n0lqpbeVgLu3eL3L8Wu3y_Vo3TJaQ,2800
109
109
  schemathesis/specs/openapi/definitions.py,sha256=Z186F0gNBSCmPg-Kk7Q-n6XxEZHIOzgUyeqixlC62XE,94058
110
- schemathesis/specs/openapi/examples.py,sha256=aE1OsR4X6OBSgRnFD0mVB2aQ3mWe65xiL2YsLb6xkeU,16169
110
+ schemathesis/specs/openapi/examples.py,sha256=_6vqwVfGuPaJ9GTqmlk6siiS0pikk5wxelLsFQJlaEc,16283
111
111
  schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
112
112
  schemathesis/specs/openapi/links.py,sha256=DCOu14VOFqKYYFbQJHWICDpmTBzJfeP2v2FXBwW3vBI,17531
113
113
  schemathesis/specs/openapi/loaders.py,sha256=AcpvTK8qdirSRcHcinCjQbwfSQSx448LAh_GvFML1C0,25515
@@ -147,8 +147,8 @@ schemathesis/transports/auth.py,sha256=yELjkEkfx4g74hNrd0Db9aFf0xDJDRIwhg2vzKOTZ
147
147
  schemathesis/transports/content_types.py,sha256=VrcRQvF5T_TUjrCyrZcYF2LOwKfs3IrLcMtkVSp1ImI,2189
148
148
  schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
149
149
  schemathesis/transports/responses.py,sha256=6-gvVcRK0Ho_lSydUysBNFWoJwZEiEgf6Iv-GWkQGd8,1675
150
- schemathesis-3.35.0.dist-info/METADATA,sha256=hljiaQNfbUOdt0Dt5mIuOUzyd_Qn4gkFJDWhW2gIev8,19287
151
- schemathesis-3.35.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
152
- schemathesis-3.35.0.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
153
- schemathesis-3.35.0.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
154
- schemathesis-3.35.0.dist-info/RECORD,,
150
+ schemathesis-3.35.1.dist-info/METADATA,sha256=LSbI2JYysmP_4r_z_-lan3zPZW1ymjSoooGoQ81W0R0,19287
151
+ schemathesis-3.35.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
152
+ schemathesis-3.35.1.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
153
+ schemathesis-3.35.1.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
154
+ schemathesis-3.35.1.dist-info/RECORD,,