schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
schemathesis/failures.py DELETED
@@ -1,145 +0,0 @@
1
- from typing import Any, Dict, List, Optional, Tuple, Union
2
-
3
- import attr
4
-
5
-
6
- @attr.s(slots=True, repr=False) # pragma: no mutate
7
- class FailureContext:
8
- """Additional data specific to certain failure kind."""
9
-
10
- # Short description of what happened
11
- title: str
12
- # A longer one
13
- message: str
14
- type: str
15
-
16
- def unique_by_key(self, check_message: Optional[str]) -> Tuple[str, ...]:
17
- """A key to distinguish different failure contexts."""
18
- return (check_message or self.message,)
19
-
20
-
21
- @attr.s(slots=True, repr=False)
22
- class ValidationErrorContext(FailureContext):
23
- """Additional information about JSON Schema validation errors."""
24
-
25
- validation_message: str = attr.ib()
26
- schema_path: List[Union[str, int]] = attr.ib()
27
- schema: Union[Dict[str, Any], bool] = attr.ib()
28
- instance_path: List[Union[str, int]] = attr.ib()
29
- instance: Union[None, bool, float, str, list, Dict[str, Any]] = attr.ib()
30
- title: str = attr.ib(default="Non-conforming response payload")
31
- message: str = attr.ib(default="Response does not conform to the defined schema")
32
- type: str = attr.ib(default="json_schema")
33
-
34
- def unique_by_key(self, check_message: Optional[str]) -> Tuple[str, ...]:
35
- # Deduplicate by JSON Schema path. All errors that happened on this sub-schema will be deduplicated
36
- return ("/".join(map(str, self.schema_path)),)
37
-
38
-
39
- @attr.s(slots=True, repr=False)
40
- class JSONDecodeErrorContext(FailureContext):
41
- """Failed to decode JSON."""
42
-
43
- validation_message: str = attr.ib()
44
- document: str = attr.ib()
45
- position: int = attr.ib()
46
- lineno: int = attr.ib()
47
- colno: int = attr.ib()
48
- title: str = attr.ib(default="JSON deserialization error")
49
- message: str = attr.ib(default="Response is not a valid JSON")
50
- type: str = attr.ib(default="json_decode")
51
-
52
- def unique_by_key(self, check_message: Optional[str]) -> Tuple[str, ...]:
53
- # Treat different JSON decoding failures as the same issue
54
- # Payloads often contain dynamic data and distinguishing it by the error location still would not be sufficient
55
- # as it may be different on different dynamic payloads
56
- return (self.title,)
57
-
58
-
59
- @attr.s(slots=True, repr=False)
60
- class ServerError(FailureContext):
61
- status_code: int = attr.ib()
62
- title: str = attr.ib(default="Internal server error")
63
- message: str = attr.ib(default="Server got itself in trouble")
64
- type: str = attr.ib(default="server_error")
65
-
66
-
67
- @attr.s(slots=True, repr=False)
68
- class MissingContentType(FailureContext):
69
- """Content type header is missing."""
70
-
71
- media_types: List[str] = attr.ib()
72
- title: str = attr.ib(default="Missing Content-Type header")
73
- message: str = attr.ib(default="Response is missing the `Content-Type` header")
74
- type: str = attr.ib(default="missing_content_type")
75
-
76
-
77
- @attr.s(slots=True, repr=False)
78
- class UndefinedContentType(FailureContext):
79
- """Response has Content-Type that is not defined in the schema."""
80
-
81
- content_type: str = attr.ib()
82
- defined_content_types: List[str] = attr.ib()
83
- title: str = attr.ib(default="Undefined Content-Type")
84
- message: str = attr.ib(default="Response has `Content-Type` that is not declared in the schema")
85
- type: str = attr.ib(default="undefined_content_type")
86
-
87
-
88
- @attr.s(slots=True, repr=False)
89
- class UndefinedStatusCode(FailureContext):
90
- """Response has a status code that is not defined in the schema."""
91
-
92
- # Response's status code
93
- status_code: int = attr.ib()
94
- # Status codes as defined in schema
95
- defined_status_codes: List[str] = attr.ib()
96
- # Defined status code with expanded wildcards
97
- allowed_status_codes: List[int] = attr.ib()
98
- title: str = attr.ib(default="Undefined status code")
99
- message: str = attr.ib(default="Response has a status code that is not declared in the schema")
100
- type: str = attr.ib(default="undefined_status_code")
101
-
102
-
103
- @attr.s(slots=True, repr=False)
104
- class MissingHeaders(FailureContext):
105
- """Some required headers are missing."""
106
-
107
- missing_headers: List[str] = attr.ib()
108
- title: str = attr.ib(default="Missing required headers")
109
- message: str = attr.ib(default="Response is missing headers required by the schema")
110
- type: str = attr.ib(default="missing_headers")
111
-
112
-
113
- @attr.s(slots=True, repr=False)
114
- class MalformedMediaType(FailureContext):
115
- """Media type name is malformed.
116
-
117
- Example: `application-json` instead of `application/json`
118
- """
119
-
120
- actual: str = attr.ib()
121
- defined: str = attr.ib()
122
- title: str = attr.ib(default="Malformed media type name")
123
- message: str = attr.ib(default="Media type name is not valid")
124
- type: str = attr.ib(default="malformed_media_type")
125
-
126
-
127
- @attr.s(slots=True, repr=False)
128
- class ResponseTimeExceeded(FailureContext):
129
- """Response took longer than expected."""
130
-
131
- elapsed: float = attr.ib()
132
- deadline: int = attr.ib()
133
- title: str = attr.ib(default="Response time exceeded")
134
- message: str = attr.ib(default="Response time exceeds the deadline")
135
- type: str = attr.ib(default="response_time_exceeded")
136
-
137
-
138
- @attr.s(slots=True, repr=False)
139
- class RequestTimeout(FailureContext):
140
- """Request took longer than timeout."""
141
-
142
- timeout: int = attr.ib()
143
- title: str = attr.ib(default="Request timeout")
144
- message: str = attr.ib(default="The request timed out")
145
- type: str = attr.ib(default="request_timeout")
@@ -1,29 +0,0 @@
1
- from typing import Iterable, Optional
2
-
3
- from . import fast_api
4
-
5
- ALL_FIXUPS = {"fast_api": fast_api}
6
-
7
-
8
- def install(fixups: Optional[Iterable[str]] = None) -> None:
9
- """Install fixups.
10
-
11
- Without the first argument installs all available fixups.
12
-
13
- :param fixups: Names of fixups to install.
14
- """
15
- fixups = fixups or list(ALL_FIXUPS.keys())
16
- for name in fixups:
17
- ALL_FIXUPS[name].install() # type: ignore
18
-
19
-
20
- def uninstall(fixups: Optional[Iterable[str]] = None) -> None:
21
- """Uninstall fixups.
22
-
23
- Without the first argument uninstalls all available fixups.
24
-
25
- :param fixups: Names of fixups to uninstall.
26
- """
27
- fixups = fixups or list(ALL_FIXUPS.keys())
28
- for name in fixups:
29
- ALL_FIXUPS[name].uninstall() # type: ignore
@@ -1,30 +0,0 @@
1
- from typing import Any, Dict
2
-
3
- from ..hooks import HookContext, register, unregister
4
- from ..utils import traverse_schema
5
-
6
-
7
- def install() -> None:
8
- register(before_load_schema)
9
-
10
-
11
- def uninstall() -> None:
12
- unregister(before_load_schema)
13
-
14
-
15
- def before_load_schema(context: HookContext, schema: Dict[str, Any]) -> None:
16
- traverse_schema(schema, _handle_boundaries)
17
-
18
-
19
- def _handle_boundaries(schema: Dict[str, Any]) -> Dict[str, Any]:
20
- """Convert Draft 7 keywords to Draft 4 compatible versions.
21
-
22
- FastAPI uses ``pydantic``, which generates Draft 7 compatible schemas.
23
- """
24
- for boundary_name, boundary_exclusive_name in (("maximum", "exclusiveMaximum"), ("minimum", "exclusiveMinimum")):
25
- value = schema.get(boundary_exclusive_name)
26
- # `bool` check is needed, since in Python `True` is an instance of `int`
27
- if isinstance(value, (int, float)) and not isinstance(value, bool):
28
- schema[boundary_exclusive_name] = True
29
- schema[boundary_name] = value
30
- return schema
schemathesis/graphql.py DELETED
@@ -1,5 +0,0 @@
1
- # pylint: disable=unused-import
2
- # Public API
3
- from .specs.graphql import nodes
4
- from .specs.graphql.loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
5
- from .specs.graphql.scalars import register_scalar
schemathesis/internal.py DELETED
@@ -1,6 +0,0 @@
1
- """A private API to work with Schemathesis internals."""
2
- from .specs.openapi import _hypothesis
3
-
4
-
5
- def clear_cache() -> None:
6
- _hypothesis.clear_cache()
schemathesis/lazy.py DELETED
@@ -1,301 +0,0 @@
1
- from inspect import signature
2
- from typing import Any, Callable, Dict, Generator, Optional, Type, Union
3
-
4
- import attr
5
- import pytest
6
- from _pytest.fixtures import FixtureRequest
7
- from hypothesis.core import HypothesisHandle
8
- from hypothesis.errors import Flaky, MultipleFailures
9
- from hypothesis.internal.escalation import format_exception, get_interesting_origin, get_trimmed_traceback
10
- from hypothesis.internal.reflection import impersonate
11
- from pytest_subtests import SubTests, nullcontext
12
-
13
- from .auth import AuthStorage
14
- from .constants import FLAKY_FAILURE_MESSAGE, CodeSampleStyle, DataGenerationMethod
15
- from .exceptions import CheckFailed, InvalidSchema, SkipTest, get_grouped_exception
16
- from .hooks import HookDispatcher, HookScope
17
- from .models import APIOperation
18
- from .schemas import BaseSchema
19
- from .types import DataGenerationMethodInput, Filter, GenericTest, NotSet
20
- from .utils import (
21
- NOT_SET,
22
- GivenInput,
23
- Ok,
24
- fail_on_no_matches,
25
- get_given_args,
26
- get_given_kwargs,
27
- given_proxy,
28
- is_given_applied,
29
- merge_given_args,
30
- validate_given_args,
31
- )
32
-
33
-
34
- @attr.s(slots=True) # pragma: no mutate
35
- class LazySchema:
36
- fixture_name: str = attr.ib() # pragma: no mutate
37
- base_url: Union[Optional[str], NotSet] = attr.ib(default=NOT_SET) # pragma: no mutate
38
- method: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
39
- endpoint: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
40
- tag: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
41
- operation_id: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
42
- app: Any = attr.ib(default=NOT_SET) # pragma: no mutate
43
- hooks: HookDispatcher = attr.ib(factory=lambda: HookDispatcher(scope=HookScope.SCHEMA)) # pragma: no mutate
44
- auth: AuthStorage = attr.ib(factory=AuthStorage) # pragma: no mutate
45
- validate_schema: bool = attr.ib(default=True) # pragma: no mutate
46
- skip_deprecated_operations: bool = attr.ib(default=False) # pragma: no mutate
47
- data_generation_methods: Union[DataGenerationMethodInput, NotSet] = attr.ib(default=NOT_SET)
48
- code_sample_style: CodeSampleStyle = attr.ib(default=CodeSampleStyle.default()) # pragma: no mutate
49
-
50
- def parametrize(
51
- self,
52
- method: Optional[Filter] = NOT_SET,
53
- endpoint: Optional[Filter] = NOT_SET,
54
- tag: Optional[Filter] = NOT_SET,
55
- operation_id: Optional[Filter] = NOT_SET,
56
- validate_schema: Union[bool, NotSet] = NOT_SET,
57
- skip_deprecated_operations: Union[bool, NotSet] = NOT_SET,
58
- data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
59
- code_sample_style: Union[str, NotSet] = NOT_SET,
60
- ) -> Callable:
61
- if method is NOT_SET:
62
- method = self.method
63
- if endpoint is NOT_SET:
64
- endpoint = self.endpoint
65
- if tag is NOT_SET:
66
- tag = self.tag
67
- if operation_id is NOT_SET:
68
- operation_id = self.operation_id
69
- if data_generation_methods is NOT_SET:
70
- data_generation_methods = self.data_generation_methods
71
- if isinstance(code_sample_style, str):
72
- _code_sample_style = CodeSampleStyle.from_str(code_sample_style)
73
- else:
74
- _code_sample_style = self.code_sample_style
75
-
76
- def wrapper(test: Callable) -> Callable:
77
- if is_given_applied(test):
78
- # The user wrapped the test function with `@schema.given`
79
- # These args & kwargs go as extra to the underlying test generator
80
- given_args = get_given_args(test)
81
- given_kwargs = get_given_kwargs(test)
82
- test_function = validate_given_args(test, given_args, given_kwargs)
83
- if test_function is not None:
84
- return test_function
85
- given_kwargs = merge_given_args(test, given_args, given_kwargs)
86
- del given_args
87
- else:
88
- given_kwargs = {}
89
-
90
- def wrapped_test(request: FixtureRequest) -> None:
91
- """The actual test, which is executed by pytest."""
92
- __tracebackhide__ = True # pylint: disable=unused-variable
93
- if hasattr(wrapped_test, "_schemathesis_hooks"):
94
- test._schemathesis_hooks = wrapped_test._schemathesis_hooks # type: ignore
95
- schema = get_schema(
96
- request=request,
97
- name=self.fixture_name,
98
- base_url=self.base_url,
99
- method=method,
100
- endpoint=endpoint,
101
- tag=tag,
102
- operation_id=operation_id,
103
- hooks=self.hooks,
104
- auth=self.auth if self.auth.provider is not None else NOT_SET,
105
- test_function=test,
106
- validate_schema=validate_schema,
107
- skip_deprecated_operations=skip_deprecated_operations,
108
- data_generation_methods=data_generation_methods,
109
- code_sample_style=_code_sample_style,
110
- app=self.app,
111
- )
112
- fixtures = get_fixtures(test, request, given_kwargs)
113
- # Changing the node id is required for better reporting - the method and path will appear there
114
- node_id = request.node._nodeid
115
- settings = getattr(wrapped_test, "_hypothesis_internal_use_settings", None)
116
- tests = list(schema.get_all_tests(test, settings, _given_kwargs=given_kwargs))
117
- if not tests:
118
- fail_on_no_matches(node_id)
119
- request.session.testscollected += len(tests)
120
- suspend_capture_ctx = _get_capturemanager(request)
121
- subtests = SubTests(request.node.ihook, suspend_capture_ctx, request)
122
- for result, data_generation_method in tests:
123
- if isinstance(result, Ok):
124
- operation, sub_test = result.ok()
125
- subtests.item._nodeid = _get_node_name(node_id, operation, data_generation_method)
126
- run_subtest(operation, data_generation_method, fixtures, sub_test, subtests)
127
- else:
128
- _schema_error(subtests, result.err(), node_id, data_generation_method)
129
- subtests.item._nodeid = node_id
130
-
131
- wrapped_test = pytest.mark.usefixtures(self.fixture_name)(wrapped_test)
132
- _copy_marks(test, wrapped_test)
133
-
134
- # Needed to prevent a failure when settings are applied to the test function
135
- wrapped_test.is_hypothesis_test = True # type: ignore
136
- wrapped_test.hypothesis = HypothesisHandle(test, wrapped_test, given_kwargs) # type: ignore
137
-
138
- return wrapped_test
139
-
140
- return wrapper
141
-
142
- def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
143
- return given_proxy(*args, **kwargs)
144
-
145
-
146
- def _copy_marks(source: Callable, target: Callable) -> None:
147
- marks = getattr(source, "pytestmark", [])
148
- # Pytest adds this attribute in `usefixtures`
149
- target.pytestmark.extend(marks) # type: ignore
150
-
151
-
152
- def _get_capturemanager(request: FixtureRequest) -> Generator:
153
- capturemanager = request.node.config.pluginmanager.get_plugin("capturemanager")
154
- if capturemanager is not None:
155
- return capturemanager.global_and_fixture_disabled
156
- return nullcontext
157
-
158
-
159
- def _get_node_name(node_id: str, operation: APIOperation, data_generation_method: DataGenerationMethod) -> str:
160
- """Make a test node name. For example: test_api[GET /users]."""
161
- return f"{node_id}[{operation.method.upper()} {operation.full_path}][{data_generation_method.as_short_name()}]"
162
-
163
-
164
- def _get_partial_node_name(node_id: str, data_generation_method: DataGenerationMethod, **kwargs: Any) -> str:
165
- """Make a test node name for failing tests caused by schema errors."""
166
- name = node_id
167
- if "method" in kwargs:
168
- name += f"[{kwargs['method']} {kwargs['path']}]"
169
- else:
170
- name += f"[{kwargs['path']}]"
171
- name += f"[{data_generation_method.as_short_name()}]"
172
- return name
173
-
174
-
175
- def run_subtest(
176
- operation: APIOperation,
177
- data_generation_method: DataGenerationMethod,
178
- fixtures: Dict[str, Any],
179
- sub_test: Callable,
180
- subtests: SubTests,
181
- ) -> None:
182
- """Run the given subtest with pytest fixtures."""
183
- __tracebackhide__ = True # pylint: disable=unused-variable
184
-
185
- # Deduplicate found checks in case of Hypothesis finding multiple of them
186
- failed_checks = {}
187
- exceptions = []
188
- inner_test = sub_test.hypothesis.inner_test # type: ignore
189
-
190
- @impersonate(inner_test) # type: ignore
191
- def collecting_wrapper(*args: Any, **kwargs: Any) -> None:
192
- __tracebackhide__ = True # pylint: disable=unused-variable
193
- try:
194
- inner_test(*args, **kwargs)
195
- except CheckFailed as failed:
196
- failed_checks[failed.__class__] = failed
197
- raise failed
198
- except Exception as exception:
199
- # Deduplicate it later, as it is more costly than for `CheckFailed`
200
- exceptions.append(exception)
201
- raise
202
-
203
- def get_exception_class() -> Type[CheckFailed]:
204
- return get_grouped_exception("Lazy", *failed_checks.values())
205
-
206
- sub_test.hypothesis.inner_test = collecting_wrapper # type: ignore
207
-
208
- with subtests.test(
209
- verbose_name=operation.verbose_name, data_generation_method=data_generation_method.as_short_name()
210
- ):
211
- try:
212
- sub_test(**fixtures)
213
- except SkipTest as exc:
214
- pytest.skip(exc.args[0])
215
- except MultipleFailures as exc:
216
- # Hypothesis doesn't report the underlying failures in these circumstances, hence we display them manually
217
- exc_class = get_exception_class()
218
- failures = "".join(f"{SEPARATOR} {failure.args[0]}" for failure in failed_checks.values())
219
- unique_exceptions = {get_interesting_origin(exception): exception for exception in exceptions}
220
- message = (
221
- f"Schemathesis found {len(failed_checks) + len(unique_exceptions)} distinct sets of failures.{failures}"
222
- )
223
- for exception in unique_exceptions.values():
224
- # Non-check exceptions
225
- message += f"{SEPARATOR}\n\n"
226
- tb = get_trimmed_traceback(exception)
227
- message += format_exception(exception, tb)
228
- raise exc_class(message, causes=tuple(failed_checks.values())).with_traceback(exc.__traceback__) from None
229
- except Flaky as exc:
230
- exc_class = get_exception_class()
231
- failure = next(iter(failed_checks.values()))
232
- message = f"{FLAKY_FAILURE_MESSAGE}{failure}"
233
- # The outer frame is the one for user's test function, take it as the root one
234
- traceback = exc.__traceback__.tb_next
235
- # The next one comes from Hypothesis internals - remove it
236
- traceback.tb_next = None
237
- raise exc_class(message, causes=tuple(failed_checks.values())).with_traceback(traceback) from None
238
-
239
-
240
- SEPARATOR = "\n===================="
241
-
242
-
243
- def _schema_error(
244
- subtests: SubTests, error: InvalidSchema, node_id: str, data_generation_method: DataGenerationMethod
245
- ) -> None:
246
- """Run a failing test, that will show the underlying problem."""
247
- sub_test = error.as_failing_test_function()
248
- # `full_path` is always available in this case
249
- kwargs = {"path": error.full_path}
250
- if error.method:
251
- kwargs["method"] = error.method.upper()
252
- subtests.item._nodeid = _get_partial_node_name(node_id, data_generation_method, **kwargs)
253
- with subtests.test(**kwargs):
254
- sub_test()
255
-
256
-
257
- def get_schema(
258
- *,
259
- request: FixtureRequest,
260
- name: str,
261
- base_url: Union[Optional[str], NotSet] = None,
262
- method: Optional[Filter] = None,
263
- endpoint: Optional[Filter] = None,
264
- tag: Optional[Filter] = None,
265
- operation_id: Optional[Filter] = None,
266
- app: Any = None,
267
- test_function: GenericTest,
268
- hooks: HookDispatcher,
269
- auth: Union[AuthStorage, NotSet],
270
- validate_schema: Union[bool, NotSet] = NOT_SET,
271
- skip_deprecated_operations: Union[bool, NotSet] = NOT_SET,
272
- data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
273
- code_sample_style: CodeSampleStyle,
274
- ) -> BaseSchema:
275
- """Loads a schema from the fixture."""
276
- schema = request.getfixturevalue(name)
277
- if not isinstance(schema, BaseSchema):
278
- raise ValueError(f"The given schema must be an instance of BaseSchema, got: {type(schema)}")
279
- return schema.clone(
280
- base_url=base_url,
281
- method=method,
282
- endpoint=endpoint,
283
- tag=tag,
284
- operation_id=operation_id,
285
- app=app,
286
- test_function=test_function,
287
- hooks=schema.hooks.merge(hooks),
288
- auth=auth,
289
- validate_schema=validate_schema,
290
- skip_deprecated_operations=skip_deprecated_operations,
291
- data_generation_methods=data_generation_methods,
292
- code_sample_style=code_sample_style,
293
- )
294
-
295
-
296
- def get_fixtures(func: Callable, request: FixtureRequest, given_kwargs: Dict[str, Any]) -> Dict[str, Any]:
297
- """Load fixtures, needed for the test function."""
298
- sig = signature(func)
299
- return {
300
- name: request.getfixturevalue(name) for name in sig.parameters if name != "case" and name not in given_kwargs
301
- }