schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__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 (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1766
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{cli → engine/phases}/probes.py +63 -70
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +153 -39
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +483 -367
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -55
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -765
  156. schemathesis/cli/output/short.py +0 -40
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -315
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -184
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,931 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import re
5
+ from contextlib import contextmanager, suppress
6
+ from dataclasses import dataclass
7
+ from functools import lru_cache, partial
8
+ from itertools import combinations
9
+ from json.encoder import _make_iterencode, c_make_encoder, encode_basestring_ascii # type: ignore
10
+ from typing import Any, Callable, Generator, Iterator, TypeVar, cast
11
+
12
+ import jsonschema
13
+ from hypothesis import strategies as st
14
+ from hypothesis.errors import InvalidArgument, Unsatisfiable
15
+ from hypothesis_jsonschema import from_schema
16
+ from hypothesis_jsonschema._canonicalise import canonicalish
17
+ from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING_FORMATS
18
+
19
+ from schemathesis.core import NOT_SET
20
+ from schemathesis.core.compat import RefResolutionError
21
+ from schemathesis.core.transforms import deepclone
22
+ from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
23
+ from schemathesis.generation import GenerationMode
24
+ from schemathesis.generation.hypothesis import examples
25
+
26
+ from ..specs.openapi.converter import update_pattern_in_schema
27
+ from ..specs.openapi.formats import STRING_FORMATS, get_default_format_strategies
28
+ from ..specs.openapi.patterns import update_quantifier
29
+
30
+
31
+ def _replace_zero_with_nonzero(x: float) -> float:
32
+ return x or 0.0
33
+
34
+
35
+ def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
36
+ return st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3)
37
+
38
+
39
+ BUFFER_SIZE = 8 * 1024
40
+ FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
41
+ NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
42
+ JSON_STRATEGY: st.SearchStrategy = st.recursive(
43
+ st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(), json_recursive_strategy
44
+ )
45
+ ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2)
46
+ OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
47
+
48
+
49
+ STRATEGIES_FOR_TYPE = {
50
+ "integer": st.integers(),
51
+ "number": NUMERIC_STRATEGY,
52
+ "boolean": st.booleans(),
53
+ "null": st.none(),
54
+ "string": st.text(),
55
+ "array": ARRAY_STRATEGY,
56
+ "object": OBJECT_STRATEGY,
57
+ }
58
+ FORMAT_STRATEGIES = {**BUILT_IN_STRING_FORMATS, **get_default_format_strategies(), **STRING_FORMATS}
59
+
60
+ UNKNOWN_PROPERTY_KEY = "x-schemathesis-unknown-property"
61
+ UNKNOWN_PROPERTY_VALUE = 42
62
+
63
+
64
+ @dataclass
65
+ class GeneratedValue:
66
+ value: Any
67
+ generation_mode: GenerationMode
68
+ description: str
69
+ parameter: str | None
70
+ location: str | None
71
+
72
+ __slots__ = ("value", "generation_mode", "description", "parameter", "location")
73
+
74
+ @classmethod
75
+ def with_positive(cls, value: Any, *, description: str) -> GeneratedValue:
76
+ return cls(
77
+ value=value,
78
+ generation_mode=GenerationMode.POSITIVE,
79
+ description=description,
80
+ location=None,
81
+ parameter=None,
82
+ )
83
+
84
+ @classmethod
85
+ def with_negative(
86
+ cls, value: Any, *, description: str, location: str, parameter: str | None = None
87
+ ) -> GeneratedValue:
88
+ return cls(
89
+ value=value,
90
+ generation_mode=GenerationMode.NEGATIVE,
91
+ description=description,
92
+ location=location,
93
+ parameter=parameter,
94
+ )
95
+
96
+
97
+ PositiveValue = GeneratedValue.with_positive
98
+ NegativeValue = GeneratedValue.with_negative
99
+
100
+
101
+ @lru_cache(maxsize=128)
102
+ def cached_draw(strategy: st.SearchStrategy) -> Any:
103
+ return examples.generate_one(strategy)
104
+
105
+
106
+ @dataclass
107
+ class CoverageContext:
108
+ generation_modes: list[GenerationMode]
109
+ location: str
110
+ path: list[str | int]
111
+
112
+ __slots__ = ("location", "generation_modes", "path")
113
+
114
+ def __init__(
115
+ self,
116
+ *,
117
+ location: str,
118
+ generation_modes: list[GenerationMode] | None = None,
119
+ path: list[str | int] | None = None,
120
+ ) -> None:
121
+ self.location = location
122
+ self.generation_modes = generation_modes if generation_modes is not None else GenerationMode.all()
123
+ self.path = path or []
124
+
125
+ @contextmanager
126
+ def at(self, key: str | int) -> Generator[None, None, None]:
127
+ self.path.append(key)
128
+ try:
129
+ yield
130
+ finally:
131
+ self.path.pop()
132
+
133
+ @property
134
+ def current_path(self) -> str:
135
+ return "/" + "/".join(str(key) for key in self.path)
136
+
137
+ def with_positive(self) -> CoverageContext:
138
+ return CoverageContext(
139
+ location=self.location,
140
+ generation_modes=[GenerationMode.POSITIVE],
141
+ path=self.path,
142
+ )
143
+
144
+ def with_negative(self) -> CoverageContext:
145
+ return CoverageContext(
146
+ location=self.location,
147
+ generation_modes=[GenerationMode.NEGATIVE],
148
+ path=self.path,
149
+ )
150
+
151
+ def is_valid_for_location(self, value: Any) -> bool:
152
+ if self.location in ("header", "cookie") and isinstance(value, str):
153
+ return is_latin_1_encodable(value) and not has_invalid_characters("", value)
154
+ return True
155
+
156
+ def generate_from(self, strategy: st.SearchStrategy) -> Any:
157
+ return cached_draw(strategy)
158
+
159
+ def generate_from_schema(self, schema: dict | bool) -> Any:
160
+ if isinstance(schema, bool):
161
+ return 0
162
+ keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example"]])
163
+ if keys == ["type"] and isinstance(schema["type"], str) and schema["type"] in STRATEGIES_FOR_TYPE:
164
+ return cached_draw(STRATEGIES_FOR_TYPE[schema["type"]])
165
+ if keys == ["format", "type"]:
166
+ if schema["type"] != "string":
167
+ return cached_draw(STRATEGIES_FOR_TYPE[schema["type"]])
168
+ elif schema["format"] in FORMAT_STRATEGIES:
169
+ return cached_draw(FORMAT_STRATEGIES[schema["format"]])
170
+ if (keys == ["maxLength", "minLength", "type"] or keys == ["maxLength", "type"]) and schema["type"] == "string":
171
+ return cached_draw(st.text(min_size=schema.get("minLength", 0), max_size=schema["maxLength"]))
172
+ if (
173
+ keys == ["properties", "required", "type"]
174
+ or keys == ["properties", "required"]
175
+ or keys == ["properties", "type"]
176
+ or keys == ["properties"]
177
+ ):
178
+ obj = {}
179
+ for key, sub_schema in schema["properties"].items():
180
+ if isinstance(sub_schema, dict) and "const" in sub_schema:
181
+ obj[key] = sub_schema["const"]
182
+ else:
183
+ obj[key] = self.generate_from_schema(sub_schema)
184
+ return obj
185
+ if (
186
+ keys == ["maximum", "minimum", "type"] or keys == ["maximum", "type"] or keys == ["minimum", "type"]
187
+ ) and schema["type"] == "integer":
188
+ return cached_draw(st.integers(min_value=schema.get("minimum"), max_value=schema.get("maximum")))
189
+ if "enum" in schema:
190
+ return cached_draw(st.sampled_from(schema["enum"]))
191
+ if "pattern" in schema:
192
+ pattern = schema["pattern"]
193
+ try:
194
+ re.compile(pattern)
195
+ except re.error:
196
+ raise Unsatisfiable from None
197
+ return cached_draw(st.from_regex(pattern))
198
+ if (keys == ["items", "type"] or keys == ["items", "minItems", "type"]) and isinstance(schema["items"], dict):
199
+ items = schema["items"]
200
+ min_items = schema.get("minItems", 0)
201
+ if "enum" in items:
202
+ return cached_draw(st.lists(st.sampled_from(items["enum"]), min_size=min_items))
203
+ sub_keys = sorted([k for k in items if not k.startswith("x-") and k not in ["description", "example"]])
204
+ if sub_keys == ["type"] and items["type"] == "string":
205
+ return cached_draw(st.lists(st.text(), min_size=min_items))
206
+ if (
207
+ sub_keys == ["properties", "required", "type"]
208
+ or sub_keys == ["properties", "type"]
209
+ or sub_keys == ["properties"]
210
+ ):
211
+ return cached_draw(
212
+ st.lists(
213
+ st.fixed_dictionaries(
214
+ {key: from_schema(sub_schema) for key, sub_schema in items["properties"].items()}
215
+ ),
216
+ min_size=min_items,
217
+ )
218
+ )
219
+
220
+ if keys == ["allOf"]:
221
+ schema = canonicalish(schema)
222
+ if isinstance(schema, dict) and "allOf" not in schema:
223
+ return self.generate_from_schema(schema)
224
+
225
+ return self.generate_from(from_schema(schema))
226
+
227
+
228
+ T = TypeVar("T")
229
+
230
+
231
+ if c_make_encoder is not None:
232
+ _iterencode = c_make_encoder(None, None, encode_basestring_ascii, None, ":", ",", True, False, False)
233
+ else:
234
+ _iterencode = _make_iterencode(
235
+ None, None, encode_basestring_ascii, None, float.__repr__, ":", ",", True, False, True
236
+ )
237
+
238
+
239
+ def _encode(o: Any) -> str:
240
+ return "".join(_iterencode(o, 0))
241
+
242
+
243
+ def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str | T]:
244
+ if isinstance(value, (dict, list)):
245
+ serialized = _encode(value)
246
+ return (type(value), serialized)
247
+ return (type(value), value)
248
+
249
+
250
+ def _cover_positive_for_type(
251
+ ctx: CoverageContext, schema: dict, ty: str | None
252
+ ) -> Generator[GeneratedValue, None, None]:
253
+ if ty == "object" or ty == "array":
254
+ template_schema = _get_template_schema(schema, ty)
255
+ template = ctx.generate_from_schema(template_schema)
256
+ else:
257
+ template = None
258
+ if GenerationMode.POSITIVE in ctx.generation_modes:
259
+ ctx = ctx.with_positive()
260
+ enum = schema.get("enum", NOT_SET)
261
+ const = schema.get("const", NOT_SET)
262
+ for key in ("anyOf", "oneOf"):
263
+ sub_schemas = schema.get(key)
264
+ if sub_schemas is not None:
265
+ for sub_schema in sub_schemas:
266
+ yield from cover_schema_iter(ctx, sub_schema)
267
+ all_of = schema.get("allOf")
268
+ if all_of is not None:
269
+ if len(all_of) == 1:
270
+ yield from cover_schema_iter(ctx, all_of[0])
271
+ else:
272
+ with suppress(jsonschema.SchemaError):
273
+ canonical = canonicalish(schema)
274
+ yield from cover_schema_iter(ctx, canonical)
275
+ if enum is not NOT_SET:
276
+ for value in enum:
277
+ yield PositiveValue(value, description="Enum value")
278
+ elif const is not NOT_SET:
279
+ yield PositiveValue(const, description="Const value")
280
+ elif ty is not None:
281
+ if ty == "null":
282
+ yield PositiveValue(None, description="Value null value")
283
+ elif ty == "boolean":
284
+ yield PositiveValue(True, description="Valid boolean value")
285
+ yield PositiveValue(False, description="Valid boolean value")
286
+ elif ty == "string":
287
+ yield from _positive_string(ctx, schema)
288
+ elif ty == "integer" or ty == "number":
289
+ yield from _positive_number(ctx, schema)
290
+ elif ty == "array":
291
+ yield from _positive_array(ctx, schema, cast(list, template))
292
+ elif ty == "object":
293
+ yield from _positive_object(ctx, schema, cast(dict, template))
294
+
295
+
296
+ @contextmanager
297
+ def _ignore_unfixable(
298
+ *,
299
+ # Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
300
+ # and it may cause errors during the interpreter shutdown
301
+ ref_error: type[Exception] = RefResolutionError,
302
+ schema_error: type[Exception] = jsonschema.SchemaError,
303
+ ) -> Generator:
304
+ try:
305
+ yield
306
+ except (Unsatisfiable, ref_error, schema_error):
307
+ pass
308
+ except InvalidArgument as exc:
309
+ message = str(exc)
310
+ if "Cannot create non-empty" not in message and "is not in the specified alphabet" not in message:
311
+ raise
312
+ except TypeError as exc:
313
+ if "first argument must be string or compiled pattern" not in str(exc):
314
+ raise
315
+
316
+
317
+ def cover_schema_iter(
318
+ ctx: CoverageContext, schema: dict | bool, seen: set[Any | tuple[type, str]] | None = None
319
+ ) -> Generator[GeneratedValue, None, None]:
320
+ if seen is None:
321
+ seen = set()
322
+ if isinstance(schema, bool):
323
+ types = ["null", "boolean", "string", "number", "array", "object"]
324
+ schema = {}
325
+ else:
326
+ types = schema.get("type", [])
327
+ push_examples_to_properties(schema)
328
+ if not isinstance(types, list):
329
+ types = [types] # type: ignore[unreachable]
330
+ if not types:
331
+ with _ignore_unfixable():
332
+ yield from _cover_positive_for_type(ctx, schema, None)
333
+ for ty in types:
334
+ with _ignore_unfixable():
335
+ yield from _cover_positive_for_type(ctx, schema, ty)
336
+ if GenerationMode.NEGATIVE in ctx.generation_modes:
337
+ template = None
338
+ for key, value in schema.items():
339
+ with _ignore_unfixable(), ctx.at(key):
340
+ if key == "enum":
341
+ yield from _negative_enum(ctx, value, seen)
342
+ elif key == "const":
343
+ for value_ in _negative_enum(ctx, [value], seen):
344
+ k = _to_hashable_key(value_.value)
345
+ if k not in seen:
346
+ yield value_
347
+ seen.add(k)
348
+ elif key == "type":
349
+ yield from _negative_type(ctx, seen, value)
350
+ elif key == "properties":
351
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
352
+ yield from _negative_properties(ctx, template, value)
353
+ elif key == "patternProperties":
354
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
355
+ yield from _negative_pattern_properties(ctx, template, value)
356
+ elif key == "items" and isinstance(value, dict):
357
+ yield from _negative_items(ctx, value)
358
+ elif key == "pattern":
359
+ min_length = schema.get("minLength")
360
+ max_length = schema.get("maxLength")
361
+ yield from _negative_pattern(ctx, value, min_length=min_length, max_length=max_length)
362
+ elif key == "format" and ("string" in types or not types):
363
+ yield from _negative_format(ctx, schema, value)
364
+ elif key == "maximum":
365
+ next = value + 1
366
+ if next not in seen:
367
+ yield NegativeValue(next, description="Value greater than maximum", location=ctx.current_path)
368
+ seen.add(next)
369
+ elif key == "minimum":
370
+ next = value - 1
371
+ if next not in seen:
372
+ yield NegativeValue(next, description="Value smaller than minimum", location=ctx.current_path)
373
+ seen.add(next)
374
+ elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
375
+ verb = "greater" if key == "exclusiveMaximum" else "smaller"
376
+ limit = "maximum" if key == "exclusiveMaximum" else "minimum"
377
+ yield NegativeValue(value, description=f"Value {verb} than {limit}", location=ctx.current_path)
378
+ seen.add(value)
379
+ elif key == "multipleOf":
380
+ for value_ in _negative_multiple_of(ctx, schema, value):
381
+ k = _to_hashable_key(value_.value)
382
+ if k not in seen:
383
+ yield value_
384
+ seen.add(k)
385
+ elif key == "minLength" and 0 < value < BUFFER_SIZE:
386
+ with suppress(InvalidArgument):
387
+ min_length = max_length = value - 1
388
+ new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
389
+ new_schema.setdefault("type", "string")
390
+ if "pattern" in new_schema:
391
+ new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
392
+ if new_schema["pattern"] == schema["pattern"]:
393
+ # Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
394
+ del new_schema["minLength"]
395
+ del new_schema["maxLength"]
396
+ value = ctx.generate_from_schema(new_schema)[:max_length]
397
+ else:
398
+ value = ctx.generate_from_schema(new_schema)
399
+ else:
400
+ value = ctx.generate_from_schema(new_schema)
401
+ k = _to_hashable_key(value)
402
+ if k not in seen:
403
+ yield NegativeValue(
404
+ value, description="String smaller than minLength", location=ctx.current_path
405
+ )
406
+ seen.add(k)
407
+ elif key == "maxLength" and value < BUFFER_SIZE:
408
+ try:
409
+ min_length = max_length = value + 1
410
+ new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
411
+ new_schema.setdefault("type", "string")
412
+ if "pattern" in new_schema:
413
+ new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
414
+ if new_schema["pattern"] == schema["pattern"]:
415
+ # Pattern wasn't updated, try to generate a valid value then extend the string to the required length
416
+ del new_schema["minLength"]
417
+ del new_schema["maxLength"]
418
+ value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
419
+ else:
420
+ value = ctx.generate_from_schema(new_schema)
421
+ else:
422
+ value = ctx.generate_from_schema(new_schema)
423
+ k = _to_hashable_key(value)
424
+ if k not in seen:
425
+ yield NegativeValue(
426
+ value, description="String larger than maxLength", location=ctx.current_path
427
+ )
428
+ seen.add(k)
429
+ except (InvalidArgument, Unsatisfiable):
430
+ pass
431
+ elif key == "uniqueItems" and value:
432
+ yield from _negative_unique_items(ctx, schema)
433
+ elif key == "required":
434
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
435
+ yield from _negative_required(ctx, template, value)
436
+ elif key == "additionalProperties" and not value and "pattern" not in schema:
437
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
438
+ yield NegativeValue(
439
+ {**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
440
+ description="Object with unexpected properties",
441
+ location=ctx.current_path,
442
+ )
443
+ elif key == "allOf":
444
+ nctx = ctx.with_negative()
445
+ if len(value) == 1:
446
+ with nctx.at(0):
447
+ yield from cover_schema_iter(nctx, value[0], seen)
448
+ else:
449
+ with _ignore_unfixable():
450
+ canonical = canonicalish(schema)
451
+ yield from cover_schema_iter(nctx, canonical, seen)
452
+ elif key == "anyOf" or key == "oneOf":
453
+ nctx = ctx.with_negative()
454
+ # NOTE: Other sub-schemas are not filtered out
455
+ for idx, sub_schema in enumerate(value):
456
+ with nctx.at(idx):
457
+ yield from cover_schema_iter(nctx, sub_schema, seen)
458
+
459
+
460
+ def _get_properties(schema: dict | bool) -> dict | bool:
461
+ if isinstance(schema, dict):
462
+ if "example" in schema:
463
+ return {"const": schema["example"]}
464
+ if "default" in schema:
465
+ return {"const": schema["default"]}
466
+ if schema.get("examples"):
467
+ return {"enum": schema["examples"]}
468
+ if schema.get("type") == "object":
469
+ return _get_template_schema(schema, "object")
470
+ _schema = deepclone(schema)
471
+ update_pattern_in_schema(_schema)
472
+ return _schema
473
+ return schema
474
+
475
+
476
+ def _get_template_schema(schema: dict, ty: str) -> dict:
477
+ if ty == "object":
478
+ properties = schema.get("properties")
479
+ if properties is not None:
480
+ return {
481
+ **schema,
482
+ "required": list(properties),
483
+ "type": ty,
484
+ "properties": {k: _get_properties(v) for k, v in properties.items()},
485
+ }
486
+ return {**schema, "type": ty}
487
+
488
+
489
+ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
490
+ """Generate positive string values."""
491
+ # Boundary and near boundary values
492
+ min_length = schema.get("minLength")
493
+ if min_length == 0:
494
+ min_length = None
495
+ max_length = schema.get("maxLength")
496
+ example = schema.get("example")
497
+ examples = schema.get("examples")
498
+ default = schema.get("default")
499
+ if example or examples or default:
500
+ if example and ctx.is_valid_for_location(example):
501
+ yield PositiveValue(example, description="Example value")
502
+ if examples:
503
+ for example in examples:
504
+ if ctx.is_valid_for_location(example):
505
+ yield PositiveValue(example, description="Example value")
506
+ if (
507
+ default
508
+ and not (example is not None and default == example)
509
+ and not (examples is not None and any(default == ex for ex in examples))
510
+ and ctx.is_valid_for_location(default)
511
+ ):
512
+ yield PositiveValue(default, description="Default value")
513
+ elif not min_length and not max_length:
514
+ # Default positive value
515
+ yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
516
+ elif "pattern" in schema:
517
+ # Without merging `maxLength` & `minLength` into a regex it is problematic
518
+ # to generate a valid value as the unredlying machinery will resort to filtering
519
+ # and it is unlikely that it will generate a string of that length
520
+ yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
521
+ return
522
+
523
+ seen = set()
524
+
525
+ if min_length is not None and min_length < BUFFER_SIZE:
526
+ # Exactly the minimum length
527
+ yield PositiveValue(
528
+ ctx.generate_from_schema({**schema, "maxLength": min_length}), description="Minimum length string"
529
+ )
530
+ seen.add(min_length)
531
+
532
+ # One character more than minimum if possible
533
+ larger = min_length + 1
534
+ if larger < BUFFER_SIZE and larger not in seen and (not max_length or larger <= max_length):
535
+ yield PositiveValue(
536
+ ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger}),
537
+ description="Near-boundary length string",
538
+ )
539
+ seen.add(larger)
540
+
541
+ if max_length is not None:
542
+ # Exactly the maximum length
543
+ if max_length < BUFFER_SIZE and max_length not in seen:
544
+ yield PositiveValue(
545
+ ctx.generate_from_schema({**schema, "minLength": max_length}), description="Maximum length string"
546
+ )
547
+ seen.add(max_length)
548
+
549
+ # One character less than maximum if possible
550
+ smaller = max_length - 1
551
+ if (
552
+ smaller < BUFFER_SIZE
553
+ and smaller not in seen
554
+ and (smaller > 0 and (min_length is None or smaller >= min_length))
555
+ ):
556
+ yield PositiveValue(
557
+ ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller}),
558
+ description="Near-boundary length string",
559
+ )
560
+ seen.add(smaller)
561
+
562
+
563
+ def closest_multiple_greater_than(y: int, x: int) -> int:
564
+ """Find the closest multiple of X that is greater than Y."""
565
+ quotient, remainder = divmod(y, x)
566
+ if remainder == 0:
567
+ return y
568
+ return x * (quotient + 1)
569
+
570
+
571
+ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
572
+ """Generate positive integer values."""
573
+ # Boundary and near boundary values
574
+ minimum = schema.get("minimum")
575
+ maximum = schema.get("maximum")
576
+ exclusive_minimum = schema.get("exclusiveMinimum")
577
+ exclusive_maximum = schema.get("exclusiveMaximum")
578
+ if exclusive_minimum is not None:
579
+ minimum = exclusive_minimum + 1
580
+ if exclusive_maximum is not None:
581
+ maximum = exclusive_maximum - 1
582
+ multiple_of = schema.get("multipleOf")
583
+ example = schema.get("example")
584
+ examples = schema.get("examples")
585
+ default = schema.get("default")
586
+
587
+ if example or examples or default:
588
+ if example:
589
+ yield PositiveValue(example, description="Example value")
590
+ if examples:
591
+ for example in examples:
592
+ yield PositiveValue(example, description="Example value")
593
+ if (
594
+ default
595
+ and not (example is not None and default == example)
596
+ and not (examples is not None and any(default == ex for ex in examples))
597
+ ):
598
+ yield PositiveValue(default, description="Default value")
599
+ elif not minimum and not maximum:
600
+ # Default positive value
601
+ yield PositiveValue(ctx.generate_from_schema(schema), description="Valid number")
602
+
603
+ seen = set()
604
+
605
+ if minimum is not None:
606
+ # Exactly the minimum
607
+ if multiple_of is not None:
608
+ smallest = closest_multiple_greater_than(minimum, multiple_of)
609
+ else:
610
+ smallest = minimum
611
+ seen.add(smallest)
612
+ yield PositiveValue(smallest, description="Minimum value")
613
+
614
+ # One more than minimum if possible
615
+ if multiple_of is not None:
616
+ larger = smallest + multiple_of
617
+ else:
618
+ larger = minimum + 1
619
+ if larger not in seen and (not maximum or larger <= maximum):
620
+ seen.add(larger)
621
+ yield PositiveValue(larger, description="Near-boundary number")
622
+
623
+ if maximum is not None:
624
+ # Exactly the maximum
625
+ if multiple_of is not None:
626
+ largest = maximum - (maximum % multiple_of)
627
+ else:
628
+ largest = maximum
629
+ if largest not in seen:
630
+ seen.add(largest)
631
+ yield PositiveValue(largest, description="Maximum value")
632
+
633
+ # One less than maximum if possible
634
+ if multiple_of is not None:
635
+ smaller = largest - multiple_of
636
+ else:
637
+ smaller = maximum - 1
638
+ if smaller not in seen and (smaller > 0 and (minimum is None or smaller >= minimum)):
639
+ seen.add(smaller)
640
+ yield PositiveValue(smaller, description="Near-boundary number")
641
+
642
+
643
+ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
644
+ seen = set()
645
+ example = schema.get("example")
646
+ examples = schema.get("examples")
647
+ default = schema.get("default")
648
+
649
+ if example or examples or default:
650
+ if example:
651
+ yield PositiveValue(example, description="Example value")
652
+ if examples:
653
+ for example in examples:
654
+ yield PositiveValue(example, description="Example value")
655
+ if (
656
+ default
657
+ and not (example is not None and default == example)
658
+ and not (examples is not None and any(default == ex for ex in examples))
659
+ ):
660
+ yield PositiveValue(default, description="Default value")
661
+ else:
662
+ yield PositiveValue(template, description="Valid array")
663
+ seen.add(len(template))
664
+
665
+ # Boundary and near-boundary sizes
666
+ min_items = schema.get("minItems")
667
+ max_items = schema.get("maxItems")
668
+ if min_items is not None:
669
+ # Do not generate an array with `minItems` length, because it is already covered by `template`
670
+
671
+ # One item more than minimum if possible
672
+ larger = min_items + 1
673
+ if larger not in seen and (max_items is None or larger <= max_items):
674
+ yield PositiveValue(
675
+ ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger}),
676
+ description="Near-boundary items array",
677
+ )
678
+ seen.add(larger)
679
+
680
+ if max_items is not None:
681
+ if max_items < BUFFER_SIZE and max_items not in seen:
682
+ yield PositiveValue(
683
+ ctx.generate_from_schema({**schema, "minItems": max_items}),
684
+ description="Maximum items array",
685
+ )
686
+ seen.add(max_items)
687
+
688
+ # One item smaller than maximum if possible
689
+ smaller = max_items - 1
690
+ if (
691
+ smaller < BUFFER_SIZE
692
+ and smaller > 0
693
+ and smaller not in seen
694
+ and (min_items is None or smaller >= min_items)
695
+ ):
696
+ yield PositiveValue(
697
+ ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller}),
698
+ description="Near-boundary items array",
699
+ )
700
+ seen.add(smaller)
701
+
702
+
703
+ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
704
+ example = schema.get("example")
705
+ examples = schema.get("examples")
706
+ default = schema.get("default")
707
+
708
+ if example or examples or default:
709
+ if example:
710
+ yield PositiveValue(example, description="Example value")
711
+ if examples:
712
+ for example in examples:
713
+ yield PositiveValue(example, description="Example value")
714
+ if (
715
+ default
716
+ and not (example is not None and default == example)
717
+ and not (examples is not None and any(default == ex for ex in examples))
718
+ ):
719
+ yield PositiveValue(default, description="Default value")
720
+ else:
721
+ yield PositiveValue(template, description="Valid object")
722
+
723
+ properties = schema.get("properties", {})
724
+ required = set(schema.get("required", []))
725
+ optional = list(set(properties) - required)
726
+ optional.sort()
727
+
728
+ # Generate combinations with required properties and one optional property
729
+ for name in optional:
730
+ combo = {k: v for k, v in template.items() if k in required or k == name}
731
+ if combo != template:
732
+ yield PositiveValue(combo, description=f"Object with all required properties and '{name}'")
733
+ # Generate one combination for each size from 2 to N-1
734
+ for selection in select_combinations(optional):
735
+ combo = {k: v for k, v in template.items() if k in required or k in selection}
736
+ yield PositiveValue(combo, description="Object with all required and a subset of optional properties")
737
+ # Generate only required properties
738
+ if set(properties) != required:
739
+ only_required = {k: v for k, v in template.items() if k in required}
740
+ yield PositiveValue(only_required, description="Object with only required properties")
741
+ seen = set()
742
+ for name, sub_schema in properties.items():
743
+ seen.add(_to_hashable_key(template.get(name)))
744
+ for new in cover_schema_iter(ctx, sub_schema):
745
+ key = _to_hashable_key(new.value)
746
+ if key not in seen:
747
+ yield PositiveValue(
748
+ {**template, name: new.value}, description=f"Object with valid '{name}' value: {new.description}"
749
+ )
750
+ seen.add(key)
751
+ seen.clear()
752
+
753
+
754
+ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
755
+ for size in range(2, len(optional)):
756
+ yield next(combinations(optional, size))
757
+
758
+
759
+ def _negative_enum(
760
+ ctx: CoverageContext, value: list, seen: set[Any | tuple[type, str]]
761
+ ) -> Generator[GeneratedValue, None, None]:
762
+ def is_not_in_value(x: Any) -> bool:
763
+ if x in value or not ctx.is_valid_for_location(x):
764
+ return False
765
+ _hashed = _to_hashable_key(x)
766
+ return _hashed not in seen
767
+
768
+ strategy = (st.none() | st.booleans() | NUMERIC_STRATEGY | st.text()).filter(is_not_in_value)
769
+ value = ctx.generate_from(strategy)
770
+ yield NegativeValue(value, description="Invalid enum value", location=ctx.current_path)
771
+ hashed = _to_hashable_key(value)
772
+ seen.add(hashed)
773
+
774
+
775
+ def _negative_properties(
776
+ ctx: CoverageContext, template: dict, properties: dict
777
+ ) -> Generator[GeneratedValue, None, None]:
778
+ nctx = ctx.with_negative()
779
+ for key, sub_schema in properties.items():
780
+ with nctx.at(key):
781
+ for value in cover_schema_iter(nctx, sub_schema):
782
+ yield NegativeValue(
783
+ {**template, key: value.value},
784
+ description=f"Object with invalid '{key}' value: {value.description}",
785
+ location=nctx.current_path,
786
+ parameter=key,
787
+ )
788
+
789
+
790
+ def _negative_pattern_properties(
791
+ ctx: CoverageContext, template: dict, pattern_properties: dict
792
+ ) -> Generator[GeneratedValue, None, None]:
793
+ nctx = ctx.with_negative()
794
+ for pattern, sub_schema in pattern_properties.items():
795
+ try:
796
+ key = ctx.generate_from(st.from_regex(pattern))
797
+ except re.error:
798
+ continue
799
+ with nctx.at(pattern):
800
+ for value in cover_schema_iter(nctx, sub_schema):
801
+ yield NegativeValue(
802
+ {**template, key: value.value},
803
+ description=f"Object with invalid pattern key '{key}' ('{pattern}') value: {value.description}",
804
+ location=nctx.current_path,
805
+ )
806
+
807
+
808
+ def _negative_items(ctx: CoverageContext, schema: dict[str, Any] | bool) -> Generator[GeneratedValue, None, None]:
809
+ """Arrays not matching the schema."""
810
+ nctx = ctx.with_negative()
811
+ for value in cover_schema_iter(nctx, schema):
812
+ yield NegativeValue(
813
+ [value.value],
814
+ description=f"Array with invalid items: {value.description}",
815
+ location=nctx.current_path,
816
+ )
817
+
818
+
819
+ def _not_matching_pattern(value: str, pattern: re.Pattern) -> bool:
820
+ return pattern.search(value) is None
821
+
822
+
823
+ def _negative_pattern(
824
+ ctx: CoverageContext, pattern: str, min_length: int | None = None, max_length: int | None = None
825
+ ) -> Generator[GeneratedValue, None, None]:
826
+ try:
827
+ compiled = re.compile(pattern)
828
+ except re.error:
829
+ return
830
+ yield NegativeValue(
831
+ ctx.generate_from(
832
+ st.text(min_size=min_length or 0, max_size=max_length)
833
+ .filter(partial(_not_matching_pattern, pattern=compiled))
834
+ .filter(ctx.is_valid_for_location)
835
+ ),
836
+ description=f"Value not matching the '{pattern}' pattern",
837
+ location=ctx.current_path,
838
+ )
839
+
840
+
841
+ def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
842
+ return {"allOf": [{k: v for k, v in schema.items() if k != key}, {"not": {key: value}}]}
843
+
844
+
845
+ def _negative_multiple_of(
846
+ ctx: CoverageContext, schema: dict, multiple_of: int | float
847
+ ) -> Generator[GeneratedValue, None, None]:
848
+ yield NegativeValue(
849
+ ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)),
850
+ description=f"Non-multiple of {multiple_of}",
851
+ location=ctx.current_path,
852
+ )
853
+
854
+
855
+ def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
856
+ unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
857
+ yield NegativeValue(unique + unique, description="Non-unique items", location=ctx.current_path)
858
+
859
+
860
+ def _negative_required(
861
+ ctx: CoverageContext, template: dict, required: list[str]
862
+ ) -> Generator[GeneratedValue, None, None]:
863
+ for key in required:
864
+ yield NegativeValue(
865
+ {k: v for k, v in template.items() if k != key},
866
+ description=f"Missing required property: {key}",
867
+ location=ctx.current_path,
868
+ parameter=key,
869
+ )
870
+
871
+
872
+ def _is_invalid_hostname(v: Any) -> bool:
873
+ return v == "" or not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, "hostname")
874
+
875
+
876
+ def _is_invalid_format(v: Any, format: str) -> bool:
877
+ return not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format)
878
+
879
+
880
+ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator[GeneratedValue, None, None]:
881
+ # Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
882
+ without_format = {k: v for k, v in schema.items() if k != "format"}
883
+ without_format.setdefault("type", "string")
884
+ strategy = from_schema(without_format)
885
+ if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
886
+ if format == "hostname":
887
+ strategy = strategy.filter(_is_invalid_hostname)
888
+ else:
889
+ strategy = strategy.filter(functools.partial(_is_invalid_format, format=format))
890
+ yield NegativeValue(
891
+ ctx.generate_from(strategy),
892
+ description=f"Value not matching the '{format}' format",
893
+ location=ctx.current_path,
894
+ )
895
+
896
+
897
+ def _is_non_integer_float(x: float) -> bool:
898
+ return x != int(x)
899
+
900
+
901
+ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator[GeneratedValue, None, None]:
902
+ if isinstance(ty, str):
903
+ types = [ty]
904
+ else:
905
+ types = ty
906
+ strategies = {ty: strategy for ty, strategy in STRATEGIES_FOR_TYPE.items() if ty not in types}
907
+ if "number" in types:
908
+ del strategies["integer"]
909
+ if "integer" in types:
910
+ strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
911
+ for strategy in strategies.values():
912
+ value = ctx.generate_from(strategy)
913
+ hashed = _to_hashable_key(value)
914
+ if hashed in seen:
915
+ continue
916
+ yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
917
+ seen.add(hashed)
918
+
919
+
920
+ def push_examples_to_properties(schema: dict[str, Any]) -> None:
921
+ """Push examples from the top-level 'examples' field to the corresponding properties."""
922
+ if "examples" in schema and "properties" in schema:
923
+ properties = schema["properties"]
924
+ for example in schema["examples"]:
925
+ if isinstance(example, dict):
926
+ for prop, value in example.items():
927
+ if prop in properties:
928
+ if "examples" not in properties[prop]:
929
+ properties[prop]["examples"] = []
930
+ if value not in schema["properties"][prop]["examples"]:
931
+ properties[prop]["examples"].append(value)