schemathesis 3.25.6__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 +783 -432
  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 +22 -5
  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 +258 -112
  23. schemathesis/cli/output/short.py +23 -8
  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 +318 -211
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +50 -15
  63. schemathesis/runner/events.py +65 -5
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +388 -177
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/runner/probes.py +11 -9
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +7 -2
  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 +45 -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 +78 -60
  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 +126 -12
  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 +360 -241
  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.6.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.6.dist-info/METADATA +0 -356
  144. schemathesis-3.25.6.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py CHANGED
@@ -1,13 +1,13 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Any
3
4
 
4
- from . import auths, checks, experimental, contrib, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
5
+ from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets
5
6
  from ._lazy_import import lazy_import
6
- from .generation import DataGenerationMethod, GenerationConfig # noqa: E402
7
- from .constants import SCHEMATHESIS_VERSION # noqa: E402
8
- from .models import Case # noqa: E402
9
- from .specs import openapi # noqa: E402
10
-
7
+ from .constants import SCHEMATHESIS_VERSION
8
+ from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig
9
+ from .models import Case
10
+ from .specs import openapi
11
11
 
12
12
  __version__ = SCHEMATHESIS_VERSION
13
13
 
schemathesis/_compat.py CHANGED
@@ -1,6 +1,6 @@
1
- from typing import Any, Type, Callable
2
- from ._lazy_import import lazy_import
1
+ from typing import Any, Callable, Type
3
2
 
3
+ from ._lazy_import import lazy_import
4
4
 
5
5
  __all__ = [ # noqa: F822
6
6
  "JSONMixin",
@@ -1,17 +1,19 @@
1
1
  """Compatibility flags based on installed dependency versions."""
2
- from packaging import version
3
2
 
4
3
  from importlib import metadata
5
4
 
5
+ from packaging import version
6
6
 
7
7
  WERKZEUG_VERSION = version.parse(metadata.version("werkzeug"))
8
8
  IS_WERKZEUG_ABOVE_3 = WERKZEUG_VERSION >= version.parse("3.0")
9
9
  IS_WERKZEUG_BELOW_2_1 = WERKZEUG_VERSION < version.parse("2.1.0")
10
10
 
11
11
  PYTEST_VERSION = version.parse(metadata.version("pytest"))
12
- IS_PYTEST_ABOVE_54 = PYTEST_VERSION >= version.parse("5.4.0")
13
12
  IS_PYTEST_ABOVE_7 = PYTEST_VERSION >= version.parse("7.0.0")
14
13
  IS_PYTEST_ABOVE_8 = PYTEST_VERSION >= version.parse("8.0.0")
15
14
 
16
15
  HYPOTHESIS_VERSION = version.parse(metadata.version("hypothesis"))
17
16
  HYPOTHESIS_HAS_STATEFUL_NAMING_IMPROVEMENTS = HYPOTHESIS_VERSION >= version.parse("6.98.14")
17
+
18
+ PYRATE_LIMITER_VERSION = version.parse(metadata.version("pyrate-limiter"))
19
+ IS_PYRATE_LIMITER_ABOVE_3 = PYRATE_LIMITER_VERSION >= version.parse("3.0")
@@ -1,26 +1,42 @@
1
1
  """High-level API for creating Hypothesis tests."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import asyncio
6
+ import json
5
7
  import warnings
6
- from typing import Any, Callable, Generator, Mapping, Optional, Tuple
8
+ from functools import wraps
9
+ from itertools import combinations
10
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
7
11
 
8
12
  import hypothesis
9
13
  from hypothesis import Phase
10
- from hypothesis import strategies as st
11
14
  from hypothesis.errors import HypothesisWarning, Unsatisfiable
12
- from hypothesis.internal.reflection import proxies
15
+ from hypothesis.internal.entropy import deterministic_PRNG
13
16
  from jsonschema.exceptions import SchemaError
14
17
 
18
+ from . import _patches
15
19
  from .auths import get_auth_storage_from_test
16
- from .constants import DEFAULT_DEADLINE
20
+ from .constants import DEFAULT_DEADLINE, NOT_SET
17
21
  from .exceptions import OperationSchemaError, SerializationNotPossible
18
- from .generation import DataGenerationMethod, GenerationConfig
22
+ from .experimental import COVERAGE_PHASE
23
+ from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
19
24
  from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
20
- from .models import APIOperation, Case
25
+ from .models import APIOperation, Case, GenerationMetadata, TestPhase
26
+ from .parameters import ParameterSet
21
27
  from .transports.content_types import parse_content_type
22
28
  from .transports.headers import has_invalid_characters, is_latin_1_encodable
23
- from .utils import GivenInput, combine_strategies
29
+ from .types import NotSet
30
+
31
+ if TYPE_CHECKING:
32
+ from .utils import GivenInput
33
+
34
+ # Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
35
+ # if e.g. Schemathesis CLI is used with multiple workers
36
+ with deterministic_PRNG():
37
+ pass
38
+
39
+ _patches.install()
24
40
 
25
41
 
26
42
  def create_test(
@@ -41,17 +57,17 @@ def create_test(
41
57
  auth_storage = get_auth_storage_from_test(test)
42
58
  strategies = []
43
59
  skip_on_not_negated = len(data_generation_methods) == 1 and DataGenerationMethod.negative in data_generation_methods
60
+ as_strategy_kwargs = as_strategy_kwargs or {}
61
+ as_strategy_kwargs.update(
62
+ {
63
+ "hooks": hook_dispatcher,
64
+ "auth_storage": auth_storage,
65
+ "generation_config": generation_config,
66
+ "skip_on_not_negated": skip_on_not_negated,
67
+ }
68
+ )
44
69
  for data_generation_method in data_generation_methods:
45
- strategies.append(
46
- operation.as_strategy(
47
- hooks=hook_dispatcher,
48
- auth_storage=auth_storage,
49
- data_generation_method=data_generation_method,
50
- generation_config=generation_config,
51
- skip_on_not_negated=skip_on_not_negated,
52
- **(as_strategy_kwargs or {}),
53
- )
54
- )
70
+ strategies.append(operation.as_strategy(data_generation_method=data_generation_method, **as_strategy_kwargs))
55
71
  strategy = combine_strategies(strategies)
56
72
  _given_kwargs = (_given_kwargs or {}).copy()
57
73
  _given_kwargs.setdefault("case", strategy)
@@ -60,7 +76,7 @@ def create_test(
60
76
  # tests in multiple threads because Hypothesis stores some internal attributes on function objects and re-writing
61
77
  # them from different threads may lead to unpredictable side-effects.
62
78
 
63
- @proxies(test) # type: ignore
79
+ @wraps(test)
64
80
  def test_function(*args: Any, **kwargs: Any) -> Any:
65
81
  __tracebackhide__ = True
66
82
  return test(*args, **kwargs)
@@ -76,13 +92,26 @@ def create_test(
76
92
  wrapped_test.hypothesis.inner_test = make_async_test(test) # type: ignore
77
93
  setup_default_deadline(wrapped_test)
78
94
  if settings is not None:
79
- wrapped_test = settings(wrapped_test)
95
+ existing_settings = _get_hypothesis_settings(wrapped_test)
96
+ if existing_settings is not None:
97
+ # Merge the user-provided settings with the current ones
98
+ default = hypothesis.settings.default
99
+ wrapped_test._hypothesis_internal_use_settings = hypothesis.settings(
100
+ wrapped_test._hypothesis_internal_use_settings,
101
+ **{item: value for item, value in settings.__dict__.items() if value != getattr(default, item)},
102
+ )
103
+ else:
104
+ wrapped_test = settings(wrapped_test)
80
105
  existing_settings = _get_hypothesis_settings(wrapped_test)
81
106
  if existing_settings is not None:
82
107
  existing_settings = remove_explain_phase(existing_settings)
83
108
  wrapped_test._hypothesis_internal_use_settings = existing_settings # type: ignore
84
109
  if Phase.explicit in existing_settings.phases:
85
- wrapped_test = add_examples(wrapped_test, operation, hook_dispatcher=hook_dispatcher)
110
+ wrapped_test = add_examples(
111
+ wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
112
+ )
113
+ if COVERAGE_PHASE.is_enabled:
114
+ wrapped_test = add_coverage(wrapped_test, operation, data_generation_methods)
86
115
  return wrapped_test
87
116
 
88
117
 
@@ -122,12 +151,20 @@ def make_async_test(test: Callable) -> Callable:
122
151
  return async_run
123
152
 
124
153
 
125
- def add_examples(test: Callable, operation: APIOperation, hook_dispatcher: HookDispatcher | None = None) -> Callable:
154
+ def add_examples(
155
+ test: Callable,
156
+ operation: APIOperation,
157
+ hook_dispatcher: HookDispatcher | None = None,
158
+ as_strategy_kwargs: dict[str, Any] | None = None,
159
+ ) -> Callable:
126
160
  """Add examples to the Hypothesis test, if they are specified in the schema."""
127
161
  from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
128
162
 
129
163
  try:
130
- examples: list[Case] = [get_single_example(strategy) for strategy in operation.get_strategies_from_examples()]
164
+ examples: list[Case] = [
165
+ get_single_example(strategy)
166
+ for strategy in operation.get_strategies_from_examples(as_strategy_kwargs=as_strategy_kwargs)
167
+ ]
131
168
  except (
132
169
  OperationSchemaError,
133
170
  HypothesisRefResolutionError,
@@ -162,18 +199,316 @@ def add_examples(test: Callable, operation: APIOperation, hook_dispatcher: HookD
162
199
  if invalid_headers:
163
200
  add_invalid_example_header_mark(original_test, invalid_headers)
164
201
  continue
165
- if example.media_type is not None:
166
- try:
167
- media_type = parse_content_type(example.media_type)
168
- if media_type == ("application", "x-www-form-urlencoded"):
169
- example.body = prepare_urlencoded(example.body)
170
- except ValueError:
171
- pass
202
+ adjust_urlencoded_payload(example)
172
203
  test = hypothesis.example(case=example)(test)
173
204
  return test
174
205
 
175
206
 
176
- def find_invalid_headers(headers: Mapping) -> Generator[Tuple[str, str], None, None]:
207
+ def adjust_urlencoded_payload(case: Case) -> None:
208
+ if case.media_type is not None:
209
+ try:
210
+ media_type = parse_content_type(case.media_type)
211
+ if media_type == ("application", "x-www-form-urlencoded"):
212
+ case.body = prepare_urlencoded(case.body)
213
+ except ValueError:
214
+ pass
215
+
216
+
217
+ def add_coverage(
218
+ test: Callable, operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
219
+ ) -> Callable:
220
+ for example in _iter_coverage_cases(operation, data_generation_methods):
221
+ adjust_urlencoded_payload(example)
222
+ test = hypothesis.example(case=example)(test)
223
+ return test
224
+
225
+
226
+ def _iter_coverage_cases(
227
+ operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
228
+ ) -> Generator[Case, None, None]:
229
+ from .specs.openapi.constants import LOCATION_TO_CONTAINER
230
+ from .specs.openapi.examples import find_in_responses, find_matching_in_responses
231
+
232
+ def _stringify_value(val: Any, location: str) -> str | list[str]:
233
+ if isinstance(val, list):
234
+ if location == "query":
235
+ # Having a list here ensures there will be multiple query parameters wit the same name
236
+ return [json.dumps(item) for item in val]
237
+ # use comma-separated values style for arrays
238
+ return ",".join(json.dumps(sub) for sub in val)
239
+ return json.dumps(val)
240
+
241
+ generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
242
+ template: dict[str, Any] = {}
243
+ responses = find_in_responses(operation)
244
+ for parameter in operation.iter_parameters():
245
+ location = parameter.location
246
+ name = parameter.name
247
+ schema = parameter.as_json_schema(operation, update_quantifiers=False)
248
+ for value in find_matching_in_responses(responses, parameter.name):
249
+ schema.setdefault("examples", []).append(value)
250
+ gen = coverage.cover_schema_iter(
251
+ coverage.CoverageContext(location=location, data_generation_methods=data_generation_methods), schema
252
+ )
253
+ value = next(gen, NOT_SET)
254
+ if isinstance(value, NotSet):
255
+ continue
256
+ container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
257
+ if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
258
+ container[name] = _stringify_value(value.value, location)
259
+ else:
260
+ container[name] = value.value
261
+ generators[(location, name)] = gen
262
+ if operation.body:
263
+ for body in operation.body:
264
+ schema = body.as_json_schema(operation, update_quantifiers=False)
265
+ # Definition could be a list for Open API 2.0
266
+ definition = body.definition if isinstance(body.definition, dict) else {}
267
+ examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
268
+ if examples:
269
+ schema.setdefault("examples", []).extend(examples)
270
+ gen = coverage.cover_schema_iter(
271
+ coverage.CoverageContext(location="body", data_generation_methods=data_generation_methods), schema
272
+ )
273
+ value = next(gen, NOT_SET)
274
+ if isinstance(value, NotSet):
275
+ continue
276
+ if "body" not in template:
277
+ template["body"] = value.value
278
+ template["media_type"] = body.media_type
279
+ case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
280
+ case.data_generation_method = value.data_generation_method
281
+ case.meta = _make_meta(
282
+ description=value.description,
283
+ location=value.location,
284
+ parameter=body.media_type,
285
+ parameter_location="body",
286
+ )
287
+ yield case
288
+ for next_value in gen:
289
+ case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
290
+ case.data_generation_method = next_value.data_generation_method
291
+ case.meta = _make_meta(
292
+ description=next_value.description,
293
+ location=next_value.location,
294
+ parameter=body.media_type,
295
+ parameter_location="body",
296
+ )
297
+ yield case
298
+ elif DataGenerationMethod.positive in data_generation_methods:
299
+ case = operation.make_case(**template)
300
+ case.data_generation_method = DataGenerationMethod.positive
301
+ case.meta = _make_meta(description="Default positive test case")
302
+ yield case
303
+
304
+ for (location, name), gen in generators.items():
305
+ container_name = LOCATION_TO_CONTAINER[location]
306
+ container = template[container_name]
307
+ for value in gen:
308
+ if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
309
+ generated = _stringify_value(value.value, location)
310
+ else:
311
+ generated = value.value
312
+ case = operation.make_case(**{**template, container_name: {**container, name: generated}})
313
+ case.data_generation_method = value.data_generation_method
314
+ case.meta = _make_meta(
315
+ description=value.description,
316
+ location=value.location,
317
+ parameter=name,
318
+ parameter_location=location,
319
+ )
320
+ yield case
321
+ if DataGenerationMethod.negative in data_generation_methods:
322
+ # Generate HTTP methods that are not specified in the spec
323
+ # NOTE: The HEAD method is excluded
324
+ methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
325
+ for method in sorted(methods):
326
+ case = operation.make_case(**template)
327
+ case._explicit_method = method
328
+ case.data_generation_method = DataGenerationMethod.negative
329
+ case.meta = _make_meta(description=f"Unspecified HTTP method: {method.upper()}")
330
+ yield case
331
+ # Generate duplicate query parameters
332
+ if operation.query:
333
+ container = template["query"]
334
+ for parameter in operation.query:
335
+ value = container[parameter.name]
336
+ case = operation.make_case(**{**template, "query": {**container, parameter.name: [value, value]}})
337
+ case.data_generation_method = DataGenerationMethod.negative
338
+ case.meta = _make_meta(
339
+ description=f"Duplicate `{parameter.name}` query parameter",
340
+ location=None,
341
+ parameter=parameter.name,
342
+ parameter_location="query",
343
+ )
344
+ yield case
345
+ # Generate missing required parameters
346
+ for parameter in operation.iter_parameters():
347
+ if parameter.is_required and parameter.location != "path":
348
+ name = parameter.name
349
+ location = parameter.location
350
+ container_name = LOCATION_TO_CONTAINER[location]
351
+ container = template[container_name]
352
+ case = operation.make_case(
353
+ **{**template, container_name: {k: v for k, v in container.items() if k != name}}
354
+ )
355
+ case.data_generation_method = DataGenerationMethod.negative
356
+ case.meta = _make_meta(
357
+ description=f"Missing `{name}` at {location}",
358
+ location=None,
359
+ parameter=name,
360
+ parameter_location=location,
361
+ )
362
+ yield case
363
+ # Generate combinations for each location
364
+ for location, parameter_set in [
365
+ ("query", operation.query),
366
+ ("header", operation.headers),
367
+ ("cookie", operation.cookies),
368
+ ]:
369
+ if not parameter_set:
370
+ continue
371
+
372
+ container_name = LOCATION_TO_CONTAINER[location]
373
+ base_container = template.get(container_name, {})
374
+
375
+ # Get required and optional parameters
376
+ required = {p.name for p in parameter_set if p.is_required}
377
+ all_params = {p.name for p in parameter_set}
378
+ optional = sorted(all_params - required)
379
+
380
+ # Helper function to create and yield a case
381
+ def make_case(
382
+ container_values: dict,
383
+ description: str,
384
+ _location: str,
385
+ _container_name: str,
386
+ _parameter: str | None,
387
+ _data_generation_method: DataGenerationMethod,
388
+ ) -> Case:
389
+ if _location in ("header", "cookie", "path", "query"):
390
+ container = {
391
+ name: _stringify_value(val, _location) if not isinstance(val, str) else val
392
+ for name, val in container_values.items()
393
+ }
394
+ else:
395
+ container = container_values
396
+
397
+ case = operation.make_case(**{**template, _container_name: container})
398
+ case.data_generation_method = _data_generation_method
399
+ case.meta = _make_meta(
400
+ description=description,
401
+ location=None,
402
+ parameter=_parameter,
403
+ parameter_location=_location,
404
+ )
405
+ return case
406
+
407
+ def _combination_schema(
408
+ combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
409
+ ) -> dict[str, Any]:
410
+ return {
411
+ "properties": {
412
+ parameter.name: parameter.as_json_schema(operation)
413
+ for parameter in _parameter_set
414
+ if parameter.name in combination
415
+ },
416
+ "required": list(_required),
417
+ "additionalProperties": False,
418
+ }
419
+
420
+ def _yield_negative(
421
+ subschema: dict[str, Any], _location: str, _container_name: str
422
+ ) -> Generator[Case, None, None]:
423
+ for more in coverage.cover_schema_iter(
424
+ coverage.CoverageContext(location=_location, data_generation_methods=[DataGenerationMethod.negative]),
425
+ subschema,
426
+ ):
427
+ yield make_case(
428
+ more.value,
429
+ more.description,
430
+ _location,
431
+ _container_name,
432
+ more.parameter,
433
+ DataGenerationMethod.negative,
434
+ )
435
+
436
+ # 1. Generate only required properties
437
+ if required and all_params != required:
438
+ only_required = {k: v for k, v in base_container.items() if k in required}
439
+ if DataGenerationMethod.positive in data_generation_methods:
440
+ yield make_case(
441
+ only_required,
442
+ "Only required properties",
443
+ location,
444
+ container_name,
445
+ None,
446
+ DataGenerationMethod.positive,
447
+ )
448
+ if DataGenerationMethod.negative in data_generation_methods:
449
+ subschema = _combination_schema(only_required, required, parameter_set)
450
+ for case in _yield_negative(subschema, location, container_name):
451
+ # Already generated in one of the blocks above
452
+ if location != "path" and not case.meta.description.startswith("Missing required property"):
453
+ yield case
454
+
455
+ # 2. Generate combinations with required properties and one optional property
456
+ for opt_param in optional:
457
+ combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
458
+ if combo != base_container and DataGenerationMethod.positive in data_generation_methods:
459
+ yield make_case(
460
+ combo,
461
+ f"All required properties and optional '{opt_param}'",
462
+ location,
463
+ container_name,
464
+ None,
465
+ DataGenerationMethod.positive,
466
+ )
467
+ if DataGenerationMethod.negative in data_generation_methods:
468
+ subschema = _combination_schema(combo, required, parameter_set)
469
+ for case in _yield_negative(subschema, location, container_name):
470
+ # Already generated in one of the blocks above
471
+ if location != "path" and not case.meta.description.startswith("Missing required property"):
472
+ yield case
473
+
474
+ # 3. Generate one combination for each size from 2 to N-1 of optional parameters
475
+ if len(optional) > 1 and DataGenerationMethod.positive in data_generation_methods:
476
+ for size in range(2, len(optional)):
477
+ for combination in combinations(optional, size):
478
+ combo = {k: v for k, v in base_container.items() if k in required or k in combination}
479
+ if combo != base_container:
480
+ yield make_case(
481
+ combo,
482
+ f"All required and {size} optional properties",
483
+ location,
484
+ container_name,
485
+ None,
486
+ DataGenerationMethod.positive,
487
+ )
488
+
489
+
490
+ def _make_meta(
491
+ *,
492
+ description: str,
493
+ location: str | None = None,
494
+ parameter: str | None = None,
495
+ parameter_location: str | None = None,
496
+ ) -> GenerationMetadata:
497
+ return GenerationMetadata(
498
+ query=None,
499
+ path_parameters=None,
500
+ headers=None,
501
+ cookies=None,
502
+ body=None,
503
+ phase=TestPhase.COVERAGE,
504
+ description=description,
505
+ location=location,
506
+ parameter=parameter,
507
+ parameter_location=parameter_location,
508
+ )
509
+
510
+
511
+ def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
177
512
  for name, value in headers.items():
178
513
  if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
179
514
  yield name, value
@@ -187,7 +522,7 @@ def prepare_urlencoded(data: Any) -> Any:
187
522
  for key, value in item.items():
188
523
  output.append((key, value))
189
524
  else:
190
- output.append(item)
525
+ output.append((item, "arbitrary-value"))
191
526
  return output
192
527
  return data
193
528
 
@@ -204,11 +539,11 @@ def add_non_serializable_mark(test: Callable, exc: SerializationNotPossible) ->
204
539
  test._schemathesis_non_serializable = exc # type: ignore
205
540
 
206
541
 
207
- def get_non_serializable_mark(test: Callable) -> Optional[SerializationNotPossible]:
542
+ def get_non_serializable_mark(test: Callable) -> SerializationNotPossible | None:
208
543
  return getattr(test, "_schemathesis_non_serializable", None)
209
544
 
210
545
 
211
- def get_invalid_regex_mark(test: Callable) -> Optional[SchemaError]:
546
+ def get_invalid_regex_mark(test: Callable) -> SchemaError | None:
212
547
  return getattr(test, "_schemathesis_invalid_regex", None)
213
548
 
214
549
 
@@ -216,31 +551,9 @@ def add_invalid_regex_mark(test: Callable, exc: SchemaError) -> None:
216
551
  test._schemathesis_invalid_regex = exc # type: ignore
217
552
 
218
553
 
219
- def get_invalid_example_headers_mark(test: Callable) -> Optional[dict[str, str]]:
554
+ def get_invalid_example_headers_mark(test: Callable) -> dict[str, str] | None:
220
555
  return getattr(test, "_schemathesis_invalid_example_headers", None)
221
556
 
222
557
 
223
558
  def add_invalid_example_header_mark(test: Callable, headers: dict[str, str]) -> None:
224
559
  test._schemathesis_invalid_example_headers = headers # type: ignore
225
-
226
-
227
- def get_single_example(strategy: st.SearchStrategy[Case]) -> Case:
228
- examples: list[Case] = []
229
- add_single_example(strategy, examples)
230
- return examples[0]
231
-
232
-
233
- def add_single_example(strategy: st.SearchStrategy[Case], examples: list[Case]) -> None:
234
- @hypothesis.given(strategy) # type: ignore
235
- @hypothesis.settings( # type: ignore
236
- database=None,
237
- max_examples=1,
238
- deadline=None,
239
- verbosity=hypothesis.Verbosity.quiet,
240
- phases=(hypothesis.Phase.generate,),
241
- suppress_health_check=list(hypothesis.HealthCheck),
242
- )
243
- def example_generating_inner_function(ex: Case) -> None:
244
- examples.append(ex)
245
-
246
- example_generating_inner_function()
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Any, Callable
3
4
 
4
5
 
schemathesis/_override.py CHANGED
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass
3
- from typing import TYPE_CHECKING, Optional
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  from .exceptions import UsageError
6
- from .parameters import ParameterSet
7
- from .types import GenericTest
8
7
 
9
8
  if TYPE_CHECKING:
10
9
  from .models import APIOperation
10
+ from .parameters import ParameterSet
11
+ from .types import GenericTest
11
12
 
12
13
 
13
14
  @dataclass
@@ -36,7 +37,7 @@ def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[s
36
37
  return output
37
38
 
38
39
 
39
- def get_override_from_mark(test: GenericTest) -> Optional[CaseOverride]:
40
+ def get_override_from_mark(test: GenericTest) -> CaseOverride | None:
40
41
  return getattr(test, "_schemathesis_override", None)
41
42
 
42
43
 
@@ -0,0 +1,21 @@
1
+ """A set of performance-related patches."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def install() -> None:
7
+ from hypothesis.internal.reflection import is_first_param_referenced_in_function
8
+ from hypothesis.strategies._internal import core
9
+ from hypothesis_jsonschema import _from_schema, _resolve
10
+
11
+ from .internal.copy import fast_deepcopy
12
+
13
+ # This one is used a lot, and under the hood it re-parses the AST of the same function
14
+ def _is_first_param_referenced_in_function(f: Any) -> bool:
15
+ if f.__name__ == "from_object_schema" and f.__module__ == "hypothesis_jsonschema._from_schema":
16
+ return True
17
+ return is_first_param_referenced_in_function(f)
18
+
19
+ core.is_first_param_referenced_in_function = _is_first_param_referenced_in_function # type: ignore
20
+ _resolve.deepcopy = fast_deepcopy # type: ignore
21
+ _from_schema.deepcopy = fast_deepcopy # type: ignore
@@ -0,0 +1,7 @@
1
+ from ._dependency_versions import IS_PYRATE_LIMITER_ABOVE_3
2
+
3
+ if IS_PYRATE_LIMITER_ABOVE_3:
4
+ from pyrate_limiter import Limiter, Rate, RateItem
5
+ else:
6
+ from pyrate_limiter import Limiter
7
+ from pyrate_limiter import RequestRate as Rate