schemathesis 3.13.0__py3-none-any.whl → 4.4.2__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.
- schemathesis/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1016
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +753 -74
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -1,508 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import os
|
|
3
|
-
import platform
|
|
4
|
-
import shutil
|
|
5
|
-
import time
|
|
6
|
-
from queue import Queue
|
|
7
|
-
from typing import Any, Dict, Generator, List, Optional, Tuple, Union, cast
|
|
8
|
-
|
|
9
|
-
import click
|
|
10
|
-
import requests
|
|
11
|
-
from hypothesis import settings
|
|
12
|
-
|
|
13
|
-
from ... import service
|
|
14
|
-
from ..._compat import metadata
|
|
15
|
-
from ...constants import CodeSampleStyle, __version__
|
|
16
|
-
from ...models import Response, Status
|
|
17
|
-
from ...runner import events
|
|
18
|
-
from ...runner.serialization import SerializedCase, SerializedError, SerializedTestResult, deduplicate_failures
|
|
19
|
-
from ..context import ExecutionContext
|
|
20
|
-
from ..handlers import EventHandler
|
|
21
|
-
|
|
22
|
-
DISABLE_SCHEMA_VALIDATION_MESSAGE = (
|
|
23
|
-
"\nYou can disable input schema validation with --validate-schema=false "
|
|
24
|
-
"command-line option\nIn this case, Schemathesis cannot guarantee proper"
|
|
25
|
-
" behavior during the test run"
|
|
26
|
-
)
|
|
27
|
-
ISSUE_TRACKER_URL = (
|
|
28
|
-
"https://github.com/schemathesis/schemathesis/issues/new?"
|
|
29
|
-
"labels=Status%3A+Review+Needed%2C+Type%3A+Bug&template=bug_report.md&title=%5BBUG%5D"
|
|
30
|
-
)
|
|
31
|
-
SPINNER_REPETITION_NUMBER = 10
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def get_terminal_width() -> int:
|
|
35
|
-
# Some CI/CD providers (e.g. CircleCI) return a (0, 0) terminal size so provide a default
|
|
36
|
-
return shutil.get_terminal_size((80, 24)).columns
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def display_section_name(title: str, separator: str = "=", extra: str = "", **kwargs: Any) -> None:
|
|
40
|
-
"""Print section name with separators in terminal with the given title nicely centered."""
|
|
41
|
-
extra = extra if not extra else f" [{extra}]"
|
|
42
|
-
message = f" {title}{extra} ".center(get_terminal_width(), separator)
|
|
43
|
-
kwargs.setdefault("bold", True)
|
|
44
|
-
click.secho(message, **kwargs)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def display_subsection(result: SerializedTestResult, color: Optional[str] = "red") -> None:
|
|
48
|
-
display_section_name(result.verbose_name, "_", result.data_generation_method, fg=color)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def get_percentage(position: int, length: int) -> str:
|
|
52
|
-
"""Format completion percentage in square brackets."""
|
|
53
|
-
percentage_message = f"{position * 100 // length}%".rjust(4)
|
|
54
|
-
return f"[{percentage_message}]"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def display_execution_result(context: ExecutionContext, event: events.AfterExecution) -> None:
|
|
58
|
-
"""Display an appropriate symbol for the given event's execution result."""
|
|
59
|
-
symbol, color = {Status.success: (".", "green"), Status.failure: ("F", "red"), Status.error: ("E", "red")}[
|
|
60
|
-
event.status
|
|
61
|
-
]
|
|
62
|
-
context.current_line_length += len(symbol)
|
|
63
|
-
click.secho(symbol, nl=False, fg=color)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def display_percentage(context: ExecutionContext, event: events.AfterExecution) -> None:
|
|
67
|
-
"""Add the current progress in % to the right side of the current line."""
|
|
68
|
-
operations_count = cast(int, context.operations_count) # is already initialized via `Initialized` event
|
|
69
|
-
current_percentage = get_percentage(context.operations_processed, operations_count)
|
|
70
|
-
styled = click.style(current_percentage, fg="cyan")
|
|
71
|
-
# Total length of the message, so it will fill to the right border of the terminal.
|
|
72
|
-
# Padding is already taken into account in `context.current_line_length`
|
|
73
|
-
length = max(get_terminal_width() - context.current_line_length + len(styled) - len(current_percentage), 1)
|
|
74
|
-
template = f"{{:>{length}}}"
|
|
75
|
-
click.echo(template.format(styled))
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def display_summary(event: events.Finished) -> None:
|
|
79
|
-
message, color = get_summary_output(event)
|
|
80
|
-
display_section_name(message, fg=color)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def get_summary_message_parts(event: events.Finished) -> List[str]:
|
|
84
|
-
parts = []
|
|
85
|
-
passed = event.passed_count
|
|
86
|
-
if passed:
|
|
87
|
-
parts.append(f"{passed} passed")
|
|
88
|
-
failed = event.failed_count
|
|
89
|
-
if failed:
|
|
90
|
-
parts.append(f"{failed} failed")
|
|
91
|
-
errored = event.errored_count
|
|
92
|
-
if errored:
|
|
93
|
-
parts.append(f"{errored} errored")
|
|
94
|
-
return parts
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def get_summary_output(event: events.Finished) -> Tuple[str, str]:
|
|
98
|
-
parts = get_summary_message_parts(event)
|
|
99
|
-
if not parts:
|
|
100
|
-
message = "Empty test suite"
|
|
101
|
-
color = "yellow"
|
|
102
|
-
else:
|
|
103
|
-
message = f'{", ".join(parts)} in {event.running_time:.2f}s'
|
|
104
|
-
if event.has_failures or event.has_errors:
|
|
105
|
-
color = "red"
|
|
106
|
-
else:
|
|
107
|
-
color = "green"
|
|
108
|
-
return message, color
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def display_hypothesis_output(hypothesis_output: List[str]) -> None:
|
|
112
|
-
"""Show falsifying examples from Hypothesis output if there are any."""
|
|
113
|
-
if hypothesis_output:
|
|
114
|
-
display_section_name("HYPOTHESIS OUTPUT")
|
|
115
|
-
output = "\n".join(hypothesis_output)
|
|
116
|
-
click.secho(output, fg="red")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def display_errors(context: ExecutionContext, event: events.Finished) -> None:
|
|
120
|
-
"""Display all errors in the test run."""
|
|
121
|
-
if not event.has_errors:
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
display_section_name("ERRORS")
|
|
125
|
-
should_display_full_traceback_message = False
|
|
126
|
-
for result in context.results:
|
|
127
|
-
if not result.has_errors:
|
|
128
|
-
continue
|
|
129
|
-
should_display_full_traceback_message |= display_single_error(context, result)
|
|
130
|
-
if event.generic_errors:
|
|
131
|
-
display_generic_errors(context, event.generic_errors)
|
|
132
|
-
if should_display_full_traceback_message and not context.show_errors_tracebacks:
|
|
133
|
-
click.secho(
|
|
134
|
-
"Add this option to your command line parameters to see full tracebacks: --show-errors-tracebacks", fg="red"
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def display_single_error(context: ExecutionContext, result: SerializedTestResult) -> bool:
|
|
139
|
-
display_subsection(result)
|
|
140
|
-
should_display_full_traceback_message = False
|
|
141
|
-
for error in result.errors:
|
|
142
|
-
should_display_full_traceback_message |= _display_error(context, error, result.seed)
|
|
143
|
-
return should_display_full_traceback_message
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def display_generic_errors(context: ExecutionContext, errors: List[SerializedError]) -> None:
|
|
147
|
-
for error in errors:
|
|
148
|
-
display_section_name(error.title or "Generic error", "_", fg="red")
|
|
149
|
-
_display_error(context, error)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def display_full_traceback_message(exception: str) -> bool:
|
|
153
|
-
# Some errors should not trigger the message that suggests to show full tracebacks to the user
|
|
154
|
-
return not exception.startswith("DeadlineExceeded")
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def _display_error(context: ExecutionContext, error: SerializedError, seed: Optional[int] = None) -> bool:
|
|
158
|
-
if context.show_errors_tracebacks:
|
|
159
|
-
message = error.exception_with_traceback
|
|
160
|
-
else:
|
|
161
|
-
message = error.exception
|
|
162
|
-
if error.exception.startswith("InvalidSchema") and context.validate_schema:
|
|
163
|
-
message += DISABLE_SCHEMA_VALIDATION_MESSAGE + "\n"
|
|
164
|
-
if error.exception.startswith("DeadlineExceeded"):
|
|
165
|
-
message += (
|
|
166
|
-
"Consider extending the deadline with the `--hypothesis-deadline` CLI option.\n"
|
|
167
|
-
"You can disable it completely with `--hypothesis-deadline=None`.\n"
|
|
168
|
-
)
|
|
169
|
-
click.secho(message, fg="red")
|
|
170
|
-
if error.example is not None:
|
|
171
|
-
display_example(context, error.example, seed=seed)
|
|
172
|
-
return display_full_traceback_message(error.exception)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def display_failures(context: ExecutionContext, event: events.Finished) -> None:
|
|
176
|
-
"""Display all failures in the test run."""
|
|
177
|
-
if not event.has_failures:
|
|
178
|
-
return
|
|
179
|
-
relevant_results = [result for result in context.results if not result.is_errored]
|
|
180
|
-
if not relevant_results:
|
|
181
|
-
return
|
|
182
|
-
display_section_name("FAILURES")
|
|
183
|
-
for result in relevant_results:
|
|
184
|
-
if not result.has_failures:
|
|
185
|
-
continue
|
|
186
|
-
display_failures_for_single_test(context, result)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def display_failures_for_single_test(context: ExecutionContext, result: SerializedTestResult) -> None:
|
|
190
|
-
"""Display a failure for a single method / path."""
|
|
191
|
-
display_subsection(result)
|
|
192
|
-
checks = deduplicate_failures(result.checks)
|
|
193
|
-
for idx, check in enumerate(checks, 1):
|
|
194
|
-
message: Optional[str]
|
|
195
|
-
if check.message:
|
|
196
|
-
message = f"{idx}. {check.message}"
|
|
197
|
-
else:
|
|
198
|
-
message = None
|
|
199
|
-
display_example(context, check.example, check.response, message, result.seed)
|
|
200
|
-
# Display every time except the last check
|
|
201
|
-
if idx != len(checks):
|
|
202
|
-
click.echo("\n")
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def reduce_schema_error(message: str) -> str:
|
|
206
|
-
"""Reduce the error schema output."""
|
|
207
|
-
end_of_message_index = message.find(":", message.find("Failed validating"))
|
|
208
|
-
if end_of_message_index != -1:
|
|
209
|
-
return message[:end_of_message_index]
|
|
210
|
-
return message
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def display_example(
|
|
214
|
-
context: ExecutionContext,
|
|
215
|
-
case: SerializedCase,
|
|
216
|
-
response: Optional[Response] = None,
|
|
217
|
-
message: Optional[str] = None,
|
|
218
|
-
seed: Optional[int] = None,
|
|
219
|
-
) -> None:
|
|
220
|
-
if message is not None:
|
|
221
|
-
if not context.verbosity:
|
|
222
|
-
message = reduce_schema_error(message)
|
|
223
|
-
click.secho(message, fg="red")
|
|
224
|
-
click.echo()
|
|
225
|
-
for line in case.text_lines:
|
|
226
|
-
click.secho(line, fg="red")
|
|
227
|
-
click.echo()
|
|
228
|
-
if response is not None and response.body is not None:
|
|
229
|
-
payload = base64.b64decode(response.body).decode(response.encoding or "utf8", errors="replace")
|
|
230
|
-
click.secho(f"----------\n\nResponse payload: `{payload}`\n", fg="red")
|
|
231
|
-
if context.code_sample_style == CodeSampleStyle.python:
|
|
232
|
-
click.secho(f"Run this Python code to reproduce this failure: \n\n {case.requests_code}\n", fg="red")
|
|
233
|
-
if context.code_sample_style == CodeSampleStyle.curl:
|
|
234
|
-
click.secho(f"Run this cURL command to reproduce this failure: \n\n {case.curl_code}\n", fg="red")
|
|
235
|
-
if seed is not None:
|
|
236
|
-
click.secho(f"Or add this option to your command line parameters: --hypothesis-seed={seed}", fg="red")
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def display_application_logs(context: ExecutionContext, event: events.Finished) -> None:
|
|
240
|
-
"""Print logs captured during the application run."""
|
|
241
|
-
if not event.has_logs:
|
|
242
|
-
return
|
|
243
|
-
display_section_name("APPLICATION LOGS")
|
|
244
|
-
for result in context.results:
|
|
245
|
-
if not result.has_logs:
|
|
246
|
-
continue
|
|
247
|
-
display_single_log(result)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def display_single_log(result: SerializedTestResult) -> None:
|
|
251
|
-
display_subsection(result, None)
|
|
252
|
-
click.echo("\n\n".join(result.logs))
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def display_statistic(context: ExecutionContext, event: events.Finished) -> None:
|
|
256
|
-
"""Format and print statistic collected by :obj:`models.TestResult`."""
|
|
257
|
-
display_section_name("SUMMARY")
|
|
258
|
-
click.echo()
|
|
259
|
-
total = event.total
|
|
260
|
-
if event.is_empty or not total:
|
|
261
|
-
click.secho("No checks were performed.", bold=True)
|
|
262
|
-
|
|
263
|
-
if total:
|
|
264
|
-
display_checks_statistics(total)
|
|
265
|
-
|
|
266
|
-
if context.cassette_file_name:
|
|
267
|
-
click.echo()
|
|
268
|
-
category = click.style("Network log", bold=True)
|
|
269
|
-
click.secho(f"{category}: {context.cassette_file_name}")
|
|
270
|
-
|
|
271
|
-
if context.junit_xml_file:
|
|
272
|
-
click.echo()
|
|
273
|
-
category = click.style("JUnit XML file", bold=True)
|
|
274
|
-
click.secho(f"{category}: {context.junit_xml_file}")
|
|
275
|
-
|
|
276
|
-
handle_service_integration(context)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def handle_service_integration(context: ExecutionContext) -> None:
|
|
280
|
-
"""If Schemathesis.io integration is enabled, wait for the handler & print the resulting status."""
|
|
281
|
-
if context.service:
|
|
282
|
-
click.echo()
|
|
283
|
-
title = click.style("Schemathesis.io", bold=True)
|
|
284
|
-
event = wait_for_service_handler(context.service.queue, title)
|
|
285
|
-
color = {
|
|
286
|
-
service.Completed: "green",
|
|
287
|
-
service.Error: "red",
|
|
288
|
-
service.Timeout: "red",
|
|
289
|
-
}[event.__class__]
|
|
290
|
-
status = click.style(event.name, fg=color, bold=True)
|
|
291
|
-
click.echo(f"{title}: {status}\r", nl=False)
|
|
292
|
-
click.echo()
|
|
293
|
-
if isinstance(event, service.Completed):
|
|
294
|
-
report_title = click.style("Report", bold=True)
|
|
295
|
-
click.echo(f"{report_title}: {event.short_url}")
|
|
296
|
-
if isinstance(event, service.Error):
|
|
297
|
-
display_service_error(event, context)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
def display_service_error(event: service.Error, context: ExecutionContext) -> None:
|
|
301
|
-
"""Show information about an error during communication with Schemathesis.io."""
|
|
302
|
-
click.echo()
|
|
303
|
-
if isinstance(event.exception, requests.HTTPError):
|
|
304
|
-
status_code = event.exception.response.status_code
|
|
305
|
-
click.secho(f"Schemathesis.io responded with HTTP {status_code}", fg="red")
|
|
306
|
-
if 500 <= status_code <= 599:
|
|
307
|
-
# Server error, should be resolved soon
|
|
308
|
-
click.secho(
|
|
309
|
-
"It is likely that we are already notified about the issue and working on a fix\n"
|
|
310
|
-
"Please, try again in 30 minutes",
|
|
311
|
-
fg="red",
|
|
312
|
-
)
|
|
313
|
-
elif status_code == 401:
|
|
314
|
-
# Likely an invalid token
|
|
315
|
-
click.secho(
|
|
316
|
-
"Please, check that you use the proper CLI upload token\n"
|
|
317
|
-
"See https://schemathesis.readthedocs.io/en/stable/service.html for more details",
|
|
318
|
-
fg="red",
|
|
319
|
-
)
|
|
320
|
-
else:
|
|
321
|
-
# Other client-side errors are likely caused by a bug on the CLI side
|
|
322
|
-
ask_to_report(event)
|
|
323
|
-
elif isinstance(event.exception, requests.RequestException):
|
|
324
|
-
ask_to_report(event, report_to_issues=False)
|
|
325
|
-
else:
|
|
326
|
-
ask_to_report(event)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def ask_to_report(event: service.Error, report_to_issues: bool = True, extra: str = "") -> None:
|
|
330
|
-
# Likely an internal Schemathesis error
|
|
331
|
-
message = event.get_message(True)
|
|
332
|
-
if isinstance(event.exception, requests.RequestException) and event.exception.response is not None:
|
|
333
|
-
response = f"Response: {event.exception.response.text}"
|
|
334
|
-
else:
|
|
335
|
-
response = ""
|
|
336
|
-
if report_to_issues:
|
|
337
|
-
ask = f"Please, consider reporting the traceback below it to our issue tracker: {ISSUE_TRACKER_URL}\n"
|
|
338
|
-
else:
|
|
339
|
-
ask = ""
|
|
340
|
-
click.secho(
|
|
341
|
-
f"An error happened during uploading reports to Schemathesis.io:\n"
|
|
342
|
-
f"{extra}"
|
|
343
|
-
f"{ask}"
|
|
344
|
-
f"{response}"
|
|
345
|
-
f"\n {message.strip()}",
|
|
346
|
-
fg="red",
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
def wait_for_service_handler(queue: Queue, title: str) -> service.Event:
|
|
351
|
-
"""Wait for the Schemathesis.io handler to finish its job."""
|
|
352
|
-
start = time.monotonic()
|
|
353
|
-
spinner = create_spinner(SPINNER_REPETITION_NUMBER)
|
|
354
|
-
# The testing process it done and we need to wait for the Schemathesis.io handler to finish
|
|
355
|
-
# It might still have some data to send
|
|
356
|
-
while queue.empty():
|
|
357
|
-
if time.monotonic() - start >= service.WORKER_FINISH_TIMEOUT:
|
|
358
|
-
return service.Timeout()
|
|
359
|
-
click.echo(f"{title}: {next(spinner)}\r", nl=False)
|
|
360
|
-
time.sleep(service.WORKER_CHECK_PERIOD)
|
|
361
|
-
return queue.get()
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
def create_spinner(repetitions: int) -> Generator[str, None, None]:
|
|
365
|
-
"""A simple spinner that yields its individual characters."""
|
|
366
|
-
assert repetitions > 0, "The number of repetitions should be greater than zero"
|
|
367
|
-
while True:
|
|
368
|
-
for ch in "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏":
|
|
369
|
-
# Skip branch coverage, as it is not possible because of the assertion above
|
|
370
|
-
for _ in range(repetitions): # pragma: no branch
|
|
371
|
-
yield ch
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
def display_checks_statistics(total: Dict[str, Dict[Union[str, Status], int]]) -> None:
|
|
375
|
-
padding = 20
|
|
376
|
-
col1_len = max(map(len, total.keys())) + padding
|
|
377
|
-
col2_len = len(str(max(total.values(), key=lambda v: v["total"])["total"])) * 2 + padding
|
|
378
|
-
col3_len = padding
|
|
379
|
-
click.secho("Performed checks:", bold=True)
|
|
380
|
-
template = f" {{:{col1_len}}}{{:{col2_len}}}{{:{col3_len}}}"
|
|
381
|
-
for check_name, results in total.items():
|
|
382
|
-
display_check_result(check_name, results, template)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def display_check_result(check_name: str, results: Dict[Union[str, Status], int], template: str) -> None:
|
|
386
|
-
"""Show results of single check execution."""
|
|
387
|
-
if Status.failure in results:
|
|
388
|
-
verdict = "FAILED"
|
|
389
|
-
color = "red"
|
|
390
|
-
else:
|
|
391
|
-
verdict = "PASSED"
|
|
392
|
-
color = "green"
|
|
393
|
-
success = results.get(Status.success, 0)
|
|
394
|
-
total = results.get("total", 0)
|
|
395
|
-
click.echo(template.format(check_name, f"{success} / {total} passed", click.style(verdict, fg=color, bold=True)))
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def display_internal_error(context: ExecutionContext, event: events.InternalError) -> None:
|
|
399
|
-
click.secho(event.message, fg="red")
|
|
400
|
-
if event.exception:
|
|
401
|
-
if context.show_errors_tracebacks:
|
|
402
|
-
message = event.exception_with_traceback
|
|
403
|
-
else:
|
|
404
|
-
message = event.exception
|
|
405
|
-
message = (
|
|
406
|
-
f"Error: {message}\n"
|
|
407
|
-
f"Add this option to your command line parameters to see full tracebacks: --show-errors-tracebacks"
|
|
408
|
-
)
|
|
409
|
-
if event.exception_type == "schemathesis.exceptions.SchemaLoadingError":
|
|
410
|
-
message += "\n" + DISABLE_SCHEMA_VALIDATION_MESSAGE
|
|
411
|
-
click.secho(message, fg="red")
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
def handle_initialized(context: ExecutionContext, event: events.Initialized) -> None:
|
|
415
|
-
"""Display information about the test session."""
|
|
416
|
-
context.operations_count = cast(int, event.operations_count) # INVARIANT: should not be `None`
|
|
417
|
-
display_section_name("Schemathesis test session starts")
|
|
418
|
-
versions = (
|
|
419
|
-
f"platform {platform.system()} -- "
|
|
420
|
-
f"Python {platform.python_version()}, "
|
|
421
|
-
f"schemathesis-{__version__}, "
|
|
422
|
-
f"hypothesis-{metadata.version('hypothesis')}, "
|
|
423
|
-
f"hypothesis_jsonschema-{metadata.version('hypothesis_jsonschema')}, "
|
|
424
|
-
f"jsonschema-{metadata.version('jsonschema')}"
|
|
425
|
-
)
|
|
426
|
-
click.echo(versions)
|
|
427
|
-
click.echo(f"rootdir: {os.getcwd()}")
|
|
428
|
-
current_profile = settings._current_profile # type: ignore[attr-defined]
|
|
429
|
-
click.echo(
|
|
430
|
-
f"hypothesis profile '{current_profile}' " # type: ignore
|
|
431
|
-
f"-> {settings.get_profile(current_profile).show_changed()}"
|
|
432
|
-
)
|
|
433
|
-
if event.location is not None:
|
|
434
|
-
click.echo(f"Schema location: {event.location}")
|
|
435
|
-
click.echo(f"Base URL: {event.base_url}")
|
|
436
|
-
click.echo(f"Specification version: {event.specification_name}")
|
|
437
|
-
click.echo(f"Workers: {context.workers_num}")
|
|
438
|
-
click.secho(f"Collected API operations: {context.operations_count}", bold=True)
|
|
439
|
-
if context.service is not None:
|
|
440
|
-
click.secho("Schemathesis.io: ENABLED", bold=True)
|
|
441
|
-
if context.operations_count >= 1:
|
|
442
|
-
click.echo()
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
TRUNCATION_PLACEHOLDER = "[...]"
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
def handle_before_execution(context: ExecutionContext, event: events.BeforeExecution) -> None:
|
|
449
|
-
"""Display what method / path will be tested next."""
|
|
450
|
-
# We should display execution result + percentage in the end. For example:
|
|
451
|
-
max_length = get_terminal_width() - len(" . [XXX%]") - len(TRUNCATION_PLACEHOLDER)
|
|
452
|
-
message = event.verbose_name
|
|
453
|
-
if event.recursion_level > 0:
|
|
454
|
-
message = f"{' ' * event.recursion_level}-> {message}"
|
|
455
|
-
# This value is not `None` - the value is set in runtime before this line
|
|
456
|
-
context.operations_count += 1 # type: ignore
|
|
457
|
-
|
|
458
|
-
message = message[:max_length] + (message[max_length:] and "[...]") + " "
|
|
459
|
-
context.current_line_length = len(message)
|
|
460
|
-
click.echo(message, nl=False)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
def handle_after_execution(context: ExecutionContext, event: events.AfterExecution) -> None:
|
|
464
|
-
"""Display the execution result + current progress at the same line with the method / path names."""
|
|
465
|
-
context.operations_processed += 1
|
|
466
|
-
context.results.append(event.result)
|
|
467
|
-
display_execution_result(context, event)
|
|
468
|
-
display_percentage(context, event)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
|
|
472
|
-
"""Show the outcome of the whole testing session."""
|
|
473
|
-
click.echo()
|
|
474
|
-
display_hypothesis_output(context.hypothesis_output)
|
|
475
|
-
display_errors(context, event)
|
|
476
|
-
display_failures(context, event)
|
|
477
|
-
display_application_logs(context, event)
|
|
478
|
-
display_statistic(context, event)
|
|
479
|
-
click.echo()
|
|
480
|
-
display_summary(event)
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
def handle_interrupted(context: ExecutionContext, event: events.Interrupted) -> None:
|
|
484
|
-
click.echo()
|
|
485
|
-
display_section_name("KeyboardInterrupt", "!", bold=False)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
def handle_internal_error(context: ExecutionContext, event: events.InternalError) -> None:
|
|
489
|
-
display_internal_error(context, event)
|
|
490
|
-
raise click.Abort
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
class DefaultOutputStyleHandler(EventHandler):
|
|
494
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
495
|
-
"""Choose and execute a proper handler for the given event."""
|
|
496
|
-
if isinstance(event, events.Initialized):
|
|
497
|
-
handle_initialized(context, event)
|
|
498
|
-
if isinstance(event, events.BeforeExecution):
|
|
499
|
-
handle_before_execution(context, event)
|
|
500
|
-
if isinstance(event, events.AfterExecution):
|
|
501
|
-
context.hypothesis_output.extend(event.hypothesis_output)
|
|
502
|
-
handle_after_execution(context, event)
|
|
503
|
-
if isinstance(event, events.Finished):
|
|
504
|
-
handle_finished(context, event)
|
|
505
|
-
if isinstance(event, events.Interrupted):
|
|
506
|
-
handle_interrupted(context, event)
|
|
507
|
-
if isinstance(event, events.InternalError):
|
|
508
|
-
handle_internal_error(context, event)
|
schemathesis/cli/output/short.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import click
|
|
2
|
-
|
|
3
|
-
from ...runner import events
|
|
4
|
-
from ..context import ExecutionContext
|
|
5
|
-
from ..handlers import EventHandler
|
|
6
|
-
from . import default
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def handle_before_execution(context: ExecutionContext, event: events.BeforeExecution) -> None:
|
|
10
|
-
if event.recursion_level > 0:
|
|
11
|
-
context.operations_count += 1 # type: ignore
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def handle_after_execution(context: ExecutionContext, event: events.AfterExecution) -> None:
|
|
15
|
-
context.operations_processed += 1
|
|
16
|
-
context.results.append(event.result)
|
|
17
|
-
context.hypothesis_output.extend(event.hypothesis_output)
|
|
18
|
-
default.display_execution_result(context, event)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ShortOutputStyleHandler(EventHandler):
|
|
22
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
23
|
-
"""Short output style shows single symbols in the progress bar.
|
|
24
|
-
|
|
25
|
-
Otherwise, identical to the default output style.
|
|
26
|
-
"""
|
|
27
|
-
if isinstance(event, events.Initialized):
|
|
28
|
-
default.handle_initialized(context, event)
|
|
29
|
-
if isinstance(event, events.BeforeExecution):
|
|
30
|
-
handle_before_execution(context, event)
|
|
31
|
-
if isinstance(event, events.AfterExecution):
|
|
32
|
-
handle_after_execution(context, event)
|
|
33
|
-
if isinstance(event, events.Finished):
|
|
34
|
-
if context.operations_count == context.operations_processed:
|
|
35
|
-
click.echo()
|
|
36
|
-
default.handle_finished(context, event)
|
|
37
|
-
if isinstance(event, events.Interrupted):
|
|
38
|
-
default.handle_interrupted(context, event)
|
|
39
|
-
if isinstance(event, events.InternalError):
|
|
40
|
-
default.handle_internal_error(context, event)
|
schemathesis/constants.py
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from typing import List
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
from packaging import version
|
|
6
|
-
|
|
7
|
-
from ._compat import metadata
|
|
8
|
-
|
|
9
|
-
try:
|
|
10
|
-
__version__ = metadata.version(__package__)
|
|
11
|
-
except metadata.PackageNotFoundError:
|
|
12
|
-
# Local run without installation
|
|
13
|
-
__version__ = "dev"
|
|
14
|
-
|
|
15
|
-
IS_PYTEST_ABOVE_54 = version.parse(pytest.__version__) >= version.parse("5.4.0")
|
|
16
|
-
|
|
17
|
-
USER_AGENT = f"schemathesis/{__version__}"
|
|
18
|
-
# Maximum test running time
|
|
19
|
-
DEFAULT_DEADLINE = 15000 # pragma: no mutate
|
|
20
|
-
DEFAULT_RESPONSE_TIMEOUT = 10000 # pragma: no mutate
|
|
21
|
-
DEFAULT_STATEFUL_RECURSION_LIMIT = 5 # pragma: no mutate
|
|
22
|
-
HTTP_METHODS = frozenset({"get", "put", "post", "delete", "options", "head", "patch", "trace"})
|
|
23
|
-
RECURSIVE_REFERENCE_ERROR_MESSAGE = (
|
|
24
|
-
"Currently, Schemathesis can't generate data for this operation due to "
|
|
25
|
-
"recursive references in the operation definition. See more information in "
|
|
26
|
-
"this issue - https://github.com/schemathesis/schemathesis/issues/947"
|
|
27
|
-
)
|
|
28
|
-
SERIALIZERS_SUGGESTION_MESSAGE = (
|
|
29
|
-
"You can register your own serializer with `schemathesis.serializers.register` "
|
|
30
|
-
"and Schemathesis will be able to make API calls with this media type. \n"
|
|
31
|
-
"See https://schemathesis.readthedocs.io/en/stable/how.html#payload-serialization for more information."
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class DataGenerationMethod(str, Enum):
|
|
36
|
-
"""Defines what data Schemathesis generates for tests."""
|
|
37
|
-
|
|
38
|
-
# Generate data, that fits the API schema
|
|
39
|
-
positive = "positive"
|
|
40
|
-
# Doesn't fit the API schema
|
|
41
|
-
negative = "negative"
|
|
42
|
-
|
|
43
|
-
@classmethod
|
|
44
|
-
def default(cls) -> "DataGenerationMethod":
|
|
45
|
-
return cls.positive
|
|
46
|
-
|
|
47
|
-
@classmethod
|
|
48
|
-
def all(cls) -> List["DataGenerationMethod"]:
|
|
49
|
-
return list(DataGenerationMethod)
|
|
50
|
-
|
|
51
|
-
def as_short_name(self) -> str:
|
|
52
|
-
return {
|
|
53
|
-
DataGenerationMethod.positive: "P",
|
|
54
|
-
DataGenerationMethod.negative: "N",
|
|
55
|
-
}[self]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class CodeSampleStyle(str, Enum):
|
|
62
|
-
"""Controls the style of code samples for failure reproduction."""
|
|
63
|
-
|
|
64
|
-
python = "python"
|
|
65
|
-
curl = "curl"
|
|
66
|
-
|
|
67
|
-
@classmethod
|
|
68
|
-
def default(cls) -> "CodeSampleStyle":
|
|
69
|
-
return cls.curl
|
|
70
|
-
|
|
71
|
-
@classmethod
|
|
72
|
-
def from_str(cls, value: str) -> "CodeSampleStyle":
|
|
73
|
-
try:
|
|
74
|
-
return cls[value]
|
|
75
|
-
except KeyError:
|
|
76
|
-
available_styles = ", ".join(cls)
|
|
77
|
-
raise ValueError(
|
|
78
|
-
f"Invalid value for code sample style: {value}. Available styles: {available_styles}"
|
|
79
|
-
) from None
|