schemathesis 3.38.2__py3-none-any.whl → 3.38.4__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.
@@ -5,8 +5,8 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import warnings
8
- from copy import copy
9
8
  from functools import wraps
9
+ from itertools import combinations
10
10
  from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
11
11
 
12
12
  import hypothesis
@@ -23,6 +23,7 @@ from .experimental import COVERAGE_PHASE
23
23
  from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
24
24
  from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
25
25
  from .models import APIOperation, Case, GenerationMetadata, TestPhase
26
+ from .parameters import ParameterSet
26
27
  from .transports.content_types import parse_content_type
27
28
  from .transports.headers import has_invalid_characters, is_latin_1_encodable
28
29
  from .types import NotSet
@@ -223,18 +224,6 @@ def _iter_coverage_cases(
223
224
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
224
225
  from .specs.openapi.examples import find_in_responses, find_matching_in_responses
225
226
 
226
- meta = GenerationMetadata(
227
- query=None,
228
- path_parameters=None,
229
- headers=None,
230
- cookies=None,
231
- body=None,
232
- phase=TestPhase.COVERAGE,
233
- description=None,
234
- location=None,
235
- parameter=None,
236
- parameter_location=None,
237
- )
238
227
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
239
228
  template: dict[str, Any] = {}
240
229
  responses = find_in_responses(operation)
@@ -275,25 +264,27 @@ def _iter_coverage_cases(
275
264
  template["media_type"] = body.media_type
276
265
  case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
277
266
  case.data_generation_method = value.data_generation_method
278
- case.meta = copy(meta)
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"
267
+ case.meta = _make_meta(
268
+ description=value.description,
269
+ location=value.location,
270
+ parameter=body.media_type,
271
+ parameter_location="body",
272
+ )
283
273
  yield case
284
274
  for next_value in gen:
285
275
  case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
286
276
  case.data_generation_method = next_value.data_generation_method
287
- case.meta = copy(meta)
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
+ case.meta = _make_meta(
278
+ description=next_value.description,
279
+ location=next_value.location,
280
+ parameter=body.media_type,
281
+ parameter_location="body",
282
+ )
292
283
  yield case
293
284
  elif DataGenerationMethod.positive in data_generation_methods:
294
285
  case = operation.make_case(**template)
295
286
  case.data_generation_method = DataGenerationMethod.positive
296
- case.meta = copy(meta)
287
+ case.meta = _make_meta(description="Default positive test case")
297
288
  yield case
298
289
  for (location, name), gen in generators.items():
299
290
  container_name = LOCATION_TO_CONTAINER[location]
@@ -305,11 +296,12 @@ def _iter_coverage_cases(
305
296
  generated = value.value
306
297
  case = operation.make_case(**{**template, container_name: {**container, name: generated}})
307
298
  case.data_generation_method = value.data_generation_method
308
- case.meta = copy(meta)
309
- case.meta.description = value.description
310
- case.meta.location = value.location
311
- case.meta.parameter = name
312
- case.meta.parameter_location = location
299
+ case.meta = _make_meta(
300
+ description=value.description,
301
+ location=value.location,
302
+ parameter=name,
303
+ parameter_location=location,
304
+ )
313
305
  yield case
314
306
  # Generate missing required parameters
315
307
  if DataGenerationMethod.negative in data_generation_methods:
@@ -323,12 +315,115 @@ def _iter_coverage_cases(
323
315
  **{**template, container_name: {k: v for k, v in container.items() if k != name}}
324
316
  )
325
317
  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
318
+ case.meta = _make_meta(
319
+ description=f"Missing `{name}` at {location}",
320
+ location=parameter.location,
321
+ parameter=name,
322
+ parameter_location=location,
323
+ )
331
324
  yield case
325
+ # Generate combinations for each location
326
+ for location, parameter_set in [
327
+ ("query", operation.query),
328
+ ("header", operation.headers),
329
+ ("cookie", operation.cookies),
330
+ ]:
331
+ if not parameter_set:
332
+ continue
333
+
334
+ container_name = LOCATION_TO_CONTAINER[location]
335
+ base_container = template.get(container_name, {})
336
+
337
+ # Get required and optional parameters
338
+ required = {p.name for p in parameter_set if p.is_required}
339
+ all_params = {p.name for p in parameter_set}
340
+ optional = sorted(all_params - required)
341
+
342
+ # Helper function to create and yield a case
343
+ def make_case(container_values: dict, description: str, _location: str, _container_name: str) -> Case:
344
+ if _location in ("header", "cookie"):
345
+ container = {
346
+ name: json.dumps(val) if not isinstance(val, str) else val for name, val in container_values.items()
347
+ }
348
+ else:
349
+ container = container_values
350
+
351
+ case = operation.make_case(**{**template, _container_name: container})
352
+ case.data_generation_method = DataGenerationMethod.positive
353
+ case.meta = _make_meta(
354
+ description=description,
355
+ location=_location,
356
+ parameter_location=_location,
357
+ )
358
+ return case
359
+
360
+ def _combination_schema(
361
+ combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
362
+ ) -> dict[str, Any]:
363
+ return {
364
+ "properties": {
365
+ parameter.name: parameter.as_json_schema(operation)
366
+ for parameter in _parameter_set
367
+ if parameter.name in combination
368
+ },
369
+ "required": list(_required),
370
+ "additionalProperties": False,
371
+ }
372
+
373
+ def _yield_negative(
374
+ subschema: dict[str, Any], _location: str, _container_name: str
375
+ ) -> Generator[Case, None, None]:
376
+ for more in coverage.cover_schema_iter(
377
+ coverage.CoverageContext(data_generation_methods=[DataGenerationMethod.negative]),
378
+ subschema,
379
+ ):
380
+ yield make_case(more.value, more.description, _location, _container_name)
381
+
382
+ # 1. Generate only required properties
383
+ if required and all_params != required:
384
+ only_required = {k: v for k, v in base_container.items() if k in required}
385
+ yield make_case(only_required, "Only required properties", location, container_name)
386
+ if DataGenerationMethod.negative in data_generation_methods:
387
+ subschema = _combination_schema(only_required, required, parameter_set)
388
+ yield from _yield_negative(subschema, location, container_name)
389
+
390
+ # 2. Generate combinations with required properties and one optional property
391
+ for opt_param in optional:
392
+ combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
393
+ if combo != base_container:
394
+ yield make_case(combo, f"All required properties and optional '{opt_param}'", location, container_name)
395
+ if DataGenerationMethod.negative in data_generation_methods:
396
+ subschema = _combination_schema(combo, required, parameter_set)
397
+ yield from _yield_negative(subschema, location, container_name)
398
+
399
+ # 3. Generate one combination for each size from 2 to N-1 of optional parameters
400
+ if len(optional) > 1:
401
+ for size in range(2, len(optional)):
402
+ for combination in combinations(optional, size):
403
+ combo = {k: v for k, v in base_container.items() if k in required or k in combination}
404
+ if combo != base_container:
405
+ yield make_case(combo, f"All required and {size} optional properties", location, container_name)
406
+
407
+
408
+ def _make_meta(
409
+ *,
410
+ description: str,
411
+ location: str | None = None,
412
+ parameter: str | None = None,
413
+ parameter_location: str | None = None,
414
+ ) -> GenerationMetadata:
415
+ return GenerationMetadata(
416
+ query=None,
417
+ path_parameters=None,
418
+ headers=None,
419
+ cookies=None,
420
+ body=None,
421
+ phase=TestPhase.COVERAGE,
422
+ description=description,
423
+ location=location,
424
+ parameter=parameter,
425
+ parameter_location=parameter_location,
426
+ )
332
427
 
333
428
 
334
429
  def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
schemathesis/schemas.py CHANGED
@@ -49,6 +49,7 @@ from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy
49
49
  if TYPE_CHECKING:
50
50
  import hypothesis
51
51
  from hypothesis.strategies import SearchStrategy
52
+ from hypothesis.vendor.pretty import RepresentationPrinter
52
53
  from pyrate_limiter import Limiter
53
54
 
54
55
  from .stateful import Stateful, StatefulTest
@@ -102,6 +103,9 @@ class BaseSchema(Mapping):
102
103
  def __post_init__(self) -> None:
103
104
  self.hook = to_filterable_hook(self.hooks) # type: ignore[method-assign]
104
105
 
106
+ def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
107
+ return None
108
+
105
109
  def include(
106
110
  self,
107
111
  func: MatcherFunc | None = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: schemathesis
3
- Version: 3.38.2
3
+ Version: 3.38.4
4
4
  Summary: Property-based testing framework for Open API and GraphQL based apps
5
5
  Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
6
6
  Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
@@ -1,7 +1,7 @@
1
1
  schemathesis/__init__.py,sha256=UW2Bq8hDDkcBeAAA7PzpBFXkOOxkmHox-mfQwzHDjL0,1914
2
2
  schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
3
3
  schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
4
- schemathesis/_hypothesis.py,sha256=-fXguhIC3AoSzgf5ZzQI6W9M7_lSwsf2wTVzbiGAK58,16595
4
+ schemathesis/_hypothesis.py,sha256=lm0J9uQFENfm7-6_wEaNKshGF1wmZebfZJ9G_sUhlqE,20839
5
5
  schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
6
6
  schemathesis/_override.py,sha256=TAjYB3eJQmlw9K_xiR9ptt9Wj7if4U7UFlUhGjpBAoM,1625
7
7
  schemathesis/_patches.py,sha256=Hsbpn4UVeXUQD2Kllrbq01CSWsTYENWa0VJTyhX5C2k,895
@@ -22,7 +22,7 @@ schemathesis/models.py,sha256=2kMMJ3JVe4_91uhRxgsZ_G1FOyksxTiYAo52M5asWLA,49868
22
22
  schemathesis/parameters.py,sha256=_LN3NL5XwoRfvjcU8o2ArrNFK9sbBZo25UFdxuywkRw,2425
23
23
  schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  schemathesis/sanitization.py,sha256=Lycn1VVfula9B6XpzkxTHja7CZ7RHqbUh9kBic0Yi4M,9056
25
- schemathesis/schemas.py,sha256=3xTZOZ1lLdAdwLAkiW0eakRb96mQ0MpbcwmrT-XO4KA,20457
25
+ schemathesis/schemas.py,sha256=KyG8IUNv7_3tPBzT9ARGDmUVEPvHHj6f1wW7GYbfvI4,20623
26
26
  schemathesis/serializers.py,sha256=HyYVSVR71FhWfIErnH6OoGLOa4tkh9mTeVUTIpzEW24,11739
27
27
  schemathesis/targets.py,sha256=XIGRghvEzbmEJjse9aZgNEj67L3jAbiazm2rxURWgDE,2351
28
28
  schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
@@ -153,8 +153,8 @@ schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZ
153
153
  schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
154
154
  schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
155
155
  schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
156
- schemathesis-3.38.2.dist-info/METADATA,sha256=hqeX6MagooG7ljuzUgKYAIxgixMTXQj0xmAy3bC-VJw,12956
157
- schemathesis-3.38.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
158
- schemathesis-3.38.2.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
- schemathesis-3.38.2.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
- schemathesis-3.38.2.dist-info/RECORD,,
156
+ schemathesis-3.38.4.dist-info/METADATA,sha256=hXln3HxI_xM3plasHcuEeu6OkknqAZgZBAZr-LJZxE0,12956
157
+ schemathesis-3.38.4.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
158
+ schemathesis-3.38.4.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
+ schemathesis-3.38.4.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
+ schemathesis-3.38.4.dist-info/RECORD,,