schemathesis 3.25.5__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +793 -448
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +24 -4
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +286 -115
  23. schemathesis/cli/output/short.py +25 -6
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +323 -213
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +72 -22
  63. schemathesis/runner/events.py +86 -6
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +447 -187
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/{cli → runner}/probes.py +37 -25
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +17 -4
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +60 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +79 -61
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +143 -31
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +368 -242
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.5.dist-info/METADATA +0 -356
  144. schemathesis-3.25.5.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/failures.py CHANGED
@@ -1,10 +1,14 @@
1
1
  from __future__ import annotations
2
+
2
3
  import textwrap
3
4
  from dataclasses import dataclass
4
- from json import JSONDecodeError
5
- from typing import Any, TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from schemathesis.internal.output import OutputConfig
6
8
 
7
9
  if TYPE_CHECKING:
10
+ from json import JSONDecodeError
11
+
8
12
  from graphql.error import GraphQLFormattedError
9
13
  from jsonschema import ValidationError
10
14
 
@@ -41,16 +45,27 @@ class ValidationErrorContext(FailureContext):
41
45
  return ("/".join(map(str, self.schema_path)),)
42
46
 
43
47
  @classmethod
44
- def from_exception(cls, exc: ValidationError) -> ValidationErrorContext:
45
- from .exceptions import truncated_json
46
-
47
- schema = textwrap.indent(truncated_json(exc.schema, max_lines=20), prefix=" ")
48
- value = textwrap.indent(truncated_json(exc.instance, max_lines=20), prefix=" ")
49
- message = f"{exc.message}\n\nSchema:\n\n{schema}\n\nValue:\n\n{value}"
48
+ def from_exception(
49
+ cls, exc: ValidationError, *, output_config: OutputConfig | None = None
50
+ ) -> ValidationErrorContext:
51
+ from .internal.output import truncate_json
52
+
53
+ output_config = OutputConfig.from_parent(output_config, max_lines=20)
54
+ schema = textwrap.indent(truncate_json(exc.schema, config=output_config), prefix=" ")
55
+ value = textwrap.indent(truncate_json(exc.instance, config=output_config), prefix=" ")
56
+ schema_path = list(exc.absolute_schema_path)
57
+ if len(schema_path) > 1:
58
+ # Exclude the last segment, which is already in the schema
59
+ schema_title = "Schema at "
60
+ for segment in schema_path[:-1]:
61
+ schema_title += f"/{segment}"
62
+ else:
63
+ schema_title = "Schema"
64
+ message = f"{exc.message}\n\n{schema_title}:\n\n{schema}\n\nValue:\n\n{value}"
50
65
  return cls(
51
66
  message=message,
52
67
  validation_message=exc.message,
53
- schema_path=list(exc.absolute_schema_path),
68
+ schema_path=schema_path,
54
69
  schema=exc.schema,
55
70
  instance_path=list(exc.absolute_path),
56
71
  instance=exc.instance,
@@ -117,6 +132,59 @@ class UndefinedContentType(FailureContext):
117
132
  type: str = "undefined_content_type"
118
133
 
119
134
 
135
+ @dataclass(repr=False)
136
+ class AcceptedNegativeData(FailureContext):
137
+ """Response with negative data was accepted."""
138
+
139
+ message: str
140
+ status_code: int
141
+ allowed_statuses: list[str]
142
+ title: str = "Accepted negative data"
143
+ type: str = "accepted_negative_data"
144
+
145
+
146
+ @dataclass(repr=False)
147
+ class RejectedPositiveData(FailureContext):
148
+ """Response with positive data was rejected."""
149
+
150
+ message: str
151
+ status_code: int
152
+ allowed_statuses: list[str]
153
+ title: str = "Rejected positive data"
154
+ type: str = "rejected_positive_data"
155
+
156
+
157
+ @dataclass(repr=False)
158
+ class UseAfterFree(FailureContext):
159
+ """Resource was used after a successful DELETE operation on it."""
160
+
161
+ message: str
162
+ free: str
163
+ usage: str
164
+ title: str = "Use after free"
165
+ type: str = "use_after_free"
166
+
167
+
168
+ @dataclass(repr=False)
169
+ class EnsureResourceAvailability(FailureContext):
170
+ """Resource is not available immediately after creation."""
171
+
172
+ message: str
173
+ created_with: str
174
+ not_available_with: str
175
+ title: str = "Resource is not available after creation"
176
+ type: str = "ensure_resource_availability"
177
+
178
+
179
+ @dataclass(repr=False)
180
+ class IgnoredAuth(FailureContext):
181
+ """The API operation does not check the specified authentication."""
182
+
183
+ message: str
184
+ title: str = "Authentication declared but not enforced for this operation"
185
+ type: str = "ignored_auth"
186
+
187
+
120
188
  @dataclass(repr=False)
121
189
  class UndefinedStatusCode(FailureContext):
122
190
  """Response has a status code that is not defined in the schema."""
schemathesis/filters.py CHANGED
@@ -1,12 +1,17 @@
1
1
  """Filtering system that allows users to filter API operations based on certain criteria."""
2
+
2
3
  from __future__ import annotations
4
+
5
+ import json
3
6
  import re
4
7
  from dataclasses import dataclass, field
5
8
  from functools import partial
6
9
  from types import SimpleNamespace
7
- from typing import TYPE_CHECKING, Callable, List, Union, Protocol
10
+ from typing import TYPE_CHECKING, Any, Callable, List, Protocol, Union
8
11
 
9
12
  from .exceptions import UsageError
13
+ from .types import Filter as FilterType
14
+ from .types import NotSet
10
15
 
11
16
  if TYPE_CHECKING:
12
17
  from .models import APIOperation
@@ -49,16 +54,21 @@ class Matcher:
49
54
  func = partial(by_value_list, attribute=attribute, expected=expected)
50
55
  else:
51
56
  func = partial(by_value, attribute=attribute, expected=expected)
52
- label = f"{attribute}={repr(expected)}"
57
+ label = f"{attribute}={expected!r}"
53
58
  return cls(func, label=label, _hash=hash(label))
54
59
 
55
60
  @classmethod
56
61
  def for_regex(cls, attribute: str, regex: RegexValue) -> Matcher:
57
62
  """Matcher that checks whether the specified attribute has the provided regex."""
58
63
  if isinstance(regex, str):
59
- regex = re.compile(regex)
64
+ flags: re.RegexFlag | int
65
+ if attribute == "method":
66
+ flags = re.IGNORECASE
67
+ else:
68
+ flags = 0
69
+ regex = re.compile(regex, flags=flags)
60
70
  func = partial(by_regex, attribute=attribute, regex=regex)
61
- label = f"{attribute}_regex={repr(regex)}"
71
+ label = f"{attribute}_regex={regex!r}"
62
72
  return cls(func, label=label, _hash=hash(label))
63
73
 
64
74
  def match(self, ctx: HasAPIOperation) -> bool:
@@ -69,6 +79,8 @@ class Matcher:
69
79
  def get_operation_attribute(operation: APIOperation, attribute: str) -> str | list[str] | None:
70
80
  if attribute == "tag":
71
81
  return operation.tags
82
+ if attribute == "operation_id":
83
+ return operation.definition.raw.get("operationId")
72
84
  # Just uppercase `method`
73
85
  value = getattr(operation, attribute)
74
86
  if attribute == "method":
@@ -99,8 +111,8 @@ def by_regex(ctx: HasAPIOperation, attribute: str, regex: re.Pattern) -> bool:
99
111
  if value is None:
100
112
  return False
101
113
  if isinstance(value, list):
102
- return any(bool(regex.match(entry)) for entry in value)
103
- return bool(regex.match(value))
114
+ return any(bool(regex.search(entry)) for entry in value)
115
+ return bool(regex.search(value))
104
116
 
105
117
 
106
118
  @dataclass(repr=False, frozen=True)
@@ -109,6 +121,8 @@ class Filter:
109
121
 
110
122
  matchers: tuple[Matcher, ...]
111
123
 
124
+ __slots__ = ("matchers",)
125
+
112
126
  def __repr__(self) -> str:
113
127
  inner = " && ".join(matcher.label for matcher in self.matchers)
114
128
  return f"<{self.__class__.__name__}: [{inner}]>"
@@ -125,8 +139,34 @@ class Filter:
125
139
  class FilterSet:
126
140
  """Combines multiple filters to apply inclusion and exclusion rules on API operations."""
127
141
 
128
- _includes: set[Filter] = field(default_factory=set)
129
- _excludes: set[Filter] = field(default_factory=set)
142
+ _includes: set[Filter]
143
+ _excludes: set[Filter]
144
+
145
+ __slots__ = ("_includes", "_excludes")
146
+
147
+ def __init__(self, _includes: set[Filter] | None = None, _excludes: set[Filter] | None = None) -> None:
148
+ self._includes = _includes or set()
149
+ self._excludes = _excludes or set()
150
+
151
+ def clone(self) -> FilterSet:
152
+ return FilterSet(_includes=self._includes.copy(), _excludes=self._excludes.copy())
153
+
154
+ def merge(self, other: FilterSet) -> FilterSet:
155
+ def _merge(lhs: set[Filter], rhs: set[Filter]) -> set[Filter]:
156
+ result = lhs.copy()
157
+ for new in rhs:
158
+ for old in lhs:
159
+ for new_matcher in new.matchers:
160
+ for old_matcher in old.matchers:
161
+ if "=" in new_matcher.label and "=" in old_matcher.label:
162
+ if new_matcher.label.split("=")[0] == old_matcher.label.split("=")[0]:
163
+ result.remove(old)
164
+ result.add(new)
165
+ return result
166
+
167
+ return FilterSet(
168
+ _includes=_merge(self._includes, other._includes), _excludes=_merge(self._excludes, other._excludes)
169
+ )
130
170
 
131
171
  def apply_to(self, operations: list[APIOperation]) -> list[APIOperation]:
132
172
  """Get a filtered list of the given operations that match the filters."""
@@ -166,6 +206,8 @@ class FilterSet:
166
206
  path_regex: RegexValue | None = None,
167
207
  tag: FilterValue | None = None,
168
208
  tag_regex: RegexValue | None = None,
209
+ operation_id: FilterValue | None = None,
210
+ operation_id_regex: RegexValue | None = None,
169
211
  ) -> None:
170
212
  """Add a new INCLUDE filter."""
171
213
  self._add_filter(
@@ -179,6 +221,8 @@ class FilterSet:
179
221
  path_regex=path_regex,
180
222
  tag=tag,
181
223
  tag_regex=tag_regex,
224
+ operation_id=operation_id,
225
+ operation_id_regex=operation_id_regex,
182
226
  )
183
227
 
184
228
  def exclude(
@@ -193,6 +237,8 @@ class FilterSet:
193
237
  path_regex: RegexValue | None = None,
194
238
  tag: FilterValue | None = None,
195
239
  tag_regex: RegexValue | None = None,
240
+ operation_id: FilterValue | None = None,
241
+ operation_id_regex: RegexValue | None = None,
196
242
  ) -> None:
197
243
  """Add a new EXCLUDE filter."""
198
244
  self._add_filter(
@@ -206,6 +252,8 @@ class FilterSet:
206
252
  path_regex=path_regex,
207
253
  tag=tag,
208
254
  tag_regex=tag_regex,
255
+ operation_id=operation_id,
256
+ operation_id_regex=operation_id_regex,
209
257
  )
210
258
 
211
259
  def _add_filter(
@@ -221,6 +269,8 @@ class FilterSet:
221
269
  path_regex: RegexValue | None = None,
222
270
  tag: FilterValue | None = None,
223
271
  tag_regex: RegexValue | None = None,
272
+ operation_id: FilterValue | None = None,
273
+ operation_id_regex: RegexValue | None = None,
224
274
  ) -> None:
225
275
  matchers = []
226
276
  if func is not None:
@@ -230,6 +280,7 @@ class FilterSet:
230
280
  ("method", method, method_regex),
231
281
  ("path", path, path_regex),
232
282
  ("tag", tag, tag_regex),
283
+ ("operation_id", operation_id, operation_id_regex),
233
284
  ):
234
285
  if expected is not None and regex is not None:
235
286
  # To match anything the regex should match the expected value, hence passing them together is useless
@@ -274,8 +325,12 @@ def attach_filter_chain(
274
325
  name_regex: str | None = None,
275
326
  method: FilterValue | None = None,
276
327
  method_regex: str | None = None,
328
+ tag: FilterValue | None = None,
329
+ tag_regex: RegexValue | None = None,
277
330
  path: FilterValue | None = None,
278
331
  path_regex: str | None = None,
332
+ operation_id: FilterValue | None = None,
333
+ operation_id_regex: RegexValue | None = None,
279
334
  ) -> Callable:
280
335
  __tracebackhide__ = True
281
336
  filter_func(
@@ -284,8 +339,12 @@ def attach_filter_chain(
284
339
  name_regex=name_regex,
285
340
  method=method,
286
341
  method_regex=method_regex,
342
+ tag=tag,
343
+ tag_regex=tag_regex,
287
344
  path=path,
288
345
  path_regex=path_regex,
346
+ operation_id=operation_id,
347
+ operation_id_regex=operation_id_regex,
289
348
  )
290
349
  return target
291
350
 
@@ -293,3 +352,121 @@ def attach_filter_chain(
293
352
  proxy.__name__ = attribute
294
353
 
295
354
  setattr(target, attribute, proxy)
355
+
356
+
357
+ def is_deprecated(ctx: HasAPIOperation) -> bool:
358
+ return ctx.operation.definition.raw.get("deprecated") is True
359
+
360
+
361
+ def filter_set_from_components(
362
+ *,
363
+ include: bool,
364
+ method: FilterType | None = None,
365
+ endpoint: FilterType | None = None,
366
+ tag: FilterType | None = None,
367
+ operation_id: FilterType | None = None,
368
+ skip_deprecated_operations: bool | None | NotSet = None,
369
+ parent: FilterSet | None = None,
370
+ ) -> FilterSet:
371
+ def _is_defined(x: FilterType | None) -> bool:
372
+ return x is not None and not isinstance(x, NotSet)
373
+
374
+ def _prepare_filter(filter_: FilterType | None) -> RegexValue | None:
375
+ if filter_ is None or isinstance(filter_, NotSet):
376
+ return None
377
+ if isinstance(filter_, str):
378
+ return filter_
379
+ return "|".join(f"({f})" for f in filter_)
380
+
381
+ new = FilterSet()
382
+
383
+ if _is_defined(method) or _is_defined(endpoint) or _is_defined(tag) or _is_defined(operation_id):
384
+ new._add_filter(
385
+ include,
386
+ method_regex=_prepare_filter(method),
387
+ path_regex=_prepare_filter(endpoint),
388
+ tag_regex=_prepare_filter(tag),
389
+ operation_id_regex=_prepare_filter(operation_id),
390
+ )
391
+ if skip_deprecated_operations is True and not any(
392
+ matcher.label == is_deprecated.__name__ for exclude_ in new._excludes for matcher in exclude_.matchers
393
+ ):
394
+ new.exclude(func=is_deprecated)
395
+ # Merge with the parent filter set
396
+ if parent is not None:
397
+ for include_ in parent._includes:
398
+ matchers = include_.matchers
399
+ ids = []
400
+ for idx, matcher in enumerate(matchers):
401
+ label = matcher.label
402
+ if (
403
+ (not isinstance(method, NotSet) and label.startswith("method_regex="))
404
+ or (not isinstance(endpoint, NotSet) and label.startswith("path_regex="))
405
+ or (not isinstance(tag, NotSet) and matcher.label.startswith("tag_regex="))
406
+ or (not isinstance(operation_id, NotSet) and matcher.label.startswith("operation_id_regex="))
407
+ ):
408
+ ids.append(idx)
409
+ if ids:
410
+ matchers = tuple(matcher for idx, matcher in enumerate(matchers) if idx not in ids)
411
+ if matchers:
412
+ if new._includes:
413
+ existing = new._includes.pop()
414
+ matchers = existing.matchers + matchers
415
+ new._includes.add(Filter(matchers=matchers))
416
+ for exclude_ in parent._excludes:
417
+ matchers = exclude_.matchers
418
+ ids = []
419
+ for idx, matcher in enumerate(exclude_.matchers):
420
+ if skip_deprecated_operations is False and matcher.label == is_deprecated.__name__:
421
+ ids.append(idx)
422
+ if ids:
423
+ matchers = tuple(matcher for idx, matcher in enumerate(matchers) if idx not in ids)
424
+ if matchers:
425
+ new._excludes.add(exclude_)
426
+ return new
427
+
428
+
429
+ def parse_expression(expression: str) -> tuple[str, str, Any]:
430
+ expression = expression.strip()
431
+
432
+ # Find the operator
433
+ for op in ("==", "!="):
434
+ try:
435
+ pointer, value = expression.split(op, 1)
436
+ break
437
+ except ValueError:
438
+ continue
439
+ else:
440
+ raise ValueError(f"Invalid expression: {expression}")
441
+
442
+ pointer = pointer.strip()
443
+ value = value.strip()
444
+ if not pointer or not value:
445
+ raise ValueError(f"Invalid expression: {expression}")
446
+ # Parse the JSON value
447
+ try:
448
+ return pointer, op, json.loads(value)
449
+ except json.JSONDecodeError:
450
+ # If it's not valid JSON, treat it as a string
451
+ return pointer, op, value
452
+
453
+
454
+ def expression_to_filter_function(expression: str) -> Callable[[HasAPIOperation], bool]:
455
+ from .specs.openapi.references import resolve_pointer
456
+
457
+ pointer, op, value = parse_expression(expression)
458
+
459
+ if op == "==":
460
+
461
+ def filter_function(ctx: HasAPIOperation) -> bool:
462
+ definition = ctx.operation.definition.resolved
463
+ resolved = resolve_pointer(definition, pointer)
464
+ return resolved == value
465
+ else:
466
+
467
+ def filter_function(ctx: HasAPIOperation) -> bool:
468
+ definition = ctx.operation.definition.resolved
469
+ resolved = resolve_pointer(definition, pointer)
470
+ return resolved != value
471
+
472
+ return filter_function
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Iterable
3
4
 
4
5
  from . import fast_api, utf8_bom
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Any
3
4
 
4
- from ..hooks import HookContext
5
+ from ..hooks import HookContext, register, unregister
5
6
  from ..hooks import is_installed as global_is_installed
6
- from ..hooks import register, unregister
7
7
  from ..internal.jsonschema import traverse_schema
8
8
 
9
9
 
@@ -1,9 +1,8 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
3
  from ..constants import BOM_MARK
4
- from ..hooks import HookContext
4
+ from ..hooks import HookContext, register, unregister
5
5
  from ..hooks import is_installed as global_is_installed
6
- from ..hooks import register, unregister
7
6
 
8
7
  if TYPE_CHECKING:
9
8
  from ..models import Case
@@ -1,44 +1,15 @@
1
1
  from __future__ import annotations
2
- import random
3
- from dataclasses import dataclass
4
- from enum import Enum
5
- from typing import Union, Iterable
6
-
7
-
8
- class DataGenerationMethod(str, Enum):
9
- """Defines what data Schemathesis generates for tests."""
10
-
11
- # Generate data, that fits the API schema
12
- positive = "positive"
13
- # Doesn't fit the API schema
14
- negative = "negative"
15
-
16
- @classmethod
17
- def default(cls) -> DataGenerationMethod:
18
- return cls.positive
19
-
20
- @classmethod
21
- def all(cls) -> list[DataGenerationMethod]:
22
- return list(DataGenerationMethod)
23
2
 
24
- def as_short_name(self) -> str:
25
- return {
26
- DataGenerationMethod.positive: "P",
27
- DataGenerationMethod.negative: "N",
28
- }[self]
29
-
30
- @property
31
- def is_negative(self) -> bool:
32
- return self == DataGenerationMethod.negative
3
+ import random
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING
33
6
 
34
- @classmethod
35
- def ensure_list(cls, value: DataGenerationMethodInput) -> list[DataGenerationMethod]:
36
- if isinstance(value, DataGenerationMethod):
37
- return [value]
38
- return list(value)
7
+ from ._hypothesis import add_single_example, combine_strategies, get_single_example
8
+ from ._methods import DataGenerationMethod, DataGenerationMethodInput
39
9
 
10
+ if TYPE_CHECKING:
11
+ from hypothesis.strategies import SearchStrategy
40
12
 
41
- DataGenerationMethodInput = Union[DataGenerationMethod, Iterable[DataGenerationMethod]]
42
13
 
43
14
  DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
44
15
 
@@ -58,11 +29,24 @@ def generate_random_case_id(length: int = 6) -> str:
58
29
  return output
59
30
 
60
31
 
32
+ @dataclass
33
+ class HeaderConfig:
34
+ """Configuration for generating headers."""
35
+
36
+ strategy: SearchStrategy[str] | None = None
37
+
38
+
61
39
  @dataclass
62
40
  class GenerationConfig:
63
41
  """Holds various configuration options relevant for data generation."""
64
42
 
65
43
  # Allow generating `\x00` bytes in strings
66
44
  allow_x00: bool = True
45
+ # Allowing using `null` for optional arguments in GraphQL queries
46
+ graphql_allow_null: bool = True
67
47
  # Generate strings using the given codec
68
48
  codec: str | None = "utf-8"
49
+ # Whether to generate security parameters
50
+ with_security_parameters: bool = True
51
+ # Header generation configuration
52
+ headers: HeaderConfig = field(default_factory=HeaderConfig)
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from functools import lru_cache, reduce
5
+ from operator import or_
6
+ from typing import TYPE_CHECKING, TypeVar
7
+
8
+ if TYPE_CHECKING:
9
+ from hypothesis import settings
10
+ from hypothesis import strategies as st
11
+
12
+ SCHEMATHESIS_BENCHMARK_SEED = os.environ.get("SCHEMATHESIS_BENCHMARK_SEED")
13
+
14
+
15
+ @lru_cache
16
+ def default_settings() -> settings:
17
+ from hypothesis import HealthCheck, Phase, Verbosity, settings
18
+
19
+ return settings(
20
+ database=None,
21
+ max_examples=1,
22
+ deadline=None,
23
+ verbosity=Verbosity.quiet,
24
+ phases=(Phase.generate,),
25
+ suppress_health_check=list(HealthCheck),
26
+ )
27
+
28
+
29
+ T = TypeVar("T")
30
+
31
+
32
+ def get_single_example(strategy: st.SearchStrategy[T]) -> T: # type: ignore[type-var]
33
+ examples: list[T] = []
34
+ add_single_example(strategy, examples)
35
+ return examples[0]
36
+
37
+
38
+ def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> None:
39
+ from hypothesis import given, seed
40
+
41
+ @given(strategy) # type: ignore
42
+ @default_settings() # type: ignore
43
+ def example_generating_inner_function(ex: T) -> None:
44
+ examples.append(ex)
45
+
46
+ example_generating_inner_function._hypothesis_internal_database_key = b"" # type: ignore
47
+
48
+ if SCHEMATHESIS_BENCHMARK_SEED is not None:
49
+ example_generating_inner_function = seed(SCHEMATHESIS_BENCHMARK_SEED)(example_generating_inner_function)
50
+
51
+ example_generating_inner_function()
52
+
53
+
54
+ def combine_strategies(strategies: list[st.SearchStrategy] | tuple[st.SearchStrategy]) -> st.SearchStrategy:
55
+ """Combine a list of strategies into a single one.
56
+
57
+ If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
58
+ """
59
+ return reduce(or_, strategies[1:], strategies[0])
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Iterable, Union
5
+
6
+
7
+ class DataGenerationMethod(str, Enum):
8
+ """Defines what data Schemathesis generates for tests."""
9
+
10
+ # Generate data, that fits the API schema
11
+ positive = "positive"
12
+ # Doesn't fit the API schema
13
+ negative = "negative"
14
+
15
+ @classmethod
16
+ def default(cls) -> DataGenerationMethod:
17
+ return cls.positive
18
+
19
+ @classmethod
20
+ def all(cls) -> list[DataGenerationMethod]:
21
+ return list(DataGenerationMethod)
22
+
23
+ def as_short_name(self) -> str:
24
+ return {
25
+ DataGenerationMethod.positive: "P",
26
+ DataGenerationMethod.negative: "N",
27
+ }[self]
28
+
29
+ @property
30
+ def is_positive(self) -> bool:
31
+ return self == DataGenerationMethod.positive
32
+
33
+ @property
34
+ def is_negative(self) -> bool:
35
+ return self == DataGenerationMethod.negative
36
+
37
+ @classmethod
38
+ def ensure_list(cls, value: DataGenerationMethodInput) -> list[DataGenerationMethod]:
39
+ if isinstance(value, DataGenerationMethod):
40
+ return [value]
41
+ return list(value)
42
+
43
+
44
+ DataGenerationMethodInput = Union[DataGenerationMethod, Iterable[DataGenerationMethod]]