schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,246 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import time
5
+ import uuid
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Iterator, cast
8
+
9
+ from schemathesis.core.failures import Failure
10
+ from schemathesis.core.transport import Response
11
+ from schemathesis.engine import Status
12
+ from schemathesis.generation.case import Case
13
+
14
+ if TYPE_CHECKING:
15
+ import requests
16
+
17
+ from schemathesis.generation.stateful.state_machine import Transition
18
+
19
+
20
+ @dataclass
21
+ class ScenarioRecorder:
22
+ """Tracks and organizes all data related to a logical block of testing.
23
+
24
+ Records test cases, their hierarchy, API interactions, and results of checks performed during execution.
25
+ """
26
+
27
+ id: uuid.UUID
28
+ # Human-readable label
29
+ label: str
30
+
31
+ # Recorded test cases
32
+ cases: dict[str, CaseNode]
33
+ # Results of checks categorized by test case ID
34
+ checks: dict[str, list[CheckNode]]
35
+ # Network interactions by test case ID
36
+ interactions: dict[str, Interaction]
37
+
38
+ __slots__ = ("id", "label", "status", "roots", "cases", "checks", "interactions")
39
+
40
+ def __init__(self, *, label: str) -> None:
41
+ self.id = uuid.uuid4()
42
+ self.label = label
43
+ self.cases = {}
44
+ self.checks = {}
45
+ self.interactions = {}
46
+
47
+ def record_case(self, *, parent_id: str | None, transition: Transition | None, case: Case) -> None:
48
+ """Record a test case and its relationship to a parent, if applicable."""
49
+ self.cases[case.id] = CaseNode(value=case, parent_id=parent_id, transition=transition)
50
+
51
+ def record_response(self, *, case_id: str, response: Response) -> None:
52
+ """Record the API response for a given test case."""
53
+ request = Request.from_prepared_request(response.request)
54
+ self.interactions[case_id] = Interaction(request=request, response=response)
55
+
56
+ def record_request(self, *, case_id: str, request: requests.PreparedRequest) -> None:
57
+ """Record a network-level error for a given test case."""
58
+ self.interactions[case_id] = Interaction(request=Request.from_prepared_request(request), response=None)
59
+
60
+ def record_check_failure(self, *, name: str, case_id: str, code_sample: str, failure: Failure) -> None:
61
+ """Record a failure of a check for a given test case."""
62
+ self.checks.setdefault(case_id, []).append(
63
+ CheckNode(
64
+ name=name,
65
+ status=Status.FAILURE,
66
+ failure_info=CheckFailureInfo(code_sample=code_sample, failure=failure),
67
+ )
68
+ )
69
+
70
+ def record_check_success(self, *, name: str, case_id: str) -> None:
71
+ """Record a successful pass of a check for a given test case."""
72
+ self.checks.setdefault(case_id, []).append(CheckNode(name=name, status=Status.SUCCESS, failure_info=None))
73
+
74
+ def find_failure_data(self, *, parent_id: str, failure: Failure) -> FailureData:
75
+ """Retrieve the relevant test case & interaction data for a failure.
76
+
77
+ It may happen that a failure comes from a different test case if a check generated some additional
78
+ test cases & interactions.
79
+ """
80
+ case_id = failure.case_id or parent_id
81
+ case = self.cases[case_id].value
82
+ request = self.interactions[case_id].request
83
+ response = self.interactions[case_id].response
84
+ assert isinstance(response, Response)
85
+ headers = {key: value[0] for key, value in request.headers.items()}
86
+ return FailureData(case=case, headers=headers, verify=response.verify)
87
+
88
+ def find_parent(self, *, case_id: str) -> Case | None:
89
+ """Find the parent case of a given test case, if it exists."""
90
+ case = self.cases.get(case_id)
91
+ if case is not None and case.parent_id is not None:
92
+ parent = self.cases.get(case.parent_id)
93
+ # The recorder state should always be consistent
94
+ assert parent is not None, "Parent does not exist"
95
+ return parent.value
96
+ return None
97
+
98
+ def find_related(self, *, case_id: str) -> Iterator[Case]:
99
+ """Iterate over all ancestors and their children for a given case."""
100
+ current_id = case_id
101
+ seen = {current_id}
102
+
103
+ while True:
104
+ current_node = self.cases.get(current_id)
105
+ if current_node is None or current_node.parent_id is None:
106
+ break
107
+
108
+ # Get all children of the parent (siblings of the current case)
109
+ parent_id = current_node.parent_id
110
+ for case_id, maybe_child in self.cases.items():
111
+ # If this case has the same parent and we haven't seen it yet
112
+ if parent_id == maybe_child.parent_id and case_id not in seen:
113
+ seen.add(case_id)
114
+ yield maybe_child.value
115
+
116
+ # Move up to the parent
117
+ current_id = parent_id
118
+ if current_id not in seen:
119
+ seen.add(current_id)
120
+ parent_node = self.cases.get(current_id)
121
+ if parent_node:
122
+ yield parent_node.value
123
+
124
+ def find_response(self, *, case_id: str) -> Response | None:
125
+ """Retrieve the API response for a given test case, if available."""
126
+ interaction = self.interactions.get(case_id)
127
+ if interaction is None or interaction.response is None:
128
+ return None
129
+ return interaction.response
130
+
131
+
132
+ @dataclass
133
+ class CaseNode:
134
+ """Represents a test case and its parent-child relationship."""
135
+
136
+ value: Case
137
+ parent_id: str | None
138
+ # Transition may be absent if `parent_id` is present for cases when a case is derived inside a check
139
+ # and outside of the implemented transition logic (e.g. Open API links)
140
+ transition: Transition | None
141
+
142
+ __slots__ = ("value", "parent_id", "transition")
143
+
144
+
145
+ @dataclass
146
+ class CheckNode:
147
+ name: str
148
+ status: Status
149
+ failure_info: CheckFailureInfo | None
150
+
151
+ __slots__ = ("name", "status", "failure_info")
152
+
153
+
154
+ @dataclass
155
+ class CheckFailureInfo:
156
+ code_sample: str
157
+ failure: Failure
158
+
159
+ __slots__ = ("code_sample", "failure")
160
+
161
+
162
+ def serialize_payload(payload: bytes) -> str:
163
+ return base64.b64encode(payload).decode()
164
+
165
+
166
+ @dataclass(repr=False)
167
+ class Request:
168
+ """Request data extracted from `Case`."""
169
+
170
+ method: str
171
+ uri: str
172
+ body: bytes | None
173
+ body_size: int | None
174
+ headers: dict[str, list[str]]
175
+
176
+ __slots__ = ("method", "uri", "body", "body_size", "headers", "_encoded_body_cache")
177
+
178
+ def __init__(
179
+ self,
180
+ method: str,
181
+ uri: str,
182
+ body: bytes | None,
183
+ body_size: int | None,
184
+ headers: dict[str, list[str]],
185
+ ):
186
+ self.method = method
187
+ self.uri = uri
188
+ self.body = body
189
+ self.body_size = body_size
190
+ self.headers = headers
191
+ self._encoded_body_cache: str | None = None
192
+
193
+ @classmethod
194
+ def from_prepared_request(cls, prepared: requests.PreparedRequest) -> Request:
195
+ """A prepared request version is already stored in `requests.Response`."""
196
+ body = prepared.body
197
+
198
+ if isinstance(body, str):
199
+ # can be a string for `application/x-www-form-urlencoded`
200
+ body = body.encode("utf-8")
201
+
202
+ # these values have `str` type at this point
203
+ uri = cast(str, prepared.url)
204
+ method = cast(str, prepared.method)
205
+ return cls(
206
+ uri=uri,
207
+ method=method,
208
+ headers={key: [value] for (key, value) in prepared.headers.items()},
209
+ body=body,
210
+ body_size=len(body) if body is not None else None,
211
+ )
212
+
213
+ @property
214
+ def encoded_body(self) -> str | None:
215
+ if self.body is not None:
216
+ if self._encoded_body_cache is None:
217
+ self._encoded_body_cache = serialize_payload(self.body)
218
+ return self._encoded_body_cache
219
+ return None
220
+
221
+
222
+ @dataclass
223
+ class Interaction:
224
+ """Represents a single interaction with the tested application."""
225
+
226
+ request: Request
227
+ response: Response | None
228
+ timestamp: float
229
+
230
+ __slots__ = ("request", "response", "timestamp")
231
+
232
+ def __init__(self, request: Request, response: Response | None) -> None:
233
+ self.request = request
234
+ self.response = response
235
+ self.timestamp = time.time()
236
+
237
+
238
+ @dataclass
239
+ class FailureData:
240
+ """Details about a test failure, including the case and its context."""
241
+
242
+ case: Case
243
+ headers: dict[str, str]
244
+ verify: bool
245
+
246
+ __slots__ = ("case", "headers", "verify")
schemathesis/errors.py ADDED
@@ -0,0 +1,31 @@
1
+ """Public Schemathesis errors."""
2
+
3
+ from schemathesis.core.errors import IncorrectUsage as IncorrectUsage
4
+ from schemathesis.core.errors import InternalError as InternalError
5
+ from schemathesis.core.errors import InvalidHeadersExample as InvalidHeadersExample
6
+ from schemathesis.core.errors import InvalidRateLimit as InvalidRateLimit
7
+ from schemathesis.core.errors import InvalidRegexPattern as InvalidRegexPattern
8
+ from schemathesis.core.errors import InvalidRegexType as InvalidRegexType
9
+ from schemathesis.core.errors import InvalidSchema as InvalidSchema
10
+ from schemathesis.core.errors import LoaderError as LoaderError
11
+ from schemathesis.core.errors import OperationNotFound as OperationNotFound
12
+ from schemathesis.core.errors import SchemathesisError as SchemathesisError
13
+ from schemathesis.core.errors import SerializationError as SerializationError
14
+ from schemathesis.core.errors import SerializationNotPossible as SerializationNotPossible
15
+ from schemathesis.core.errors import UnboundPrefix as UnboundPrefix
16
+
17
+ __all__ = [
18
+ "IncorrectUsage",
19
+ "InternalError",
20
+ "InvalidHeadersExample",
21
+ "InvalidRateLimit",
22
+ "InvalidRegexPattern",
23
+ "InvalidRegexType",
24
+ "InvalidSchema",
25
+ "LoaderError",
26
+ "OperationNotFound",
27
+ "SchemathesisError",
28
+ "SerializationError",
29
+ "SerializationNotPossible",
30
+ "UnboundPrefix",
31
+ ]
@@ -1,18 +1,21 @@
1
1
  import os
2
2
  from dataclasses import dataclass, field
3
3
 
4
- from ..constants import TRUE_VALUES
4
+ from schemathesis.core import string_to_boolean
5
5
 
6
6
 
7
7
  @dataclass(eq=False)
8
8
  class Experiment:
9
9
  name: str
10
- verbose_name: str
11
10
  env_var: str
12
11
  description: str
13
12
  discussion_url: str
14
13
  _storage: "ExperimentSet" = field(repr=False)
15
14
 
15
+ @property
16
+ def label(self) -> str:
17
+ return self.name.lower().replace(" ", "-")
18
+
16
19
  def enable(self) -> None:
17
20
  self._storage.enable(self)
18
21
 
@@ -25,7 +28,7 @@ class Experiment:
25
28
 
26
29
  @property
27
30
  def is_env_var_set(self) -> bool:
28
- return os.getenv(self.env_var, "").lower() in TRUE_VALUES
31
+ return string_to_boolean(os.getenv(self.env_var, "")) is True
29
32
 
30
33
 
31
34
  @dataclass
@@ -33,12 +36,9 @@ class ExperimentSet:
33
36
  available: set = field(default_factory=set)
34
37
  enabled: set = field(default_factory=set)
35
38
 
36
- def create_experiment(
37
- self, name: str, verbose_name: str, env_var: str, description: str, discussion_url: str
38
- ) -> Experiment:
39
+ def create_experiment(self, name: str, env_var: str, description: str, discussion_url: str) -> Experiment:
39
40
  instance = Experiment(
40
41
  name=name,
41
- verbose_name=verbose_name,
42
42
  env_var=f"{ENV_PREFIX}_{env_var}",
43
43
  description=description,
44
44
  discussion_url=discussion_url,
@@ -64,45 +64,14 @@ class ExperimentSet:
64
64
 
65
65
  ENV_PREFIX = "SCHEMATHESIS_EXPERIMENTAL"
66
66
  GLOBAL_EXPERIMENTS = ExperimentSet()
67
-
68
- OPEN_API_3_1 = GLOBAL_EXPERIMENTS.create_experiment(
69
- name="openapi-3.1",
70
- verbose_name="OpenAPI 3.1",
71
- env_var="OPENAPI_3_1",
72
- description="Support for response validation",
73
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/1822",
74
- )
75
- SCHEMA_ANALYSIS = GLOBAL_EXPERIMENTS.create_experiment(
76
- name="schema-analysis",
77
- verbose_name="Schema Analysis",
78
- env_var="SCHEMA_ANALYSIS",
79
- description="Analyzing API schemas via Schemathesis.io",
80
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/2056",
81
- )
82
- STATEFUL_TEST_RUNNER = GLOBAL_EXPERIMENTS.create_experiment(
83
- name="stateful-test-runner",
84
- verbose_name="New Stateful Test Runner",
85
- env_var="STATEFUL_TEST_RUNNER",
86
- description="State machine-based runner for stateful tests in CLI",
87
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
88
- )
89
- STATEFUL_ONLY = GLOBAL_EXPERIMENTS.create_experiment(
90
- name="stateful-only",
91
- verbose_name="Stateful Only",
92
- env_var="STATEFUL_ONLY",
93
- description="Run only stateful tests",
94
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
95
- )
96
67
  COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
97
- name="coverage-phase",
98
- verbose_name="Coverage phase",
68
+ name="Coverage phase",
99
69
  env_var="COVERAGE_PHASE",
100
70
  description="Generate covering test cases",
101
71
  discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
102
72
  )
103
73
  POSITIVE_DATA_ACCEPTANCE = GLOBAL_EXPERIMENTS.create_experiment(
104
- name="positive_data_acceptance",
105
- verbose_name="Positive Data Acceptance",
74
+ name="Positive Data Acceptance",
106
75
  env_var="POSITIVE_DATA_ACCEPTANCE",
107
76
  description="Verifying schema-conformant data is accepted",
108
77
  discussion_url="https://github.com/schemathesis/schemathesis/discussions/2499",
schemathesis/filters.py CHANGED
@@ -9,12 +9,11 @@ from functools import partial
9
9
  from types import SimpleNamespace
10
10
  from typing import TYPE_CHECKING, Any, Callable, List, Protocol, Union
11
11
 
12
- from .exceptions import UsageError
13
- from .types import Filter as FilterType
14
- from .types import NotSet
12
+ from schemathesis.core.errors import IncorrectUsage
13
+ from schemathesis.core.transforms import resolve_pointer
15
14
 
16
15
  if TYPE_CHECKING:
17
- from .models import APIOperation
16
+ from schemathesis.schemas import APIOperation
18
17
 
19
18
 
20
19
  class HasAPIOperation(Protocol):
@@ -151,23 +150,6 @@ class FilterSet:
151
150
  def clone(self) -> FilterSet:
152
151
  return FilterSet(_includes=self._includes.copy(), _excludes=self._excludes.copy())
153
152
 
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
- )
170
-
171
153
  def apply_to(self, operations: list[APIOperation]) -> list[APIOperation]:
172
154
  """Get a filtered list of the given operations that match the filters."""
173
155
  return [operation for operation in operations if self.match(SimpleNamespace(operation=operation))]
@@ -276,7 +258,7 @@ class FilterSet:
276
258
  if func is not None:
277
259
  matchers.append(Matcher.for_function(func))
278
260
  for attribute, expected, regex in (
279
- ("verbose_name", name, name_regex),
261
+ ("label", name, name_regex),
280
262
  ("method", method, method_regex),
281
263
  ("path", path, path_regex),
282
264
  ("tag", tag, tag_regex),
@@ -284,17 +266,17 @@ class FilterSet:
284
266
  ):
285
267
  if expected is not None and regex is not None:
286
268
  # To match anything the regex should match the expected value, hence passing them together is useless
287
- raise UsageError(ERROR_EXPECTED_AND_REGEX)
269
+ raise IncorrectUsage(ERROR_EXPECTED_AND_REGEX)
288
270
  if expected is not None:
289
271
  matchers.append(Matcher.for_value(attribute, expected))
290
272
  if regex is not None:
291
273
  matchers.append(Matcher.for_regex(attribute, regex))
292
274
 
293
275
  if not matchers:
294
- raise UsageError(ERROR_EMPTY_FILTER)
276
+ raise IncorrectUsage(ERROR_EMPTY_FILTER)
295
277
  filter_ = Filter(matchers=tuple(matchers))
296
278
  if filter_ in self._includes or filter_ in self._excludes:
297
- raise UsageError(ERROR_FILTER_EXISTS)
279
+ raise IncorrectUsage(ERROR_FILTER_EXISTS)
298
280
  if include:
299
281
  self._includes.add(filter_)
300
282
  else:
@@ -358,74 +340,6 @@ def is_deprecated(ctx: HasAPIOperation) -> bool:
358
340
  return ctx.operation.definition.raw.get("deprecated") is True
359
341
 
360
342
 
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
343
  def parse_expression(expression: str) -> tuple[str, str, Any]:
430
344
  expression = expression.strip()
431
345
 
@@ -452,8 +366,6 @@ def parse_expression(expression: str) -> tuple[str, str, Any]:
452
366
 
453
367
 
454
368
  def expression_to_filter_function(expression: str) -> Callable[[HasAPIOperation], bool]:
455
- from .specs.openapi.references import resolve_pointer
456
-
457
369
  pointer, op, value = parse_expression(expression)
458
370
 
459
371
  if op == "==":
@@ -4,14 +4,13 @@ import random
4
4
  from dataclasses import dataclass, field
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from ._hypothesis import add_single_example, combine_strategies, get_single_example
8
- from ._methods import DataGenerationMethod, DataGenerationMethodInput
7
+ from schemathesis.generation.modes import GenerationMode as GenerationMode
9
8
 
10
9
  if TYPE_CHECKING:
11
10
  from hypothesis.strategies import SearchStrategy
12
11
 
13
12
 
14
- DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
13
+ DEFAULT_GENERATOR_MODES = (GenerationMode.default(),)
15
14
 
16
15
 
17
16
  CASE_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
@@ -40,6 +39,7 @@ class HeaderConfig:
40
39
  class GenerationConfig:
41
40
  """Holds various configuration options relevant for data generation."""
42
41
 
42
+ modes: list[GenerationMode] = field(default_factory=lambda: [GenerationMode.default()])
43
43
  # Allow generating `\x00` bytes in strings
44
44
  allow_x00: bool = True
45
45
  # Allowing using `null` for optional arguments in GraphQL queries