schemathesis 4.0.13__py3-none-any.whl → 4.0.14__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.
@@ -36,6 +36,15 @@ def schemathesis(ctx: click.Context, config_file: str | None) -> None:
36
36
  config = SchemathesisConfig.from_path(config_file)
37
37
  else:
38
38
  config = SchemathesisConfig.discover()
39
+ except FileNotFoundError:
40
+ display_header(SCHEMATHESIS_VERSION)
41
+ click.secho(
42
+ f"❌ Failed to load configuration file from {config_file}",
43
+ fg="red",
44
+ bold=True,
45
+ )
46
+ click.echo("\nThe configuration file does not exist")
47
+ ctx.exit(1)
39
48
  except (TOMLDecodeError, ConfigError) as exc:
40
49
  display_header(SCHEMATHESIS_VERSION)
41
50
  click.secho(
@@ -9,7 +9,7 @@ from click.utils import LazyFile
9
9
  from schemathesis.checks import CHECKS, load_all_checks
10
10
  from schemathesis.cli.commands.run import executor, validation
11
11
  from schemathesis.cli.commands.run.filters import with_filters
12
- from schemathesis.cli.constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
12
+ from schemathesis.cli.constants import MAX_WORKERS, MIN_WORKERS
13
13
  from schemathesis.cli.core import ensure_color
14
14
  from schemathesis.cli.ext.groups import group, grouped_option
15
15
  from schemathesis.cli.ext.options import (
@@ -62,7 +62,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
62
62
  ["auto", *list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1)))],
63
63
  choices_repr=f"[auto, {MIN_WORKERS}-{MAX_WORKERS}]",
64
64
  ),
65
- default=str(DEFAULT_WORKERS),
65
+ default=None,
66
66
  show_default=True,
67
67
  callback=validation.convert_workers,
68
68
  metavar="",
@@ -172,7 +172,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
172
172
  help="Skip deprecated operations",
173
173
  is_flag=True,
174
174
  is_eager=True,
175
- default=False,
175
+ default=None,
176
176
  show_default=True,
177
177
  )
178
178
  @group("Network requests options")
@@ -206,7 +206,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
206
206
  "request_tls_verify",
207
207
  help="Path to CA bundle for TLS verification, or 'false' to disable",
208
208
  type=str,
209
- default="true",
209
+ default=None,
210
210
  show_default=True,
211
211
  callback=validation.convert_boolean_string,
212
212
  )
@@ -280,13 +280,13 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
280
280
  help="Retain exact byte sequence of payloads in cassettes, encoded as base64",
281
281
  type=bool,
282
282
  is_flag=True,
283
- default=False,
283
+ default=None,
284
284
  callback=validation.validate_preserve_bytes,
285
285
  )
286
286
  @grouped_option(
287
287
  "--output-sanitize",
288
288
  type=str,
289
- default="true",
289
+ default=None,
290
290
  show_default=True,
291
291
  help="Enable or disable automatic output sanitization to obscure sensitive data",
292
292
  metavar="BOOLEAN",
@@ -296,7 +296,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
296
296
  "--output-truncate",
297
297
  help="Truncate schemas and responses in error messages",
298
298
  type=str,
299
- default="true",
299
+ default=None,
300
300
  show_default=True,
301
301
  metavar="BOOLEAN",
302
302
  callback=validation.convert_boolean_string,
@@ -331,20 +331,21 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
331
331
  "generation_no_shrink",
332
332
  help="Disable test case shrinking. Makes test failures harder to debug but improves performance",
333
333
  is_flag=True,
334
+ default=None,
334
335
  )
335
336
  @grouped_option(
336
337
  "--generation-deterministic",
337
338
  help="Enables deterministic mode, which eliminates random variation between tests",
338
339
  is_flag=True,
339
340
  is_eager=True,
340
- default=False,
341
+ default=None,
341
342
  show_default=True,
342
343
  )
343
344
  @grouped_option(
344
345
  "--generation-allow-x00",
345
346
  help="Whether to allow the generation of 'NULL' bytes within strings",
346
347
  type=str,
347
- default="true",
348
+ default=None,
348
349
  show_default=True,
349
350
  metavar="BOOLEAN",
350
351
  callback=validation.convert_boolean_string,
@@ -353,7 +354,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
353
354
  "--generation-codec",
354
355
  help="The codec used for generating strings",
355
356
  type=str,
356
- default="utf-8",
357
+ default=None,
357
358
  callback=validation.validate_generation_codec,
358
359
  )
359
360
  @grouped_option(
@@ -371,7 +372,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
371
372
  "--generation-with-security-parameters",
372
373
  help="Whether to generate security parameters",
373
374
  type=str,
374
- default="true",
375
+ default=None,
375
376
  show_default=True,
376
377
  callback=validation.convert_boolean_string,
377
378
  metavar="BOOLEAN",
@@ -380,7 +381,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
380
381
  "--generation-graphql-allow-null",
381
382
  help="Whether to use `null` values for optional arguments in GraphQL queries",
382
383
  type=str,
383
- default="true",
384
+ default=None,
384
385
  show_default=True,
385
386
  callback=validation.convert_boolean_string,
386
387
  metavar="BOOLEAN",
@@ -398,7 +399,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
398
399
  "generation_unique_inputs",
399
400
  help="Force the generation of unique test cases",
400
401
  is_flag=True,
401
- default=False,
402
+ default=None,
402
403
  show_default=True,
403
404
  metavar="BOOLEAN",
404
405
  )
@@ -440,15 +441,15 @@ def run(
440
441
  exclude_operation_id_regex: str | None,
441
442
  include_by: Callable | None = None,
442
443
  exclude_by: Callable | None = None,
443
- exclude_deprecated: bool = False,
444
- workers: int = DEFAULT_WORKERS,
444
+ exclude_deprecated: bool | None = None,
445
+ workers: int | None = None,
445
446
  base_url: str | None,
446
447
  wait_for_schema: float | None = None,
447
448
  suppress_health_check: list[HealthCheck] | None,
448
449
  warnings: bool | list[SchemathesisWarning] | None,
449
450
  rate_limit: str | None = None,
450
451
  request_timeout: int | None = None,
451
- request_tls_verify: bool = True,
452
+ request_tls_verify: bool | None = None,
452
453
  request_cert: str | None = None,
453
454
  request_cert_key: str | None = None,
454
455
  request_proxy: str | None = None,
@@ -457,21 +458,21 @@ def run(
457
458
  report_junit_path: LazyFile | None = None,
458
459
  report_vcr_path: LazyFile | None = None,
459
460
  report_har_path: LazyFile | None = None,
460
- report_preserve_bytes: bool = False,
461
- output_sanitize: bool = True,
462
- output_truncate: bool = True,
461
+ report_preserve_bytes: bool | None = None,
462
+ output_sanitize: bool | None = None,
463
+ output_truncate: bool | None = None,
463
464
  generation_modes: list[GenerationMode],
464
465
  generation_seed: int | None = None,
465
466
  generation_max_examples: int | None = None,
466
467
  generation_maximize: list[MetricFunction] | None,
467
- generation_deterministic: bool = False,
468
+ generation_deterministic: bool | None = None,
468
469
  generation_database: str | None = None,
469
- generation_unique_inputs: bool = False,
470
- generation_allow_x00: bool = True,
471
- generation_graphql_allow_null: bool = True,
472
- generation_with_security_parameters: bool = True,
473
- generation_codec: str = "utf-8",
474
- generation_no_shrink: bool = False,
470
+ generation_unique_inputs: bool | None = None,
471
+ generation_allow_x00: bool | None = None,
472
+ generation_graphql_allow_null: bool | None = None,
473
+ generation_with_security_parameters: bool | None = None,
474
+ generation_codec: str | None = None,
475
+ generation_no_shrink: bool | None = None,
475
476
  force_color: bool = False,
476
477
  no_color: bool = False,
477
478
  **__kwargs: Any,
@@ -73,7 +73,11 @@ def validate_base_url(ctx: click.core.Context, param: click.core.Parameter, raw_
73
73
  return raw_value
74
74
 
75
75
 
76
- def validate_generation_codec(ctx: click.core.Context, param: click.core.Parameter, raw_value: str) -> str:
76
+ def validate_generation_codec(
77
+ ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
78
+ ) -> str | None:
79
+ if raw_value is None:
80
+ return raw_value
77
81
  try:
78
82
  codecs.getencoder(raw_value)
79
83
  except LookupError as exc:
@@ -214,7 +218,11 @@ def convert_generation_mode(ctx: click.core.Context, param: click.core.Parameter
214
218
  return [GenerationMode(value)]
215
219
 
216
220
 
217
- def convert_boolean_string(ctx: click.core.Context, param: click.core.Parameter, value: str) -> str | bool:
221
+ def convert_boolean_string(
222
+ ctx: click.core.Context, param: click.core.Parameter, value: str | None
223
+ ) -> str | bool | None:
224
+ if value is None:
225
+ return value
218
226
  return string_to_boolean(value)
219
227
 
220
228
 
@@ -226,7 +234,9 @@ def reraise_format_error(raw_value: str) -> Generator[None, None, None]:
226
234
  raise click.BadParameter(f"Expected KEY:VALUE format, received {raw_value}.") from exc
227
235
 
228
236
 
229
- def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str) -> int:
237
+ def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str | None) -> int | None:
238
+ if value is None:
239
+ return value
230
240
  if value == "auto":
231
241
  return get_workers_count()
232
242
  return int(value)
@@ -105,34 +105,34 @@ class GenerationConfig(DiffBase):
105
105
  *,
106
106
  modes: list[GenerationMode] | None = None,
107
107
  max_examples: int | None = None,
108
- no_shrink: bool = False,
108
+ no_shrink: bool | None = None,
109
109
  deterministic: bool | None = None,
110
- allow_x00: bool = True,
110
+ allow_x00: bool | None = None,
111
111
  codec: str | None = None,
112
112
  maximize: list[MetricFunction] | None = None,
113
113
  with_security_parameters: bool | None = None,
114
- graphql_allow_null: bool = True,
114
+ graphql_allow_null: bool | None = None,
115
115
  database: str | None = None,
116
- unique_inputs: bool = False,
116
+ unique_inputs: bool | None = None,
117
117
  exclude_header_characters: str | None = None,
118
118
  ) -> None:
119
119
  if modes is not None:
120
120
  self.modes = modes
121
121
  if max_examples is not None:
122
122
  self.max_examples = max_examples
123
- self.no_shrink = no_shrink
123
+ self.no_shrink = no_shrink or False
124
124
  self.deterministic = deterministic or False
125
- self.allow_x00 = allow_x00
125
+ self.allow_x00 = allow_x00 if allow_x00 is not None else True
126
126
  if codec is not None:
127
127
  self.codec = codec
128
128
  if maximize is not None:
129
129
  self.maximize = maximize
130
130
  if with_security_parameters is not None:
131
131
  self.with_security_parameters = with_security_parameters
132
- self.graphql_allow_null = graphql_allow_null
132
+ self.graphql_allow_null = graphql_allow_null if graphql_allow_null is not None else True
133
133
  if database is not None:
134
134
  self.database = database
135
- self.unique_inputs = unique_inputs
135
+ self.unique_inputs = unique_inputs or False
136
136
  if exclude_header_characters is not None:
137
137
  self.exclude_header_characters = exclude_header_characters
138
138
 
@@ -98,7 +98,7 @@ class ProjectConfig(DiffBase):
98
98
  workers: int | Literal["auto"] = DEFAULT_WORKERS,
99
99
  proxy: str | None = None,
100
100
  continue_on_failure: bool | None = None,
101
- tls_verify: bool | str | None = None,
101
+ tls_verify: bool | str = True,
102
102
  rate_limit: str | None = None,
103
103
  request_timeout: float | int | None = None,
104
104
  request_cert: str | None = None,
@@ -155,7 +155,7 @@ class ProjectConfig(DiffBase):
155
155
  workers=data.get("workers", DEFAULT_WORKERS),
156
156
  proxy=resolve(data.get("proxy")),
157
157
  continue_on_failure=data.get("continue-on-failure", None),
158
- tls_verify=resolve(data.get("tls-verify")),
158
+ tls_verify=resolve(data.get("tls-verify", True)),
159
159
  rate_limit=resolve(data.get("rate-limit")),
160
160
  request_timeout=data.get("request-timeout"),
161
161
  request_cert=resolve(data.get("request-cert")),
@@ -94,7 +94,7 @@ class ReportsConfig(DiffBase):
94
94
  vcr_path: str | None = None,
95
95
  har_path: str | None = None,
96
96
  directory: Path = DEFAULT_REPORT_DIRECTORY,
97
- preserve_bytes: bool = False,
97
+ preserve_bytes: bool | None = None,
98
98
  ) -> None:
99
99
  formats = formats or []
100
100
  if junit_path is not None or ReportFormat.JUNIT in formats:
@@ -8,8 +8,9 @@ from functools import lru_cache, partial
8
8
  from itertools import combinations
9
9
  from json.encoder import _make_iterencode, c_make_encoder, encode_basestring_ascii # type: ignore
10
10
  from typing import Any, Callable, Generator, Iterator, TypeVar, cast
11
+ from urllib.parse import quote_plus
11
12
 
12
- import jsonschema
13
+ import jsonschema.protocols
13
14
  from hypothesis import strategies as st
14
15
  from hypothesis.errors import InvalidArgument, Unsatisfiable
15
16
  from hypothesis_jsonschema import from_schema
@@ -19,7 +20,7 @@ from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING
19
20
  from schemathesis.core import INTERNAL_BUFFER_SIZE, NOT_SET
20
21
  from schemathesis.core.compat import RefResolutionError
21
22
  from schemathesis.core.transforms import deepclone
22
- from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
23
+ from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
23
24
  from schemathesis.generation import GenerationMode
24
25
  from schemathesis.generation.hypothesis import examples
25
26
  from schemathesis.openapi.generation.filters import is_invalid_path_parameter
@@ -34,7 +35,7 @@ def _replace_zero_with_nonzero(x: float) -> float:
34
35
 
35
36
 
36
37
  def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
37
- return st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3)
38
+ return st.lists(strategy, max_size=2) | st.dictionaries(st.text(), strategy, max_size=2)
38
39
 
39
40
 
40
41
  NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN = 100
@@ -42,10 +43,12 @@ NEGATIVE_MODE_MAX_ITEMS = 15
42
43
  FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
43
44
  NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
44
45
  JSON_STRATEGY: st.SearchStrategy = st.recursive(
45
- st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(), json_recursive_strategy
46
+ st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(max_size=16),
47
+ json_recursive_strategy,
48
+ max_leaves=2,
46
49
  )
47
- ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2)
48
- OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
50
+ ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2, max_size=3)
51
+ OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(max_size=16), JSON_STRATEGY, max_size=2)
49
52
 
50
53
 
51
54
  STRATEGIES_FOR_TYPE = {
@@ -112,8 +115,9 @@ class CoverageContext:
112
115
  is_required: bool
113
116
  path: list[str | int]
114
117
  custom_formats: dict[str, st.SearchStrategy]
118
+ validator_cls: type[jsonschema.protocols.Validator]
115
119
 
116
- __slots__ = ("location", "generation_modes", "is_required", "path", "custom_formats")
120
+ __slots__ = ("location", "generation_modes", "is_required", "path", "custom_formats", "validator_cls")
117
121
 
118
122
  def __init__(
119
123
  self,
@@ -123,12 +127,14 @@ class CoverageContext:
123
127
  is_required: bool,
124
128
  path: list[str | int] | None = None,
125
129
  custom_formats: dict[str, st.SearchStrategy],
130
+ validator_cls: type[jsonschema.protocols.Validator],
126
131
  ) -> None:
127
132
  self.location = location
128
133
  self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
129
134
  self.is_required = is_required
130
135
  self.path = path or []
131
136
  self.custom_formats = custom_formats
137
+ self.validator_cls = validator_cls
132
138
 
133
139
  @contextmanager
134
140
  def at(self, key: str | int) -> Generator[None, None, None]:
@@ -149,6 +155,7 @@ class CoverageContext:
149
155
  is_required=self.is_required,
150
156
  path=self.path,
151
157
  custom_formats=self.custom_formats,
158
+ validator_cls=self.validator_cls,
152
159
  )
153
160
 
154
161
  def with_negative(self) -> CoverageContext:
@@ -158,6 +165,7 @@ class CoverageContext:
158
165
  is_required=self.is_required,
159
166
  path=self.path,
160
167
  custom_formats=self.custom_formats,
168
+ validator_cls=self.validator_cls,
161
169
  )
162
170
 
163
171
  def is_valid_for_location(self, value: Any) -> bool:
@@ -173,20 +181,21 @@ class CoverageContext:
173
181
  if isinstance(value, list) and not self.is_required:
174
182
  # Optional parameters should be present
175
183
  return any(item not in [{}, []] for item in value)
176
- if isinstance(value, dict) and not self.is_required:
177
- return bool(value)
178
184
  return True
179
185
 
186
+ def will_be_serialized_to_string(self) -> bool:
187
+ return self.location in ("query", "path", "header", "cookie")
188
+
180
189
  def can_be_negated(self, schema: dict[str, Any]) -> bool:
181
190
  # Path, query, header, and cookie parameters will be stringified anyway
182
191
  # If there are no constraints, then anything will match the original schema after serialization
183
- if self.location in ("query", "path", "header", "cookie"):
192
+ if self.will_be_serialized_to_string():
184
193
  cleaned = {
185
194
  k: v
186
195
  for k, v in schema.items()
187
196
  if not k.startswith("x-") and k not in ["description", "example", "examples"]
188
197
  }
189
- return cleaned != {}
198
+ return cleaned not in [{}, {"type": "string"}]
190
199
  return True
191
200
 
192
201
  def generate_from(self, strategy: st.SearchStrategy) -> Any:
@@ -411,7 +420,7 @@ def cover_schema_iter(
411
420
  for value_ in _negative_enum(ctx, [value], seen):
412
421
  yield value_
413
422
  elif key == "type":
414
- yield from _negative_type(ctx, value, seen)
423
+ yield from _negative_type(ctx, value, seen, schema)
415
424
  elif key == "properties":
416
425
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
417
426
  yield from _negative_properties(ctx, template, value)
@@ -1038,7 +1047,7 @@ def _negative_multiple_of(
1038
1047
 
1039
1048
 
1040
1049
  def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
1041
- unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
1050
+ unique = jsonify(ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1}))
1042
1051
  yield NegativeValue(unique + unique, description="Non-unique items", location=ctx.current_path)
1043
1052
 
1044
1053
 
@@ -1086,22 +1095,120 @@ def _is_non_integer_float(x: float) -> bool:
1086
1095
  return x != int(x)
1087
1096
 
1088
1097
 
1098
+ def is_valid_header_value(value: Any) -> bool:
1099
+ value = str(value)
1100
+ if not is_latin_1_encodable(value):
1101
+ return False
1102
+ if has_invalid_characters("A", value):
1103
+ return False
1104
+ return True
1105
+
1106
+
1107
+ def jsonify(value: Any) -> Any:
1108
+ if isinstance(value, bool):
1109
+ return "true" if value else "false"
1110
+ elif value is None:
1111
+ return "null"
1112
+
1113
+ stack: list = [value]
1114
+ while stack:
1115
+ item = stack.pop()
1116
+ if isinstance(item, dict):
1117
+ for key, sub_item in item.items():
1118
+ if isinstance(sub_item, bool):
1119
+ item[key] = "true" if sub_item else "false"
1120
+ elif sub_item is None:
1121
+ item[key] = "null"
1122
+ elif isinstance(sub_item, dict):
1123
+ stack.append(sub_item)
1124
+ elif isinstance(sub_item, list):
1125
+ stack.extend(item)
1126
+ elif isinstance(item, list):
1127
+ for idx, sub_item in enumerate(item):
1128
+ if isinstance(sub_item, bool):
1129
+ item[idx] = "true" if sub_item else "false"
1130
+ elif sub_item is None:
1131
+ item[idx] = "null"
1132
+ else:
1133
+ stack.extend(item)
1134
+ return value
1135
+
1136
+
1137
+ def quote_path_parameter(value: Any) -> str:
1138
+ if isinstance(value, str):
1139
+ if value == ".":
1140
+ return "%2E"
1141
+ elif value == "..":
1142
+ return "%2E%2E"
1143
+ else:
1144
+ return quote_plus(value)
1145
+ if isinstance(value, list):
1146
+ return ",".join(map(str, value))
1147
+ return str(value)
1148
+
1149
+
1089
1150
  def _negative_type(
1090
- ctx: CoverageContext,
1091
- ty: str | list[str],
1092
- seen: HashSet,
1151
+ ctx: CoverageContext, ty: str | list[str], seen: HashSet, schema: dict[str, Any]
1093
1152
  ) -> Generator[GeneratedValue, None, None]:
1094
1153
  if isinstance(ty, str):
1095
1154
  types = [ty]
1096
1155
  else:
1097
1156
  types = ty
1098
1157
  strategies = {ty: strategy for ty, strategy in STRATEGIES_FOR_TYPE.items() if ty not in types}
1158
+
1159
+ filter_func = {
1160
+ "path": lambda x: not is_invalid_path_parameter(x),
1161
+ "header": is_valid_header_value,
1162
+ "cookie": is_valid_header_value,
1163
+ "query": lambda x: not contains_unicode_surrogate_pair(x),
1164
+ }.get(ctx.location)
1165
+
1099
1166
  if "number" in types:
1100
1167
  del strategies["integer"]
1101
1168
  if "integer" in types:
1102
1169
  strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
1103
1170
  if ctx.location == "query":
1104
1171
  strategies.pop("object", None)
1172
+ if filter_func is not None:
1173
+ for ty, strategy in strategies.items():
1174
+ strategies[ty] = strategy.filter(filter_func)
1175
+
1176
+ pattern = schema.get("pattern")
1177
+ if pattern is not None:
1178
+ try:
1179
+ re.compile(pattern)
1180
+ except re.error:
1181
+ schema = schema.copy()
1182
+ del schema["pattern"]
1183
+ return
1184
+
1185
+ validator = ctx.validator_cls(
1186
+ schema,
1187
+ format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
1188
+ )
1189
+ is_valid = validator.is_valid
1190
+ try:
1191
+ is_valid(None)
1192
+ apply_validation = True
1193
+ except Exception:
1194
+ # Schema is not correct and we can't validate the generated instances.
1195
+ # In such a scenario it is better to generate at least something with some chances to have a false
1196
+ # positive failure
1197
+ apply_validation = False
1198
+
1199
+ def _does_not_match_the_original_schema(value: Any) -> bool:
1200
+ return not is_valid(str(value))
1201
+
1202
+ if ctx.location == "path":
1203
+ for ty, strategy in strategies.items():
1204
+ strategies[ty] = strategy.map(jsonify).map(quote_path_parameter)
1205
+ elif ctx.location == "query":
1206
+ for ty, strategy in strategies.items():
1207
+ strategies[ty] = strategy.map(jsonify)
1208
+
1209
+ if apply_validation and ctx.will_be_serialized_to_string():
1210
+ for ty, strategy in strategies.items():
1211
+ strategies[ty] = strategy.filter(_does_not_match_the_original_schema)
1105
1212
  for strategy in strategies.values():
1106
1213
  value = ctx.generate_from(strategy)
1107
1214
  if seen.insert(value) and ctx.is_valid_for_location(value):
@@ -462,6 +462,7 @@ def _iter_coverage_cases(
462
462
  from schemathesis.specs.openapi._hypothesis import _build_custom_formats
463
463
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
464
464
  from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
465
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
465
466
  from schemathesis.specs.openapi.serialization import get_serializers_for_operation
466
467
 
467
468
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
@@ -476,6 +477,8 @@ def _iter_coverage_cases(
476
477
 
477
478
  seen_negative = coverage.HashSet()
478
479
  seen_positive = coverage.HashSet()
480
+ assert isinstance(operation.schema, BaseOpenAPISchema)
481
+ validator_cls = operation.schema.validator_cls
479
482
 
480
483
  for parameter in operation.iter_parameters():
481
484
  location = parameter.location
@@ -489,6 +492,7 @@ def _iter_coverage_cases(
489
492
  generation_modes=generation_modes,
490
493
  is_required=parameter.is_required,
491
494
  custom_formats=custom_formats,
495
+ validator_cls=validator_cls,
492
496
  ),
493
497
  schema,
494
498
  )
@@ -513,6 +517,7 @@ def _iter_coverage_cases(
513
517
  generation_modes=generation_modes,
514
518
  is_required=body.is_required,
515
519
  custom_formats=custom_formats,
520
+ validator_cls=validator_cls,
516
521
  ),
517
522
  schema,
518
523
  )
@@ -658,14 +663,14 @@ def _iter_coverage_cases(
658
663
  name = parameter.name
659
664
  location = parameter.location
660
665
  container_name = LOCATION_TO_CONTAINER[location]
661
- # NOTE: if the schema is overly permissive we may not have any negative test cases
662
- if container_name in template:
663
- container = template[container_name]
664
- data = template.with_container(
665
- container_name=container_name,
666
- value={k: v for k, v in container.items() if k != name},
667
- generation_mode=GenerationMode.NEGATIVE,
668
- )
666
+ container = template.get(container_name, {})
667
+ data = template.with_container(
668
+ container_name=container_name,
669
+ value={k: v for k, v in container.items() if k != name},
670
+ generation_mode=GenerationMode.NEGATIVE,
671
+ )
672
+
673
+ if seen_negative.insert(data.kwargs):
669
674
  yield operation.Case(
670
675
  **data.kwargs,
671
676
  _meta=CaseMetadata(
@@ -747,6 +752,7 @@ def _iter_coverage_cases(
747
752
  generation_modes=[GenerationMode.NEGATIVE],
748
753
  is_required=is_required,
749
754
  custom_formats=custom_formats,
755
+ validator_cls=validator_cls,
750
756
  ),
751
757
  subschema,
752
758
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 4.0.13
3
+ Version: 4.0.14
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://github.com/schemathesis/schemathesis/blob/master/CHANGELOG.md
@@ -10,15 +10,15 @@ schemathesis/cli/__init__.py,sha256=U9gjzWWpiFhaqevPjZbwyTNjABdpvXETI4HgwdGKnvs,
10
10
  schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
11
11
  schemathesis/cli/constants.py,sha256=CVcQNHEiX-joAQmyuEVKWPOSxDHsOw_EXXZsEclzLuY,341
12
12
  schemathesis/cli/core.py,sha256=ue7YUdVo3YvuzGL4s6i62NL6YqNDeVPBSnQ1znrvG2w,480
13
- schemathesis/cli/commands/__init__.py,sha256=rubTCCRGuMIbNYOl8yQEioiuHtTq__tWjkUtFWYGhqQ,3433
13
+ schemathesis/cli/commands/__init__.py,sha256=DNzKEnXu7GjGSVe0244ZErmygUBA3nGSyVY6JP3ixD0,3740
14
14
  schemathesis/cli/commands/data.py,sha256=_ALywjIeCZjuaoDQFy-Kj8RZkEGqXd-Y95O47h8Jszs,171
15
- schemathesis/cli/commands/run/__init__.py,sha256=qr6wSZSQMbLDNqsEyChLJr0616GuY1Wcg5gHjoTbPss,18648
15
+ schemathesis/cli/commands/run/__init__.py,sha256=F8KgDQwWRqbJxnAL9nHREphcgGW-Ghn-kbe4yAquadw,18686
16
16
  schemathesis/cli/commands/run/context.py,sha256=taegOHWc_B-HDwiU1R9Oi4q57mdfLXc-B954QUj8t7A,7984
17
17
  schemathesis/cli/commands/run/events.py,sha256=ew0TQOc9T2YBZynYWv95k9yfAk8-hGuZDLMxjT8EhvY,1595
18
18
  schemathesis/cli/commands/run/executor.py,sha256=kFbZw583SZ-jqjv8goTp2yEJOpZ_bzecyTeZvdc6qTE,5327
19
19
  schemathesis/cli/commands/run/filters.py,sha256=pzkNRcf5vLPSsMfnvt711GNzRSBK5iZIFjPA0fiH1N4,1701
20
20
  schemathesis/cli/commands/run/loaders.py,sha256=6j0ez7wduAUYbUT28ELKxMf-dYEWr_67m_KIuTSyNGk,4358
21
- schemathesis/cli/commands/run/validation.py,sha256=FzCzYdW1-hn3OgyzPO1p6wHEX5PG7HdewbPRvclV_vc,9002
21
+ schemathesis/cli/commands/run/validation.py,sha256=DQaMiBLN2tYT9hONvv8xnyPvNXZH768UlOdUxTd5kZs,9193
22
22
  schemathesis/cli/commands/run/handlers/__init__.py,sha256=TPZ3KdGi8m0fjlN0GjA31MAXXn1qI7uU4FtiDwroXZI,1915
23
23
  schemathesis/cli/commands/run/handlers/base.py,sha256=yDsTtCiztLksfk7cRzg8JlaAVOfS-zwK3tsJMOXAFyc,530
24
24
  schemathesis/cli/commands/run/handlers/cassettes.py,sha256=rRD4byjp4HXCkJS-zx3jSIFOJsPq77ejPpYeyCtsEZs,19461
@@ -34,15 +34,15 @@ schemathesis/config/_checks.py,sha256=F0r16eSSiICvoiTUkNNOE2PH73EGd8bikoeZdME_3Y
34
34
  schemathesis/config/_diff_base.py,sha256=-XqS6cTzZC5fplz8_2RSZHDMSAPJhBBIEP6H8wcgHmo,4221
35
35
  schemathesis/config/_env.py,sha256=8XfIyrnGNQuCDnfG0lwmKRFbasRUjgeQGBAMupsmtOU,613
36
36
  schemathesis/config/_error.py,sha256=TxuuqQ1olwJc7P7ssfxXb1dB_Xn5uVsoazjvYvRxrxA,5437
37
- schemathesis/config/_generation.py,sha256=_THqCfC20i8RRRsO2hAwoJ52FV-CS1xOA6me3Wp3FHw,5087
37
+ schemathesis/config/_generation.py,sha256=giWs4z17z9nRe_9Z3mAZ3LEoyh4hkcJnlAA6LSy6iEo,5210
38
38
  schemathesis/config/_health_check.py,sha256=zC9inla5ibMBlEy5WyM4_TME7ju_KH3Bwfo21RI3Gks,561
39
39
  schemathesis/config/_operations.py,sha256=2M36b4MMoFtaaFpe9yG-aWRqh0Qm1dpdk5M0V23X2yA,12129
40
40
  schemathesis/config/_output.py,sha256=3G9SOi-4oNcQPHeNRG3HggFCwvcKOW1kF28a9m0H-pU,4434
41
41
  schemathesis/config/_parameters.py,sha256=i76Hwaf834fBAMmtKfKTl1SFCicJ-Y-5tZt5QNGW2fA,618
42
42
  schemathesis/config/_phases.py,sha256=NFUhn-xzEQdNtgNVW1t51lMquXbjRNGR_QuiCRLCi28,6454
43
- schemathesis/config/_projects.py,sha256=1rbI178wv743492FnexjdlsGvggNYpsbVUzQcUcJhAk,19487
43
+ schemathesis/config/_projects.py,sha256=_fbivneAqa2Y7sCX0T1CBSjo3CHPD1qLZcJYsYnWpQk,19486
44
44
  schemathesis/config/_rate_limit.py,sha256=ekEW-jP_Ichk_O6hYpj-h2TTTKfp7Fm0nyFUbvlWcbA,456
45
- schemathesis/config/_report.py,sha256=aYLnPO74B7Wfn_qTwlEp5zY9L74U1XFuYS10yjwKKWY,3885
45
+ schemathesis/config/_report.py,sha256=ZECDpaCY4WWHD5UbjvgZoSjLz-rlTvfd5Ivzdgzqf2I,3891
46
46
  schemathesis/config/_validator.py,sha256=IcE8geFZ0ZwR18rkIRs25i7pTl7Z84XbjYGUB-mqReU,258
47
47
  schemathesis/config/_warnings.py,sha256=sI0VZcTj3dOnphhBwYwU_KTagxr89HGWTtQ99HcY84k,772
48
48
  schemathesis/config/schema.json,sha256=wC1qe9M_fXotfmlBOmW_FCTRw9K5YC814-PipMGKllE,18907
@@ -85,13 +85,13 @@ schemathesis/engine/phases/unit/_executor.py,sha256=9MmZoKSBVSPk0LWwN3PZ3iaO9nzp
85
85
  schemathesis/engine/phases/unit/_pool.py,sha256=iU0hdHDmohPnEv7_S1emcabuzbTf-Cznqwn0pGQ5wNQ,2480
86
86
  schemathesis/generation/__init__.py,sha256=tvNO2FLiY8z3fZ_kL_QJhSgzXfnT4UqwSXMHCwfLI0g,645
87
87
  schemathesis/generation/case.py,sha256=zwAwFQ-Fp7SOxCXYOQyAdwAtNwVJe63PdLpvqackFQY,12296
88
- schemathesis/generation/coverage.py,sha256=0d52xHf1HUbilPmCeJlplnw7knSdc0lEv4Hr0HXYUTE,49949
88
+ schemathesis/generation/coverage.py,sha256=kJGX7_YYQsM9rBMtalLlmrSFUIVXn8t4Z61Kpowa3rg,53705
89
89
  schemathesis/generation/meta.py,sha256=adkoMuCfzSjHJ9ZDocQn0GnVldSCkLL3eVR5A_jafwM,2552
90
90
  schemathesis/generation/metrics.py,sha256=cZU5HdeAMcLFEDnTbNE56NuNq4P0N4ew-g1NEz5-kt4,2836
91
91
  schemathesis/generation/modes.py,sha256=Q1fhjWr3zxabU5qdtLvKfpMFZJAwlW9pnxgenjeXTyU,481
92
92
  schemathesis/generation/overrides.py,sha256=OBWqDQPreiliaf2M-oyXppVKHoJkCRzxtwSJx1b6AFw,3759
93
93
  schemathesis/generation/hypothesis/__init__.py,sha256=SVwM-rx07jPZzms0idWYACgUtWAxh49HRuTnaQ__zf0,1549
94
- schemathesis/generation/hypothesis/builder.py,sha256=EPJkeyeirU-pMuD4NGrY1e4HRp0cmBNg_1NLRFRxOfk,34550
94
+ schemathesis/generation/hypothesis/builder.py,sha256=OUEYPXbjihrZa6Jjya83HslRCX_V7RhqnZuGs69QiZg,34769
95
95
  schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
96
96
  schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
97
97
  schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
@@ -157,8 +157,8 @@ schemathesis/transport/prepare.py,sha256=erYXRaxpQokIDzaIuvt_csHcw72iHfCyNq8VNEz
157
157
  schemathesis/transport/requests.py,sha256=rziZTrZCVMAqgy6ldB8iTwhkpAsnjKSgK8hj5Sq3ThE,10656
158
158
  schemathesis/transport/serialization.py,sha256=igUXKZ_VJ9gV7P0TUc5PDQBJXl_s0kK9T3ljGWWvo6E,10339
159
159
  schemathesis/transport/wsgi.py,sha256=KoAfvu6RJtzyj24VGB8e-Iaa9smpgXJ3VsM8EgAz2tc,6152
160
- schemathesis-4.0.13.dist-info/METADATA,sha256=2E4DqYcjnW29smDSYVi-vCZVU_oFIYqvRGVZgHOAniw,8472
161
- schemathesis-4.0.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
162
- schemathesis-4.0.13.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
163
- schemathesis-4.0.13.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
164
- schemathesis-4.0.13.dist-info/RECORD,,
160
+ schemathesis-4.0.14.dist-info/METADATA,sha256=z1a4R4d4JKcMhzNP3aNTB-Rdg_K4hJibT2THKGrng5I,8472
161
+ schemathesis-4.0.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
162
+ schemathesis-4.0.14.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
163
+ schemathesis-4.0.14.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
164
+ schemathesis-4.0.14.dist-info/RECORD,,