schemathesis 3.38.5__py3-none-any.whl → 3.38.6__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.
@@ -224,6 +224,12 @@ def _iter_coverage_cases(
224
224
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
225
225
  from .specs.openapi.examples import find_in_responses, find_matching_in_responses
226
226
 
227
+ def _stringify_value(val: Any) -> str:
228
+ if isinstance(val, list):
229
+ # use comma-separated values style for arrays
230
+ return ",".join(json.dumps(sub) for sub in val)
231
+ return json.dumps(val)
232
+
227
233
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
228
234
  template: dict[str, Any] = {}
229
235
  responses = find_in_responses(operation)
@@ -240,8 +246,8 @@ def _iter_coverage_cases(
240
246
  location = parameter.location
241
247
  name = parameter.name
242
248
  container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
243
- if location in ("header", "cookie") and not isinstance(value.value, str):
244
- container[name] = json.dumps(value.value)
249
+ if location in ("header", "cookie", "path") and not isinstance(value.value, str):
250
+ container[name] = _stringify_value(value.value)
245
251
  else:
246
252
  container[name] = value.value
247
253
  generators[(location, name)] = gen
@@ -286,12 +292,13 @@ def _iter_coverage_cases(
286
292
  case.data_generation_method = DataGenerationMethod.positive
287
293
  case.meta = _make_meta(description="Default positive test case")
288
294
  yield case
295
+
289
296
  for (location, name), gen in generators.items():
290
297
  container_name = LOCATION_TO_CONTAINER[location]
291
298
  container = template[container_name]
292
299
  for value in gen:
293
- if location in ("header", "cookie") and not isinstance(value.value, str):
294
- generated = json.dumps(value.value)
300
+ if location in ("header", "cookie", "path") and not isinstance(value.value, str):
301
+ generated = _stringify_value(value.value)
295
302
  else:
296
303
  generated = value.value
297
304
  case = operation.make_case(**{**template, container_name: {**container, name: generated}})
@@ -340,10 +347,13 @@ def _iter_coverage_cases(
340
347
  optional = sorted(all_params - required)
341
348
 
342
349
  # 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"):
350
+ def make_case(
351
+ container_values: dict, description: str, _location: str, _container_name: str, _parameter: str | None
352
+ ) -> Case:
353
+ if _location in ("header", "cookie", "path"):
345
354
  container = {
346
- name: json.dumps(val) if not isinstance(val, str) else val for name, val in container_values.items()
355
+ name: _stringify_value(val) if not isinstance(val, str) else val
356
+ for name, val in container_values.items()
347
357
  }
348
358
  else:
349
359
  container = container_values
@@ -353,6 +363,7 @@ def _iter_coverage_cases(
353
363
  case.meta = _make_meta(
354
364
  description=description,
355
365
  location=_location,
366
+ parameter=_parameter,
356
367
  parameter_location=_location,
357
368
  )
358
369
  return case
@@ -377,12 +388,12 @@ def _iter_coverage_cases(
377
388
  coverage.CoverageContext(data_generation_methods=[DataGenerationMethod.negative]),
378
389
  subschema,
379
390
  ):
380
- yield make_case(more.value, more.description, _location, _container_name)
391
+ yield make_case(more.value, more.description, _location, _container_name, more.parameter)
381
392
 
382
393
  # 1. Generate only required properties
383
394
  if required and all_params != required:
384
395
  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)
396
+ yield make_case(only_required, "Only required properties", location, container_name, None)
386
397
  if DataGenerationMethod.negative in data_generation_methods:
387
398
  subschema = _combination_schema(only_required, required, parameter_set)
388
399
  yield from _yield_negative(subschema, location, container_name)
@@ -391,7 +402,9 @@ def _iter_coverage_cases(
391
402
  for opt_param in optional:
392
403
  combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
393
404
  if combo != base_container:
394
- yield make_case(combo, f"All required properties and optional '{opt_param}'", location, container_name)
405
+ yield make_case(
406
+ combo, f"All required properties and optional '{opt_param}'", location, container_name, None
407
+ )
395
408
  if DataGenerationMethod.negative in data_generation_methods:
396
409
  subschema = _combination_schema(combo, required, parameter_set)
397
410
  yield from _yield_negative(subschema, location, container_name)
@@ -402,7 +415,9 @@ def _iter_coverage_cases(
402
415
  for combination in combinations(optional, size):
403
416
  combo = {k: v for k, v in base_container.items() if k in required or k in combination}
404
417
  if combo != base_container:
405
- yield make_case(combo, f"All required and {size} optional properties", location, container_name)
418
+ yield make_case(
419
+ combo, f"All required and {size} optional properties", location, container_name, None
420
+ )
406
421
 
407
422
 
408
423
  def _make_meta(
@@ -39,7 +39,7 @@ NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
39
39
  JSON_STRATEGY: st.SearchStrategy = st.recursive(
40
40
  st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(), json_recursive_strategy
41
41
  )
42
- ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY)
42
+ ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2)
43
43
  OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
44
44
 
45
45
 
@@ -63,23 +63,31 @@ class GeneratedValue:
63
63
  value: Any
64
64
  data_generation_method: DataGenerationMethod
65
65
  description: str
66
+ parameter: str | None
66
67
  location: str | None
67
68
 
68
- __slots__ = ("value", "data_generation_method", "description", "location")
69
+ __slots__ = ("value", "data_generation_method", "description", "parameter", "location")
69
70
 
70
71
  @classmethod
71
72
  def with_positive(cls, value: Any, *, description: str) -> GeneratedValue:
72
73
  return cls(
73
- value=value, data_generation_method=DataGenerationMethod.positive, description=description, location=None
74
+ value=value,
75
+ data_generation_method=DataGenerationMethod.positive,
76
+ description=description,
77
+ location=None,
78
+ parameter=None,
74
79
  )
75
80
 
76
81
  @classmethod
77
- def with_negative(cls, value: Any, *, description: str, location: str) -> GeneratedValue:
82
+ def with_negative(
83
+ cls, value: Any, *, description: str, location: str, parameter: str | None = None
84
+ ) -> GeneratedValue:
78
85
  return cls(
79
86
  value=value,
80
87
  data_generation_method=DataGenerationMethod.negative,
81
88
  description=description,
82
89
  location=location,
90
+ parameter=parameter,
83
91
  )
84
92
 
85
93
 
@@ -218,11 +226,11 @@ def _encode(o: Any) -> str:
218
226
  return "".join(_iterencode(o, 0))
219
227
 
220
228
 
221
- def _to_hashable_key(value: T, _encode: Callable = _encode) -> T | tuple[type, str]:
229
+ def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str | T]:
222
230
  if isinstance(value, (dict, list)):
223
231
  serialized = _encode(value)
224
232
  return (type(value), serialized)
225
- return value
233
+ return (type(value), value)
226
234
 
227
235
 
228
236
  def _cover_positive_for_type(
@@ -425,7 +433,8 @@ def cover_schema_iter(
425
433
  elif key == "allOf":
426
434
  nctx = ctx.with_negative()
427
435
  if len(value) == 1:
428
- yield from cover_schema_iter(nctx, value[0], seen)
436
+ with nctx.location(0):
437
+ yield from cover_schema_iter(nctx, value[0], seen)
429
438
  else:
430
439
  with _ignore_unfixable():
431
440
  canonical = canonicalish(schema)
@@ -755,6 +764,7 @@ def _negative_properties(
755
764
  {**template, key: value.value},
756
765
  description=f"Object with invalid '{key}' value: {value.description}",
757
766
  location=nctx.current_location,
767
+ parameter=key,
758
768
  )
759
769
 
760
770
 
@@ -391,14 +391,14 @@ def ignored_auth(ctx: CheckContext, response: GenericResponse, case: Case) -> bo
391
391
  # Check if invalid auth will give an error
392
392
  _remove_auth_from_case(case, security_parameters)
393
393
  new_response = case.operation.schema.transport.send(case)
394
- if 200 <= new_response.status_code < 300:
394
+ if new_response.status_code != 401:
395
395
  _update_response(response, new_response)
396
396
  _raise_no_auth_error(new_response, case.operation.verbose_name, "that requires authentication")
397
397
  # Try to set invalid auth and check if it succeeds
398
398
  for parameter in security_parameters:
399
399
  _set_auth_for_case(case, parameter)
400
400
  new_response = case.operation.schema.transport.send(case)
401
- if 200 <= new_response.status_code < 300:
401
+ if new_response.status_code != 401:
402
402
  _update_response(response, new_response)
403
403
  _raise_no_auth_error(new_response, case.operation.verbose_name, "with any auth")
404
404
  _remove_auth_from_case(case, security_parameters)
@@ -44,21 +44,38 @@ def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, m
44
44
  if parsed[0][0] == ANCHOR:
45
45
  # Starts with an anchor
46
46
  op, value = parsed[1]
47
- leading_anchor = pattern[0]
48
- return leading_anchor + _update_quantifier(op, value, pattern[1:], min_length, max_length)
47
+ anchor_length = _get_anchor_length(parsed[0][1])
48
+ leading_anchor = pattern[:anchor_length]
49
+ return leading_anchor + _update_quantifier(op, value, pattern[anchor_length:], min_length, max_length)
49
50
  if parsed[1][0] == ANCHOR:
50
51
  # Ends with an anchor
51
52
  op, value = parsed[0]
52
- trailing_anchor = pattern[-1]
53
- return _update_quantifier(op, value, pattern[:-1], min_length, max_length) + trailing_anchor
53
+ anchor_length = _get_anchor_length(parsed[1][1])
54
+ trailing_anchor = pattern[-anchor_length:]
55
+ return _update_quantifier(op, value, pattern[:-anchor_length], min_length, max_length) + trailing_anchor
54
56
  elif len(parsed) == 3 and parsed[0][0] == ANCHOR and parsed[2][0] == ANCHOR:
55
57
  op, value = parsed[1]
56
- leading_anchor = pattern[0]
57
- trailing_anchor = pattern[-1]
58
- return leading_anchor + _update_quantifier(op, value, pattern[1:-1], min_length, max_length) + trailing_anchor
58
+ leading_anchor_length = _get_anchor_length(parsed[0][1])
59
+ trailing_anchor_length = _get_anchor_length(parsed[2][1])
60
+ leading_anchor = pattern[:leading_anchor_length]
61
+ trailing_anchor = pattern[-trailing_anchor_length:]
62
+ return (
63
+ leading_anchor
64
+ + _update_quantifier(
65
+ op, value, pattern[leading_anchor_length:-trailing_anchor_length], min_length, max_length
66
+ )
67
+ + trailing_anchor
68
+ )
59
69
  return pattern
60
70
 
61
71
 
72
+ def _get_anchor_length(node_type: int) -> int:
73
+ """Determine the length of the anchor based on its type."""
74
+ if node_type in {sre.AT_BEGINNING_STRING, sre.AT_END_STRING, sre.AT_BOUNDARY, sre.AT_NON_BOUNDARY}:
75
+ return 2 # \A, \Z, \b, or \B
76
+ return 1 # ^ or $ or their multiline/locale/unicode variants
77
+
78
+
62
79
  def _update_quantifier(op: int, value: tuple, pattern: str, min_length: int | None, max_length: int | None) -> str:
63
80
  """Update the quantifier based on the operation type and given constraints."""
64
81
  if op in REPEATS:
@@ -113,7 +130,7 @@ def _strip_quantifier(pattern: str) -> str:
113
130
  return pattern[:-2]
114
131
  if pattern.endswith(("?", "*", "+")):
115
132
  pattern = pattern[:-1]
116
- if pattern.endswith("}"):
133
+ if pattern.endswith("}") and "{" in pattern:
117
134
  # Find the start of the exact quantifier and drop everything since that index
118
135
  idx = pattern.rfind("{")
119
136
  pattern = pattern[:idx]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: schemathesis
3
- Version: 3.38.5
3
+ Version: 3.38.6
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
@@ -9,8 +9,7 @@ Project-URL: Funding, https://github.com/sponsors/Stranger6667
9
9
  Project-URL: Source Code, https://github.com/schemathesis/schemathesis
10
10
  Author-email: Dmitry Dygalo <dmitry@dygalo.dev>
11
11
  Maintainer-email: Dmitry Dygalo <dmitry@dygalo.dev>
12
- License-Expression: MIT
13
- License-File: LICENSE
12
+ License: MIT
14
13
  Keywords: graphql,hypothesis,openapi,pytest,testing
15
14
  Classifier: Development Status :: 5 - Production/Stable
16
15
  Classifier: Environment :: Console
@@ -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=lm0J9uQFENfm7-6_wEaNKshGF1wmZebfZJ9G_sUhlqE,20839
4
+ schemathesis/_hypothesis.py,sha256=SHFQKVeKsKX55D7Rfb2DTG7gq7h-jLXMGGD8RG81ac4,21339
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
@@ -61,7 +61,7 @@ schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZE
61
61
  schemathesis/generation/__init__.py,sha256=29Zys_tD6kfngaC4zHeC6TOBZQcmo7CWm7KDSYsHStQ,1581
62
62
  schemathesis/generation/_hypothesis.py,sha256=74fzLPHugZgMQXerWYFAMqCAjtAXz5E4gek7Gnkhli4,1756
63
63
  schemathesis/generation/_methods.py,sha256=r8oVlJ71_gXcnEhU-byw2E0R2RswQQFm8U7yGErSqbw,1204
64
- schemathesis/generation/coverage.py,sha256=hMtISl3fYaTmE78sFCBd42BPJfPvgSEZJztlXnKxTRo,38310
64
+ schemathesis/generation/coverage.py,sha256=6VCaQ8bUtBmbG-DCOS11rXHA-s6hiIzVtbXAB0pYMZU,38604
65
65
  schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
66
66
  schemathesis/internal/checks.py,sha256=SBx2gesB-XzgVSMX_u7Mb416jSxJ68eQKtcdkWlkyOo,2441
67
67
  schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
@@ -107,7 +107,7 @@ schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzM
107
107
  schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
108
108
  schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
109
109
  schemathesis/specs/openapi/_hypothesis.py,sha256=nU8UDn1PzGCre4IVmwIuO9-CZv1KJe1fYY0d2BojhSo,22981
110
- schemathesis/specs/openapi/checks.py,sha256=ugo7-6iAu2fX8OmQgyvSunMO9Nt9cr6PNdT3vyX8Z4I,23914
110
+ schemathesis/specs/openapi/checks.py,sha256=NZwcqmLnINq3rUUUmdof0GGB0AjaqSF5dOQy08nsJPg,23902
111
111
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
112
112
  schemathesis/specs/openapi/converter.py,sha256=Yxw9lS_JKEyi-oJuACT07fm04bqQDlAu-iHwzkeDvE4,3546
113
113
  schemathesis/specs/openapi/definitions.py,sha256=WTkWwCgTc3OMxfKsqh6YDoGfZMTThSYrHGp8h0vLAK0,93935
@@ -117,7 +117,7 @@ schemathesis/specs/openapi/links.py,sha256=C4Uir2P_EcpqME8ee_a1vdUM8Tm3ZcKNn2YsG
117
117
  schemathesis/specs/openapi/loaders.py,sha256=5B1cgYEBj3h2psPQxzrQ5Xq5owLVGw-u9HsCQIx7yFE,25705
118
118
  schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
119
119
  schemathesis/specs/openapi/parameters.py,sha256=LUahlWKCDSlp94v2IA1Q90pyeECgO6FmrqbzCU-9Z0Y,14658
120
- schemathesis/specs/openapi/patterns.py,sha256=IK2BkXI1xByEz5if6jvydFE07nq5rDa4k_-2xX7ifG8,4715
120
+ schemathesis/specs/openapi/patterns.py,sha256=aEOiJeqI_qcE9bE2Viz6TUA8UppiTHm6QFxrLJryag8,5520
121
121
  schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
122
122
  schemathesis/specs/openapi/schemas.py,sha256=MLU2h9DrQNCDkk74MFFSj-8BsKjkJsf9lJQHPxLFVps,53845
123
123
  schemathesis/specs/openapi/security.py,sha256=Z-6pk2Ga1PTUtBe298KunjVHsNh5A-teegeso7zcPIE,7138
@@ -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.5.dist-info/METADATA,sha256=Bd928CBGxx6iB8XZyXL8M1VR85eXY2wUm9h2o5uwuEg,12956
157
- schemathesis-3.38.5.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
158
- schemathesis-3.38.5.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
- schemathesis-3.38.5.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
- schemathesis-3.38.5.dist-info/RECORD,,
156
+ schemathesis-3.38.6.dist-info/METADATA,sha256=CbrAdVVhZVREx6pwkPRARvVXqETpf8YE34flEN3Tr4I,12923
157
+ schemathesis-3.38.6.dist-info/WHEEL,sha256=WJ9WQ4-pUYxfD_tEj5GvKSG9KSULNumqkTQdolV8mME,87
158
+ schemathesis-3.38.6.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
+ schemathesis-3.38.6.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
+ schemathesis-3.38.6.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.26.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any