schemathesis 3.25.5__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +793 -448
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +24 -4
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +286 -115
  23. schemathesis/cli/output/short.py +25 -6
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +323 -213
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +72 -22
  63. schemathesis/runner/events.py +86 -6
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +447 -187
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/{cli → runner}/probes.py +37 -25
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +17 -4
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +60 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +79 -61
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +143 -31
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +368 -242
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.5.dist-info/METADATA +0 -356
  144. schemathesis-3.25.5.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,43 +1,124 @@
1
1
  from __future__ import annotations
2
+
2
3
  import platform
4
+ import textwrap
3
5
  from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, cast
5
7
 
6
8
  from junit_xml import TestCase, TestSuite, to_xml_report_file
7
9
 
10
+ from ..exceptions import RuntimeErrorType
11
+ from ..internal.output import prepare_response_payload
8
12
  from ..models import Status
9
13
  from ..runner import events
10
- from ..runner.serialization import deduplicate_failures
11
14
  from .handlers import EventHandler
12
-
15
+ from .reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
13
16
 
14
17
  if TYPE_CHECKING:
15
18
  from click.utils import LazyFile
19
+
20
+ from ..runner.serialization import SerializedCheck, SerializedError
16
21
  from .context import ExecutionContext
17
22
 
18
23
 
19
24
  @dataclass
20
25
  class JunitXMLHandler(EventHandler):
21
26
  file_handle: LazyFile
22
- test_cases: list = field(default_factory=list)
27
+ test_cases: dict = field(default_factory=dict)
23
28
 
24
29
  def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
25
- if isinstance(event, events.AfterExecution):
26
- test_case = TestCase(
27
- f"{event.result.method} {event.result.path}",
28
- elapsed_sec=event.elapsed_time,
29
- allow_multiple_subelements=True,
30
- )
31
- if event.status == Status.failure:
32
- checks = deduplicate_failures(event.result.checks)
33
- for idx, check in enumerate(checks, 1):
34
- # `check.message` is always not empty for events with `failure` status
35
- test_case.add_failure_info(message=f"{idx}. {check.message}")
36
- if event.status == Status.error:
37
- test_case.add_error_info(
38
- message=event.result.errors[-1].exception, output=event.result.errors[-1].exception_with_traceback
39
- )
40
- self.test_cases.append(test_case)
41
- if isinstance(event, events.Finished):
42
- test_suites = [TestSuite("schemathesis", test_cases=self.test_cases, hostname=platform.node())]
30
+ if isinstance(event, (events.AfterExecution, events.AfterStatefulExecution)):
31
+ event_: events.AfterExecution | events.AfterStatefulExecution = event
32
+ if isinstance(event_, events.AfterExecution):
33
+ name = f"{event_.result.method} {event_.result.path}"
34
+ else:
35
+ name = event_.result.verbose_name
36
+ if name in self.test_cases:
37
+ test_case = self.test_cases[name]
38
+ test_case.elapsed_sec += event_.elapsed_time
39
+ else:
40
+ test_case = TestCase(name, elapsed_sec=event_.elapsed_time, allow_multiple_subelements=True)
41
+ if event_.status == Status.failure:
42
+ _add_failure(test_case, event_.result.checks, context)
43
+ elif event_.status == Status.error:
44
+ test_case.add_error_info(output=build_error_message(context, event_.result.errors[-1]))
45
+ elif event_.status == Status.skip:
46
+ test_case.add_skipped_info(output=event_.result.skip_reason)
47
+ self.test_cases[name] = test_case
48
+ elif isinstance(event, events.Finished):
49
+ test_suites = [
50
+ TestSuite("schemathesis", test_cases=list(self.test_cases.values()), hostname=platform.node())
51
+ ]
43
52
  to_xml_report_file(file_descriptor=self.file_handle, test_suites=test_suites, prettyprint=True)
53
+
54
+
55
+ def _add_failure(test_case: TestCase, checks: list[SerializedCheck], context: ExecutionContext) -> None:
56
+ messages = []
57
+ for idx, (code_sample, group) in enumerate(group_by_case(checks, context.code_sample_style), 1):
58
+ checks = sorted(group, key=lambda c: c.name != "not_a_server_error")
59
+ messages.append(build_failure_message(context, idx, code_sample, checks))
60
+ test_case.add_failure_info(message="\n\n".join(messages))
61
+
62
+
63
+ def build_failure_message(context: ExecutionContext, idx: int, code_sample: str, checks: list[SerializedCheck]) -> str:
64
+ from ..transports.responses import get_reason
65
+
66
+ message = ""
67
+ for check_idx, check in enumerate(checks):
68
+ if check_idx == 0:
69
+ message += f"{idx}. {TEST_CASE_ID_TITLE}: {check.example.id}\n"
70
+ message += f"\n- {check.title}\n"
71
+ formatted_message = check.formatted_message
72
+ if formatted_message:
73
+ message += f"\n{formatted_message}\n"
74
+ if check_idx + 1 == len(checks):
75
+ if check.response is not None:
76
+ status_code = check.response.status_code
77
+ reason = get_reason(status_code)
78
+ message += f"\n[{check.response.status_code}] {reason}:\n"
79
+ if check.response.body is not None:
80
+ if not check.response.body:
81
+ message += "\n <EMPTY>\n"
82
+ else:
83
+ encoding = check.response.encoding or "utf8"
84
+ try:
85
+ # Checked that is not None
86
+ body = cast(bytes, check.response.deserialize_body())
87
+ payload = body.decode(encoding)
88
+ payload = prepare_response_payload(payload, config=context.output_config)
89
+ payload = textwrap.indent(f"\n`{payload}`\n", prefix=" ")
90
+ message += payload
91
+ except UnicodeDecodeError:
92
+ message += "\n <BINARY>\n"
93
+
94
+ message += f"\nReproduce with: \n\n {code_sample}"
95
+ return message
96
+
97
+
98
+ def build_error_message(context: ExecutionContext, error: SerializedError) -> str:
99
+ message = ""
100
+ if error.title:
101
+ if error.type == RuntimeErrorType.SCHEMA_GENERIC:
102
+ message = "Schema Error\n"
103
+ else:
104
+ message = f"{error.title}\n"
105
+ if error.message:
106
+ message += f"\n{error.message}\n"
107
+ elif error.message:
108
+ message = error.message
109
+ else:
110
+ message = error.exception
111
+ if error.extras:
112
+ extras = error.extras
113
+ elif context.show_trace and error.type.has_useful_traceback:
114
+ extras = split_traceback(error.exception_with_traceback)
115
+ else:
116
+ extras = []
117
+ if extras:
118
+ message += "\n"
119
+ for extra in extras:
120
+ message += f" {extra}\n"
121
+ suggestion = get_runtime_error_suggestion(error.type, bold=str)
122
+ if suggestion is not None:
123
+ message += f"\nTip: {suggestion}"
124
+ return message
@@ -1,11 +1,15 @@
1
1
  from __future__ import annotations
2
- from enum import Enum
3
- from typing import Any, NoReturn
2
+
3
+ from typing import TYPE_CHECKING, Any, NoReturn
4
4
 
5
5
  import click
6
6
 
7
7
  from ..constants import NOT_SET
8
- from ..types import NotSet
8
+
9
+ if TYPE_CHECKING:
10
+ from enum import Enum
11
+
12
+ from ..types import NotSet
9
13
 
10
14
 
11
15
  class CustomHelpMessageChoice(click.Choice):
@@ -54,6 +58,13 @@ class CsvChoice(BaseCsvChoice):
54
58
  self.fail_on_invalid_options(invalid_options, selected)
55
59
 
56
60
 
61
+ class CsvListChoice(click.ParamType):
62
+ def convert( # type: ignore[return]
63
+ self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
64
+ ) -> list[str]:
65
+ return [item for item in value.split(",") if item]
66
+
67
+
57
68
  class OptionalInt(click.types.IntRange):
58
69
  def convert( # type: ignore
59
70
  self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
@@ -64,4 +75,4 @@ class OptionalInt(click.types.IntRange):
64
75
  int(value)
65
76
  return super().convert(value, param, ctx)
66
77
  except ValueError:
67
- self.fail("%s is not a valid integer or None." % value, param, ctx)
78
+ self.fail(f"{value} is not a valid integer or None.", param, ctx)