schemathesis 3.39.10__py3-none-any.whl → 3.39.12__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.
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  from dataclasses import dataclass
7
- import json
8
7
  import warnings
9
8
  from functools import wraps
10
9
  from itertools import combinations
@@ -16,8 +15,10 @@ from hypothesis.errors import HypothesisWarning, Unsatisfiable
16
15
  from hypothesis.internal.entropy import deterministic_PRNG
17
16
  from jsonschema.exceptions import SchemaError
18
17
 
18
+ from schemathesis.serializers import get_first_matching_media_type
19
+
19
20
  from . import _patches
20
- from .auths import get_auth_storage_from_test
21
+ from .auths import AuthStorage, get_auth_storage_from_test
21
22
  from .constants import DEFAULT_DEADLINE, NOT_SET
22
23
  from .exceptions import OperationSchemaError, SerializationNotPossible
23
24
  from .experimental import COVERAGE_PHASE
@@ -28,6 +29,7 @@ from .parameters import ParameterSet
28
29
  from .transports.content_types import parse_content_type
29
30
  from .transports.headers import has_invalid_characters, is_latin_1_encodable
30
31
  from .types import NotSet
32
+ from schemathesis import auths
31
33
 
32
34
  if TYPE_CHECKING:
33
35
  from .utils import GivenInput
@@ -112,7 +114,15 @@ def create_test(
112
114
  wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
113
115
  )
114
116
  if COVERAGE_PHASE.is_enabled:
115
- wrapped_test = add_coverage(wrapped_test, operation, data_generation_methods)
117
+ unexpected_methods = generation_config.unexpected_methods if generation_config else None
118
+ wrapped_test = add_coverage(
119
+ wrapped_test,
120
+ operation,
121
+ data_generation_methods,
122
+ auth_storage,
123
+ as_strategy_kwargs,
124
+ unexpected_methods,
125
+ )
116
126
  return wrapped_test
117
127
 
118
128
 
@@ -216,20 +226,46 @@ def adjust_urlencoded_payload(case: Case) -> None:
216
226
 
217
227
 
218
228
  def add_coverage(
219
- test: Callable, operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
229
+ test: Callable,
230
+ operation: APIOperation,
231
+ data_generation_methods: list[DataGenerationMethod],
232
+ auth_storage: AuthStorage | None,
233
+ as_strategy_kwargs: dict[str, Any],
234
+ unexpected_methods: set[str] | None = None,
220
235
  ) -> Callable:
221
- for example in _iter_coverage_cases(operation, data_generation_methods):
222
- adjust_urlencoded_payload(example)
223
- test = hypothesis.example(case=example)(test)
236
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
237
+
238
+ auth_context = auths.AuthContext(
239
+ operation=operation,
240
+ app=operation.app,
241
+ )
242
+ overrides = {
243
+ container: as_strategy_kwargs[container]
244
+ for container in LOCATION_TO_CONTAINER.values()
245
+ if container in as_strategy_kwargs
246
+ }
247
+ for case in _iter_coverage_cases(operation, data_generation_methods, unexpected_methods):
248
+ if case.media_type and get_first_matching_media_type(case.media_type) is None:
249
+ continue
250
+ adjust_urlencoded_payload(case)
251
+ auths.set_on_case(case, auth_context, auth_storage)
252
+ for container_name, value in overrides.items():
253
+ container = getattr(case, container_name)
254
+ if container is None:
255
+ setattr(case, container_name, value)
256
+ else:
257
+ container.update(value)
258
+ test = hypothesis.example(case=case)(test)
224
259
  return test
225
260
 
226
261
 
227
262
  class Template:
228
- __slots__ = ("_components", "_template")
263
+ __slots__ = ("_components", "_template", "_serializers")
229
264
 
230
- def __init__(self) -> None:
265
+ def __init__(self, serializers: dict[str, Callable]) -> None:
231
266
  self._components: dict[str, DataGenerationMethod] = {}
232
267
  self._template: dict[str, Any] = {}
268
+ self._serializers = serializers
233
269
 
234
270
  def __contains__(self, key: str) -> bool:
235
271
  return key in self._template
@@ -251,36 +287,58 @@ class Template:
251
287
  self._components[component_name] = DataGenerationMethod.negative
252
288
 
253
289
  container = self._template.setdefault(component_name, {})
254
- if _should_stringify(location, value):
255
- container[name] = _stringify_value(value.value, location)
256
- else:
257
- container[name] = value.value
290
+ container[name] = value.value
258
291
 
259
292
  def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
260
293
  self._template["body"] = body.value
261
294
  self._template["media_type"] = media_type
262
295
  self._components["body"] = body.data_generation_method
263
296
 
297
+ def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
298
+ from schemathesis.specs.openapi._hypothesis import quote_all
299
+
300
+ output = {}
301
+ for container_name, value in kwargs.items():
302
+ serializer = self._serializers.get(container_name)
303
+ if container_name in ("headers", "cookies") and isinstance(value, dict):
304
+ value = _stringify_value(value, container_name)
305
+ if serializer is not None:
306
+ value = serializer(value)
307
+ if container_name == "query" and isinstance(value, dict):
308
+ value = _stringify_value(value, container_name)
309
+ if container_name == "path_parameters" and isinstance(value, dict):
310
+ value = _stringify_value(quote_all(value), container_name)
311
+ output[container_name] = value
312
+ return output
313
+
264
314
  def unmodified(self) -> TemplateValue:
265
- return TemplateValue(kwargs=self._template.copy(), components=self._components.copy())
315
+ kwargs = self._template.copy()
316
+ kwargs = self._serialize(kwargs)
317
+ return TemplateValue(kwargs=kwargs, components=self._components.copy())
266
318
 
267
319
  def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
268
320
  kwargs = {**self._template, "media_type": media_type, "body": value.value}
321
+ kwargs = self._serialize(kwargs)
269
322
  components = {**self._components, "body": value.data_generation_method}
270
323
  return TemplateValue(kwargs=kwargs, components=components)
271
324
 
272
325
  def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
273
326
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
274
327
 
275
- if _should_stringify(location, value):
276
- generated = _stringify_value(value.value, location)
277
- else:
278
- generated = value.value
279
-
280
328
  container_name = LOCATION_TO_CONTAINER[location]
281
329
  container = self._template[container_name]
282
- kwargs = {**self._template, container_name: {**container, name: generated}}
283
- components = {**self._components, container_name: value.data_generation_method}
330
+ return self.with_container(
331
+ container_name=container_name,
332
+ value={**container, name: value.value},
333
+ data_generation_method=value.data_generation_method,
334
+ )
335
+
336
+ def with_container(
337
+ self, *, container_name: str, value: Any, data_generation_method: DataGenerationMethod
338
+ ) -> TemplateValue:
339
+ kwargs = {**self._template, container_name: value}
340
+ kwargs = self._serialize(kwargs)
341
+ components = {**self._components, container_name: data_generation_method}
284
342
  return TemplateValue(kwargs=kwargs, components=components)
285
343
 
286
344
 
@@ -291,38 +349,41 @@ class TemplateValue:
291
349
  __slots__ = ("kwargs", "components")
292
350
 
293
351
 
294
- def _should_stringify(location: str, value: coverage.GeneratedValue) -> bool:
295
- return location in ("header", "cookie", "path", "query") and not isinstance(value.value, str)
296
-
297
-
298
- def _stringify_value(val: Any, location: str) -> str | list[str]:
352
+ def _stringify_value(val: Any, container_name: str) -> Any:
353
+ if val is None:
354
+ return "null"
355
+ if val is True:
356
+ return "true"
357
+ if val is False:
358
+ return "false"
359
+ if isinstance(val, (int, float)):
360
+ return str(val)
299
361
  if isinstance(val, list):
300
- if location == "query":
362
+ if container_name == "query":
301
363
  # Having a list here ensures there will be multiple query parameters wit the same name
302
- return [json.dumps(item) for item in val]
364
+ return [_stringify_value(item, container_name) for item in val]
303
365
  # use comma-separated values style for arrays
304
- return ",".join(json.dumps(sub) for sub in val)
305
- return json.dumps(val)
366
+ return ",".join(_stringify_value(sub, container_name) for sub in val)
367
+ if isinstance(val, dict):
368
+ return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
369
+ return val
306
370
 
307
371
 
308
372
  def _iter_coverage_cases(
309
- operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
373
+ operation: APIOperation,
374
+ data_generation_methods: list[DataGenerationMethod],
375
+ unexpected_methods: set[str] | None = None,
310
376
  ) -> Generator[Case, None, None]:
311
377
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
312
378
  from .specs.openapi.examples import find_in_responses, find_matching_in_responses
313
-
314
- def _stringify_value(val: Any, location: str) -> str | list[str]:
315
- if isinstance(val, list):
316
- if location == "query":
317
- # Having a list here ensures there will be multiple query parameters wit the same name
318
- return [json.dumps(item) for item in val]
319
- # use comma-separated values style for arrays
320
- return ",".join(json.dumps(sub) for sub in val)
321
- return json.dumps(val)
379
+ from schemathesis.specs.openapi.serialization import get_serializers_for_operation
322
380
 
323
381
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
324
- template = Template()
382
+ serializers = get_serializers_for_operation(operation)
383
+ template = Template(serializers)
325
384
  responses = find_in_responses(operation)
385
+ # NOTE: The HEAD method is excluded
386
+ unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
326
387
  for parameter in operation.iter_parameters():
327
388
  location = parameter.location
328
389
  name = parameter.name
@@ -398,8 +459,7 @@ def _iter_coverage_cases(
398
459
  yield case
399
460
  if DataGenerationMethod.negative in data_generation_methods:
400
461
  # Generate HTTP methods that are not specified in the spec
401
- # NOTE: The HEAD method is excluded
402
- methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
462
+ methods = unexpected_methods - set(operation.schema[operation.path])
403
463
  for method in sorted(methods):
404
464
  data = template.unmodified()
405
465
  case = operation.make_case(**data.kwargs)
@@ -415,10 +475,12 @@ def _iter_coverage_cases(
415
475
  # I.e. contains just `default` value without any other keywords
416
476
  value = container.get(parameter.name, NOT_SET)
417
477
  if value is not NOT_SET:
418
- data = template.unmodified()
419
- case = operation.make_case(
420
- **{**data.kwargs, "query": {**container, parameter.name: [value, value]}}
478
+ data = template.with_container(
479
+ container_name="query",
480
+ value={**container, parameter.name: [value, value]},
481
+ data_generation_method=DataGenerationMethod.negative,
421
482
  )
483
+ case = operation.make_case(**data.kwargs)
422
484
  case.data_generation_method = DataGenerationMethod.negative
423
485
  case.meta = _make_meta(
424
486
  description=f"Duplicate `{parameter.name}` query parameter",
@@ -435,17 +497,19 @@ def _iter_coverage_cases(
435
497
  location = parameter.location
436
498
  container_name = LOCATION_TO_CONTAINER[location]
437
499
  container = template[container_name]
438
- data = template.unmodified()
439
- case = operation.make_case(
440
- **{**data.kwargs, container_name: {k: v for k, v in container.items() if k != name}}
500
+ data = template.with_container(
501
+ container_name=container_name,
502
+ value={k: v for k, v in container.items() if k != name},
503
+ data_generation_method=DataGenerationMethod.negative,
441
504
  )
505
+ case = operation.make_case(**data.kwargs)
442
506
  case.data_generation_method = DataGenerationMethod.negative
443
507
  case.meta = _make_meta(
444
508
  description=f"Missing `{name}` at {location}",
445
509
  location=None,
446
510
  parameter=name,
447
511
  parameter_location=location,
448
- **{**data.components, container_name: DataGenerationMethod.negative},
512
+ **data.components,
449
513
  )
450
514
  yield case
451
515
  # Generate combinations for each location
@@ -474,23 +538,17 @@ def _iter_coverage_cases(
474
538
  _parameter: str | None,
475
539
  _data_generation_method: DataGenerationMethod,
476
540
  ) -> Case:
477
- if _location in ("header", "cookie", "path", "query"):
478
- container = {
479
- name: _stringify_value(val, _location) if not isinstance(val, str) else val
480
- for name, val in container_values.items()
481
- }
482
- else:
483
- container = container_values
484
-
485
- data = template.unmodified()
486
- case = operation.make_case(**{**data.kwargs, _container_name: container})
541
+ data = template.with_container(
542
+ container_name=_container_name, value=container_values, data_generation_method=_data_generation_method
543
+ )
544
+ case = operation.make_case(**data.kwargs)
487
545
  case.data_generation_method = _data_generation_method
488
546
  case.meta = _make_meta(
489
547
  description=description,
490
548
  location=None,
491
549
  parameter=_parameter,
492
550
  parameter_location=_location,
493
- **{**data.components, _container_name: _data_generation_method},
551
+ **data.components,
494
552
  )
495
553
  return case
496
554
 
@@ -9,7 +9,7 @@ from collections import defaultdict
9
9
  from dataclasses import dataclass
10
10
  from enum import Enum
11
11
  from queue import Queue
12
- from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence, Type, cast
12
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence, cast
13
13
  from urllib.parse import urlparse
14
14
 
15
15
  import click
@@ -27,6 +27,7 @@ from ..constants import (
27
27
  DEFAULT_STATEFUL_RECURSION_LIMIT,
28
28
  EXTENSIONS_DOCUMENTATION_URL,
29
29
  HOOKS_MODULE_ENV_VAR,
30
+ HTTP_METHODS,
30
31
  HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
31
32
  ISSUE_TRACKER_URL,
32
33
  WAIT_FOR_SCHEMA_ENV_VAR,
@@ -323,6 +324,16 @@ REPORT_TO_SERVICE = ReportToService()
323
324
  multiple=True,
324
325
  metavar="",
325
326
  )
327
+ @grouped_option(
328
+ "--experimental-coverage-unexpected-methods",
329
+ "coverage_unexpected_methods",
330
+ help="HTTP methods to use when generating test cases with methods not specified in the API during the coverage phase.",
331
+ type=CsvChoice(sorted(HTTP_METHODS), case_sensitive=False),
332
+ callback=callbacks.convert_http_methods,
333
+ metavar="",
334
+ default=None,
335
+ envvar="SCHEMATHESIS_EXPERIMENTAL_COVERAGE_UNEXPECTED_METHODS",
336
+ )
326
337
  @grouped_option(
327
338
  "--experimental-no-failfast",
328
339
  "no_failfast",
@@ -874,6 +885,7 @@ def run(
874
885
  set_path: dict[str, str],
875
886
  experiments: list,
876
887
  no_failfast: bool,
888
+ coverage_unexpected_methods: set[str] | None,
877
889
  missing_required_header_allowed_statuses: list[str],
878
890
  positive_data_acceptance_allowed_statuses: list[str],
879
891
  negative_data_rejection_allowed_statuses: list[str],
@@ -1000,6 +1012,7 @@ def run(
1000
1012
  graphql_allow_null=generation_graphql_allow_null,
1001
1013
  codec=generation_codec,
1002
1014
  with_security_parameters=generation_with_security_parameters,
1015
+ unexpected_methods=coverage_unexpected_methods,
1003
1016
  )
1004
1017
 
1005
1018
  report: ReportToService | click.utils.LazyFile | None
@@ -344,6 +344,14 @@ def convert_checks(ctx: click.core.Context, param: click.core.Parameter, value:
344
344
  return reduce(operator.iadd, value, [])
345
345
 
346
346
 
347
+ def convert_http_methods(
348
+ ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
349
+ ) -> set[str] | None:
350
+ if value is None:
351
+ return value
352
+ return {item.lower() for item in value}
353
+
354
+
347
355
  def convert_status_codes(
348
356
  ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
349
357
  ) -> list[str] | None:
@@ -26,7 +26,12 @@ class CustomHelpMessageChoice(click.Choice):
26
26
  class BaseCsvChoice(click.Choice):
27
27
  def parse_value(self, value: str) -> tuple[list[str], set[str]]:
28
28
  selected = [item for item in value.split(",") if item]
29
- invalid_options = set(selected) - set(self.choices)
29
+ if not self.case_sensitive:
30
+ invalid_options = {
31
+ item for item in selected if item.upper() not in {choice.upper() for choice in self.choices}
32
+ }
33
+ else:
34
+ invalid_options = set(selected) - set(self.choices)
30
35
  return selected, invalid_options
31
36
 
32
37
  def fail_on_invalid_options(self, invalid_options: set[str], selected: list[str]) -> NoReturn:
schemathesis/failures.py CHANGED
@@ -143,6 +143,12 @@ class AcceptedNegativeData(FailureContext):
143
143
  title: str = "Accepted negative data"
144
144
  type: str = "accepted_negative_data"
145
145
 
146
+ def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
147
+ return (
148
+ check_message or self.message,
149
+ str(self.status_code),
150
+ )
151
+
146
152
 
147
153
  @dataclass(repr=False)
148
154
  class RejectedPositiveData(FailureContext):
@@ -50,3 +50,4 @@ class GenerationConfig:
50
50
  with_security_parameters: bool = True
51
51
  # Header generation configuration
52
52
  headers: HeaderConfig = field(default_factory=HeaderConfig)
53
+ unexpected_methods: set[str] | None = None
@@ -21,7 +21,9 @@ CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[b
21
21
  @dataclass
22
22
  class NegativeDataRejectionConfig:
23
23
  # 5xx will pass through
24
- allowed_statuses: list[str] = field(default_factory=lambda: ["400", "401", "403", "404", "422", "428", "5xx"])
24
+ allowed_statuses: list[str] = field(
25
+ default_factory=lambda: ["400", "401", "403", "404", "406", "422", "428", "5xx"]
26
+ )
25
27
 
26
28
 
27
29
  @dataclass
@@ -150,13 +150,6 @@ class BaseRunner:
150
150
  def _should_warn_about_only_4xx(result: TestResult) -> bool:
151
151
  if all(check.response is None for check in result.checks):
152
152
  return False
153
- # Don't warn if we saw any 2xx or 5xx responses
154
- if any(
155
- check.response.status_code < 400 or check.response.status_code >= 500
156
- for check in result.checks
157
- if check.response is not None
158
- ):
159
- return False
160
153
  # Don't duplicate auth warnings
161
154
  if {check.response.status_code for check in result.checks if check.response is not None} <= {401, 403}:
162
155
  return False
@@ -164,11 +157,28 @@ class BaseRunner:
164
157
  return True
165
158
 
166
159
  def _check_warnings() -> None:
160
+ # Warn if all positive test cases got 4xx in return and no failure was found
161
+ def all_positive_are_rejected(result: TestResult) -> bool:
162
+ seen_positive = False
163
+ for check in result.checks:
164
+ if check.example.data_generation_method != DataGenerationMethod.positive:
165
+ continue
166
+ seen_positive = True
167
+ if check.response is None:
168
+ continue
169
+ # At least one positive response for positive test case
170
+ if 200 <= check.response.status_code < 300:
171
+ return False
172
+ # If there are positive test cases, and we ended up here, then there are no 2xx responses for them
173
+ # Otherwise, there are no positive test cases at all and this check should pass
174
+ return seen_positive
175
+
167
176
  for result in ctx.data.results:
168
177
  # Only warn about 4xx responses in successful positive test scenarios
169
178
  if (
170
179
  all(check.value == Status.success for check in result.checks)
171
- and result.data_generation_method == [DataGenerationMethod.positive]
180
+ and DataGenerationMethod.positive in result.data_generation_method
181
+ and all_positive_are_rejected(result)
172
182
  and _should_warn_about_only_4xx(result)
173
183
  ):
174
184
  ctx.add_warning(
@@ -294,7 +294,12 @@ def missing_required_header(ctx: CheckContext, response: GenericResponse, case:
294
294
 
295
295
 
296
296
  def unsupported_method(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
297
- if case.meta and case.meta.description and case.meta.description.startswith("Unspecified HTTP method:"):
297
+ if (
298
+ case.meta
299
+ and case.meta.description
300
+ and case.meta.description.startswith("Unspecified HTTP method:")
301
+ and response.request.method != "OPTIONS"
302
+ ):
298
303
  if response.status_code != 405:
299
304
  raise AssertionError(
300
305
  f"Unexpected response status for unspecified HTTP method: {response.status_code}\nExpected: 405"
@@ -9,6 +9,8 @@ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
9
9
  import requests
10
10
  from hypothesis_jsonschema import from_schema
11
11
 
12
+ from schemathesis.specs.openapi.serialization import get_serializers_for_operation
13
+
12
14
  from ...constants import DEFAULT_RESPONSE_TIMEOUT
13
15
  from ...generation import get_single_example
14
16
  from ...internal.copy import fast_deepcopy
@@ -48,11 +50,7 @@ def get_strategies_from_examples(
48
50
  operation: APIOperation[OpenAPIParameter, Case], as_strategy_kwargs: dict[str, Any] | None = None
49
51
  ) -> list[SearchStrategy[Case]]:
50
52
  """Build a set of strategies that generate test cases based on explicit examples in the schema."""
51
- maps = {}
52
- for location, container in LOCATION_TO_CONTAINER.items():
53
- serializer = operation.get_parameter_serializer(location)
54
- if serializer is not None:
55
- maps[container] = serializer
53
+ maps = get_serializers_for_operation(operation)
56
54
 
57
55
  def serialize_components(case: Case) -> Case:
58
56
  """Applies special serialization rules for case components.
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  import re
4
4
  from functools import lru_cache
5
5
 
6
+ from ...exceptions import InternalError
7
+
6
8
  try: # pragma: no cover
7
9
  import re._constants as sre
8
10
  import re._parser as sre_parse
@@ -29,7 +31,15 @@ def update_quantifier(pattern: str, min_length: int | None, max_length: int | No
29
31
 
30
32
  try:
31
33
  parsed = sre_parse.parse(pattern)
32
- return _handle_parsed_pattern(parsed, pattern, min_length, max_length)
34
+ updated = _handle_parsed_pattern(parsed, pattern, min_length, max_length)
35
+ try:
36
+ re.compile(updated)
37
+ except re.error as exc:
38
+ raise InternalError(
39
+ f"The combination of min_length={min_length} and max_length={max_length} applied to the original pattern '{pattern}' resulted in an invalid regex: '{updated}'. "
40
+ "This indicates a bug in the regex quantifier merging logic"
41
+ ) from exc
42
+ return updated
33
43
  except re.error:
34
44
  # Invalid pattern
35
45
  return pattern
@@ -114,6 +124,21 @@ def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None,
114
124
 
115
125
  for op, value in pattern_parts:
116
126
  if op == LITERAL:
127
+ # Check if the literal comes from a bracketed expression,
128
+ # e.g. Python regex parses "[+]" as a single LITERAL token.
129
+ if pattern[current_position] == "[":
130
+ # Find the matching closing bracket.
131
+ end_idx = current_position + 1
132
+ while end_idx < len(pattern):
133
+ # Check for an unescaped closing bracket.
134
+ if pattern[end_idx] == "]" and (end_idx == current_position + 1 or pattern[end_idx - 1] != "\\"):
135
+ end_idx += 1
136
+ break
137
+ end_idx += 1
138
+ # Append the entire character set.
139
+ result += pattern[current_position:end_idx]
140
+ current_position = end_idx
141
+ continue
117
142
  if pattern[current_position] == "\\":
118
143
  # Escaped value
119
144
  current_position += 2
@@ -261,13 +286,18 @@ def _handle_repeat_quantifier(
261
286
  min_length, max_length = _build_size(min_repeat, max_repeat, min_length, max_length)
262
287
  if min_length > max_length:
263
288
  return pattern
264
- return f"({_strip_quantifier(pattern).strip(')(')})" + _build_quantifier(min_length, max_length)
289
+ inner = _strip_quantifier(pattern)
290
+ if inner.startswith("(") and inner.endswith(")"):
291
+ inner = inner[1:-1]
292
+ return f"({inner})" + _build_quantifier(min_length, max_length)
265
293
 
266
294
 
267
295
  def _handle_literal_or_in_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
268
296
  """Handle literal or character class quantifiers."""
269
297
  min_length = 1 if min_length is None else max(min_length, 1)
270
- return f"({pattern.strip(')(')})" + _build_quantifier(min_length, max_length)
298
+ if pattern.startswith("(") and pattern.endswith(")"):
299
+ pattern = pattern[1:-1]
300
+ return f"({pattern})" + _build_quantifier(min_length, max_length)
271
301
 
272
302
 
273
303
  def _build_quantifier(minimum: int | None, maximum: int | None) -> str:
@@ -3,6 +3,9 @@ from __future__ import annotations
3
3
  import json
4
4
  from typing import Any, Callable, Dict, Generator, List
5
5
 
6
+ from schemathesis.schemas import APIOperation
7
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
8
+
6
9
  from ...utils import compose
7
10
 
8
11
  Generated = Dict[str, Any]
@@ -11,6 +14,15 @@ DefinitionList = List[Definition]
11
14
  MapFunction = Callable[[Generated], Generated]
12
15
 
13
16
 
17
+ def get_serializers_for_operation(operation: APIOperation) -> dict[str, Callable]:
18
+ serializers = {}
19
+ for location, container in LOCATION_TO_CONTAINER.items():
20
+ serializer = operation.get_parameter_serializer(location)
21
+ if serializer is not None:
22
+ serializers[container] = serializer
23
+ return serializers
24
+
25
+
14
26
  def make_serializer(
15
27
  func: Callable[[DefinitionList], Generator[Callable | None, None, None]],
16
28
  ) -> Callable[[DefinitionList], Callable | None]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 3.39.10
3
+ Version: 3.39.12
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=UW2Bq8hDDkcBeAAA7PzpBFXkOOxkmHox-mfQwzHDjL0,1914
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=x1AKO4Cr3LWZQPcRlIvor7ACn5t-mxLJfdHFgpdGHmI,28437
4
+ schemathesis/_hypothesis.py,sha256=CEfWX38CsPy-RzwMGdKuJD9mY_AV8fIq_ZhabGp4tW0,30759
5
5
  schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
6
6
  schemathesis/_override.py,sha256=TAjYB3eJQmlw9K_xiR9ptt9Wj7if4U7UFlUhGjpBAoM,1625
7
7
  schemathesis/_patches.py,sha256=Hsbpn4UVeXUQD2Kllrbq01CSWsTYENWa0VJTyhX5C2k,895
@@ -12,7 +12,7 @@ schemathesis/checks.py,sha256=YPUI1N5giGBy1072vd77e6HWelGAKrJUmJLEG4oqfF8,2630
12
12
  schemathesis/code_samples.py,sha256=rsdTo6ksyUs3ZMhqx0mmmkPSKUCFa--snIOYsXgZd80,4120
13
13
  schemathesis/constants.py,sha256=RHwog2lAz84qG6KCpP1U15A4a9w1xcwbgZ97aY4juQg,2555
14
14
  schemathesis/exceptions.py,sha256=5zjPlyVoQNJGbwufplL6ZVV7FEBPBNPHGdlQRJ7xnhE,20449
15
- schemathesis/failures.py,sha256=mrDu7F-OrQY8pRMNVtIxTjovhfyIkcXYjnSkRw-OMuQ,8016
15
+ schemathesis/failures.py,sha256=OsQM5FDs79usUM4MLA20WIDaoW6SKTR39IyKDjeiwtQ,8197
16
16
  schemathesis/filters.py,sha256=f3c_yXIBwIin-9Y0qU2TkcC1NEM_Mw34jGUHQc0BOyw,17026
17
17
  schemathesis/graphql.py,sha256=XiuKcfoOB92iLFC8zpz2msLkM0_V0TLdxPNBqrrGZ8w,216
18
18
  schemathesis/hooks.py,sha256=p5AXgjVGtka0jn9MOeyBaRUtNbqZTs4iaJqytYTacHc,14856
@@ -28,16 +28,16 @@ schemathesis/targets.py,sha256=XIGRghvEzbmEJjse9aZgNEj67L3jAbiazm2rxURWgDE,2351
28
28
  schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
29
29
  schemathesis/types.py,sha256=Tem2Q_zyMCd0Clp5iGKv6Fu13wdcbxVE8tCVH9WWt7A,1065
30
30
  schemathesis/utils.py,sha256=LwqxqoAKmRiAdj-qUbNmgQgsamc49V5lc5TnOIDuuMA,4904
31
- schemathesis/cli/__init__.py,sha256=IxJnRTCp0bl1B044jjD9GFJ2pisckw3-PBnNHcABBy4,75795
31
+ schemathesis/cli/__init__.py,sha256=jiDVw31fF8oNl0TyAWJpDo-166PkCqNV4t1rUcy_4oo,76352
32
32
  schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
33
- schemathesis/cli/callbacks.py,sha256=-VA_I_mVma9WxFNtUR8d2KNICKJD5ScayfSdKKPEP5Y,16321
33
+ schemathesis/cli/callbacks.py,sha256=UMCYPfoHXgxtT-uui0zlfeNoOLzteJu90q9ID3qt1m4,16540
34
34
  schemathesis/cli/cassettes.py,sha256=zji-B-uuwyr0Z0BzQX-DLMV6lWb58JtLExcUE1v3m4Y,20153
35
35
  schemathesis/cli/constants.py,sha256=wk-0GsoJIel8wFFerQ6Kf_6eAYUtIWkwMFwyAqv3yj4,1635
36
36
  schemathesis/cli/context.py,sha256=j_lvYQiPa6Q7P4P_IGCM9V2y2gJSpDbpxIIzR5oFB2I,2567
37
37
  schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
38
38
  schemathesis/cli/handlers.py,sha256=EXSAFe5TQlHANz1AVlSttfsoDT2oeaeFbqq1N7e2udw,467
39
39
  schemathesis/cli/junitxml.py,sha256=_psBdqGwH4OKySSWeva41mbgGLav86UnWhQyOt99gnU,5331
40
- schemathesis/cli/options.py,sha256=yL7nrzKkbGCc4nQya9wpTW48XGz_OT9hOFrzPxRrDe4,2853
40
+ schemathesis/cli/options.py,sha256=jPqJxkuAb91wtB_aOUFkGDdGtJ3UDwT2Nn3vvL-odsE,3062
41
41
  schemathesis/cli/reporting.py,sha256=KC3sxSc1u4aFQ-0Q8CQ3G4HTEl7QxlubGnJgNKmVJdQ,3627
42
42
  schemathesis/cli/sanitization.py,sha256=Onw_NWZSom6XTVNJ5NHnC0PAhrYAcGzIXJbsBCzLkn4,1005
43
43
  schemathesis/cli/output/__init__.py,sha256=AXaUzQ1nhQ-vXhW4-X-91vE2VQtEcCOrGtQXXNN55iQ,29
@@ -58,12 +58,12 @@ schemathesis/extra/pytest_plugin.py,sha256=3FF7pcqK26J__FGq6uOZdaD-1tZ-khQEwpdwb
58
58
  schemathesis/fixups/__init__.py,sha256=RP5QYJVJhp8LXjhH89fCRaIVU26dHCy74jD9seoYMuc,967
59
59
  schemathesis/fixups/fast_api.py,sha256=mn-KzBqnR8jl4W5fY-_ZySabMDMUnpzCIESMHnlvE1c,1304
60
60
  schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZETA,745
61
- schemathesis/generation/__init__.py,sha256=29Zys_tD6kfngaC4zHeC6TOBZQcmo7CWm7KDSYsHStQ,1581
61
+ schemathesis/generation/__init__.py,sha256=PClFLK3bu-8Gsy71rgdD0ULMqySrzX-Um8Tan77x_5A,1628
62
62
  schemathesis/generation/_hypothesis.py,sha256=74fzLPHugZgMQXerWYFAMqCAjtAXz5E4gek7Gnkhli4,1756
63
63
  schemathesis/generation/_methods.py,sha256=r8oVlJ71_gXcnEhU-byw2E0R2RswQQFm8U7yGErSqbw,1204
64
64
  schemathesis/generation/coverage.py,sha256=1CilQSe2DIdMdeWA6RL22so2bZULPRwc0CQBRxcLRFs,39370
65
65
  schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
66
- schemathesis/internal/checks.py,sha256=YBhldvs-oQTrtvTlz3cjaO9Ri2oQeyobFcquO4Y0UJ8,2720
66
+ schemathesis/internal/checks.py,sha256=ZPvsPJ7gWwK0IpzBFgOMaq4L2e0yfeC8qxPrnpauVFA,2741
67
67
  schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
68
68
  schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
69
69
  schemathesis/internal/deprecation.py,sha256=XnzwSegbbdQyoTF1OGW_s9pdjIfN_Uzzdb2rfah1w2o,1261
@@ -80,7 +80,7 @@ schemathesis/runner/probes.py,sha256=no5AfO3kse25qvHevjeUfB0Q3C860V2AYzschUW3QMQ
80
80
  schemathesis/runner/serialization.py,sha256=vZi1wd9HX9Swp9VJ_hZFeDgy3Y726URpHra-TbPvQhk,20762
81
81
  schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
82
82
  schemathesis/runner/impl/context.py,sha256=KY06FXVOFQ6DBaa_FomSBXL81ULs3D21IW1u3yLqs1E,2434
83
- schemathesis/runner/impl/core.py,sha256=vNWUu5F36brmhsv0P9UYavXLklQo5O2A6byQmS75enY,49289
83
+ schemathesis/runner/impl/core.py,sha256=pB_0zs_MfzAcbrVvRU85mONAn8QXVqFfM_y7yV8a2l0,49962
84
84
  schemathesis/runner/impl/solo.py,sha256=y5QSxgK8nBCEjZVD5BpFvYUXmB6tEjk6TwxAo__NejA,2911
85
85
  schemathesis/runner/impl/threadpool.py,sha256=yNR5LYE8f3N_4t42OwSgy0_qdGgBPM7d11F9c9oEAAs,15075
86
86
  schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
@@ -107,21 +107,21 @@ schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzM
107
107
  schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
108
108
  schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
109
109
  schemathesis/specs/openapi/_hypothesis.py,sha256=nU8UDn1PzGCre4IVmwIuO9-CZv1KJe1fYY0d2BojhSo,22981
110
- schemathesis/specs/openapi/checks.py,sha256=NzUoZ0gZMjyC12KfV7J-5ww8IMaaaNiKum4y7bmA_EA,26816
110
+ schemathesis/specs/openapi/checks.py,sha256=cuHTZsoHV2fdUz23_F99-mLelT1xtvaiS9EcG7R-hZs,26897
111
111
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
112
112
  schemathesis/specs/openapi/converter.py,sha256=Yxw9lS_JKEyi-oJuACT07fm04bqQDlAu-iHwzkeDvE4,3546
113
113
  schemathesis/specs/openapi/definitions.py,sha256=WTkWwCgTc3OMxfKsqh6YDoGfZMTThSYrHGp8h0vLAK0,93935
114
- schemathesis/specs/openapi/examples.py,sha256=hdeq7et8AexYGY2iU6SfMZWJ7G0PbOfapUtc4upNs_4,20483
114
+ schemathesis/specs/openapi/examples.py,sha256=yBK0hjq5ROjk7BCLe7BO2dr7raijeZ6_KlZEol-cU-E,20401
115
115
  schemathesis/specs/openapi/formats.py,sha256=3KtEC-8nQRwMErS-WpMadXsr8R0O-NzYwFisZqMuc-8,2761
116
116
  schemathesis/specs/openapi/links.py,sha256=C4Uir2P_EcpqME8ee_a1vdUM8Tm3ZcKNn2YsGjZiMUQ,17935
117
117
  schemathesis/specs/openapi/loaders.py,sha256=jlTYLoG5sVRh8xycIF2M2VDCZ44M80Sct07a_ycg1Po,25698
118
118
  schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
119
119
  schemathesis/specs/openapi/parameters.py,sha256=X_3PKqUScIiN_vbSFEauPYyxASyFv-_9lZ_9QEZRLqo,14655
120
- schemathesis/specs/openapi/patterns.py,sha256=OxZp31cBEHv8fwoeYJ9JcdWNHFMIGzRISNN3dCBc9Dg,11260
120
+ schemathesis/specs/openapi/patterns.py,sha256=L99UtslPvwObCVf5ndq3vL2YjQ7H1nMb-ZNMcyz_Qvk,12677
121
121
  schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
122
122
  schemathesis/specs/openapi/schemas.py,sha256=JA9SiBnwYg75kYnd4_0CWOuQv_XTfYwuDeGmFe4RtVo,53724
123
123
  schemathesis/specs/openapi/security.py,sha256=Z-6pk2Ga1PTUtBe298KunjVHsNh5A-teegeso7zcPIE,7138
124
- schemathesis/specs/openapi/serialization.py,sha256=5qGdFHZ3n80UlbSXrO_bkr4Al_7ci_Z3aSUjZczNDQY,11384
124
+ schemathesis/specs/openapi/serialization.py,sha256=rcZfqQbWer_RELedu4Sh5h_RhKYPWTfUjnmLwpP2R_A,11842
125
125
  schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
126
126
  schemathesis/specs/openapi/validation.py,sha256=Q9ThZlwU-mSz7ExDnIivnZGi1ivC5hlX2mIMRAM79kc,999
127
127
  schemathesis/specs/openapi/expressions/__init__.py,sha256=hFpJrIWbPi55GcIVjNFRDDUL8xmDu3mdbdldoHBoFJ0,1729
@@ -153,8 +153,8 @@ schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZ
153
153
  schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
154
154
  schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
155
155
  schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
156
- schemathesis-3.39.10.dist-info/METADATA,sha256=3nOKrS8YGysaLXjvt4uIeysV4bbnLxm1WRoG_DD8W8s,11977
157
- schemathesis-3.39.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
158
- schemathesis-3.39.10.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
- schemathesis-3.39.10.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
- schemathesis-3.39.10.dist-info/RECORD,,
156
+ schemathesis-3.39.12.dist-info/METADATA,sha256=B2ychHNPR-8UTeOqBUcqdtLzYPdZEO61YaYadjp9B2A,11977
157
+ schemathesis-3.39.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
158
+ schemathesis-3.39.12.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
+ schemathesis-3.39.12.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
+ schemathesis-3.39.12.dist-info/RECORD,,