schemathesis 3.35.4__py3-none-any.whl → 3.36.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.
- schemathesis/__init__.py +5 -5
- schemathesis/_hypothesis.py +12 -6
- schemathesis/_override.py +4 -4
- schemathesis/auths.py +1 -1
- schemathesis/checks.py +8 -5
- schemathesis/cli/__init__.py +23 -26
- schemathesis/cli/callbacks.py +6 -4
- schemathesis/cli/cassettes.py +67 -41
- schemathesis/cli/context.py +7 -6
- schemathesis/cli/junitxml.py +1 -1
- schemathesis/cli/options.py +7 -4
- schemathesis/cli/output/default.py +5 -5
- schemathesis/cli/reporting.py +4 -2
- schemathesis/code_samples.py +4 -3
- schemathesis/contrib/unique_data.py +1 -2
- schemathesis/exceptions.py +4 -3
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/pytest_plugin.py +6 -3
- schemathesis/failures.py +2 -1
- schemathesis/filters.py +2 -2
- schemathesis/generation/__init__.py +2 -2
- schemathesis/generation/_hypothesis.py +1 -1
- schemathesis/generation/coverage.py +53 -12
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +3 -3
- schemathesis/internal/checks.py +53 -0
- schemathesis/lazy.py +10 -7
- schemathesis/loaders.py +3 -3
- schemathesis/models.py +59 -23
- schemathesis/runner/__init__.py +12 -6
- schemathesis/runner/events.py +1 -1
- schemathesis/runner/impl/context.py +72 -0
- schemathesis/runner/impl/core.py +105 -67
- schemathesis/runner/impl/solo.py +17 -20
- schemathesis/runner/impl/threadpool.py +65 -72
- schemathesis/runner/serialization.py +4 -3
- schemathesis/sanitization.py +2 -1
- schemathesis/schemas.py +20 -22
- schemathesis/serializers.py +2 -0
- schemathesis/service/client.py +1 -1
- schemathesis/service/events.py +4 -1
- schemathesis/service/extensions.py +2 -2
- schemathesis/service/hosts.py +4 -2
- schemathesis/service/models.py +3 -3
- schemathesis/service/report.py +3 -3
- schemathesis/service/serialization.py +4 -2
- schemathesis/specs/graphql/loaders.py +5 -4
- schemathesis/specs/graphql/schemas.py +13 -8
- schemathesis/specs/openapi/checks.py +76 -27
- schemathesis/specs/openapi/definitions.py +1 -5
- schemathesis/specs/openapi/examples.py +92 -2
- schemathesis/specs/openapi/expressions/__init__.py +7 -0
- schemathesis/specs/openapi/expressions/extractors.py +4 -1
- schemathesis/specs/openapi/expressions/nodes.py +5 -3
- schemathesis/specs/openapi/links.py +4 -4
- schemathesis/specs/openapi/loaders.py +6 -5
- schemathesis/specs/openapi/negative/__init__.py +5 -3
- schemathesis/specs/openapi/negative/mutations.py +5 -4
- schemathesis/specs/openapi/parameters.py +4 -2
- schemathesis/specs/openapi/schemas.py +28 -13
- schemathesis/specs/openapi/security.py +6 -4
- schemathesis/specs/openapi/stateful/__init__.py +2 -2
- schemathesis/specs/openapi/stateful/statistic.py +3 -3
- schemathesis/specs/openapi/stateful/types.py +3 -2
- schemathesis/stateful/__init__.py +3 -3
- schemathesis/stateful/config.py +2 -1
- schemathesis/stateful/context.py +13 -3
- schemathesis/stateful/events.py +3 -3
- schemathesis/stateful/runner.py +24 -6
- schemathesis/stateful/sink.py +1 -1
- schemathesis/stateful/state_machine.py +7 -6
- schemathesis/stateful/statistic.py +3 -1
- schemathesis/stateful/validation.py +10 -5
- schemathesis/transports/__init__.py +2 -2
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +2 -1
- schemathesis/transports/content_types.py +1 -1
- schemathesis/transports/responses.py +2 -1
- schemathesis/utils.py +4 -2
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/METADATA +1 -1
- schemathesis-3.36.0.dist-info/RECORD +157 -0
- schemathesis-3.35.4.dist-info/RECORD +0 -154
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,7 +6,6 @@ import shutil
|
|
|
6
6
|
import textwrap
|
|
7
7
|
import time
|
|
8
8
|
from importlib import metadata
|
|
9
|
-
from queue import Queue
|
|
10
9
|
from types import GeneratorType
|
|
11
10
|
from typing import TYPE_CHECKING, Any, Generator, Literal, cast
|
|
12
11
|
|
|
@@ -44,6 +43,8 @@ from ..handlers import EventHandler
|
|
|
44
43
|
from ..reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
|
|
45
44
|
|
|
46
45
|
if TYPE_CHECKING:
|
|
46
|
+
from queue import Queue
|
|
47
|
+
|
|
47
48
|
import requests
|
|
48
49
|
|
|
49
50
|
SPINNER_REPETITION_NUMBER = 10
|
|
@@ -372,7 +373,7 @@ def display_analysis(context: ExecutionContext) -> None:
|
|
|
372
373
|
click.echo()
|
|
373
374
|
if isinstance(analysis, AnalysisSuccess):
|
|
374
375
|
click.secho(analysis.message, bold=True)
|
|
375
|
-
click.echo("\nAnalysis took: {:.2f}ms"
|
|
376
|
+
click.echo(f"\nAnalysis took: {analysis.elapsed:.2f}ms")
|
|
376
377
|
if analysis.extensions:
|
|
377
378
|
known = []
|
|
378
379
|
failed = []
|
|
@@ -417,8 +418,8 @@ def display_analysis(context: ExecutionContext) -> None:
|
|
|
417
418
|
click.secho("Error\n", fg="red", bold=True)
|
|
418
419
|
_display_service_network_error(response)
|
|
419
420
|
click.echo()
|
|
420
|
-
return
|
|
421
|
-
|
|
421
|
+
return
|
|
422
|
+
if isinstance(exception, requests.RequestException):
|
|
422
423
|
message, extras = extract_requests_exception_details(exception)
|
|
423
424
|
suggestion = "Please check your network connection and try again."
|
|
424
425
|
title = "Network Error"
|
|
@@ -649,7 +650,6 @@ def wait_for_report_handler(queue: Queue, title: str, timeout: float = service.W
|
|
|
649
650
|
|
|
650
651
|
def create_spinner(repetitions: int) -> Generator[str, None, None]:
|
|
651
652
|
"""A simple spinner that yields its individual characters."""
|
|
652
|
-
assert repetitions > 0, "The number of repetitions should be greater than zero"
|
|
653
653
|
while True:
|
|
654
654
|
for ch in "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏":
|
|
655
655
|
# Skip branch coverage, as it is not possible because of the assertion above
|
schemathesis/cli/reporting.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from itertools import groupby
|
|
4
|
-
from typing import Callable, Generator, Iterator
|
|
4
|
+
from typing import TYPE_CHECKING, Callable, Generator, Iterator
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
|
-
from ..code_samples import CodeSampleStyle
|
|
9
8
|
from ..exceptions import RuntimeErrorType
|
|
10
9
|
from ..runner.serialization import SerializedCheck, deduplicate_failures
|
|
11
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..code_samples import CodeSampleStyle
|
|
13
|
+
|
|
12
14
|
TEST_CASE_ID_TITLE = "Test Case ID"
|
|
13
15
|
|
|
14
16
|
|
schemathesis/code_samples.py
CHANGED
|
@@ -6,11 +6,12 @@ from shlex import quote
|
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from .constants import SCHEMATHESIS_TEST_CASE_HEADER
|
|
9
|
-
from .types import Headers
|
|
10
9
|
|
|
11
10
|
if TYPE_CHECKING:
|
|
12
11
|
from requests.structures import CaseInsensitiveDict
|
|
13
12
|
|
|
13
|
+
from .types import Headers
|
|
14
|
+
|
|
14
15
|
|
|
15
16
|
@lru_cache
|
|
16
17
|
def get_excluded_headers() -> CaseInsensitiveDict:
|
|
@@ -120,9 +121,9 @@ def _generate_requests(
|
|
|
120
121
|
url = _escape_single_quotes(url)
|
|
121
122
|
command = f"requests.{method.lower()}('{url}'"
|
|
122
123
|
if body:
|
|
123
|
-
command += f", data={
|
|
124
|
+
command += f", data={body!r}"
|
|
124
125
|
if headers:
|
|
125
|
-
command += f", headers={
|
|
126
|
+
command += f", headers={headers!r}"
|
|
126
127
|
if not verify:
|
|
127
128
|
command += ", verify=False"
|
|
128
129
|
command += ")"
|
|
@@ -13,8 +13,7 @@ if TYPE_CHECKING:
|
|
|
13
13
|
|
|
14
14
|
def install() -> None:
|
|
15
15
|
warnings.warn(
|
|
16
|
-
"The
|
|
17
|
-
"are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
|
|
16
|
+
"The `schemathesis.contrib.unique_data` hook is **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
|
|
18
17
|
"strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
|
|
19
18
|
"This leads to cryptic error messages about external state and flaky test runs, "
|
|
20
19
|
"therefore it will be removed in Schemathesis 4.0",
|
schemathesis/exceptions.py
CHANGED
|
@@ -5,21 +5,22 @@ import re
|
|
|
5
5
|
import traceback
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from hashlib import sha1
|
|
8
|
-
from json import JSONDecodeError
|
|
9
|
-
from types import TracebackType
|
|
10
8
|
from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn
|
|
11
9
|
|
|
12
10
|
from .constants import SERIALIZERS_SUGGESTION_MESSAGE
|
|
13
|
-
from .failures import FailureContext
|
|
14
11
|
from .internal.output import truncate_json
|
|
15
12
|
|
|
16
13
|
if TYPE_CHECKING:
|
|
14
|
+
from json import JSONDecodeError
|
|
15
|
+
from types import TracebackType
|
|
16
|
+
|
|
17
17
|
import hypothesis.errors
|
|
18
18
|
from graphql.error import GraphQLFormattedError
|
|
19
19
|
from jsonschema import RefResolutionError, ValidationError
|
|
20
20
|
from jsonschema import SchemaError as JsonSchemaError
|
|
21
21
|
from requests import RequestException
|
|
22
22
|
|
|
23
|
+
from .failures import FailureContext
|
|
23
24
|
from .transports.responses import GenericResponse
|
|
24
25
|
|
|
25
26
|
|
schemathesis/extra/_flask.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from . import _server
|
|
6
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from flask import Flask
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
def run_server(app: Flask, port: int | None = None, timeout: float = 0.05) -> int:
|
|
9
12
|
"""Start a thread with the given aiohttp application."""
|
|
@@ -3,12 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import unittest
|
|
4
4
|
from contextlib import contextmanager
|
|
5
5
|
from functools import partial
|
|
6
|
-
from typing import Any, Callable, Generator, Type, cast
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Type, cast
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
from _pytest import fixtures, nodes
|
|
10
10
|
from _pytest.config import hookimpl
|
|
11
|
-
from _pytest.fixtures import FuncFixtureInfo
|
|
12
11
|
from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
|
|
13
12
|
from hypothesis import reporting
|
|
14
13
|
from hypothesis.errors import InvalidArgument, Unsatisfiable
|
|
@@ -30,7 +29,6 @@ from ..exceptions import (
|
|
|
30
29
|
UsageError,
|
|
31
30
|
)
|
|
32
31
|
from ..internal.result import Ok, Result
|
|
33
|
-
from ..models import APIOperation
|
|
34
32
|
from ..utils import (
|
|
35
33
|
PARAMETRIZE_MARKER,
|
|
36
34
|
fail_on_no_matches,
|
|
@@ -42,6 +40,11 @@ from ..utils import (
|
|
|
42
40
|
validate_given_args,
|
|
43
41
|
)
|
|
44
42
|
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from _pytest.fixtures import FuncFixtureInfo
|
|
45
|
+
|
|
46
|
+
from ..models import APIOperation
|
|
47
|
+
|
|
45
48
|
|
|
46
49
|
class SchemathesisFunction(Function):
|
|
47
50
|
def __init__(
|
schemathesis/failures.py
CHANGED
|
@@ -2,12 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import textwrap
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from json import JSONDecodeError
|
|
6
5
|
from typing import TYPE_CHECKING, Any
|
|
7
6
|
|
|
8
7
|
from schemathesis.internal.output import OutputConfig
|
|
9
8
|
|
|
10
9
|
if TYPE_CHECKING:
|
|
10
|
+
from json import JSONDecodeError
|
|
11
|
+
|
|
11
12
|
from graphql.error import GraphQLFormattedError
|
|
12
13
|
from jsonschema import ValidationError
|
|
13
14
|
|
schemathesis/filters.py
CHANGED
|
@@ -54,7 +54,7 @@ class Matcher:
|
|
|
54
54
|
func = partial(by_value_list, attribute=attribute, expected=expected)
|
|
55
55
|
else:
|
|
56
56
|
func = partial(by_value, attribute=attribute, expected=expected)
|
|
57
|
-
label = f"{attribute}={
|
|
57
|
+
label = f"{attribute}={expected!r}"
|
|
58
58
|
return cls(func, label=label, _hash=hash(label))
|
|
59
59
|
|
|
60
60
|
@classmethod
|
|
@@ -68,7 +68,7 @@ class Matcher:
|
|
|
68
68
|
flags = 0
|
|
69
69
|
regex = re.compile(regex, flags=flags)
|
|
70
70
|
func = partial(by_regex, attribute=attribute, regex=regex)
|
|
71
|
-
label = f"{attribute}_regex={
|
|
71
|
+
label = f"{attribute}_regex={regex!r}"
|
|
72
72
|
return cls(func, label=label, _hash=hash(label))
|
|
73
73
|
|
|
74
74
|
def match(self, ctx: HasAPIOperation) -> bool:
|
|
@@ -4,8 +4,8 @@ import random
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from ._hypothesis import add_single_example, combine_strategies, get_single_example
|
|
8
|
-
from ._methods import DataGenerationMethod, DataGenerationMethodInput
|
|
7
|
+
from ._hypothesis import add_single_example, combine_strategies, get_single_example
|
|
8
|
+
from ._methods import DataGenerationMethod, DataGenerationMethodInput
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from hypothesis.strategies import SearchStrategy
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
from contextlib import contextmanager, suppress
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from functools import lru_cache
|
|
7
|
-
from typing import Any, Generator,
|
|
7
|
+
from typing import Any, Generator, TypeVar, cast
|
|
8
8
|
|
|
9
9
|
import jsonschema
|
|
10
10
|
from hypothesis import strategies as st
|
|
@@ -140,8 +140,8 @@ def _ignore_unfixable(
|
|
|
140
140
|
*,
|
|
141
141
|
# Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
|
|
142
142
|
# and it may cause errors during the interpreter shutdown
|
|
143
|
-
ref_error:
|
|
144
|
-
schema_error:
|
|
143
|
+
ref_error: type[Exception] = jsonschema.RefResolutionError,
|
|
144
|
+
schema_error: type[Exception] = jsonschema.SchemaError,
|
|
145
145
|
) -> Generator:
|
|
146
146
|
try:
|
|
147
147
|
yield
|
|
@@ -172,7 +172,7 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
|
|
|
172
172
|
yield from _cover_positive_for_type(ctx, schema, ty)
|
|
173
173
|
if DataGenerationMethod.negative in ctx.data_generation_methods:
|
|
174
174
|
template = None
|
|
175
|
-
seen:
|
|
175
|
+
seen: set[Any | tuple[type, str]] = set()
|
|
176
176
|
for key, value in schema.items():
|
|
177
177
|
with _ignore_unfixable():
|
|
178
178
|
if key == "enum":
|
|
@@ -238,7 +238,9 @@ def _get_properties(schema: dict | bool) -> dict | bool:
|
|
|
238
238
|
if isinstance(schema, dict):
|
|
239
239
|
if "example" in schema:
|
|
240
240
|
return {"const": schema["example"]}
|
|
241
|
-
if "
|
|
241
|
+
if "default" in schema:
|
|
242
|
+
return {"const": schema["default"]}
|
|
243
|
+
if schema.get("examples"):
|
|
242
244
|
return {"enum": schema["examples"]}
|
|
243
245
|
if schema.get("type") == "object":
|
|
244
246
|
return _get_template_schema(schema, "object")
|
|
@@ -265,12 +267,19 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
265
267
|
max_length = schema.get("maxLength")
|
|
266
268
|
example = schema.get("example")
|
|
267
269
|
examples = schema.get("examples")
|
|
268
|
-
|
|
270
|
+
default = schema.get("default")
|
|
271
|
+
if example or examples or default:
|
|
269
272
|
if example:
|
|
270
273
|
yield PositiveValue(example)
|
|
271
274
|
if examples:
|
|
272
275
|
for example in examples:
|
|
273
276
|
yield PositiveValue(example)
|
|
277
|
+
if (
|
|
278
|
+
default
|
|
279
|
+
and not (example is not None and default == example)
|
|
280
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
281
|
+
):
|
|
282
|
+
yield PositiveValue(default)
|
|
274
283
|
elif not min_length and not max_length:
|
|
275
284
|
# Default positive value
|
|
276
285
|
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
@@ -327,13 +336,20 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
327
336
|
multiple_of = schema.get("multipleOf")
|
|
328
337
|
example = schema.get("example")
|
|
329
338
|
examples = schema.get("examples")
|
|
339
|
+
default = schema.get("default")
|
|
330
340
|
|
|
331
|
-
if example or examples:
|
|
341
|
+
if example or examples or default:
|
|
332
342
|
if example:
|
|
333
343
|
yield PositiveValue(example)
|
|
334
344
|
if examples:
|
|
335
345
|
for example in examples:
|
|
336
346
|
yield PositiveValue(example)
|
|
347
|
+
if (
|
|
348
|
+
default
|
|
349
|
+
and not (example is not None and default == example)
|
|
350
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
351
|
+
):
|
|
352
|
+
yield PositiveValue(default)
|
|
337
353
|
elif not minimum and not maximum:
|
|
338
354
|
# Default positive value
|
|
339
355
|
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
@@ -382,13 +398,20 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
382
398
|
seen = set()
|
|
383
399
|
example = schema.get("example")
|
|
384
400
|
examples = schema.get("examples")
|
|
401
|
+
default = schema.get("default")
|
|
385
402
|
|
|
386
|
-
if example or examples:
|
|
403
|
+
if example or examples or default:
|
|
387
404
|
if example:
|
|
388
405
|
yield PositiveValue(example)
|
|
389
406
|
if examples:
|
|
390
407
|
for example in examples:
|
|
391
408
|
yield PositiveValue(example)
|
|
409
|
+
if (
|
|
410
|
+
default
|
|
411
|
+
and not (example is not None and default == example)
|
|
412
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
413
|
+
):
|
|
414
|
+
yield PositiveValue(default)
|
|
392
415
|
else:
|
|
393
416
|
yield PositiveValue(template)
|
|
394
417
|
seen.add(len(template))
|
|
@@ -425,19 +448,37 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
425
448
|
def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
|
|
426
449
|
example = schema.get("example")
|
|
427
450
|
examples = schema.get("examples")
|
|
451
|
+
default = schema.get("default")
|
|
428
452
|
|
|
429
|
-
if example or examples:
|
|
453
|
+
if example or examples or default:
|
|
430
454
|
if example:
|
|
431
455
|
yield PositiveValue(example)
|
|
432
456
|
if examples:
|
|
433
457
|
for example in examples:
|
|
434
458
|
yield PositiveValue(example)
|
|
459
|
+
if (
|
|
460
|
+
default
|
|
461
|
+
and not (example is not None and default == example)
|
|
462
|
+
and not (examples is not None and any(default == ex for ex in examples))
|
|
463
|
+
):
|
|
464
|
+
yield PositiveValue(default)
|
|
465
|
+
|
|
435
466
|
else:
|
|
436
467
|
yield PositiveValue(template)
|
|
437
|
-
|
|
468
|
+
|
|
438
469
|
properties = schema.get("properties", {})
|
|
439
|
-
|
|
440
|
-
|
|
470
|
+
required = set(schema.get("required", []))
|
|
471
|
+
optional = list(set(properties) - required)
|
|
472
|
+
optional.sort()
|
|
473
|
+
|
|
474
|
+
# Generate combinations with required properties and one optional property
|
|
475
|
+
for name in optional:
|
|
476
|
+
combo = {k: v for k, v in template.items() if k in required or k == name}
|
|
477
|
+
if combo != template:
|
|
478
|
+
yield PositiveValue(combo)
|
|
479
|
+
# Generate only required properties
|
|
480
|
+
if set(properties) != required:
|
|
481
|
+
only_required = {k: v for k, v in template.items() if k in required}
|
|
441
482
|
yield PositiveValue(only_required)
|
|
442
483
|
seen = set()
|
|
443
484
|
for name, sub_schema in properties.items():
|
schemathesis/graphql.py
CHANGED
schemathesis/hooks.py
CHANGED
|
@@ -6,11 +6,10 @@ from copy import deepcopy
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from enum import Enum, unique
|
|
8
8
|
from functools import partial
|
|
9
|
-
from typing import TYPE_CHECKING, Any, Callable, ClassVar,
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, cast
|
|
10
10
|
|
|
11
11
|
from .filters import FilterSet, attach_filter_chain
|
|
12
12
|
from .internal.deprecation import deprecated_property
|
|
13
|
-
from .types import GenericTest
|
|
14
13
|
|
|
15
14
|
if TYPE_CHECKING:
|
|
16
15
|
from hypothesis import strategies as st
|
|
@@ -18,6 +17,7 @@ if TYPE_CHECKING:
|
|
|
18
17
|
from .models import APIOperation, Case
|
|
19
18
|
from .schemas import BaseSchema
|
|
20
19
|
from .transports.responses import GenericResponse
|
|
20
|
+
from .types import GenericTest
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@unique
|
|
@@ -108,7 +108,7 @@ class HookDispatcher:
|
|
|
108
108
|
"""
|
|
109
109
|
|
|
110
110
|
scope: HookScope
|
|
111
|
-
_hooks:
|
|
111
|
+
_hooks: defaultdict[str, list[Callable]] = field(default_factory=lambda: defaultdict(list))
|
|
112
112
|
_specs: ClassVar[dict[str, RegisteredHook]] = {}
|
|
113
113
|
|
|
114
114
|
def __post_init__(self) -> None:
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import warnings
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Callable, Optional
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..models import Case
|
|
10
|
+
from ..transports.responses import GenericResponse
|
|
11
|
+
from requests.structures import CaseInsensitiveDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[bool]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class CheckContext:
|
|
19
|
+
"""Context for Schemathesis checks.
|
|
20
|
+
|
|
21
|
+
Provides access to broader test execution data beyond individual test cases.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
headers: CaseInsensitiveDict | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def wrap_check(check: Callable) -> CheckFunction:
|
|
28
|
+
"""Make older checks compatible with the new signature."""
|
|
29
|
+
signature = inspect.signature(check)
|
|
30
|
+
parameters = len(signature.parameters)
|
|
31
|
+
|
|
32
|
+
if parameters == 3:
|
|
33
|
+
# New style check, return as is
|
|
34
|
+
return check
|
|
35
|
+
|
|
36
|
+
if parameters == 2:
|
|
37
|
+
# Old style check, wrap it
|
|
38
|
+
warnings.warn(
|
|
39
|
+
f"The check function '{check.__name__}' uses an outdated signature. "
|
|
40
|
+
"Please update it to accept 'ctx' as the first argument: "
|
|
41
|
+
"(ctx: CheckContext, response: GenericResponse, case: Case) -> Optional[bool]",
|
|
42
|
+
DeprecationWarning,
|
|
43
|
+
stacklevel=2,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def wrapper(_: CheckContext, response: GenericResponse, case: Case) -> Optional[bool]:
|
|
47
|
+
return check(response, case)
|
|
48
|
+
|
|
49
|
+
wrapper.__name__ = check.__name__
|
|
50
|
+
|
|
51
|
+
return wrapper
|
|
52
|
+
|
|
53
|
+
raise ValueError(f"Invalid check function signature. Expected 2 or 3 parameters, got {parameters}")
|
schemathesis/lazy.py
CHANGED
|
@@ -2,15 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from inspect import signature
|
|
5
|
-
from typing import Any, Callable, Generator
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
|
-
from _pytest.fixtures import FixtureRequest
|
|
9
8
|
from hypothesis.core import HypothesisHandle
|
|
10
9
|
from hypothesis.errors import Flaky
|
|
11
10
|
from hypothesis.internal.escalation import format_exception, get_trimmed_traceback
|
|
12
11
|
from hypothesis.internal.reflection import impersonate
|
|
13
|
-
from pyrate_limiter import Limiter
|
|
14
12
|
from pytest_subtests import SubTests, nullcontext
|
|
15
13
|
|
|
16
14
|
from ._compat import MultipleFailures, get_interesting_origin
|
|
@@ -20,14 +18,10 @@ from .code_samples import CodeSampleStyle
|
|
|
20
18
|
from .constants import FLAKY_FAILURE_MESSAGE, NOT_SET
|
|
21
19
|
from .exceptions import CheckFailed, OperationSchemaError, SkipTest, get_grouped_exception
|
|
22
20
|
from .filters import FilterSet, FilterValue, MatcherFunc, RegexValue, filter_set_from_components, is_deprecated
|
|
23
|
-
from .generation import DataGenerationMethodInput, GenerationConfig
|
|
24
21
|
from .hooks import HookDispatcher, HookScope
|
|
25
22
|
from .internal.deprecation import warn_filtration_arguments
|
|
26
|
-
from .internal.output import OutputConfig
|
|
27
23
|
from .internal.result import Ok
|
|
28
|
-
from .models import APIOperation
|
|
29
24
|
from .schemas import BaseSchema
|
|
30
|
-
from .types import Filter, GenericTest, NotSet
|
|
31
25
|
from .utils import (
|
|
32
26
|
GivenInput,
|
|
33
27
|
fail_on_no_matches,
|
|
@@ -39,6 +33,15 @@ from .utils import (
|
|
|
39
33
|
validate_given_args,
|
|
40
34
|
)
|
|
41
35
|
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from _pytest.fixtures import FixtureRequest
|
|
38
|
+
from pyrate_limiter import Limiter
|
|
39
|
+
|
|
40
|
+
from .generation import DataGenerationMethodInput, GenerationConfig
|
|
41
|
+
from .internal.output import OutputConfig
|
|
42
|
+
from .models import APIOperation
|
|
43
|
+
from .types import Filter, GenericTest, NotSet
|
|
44
|
+
|
|
42
45
|
|
|
43
46
|
@dataclass
|
|
44
47
|
class LazySchema:
|
schemathesis/loaders.py
CHANGED
|
@@ -51,13 +51,13 @@ def _raise_for_status(response: GenericResponse) -> None:
|
|
|
51
51
|
else:
|
|
52
52
|
type_ = SchemaErrorType.HTTP_CLIENT_ERROR
|
|
53
53
|
else:
|
|
54
|
-
return
|
|
54
|
+
return
|
|
55
55
|
raise SchemaError(message=message, type=type_, url=response.request.url, response=response, extras=[])
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def load_app(path: str) -> Any:
|
|
59
59
|
"""Import an application from a string."""
|
|
60
|
-
path, name = (re.split(
|
|
60
|
+
path, name = ([*re.split(":(?![\\\\/])", path, maxsplit=1), ""])[:2]
|
|
61
61
|
__import__(path)
|
|
62
62
|
# accessing the module from sys.modules returns a proper module, while `__import__`
|
|
63
63
|
# may return a parent module (system dependent)
|
|
@@ -92,7 +92,7 @@ def get_yaml_loader() -> type[yaml.SafeLoader]:
|
|
|
92
92
|
|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
|
|
93
93
|
|[-+]?\.(?:inf|Inf|INF)
|
|
94
94
|
|\.(?:nan|NaN|NAN))$""",
|
|
95
|
-
re.
|
|
95
|
+
re.VERBOSE,
|
|
96
96
|
),
|
|
97
97
|
list("-+0123456789."),
|
|
98
98
|
)
|