schemathesis 3.37.1__py3-none-any.whl → 3.38.1__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 (31) hide show
  1. schemathesis/_hypothesis.py +44 -8
  2. schemathesis/_patches.py +21 -0
  3. schemathesis/cli/__init__.py +1 -1
  4. schemathesis/cli/cassettes.py +18 -0
  5. schemathesis/extra/pytest_plugin.py +1 -1
  6. schemathesis/generation/_hypothesis.py +2 -0
  7. schemathesis/generation/coverage.py +257 -59
  8. schemathesis/internal/checks.py +5 -3
  9. schemathesis/internal/diff.py +15 -0
  10. schemathesis/models.py +76 -4
  11. schemathesis/runner/impl/context.py +5 -1
  12. schemathesis/runner/impl/core.py +14 -4
  13. schemathesis/runner/serialization.py +10 -3
  14. schemathesis/serializers.py +3 -0
  15. schemathesis/service/extensions.py +1 -1
  16. schemathesis/service/metadata.py +3 -3
  17. schemathesis/specs/openapi/_hypothesis.py +9 -46
  18. schemathesis/specs/openapi/checks.py +7 -2
  19. schemathesis/specs/openapi/converter.py +27 -11
  20. schemathesis/specs/openapi/formats.py +44 -0
  21. schemathesis/specs/openapi/negative/mutations.py +5 -0
  22. schemathesis/specs/openapi/parameters.py +16 -14
  23. schemathesis/specs/openapi/schemas.py +6 -2
  24. schemathesis/stateful/context.py +1 -1
  25. schemathesis/stateful/runner.py +6 -2
  26. schemathesis/utils.py +6 -4
  27. {schemathesis-3.37.1.dist-info → schemathesis-3.38.1.dist-info}/METADATA +2 -1
  28. {schemathesis-3.37.1.dist-info → schemathesis-3.38.1.dist-info}/RECORD +31 -29
  29. {schemathesis-3.37.1.dist-info → schemathesis-3.38.1.dist-info}/WHEEL +0 -0
  30. {schemathesis-3.37.1.dist-info → schemathesis-3.38.1.dist-info}/entry_points.txt +0 -0
  31. {schemathesis-3.37.1.dist-info → schemathesis-3.38.1.dist-info}/licenses/LICENSE +0 -0
@@ -6,15 +6,16 @@ import asyncio
6
6
  import json
7
7
  import warnings
8
8
  from copy import copy
9
+ from functools import wraps
9
10
  from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
10
11
 
11
12
  import hypothesis
12
13
  from hypothesis import Phase
13
14
  from hypothesis.errors import HypothesisWarning, Unsatisfiable
14
15
  from hypothesis.internal.entropy import deterministic_PRNG
15
- from hypothesis.internal.reflection import proxies
16
16
  from jsonschema.exceptions import SchemaError
17
17
 
18
+ from . import _patches
18
19
  from .auths import get_auth_storage_from_test
19
20
  from .constants import DEFAULT_DEADLINE, NOT_SET
20
21
  from .exceptions import OperationSchemaError, SerializationNotPossible
@@ -29,11 +30,13 @@ from .types import NotSet
29
30
  if TYPE_CHECKING:
30
31
  from .utils import GivenInput
31
32
 
32
- # Forcefully initializes Hypothesis' global PRNG to avoid races that initilize it
33
+ # Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
33
34
  # if e.g. Schemathesis CLI is used with multiple workers
34
35
  with deterministic_PRNG():
35
36
  pass
36
37
 
38
+ _patches.install()
39
+
37
40
 
38
41
  def create_test(
39
42
  *,
@@ -72,7 +75,7 @@ def create_test(
72
75
  # tests in multiple threads because Hypothesis stores some internal attributes on function objects and re-writing
73
76
  # them from different threads may lead to unpredictable side-effects.
74
77
 
75
- @proxies(test) # type: ignore
78
+ @wraps(test)
76
79
  def test_function(*args: Any, **kwargs: Any) -> Any:
77
80
  __tracebackhide__ = True
78
81
  return test(*args, **kwargs)
@@ -220,7 +223,6 @@ def _iter_coverage_cases(
220
223
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
221
224
  from .specs.openapi.examples import find_in_responses, find_matching_in_responses
222
225
 
223
- ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
224
226
  meta = GenerationMetadata(
225
227
  query=None,
226
228
  path_parameters=None,
@@ -229,15 +231,20 @@ def _iter_coverage_cases(
229
231
  body=None,
230
232
  phase=TestPhase.COVERAGE,
231
233
  description=None,
234
+ location=None,
235
+ parameter=None,
236
+ parameter_location=None,
232
237
  )
233
238
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
234
239
  template: dict[str, Any] = {}
235
240
  responses = find_in_responses(operation)
236
241
  for parameter in operation.iter_parameters():
237
- schema = parameter.as_json_schema(operation)
242
+ schema = parameter.as_json_schema(operation, update_quantifiers=False)
238
243
  for value in find_matching_in_responses(responses, parameter.name):
239
244
  schema.setdefault("examples", []).append(value)
240
- gen = coverage.cover_schema_iter(ctx, schema)
245
+ gen = coverage.cover_schema_iter(
246
+ coverage.CoverageContext(data_generation_methods=data_generation_methods), schema
247
+ )
241
248
  value = next(gen, NOT_SET)
242
249
  if isinstance(value, NotSet):
243
250
  continue
@@ -251,13 +258,15 @@ def _iter_coverage_cases(
251
258
  generators[(location, name)] = gen
252
259
  if operation.body:
253
260
  for body in operation.body:
254
- schema = body.as_json_schema(operation)
261
+ schema = body.as_json_schema(operation, update_quantifiers=False)
255
262
  # Definition could be a list for Open API 2.0
256
263
  definition = body.definition if isinstance(body.definition, dict) else {}
257
264
  examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
258
265
  if examples:
259
266
  schema.setdefault("examples", []).extend(examples)
260
- gen = coverage.cover_schema_iter(ctx, schema)
267
+ gen = coverage.cover_schema_iter(
268
+ coverage.CoverageContext(data_generation_methods=data_generation_methods), schema
269
+ )
261
270
  value = next(gen, NOT_SET)
262
271
  if isinstance(value, NotSet):
263
272
  continue
@@ -268,12 +277,18 @@ def _iter_coverage_cases(
268
277
  case.data_generation_method = value.data_generation_method
269
278
  case.meta = copy(meta)
270
279
  case.meta.description = value.description
280
+ case.meta.location = value.location
281
+ case.meta.parameter = body.media_type
282
+ case.meta.parameter_location = "body"
271
283
  yield case
272
284
  for next_value in gen:
273
285
  case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
274
286
  case.data_generation_method = next_value.data_generation_method
275
287
  case.meta = copy(meta)
276
288
  case.meta.description = next_value.description
289
+ case.meta.location = next_value.location
290
+ case.meta.parameter = body.media_type
291
+ case.meta.parameter_location = "body"
277
292
  yield case
278
293
  elif DataGenerationMethod.positive in data_generation_methods:
279
294
  case = operation.make_case(**template)
@@ -292,7 +307,28 @@ def _iter_coverage_cases(
292
307
  case.data_generation_method = value.data_generation_method
293
308
  case.meta = copy(meta)
294
309
  case.meta.description = value.description
310
+ case.meta.location = value.location
311
+ case.meta.parameter = name
312
+ case.meta.parameter_location = location
295
313
  yield case
314
+ # Generate missing required parameters
315
+ if DataGenerationMethod.negative in data_generation_methods:
316
+ for parameter in operation.iter_parameters():
317
+ if parameter.is_required:
318
+ name = parameter.name
319
+ location = parameter.location
320
+ container_name = LOCATION_TO_CONTAINER[location]
321
+ container = template[container_name]
322
+ case = operation.make_case(
323
+ **{**template, container_name: {k: v for k, v in container.items() if k != name}}
324
+ )
325
+ case.data_generation_method = DataGenerationMethod.negative
326
+ case.meta = copy(meta)
327
+ case.meta.description = f"Missing `{name}` at {location}"
328
+ case.meta.location = parameter.location
329
+ case.meta.parameter = name
330
+ case.meta.parameter_location = location
331
+ yield case
296
332
 
297
333
 
298
334
  def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
@@ -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
@@ -36,7 +36,7 @@ from ..filters import FilterSet, expression_to_filter_function, is_deprecated
36
36
  from ..fixups import ALL_FIXUPS
37
37
  from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
38
38
  from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
39
- from ..internal.checks import CheckConfig, PositiveDataAcceptanceConfig
39
+ from ..internal.checks import CheckConfig
40
40
  from ..internal.datetime import current_datetime
41
41
  from ..internal.output import OutputConfig
42
42
  from ..internal.validation import file_exists
@@ -244,6 +244,24 @@ http_interactions:"""
244
244
  write_double_quoted(stream, interaction.description)
245
245
  else:
246
246
  stream.write("null")
247
+
248
+ stream.write("\n location: ")
249
+ if interaction.location is not None:
250
+ write_double_quoted(stream, interaction.location)
251
+ else:
252
+ stream.write("null")
253
+
254
+ stream.write("\n parameter: ")
255
+ if interaction.parameter is not None:
256
+ write_double_quoted(stream, interaction.parameter)
257
+ else:
258
+ stream.write("null")
259
+
260
+ stream.write("\n parameter_location: ")
261
+ if interaction.parameter_location is not None:
262
+ write_double_quoted(stream, interaction.parameter_location)
263
+ else:
264
+ stream.write("null")
247
265
  stream.write(
248
266
  f"""
249
267
  phase: {phase}
@@ -200,7 +200,7 @@ class SchemathesisCase(PyCollector):
200
200
  kwargs["_ispytest"] = True
201
201
  metafunc = Metafunc(definition, fixtureinfo, self.config, **kwargs)
202
202
  methods = []
203
- if hasattr(module, "pytest_generate_tests"):
203
+ if module is not None and hasattr(module, "pytest_generate_tests"):
204
204
  methods.append(module.pytest_generate_tests)
205
205
  if hasattr(cls, "pytest_generate_tests"):
206
206
  cls = cast(Type, cls)
@@ -43,6 +43,8 @@ def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> Non
43
43
  def example_generating_inner_function(ex: T) -> None:
44
44
  examples.append(ex)
45
45
 
46
+ example_generating_inner_function._hypothesis_internal_database_key = b"" # type: ignore
47
+
46
48
  if SCHEMATHESIS_BENCHMARK_SEED is not None:
47
49
  example_generating_inner_function = seed(SCHEMATHESIS_BENCHMARK_SEED)(example_generating_inner_function)
48
50