schemathesis 3.39.16__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 +233 -307
  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.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.16.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 -717
  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.16.dist-info/METADATA +0 -293
  251. schemathesis-3.39.16.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.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -11,7 +11,8 @@ from hypothesis import reject
11
11
  from hypothesis import strategies as st
12
12
  from hypothesis.strategies._internal.featureflags import FeatureStrategy
13
13
 
14
- from ....internal.copy import fast_deepcopy
14
+ from schemathesis.core.transforms import deepclone
15
+
15
16
  from ..utils import get_type, is_header_location
16
17
  from .types import Draw, Schema
17
18
  from .utils import can_negate
@@ -19,7 +20,7 @@ from .utils import can_negate
19
20
  T = TypeVar("T")
20
21
 
21
22
 
22
- class MutationResult(enum.Enum):
23
+ class MutationResult(int, enum.Enum):
23
24
  """The result of applying some mutation to some schema.
24
25
 
25
26
  Failing to mutate something means that by applying some mutation, it is not possible to change
@@ -111,7 +112,7 @@ class MutationContext:
111
112
  # Body can be of any type and does not have any specific type semantic.
112
113
  mutations = draw(ordered(get_mutations(draw, self.keywords)))
113
114
  # Deep copy all keywords to avoid modifying the original schema
114
- new_schema = fast_deepcopy(self.keywords)
115
+ new_schema = deepclone(self.keywords)
115
116
  enabled_mutations = draw(st.shared(FeatureStrategy(), key="mutations")) # type: ignore
116
117
  # Always apply at least one mutation, otherwise everything is rejected, and we'd like to avoid it
117
118
  # for performance reasons
@@ -401,8 +402,8 @@ def negate_constraints(context: MutationContext, draw: Draw, schema: Schema) ->
401
402
  if key in DEPENDENCIES:
402
403
  # If this keyword has a dependency, then it should be also negated
403
404
  dependency = DEPENDENCIES[key]
404
- if dependency not in negated:
405
- negated[dependency] = copied[dependency] # Assuming the schema is valid
405
+ if dependency not in negated and dependency in copied:
406
+ negated[dependency] = copied[dependency]
406
407
  else:
407
408
  schema[key] = value
408
409
  if is_negated:
@@ -4,12 +4,13 @@ import json
4
4
  from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING, Any, ClassVar, Iterable
6
6
 
7
- from ...exceptions import OperationSchemaError
8
- from ...parameters import Parameter
7
+ from schemathesis.core.errors import InvalidSchema
8
+ from schemathesis.schemas import Parameter
9
+
9
10
  from .converter import to_json_schema_recursive
10
11
 
11
12
  if TYPE_CHECKING:
12
- from ...models import APIOperation
13
+ from ...schemas import APIOperation
13
14
 
14
15
 
15
16
  @dataclass(eq=False)
@@ -22,6 +23,7 @@ class OpenAPIParameter(Parameter):
22
23
  supported_jsonschema_keywords: ClassVar[tuple[str, ...]]
23
24
 
24
25
  def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
26
+
25
27
  @property
26
28
  def description(self) -> str | None:
27
29
  """A brief parameter description."""
@@ -312,9 +314,6 @@ def parameters_to_json_schema(
312
314
  ) -> dict[str, Any]:
313
315
  """Create an "object" JSON schema from a list of Open API parameters.
314
316
 
315
- :param List[OpenAPIParameter] parameters: A list of Open API parameters related to the same location. All of
316
- them are expected to have the same "in" value.
317
-
318
317
  For each input parameter, there will be a property in the output schema.
319
318
 
320
319
  This:
@@ -371,13 +370,12 @@ def get_parameter_schema(operation: APIOperation, data: dict[str, Any]) -> dict[
371
370
  # In Open API 3.0, there could be "schema" or "content" field. They are mutually exclusive.
372
371
  if "schema" in data:
373
372
  if not isinstance(data["schema"], dict):
374
- raise OperationSchemaError(
373
+ raise InvalidSchema(
375
374
  INVALID_SCHEMA_MESSAGE.format(
376
375
  location=data.get("in", ""), name=data.get("name", "<UNKNOWN>"), schema=data["schema"]
377
376
  ),
378
377
  path=operation.path,
379
378
  method=operation.method,
380
- full_path=operation.full_path,
381
379
  )
382
380
  return data["schema"]
383
381
  # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-10
@@ -385,11 +383,10 @@ def get_parameter_schema(operation: APIOperation, data: dict[str, Any]) -> dict[
385
383
  try:
386
384
  content = data["content"]
387
385
  except KeyError as exc:
388
- raise OperationSchemaError(
386
+ raise InvalidSchema(
389
387
  MISSING_SCHEMA_OR_CONTENT_MESSAGE.format(location=data.get("in", ""), name=data.get("name", "<UNKNOWN>")),
390
388
  path=operation.path,
391
389
  method=operation.method,
392
- full_path=operation.full_path,
393
390
  ) from exc
394
391
  options = iter(content.values())
395
392
  media_type_object = next(options)
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import re
4
4
  from functools import lru_cache
5
5
 
6
- from ...exceptions import InternalError
6
+ from schemathesis.core.errors import InternalError
7
7
 
8
8
  try: # pragma: no cover
9
9
  import re._constants as sre
@@ -19,11 +19,12 @@ if hasattr(sre, "POSSESSIVE_REPEAT"):
19
19
  else:
20
20
  REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT)
21
21
  LITERAL = sre.LITERAL
22
+ NOT_LITERAL = sre.NOT_LITERAL
22
23
  IN = sre.IN
23
24
  MAXREPEAT = sre_parse.MAXREPEAT
24
25
 
25
26
 
26
- @lru_cache()
27
+ @lru_cache
27
28
  def update_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
28
29
  """Update the quantifier of a regular expression based on given min and max lengths."""
29
30
  if not pattern or (min_length in (None, 0) and max_length is None):
@@ -69,13 +70,18 @@ def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, m
69
70
  trailing_anchor_length = _get_anchor_length(parsed[2][1])
70
71
  leading_anchor = pattern[:leading_anchor_length]
71
72
  trailing_anchor = pattern[-trailing_anchor_length:]
72
- return (
73
- leading_anchor
74
- + _update_quantifier(
75
- op, value, pattern[leading_anchor_length:-trailing_anchor_length], min_length, max_length
76
- )
77
- + trailing_anchor
78
- )
73
+ # Special case for patterns canonicalisation. Some frameworks generate `\\w\\W` instead of `.`
74
+ # Such patterns lead to significantly slower data generation
75
+ if op == sre.IN and _matches_anything(value):
76
+ op = sre.ANY
77
+ value = None
78
+ inner_pattern = "."
79
+ elif op in REPEATS and len(value[2]) == 1 and value[2][0][0] == sre.IN and _matches_anything(value[2][0][1]):
80
+ value = (value[0], value[1], [(sre.ANY, None)], *value[3:])
81
+ inner_pattern = "."
82
+ else:
83
+ inner_pattern = pattern[leading_anchor_length:-trailing_anchor_length]
84
+ return leading_anchor + _update_quantifier(op, value, inner_pattern, min_length, max_length) + trailing_anchor
79
85
  elif (
80
86
  len(parsed) > 3
81
87
  and parsed[0][0] == ANCHOR
@@ -86,6 +92,19 @@ def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, m
86
92
  return pattern
87
93
 
88
94
 
95
+ def _matches_anything(value: list) -> bool:
96
+ """Check if the given pattern is equivalent to '.' (match any character)."""
97
+ # Common forms: [\w\W], [\s\S], etc.
98
+ return value in (
99
+ [(sre.CATEGORY, sre.CATEGORY_WORD), (sre.CATEGORY, sre.CATEGORY_NOT_WORD)],
100
+ [(sre.CATEGORY, sre.CATEGORY_SPACE), (sre.CATEGORY, sre.CATEGORY_NOT_SPACE)],
101
+ [(sre.CATEGORY, sre.CATEGORY_DIGIT), (sre.CATEGORY, sre.CATEGORY_NOT_DIGIT)],
102
+ [(sre.CATEGORY, sre.CATEGORY_NOT_WORD), (sre.CATEGORY, sre.CATEGORY_WORD)],
103
+ [(sre.CATEGORY, sre.CATEGORY_NOT_SPACE), (sre.CATEGORY, sre.CATEGORY_SPACE)],
104
+ [(sre.CATEGORY, sre.CATEGORY_NOT_DIGIT), (sre.CATEGORY, sre.CATEGORY_DIGIT)],
105
+ )
106
+
107
+
89
108
  def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None, max_length: int | None) -> str:
90
109
  """Update regex pattern with multiple quantified patterns to satisfy length constraints."""
91
110
  # Extract anchors
@@ -96,8 +115,20 @@ def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None,
96
115
 
97
116
  pattern_parts = parsed[1:-1]
98
117
 
118
+ # Calculate total fixed length and per-repetition lengths
119
+ fixed_length = 0
120
+ quantifier_bounds = []
121
+ repetition_lengths = []
122
+
123
+ for op, value in pattern_parts:
124
+ if op in (LITERAL, NOT_LITERAL):
125
+ fixed_length += 1
126
+ elif op in REPEATS:
127
+ min_repeat, max_repeat, subpattern = value
128
+ quantifier_bounds.append((min_repeat, max_repeat))
129
+ repetition_lengths.append(_calculate_min_repetition_length(subpattern))
130
+
99
131
  # Adjust length constraints by subtracting fixed literals length
100
- fixed_length = sum(1 for op, _ in pattern_parts if op == LITERAL)
101
132
  if min_length is not None:
102
133
  min_length -= fixed_length
103
134
  if min_length < 0:
@@ -107,13 +138,10 @@ def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None,
107
138
  if max_length < 0:
108
139
  return pattern
109
140
 
110
- # Extract only min/max bounds from quantified parts
111
- quantifier_bounds = [value[:2] for op, value in pattern_parts if op in REPEATS]
112
-
113
141
  if not quantifier_bounds:
114
142
  return pattern
115
143
 
116
- length_distribution = _distribute_length_constraints(quantifier_bounds, min_length, max_length)
144
+ length_distribution = _distribute_length_constraints(quantifier_bounds, repetition_lengths, min_length, max_length)
117
145
  if not length_distribution:
118
146
  return pattern
119
147
 
@@ -194,7 +222,7 @@ def _find_quantified_end(pattern: str, start: int) -> int:
194
222
 
195
223
 
196
224
  def _distribute_length_constraints(
197
- bounds: list[tuple[int, int]], min_length: int | None, max_length: int | None
225
+ bounds: list[tuple[int, int]], repetition_lengths: list[int], min_length: int | None, max_length: int | None
198
226
  ) -> list[tuple[int, int]] | None:
199
227
  """Distribute length constraints among quantified pattern parts."""
200
228
  # Handle exact length case with dynamic programming
@@ -210,18 +238,22 @@ def _distribute_length_constraints(
210
238
  if pos == len(bounds):
211
239
  return [()] if remaining == 0 else None
212
240
 
213
- max_len: int
214
- min_len, max_len = bounds[pos]
215
- if max_len == MAXREPEAT:
216
- max_len = remaining + 1
217
- else:
218
- max_len += 1
241
+ max_repeat: int
242
+ min_repeat, max_repeat = bounds[pos]
243
+ repeat_length = repetition_lengths[pos]
244
+
245
+ if max_repeat == MAXREPEAT:
246
+ max_repeat = remaining // repeat_length + 1 if repeat_length > 0 else remaining + 1
219
247
 
220
248
  # Try each possible length for current quantifier
221
- for length in range(min_len, max_len):
222
- rest = find_valid_combination(pos + 1, remaining - length)
249
+ for repeat_count in range(min_repeat, max_repeat + 1):
250
+ used_length = repeat_count * repeat_length
251
+ if used_length > remaining:
252
+ break
253
+
254
+ rest = find_valid_combination(pos + 1, remaining - used_length)
223
255
  if rest is not None:
224
- dp[(pos, remaining)] = [(length,) + r for r in rest]
256
+ dp[(pos, remaining)] = [(repeat_count,) + r for r in rest]
225
257
  return dp[(pos, remaining)]
226
258
 
227
259
  dp[(pos, remaining)] = None
@@ -262,6 +294,22 @@ def _distribute_length_constraints(
262
294
  return result
263
295
 
264
296
 
297
+ def _calculate_min_repetition_length(subpattern: list) -> int:
298
+ """Calculate minimum length contribution per repetition of a quantified group."""
299
+ total = 0
300
+ for op, value in subpattern:
301
+ if op in [LITERAL, NOT_LITERAL, IN, sre.ANY]:
302
+ total += 1
303
+ elif op == sre.SUBPATTERN:
304
+ _, _, _, inner_pattern = value
305
+ total += _calculate_min_repetition_length(inner_pattern)
306
+ elif op in REPEATS:
307
+ min_repeat, _, inner_pattern = value
308
+ inner_min = _calculate_min_repetition_length(inner_pattern)
309
+ total += min_repeat * inner_min
310
+ return total
311
+
312
+
265
313
  def _get_anchor_length(node_type: int) -> int:
266
314
  """Determine the length of the anchor based on its type."""
267
315
  if node_type in {sre.AT_BEGINNING_STRING, sre.AT_END_STRING, sre.AT_BOUNDARY, sre.AT_NON_BOUNDARY}:
@@ -269,15 +317,28 @@ def _get_anchor_length(node_type: int) -> int:
269
317
  return 1 # ^ or $ or their multiline/locale/unicode variants
270
318
 
271
319
 
272
- def _update_quantifier(op: int, value: tuple, pattern: str, min_length: int | None, max_length: int | None) -> str:
320
+ def _update_quantifier(
321
+ op: int, value: tuple | None, pattern: str, min_length: int | None, max_length: int | None
322
+ ) -> str:
273
323
  """Update the quantifier based on the operation type and given constraints."""
274
- if op in REPEATS:
324
+ if op in REPEATS and value is not None:
275
325
  return _handle_repeat_quantifier(value, pattern, min_length, max_length)
276
- if op in (LITERAL, IN) and max_length != 0:
326
+ if op in (LITERAL, NOT_LITERAL, IN) and max_length != 0:
277
327
  return _handle_literal_or_in_quantifier(pattern, min_length, max_length)
328
+ if op == sre.ANY and value is None:
329
+ # Equivalent to `.` which is in turn is the same as `.{1}`
330
+ return _handle_repeat_quantifier(
331
+ SINGLE_ANY,
332
+ pattern,
333
+ min_length,
334
+ max_length,
335
+ )
278
336
  return pattern
279
337
 
280
338
 
339
+ SINGLE_ANY = sre_parse.parse(".{1}")[0][1]
340
+
341
+
281
342
  def _handle_repeat_quantifier(
282
343
  value: tuple[int, int, tuple], pattern: str, min_length: int | None, max_length: int | None
283
344
  ) -> str:
@@ -324,10 +385,12 @@ def _build_size(min_repeat: int, max_repeat: int, min_length: int | None, max_le
324
385
  def _strip_quantifier(pattern: str) -> str:
325
386
  """Remove quantifier from the pattern."""
326
387
  # Lazy & posessive quantifiers
327
- if pattern.endswith(("*?", "+?", "??", "*+", "?+", "++")):
328
- return pattern[:-2]
329
- if pattern.endswith(("?", "*", "+")):
330
- pattern = pattern[:-1]
388
+ for marker in ("*?", "+?", "??", "*+", "?+", "++"):
389
+ if pattern.endswith(marker) and not pattern.endswith(rf"\{marker}"):
390
+ return pattern[:-2]
391
+ for marker in ("?", "*", "+"):
392
+ if pattern.endswith(marker) and not pattern.endswith(rf"\{marker}"):
393
+ pattern = pattern[:-1]
331
394
  if pattern.endswith("}") and "{" in pattern:
332
395
  # Find the start of the exact quantifier and drop everything since that index
333
396
  idx = pattern.rfind("{")
@@ -1,18 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
- from dataclasses import dataclass
5
4
  from functools import lru_cache
6
5
  from typing import Any, Callable, Dict, Union, overload
7
6
  from urllib.request import urlopen
8
7
 
9
- import jsonschema
10
8
  import requests
11
- from jsonschema.exceptions import RefResolutionError
12
9
 
13
- from ...constants import DEFAULT_RESPONSE_TIMEOUT
14
- from ...internal.copy import fast_deepcopy
15
- from ...loaders import load_yaml
10
+ from schemathesis.core.compat import RefResolutionError, RefResolver
11
+ from schemathesis.core.deserialization import deserialize_yaml
12
+ from schemathesis.core.transforms import deepclone
13
+ from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
14
+
16
15
  from .constants import ALL_KEYWORDS
17
16
  from .converter import to_json_schema_recursive
18
17
  from .utils import get_type
@@ -24,7 +23,7 @@ RECURSION_DEPTH_LIMIT = 100
24
23
  def load_file_impl(location: str, opener: Callable) -> dict[str, Any]:
25
24
  """Load a schema from the given file."""
26
25
  with opener(location) as fd:
27
- return load_yaml(fd)
26
+ return deserialize_yaml(fd)
28
27
 
29
28
 
30
29
  @lru_cache
@@ -41,14 +40,14 @@ def load_file_uri(location: str) -> dict[str, Any]:
41
40
 
42
41
  def load_remote_uri(uri: str) -> Any:
43
42
  """Load the resource and parse it as YAML / JSON."""
44
- response = requests.get(uri, timeout=DEFAULT_RESPONSE_TIMEOUT / 1000)
45
- return load_yaml(response.content)
43
+ response = requests.get(uri, timeout=DEFAULT_RESPONSE_TIMEOUT)
44
+ return deserialize_yaml(response.content)
46
45
 
47
46
 
48
47
  JSONType = Union[None, bool, float, str, list, Dict[str, Any]]
49
48
 
50
49
 
51
- class InliningResolver(jsonschema.RefResolver):
50
+ class InliningResolver(RefResolver):
52
51
  """Inlines resolved schemas."""
53
52
 
54
53
  def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -75,12 +74,10 @@ class InliningResolver(jsonschema.RefResolver):
75
74
  raise
76
75
 
77
76
  @overload
78
- def resolve_all(self, item: dict[str, Any], recursion_level: int = 0) -> dict[str, Any]:
79
- pass
77
+ def resolve_all(self, item: dict[str, Any], recursion_level: int = 0) -> dict[str, Any]: ...
80
78
 
81
79
  @overload
82
- def resolve_all(self, item: list, recursion_level: int = 0) -> list:
83
- pass
80
+ def resolve_all(self, item: list, recursion_level: int = 0) -> list: ...
84
81
 
85
82
  def resolve_all(self, item: JSONType, recursion_level: int = 0) -> JSONType:
86
83
  """Recursively resolve all references in the given object."""
@@ -95,7 +92,7 @@ class InliningResolver(jsonschema.RefResolver):
95
92
  # In other cases, this method create new objects for mutable types (dict & list)
96
93
  next_recursion_level = recursion_level + 1
97
94
  if next_recursion_level > RECURSION_DEPTH_LIMIT:
98
- copied = fast_deepcopy(resolved)
95
+ copied = deepclone(resolved)
99
96
  remove_optional_references(copied)
100
97
  return copied
101
98
  return resolve(resolved, next_recursion_level)
@@ -238,41 +235,3 @@ def remove_optional_references(schema: dict[str, Any]) -> None:
238
235
  clean_additional_properties(definition)
239
236
  for k in on_single_item_combinators(definition):
240
237
  del definition[k]
241
-
242
-
243
- @dataclass
244
- class Unresolvable:
245
- pass
246
-
247
-
248
- UNRESOLVABLE = Unresolvable()
249
-
250
-
251
- def resolve_pointer(document: Any, pointer: str) -> dict | list | str | int | float | None | Unresolvable:
252
- """Implementation is adapted from Rust's `serde-json` crate.
253
-
254
- Ref: https://github.com/serde-rs/json/blob/master/src/value/mod.rs#L751
255
- """
256
- if not pointer:
257
- return document
258
- if not pointer.startswith("/"):
259
- return UNRESOLVABLE
260
-
261
- def replace(value: str) -> str:
262
- return value.replace("~1", "/").replace("~0", "~")
263
-
264
- tokens = map(replace, pointer.split("/")[1:])
265
- target = document
266
- for token in tokens:
267
- if isinstance(target, dict):
268
- target = target.get(token, UNRESOLVABLE)
269
- if target is UNRESOLVABLE:
270
- return UNRESOLVABLE
271
- elif isinstance(target, list):
272
- try:
273
- target = target[int(token)]
274
- except IndexError:
275
- return UNRESOLVABLE
276
- else:
277
- return UNRESOLVABLE
278
- return target