schemathesis 3.38.1__py3-none-any.whl → 3.38.3__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
@@ -223,18 +223,6 @@ def _iter_coverage_cases(
223
223
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
224
224
  from .specs.openapi.examples import find_in_responses, find_matching_in_responses
225
225
 
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
226
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
239
227
  template: dict[str, Any] = {}
240
228
  responses = find_in_responses(operation)
@@ -275,25 +263,27 @@ def _iter_coverage_cases(
275
263
  template["media_type"] = body.media_type
276
264
  case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
277
265
  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"
266
+ case.meta = _make_meta(
267
+ description=value.description,
268
+ location=value.location,
269
+ parameter=body.media_type,
270
+ parameter_location="body",
271
+ )
283
272
  yield case
284
273
  for next_value in gen:
285
274
  case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
286
275
  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"
276
+ case.meta = _make_meta(
277
+ description=next_value.description,
278
+ location=next_value.location,
279
+ parameter=body.media_type,
280
+ parameter_location="body",
281
+ )
292
282
  yield case
293
283
  elif DataGenerationMethod.positive in data_generation_methods:
294
284
  case = operation.make_case(**template)
295
285
  case.data_generation_method = DataGenerationMethod.positive
296
- case.meta = copy(meta)
286
+ case.meta = _make_meta(description="Default positive test case")
297
287
  yield case
298
288
  for (location, name), gen in generators.items():
299
289
  container_name = LOCATION_TO_CONTAINER[location]
@@ -305,16 +295,17 @@ def _iter_coverage_cases(
305
295
  generated = value.value
306
296
  case = operation.make_case(**{**template, container_name: {**container, name: generated}})
307
297
  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
298
+ case.meta = _make_meta(
299
+ description=value.description,
300
+ location=value.location,
301
+ parameter=name,
302
+ parameter_location=location,
303
+ )
313
304
  yield case
314
305
  # Generate missing required parameters
315
306
  if DataGenerationMethod.negative in data_generation_methods:
316
307
  for parameter in operation.iter_parameters():
317
- if parameter.is_required:
308
+ if parameter.is_required and parameter.location != "path":
318
309
  name = parameter.name
319
310
  location = parameter.location
320
311
  container_name = LOCATION_TO_CONTAINER[location]
@@ -323,12 +314,87 @@ def _iter_coverage_cases(
323
314
  **{**template, container_name: {k: v for k, v in container.items() if k != name}}
324
315
  )
325
316
  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
317
+ case.meta = _make_meta(
318
+ description=f"Missing `{name}` at {location}",
319
+ location=parameter.location,
320
+ parameter=name,
321
+ parameter_location=location,
322
+ )
331
323
  yield case
324
+ # Generate combinations for each location
325
+ for location, parameter_set in [
326
+ ("query", operation.query),
327
+ ("header", operation.headers),
328
+ ("cookie", operation.cookies),
329
+ ]:
330
+ if not parameter_set:
331
+ continue
332
+
333
+ container_name = LOCATION_TO_CONTAINER[location]
334
+ base_container = template.get(container_name, {})
335
+
336
+ # Get required and optional parameters
337
+ required = {p.name for p in parameter_set if p.is_required}
338
+ all_params = {p.name for p in parameter_set}
339
+ optional = sorted(all_params - required)
340
+
341
+ # Helper function to create and yield a case
342
+ def make_case(container_values: dict, description: str, _location: str, _container_name: str) -> Case:
343
+ if _location in ("header", "cookie"):
344
+ container = {
345
+ name: json.dumps(val) if not isinstance(val, str) else val for name, val in container_values.items()
346
+ }
347
+ else:
348
+ container = container_values
349
+
350
+ case = operation.make_case(**{**template, _container_name: container})
351
+ case.data_generation_method = DataGenerationMethod.positive
352
+ case.meta = _make_meta(
353
+ description=description,
354
+ location=_location,
355
+ parameter_location=_location,
356
+ )
357
+ return case
358
+
359
+ # 1. Generate only required properties
360
+ if required and all_params != required:
361
+ only_required = {k: v for k, v in base_container.items() if k in required}
362
+ yield make_case(only_required, "Only required properties", location, container_name)
363
+
364
+ # 2. Generate combinations with required properties and one optional property
365
+ for opt_param in optional:
366
+ combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
367
+ if combo != base_container:
368
+ yield make_case(combo, f"All required properties and optional '{opt_param}'", location, container_name)
369
+
370
+ # 3. Generate one combination for each size from 2 to N-1 of optional parameters
371
+ if len(optional) > 1:
372
+ for size in range(2, len(optional)):
373
+ for combination in combinations(optional, size):
374
+ combo = {k: v for k, v in base_container.items() if k in required or k in combination}
375
+ if combo != base_container:
376
+ yield make_case(combo, f"All required and {size} optional properties", location, container_name)
377
+
378
+
379
+ def _make_meta(
380
+ *,
381
+ description: str,
382
+ location: str | None = None,
383
+ parameter: str | None = None,
384
+ parameter_location: str | None = None,
385
+ ) -> GenerationMetadata:
386
+ return GenerationMetadata(
387
+ query=None,
388
+ path_parameters=None,
389
+ headers=None,
390
+ cookies=None,
391
+ body=None,
392
+ phase=TestPhase.COVERAGE,
393
+ description=description,
394
+ location=location,
395
+ parameter=parameter,
396
+ parameter_location=parameter_location,
397
+ )
332
398
 
333
399
 
334
400
  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.1
3
+ Version: 3.38.3
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=9ZwAvjvS07O9tz4LxOFI8ZfQASKIz3rocsYIfE4KTU0,16562
4
+ schemathesis/_hypothesis.py,sha256=F-cGpyEX0rWiCPOS58WU067wvL-PsxderMRxX3qS91k,19376
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.1.dist-info/METADATA,sha256=uff9hJ2ktn_0hhxt3lSUCCJCzkLmVGaJ5CiuXmta8_c,12956
157
- schemathesis-3.38.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
158
- schemathesis-3.38.1.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
- schemathesis-3.38.1.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
- schemathesis-3.38.1.dist-info/RECORD,,
156
+ schemathesis-3.38.3.dist-info/METADATA,sha256=N4rZPrKlOyw9dEae6FkXFD-WLAMsYDCQNLFWEZePxmA,12956
157
+ schemathesis-3.38.3.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
158
+ schemathesis-3.38.3.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
+ schemathesis-3.38.3.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
+ schemathesis-3.38.3.dist-info/RECORD,,