schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. schemathesis/__init__.py +3 -7
  2. schemathesis/checks.py +17 -7
  3. schemathesis/cli/commands/__init__.py +51 -3
  4. schemathesis/cli/commands/data.py +10 -0
  5. schemathesis/cli/commands/run/__init__.py +147 -260
  6. schemathesis/cli/commands/run/context.py +2 -3
  7. schemathesis/cli/commands/run/events.py +4 -0
  8. schemathesis/cli/commands/run/executor.py +60 -73
  9. schemathesis/cli/commands/run/filters.py +15 -165
  10. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  11. schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
  12. schemathesis/cli/commands/run/handlers/output.py +26 -47
  13. schemathesis/cli/commands/run/loaders.py +35 -50
  14. schemathesis/cli/commands/run/validation.py +36 -161
  15. schemathesis/cli/core.py +5 -3
  16. schemathesis/cli/ext/fs.py +7 -5
  17. schemathesis/cli/ext/options.py +0 -21
  18. schemathesis/config/__init__.py +188 -0
  19. schemathesis/config/_auth.py +51 -0
  20. schemathesis/config/_checks.py +268 -0
  21. schemathesis/config/_diff_base.py +99 -0
  22. schemathesis/config/_env.py +21 -0
  23. schemathesis/config/_error.py +156 -0
  24. schemathesis/config/_generation.py +150 -0
  25. schemathesis/config/_health_check.py +24 -0
  26. schemathesis/config/_operations.py +313 -0
  27. schemathesis/config/_output.py +171 -0
  28. schemathesis/config/_parameters.py +19 -0
  29. schemathesis/config/_phases.py +151 -0
  30. schemathesis/config/_projects.py +495 -0
  31. schemathesis/config/_rate_limit.py +17 -0
  32. schemathesis/config/_report.py +116 -0
  33. schemathesis/config/_validator.py +9 -0
  34. schemathesis/config/schema.json +837 -0
  35. schemathesis/core/__init__.py +2 -0
  36. schemathesis/core/compat.py +16 -9
  37. schemathesis/core/errors.py +19 -2
  38. schemathesis/core/failures.py +6 -7
  39. schemathesis/core/hooks.py +20 -0
  40. schemathesis/core/output/__init__.py +14 -37
  41. schemathesis/core/output/sanitization.py +3 -146
  42. schemathesis/core/validation.py +16 -0
  43. schemathesis/engine/__init__.py +2 -4
  44. schemathesis/engine/context.py +41 -43
  45. schemathesis/engine/core.py +7 -5
  46. schemathesis/engine/phases/__init__.py +10 -0
  47. schemathesis/engine/phases/probes.py +8 -8
  48. schemathesis/engine/phases/stateful/_executor.py +68 -43
  49. schemathesis/engine/phases/unit/__init__.py +23 -15
  50. schemathesis/engine/phases/unit/_executor.py +77 -17
  51. schemathesis/engine/phases/unit/_pool.py +1 -1
  52. schemathesis/errors.py +2 -0
  53. schemathesis/filters.py +2 -3
  54. schemathesis/generation/__init__.py +6 -31
  55. schemathesis/generation/case.py +5 -3
  56. schemathesis/generation/coverage.py +153 -123
  57. schemathesis/generation/hypothesis/builder.py +40 -14
  58. schemathesis/generation/meta.py +3 -3
  59. schemathesis/generation/overrides.py +37 -1
  60. schemathesis/generation/stateful/state_machine.py +8 -1
  61. schemathesis/graphql/loaders.py +21 -12
  62. schemathesis/openapi/checks.py +12 -8
  63. schemathesis/openapi/generation/filters.py +10 -8
  64. schemathesis/openapi/loaders.py +22 -13
  65. schemathesis/pytest/lazy.py +2 -5
  66. schemathesis/pytest/plugin.py +11 -2
  67. schemathesis/schemas.py +13 -61
  68. schemathesis/specs/graphql/schemas.py +11 -15
  69. schemathesis/specs/openapi/_hypothesis.py +12 -8
  70. schemathesis/specs/openapi/checks.py +16 -18
  71. schemathesis/specs/openapi/examples.py +4 -3
  72. schemathesis/specs/openapi/formats.py +2 -2
  73. schemathesis/specs/openapi/negative/__init__.py +2 -2
  74. schemathesis/specs/openapi/patterns.py +46 -16
  75. schemathesis/specs/openapi/references.py +2 -3
  76. schemathesis/specs/openapi/schemas.py +11 -20
  77. schemathesis/specs/openapi/stateful/__init__.py +10 -5
  78. schemathesis/transport/prepare.py +7 -6
  79. schemathesis/transport/requests.py +3 -1
  80. schemathesis/transport/wsgi.py +3 -4
  81. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/METADATA +7 -8
  82. schemathesis-4.0.0a11.dist-info/RECORD +166 -0
  83. schemathesis/cli/commands/run/checks.py +0 -79
  84. schemathesis/cli/commands/run/hypothesis.py +0 -78
  85. schemathesis/cli/commands/run/reports.py +0 -72
  86. schemathesis/cli/hooks.py +0 -36
  87. schemathesis/engine/config.py +0 -59
  88. schemathesis/experimental/__init__.py +0 -72
  89. schemathesis-4.0.0a10.dist-info/RECORD +0 -153
  90. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/WHEEL +0 -0
  91. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/entry_points.txt +0 -0
  92. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/licenses/LICENSE +0 -0
schemathesis/schemas.py CHANGED
@@ -14,16 +14,15 @@ from typing import (
14
14
  NoReturn,
15
15
  TypeVar,
16
16
  )
17
- from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
17
+ from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
18
18
 
19
19
  from schemathesis import transport
20
+ from schemathesis.config import ProjectConfig
20
21
  from schemathesis.core import NOT_SET, NotSet
21
22
  from schemathesis.core.errors import IncorrectUsage, InvalidSchema
22
- from schemathesis.core.output import OutputConfig
23
- from schemathesis.core.rate_limit import build_limiter
24
23
  from schemathesis.core.result import Ok, Result
25
24
  from schemathesis.core.transport import Response
26
- from schemathesis.generation import GenerationConfig, GenerationMode
25
+ from schemathesis.generation import GenerationMode
27
26
  from schemathesis.generation.case import Case
28
27
  from schemathesis.generation.hypothesis import strategies
29
28
  from schemathesis.generation.hypothesis.given import GivenInput, given_proxy
@@ -42,7 +41,6 @@ from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScop
42
41
 
43
42
  if TYPE_CHECKING:
44
43
  from hypothesis.strategies import SearchStrategy
45
- from pyrate_limiter import Limiter
46
44
  from typing_extensions import Self
47
45
 
48
46
  from schemathesis.core import Specification
@@ -102,16 +100,13 @@ class ApiOperationsCount:
102
100
  @dataclass(eq=False)
103
101
  class BaseSchema(Mapping):
104
102
  raw_schema: dict[str, Any]
103
+ config: ProjectConfig
105
104
  location: str | None = None
106
- base_url: str | None = None
107
105
  filter_set: FilterSet = field(default_factory=FilterSet)
108
106
  app: Any = None
109
107
  hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
110
108
  auth: AuthStorage = field(default_factory=AuthStorage)
111
109
  test_function: Callable | None = None
112
- generation_config: GenerationConfig = field(default_factory=GenerationConfig)
113
- output_config: OutputConfig = field(default_factory=OutputConfig)
114
- rate_limiter: Limiter | None = None
115
110
 
116
111
  def __post_init__(self) -> None:
117
112
  self.hook = to_filterable_hook(self.hooks) # type: ignore[method-assign]
@@ -227,8 +222,8 @@ class BaseSchema(Mapping):
227
222
  """Base path for the schema."""
228
223
  # if `base_url` is specified, then it should include base path
229
224
  # Example: http://127.0.0.1:8080/api
230
- if self.base_url:
231
- path = urlsplit(self.base_url).path
225
+ if self.config.base_url:
226
+ path = urlsplit(self.config.base_url).path
232
227
  else:
233
228
  path = self._get_base_path()
234
229
  if not path.endswith("/"):
@@ -244,7 +239,7 @@ class BaseSchema(Mapping):
244
239
  return urlunsplit(parts)
245
240
 
246
241
  def get_base_url(self) -> str:
247
- base_url = self.base_url
242
+ base_url = self.config.base_url
248
243
  if base_url is not None:
249
244
  return base_url.rstrip("/")
250
245
  return self._build_base_url()
@@ -259,9 +254,7 @@ class BaseSchema(Mapping):
259
254
  def _measure_statistic(self) -> ApiStatistic:
260
255
  raise NotImplementedError
261
256
 
262
- def get_all_operations(
263
- self, generation_config: GenerationConfig | None = None
264
- ) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
257
+ def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
265
258
  raise NotImplementedError
266
259
 
267
260
  def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
@@ -317,15 +310,12 @@ class BaseSchema(Mapping):
317
310
 
318
311
  return self.__class__(
319
312
  self.raw_schema,
313
+ config=self.config,
320
314
  location=self.location,
321
- base_url=self.base_url,
322
315
  app=self.app,
323
316
  hooks=self.hooks,
324
317
  auth=self.auth,
325
318
  test_function=_test_function,
326
- generation_config=self.generation_config,
327
- output_config=self.output_config,
328
- rate_limiter=self.rate_limiter,
329
319
  filter_set=_filter_set,
330
320
  )
331
321
 
@@ -379,7 +369,6 @@ class BaseSchema(Mapping):
379
369
  hooks: HookDispatcher | None = None,
380
370
  auth_storage: AuthStorage | None = None,
381
371
  generation_mode: GenerationMode = GenerationMode.default(),
382
- generation_config: GenerationConfig | None = None,
383
372
  **kwargs: Any,
384
373
  ) -> SearchStrategy:
385
374
  raise NotImplementedError
@@ -408,7 +397,6 @@ class BaseSchema(Mapping):
408
397
  hooks: HookDispatcher | None = None,
409
398
  auth_storage: AuthStorage | None = None,
410
399
  generation_mode: GenerationMode = GenerationMode.default(),
411
- generation_config: GenerationConfig | None = None,
412
400
  **kwargs: Any,
413
401
  ) -> SearchStrategy:
414
402
  """Build a strategy for generating test cases for all defined API operations."""
@@ -417,7 +405,6 @@ class BaseSchema(Mapping):
417
405
  hooks=hooks,
418
406
  auth_storage=auth_storage,
419
407
  generation_mode=generation_mode,
420
- generation_config=generation_config,
421
408
  **kwargs,
422
409
  )
423
410
  for operation in self.get_all_operations()
@@ -428,48 +415,16 @@ class BaseSchema(Mapping):
428
415
  def configure(
429
416
  self,
430
417
  *,
431
- base_url: str | None | NotSet = NOT_SET,
432
418
  location: str | None | NotSet = NOT_SET,
433
- rate_limit: str | None | NotSet = NOT_SET,
434
- generation: GenerationConfig | NotSet = NOT_SET,
435
- output: OutputConfig | NotSet = NOT_SET,
436
419
  app: Any | NotSet = NOT_SET,
437
420
  ) -> Self:
438
- if not isinstance(base_url, NotSet):
439
- if base_url is not None:
440
- validate_base_url(base_url)
441
- self.base_url = base_url
442
421
  if not isinstance(location, NotSet):
443
422
  self.location = location
444
- if not isinstance(rate_limit, NotSet):
445
- if isinstance(rate_limit, str):
446
- self.rate_limiter = build_limiter(rate_limit)
447
- else:
448
- self.rate_limiter = None
449
- if not isinstance(generation, NotSet):
450
- self.generation_config = generation
451
- if not isinstance(output, NotSet):
452
- self.output_config = output
453
423
  if not isinstance(app, NotSet):
454
424
  self.app = app
455
425
  return self
456
426
 
457
427
 
458
- INVALID_BASE_URL_MESSAGE = (
459
- "The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
460
- "Make sure it is a properly formatted URL."
461
- )
462
-
463
-
464
- def validate_base_url(value: str) -> None:
465
- try:
466
- netloc = urlparse(value).netloc
467
- except ValueError as exc:
468
- raise ValueError(INVALID_BASE_URL_MESSAGE) from exc
469
- if value and not netloc:
470
- raise ValueError(INVALID_BASE_URL_MESSAGE)
471
-
472
-
473
428
  @dataclass
474
429
  class APIOperationMap(Mapping):
475
430
  _schema: BaseSchema
@@ -489,7 +444,6 @@ class APIOperationMap(Mapping):
489
444
  hooks: HookDispatcher | None = None,
490
445
  auth_storage: AuthStorage | None = None,
491
446
  generation_mode: GenerationMode = GenerationMode.default(),
492
- generation_config: GenerationConfig | None = None,
493
447
  **kwargs: Any,
494
448
  ) -> SearchStrategy:
495
449
  """Build a strategy for generating test cases for all API operations defined in this subset."""
@@ -498,7 +452,6 @@ class APIOperationMap(Mapping):
498
452
  hooks=hooks,
499
453
  auth_storage=auth_storage,
500
454
  generation_mode=generation_mode,
501
- generation_config=generation_config,
502
455
  **kwargs,
503
456
  )
504
457
  for operation in self._data.values()
@@ -638,6 +591,9 @@ class APIOperation(Generic[P]):
638
591
  if self.label is None:
639
592
  self.label = f"{self.method.upper()} {self.path}" # type: ignore
640
593
 
594
+ def __deepcopy__(self, memo: dict) -> APIOperation[P]:
595
+ return self
596
+
641
597
  @property
642
598
  def full_path(self) -> str:
643
599
  return self.schema.get_full_path(self.path)
@@ -688,13 +644,10 @@ class APIOperation(Generic[P]):
688
644
  hooks: HookDispatcher | None = None,
689
645
  auth_storage: AuthStorage | None = None,
690
646
  generation_mode: GenerationMode = GenerationMode.default(),
691
- generation_config: GenerationConfig | None = None,
692
647
  **kwargs: Any,
693
648
  ) -> SearchStrategy[Case]:
694
649
  """Turn this API operation into a Hypothesis strategy."""
695
- strategy = self.schema.get_case_strategy(
696
- self, hooks, auth_storage, generation_mode, generation_config=generation_config, **kwargs
697
- )
650
+ strategy = self.schema.get_case_strategy(self, hooks, auth_storage, generation_mode, **kwargs)
698
651
 
699
652
  def _apply_hooks(dispatcher: HookDispatcher, _strategy: SearchStrategy[Case]) -> SearchStrategy[Case]:
700
653
  context = HookContext(self)
@@ -722,7 +675,6 @@ class APIOperation(Generic[P]):
722
675
 
723
676
  def get_strategies_from_examples(self, **kwargs: Any) -> list[SearchStrategy[Case]]:
724
677
  """Get examples from the API operation."""
725
- kwargs.setdefault("generation_config", self.schema.generation_config)
726
678
  return self.schema.get_strategies_from_examples(self, **kwargs)
727
679
 
728
680
  def get_parameter_serializer(self, location: str) -> Callable | None:
@@ -28,7 +28,7 @@ from schemathesis import auths
28
28
  from schemathesis.core import NOT_SET, NotSet, Specification
29
29
  from schemathesis.core.errors import InvalidSchema, OperationNotFound
30
30
  from schemathesis.core.result import Ok, Result
31
- from schemathesis.generation import GenerationConfig, GenerationMode
31
+ from schemathesis.generation import GenerationMode
32
32
  from schemathesis.generation.case import Case
33
33
  from schemathesis.generation.meta import (
34
34
  CaseMetadata,
@@ -139,8 +139,8 @@ class GraphQLSchema(BaseSchema):
139
139
 
140
140
  @property
141
141
  def base_path(self) -> str:
142
- if self.base_url:
143
- return urlsplit(self.base_url).path
142
+ if self.config.base_url:
143
+ return urlsplit(self.config.base_url).path
144
144
  return self._get_base_path()
145
145
 
146
146
  def _get_base_path(self) -> str:
@@ -171,9 +171,7 @@ class GraphQLSchema(BaseSchema):
171
171
  statistic.operations.selected += 1
172
172
  return statistic
173
173
 
174
- def get_all_operations(
175
- self, generation_config: GenerationConfig | None = None
176
- ) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
174
+ def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
177
175
  schema = self.client_schema
178
176
  for root_type, operation_type in (
179
177
  (RootType.QUERY, schema.query_type),
@@ -226,7 +224,6 @@ class GraphQLSchema(BaseSchema):
226
224
  hooks: HookDispatcher | None = None,
227
225
  auth_storage: AuthStorage | None = None,
228
226
  generation_mode: GenerationMode = GenerationMode.default(),
229
- generation_config: GenerationConfig | None = None,
230
227
  **kwargs: Any,
231
228
  ) -> SearchStrategy:
232
229
  return graphql_cases(
@@ -234,7 +231,6 @@ class GraphQLSchema(BaseSchema):
234
231
  hooks=hooks,
235
232
  auth_storage=auth_storage,
236
233
  generation_mode=generation_mode,
237
- generation_config=generation_config or self.generation_config,
238
234
  **kwargs,
239
235
  )
240
236
 
@@ -326,14 +322,13 @@ def graphql_cases(
326
322
  hooks: HookDispatcher | None = None,
327
323
  auth_storage: auths.AuthStorage | None = None,
328
324
  generation_mode: GenerationMode = GenerationMode.default(),
329
- generation_config: GenerationConfig,
330
325
  path_parameters: NotSet | dict[str, Any] = NOT_SET,
331
326
  headers: NotSet | dict[str, Any] = NOT_SET,
332
327
  cookies: NotSet | dict[str, Any] = NOT_SET,
333
328
  query: NotSet | dict[str, Any] = NOT_SET,
334
329
  body: Any = NOT_SET,
335
330
  media_type: str | None = None,
336
- phase: TestPhase = TestPhase.GENERATE,
331
+ phase: TestPhase = TestPhase.FUZZING,
337
332
  ) -> Any:
338
333
  start = time.monotonic()
339
334
  definition = cast(GraphQLOperationDefinition, operation.definition)
@@ -343,14 +338,15 @@ def graphql_cases(
343
338
  }[definition.root_type]
344
339
  hook_context = HookContext(operation)
345
340
  custom_scalars = {**get_extra_scalar_strategies(), **CUSTOM_SCALARS}
341
+ generation = operation.schema.config.generation_for(operation=operation, phase="fuzzing")
346
342
  strategy = strategy_factory(
347
343
  operation.schema.client_schema, # type: ignore[attr-defined]
348
344
  fields=[definition.field_name],
349
345
  custom_scalars=custom_scalars,
350
346
  print_ast=_noop, # type: ignore
351
- allow_x00=generation_config.allow_x00,
352
- allow_null=generation_config.graphql_allow_null,
353
- codec=generation_config.codec,
347
+ allow_x00=generation.allow_x00,
348
+ allow_null=generation.graphql_allow_null,
349
+ codec=generation.codec,
354
350
  )
355
351
  strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
356
352
  body = draw(strategy)
@@ -361,8 +357,8 @@ def graphql_cases(
361
357
  query_ = _generate_parameter("query", query, draw, operation, hook_context, hooks)
362
358
 
363
359
  _phase_data = {
364
- TestPhase.EXPLICIT: ExplicitPhaseData(),
365
- TestPhase.GENERATE: GeneratePhaseData(),
360
+ TestPhase.EXAMPLES: ExplicitPhaseData(),
361
+ TestPhase.FUZZING: GeneratePhaseData(),
366
362
  }[phase]
367
363
  phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
368
364
  instance = operation.Case(
@@ -11,6 +11,7 @@ from hypothesis import event, note, reject
11
11
  from hypothesis import strategies as st
12
12
  from hypothesis_jsonschema import from_schema
13
13
 
14
+ from schemathesis.config import GenerationConfig
14
15
  from schemathesis.core import NOT_SET, NotSet, media_types
15
16
  from schemathesis.core.control import SkipTest
16
17
  from schemathesis.core.errors import SERIALIZERS_SUGGESTION_MESSAGE, SerializationNotPossible
@@ -30,7 +31,7 @@ from schemathesis.openapi.generation.filters import is_valid_header, is_valid_pa
30
31
  from schemathesis.schemas import APIOperation
31
32
 
32
33
  from ... import auths
33
- from ...generation import GenerationConfig, GenerationMode
34
+ from ...generation import GenerationMode
34
35
  from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
35
36
  from .constants import LOCATION_TO_CONTAINER
36
37
  from .formats import HEADER_FORMAT, STRING_FORMATS, get_default_format_strategies, header_values
@@ -52,14 +53,14 @@ def openapi_cases(
52
53
  hooks: HookDispatcher | None = None,
53
54
  auth_storage: auths.AuthStorage | None = None,
54
55
  generation_mode: GenerationMode = GenerationMode.default(),
55
- generation_config: GenerationConfig,
56
56
  path_parameters: NotSet | dict[str, Any] = NOT_SET,
57
57
  headers: NotSet | dict[str, Any] = NOT_SET,
58
58
  cookies: NotSet | dict[str, Any] = NOT_SET,
59
59
  query: NotSet | dict[str, Any] = NOT_SET,
60
60
  body: Any = NOT_SET,
61
61
  media_type: str | None = None,
62
- phase: TestPhase = TestPhase.GENERATE,
62
+ phase: TestPhase = TestPhase.FUZZING,
63
+ __is_stateful_phase: bool = False,
63
64
  ) -> Any:
64
65
  """A strategy that creates `Case` instances.
65
66
 
@@ -76,6 +77,9 @@ def openapi_cases(
76
77
  start = time.monotonic()
77
78
  strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
78
79
 
80
+ phase_name = "stateful" if __is_stateful_phase else phase.value
81
+ generation_config = operation.schema.config.generation_for(operation=operation, phase=phase_name)
82
+
79
83
  context = HookContext(operation)
80
84
 
81
85
  path_parameters_ = generate_parameter(
@@ -147,8 +151,8 @@ def openapi_cases(
147
151
  reject()
148
152
 
149
153
  _phase_data = {
150
- TestPhase.EXPLICIT: ExplicitPhaseData(),
151
- TestPhase.GENERATE: GeneratePhaseData(),
154
+ TestPhase.EXAMPLES: ExplicitPhaseData(),
155
+ TestPhase.FUZZING: GeneratePhaseData(),
152
156
  }[phase]
153
157
  phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
154
158
 
@@ -407,10 +411,10 @@ def _build_custom_formats(
407
411
  custom_formats: dict[str, st.SearchStrategy] | None, generation_config: GenerationConfig
408
412
  ) -> dict[str, st.SearchStrategy]:
409
413
  custom_formats = {**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})}
410
- if generation_config.headers.strategy is not None:
411
- custom_formats[HEADER_FORMAT] = generation_config.headers.strategy
414
+ if generation_config.exclude_header_characters is not None:
415
+ custom_formats[HEADER_FORMAT] = header_values(exclude_characters=generation_config.exclude_header_characters)
412
416
  elif not generation_config.allow_x00:
413
- custom_formats[HEADER_FORMAT] = header_values(blacklist_characters="\n\r\x00")
417
+ custom_formats[HEADER_FORMAT] = header_values(exclude_characters="\n\r\x00")
414
418
  return custom_formats
415
419
 
416
420
 
@@ -22,9 +22,6 @@ from schemathesis.openapi.checks import (
22
22
  MalformedMediaType,
23
23
  MissingContentType,
24
24
  MissingHeaders,
25
- MissingRequiredHeaderConfig,
26
- NegativeDataRejectionConfig,
27
- PositiveDataAcceptanceConfig,
28
25
  RejectedPositiveData,
29
26
  UndefinedContentType,
30
27
  UndefinedStatusCode,
@@ -185,7 +182,7 @@ def response_headers_conformance(ctx: CheckContext, response: Response, case: Ca
185
182
  title="Response header does not conform to the schema",
186
183
  operation=case.operation.label,
187
184
  exc=exc,
188
- output_config=case.operation.schema.output_config,
185
+ config=case.operation.schema.config.output,
189
186
  )
190
187
  )
191
188
  return _maybe_raise_one_or_more(errors) # type: ignore[func-returns-value]
@@ -233,8 +230,8 @@ def negative_data_rejection(ctx: CheckContext, response: Response, case: Case) -
233
230
  ):
234
231
  return True
235
232
 
236
- config = ctx.config.get(negative_data_rejection, NegativeDataRejectionConfig())
237
- allowed_statuses = expand_status_codes(config.allowed_statuses or [])
233
+ config = ctx.config.negative_data_rejection
234
+ allowed_statuses = expand_status_codes(config.expected_statuses or [])
238
235
 
239
236
  if (
240
237
  case.meta.generation.mode.is_negative
@@ -243,9 +240,9 @@ def negative_data_rejection(ctx: CheckContext, response: Response, case: Case) -
243
240
  ):
244
241
  raise AcceptedNegativeData(
245
242
  operation=case.operation.label,
246
- message=f"Allowed statuses: {', '.join(config.allowed_statuses)}",
243
+ message=f"Allowed statuses: {', '.join(config.expected_statuses)}",
247
244
  status_code=response.status_code,
248
- allowed_statuses=config.allowed_statuses,
245
+ expected_statuses=config.expected_statuses,
249
246
  )
250
247
  return None
251
248
 
@@ -261,21 +258,21 @@ def positive_data_acceptance(ctx: CheckContext, response: Response, case: Case)
261
258
  ):
262
259
  return True
263
260
 
264
- config = ctx.config.get(positive_data_acceptance, PositiveDataAcceptanceConfig())
265
- allowed_statuses = expand_status_codes(config.allowed_statuses or [])
261
+ config = ctx.config.positive_data_acceptance
262
+ allowed_statuses = expand_status_codes(config.expected_statuses or [])
266
263
 
267
264
  if case.meta.generation.mode.is_positive and response.status_code not in allowed_statuses:
268
265
  raise RejectedPositiveData(
269
266
  operation=case.operation.label,
270
- message=f"Allowed statuses: {', '.join(config.allowed_statuses)}",
267
+ message=f"Allowed statuses: {', '.join(config.expected_statuses)}",
271
268
  status_code=response.status_code,
272
- allowed_statuses=config.allowed_statuses,
269
+ allowed_statuses=config.expected_statuses,
273
270
  )
274
271
  return None
275
272
 
276
273
 
274
+ @schemathesis.check
277
275
  def missing_required_header(ctx: CheckContext, response: Response, case: Case) -> bool | None:
278
- # NOTE: This check is intentionally not registered with `@schemathesis.check` because it is experimental
279
276
  meta = case.meta
280
277
  if meta is None or not isinstance(meta.phase.data, CoveragePhaseData) or is_unexpected_http_status_case(case):
281
278
  return None
@@ -287,16 +284,17 @@ def missing_required_header(ctx: CheckContext, response: Response, case: Case) -
287
284
  and data.description.startswith("Missing ")
288
285
  ):
289
286
  if data.parameter.lower() == "authorization":
290
- allowed_statuses = {401}
287
+ expected_statuses = {401}
291
288
  else:
292
- config = ctx.config.get(missing_required_header, MissingRequiredHeaderConfig())
293
- allowed_statuses = expand_status_codes(config.allowed_statuses or [])
294
- if response.status_code not in allowed_statuses:
295
- allowed = f"Allowed statuses: {', '.join(map(str, allowed_statuses))}"
289
+ config = ctx.config.missing_required_header
290
+ expected_statuses = expand_status_codes(config.expected_statuses or [])
291
+ if response.status_code not in expected_statuses:
292
+ allowed = f"Allowed statuses: {', '.join(map(str, expected_statuses))}"
296
293
  raise AssertionError(f"Unexpected response status for a missing header: {response.status_code}\n{allowed}")
297
294
  return None
298
295
 
299
296
 
297
+ @schemathesis.check
300
298
  def unsupported_method(ctx: CheckContext, response: Response, case: Case) -> bool | None:
301
299
  meta = case.meta
302
300
  if meta is None or not isinstance(meta.phase.data, CoveragePhaseData) or response.request.method == "OPTIONS":
@@ -9,9 +9,9 @@ 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.config import GenerationConfig
12
13
  from schemathesis.core.transforms import deepclone
13
14
  from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
14
- from schemathesis.generation import GenerationConfig
15
15
  from schemathesis.generation.case import Case
16
16
  from schemathesis.generation.hypothesis import examples
17
17
  from schemathesis.generation.meta import TestPhase
@@ -68,7 +68,7 @@ def get_strategies_from_examples(
68
68
  # Add examples from parameter's schemas
69
69
  examples.extend(extract_from_schemas(operation))
70
70
  return [
71
- openapi_cases(operation=operation, **{**parameters, **kwargs, "phase": TestPhase.EXPLICIT}).map(
71
+ openapi_cases(operation=operation, **{**parameters, **kwargs, "phase": TestPhase.EXAMPLES}).map(
72
72
  serialize_components
73
73
  )
74
74
  for parameters in produce_combinations(examples)
@@ -274,12 +274,13 @@ def extract_from_schema(
274
274
  continue
275
275
  variants[name] = values
276
276
  if variants:
277
+ config = operation.schema.config.generation_for(operation=operation, phase="examples")
277
278
  for name, subschema in to_generate.items():
278
279
  if name in variants:
279
280
  # Generated by one of `anyOf` or similar sub-schemas
280
281
  continue
281
282
  subschema = operation.schema.prepare_schema(subschema)
282
- generated = _generate_single_example(subschema, operation.schema.generation_config)
283
+ generated = _generate_single_example(subschema, config)
283
284
  variants[name] = [generated]
284
285
  # Calculate the maximum number of examples any property has
285
286
  total_combos = max(len(examples) for examples in variants.values())
@@ -38,11 +38,11 @@ def unregister_string_format(name: str) -> None:
38
38
  raise ValueError(f"Unknown Open API format: {name}") from exc
39
39
 
40
40
 
41
- def header_values(blacklist_characters: str = "\n\r") -> st.SearchStrategy[str]:
41
+ def header_values(exclude_characters: str = "\n\r") -> st.SearchStrategy[str]:
42
42
  from hypothesis import strategies as st
43
43
 
44
44
  return st.text(
45
- alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters=blacklist_characters)
45
+ alphabet=st.characters(min_codepoint=0, max_codepoint=255, exclude_characters=exclude_characters)
46
46
  # Header values with leading non-visible chars can't be sent with `requests`
47
47
  ).map(str.lstrip)
48
48
 
@@ -9,12 +9,12 @@ import jsonschema
9
9
  from hypothesis import strategies as st
10
10
  from hypothesis_jsonschema import from_schema
11
11
 
12
+ from schemathesis.config import GenerationConfig
13
+
12
14
  from ..constants import ALL_KEYWORDS
13
15
  from .mutations import MutationContext
14
16
 
15
17
  if TYPE_CHECKING:
16
- from schemathesis.generation import GenerationConfig
17
-
18
18
  from .types import Draw, Schema
19
19
 
20
20
 
@@ -19,6 +19,7 @@ if hasattr(sre, "POSSESSIVE_REPEAT"):
19
19
  else:
20
20
  REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT)
21
21
  LITERAL = sre.LITERAL
22
+ NOT_LITERAL = sre.NOT_LITERAL
22
23
  IN = sre.IN
23
24
  MAXREPEAT = sre_parse.MAXREPEAT
24
25
 
@@ -114,8 +115,20 @@ def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None,
114
115
 
115
116
  pattern_parts = parsed[1:-1]
116
117
 
118
+ # Calculate total fixed length and per-repetition lengths
119
+ fixed_length = 0
120
+ quantifier_bounds = []
121
+ repetition_lengths = []
122
+
123
+ for op, value in pattern_parts:
124
+ if op in (LITERAL, NOT_LITERAL):
125
+ fixed_length += 1
126
+ elif op in REPEATS:
127
+ min_repeat, max_repeat, subpattern = value
128
+ quantifier_bounds.append((min_repeat, max_repeat))
129
+ repetition_lengths.append(_calculate_min_repetition_length(subpattern))
130
+
117
131
  # Adjust length constraints by subtracting fixed literals length
118
- fixed_length = sum(1 for op, _ in pattern_parts if op == LITERAL)
119
132
  if min_length is not None:
120
133
  min_length -= fixed_length
121
134
  if min_length < 0:
@@ -125,13 +138,10 @@ def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None,
125
138
  if max_length < 0:
126
139
  return pattern
127
140
 
128
- # Extract only min/max bounds from quantified parts
129
- quantifier_bounds = [value[:2] for op, value in pattern_parts if op in REPEATS]
130
-
131
141
  if not quantifier_bounds:
132
142
  return pattern
133
143
 
134
- length_distribution = _distribute_length_constraints(quantifier_bounds, min_length, max_length)
144
+ length_distribution = _distribute_length_constraints(quantifier_bounds, repetition_lengths, min_length, max_length)
135
145
  if not length_distribution:
136
146
  return pattern
137
147
 
@@ -212,7 +222,7 @@ def _find_quantified_end(pattern: str, start: int) -> int:
212
222
 
213
223
 
214
224
  def _distribute_length_constraints(
215
- bounds: list[tuple[int, int]], min_length: int | None, max_length: int | None
225
+ bounds: list[tuple[int, int]], repetition_lengths: list[int], min_length: int | None, max_length: int | None
216
226
  ) -> list[tuple[int, int]] | None:
217
227
  """Distribute length constraints among quantified pattern parts."""
218
228
  # Handle exact length case with dynamic programming
@@ -228,18 +238,22 @@ def _distribute_length_constraints(
228
238
  if pos == len(bounds):
229
239
  return [()] if remaining == 0 else None
230
240
 
231
- max_len: int
232
- min_len, max_len = bounds[pos]
233
- if max_len == MAXREPEAT:
234
- max_len = remaining + 1
235
- else:
236
- max_len += 1
241
+ max_repeat: int
242
+ min_repeat, max_repeat = bounds[pos]
243
+ repeat_length = repetition_lengths[pos]
244
+
245
+ if max_repeat == MAXREPEAT:
246
+ max_repeat = remaining // repeat_length + 1 if repeat_length > 0 else remaining + 1
237
247
 
238
248
  # Try each possible length for current quantifier
239
- for length in range(min_len, max_len):
240
- rest = find_valid_combination(pos + 1, remaining - length)
249
+ for repeat_count in range(min_repeat, max_repeat + 1):
250
+ used_length = repeat_count * repeat_length
251
+ if used_length > remaining:
252
+ break
253
+
254
+ rest = find_valid_combination(pos + 1, remaining - used_length)
241
255
  if rest is not None:
242
- dp[(pos, remaining)] = [(length,) + r for r in rest]
256
+ dp[(pos, remaining)] = [(repeat_count,) + r for r in rest]
243
257
  return dp[(pos, remaining)]
244
258
 
245
259
  dp[(pos, remaining)] = None
@@ -280,6 +294,22 @@ def _distribute_length_constraints(
280
294
  return result
281
295
 
282
296
 
297
+ def _calculate_min_repetition_length(subpattern: list) -> int:
298
+ """Calculate minimum length contribution per repetition of a quantified group."""
299
+ total = 0
300
+ for op, value in subpattern:
301
+ if op in [LITERAL, NOT_LITERAL, IN, sre.ANY]:
302
+ total += 1
303
+ elif op == sre.SUBPATTERN:
304
+ _, _, _, inner_pattern = value
305
+ total += _calculate_min_repetition_length(inner_pattern)
306
+ elif op in REPEATS:
307
+ min_repeat, _, inner_pattern = value
308
+ inner_min = _calculate_min_repetition_length(inner_pattern)
309
+ total += min_repeat * inner_min
310
+ return total
311
+
312
+
283
313
  def _get_anchor_length(node_type: int) -> int:
284
314
  """Determine the length of the anchor based on its type."""
285
315
  if node_type in {sre.AT_BEGINNING_STRING, sre.AT_END_STRING, sre.AT_BOUNDARY, sre.AT_NON_BOUNDARY}:
@@ -293,7 +323,7 @@ def _update_quantifier(
293
323
  """Update the quantifier based on the operation type and given constraints."""
294
324
  if op in REPEATS and value is not None:
295
325
  return _handle_repeat_quantifier(value, pattern, min_length, max_length)
296
- if op in (LITERAL, IN) and max_length != 0:
326
+ if op in (LITERAL, NOT_LITERAL, IN) and max_length != 0:
297
327
  return _handle_literal_or_in_quantifier(pattern, min_length, max_length)
298
328
  if op == sre.ANY and value is None:
299
329
  # Equivalent to `.` which is in turn is the same as `.{1}`
@@ -5,10 +5,9 @@ from functools import lru_cache
5
5
  from typing import Any, Callable, Dict, Union, overload
6
6
  from urllib.request import urlopen
7
7
 
8
- import jsonschema
9
8
  import requests
10
9
 
11
- from schemathesis.core.compat import RefResolutionError
10
+ from schemathesis.core.compat import RefResolutionError, RefResolver
12
11
  from schemathesis.core.deserialization import deserialize_yaml
13
12
  from schemathesis.core.transforms import deepclone
14
13
  from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
@@ -48,7 +47,7 @@ def load_remote_uri(uri: str) -> Any:
48
47
  JSONType = Union[None, bool, float, str, list, Dict[str, Any]]
49
48
 
50
49
 
51
- class InliningResolver(jsonschema.RefResolver):
50
+ class InliningResolver(RefResolver):
52
51
  """Inlines resolved schemas."""
53
52
 
54
53
  def __init__(self, *args: Any, **kwargs: Any) -> None: