schemathesis 3.37.0__py3-none-any.whl → 3.38.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 (35) hide show
  1. schemathesis/_hypothesis.py +18 -8
  2. schemathesis/_patches.py +21 -0
  3. schemathesis/cli/__init__.py +1 -1
  4. schemathesis/cli/cassettes.py +6 -0
  5. schemathesis/extra/pytest_plugin.py +1 -1
  6. schemathesis/generation/_hypothesis.py +2 -0
  7. schemathesis/generation/coverage.py +257 -59
  8. schemathesis/hooks.py +4 -0
  9. schemathesis/internal/checks.py +4 -2
  10. schemathesis/internal/diff.py +15 -0
  11. schemathesis/models.py +65 -3
  12. schemathesis/parameters.py +5 -0
  13. schemathesis/runner/impl/context.py +10 -1
  14. schemathesis/runner/impl/core.py +14 -4
  15. schemathesis/runner/serialization.py +6 -3
  16. schemathesis/serializers.py +3 -0
  17. schemathesis/service/extensions.py +1 -1
  18. schemathesis/service/metadata.py +3 -3
  19. schemathesis/specs/openapi/_hypothesis.py +7 -46
  20. schemathesis/specs/openapi/checks.py +7 -2
  21. schemathesis/specs/openapi/converter.py +27 -11
  22. schemathesis/specs/openapi/formats.py +44 -0
  23. schemathesis/specs/openapi/links.py +4 -0
  24. schemathesis/specs/openapi/negative/mutations.py +5 -0
  25. schemathesis/specs/openapi/parameters.py +21 -14
  26. schemathesis/specs/openapi/schemas.py +6 -2
  27. schemathesis/stateful/context.py +1 -1
  28. schemathesis/stateful/runner.py +6 -2
  29. schemathesis/transports/__init__.py +4 -0
  30. schemathesis/utils.py +6 -4
  31. {schemathesis-3.37.0.dist-info → schemathesis-3.38.0.dist-info}/METADATA +2 -1
  32. {schemathesis-3.37.0.dist-info → schemathesis-3.38.0.dist-info}/RECORD +35 -33
  33. {schemathesis-3.37.0.dist-info → schemathesis-3.38.0.dist-info}/WHEEL +0 -0
  34. {schemathesis-3.37.0.dist-info → schemathesis-3.38.0.dist-info}/entry_points.txt +0 -0
  35. {schemathesis-3.37.0.dist-info → schemathesis-3.38.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/models.py CHANGED
@@ -16,6 +16,7 @@ from typing import (
16
16
  Generator,
17
17
  Generic,
18
18
  Iterator,
19
+ Literal,
19
20
  NoReturn,
20
21
  Sequence,
21
22
  Type,
@@ -26,6 +27,7 @@ from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
26
27
 
27
28
  from . import serializers
28
29
  from ._dependency_versions import IS_WERKZEUG_ABOVE_3
30
+ from ._override import CaseOverride
29
31
  from .code_samples import CodeSampleStyle
30
32
  from .constants import (
31
33
  NOT_SET,
@@ -48,6 +50,7 @@ from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, dispatch
48
50
  from .internal.checks import CheckContext
49
51
  from .internal.copy import fast_deepcopy
50
52
  from .internal.deprecation import deprecated_function, deprecated_property
53
+ from .internal.diff import diff
51
54
  from .internal.output import prepare_response_payload
52
55
  from .parameters import Parameter, ParameterSet, PayloadAlternatives
53
56
  from .sanitization import sanitize_request, sanitize_response
@@ -61,6 +64,7 @@ if TYPE_CHECKING:
61
64
  import requests.auth
62
65
  import werkzeug
63
66
  from hypothesis import strategies as st
67
+ from hypothesis.vendor.pretty import RepresentationPrinter
64
68
  from requests.structures import CaseInsensitiveDict
65
69
 
66
70
  from .auths import AuthStorage
@@ -155,8 +159,9 @@ class GenerationMetadata:
155
159
  body: DataGenerationMethod | None
156
160
  phase: TestPhase
157
161
  description: str | None
162
+ location: str | None
158
163
 
159
- __slots__ = ("query", "path_parameters", "headers", "cookies", "body", "phase", "description")
164
+ __slots__ = ("query", "path_parameters", "headers", "cookies", "body", "phase", "description", "location")
160
165
 
161
166
 
162
167
  @dataclass(repr=False)
@@ -186,6 +191,26 @@ class Case:
186
191
  _auth: requests.auth.AuthBase | None = None
187
192
  _has_explicit_auth: bool = False
188
193
 
194
+ def __post_init__(self) -> None:
195
+ self._original_path_parameters = self.path_parameters.copy() if self.path_parameters else None
196
+ self._original_headers = self.headers.copy() if self.headers else None
197
+ self._original_cookies = self.cookies.copy() if self.cookies else None
198
+ self._original_query = self.query.copy() if self.query else None
199
+
200
+ def _has_generated_component(self, name: str) -> bool:
201
+ assert name in ["path_parameters", "headers", "cookies", "query"]
202
+ if self.meta is None:
203
+ return False
204
+ return getattr(self.meta, name) is not None
205
+
206
+ def _get_diff(self, component: Literal["path_parameters", "headers", "query", "cookies"]) -> dict[str, Any]:
207
+ original = getattr(self, f"_original_{component}")
208
+ current = getattr(self, component)
209
+ if not (current and original):
210
+ return {}
211
+ original_value = original if self._has_generated_component(component) else {}
212
+ return diff(original_value, current)
213
+
189
214
  def __repr__(self) -> str:
190
215
  parts = [f"{self.__class__.__name__}("]
191
216
  first = True
@@ -202,6 +227,18 @@ class Case:
202
227
  def __hash__(self) -> int:
203
228
  return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
204
229
 
230
+ @property
231
+ def _override(self) -> CaseOverride:
232
+ return CaseOverride(
233
+ path_parameters=self._get_diff("path_parameters"),
234
+ headers=self._get_diff("headers"),
235
+ query=self._get_diff("query"),
236
+ cookies=self._get_diff("cookies"),
237
+ )
238
+
239
+ def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
240
+ return None
241
+
205
242
  @deprecated_property(removed_in="4.0", replacement="`operation`")
206
243
  def endpoint(self) -> APIOperation:
207
244
  return self.operation
@@ -456,7 +493,9 @@ class Case:
456
493
  checks = tuple(check for check in checks if check not in excluded_checks)
457
494
  additional_checks = tuple(check for check in _additional_checks if check not in excluded_checks)
458
495
  failed_checks = []
459
- ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
496
+ ctx = CheckContext(
497
+ override=self._override, auth=None, headers=CaseInsensitiveDict(headers) if headers else None
498
+ )
460
499
  for check in chain(checks, additional_checks):
461
500
  copied_case = self.partial_deepcopy()
462
501
  try:
@@ -525,12 +564,21 @@ class Case:
525
564
  session: requests.Session | None = None,
526
565
  headers: dict[str, Any] | None = None,
527
566
  checks: tuple[CheckFunction, ...] = (),
567
+ additional_checks: tuple[CheckFunction, ...] = (),
568
+ excluded_checks: tuple[CheckFunction, ...] = (),
528
569
  code_sample_style: str | None = None,
529
570
  **kwargs: Any,
530
571
  ) -> requests.Response:
531
572
  __tracebackhide__ = True
532
573
  response = self.call(base_url, session, headers, **kwargs)
533
- self.validate_response(response, checks, code_sample_style=code_sample_style, headers=headers)
574
+ self.validate_response(
575
+ response,
576
+ checks,
577
+ code_sample_style=code_sample_style,
578
+ headers=headers,
579
+ additional_checks=additional_checks,
580
+ excluded_checks=excluded_checks,
581
+ )
534
582
  return response
535
583
 
536
584
  def _get_url(self, base_url: str | None) -> str:
@@ -606,6 +654,9 @@ class OperationDefinition(Generic[D]):
606
654
 
607
655
  __slots__ = ("raw", "resolved", "scope")
608
656
 
657
+ def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
658
+ return None
659
+
609
660
 
610
661
  C = TypeVar("C", bound=Case)
611
662
 
@@ -1018,7 +1069,10 @@ class Interaction:
1018
1069
  status: Status
1019
1070
  data_generation_method: DataGenerationMethod
1020
1071
  phase: TestPhase | None
1072
+ # `description` & `location` are related to metadata about this interaction
1073
+ # NOTE: It will be better to keep it in a separate attribute
1021
1074
  description: str | None
1075
+ location: str | None
1022
1076
  recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
1023
1077
 
1024
1078
  @classmethod
@@ -1049,6 +1103,7 @@ class Interaction:
1049
1103
  data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
1050
1104
  phase=case.meta.phase if case.meta is not None else None,
1051
1105
  description=case.meta.description if case.meta is not None else None,
1106
+ location=case.meta.location if case.meta is not None else None,
1052
1107
  )
1053
1108
 
1054
1109
  @classmethod
@@ -1073,6 +1128,7 @@ class Interaction:
1073
1128
  data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
1074
1129
  phase=case.meta.phase if case.meta is not None else None,
1075
1130
  description=case.meta.description if case.meta is not None else None,
1131
+ location=case.meta.location if case.meta is not None else None,
1076
1132
  )
1077
1133
 
1078
1134
 
@@ -1098,6 +1154,9 @@ class TestResult:
1098
1154
  # DEPRECATED: Seed is the same per test run
1099
1155
  seed: int | None = None
1100
1156
 
1157
+ def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
1158
+ return None
1159
+
1101
1160
  def mark_errored(self) -> None:
1102
1161
  self.is_errored = True
1103
1162
 
@@ -1191,6 +1250,9 @@ class TestResultSet:
1191
1250
  generic_errors: list[OperationSchemaError] = field(default_factory=list)
1192
1251
  warnings: list[str] = field(default_factory=list)
1193
1252
 
1253
+ def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
1254
+ return None
1255
+
1194
1256
  def __iter__(self) -> Iterator[TestResult]:
1195
1257
  return iter(self.results)
1196
1258
 
@@ -9,6 +9,8 @@ from dataclasses import dataclass, field
9
9
  from typing import TYPE_CHECKING, Any, Generator, Generic, TypeVar
10
10
 
11
11
  if TYPE_CHECKING:
12
+ from hypothesis.vendor.pretty import RepresentationPrinter
13
+
12
14
  from .models import APIOperation
13
15
 
14
16
 
@@ -55,6 +57,9 @@ class ParameterSet(Generic[P]):
55
57
 
56
58
  items: list[P] = field(default_factory=list)
57
59
 
60
+ def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
61
+ return None
62
+
58
63
  def add(self, parameter: P) -> None:
59
64
  """Add a new parameter."""
60
65
  self.items.append(parameter)
@@ -10,6 +10,9 @@ from ...models import TestResult, TestResultSet
10
10
  if TYPE_CHECKING:
11
11
  import threading
12
12
 
13
+ from hypothesis.vendor.pretty import RepresentationPrinter
14
+
15
+ from ..._override import CaseOverride
13
16
  from ...exceptions import OperationSchemaError
14
17
  from ...models import Case
15
18
  from ...types import NotSet, RawAuth
@@ -26,8 +29,9 @@ class RunnerContext:
26
29
  unique_data: bool
27
30
  outcome_cache: dict[int, BaseException | None]
28
31
  checks_config: CheckConfig
32
+ override: CaseOverride | None
29
33
 
30
- __slots__ = ("data", "auth", "seed", "stop_event", "unique_data", "outcome_cache", "checks_config")
34
+ __slots__ = ("data", "auth", "seed", "stop_event", "unique_data", "outcome_cache", "checks_config", "override")
31
35
 
32
36
  def __init__(
33
37
  self,
@@ -37,6 +41,7 @@ class RunnerContext:
37
41
  stop_event: threading.Event,
38
42
  unique_data: bool,
39
43
  checks_config: CheckConfig,
44
+ override: CaseOverride | None,
40
45
  ) -> None:
41
46
  self.data = TestResultSet(seed=seed)
42
47
  self.auth = auth
@@ -45,6 +50,10 @@ class RunnerContext:
45
50
  self.outcome_cache = {}
46
51
  self.unique_data = unique_data
47
52
  self.checks_config = checks_config
53
+ self.override = override
54
+
55
+ def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
56
+ return None
48
57
 
49
58
  @property
50
59
  def is_stopped(self) -> bool:
@@ -138,6 +138,7 @@ class BaseRunner:
138
138
  stop_event=stop_event,
139
139
  unique_data=self.unique_data,
140
140
  checks_config=self.checks_config,
141
+ override=self.override,
141
142
  )
142
143
  start_time = time.monotonic()
143
144
  initialized = None
@@ -429,7 +430,7 @@ def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.Pr
429
430
  results = probes.run(schema, config)
430
431
  for result in results:
431
432
  if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
432
- from ...specs.openapi._hypothesis import HEADER_FORMAT, header_values
433
+ from ...specs.openapi.formats import HEADER_FORMAT, header_values
433
434
 
434
435
  formats.register(HEADER_FORMAT, header_values(blacklist_characters="\n\r\x00"))
435
436
  return results
@@ -1025,7 +1026,10 @@ def _network_test(
1025
1026
  status = Status.success
1026
1027
 
1027
1028
  check_ctx = CheckContext(
1028
- auth=ctx.auth, headers=CaseInsensitiveDict(headers) if headers else None, config=ctx.checks_config
1029
+ override=ctx.override,
1030
+ auth=ctx.auth,
1031
+ headers=CaseInsensitiveDict(headers) if headers else None,
1032
+ config=ctx.checks_config,
1029
1033
  )
1030
1034
  try:
1031
1035
  run_checks(
@@ -1119,7 +1123,10 @@ def _wsgi_test(
1119
1123
  status = Status.success
1120
1124
  check_results: list[Check] = []
1121
1125
  check_ctx = CheckContext(
1122
- auth=ctx.auth, headers=CaseInsensitiveDict(headers) if headers else None, config=ctx.checks_config
1126
+ override=ctx.override,
1127
+ auth=ctx.auth,
1128
+ headers=CaseInsensitiveDict(headers) if headers else None,
1129
+ config=ctx.checks_config,
1123
1130
  )
1124
1131
  try:
1125
1132
  run_checks(
@@ -1201,7 +1208,10 @@ def _asgi_test(
1201
1208
  status = Status.success
1202
1209
  check_results: list[Check] = []
1203
1210
  check_ctx = CheckContext(
1204
- auth=ctx.auth, headers=CaseInsensitiveDict(headers) if headers else None, config=ctx.checks_config
1211
+ override=ctx.override,
1212
+ auth=ctx.auth,
1213
+ headers=CaseInsensitiveDict(headers) if headers else None,
1214
+ config=ctx.checks_config,
1205
1215
  )
1206
1216
  try:
1207
1217
  run_checks(
@@ -275,9 +275,10 @@ class SerializedError:
275
275
  message = f"Scalar type '{scalar_name}' is not recognized"
276
276
  extras = []
277
277
  title = "Unknown GraphQL Scalar"
278
- elif isinstance(exception, hypothesis.errors.InvalidArgument) and (
279
- str(exception).endswith("larger than Hypothesis is designed to handle")
280
- or "can neber generate an example, because min_size is larger than Hypothesis suports."
278
+ elif (
279
+ isinstance(exception, hypothesis.errors.InvalidArgument)
280
+ and str(exception).endswith("larger than Hypothesis is designed to handle")
281
+ or "can never generate an example, because min_size is larger than Hypothesis supports" in str(exception)
281
282
  ):
282
283
  type_ = RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE
283
284
  message = HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE
@@ -396,6 +397,7 @@ class SerializedInteraction:
396
397
  data_generation_method: DataGenerationMethod
397
398
  phase: TestPhase | None
398
399
  description: str | None
400
+ location: str | None
399
401
  recorded_at: str
400
402
 
401
403
  @classmethod
@@ -408,6 +410,7 @@ class SerializedInteraction:
408
410
  data_generation_method=interaction.data_generation_method,
409
411
  phase=interaction.phase,
410
412
  description=interaction.description,
413
+ location=interaction.location,
411
414
  recorded_at=interaction.recorded_at,
412
415
  )
413
416
 
@@ -45,6 +45,9 @@ class Binary(str):
45
45
 
46
46
  __slots__ = ("data",)
47
47
 
48
+ def __hash__(self) -> int:
49
+ return hash(self.data)
50
+
48
51
 
49
52
  @dataclass
50
53
  class SerializerContext:
@@ -204,7 +204,7 @@ def make_strftime(format: str) -> Callable:
204
204
 
205
205
 
206
206
  def _get_map_function(definition: TransformFunctionDefinition) -> Result[Callable | None, Exception]:
207
- from ..specs.openapi._hypothesis import Binary
207
+ from ..serializers import Binary
208
208
 
209
209
  TRANSFORM_FACTORIES: dict[str, Callable] = {
210
210
  "str": lambda: str,
@@ -35,7 +35,7 @@ class CliMetadata:
35
35
  version: str = SCHEMATHESIS_VERSION
36
36
 
37
37
 
38
- DEPDENDENCY_NAMES = ["hypothesis", "hypothesis-jsonschema", "hypothesis-graphql"]
38
+ DEPENDENCY_NAMES = ["hypothesis", "hypothesis-jsonschema", "hypothesis-graphql"]
39
39
 
40
40
 
41
41
  @dataclass
@@ -53,7 +53,7 @@ class Dependency:
53
53
 
54
54
 
55
55
  def collect_dependency_versions() -> list[Dependency]:
56
- return [Dependency.from_name(name) for name in DEPDENDENCY_NAMES]
56
+ return [Dependency.from_name(name) for name in DEPENDENCY_NAMES]
57
57
 
58
58
 
59
59
  @dataclass
@@ -68,4 +68,4 @@ class Metadata:
68
68
  cli: CliMetadata = field(default_factory=CliMetadata)
69
69
  # Used Docker image if any
70
70
  docker_image: str | None = field(default_factory=lambda: os.getenv(DOCKER_IMAGE_ENV_VAR))
71
- depdenencies: list[Dependency] = field(default_factory=collect_dependency_versions)
71
+ depedenencies: list[Dependency] = field(default_factory=collect_dependency_versions)
@@ -1,11 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- import string
4
3
  import time
5
- from base64 import b64encode
6
4
  from contextlib import suppress
7
5
  from dataclasses import dataclass
8
- from functools import lru_cache
9
6
  from typing import Any, Callable, Dict, Iterable, Optional
10
7
  from urllib.parse import quote_plus
11
8
  from weakref import WeakKeyDictionary
@@ -13,7 +10,6 @@ from weakref import WeakKeyDictionary
13
10
  from hypothesis import reject
14
11
  from hypothesis import strategies as st
15
12
  from hypothesis_jsonschema import from_schema
16
- from requests.auth import _basic_auth_str
17
13
  from requests.structures import CaseInsensitiveDict
18
14
  from requests.utils import to_key_val_list
19
15
 
@@ -26,56 +22,22 @@ from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
26
22
  from ...internal.copy import fast_deepcopy
27
23
  from ...internal.validation import is_illegal_surrogate
28
24
  from ...models import APIOperation, Case, GenerationMetadata, TestPhase, cant_serialize
29
- from ...serializers import Binary
30
25
  from ...transports.content_types import parse_content_type
31
26
  from ...transports.headers import has_invalid_characters, is_latin_1_encodable
32
27
  from ...types import NotSet
33
- from ...utils import compose, skip
28
+ from ...utils import skip
34
29
  from .constants import LOCATION_TO_CONTAINER
35
- from .formats import STRING_FORMATS
30
+ from .formats import HEADER_FORMAT, STRING_FORMATS, get_default_format_strategies, header_values
36
31
  from .media_types import MEDIA_TYPES
37
32
  from .negative import negative_schema
38
33
  from .negative.utils import can_negate
39
34
  from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
40
35
  from .utils import is_header_location
41
36
 
42
- HEADER_FORMAT = "_header_value"
43
37
  SLASH = "/"
44
38
  StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str], GenerationConfig], st.SearchStrategy]
45
39
 
46
40
 
47
- def header_values(blacklist_characters: str = "\n\r") -> st.SearchStrategy[str]:
48
- return st.text(
49
- alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters=blacklist_characters)
50
- # Header values with leading non-visible chars can't be sent with `requests`
51
- ).map(str.lstrip)
52
-
53
-
54
- @lru_cache
55
- def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
56
- """Get all default "format" strategies."""
57
-
58
- def make_basic_auth_str(item: tuple[str, str]) -> str:
59
- return _basic_auth_str(*item)
60
-
61
- latin1_text = st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255))
62
-
63
- # Define valid characters here to avoid filtering them out in `is_valid_header` later
64
- header_value = header_values()
65
-
66
- return {
67
- "binary": st.binary().map(Binary),
68
- "byte": st.binary().map(lambda x: b64encode(x).decode()),
69
- # RFC 7230, Section 3.2.6
70
- "_header_name": st.text(
71
- min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)
72
- ),
73
- HEADER_FORMAT: header_value,
74
- "_basic_auth": st.tuples(latin1_text, latin1_text).map(make_basic_auth_str),
75
- "_bearer_auth": header_value.map("Bearer {}".format),
76
- }
77
-
78
-
79
41
  def is_valid_header(headers: dict[str, Any]) -> bool:
80
42
  """Verify if the generated headers are valid."""
81
43
  for name, value in headers.items():
@@ -216,6 +178,7 @@ def get_case_strategy(
216
178
  body=body_.generator,
217
179
  phase=phase,
218
180
  description=None,
181
+ location=None,
219
182
  ),
220
183
  )
221
184
  auth_context = auths.AuthContext(
@@ -417,12 +380,10 @@ def get_parameters_strategy(
417
380
  # `True` / `False` / `None` improves chances of them passing validation in apps
418
381
  # that expect boolean / null types
419
382
  # and not aware of Python-specific representation of those types
420
- map_func = {
421
- "path": compose(quote_all, jsonify_python_specific_types),
422
- "query": jsonify_python_specific_types,
423
- }.get(location)
424
- if map_func:
425
- strategy = strategy.map(map_func) # type: ignore
383
+ if location == "path":
384
+ strategy = strategy.map(quote_all).map(jsonify_python_specific_types)
385
+ elif location == "query":
386
+ strategy = strategy.map(jsonify_python_specific_types)
426
387
  _PARAMETER_STRATEGIES_CACHE.setdefault(operation, {})[nested_cache_key] = strategy
427
388
  return strategy
428
389
  # No parameters defined for this location
@@ -476,17 +476,22 @@ def _contains_auth(
476
476
  return p["in"] == "cookie" and (p["name"] in cookies or p["name"] in header_cookies)
477
477
 
478
478
  for parameter in security_parameters:
479
+ name = parameter["name"]
479
480
  if has_header(parameter):
480
- if ctx.headers is not None and parameter["name"] in ctx.headers:
481
+ if (ctx.headers is not None and name in ctx.headers) or (ctx.override and name in ctx.override.headers):
481
482
  return AuthKind.EXPLICIT
482
483
  return AuthKind.GENERATED
483
484
  if has_cookie(parameter):
484
485
  if ctx.headers is not None and "Cookie" in ctx.headers:
485
486
  cookies = cast(RequestsCookieJar, ctx.headers["Cookie"]) # type: ignore
486
- if parameter["name"] in cookies:
487
+ if name in cookies:
487
488
  return AuthKind.EXPLICIT
489
+ if ctx.override and name in ctx.override.cookies:
490
+ return AuthKind.EXPLICIT
488
491
  return AuthKind.GENERATED
489
492
  if has_query(parameter):
493
+ if ctx.override and name in ctx.override.query:
494
+ return AuthKind.EXPLICIT
490
495
  return AuthKind.GENERATED
491
496
 
492
497
  return None
@@ -9,7 +9,12 @@ from .patterns import update_quantifier
9
9
 
10
10
 
11
11
  def to_json_schema(
12
- schema: dict[str, Any], *, nullable_name: str, copy: bool = True, is_response_schema: bool = False
12
+ schema: dict[str, Any],
13
+ *,
14
+ nullable_name: str,
15
+ copy: bool = True,
16
+ is_response_schema: bool = False,
17
+ update_quantifiers: bool = True,
13
18
  ) -> dict[str, Any]:
14
19
  """Convert Open API parameters to JSON Schema.
15
20
 
@@ -25,6 +30,19 @@ def to_json_schema(
25
30
  if schema_type == "file":
26
31
  schema["type"] = "string"
27
32
  schema["format"] = "binary"
33
+ if update_quantifiers:
34
+ update_pattern_in_schema(schema)
35
+ if schema_type == "object":
36
+ if is_response_schema:
37
+ # Write-only properties should not occur in responses
38
+ rewrite_properties(schema, is_write_only)
39
+ else:
40
+ # Read-only properties should not occur in requests
41
+ rewrite_properties(schema, is_read_only)
42
+ return schema
43
+
44
+
45
+ def update_pattern_in_schema(schema: dict[str, Any]) -> None:
28
46
  pattern = schema.get("pattern")
29
47
  min_length = schema.get("minLength")
30
48
  max_length = schema.get("maxLength")
@@ -34,14 +52,6 @@ def to_json_schema(
34
52
  schema.pop("minLength", None)
35
53
  schema.pop("maxLength", None)
36
54
  schema["pattern"] = new_pattern
37
- if schema_type == "object":
38
- if is_response_schema:
39
- # Write-only properties should not occur in responses
40
- rewrite_properties(schema, is_write_only)
41
- else:
42
- # Read-only properties should not occur in requests
43
- rewrite_properties(schema, is_read_only)
44
- return schema
45
55
 
46
56
 
47
57
  def rewrite_properties(schema: dict[str, Any], predicate: Callable[[dict[str, Any]], bool]) -> None:
@@ -82,6 +92,12 @@ def is_read_only(schema: dict[str, Any] | bool) -> bool:
82
92
 
83
93
 
84
94
  def to_json_schema_recursive(
85
- schema: dict[str, Any], nullable_name: str, is_response_schema: bool = False
95
+ schema: dict[str, Any], nullable_name: str, is_response_schema: bool = False, update_quantifiers: bool = True
86
96
  ) -> dict[str, Any]:
87
- return traverse_schema(schema, to_json_schema, nullable_name=nullable_name, is_response_schema=is_response_schema)
97
+ return traverse_schema(
98
+ schema,
99
+ to_json_schema,
100
+ nullable_name=nullable_name,
101
+ is_response_schema=is_response_schema,
102
+ update_quantifiers=update_quantifiers,
103
+ )
@@ -1,5 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import string
4
+ from base64 import b64encode
5
+ from functools import lru_cache
3
6
  from typing import TYPE_CHECKING
4
7
 
5
8
  if TYPE_CHECKING:
@@ -33,5 +36,46 @@ def unregister_string_format(name: str) -> None:
33
36
  raise ValueError(f"Unknown Open API format: {name}") from exc
34
37
 
35
38
 
39
+ def header_values(blacklist_characters: str = "\n\r") -> st.SearchStrategy[str]:
40
+ from hypothesis import strategies as st
41
+
42
+ return st.text(
43
+ alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters=blacklist_characters)
44
+ # Header values with leading non-visible chars can't be sent with `requests`
45
+ ).map(str.lstrip)
46
+
47
+
48
+ HEADER_FORMAT = "_header_value"
49
+
50
+
51
+ @lru_cache
52
+ def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
53
+ """Get all default "format" strategies."""
54
+ from hypothesis import strategies as st
55
+ from requests.auth import _basic_auth_str
56
+
57
+ from ...serializers import Binary
58
+
59
+ def make_basic_auth_str(item: tuple[str, str]) -> str:
60
+ return _basic_auth_str(*item)
61
+
62
+ latin1_text = st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255))
63
+
64
+ # Define valid characters here to avoid filtering them out in `is_valid_header` later
65
+ header_value = header_values()
66
+
67
+ return {
68
+ "binary": st.binary().map(Binary),
69
+ "byte": st.binary().map(lambda x: b64encode(x).decode()),
70
+ # RFC 7230, Section 3.2.6
71
+ "_header_name": st.text(
72
+ min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)
73
+ ),
74
+ HEADER_FORMAT: header_value,
75
+ "_basic_auth": st.tuples(latin1_text, latin1_text).map(make_basic_auth_str),
76
+ "_bearer_auth": header_value.map("Bearer {}".format),
77
+ }
78
+
79
+
36
80
  register = register_string_format
37
81
  unregister = unregister_string_format
@@ -21,6 +21,7 @@ from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter
21
21
  from .references import RECURSION_DEPTH_LIMIT, Unresolvable
22
22
 
23
23
  if TYPE_CHECKING:
24
+ from hypothesis.vendor.pretty import RepresentationPrinter
24
25
  from jsonschema import RefResolver
25
26
 
26
27
  from ...parameters import ParameterSet
@@ -203,6 +204,9 @@ class OpenAPILink(Direction):
203
204
  method = self.operation.method
204
205
  return f"state.schema['{path}']['{method}'].links['{self.status_code}']['{self.name}']"
205
206
 
207
+ def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
208
+ return printer.text(repr(self))
209
+
206
210
  def __post_init__(self) -> None:
207
211
  extension = self.definition.get(SCHEMATHESIS_LINK_EXTENSION)
208
212
  self.parameters = [
@@ -372,6 +372,11 @@ def negate_constraints(context: MutationContext, draw: Draw, schema: Schema) ->
372
372
  # Should we negate this key?
373
373
  if k == "required":
374
374
  return v != []
375
+ if k in ("example", "examples"):
376
+ return False
377
+ if context.is_path_location and k == "minLength" and v == 1:
378
+ # Empty path parameter will be filtered out
379
+ return False
375
380
  return not (
376
381
  k in ("type", "properties", "items", "minItems")
377
382
  or (k == "additionalProperties" and context.is_header_location)