schemathesis 3.39.15__py3-none-any.whl → 4.0.0__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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -16,14 +16,17 @@ from hypothesis_jsonschema import from_schema
16
16
  from hypothesis_jsonschema._canonicalise import canonicalish
17
17
  from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING_FORMATS
18
18
 
19
- from ..constants import NOT_SET
20
- from ..internal.copy import fast_deepcopy
19
+ from schemathesis.core import INTERNAL_BUFFER_SIZE, 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
+ from schemathesis.openapi.generation.filters import is_invalid_path_parameter
26
+
21
27
  from ..specs.openapi.converter import update_pattern_in_schema
22
28
  from ..specs.openapi.formats import STRING_FORMATS, get_default_format_strategies
23
29
  from ..specs.openapi.patterns import update_quantifier
24
- from ..transports.headers import has_invalid_characters, is_latin_1_encodable
25
- from ._hypothesis import get_single_example
26
- from ._methods import DataGenerationMethod
27
30
 
28
31
 
29
32
  def _replace_zero_with_nonzero(x: float) -> float:
@@ -34,7 +37,8 @@ def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
34
37
  return st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3)
35
38
 
36
39
 
37
- BUFFER_SIZE = 8 * 1024
40
+ NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN = 100
41
+ NEGATIVE_MODE_MAX_ITEMS = 15
38
42
  FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
39
43
  NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
40
44
  JSON_STRATEGY: st.SearchStrategy = st.recursive(
@@ -62,18 +66,18 @@ UNKNOWN_PROPERTY_VALUE = 42
62
66
  @dataclass
63
67
  class GeneratedValue:
64
68
  value: Any
65
- data_generation_method: DataGenerationMethod
69
+ generation_mode: GenerationMode
66
70
  description: str
67
71
  parameter: str | None
68
72
  location: str | None
69
73
 
70
- __slots__ = ("value", "data_generation_method", "description", "parameter", "location")
74
+ __slots__ = ("value", "generation_mode", "description", "parameter", "location")
71
75
 
72
76
  @classmethod
73
77
  def with_positive(cls, value: Any, *, description: str) -> GeneratedValue:
74
78
  return cls(
75
79
  value=value,
76
- data_generation_method=DataGenerationMethod.positive,
80
+ generation_mode=GenerationMode.POSITIVE,
77
81
  description=description,
78
82
  location=None,
79
83
  parameter=None,
@@ -85,7 +89,7 @@ class GeneratedValue:
85
89
  ) -> GeneratedValue:
86
90
  return cls(
87
91
  value=value,
88
- data_generation_method=DataGenerationMethod.negative,
92
+ generation_mode=GenerationMode.NEGATIVE,
89
93
  description=description,
90
94
  location=location,
91
95
  parameter=parameter,
@@ -98,28 +102,26 @@ NegativeValue = GeneratedValue.with_negative
98
102
 
99
103
  @lru_cache(maxsize=128)
100
104
  def cached_draw(strategy: st.SearchStrategy) -> Any:
101
- return get_single_example(strategy)
105
+ return examples.generate_one(strategy)
102
106
 
103
107
 
104
108
  @dataclass
105
109
  class CoverageContext:
106
- data_generation_methods: list[DataGenerationMethod]
110
+ generation_modes: list[GenerationMode]
107
111
  location: str
108
112
  path: list[str | int]
109
113
 
110
- __slots__ = ("location", "data_generation_methods", "path")
114
+ __slots__ = ("location", "generation_modes", "path")
111
115
 
112
116
  def __init__(
113
117
  self,
114
118
  *,
115
119
  location: str,
116
- data_generation_methods: list[DataGenerationMethod] | None = None,
120
+ generation_modes: list[GenerationMode] | None = None,
117
121
  path: list[str | int] | None = None,
118
122
  ) -> None:
119
123
  self.location = location
120
- self.data_generation_methods = (
121
- data_generation_methods if data_generation_methods is not None else DataGenerationMethod.all()
122
- )
124
+ self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
123
125
  self.path = path or []
124
126
 
125
127
  @contextmanager
@@ -137,20 +139,22 @@ class CoverageContext:
137
139
  def with_positive(self) -> CoverageContext:
138
140
  return CoverageContext(
139
141
  location=self.location,
140
- data_generation_methods=[DataGenerationMethod.positive],
142
+ generation_modes=[GenerationMode.POSITIVE],
141
143
  path=self.path,
142
144
  )
143
145
 
144
146
  def with_negative(self) -> CoverageContext:
145
147
  return CoverageContext(
146
148
  location=self.location,
147
- data_generation_methods=[DataGenerationMethod.negative],
149
+ generation_modes=[GenerationMode.NEGATIVE],
148
150
  path=self.path,
149
151
  )
150
152
 
151
153
  def is_valid_for_location(self, value: Any) -> bool:
152
154
  if self.location in ("header", "cookie") and isinstance(value, str):
153
155
  return not value or (is_latin_1_encodable(value) and not has_invalid_characters("", value))
156
+ elif self.location == "path":
157
+ return not is_invalid_path_parameter(value)
154
158
  return True
155
159
 
156
160
  def generate_from(self, strategy: st.SearchStrategy) -> Any:
@@ -159,7 +163,7 @@ class CoverageContext:
159
163
  def generate_from_schema(self, schema: dict | bool) -> Any:
160
164
  if isinstance(schema, bool):
161
165
  return 0
162
- keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example"]])
166
+ keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example", "examples"]])
163
167
  if keys == ["type"] and isinstance(schema["type"], str) and schema["type"] in STRATEGIES_FOR_TYPE:
164
168
  return cached_draw(STRATEGIES_FOR_TYPE[schema["type"]])
165
169
  if keys == ["format", "type"]:
@@ -251,15 +255,36 @@ def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str |
251
255
  return (type(value), value)
252
256
 
253
257
 
258
+ class HashSet:
259
+ """Helper to track already generated values."""
260
+
261
+ __slots__ = ("_data",)
262
+
263
+ def __init__(self) -> None:
264
+ self._data: set[tuple] = set()
265
+
266
+ def insert(self, value: Any) -> bool:
267
+ key = _to_hashable_key(value)
268
+ before = len(self._data)
269
+ self._data.add(key)
270
+ return len(self._data) > before
271
+
272
+ def clear(self) -> None:
273
+ self._data.clear()
274
+
275
+
254
276
  def _cover_positive_for_type(
255
277
  ctx: CoverageContext, schema: dict, ty: str | None
256
278
  ) -> Generator[GeneratedValue, None, None]:
257
279
  if ty == "object" or ty == "array":
258
280
  template_schema = _get_template_schema(schema, ty)
259
281
  template = ctx.generate_from_schema(template_schema)
282
+ elif "properties" in schema or "required" in schema:
283
+ template_schema = _get_template_schema(schema, "object")
284
+ template = ctx.generate_from_schema(template_schema)
260
285
  else:
261
286
  template = None
262
- if DataGenerationMethod.positive in ctx.data_generation_methods:
287
+ if GenerationMode.POSITIVE in ctx.generation_modes:
263
288
  ctx = ctx.with_positive()
264
289
  enum = schema.get("enum", NOT_SET)
265
290
  const = schema.get("const", NOT_SET)
@@ -295,6 +320,8 @@ def _cover_positive_for_type(
295
320
  yield from _positive_array(ctx, schema, cast(list, template))
296
321
  elif ty == "object":
297
322
  yield from _positive_object(ctx, schema, cast(dict, template))
323
+ elif "properties" in schema or "required" in schema:
324
+ yield from _positive_object(ctx, schema, cast(dict, template))
298
325
 
299
326
 
300
327
  @contextmanager
@@ -302,7 +329,7 @@ def _ignore_unfixable(
302
329
  *,
303
330
  # Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
304
331
  # and it may cause errors during the interpreter shutdown
305
- ref_error: type[Exception] = jsonschema.RefResolutionError,
332
+ ref_error: type[Exception] = RefResolutionError,
306
333
  schema_error: type[Exception] = jsonschema.SchemaError,
307
334
  ) -> Generator:
308
335
  try:
@@ -319,10 +346,10 @@ def _ignore_unfixable(
319
346
 
320
347
 
321
348
  def cover_schema_iter(
322
- ctx: CoverageContext, schema: dict | bool, seen: set[Any | tuple[type, str]] | None = None
349
+ ctx: CoverageContext, schema: dict | bool, seen: HashSet | None = None
323
350
  ) -> Generator[GeneratedValue, None, None]:
324
351
  if seen is None:
325
- seen = set()
352
+ seen = HashSet()
326
353
  if isinstance(schema, bool):
327
354
  types = ["null", "boolean", "string", "number", "array", "object"]
328
355
  schema = {}
@@ -337,7 +364,7 @@ def cover_schema_iter(
337
364
  for ty in types:
338
365
  with _ignore_unfixable():
339
366
  yield from _cover_positive_for_type(ctx, schema, ty)
340
- if DataGenerationMethod.negative in ctx.data_generation_methods:
367
+ if GenerationMode.NEGATIVE in ctx.generation_modes:
341
368
  template = None
342
369
  for key, value in schema.items():
343
370
  with _ignore_unfixable(), ctx.at(key):
@@ -345,12 +372,9 @@ def cover_schema_iter(
345
372
  yield from _negative_enum(ctx, value, seen)
346
373
  elif key == "const":
347
374
  for value_ in _negative_enum(ctx, [value], seen):
348
- k = _to_hashable_key(value_.value)
349
- if k not in seen:
350
- yield value_
351
- seen.add(k)
375
+ yield value_
352
376
  elif key == "type":
353
- yield from _negative_type(ctx, seen, value)
377
+ yield from _negative_type(ctx, value, seen)
354
378
  elif key == "properties":
355
379
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
356
380
  yield from _negative_properties(ctx, template, value)
@@ -367,69 +391,75 @@ def cover_schema_iter(
367
391
  yield from _negative_format(ctx, schema, value)
368
392
  elif key == "maximum":
369
393
  next = value + 1
370
- if next not in seen:
394
+ if seen.insert(next):
371
395
  yield NegativeValue(next, description="Value greater than maximum", location=ctx.current_path)
372
- seen.add(next)
373
396
  elif key == "minimum":
374
397
  next = value - 1
375
- if next not in seen:
398
+ if seen.insert(next):
376
399
  yield NegativeValue(next, description="Value smaller than minimum", location=ctx.current_path)
377
- seen.add(next)
378
- elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
400
+ elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and seen.insert(value):
379
401
  verb = "greater" if key == "exclusiveMaximum" else "smaller"
380
402
  limit = "maximum" if key == "exclusiveMaximum" else "minimum"
381
403
  yield NegativeValue(value, description=f"Value {verb} than {limit}", location=ctx.current_path)
382
- seen.add(value)
383
404
  elif key == "multipleOf":
384
405
  for value_ in _negative_multiple_of(ctx, schema, value):
385
- k = _to_hashable_key(value_.value)
386
- if k not in seen:
406
+ if seen.insert(value_.value):
387
407
  yield value_
388
- seen.add(k)
389
- elif key == "minLength" and 0 < value < BUFFER_SIZE:
390
- with suppress(InvalidArgument):
391
- min_length = max_length = value - 1
392
- new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
393
- new_schema.setdefault("type", "string")
394
- if "pattern" in new_schema:
395
- new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
396
- if new_schema["pattern"] == schema["pattern"]:
397
- # Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
398
- del new_schema["minLength"]
399
- del new_schema["maxLength"]
400
- value = ctx.generate_from_schema(new_schema)[:max_length]
401
- else:
402
- value = ctx.generate_from_schema(new_schema)
403
- else:
404
- value = ctx.generate_from_schema(new_schema)
405
- k = _to_hashable_key(value)
406
- if k not in seen:
408
+ elif key == "minLength" and 0 < value < INTERNAL_BUFFER_SIZE:
409
+ if value == 1:
410
+ # In this case, the only possible negative string is an empty one
411
+ # The `pattern` value may require an non-empty one and the generation will fail
412
+ # However, it is fine to violate `pattern` here as it is negative string generation anyway
413
+ value = ""
414
+ if seen.insert(value):
407
415
  yield NegativeValue(
408
416
  value, description="String smaller than minLength", location=ctx.current_path
409
417
  )
410
- seen.add(k)
411
- elif key == "maxLength" and value < BUFFER_SIZE:
418
+ else:
419
+ with suppress(InvalidArgument):
420
+ min_length = max_length = value - 1
421
+ new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
422
+ new_schema.setdefault("type", "string")
423
+ if "pattern" in new_schema:
424
+ new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
425
+ if new_schema["pattern"] == schema["pattern"]:
426
+ # Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
427
+ del new_schema["minLength"]
428
+ del new_schema["maxLength"]
429
+ value = ctx.generate_from_schema(new_schema)[:max_length]
430
+ else:
431
+ value = ctx.generate_from_schema(new_schema)
432
+ else:
433
+ value = ctx.generate_from_schema(new_schema)
434
+ if seen.insert(value):
435
+ yield NegativeValue(
436
+ value, description="String smaller than minLength", location=ctx.current_path
437
+ )
438
+ elif key == "maxLength" and value < INTERNAL_BUFFER_SIZE:
412
439
  try:
413
440
  min_length = max_length = value + 1
414
441
  new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
415
442
  new_schema.setdefault("type", "string")
416
443
  if "pattern" in new_schema:
417
- new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
418
- if new_schema["pattern"] == schema["pattern"]:
419
- # Pattern wasn't updated, try to generate a valid value then extend the string to the required length
420
- del new_schema["minLength"]
421
- del new_schema["maxLength"]
422
- value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
423
- else:
444
+ if value > NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN:
445
+ # Large `maxLength` value can be extremely slow to generate when combined with `pattern`
446
+ del new_schema["pattern"]
424
447
  value = ctx.generate_from_schema(new_schema)
448
+ else:
449
+ new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
450
+ if new_schema["pattern"] == schema["pattern"]:
451
+ # Pattern wasn't updated, try to generate a valid value then extend the string to the required length
452
+ del new_schema["minLength"]
453
+ del new_schema["maxLength"]
454
+ value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
455
+ else:
456
+ value = ctx.generate_from_schema(new_schema)
425
457
  else:
426
458
  value = ctx.generate_from_schema(new_schema)
427
- k = _to_hashable_key(value)
428
- if k not in seen:
459
+ if seen.insert(value):
429
460
  yield NegativeValue(
430
461
  value, description="String larger than maxLength", location=ctx.current_path
431
462
  )
432
- seen.add(k)
433
463
  except (InvalidArgument, Unsatisfiable):
434
464
  pass
435
465
  elif key == "uniqueItems" and value:
@@ -437,34 +467,64 @@ def cover_schema_iter(
437
467
  elif key == "required":
438
468
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
439
469
  yield from _negative_required(ctx, template, value)
440
- elif key == "maxItems" and isinstance(value, int) and value < BUFFER_SIZE:
441
- try:
442
- # Force the array to have one more item than allowed
443
- new_schema = {**schema, "minItems": value + 1, "maxItems": value + 1, "type": "array"}
444
- array_value = ctx.generate_from_schema(new_schema)
445
- k = _to_hashable_key(array_value)
446
- if k not in seen:
470
+ elif key == "maxItems" and isinstance(value, int) and value < INTERNAL_BUFFER_SIZE:
471
+ if value > NEGATIVE_MODE_MAX_ITEMS:
472
+ # It could be extremely slow to generate large arrays
473
+ # Generate values up to the limit and reuse them to construct the final array
474
+ new_schema = {
475
+ **schema,
476
+ "minItems": NEGATIVE_MODE_MAX_ITEMS,
477
+ "maxItems": NEGATIVE_MODE_MAX_ITEMS,
478
+ "type": "array",
479
+ }
480
+ if "items" in schema and isinstance(schema["items"], dict):
481
+ # The schema may have another large array nested, therefore generate covering cases
482
+ # and use them to build an array for the current schema
483
+ negative = [case.value for case in cover_schema_iter(ctx, schema["items"])]
484
+ positive = [case.value for case in cover_schema_iter(ctx.with_positive(), schema["items"])]
485
+ # Interleave positive & negative values
486
+ array_value = [value for pair in zip(positive, negative) for value in pair][
487
+ :NEGATIVE_MODE_MAX_ITEMS
488
+ ]
489
+ else:
490
+ array_value = ctx.generate_from_schema(new_schema)
491
+
492
+ # Extend the array to be of length value + 1 by repeating its own elements
493
+ diff = value + 1 - len(array_value)
494
+ if diff > 0 and array_value:
495
+ array_value += (
496
+ array_value * (diff // len(array_value)) + array_value[: diff % len(array_value)]
497
+ )
498
+ if seen.insert(array_value):
447
499
  yield NegativeValue(
448
500
  array_value,
449
501
  description="Array with more items than allowed by maxItems",
450
502
  location=ctx.current_path,
451
503
  )
452
- seen.add(k)
453
- except (InvalidArgument, Unsatisfiable):
454
- pass
504
+ else:
505
+ try:
506
+ # Force the array to have one more item than allowed
507
+ new_schema = {**schema, "minItems": value + 1, "maxItems": value + 1, "type": "array"}
508
+ array_value = ctx.generate_from_schema(new_schema)
509
+ if seen.insert(array_value):
510
+ yield NegativeValue(
511
+ array_value,
512
+ description="Array with more items than allowed by maxItems",
513
+ location=ctx.current_path,
514
+ )
515
+ except (InvalidArgument, Unsatisfiable):
516
+ pass
455
517
  elif key == "minItems" and isinstance(value, int) and value > 0:
456
518
  try:
457
519
  # Force the array to have one less item than the minimum
458
520
  new_schema = {**schema, "minItems": value - 1, "maxItems": value - 1, "type": "array"}
459
521
  array_value = ctx.generate_from_schema(new_schema)
460
- k = _to_hashable_key(array_value)
461
- if k not in seen:
522
+ if seen.insert(array_value):
462
523
  yield NegativeValue(
463
524
  array_value,
464
525
  description="Array with fewer items than allowed by minItems",
465
526
  location=ctx.current_path,
466
527
  )
467
- seen.add(k)
468
528
  except (InvalidArgument, Unsatisfiable):
469
529
  pass
470
530
  elif (
@@ -506,7 +566,7 @@ def _get_properties(schema: dict | bool) -> dict | bool:
506
566
  return {"enum": schema["examples"]}
507
567
  if schema.get("type") == "object":
508
568
  return _get_template_schema(schema, "object")
509
- _schema = fast_deepcopy(schema)
569
+ _schema = deepclone(schema)
510
570
  update_pattern_in_schema(_schema)
511
571
  return _schema
512
572
  return schema
@@ -525,6 +585,22 @@ def _get_template_schema(schema: dict, ty: str) -> dict:
525
585
  return {**schema, "type": ty}
526
586
 
527
587
 
588
+ def _ensure_valid_path_parameter_schema(schema: dict[str, Any]) -> dict[str, Any]:
589
+ # Path parameters should have at least 1 character length and don't contain any characters with special treatment
590
+ # on the transport level.
591
+ # The implementation below sneaks into `not` to avoid clashing with existing `pattern` keyword
592
+ not_ = schema.get("not", {}).copy()
593
+ not_["pattern"] = r"[/{}]"
594
+ return {**schema, "minLength": 1, "not": not_}
595
+
596
+
597
+ def _ensure_valid_headers_schema(schema: dict[str, Any]) -> dict[str, Any]:
598
+ # Reject any character that is not A-Z, a-z, or 0-9 for simplicity
599
+ not_ = schema.get("not", {}).copy()
600
+ not_["pattern"] = r"[^A-Za-z0-9]"
601
+ return {**schema, "not": not_}
602
+
603
+
528
604
  def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
529
605
  """Generate positive string values."""
530
606
  # Boundary and near boundary values
@@ -532,67 +608,90 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
532
608
  if min_length == 0:
533
609
  min_length = None
534
610
  max_length = schema.get("maxLength")
611
+ if ctx.location == "path":
612
+ schema = _ensure_valid_path_parameter_schema(schema)
613
+ elif ctx.location in ("header", "cookie") and not ("format" in schema and schema["format"] in FORMAT_STRATEGIES):
614
+ # Don't apply it for known formats - they will insure the correct format during generation
615
+ schema = _ensure_valid_headers_schema(schema)
616
+
535
617
  example = schema.get("example")
536
618
  examples = schema.get("examples")
537
619
  default = schema.get("default")
620
+
621
+ # Two-layer check to avoid potentially expensive data generation using schema constraints as a key
622
+ seen_values = HashSet()
623
+ seen_constraints: set[tuple] = set()
624
+
538
625
  if example or examples or default:
539
- if example and ctx.is_valid_for_location(example):
626
+ has_valid_example = False
627
+ if example and ctx.is_valid_for_location(example) and seen_values.insert(example):
628
+ has_valid_example = True
540
629
  yield PositiveValue(example, description="Example value")
541
630
  if examples:
542
631
  for example in examples:
543
- if ctx.is_valid_for_location(example):
632
+ if ctx.is_valid_for_location(example) and seen_values.insert(example):
633
+ has_valid_example = True
544
634
  yield PositiveValue(example, description="Example value")
545
635
  if (
546
636
  default
547
637
  and not (example is not None and default == example)
548
638
  and not (examples is not None and any(default == ex for ex in examples))
549
639
  and ctx.is_valid_for_location(default)
640
+ and seen_values.insert(default)
550
641
  ):
642
+ has_valid_example = True
551
643
  yield PositiveValue(default, description="Default value")
552
- elif not min_length and not max_length:
553
- # Default positive value
554
- yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
555
- elif "pattern" in schema:
556
- yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
557
-
558
- seen = set()
559
-
560
- if min_length is not None and min_length < BUFFER_SIZE:
644
+ if not has_valid_example:
645
+ if not min_length and not max_length or "pattern" in schema:
646
+ value = ctx.generate_from_schema(schema)
647
+ seen_values.insert(value)
648
+ seen_constraints.add((min_length, max_length))
649
+ yield PositiveValue(value, description="Valid string")
650
+ elif not min_length and not max_length or "pattern" in schema:
651
+ value = ctx.generate_from_schema(schema)
652
+ seen_values.insert(value)
653
+ seen_constraints.add((min_length, max_length))
654
+ yield PositiveValue(value, description="Valid string")
655
+
656
+ if min_length is not None and min_length < INTERNAL_BUFFER_SIZE:
561
657
  # Exactly the minimum length
562
- yield PositiveValue(
563
- ctx.generate_from_schema({**schema, "maxLength": min_length}), description="Minimum length string"
564
- )
565
- seen.add(min_length)
658
+ key = (min_length, min_length)
659
+ if key not in seen_constraints:
660
+ seen_constraints.add(key)
661
+ value = ctx.generate_from_schema({**schema, "maxLength": min_length})
662
+ if seen_values.insert(value):
663
+ yield PositiveValue(value, description="Minimum length string")
566
664
 
567
665
  # One character more than minimum if possible
568
666
  larger = min_length + 1
569
- if larger < BUFFER_SIZE and larger not in seen and (not max_length or larger <= max_length):
570
- yield PositiveValue(
571
- ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger}),
572
- description="Near-boundary length string",
573
- )
574
- seen.add(larger)
667
+ key = (larger, larger)
668
+ if larger < INTERNAL_BUFFER_SIZE and key not in seen_constraints and (not max_length or larger <= max_length):
669
+ seen_constraints.add(key)
670
+ value = ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger})
671
+ if seen_values.insert(value):
672
+ yield PositiveValue(value, description="Near-boundary length string")
575
673
 
576
674
  if max_length is not None:
577
675
  # Exactly the maximum length
578
- if max_length < BUFFER_SIZE and max_length not in seen:
579
- yield PositiveValue(
580
- ctx.generate_from_schema({**schema, "minLength": max_length}), description="Maximum length string"
581
- )
582
- seen.add(max_length)
676
+ key = (max_length, max_length)
677
+ if max_length < INTERNAL_BUFFER_SIZE and key not in seen_constraints:
678
+ seen_constraints.add(key)
679
+ value = ctx.generate_from_schema({**schema, "minLength": max_length, "maxLength": max_length})
680
+ if seen_values.insert(value):
681
+ yield PositiveValue(value, description="Maximum length string")
583
682
 
584
683
  # One character less than maximum if possible
585
684
  smaller = max_length - 1
685
+ key = (smaller, smaller)
586
686
  if (
587
- smaller < BUFFER_SIZE
588
- and smaller not in seen
687
+ smaller < INTERNAL_BUFFER_SIZE
688
+ and key not in seen_constraints
589
689
  and (smaller > 0 and (min_length is None or smaller >= min_length))
590
690
  ):
591
- yield PositiveValue(
592
- ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller}),
593
- description="Near-boundary length string",
594
- )
595
- seen.add(smaller)
691
+ seen_constraints.add(key)
692
+ value = ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller})
693
+ if seen_values.insert(value):
694
+ yield PositiveValue(value, description="Near-boundary length string")
596
695
 
597
696
 
598
697
  def closest_multiple_greater_than(y: int, x: int) -> int:
@@ -619,23 +718,26 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
619
718
  examples = schema.get("examples")
620
719
  default = schema.get("default")
621
720
 
721
+ seen = HashSet()
722
+
622
723
  if example or examples or default:
623
- if example:
724
+ if example and seen.insert(example):
624
725
  yield PositiveValue(example, description="Example value")
625
726
  if examples:
626
727
  for example in examples:
627
- yield PositiveValue(example, description="Example value")
728
+ if seen.insert(example):
729
+ yield PositiveValue(example, description="Example value")
628
730
  if (
629
731
  default
630
732
  and not (example is not None and default == example)
631
733
  and not (examples is not None and any(default == ex for ex in examples))
734
+ and seen.insert(default)
632
735
  ):
633
736
  yield PositiveValue(default, description="Default value")
634
737
  elif not minimum and not maximum:
635
- # Default positive value
636
- yield PositiveValue(ctx.generate_from_schema(schema), description="Valid number")
637
-
638
- seen = set()
738
+ value = ctx.generate_from_schema(schema)
739
+ seen.insert(value)
740
+ yield PositiveValue(value, description="Valid number")
639
741
 
640
742
  if minimum is not None:
641
743
  # Exactly the minimum
@@ -643,16 +745,15 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
643
745
  smallest = closest_multiple_greater_than(minimum, multiple_of)
644
746
  else:
645
747
  smallest = minimum
646
- seen.add(smallest)
647
- yield PositiveValue(smallest, description="Minimum value")
748
+ if seen.insert(smallest):
749
+ yield PositiveValue(smallest, description="Minimum value")
648
750
 
649
751
  # One more than minimum if possible
650
752
  if multiple_of is not None:
651
753
  larger = smallest + multiple_of
652
754
  else:
653
755
  larger = minimum + 1
654
- if larger not in seen and (not maximum or larger <= maximum):
655
- seen.add(larger)
756
+ if (not maximum or larger <= maximum) and seen.insert(larger):
656
757
  yield PositiveValue(larger, description="Near-boundary number")
657
758
 
658
759
  if maximum is not None:
@@ -661,8 +762,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
661
762
  largest = maximum - (maximum % multiple_of)
662
763
  else:
663
764
  largest = maximum
664
- if largest not in seen:
665
- seen.add(largest)
765
+ if seen.insert(largest):
666
766
  yield PositiveValue(largest, description="Maximum value")
667
767
 
668
768
  # One less than maximum if possible
@@ -670,69 +770,79 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
670
770
  smaller = largest - multiple_of
671
771
  else:
672
772
  smaller = maximum - 1
673
- if smaller not in seen and (smaller > 0 and (minimum is None or smaller >= minimum)):
674
- seen.add(smaller)
773
+ if (smaller > 0 and (minimum is None or smaller >= minimum)) and seen.insert(smaller):
675
774
  yield PositiveValue(smaller, description="Near-boundary number")
676
775
 
677
776
 
678
777
  def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
679
- seen = set()
680
778
  example = schema.get("example")
681
779
  examples = schema.get("examples")
682
780
  default = schema.get("default")
683
781
 
782
+ seen = HashSet()
783
+ seen_constraints: set[tuple] = set()
784
+
684
785
  if example or examples or default:
685
- if example:
786
+ if example and seen.insert(example):
686
787
  yield PositiveValue(example, description="Example value")
687
788
  if examples:
688
789
  for example in examples:
689
- yield PositiveValue(example, description="Example value")
790
+ if seen.insert(example):
791
+ yield PositiveValue(example, description="Example value")
690
792
  if (
691
793
  default
692
794
  and not (example is not None and default == example)
693
795
  and not (examples is not None and any(default == ex for ex in examples))
796
+ and seen.insert(default)
694
797
  ):
695
798
  yield PositiveValue(default, description="Default value")
696
- else:
799
+ elif seen.insert(template):
697
800
  yield PositiveValue(template, description="Valid array")
698
- seen.add(len(template))
699
801
 
700
802
  # Boundary and near-boundary sizes
701
803
  min_items = schema.get("minItems")
702
804
  max_items = schema.get("maxItems")
703
805
  if min_items is not None:
704
806
  # Do not generate an array with `minItems` length, because it is already covered by `template`
705
-
706
807
  # One item more than minimum if possible
707
808
  larger = min_items + 1
708
- if larger not in seen and (max_items is None or larger <= max_items):
709
- yield PositiveValue(
710
- ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger}),
711
- description="Near-boundary items array",
712
- )
713
- seen.add(larger)
809
+ if (max_items is None or larger <= max_items) and larger not in seen_constraints:
810
+ seen_constraints.add(larger)
811
+ value = ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
812
+ if seen.insert(value):
813
+ yield PositiveValue(value, description="Near-boundary items array")
714
814
 
715
815
  if max_items is not None:
716
- if max_items < BUFFER_SIZE and max_items not in seen:
717
- yield PositiveValue(
718
- ctx.generate_from_schema({**schema, "minItems": max_items}),
719
- description="Maximum items array",
720
- )
721
- seen.add(max_items)
816
+ if max_items < INTERNAL_BUFFER_SIZE and max_items not in seen_constraints:
817
+ seen_constraints.add(max_items)
818
+ value = ctx.generate_from_schema({**schema, "minItems": max_items})
819
+ if seen.insert(value):
820
+ yield PositiveValue(value, description="Maximum items array")
722
821
 
723
822
  # One item smaller than maximum if possible
724
823
  smaller = max_items - 1
725
824
  if (
726
- smaller < BUFFER_SIZE
825
+ smaller < INTERNAL_BUFFER_SIZE
727
826
  and smaller > 0
728
- and smaller not in seen
729
827
  and (min_items is None or smaller >= min_items)
828
+ and smaller not in seen_constraints
730
829
  ):
731
- yield PositiveValue(
732
- ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller}),
733
- description="Near-boundary items array",
734
- )
735
- seen.add(smaller)
830
+ value = ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
831
+ if seen.insert(value):
832
+ yield PositiveValue(value, description="Near-boundary items array")
833
+
834
+ if "items" in schema and "enum" in schema["items"] and isinstance(schema["items"]["enum"], list) and max_items != 0:
835
+ # Ensure there is enough items to pass `minItems` if it is specified
836
+ length = min_items or 1
837
+ for variant in schema["items"]["enum"]:
838
+ value = [variant] * length
839
+ if seen.insert(value):
840
+ yield PositiveValue(value, description="Enum value from available for items array")
841
+ elif min_items is None and max_items is None and "items" in schema and isinstance(schema["items"], dict):
842
+ # Otherwise only an empty array is generated
843
+ sub_schema = schema["items"]
844
+ for item in cover_schema_iter(ctx, sub_schema):
845
+ yield PositiveValue([item.value], description=f"Single-item array: {item.description}")
736
846
 
737
847
 
738
848
  def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
@@ -773,16 +883,14 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
773
883
  if set(properties) != required:
774
884
  only_required = {k: v for k, v in template.items() if k in required}
775
885
  yield PositiveValue(only_required, description="Object with only required properties")
776
- seen = set()
886
+ seen = HashSet()
777
887
  for name, sub_schema in properties.items():
778
- seen.add(_to_hashable_key(template.get(name)))
888
+ seen.insert(template.get(name))
779
889
  for new in cover_schema_iter(ctx, sub_schema):
780
- key = _to_hashable_key(new.value)
781
- if key not in seen:
890
+ if seen.insert(new.value):
782
891
  yield PositiveValue(
783
892
  {**template, name: new.value}, description=f"Object with valid '{name}' value: {new.description}"
784
893
  )
785
- seen.add(key)
786
894
  seen.clear()
787
895
 
788
896
 
@@ -791,14 +899,11 @@ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
791
899
  yield next(combinations(optional, size))
792
900
 
793
901
 
794
- def _negative_enum(
795
- ctx: CoverageContext, value: list, seen: set[Any | tuple[type, str]]
796
- ) -> Generator[GeneratedValue, None, None]:
902
+ def _negative_enum(ctx: CoverageContext, value: list, seen: HashSet) -> Generator[GeneratedValue, None, None]:
797
903
  def is_not_in_value(x: Any) -> bool:
798
904
  if x in value or not ctx.is_valid_for_location(x):
799
905
  return False
800
- _hashed = _to_hashable_key(x)
801
- return _hashed not in seen
906
+ return seen.insert(x)
802
907
 
803
908
  strategy = (
804
909
  st.text(alphabet=st.characters(min_codepoint=65, max_codepoint=122, categories=["L"]), min_size=3)
@@ -806,10 +911,11 @@ def _negative_enum(
806
911
  | st.booleans()
807
912
  | NUMERIC_STRATEGY
808
913
  ).filter(is_not_in_value)
809
- value = ctx.generate_from(strategy)
810
- yield NegativeValue(value, description="Invalid enum value", location=ctx.current_path)
811
- hashed = _to_hashable_key(value)
812
- seen.add(hashed)
914
+ yield NegativeValue(
915
+ ctx.generate_from(strategy),
916
+ description="Invalid enum value",
917
+ location=ctx.current_path,
918
+ )
813
919
 
814
920
 
815
921
  def _negative_properties(
@@ -938,7 +1044,11 @@ def _is_non_integer_float(x: float) -> bool:
938
1044
  return x != int(x)
939
1045
 
940
1046
 
941
- def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator[GeneratedValue, None, None]:
1047
+ def _negative_type(
1048
+ ctx: CoverageContext,
1049
+ ty: str | list[str],
1050
+ seen: HashSet,
1051
+ ) -> Generator[GeneratedValue, None, None]:
942
1052
  if isinstance(ty, str):
943
1053
  types = [ty]
944
1054
  else:
@@ -950,11 +1060,8 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
950
1060
  strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
951
1061
  for strategy in strategies.values():
952
1062
  value = ctx.generate_from(strategy)
953
- hashed = _to_hashable_key(value)
954
- if hashed in seen:
955
- continue
956
- yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
957
- seen.add(hashed)
1063
+ if seen.insert(value):
1064
+ yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
958
1065
 
959
1066
 
960
1067
  def push_examples_to_properties(schema: dict[str, Any]) -> None: