schemathesis 4.0.0a6__py3-none-any.whl → 4.0.0a8__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 CHANGED
@@ -8,6 +8,8 @@ from schemathesis.core.version import SCHEMATHESIS_VERSION
8
8
  from schemathesis.generation import GenerationConfig, GenerationMode, HeaderConfig
9
9
  from schemathesis.generation.case import Case
10
10
  from schemathesis.generation.targets import TargetContext, TargetFunction, target
11
+ from schemathesis.hooks import HookContext
12
+ from schemathesis.schemas import BaseSchema
11
13
 
12
14
  __version__ = SCHEMATHESIS_VERSION
13
15
 
@@ -26,6 +28,8 @@ __all__ = [
26
28
  "Response",
27
29
  "TargetContext",
28
30
  "TargetFunction",
31
+ "HookContext",
32
+ "BaseSchema",
29
33
  "__version__",
30
34
  "auth",
31
35
  "check",
@@ -305,6 +305,16 @@ DEFAULT_PHASES = ("examples", "coverage", "fuzzing", "stateful")
305
305
  multiple=True,
306
306
  metavar="FEATURES",
307
307
  )
308
+ @grouped_option(
309
+ "--experimental-coverage-unexpected-methods",
310
+ "coverage_unexpected_methods",
311
+ help="HTTP methods to use when generating test cases with methods not specified in the API during the coverage phase.",
312
+ type=CsvChoice(["get", "put", "post", "delete", "options", "head", "patch", "trace"], case_sensitive=False),
313
+ callback=validation.convert_http_methods,
314
+ metavar="",
315
+ default=None,
316
+ envvar="SCHEMATHESIS_EXPERIMENTAL_COVERAGE_UNEXPECTED_METHODS",
317
+ )
308
318
  @grouped_option(
309
319
  "--experimental-missing-required-header-allowed-statuses",
310
320
  "missing_required_header_allowed_statuses",
@@ -489,6 +499,7 @@ def run(
489
499
  set_cookie: dict[str, str],
490
500
  set_path: dict[str, str],
491
501
  experiments: list,
502
+ coverage_unexpected_methods: set[str] | None,
492
503
  missing_required_header_allowed_statuses: list[str],
493
504
  positive_data_acceptance_allowed_statuses: list[str],
494
505
  negative_data_rejection_allowed_statuses: list[str],
@@ -655,6 +666,7 @@ def run(
655
666
  graphql_allow_null=generation_graphql_allow_null,
656
667
  codec=generation_codec,
657
668
  with_security_parameters=generation_with_security_parameters,
669
+ unexpected_methods=coverage_unexpected_methods,
658
670
  ),
659
671
  max_failures=max_failures,
660
672
  continue_on_failure=continue_on_failure,
@@ -270,6 +270,14 @@ def reduce_list(ctx: click.core.Context, param: click.core.Parameter, value: tup
270
270
  return reduce(operator.iadd, value, [])
271
271
 
272
272
 
273
+ def convert_http_methods(
274
+ ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
275
+ ) -> set[str] | None:
276
+ if value is None:
277
+ return value
278
+ return {item.lower() for item in value}
279
+
280
+
273
281
  def convert_status_codes(
274
282
  ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
275
283
  ) -> list[str] | None:
@@ -251,7 +251,7 @@ class FailureGroup(BaseExceptionGroup):
251
251
  return super().__new__(cls, message, list(failures))
252
252
 
253
253
 
254
- class MessageBlock(Enum):
254
+ class MessageBlock(str, Enum):
255
255
  CASE_ID = "case_id"
256
256
  FAILURE = "failure"
257
257
  STATUS = "status"
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
10
10
  from schemathesis.engine.events import EventGenerator
11
11
 
12
12
 
13
- class PhaseName(enum.Enum):
13
+ class PhaseName(str, enum.Enum):
14
14
  """Available execution phases."""
15
15
 
16
16
  PROBING = "API probing"
@@ -51,43 +51,48 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
51
51
  status = None
52
52
  is_executed = False
53
53
 
54
- with WorkerPool(
55
- workers_num=workers_num,
56
- producer=producer,
57
- worker_factory=worker_task,
58
- ctx=engine,
59
- mode=mode,
60
- phase=phase.name,
61
- suite_id=suite_started.id,
62
- ) as pool:
63
- try:
64
- while True:
65
- try:
66
- event = pool.events_queue.get(timeout=WORKER_TIMEOUT)
67
- is_executed = True
68
- if engine.is_interrupted:
69
- raise KeyboardInterrupt
70
- yield event
71
- if isinstance(event, events.NonFatalError):
72
- status = Status.ERROR
73
- if isinstance(event, events.ScenarioFinished):
74
- if event.status != Status.SKIP and (status is None or status < event.status):
75
- status = event.status
76
- if event.status in (Status.ERROR, Status.FAILURE):
77
- engine.control.count_failure()
78
- if isinstance(event, events.Interrupted) or engine.is_interrupted:
79
- status = Status.INTERRUPTED
80
- engine.stop()
81
- if engine.has_to_stop:
82
- break # type: ignore[unreachable]
83
- except queue.Empty:
84
- if all(not worker.is_alive() for worker in pool.workers):
85
- break
86
- continue
87
- except KeyboardInterrupt:
88
- engine.stop()
89
- status = Status.INTERRUPTED
90
- yield events.Interrupted(phase=phase.name)
54
+ try:
55
+ with WorkerPool(
56
+ workers_num=workers_num,
57
+ producer=producer,
58
+ worker_factory=worker_task,
59
+ ctx=engine,
60
+ mode=mode,
61
+ phase=phase.name,
62
+ suite_id=suite_started.id,
63
+ ) as pool:
64
+ try:
65
+ while True:
66
+ try:
67
+ event = pool.events_queue.get(timeout=WORKER_TIMEOUT)
68
+ is_executed = True
69
+ if engine.is_interrupted:
70
+ raise KeyboardInterrupt
71
+ yield event
72
+ if isinstance(event, events.NonFatalError):
73
+ status = Status.ERROR
74
+ if isinstance(event, events.ScenarioFinished):
75
+ if event.status != Status.SKIP and (status is None or status < event.status):
76
+ status = event.status
77
+ if event.status in (Status.ERROR, Status.FAILURE):
78
+ engine.control.count_failure()
79
+ if isinstance(event, events.Interrupted) or engine.is_interrupted:
80
+ status = Status.INTERRUPTED
81
+ engine.stop()
82
+ if engine.has_to_stop:
83
+ break # type: ignore[unreachable]
84
+ except queue.Empty:
85
+ if all(not worker.is_alive() for worker in pool.workers):
86
+ break
87
+ continue
88
+ except KeyboardInterrupt:
89
+ # Soft stop, waiting for workers to terminate
90
+ engine.stop()
91
+ status = Status.INTERRUPTED
92
+ yield events.Interrupted(phase=phase.name)
93
+ except KeyboardInterrupt:
94
+ # Hard stop, don't wait for worker threads
95
+ pass
91
96
 
92
97
  if not is_executed:
93
98
  phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
@@ -50,3 +50,4 @@ class GenerationConfig:
50
50
  with_security_parameters: bool = True
51
51
  # Header generation configuration
52
52
  headers: HeaderConfig = field(default_factory=HeaderConfig)
53
+ unexpected_methods: set[str] | None = None
@@ -150,7 +150,7 @@ class CoverageContext:
150
150
 
151
151
  def is_valid_for_location(self, value: Any) -> bool:
152
152
  if self.location in ("header", "cookie") and isinstance(value, str):
153
- return is_latin_1_encodable(value) and not has_invalid_characters("", value)
153
+ return not value or (is_latin_1_encodable(value) and not has_invalid_characters("", value))
154
154
  return True
155
155
 
156
156
  def generate_from(self, strategy: st.SearchStrategy) -> Any:
@@ -437,6 +437,36 @@ def cover_schema_iter(
437
437
  elif key == "required":
438
438
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
439
439
  yield from _negative_required(ctx, template, value)
440
+ elif key == "maxItems" and isinstance(value, int) and value < BUFFER_SIZE:
441
+ try:
442
+ # Force the array to have one more item than allowed
443
+ new_schema = {**schema, "minItems": value + 1, "maxItems": value + 1, "type": "array"}
444
+ array_value = ctx.generate_from_schema(new_schema)
445
+ k = _to_hashable_key(array_value)
446
+ if k not in seen:
447
+ yield NegativeValue(
448
+ array_value,
449
+ description="Array with more items than allowed by maxItems",
450
+ location=ctx.current_path,
451
+ )
452
+ seen.add(k)
453
+ except (InvalidArgument, Unsatisfiable):
454
+ pass
455
+ elif key == "minItems" and isinstance(value, int) and value > 0:
456
+ try:
457
+ # Force the array to have one less item than the minimum
458
+ new_schema = {**schema, "minItems": value - 1, "maxItems": value - 1, "type": "array"}
459
+ array_value = ctx.generate_from_schema(new_schema)
460
+ k = _to_hashable_key(array_value)
461
+ if k not in seen:
462
+ yield NegativeValue(
463
+ array_value,
464
+ description="Array with fewer items than allowed by minItems",
465
+ location=ctx.current_path,
466
+ )
467
+ seen.add(k)
468
+ except (InvalidArgument, Unsatisfiable):
469
+ pass
440
470
  elif (
441
471
  key == "additionalProperties"
442
472
  and not value
@@ -770,7 +800,12 @@ def _negative_enum(
770
800
  _hashed = _to_hashable_key(x)
771
801
  return _hashed not in seen
772
802
 
773
- strategy = (st.none() | st.booleans() | NUMERIC_STRATEGY | st.text()).filter(is_not_in_value)
803
+ strategy = (
804
+ st.text(alphabet=st.characters(min_codepoint=65, max_codepoint=122, categories=["L"]), min_size=3)
805
+ | st.none()
806
+ | st.booleans()
807
+ | NUMERIC_STRATEGY
808
+ ).filter(is_not_in_value)
774
809
  value = ctx.generate_from(strategy)
775
810
  yield NegativeValue(value, description="Invalid enum value", location=ctx.current_path)
776
811
  hashed = _to_hashable_key(value)
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  from dataclasses import dataclass, field
4
5
  from enum import Enum
5
6
  from functools import wraps
6
7
  from itertools import combinations
8
+ import os
7
9
  from time import perf_counter
8
10
  from typing import Any, Callable, Generator, Mapping
9
11
 
@@ -15,7 +17,7 @@ from jsonschema.exceptions import SchemaError
15
17
 
16
18
  from schemathesis import auths
17
19
  from schemathesis.auths import AuthStorage, AuthStorageMark
18
- from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
20
+ from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types, string_to_boolean
19
21
  from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
20
22
  from schemathesis.core.marks import Mark
21
23
  from schemathesis.core.transport import prepare_urlencoded
@@ -38,7 +40,7 @@ from schemathesis.schemas import APIOperation, ParameterSet
38
40
  setup()
39
41
 
40
42
 
41
- class HypothesisTestMode(Enum):
43
+ class HypothesisTestMode(str, Enum):
42
44
  EXAMPLES = "examples"
43
45
  COVERAGE = "coverage"
44
46
  FUZZING = "fuzzing"
@@ -120,15 +122,23 @@ def create_test(
120
122
  ):
121
123
  hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
122
124
 
125
+ disable_coverage = string_to_boolean(os.getenv("SCHEMATHESIS_DISABLE_COVERAGE", ""))
126
+
123
127
  if (
124
- HypothesisTestMode.COVERAGE in config.modes
128
+ not disable_coverage
129
+ and HypothesisTestMode.COVERAGE in config.modes
125
130
  and Phase.explicit in settings.phases
126
131
  and specification.supports_feature(SpecificationFeature.COVERAGE)
127
132
  and not config.given_args
128
133
  and not config.given_kwargs
129
134
  ):
130
135
  hypothesis_test = add_coverage(
131
- hypothesis_test, operation, config.generation.modes, auth_storage, config.as_strategy_kwargs
136
+ hypothesis_test,
137
+ operation,
138
+ config.generation.modes,
139
+ auth_storage,
140
+ config.as_strategy_kwargs,
141
+ config.generation.unexpected_methods,
132
142
  )
133
143
 
134
144
  setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
@@ -153,7 +163,24 @@ def create_base_test(
153
163
  __tracebackhide__ = True
154
164
  return test_function(*args, **kwargs)
155
165
 
156
- return hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
166
+ funcobj = hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
167
+
168
+ if asyncio.iscoroutinefunction(test_function):
169
+ funcobj.hypothesis.inner_test = make_async_test(test_function) # type: ignore
170
+ return funcobj
171
+
172
+
173
+ def make_async_test(test: Callable) -> Callable:
174
+ def async_run(*args: Any, **kwargs: Any) -> None:
175
+ try:
176
+ loop = asyncio.get_event_loop()
177
+ except RuntimeError:
178
+ loop = asyncio.new_event_loop()
179
+ coro = test(*args, **kwargs)
180
+ future = asyncio.ensure_future(coro, loop=loop)
181
+ loop.run_until_complete(future)
182
+
183
+ return async_run
157
184
 
158
185
 
159
186
  def add_examples(
@@ -215,6 +242,7 @@ def add_coverage(
215
242
  generation_modes: list[GenerationMode],
216
243
  auth_storage: AuthStorage | None,
217
244
  as_strategy_kwargs: dict[str, Any],
245
+ unexpected_methods: set[str] | None = None,
218
246
  ) -> Callable:
219
247
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
220
248
 
@@ -227,7 +255,7 @@ def add_coverage(
227
255
  for container in LOCATION_TO_CONTAINER.values()
228
256
  if container in as_strategy_kwargs
229
257
  }
230
- for case in _iter_coverage_cases(operation, generation_modes):
258
+ for case in _iter_coverage_cases(operation, generation_modes, unexpected_methods):
231
259
  if case.media_type and operation.schema.transport.get_first_matching_media_type(case.media_type) is None:
232
260
  continue
233
261
  adjust_urlencoded_payload(case)
@@ -362,7 +390,9 @@ def _stringify_value(val: Any, container_name: str) -> Any:
362
390
 
363
391
 
364
392
  def _iter_coverage_cases(
365
- operation: APIOperation, generation_modes: list[GenerationMode]
393
+ operation: APIOperation,
394
+ generation_modes: list[GenerationMode],
395
+ unexpected_methods: set[str] | None = None,
366
396
  ) -> Generator[Case, None, None]:
367
397
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
368
398
  from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
@@ -374,6 +404,8 @@ def _iter_coverage_cases(
374
404
 
375
405
  instant = Instant()
376
406
  responses = find_in_responses(operation)
407
+ # NOTE: The HEAD method is excluded
408
+ unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
377
409
  for parameter in operation.iter_parameters():
378
410
  location = parameter.location
379
411
  name = parameter.name
@@ -488,7 +520,7 @@ def _iter_coverage_cases(
488
520
  )
489
521
  if GenerationMode.NEGATIVE in generation_modes:
490
522
  # Generate HTTP methods that are not specified in the spec
491
- methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
523
+ methods = unexpected_methods - set(operation.schema[operation.path])
492
524
  for method in sorted(methods):
493
525
  instant = Instant()
494
526
  data = template.unmodified()
schemathesis/hooks.py CHANGED
@@ -21,7 +21,7 @@ HookDispatcherMark = Mark["HookDispatcher"](attr_name="hook_dispatcher")
21
21
 
22
22
 
23
23
  @unique
24
- class HookScope(Enum):
24
+ class HookScope(int, Enum):
25
25
  GLOBAL = 1
26
26
  SCHEMA = 2
27
27
  TEST = 3
@@ -108,7 +108,12 @@ class SchemathesisCase(PyCollector):
108
108
  This implementation is based on the original one in pytest, but with slight adjustments
109
109
  to produce tests out of hypothesis ones.
110
110
  """
111
- from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode, create_test
111
+ from schemathesis.generation.hypothesis.builder import (
112
+ HypothesisTestConfig,
113
+ HypothesisTestMode,
114
+ create_test,
115
+ make_async_test,
116
+ )
112
117
 
113
118
  is_trio_test = False
114
119
  for mark in getattr(self.test_function, "pytestmark", []):
@@ -221,19 +226,6 @@ class SchemathesisCase(PyCollector):
221
226
  pytest.fail("Error during collection")
222
227
 
223
228
 
224
- def make_async_test(test: Callable) -> Callable:
225
- def async_run(*args: Any, **kwargs: Any) -> None:
226
- try:
227
- loop = asyncio.get_event_loop()
228
- except RuntimeError:
229
- loop = asyncio.new_event_loop()
230
- coro = test(*args, **kwargs)
231
- future = asyncio.ensure_future(coro, loop=loop)
232
- loop.run_until_complete(future)
233
-
234
- return async_run
235
-
236
-
237
229
  @hookimpl(hookwrapper=True) # type:ignore
238
230
  def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -> Generator[None, Any, None]:
239
231
  """Switch to a different collector if the test is parametrized marked by schemathesis."""
schemathesis/schemas.py CHANGED
@@ -14,7 +14,7 @@ from typing import (
14
14
  NoReturn,
15
15
  TypeVar,
16
16
  )
17
- from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
17
+ from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
18
18
 
19
19
  from schemathesis import transport
20
20
  from schemathesis.core import NOT_SET, NotSet
@@ -436,6 +436,8 @@ class BaseSchema(Mapping):
436
436
  app: Any | NotSet = NOT_SET,
437
437
  ) -> Self:
438
438
  if not isinstance(base_url, NotSet):
439
+ if base_url is not None:
440
+ validate_base_url(base_url)
439
441
  self.base_url = base_url
440
442
  if not isinstance(location, NotSet):
441
443
  self.location = location
@@ -453,6 +455,21 @@ class BaseSchema(Mapping):
453
455
  return self
454
456
 
455
457
 
458
+ INVALID_BASE_URL_MESSAGE = (
459
+ "The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
460
+ "Make sure it is a properly formatted URL."
461
+ )
462
+
463
+
464
+ def validate_base_url(value: str) -> None:
465
+ try:
466
+ netloc = urlparse(value).netloc
467
+ except ValueError as exc:
468
+ raise ValueError(INVALID_BASE_URL_MESSAGE) from exc
469
+ if value and not netloc:
470
+ raise ValueError(INVALID_BASE_URL_MESSAGE)
471
+
472
+
456
473
  @dataclass
457
474
  class APIOperationMap(Mapping):
458
475
  _schema: BaseSchema
@@ -460,7 +460,7 @@ def ensure_resource_availability(ctx: CheckContext, response: Response, case: Ca
460
460
  )
461
461
 
462
462
 
463
- class AuthKind(enum.Enum):
463
+ class AuthKind(str, enum.Enum):
464
464
  EXPLICIT = "explicit"
465
465
  GENERATED = "generated"
466
466
 
@@ -6,7 +6,7 @@ from typing import Callable, Generator
6
6
 
7
7
 
8
8
  @unique
9
- class TokenType(Enum):
9
+ class TokenType(int, Enum):
10
10
  VARIABLE = 1
11
11
  STRING = 2
12
12
  POINTER = 3
@@ -25,7 +25,7 @@ class Node:
25
25
 
26
26
 
27
27
  @unique
28
- class NodeType(Enum):
28
+ class NodeType(str, Enum):
29
29
  URL = "$url"
30
30
  METHOD = "$method"
31
31
  STATUS_CODE = "$statusCode"
@@ -20,7 +20,7 @@ from .utils import can_negate
20
20
  T = TypeVar("T")
21
21
 
22
22
 
23
- class MutationResult(enum.Enum):
23
+ class MutationResult(int, enum.Enum):
24
24
  """The result of applying some mutation to some schema.
25
25
 
26
26
  Failing to mutate something means that by applying some mutation, it is not possible to change
@@ -134,6 +134,7 @@ class OpenAPI20Parameter(OpenAPIParameter):
134
134
  "multipleOf",
135
135
  "example",
136
136
  "examples",
137
+ "default",
137
138
  )
138
139
 
139
140
 
@@ -178,6 +179,7 @@ class OpenAPI30Parameter(OpenAPIParameter):
178
179
  "format",
179
180
  "example",
180
181
  "examples",
182
+ "default",
181
183
  )
182
184
 
183
185
  def from_open_api_to_json_schema(self, operation: APIOperation, open_api_schema: dict[str, Any]) -> dict[str, Any]:
@@ -230,6 +232,7 @@ class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
230
232
  "additionalProperties",
231
233
  "example",
232
234
  "examples",
235
+ "default",
233
236
  )
234
237
  # NOTE. For Open API 2.0 bodies, we still give `x-example` precedence over the schema-level `example` field to keep
235
238
  # the precedence rules consistent.
@@ -139,7 +139,10 @@ class ConvertingResolver(InliningResolver):
139
139
  def resolve(self, ref: str) -> tuple[str, Any]:
140
140
  url, document = super().resolve(ref)
141
141
  document = to_json_schema_recursive(
142
- document, nullable_name=self.nullable_name, is_response_schema=self.is_response_schema
142
+ document,
143
+ nullable_name=self.nullable_name,
144
+ is_response_schema=self.is_response_schema,
145
+ update_quantifiers=False,
143
146
  )
144
147
  return url, document
145
148
 
@@ -1022,24 +1022,30 @@ class SwaggerV20(BaseOpenAPISchema):
1022
1022
  content_types = self.get_request_payload_content_types(operation)
1023
1023
  is_multipart = "multipart/form-data" in content_types
1024
1024
 
1025
- def add_file(file_value: Any) -> None:
1026
- if isinstance(file_value, list):
1027
- for item in file_value:
1028
- files.append((name, (None, item)))
1029
- else:
1030
- files.append((name, file_value))
1025
+ known_fields: dict[str, dict] = {}
1031
1026
 
1032
1027
  for parameter in operation.body:
1033
1028
  if isinstance(parameter, OpenAPI20CompositeBody):
1034
1029
  for form_parameter in parameter.definition:
1035
- name = form_parameter.name
1036
- # It might be not in `form_data`, if the parameter is optional
1037
- if name in form_data:
1038
- value = form_data[name]
1039
- if form_parameter.definition.get("type") == "file" or is_multipart:
1040
- add_file(value)
1041
- else:
1042
- data[name] = value
1030
+ known_fields[form_parameter.name] = form_parameter.definition
1031
+
1032
+ def add_file(name: str, value: Any) -> None:
1033
+ if isinstance(value, list):
1034
+ for item in value:
1035
+ files.append((name, (None, item)))
1036
+ else:
1037
+ files.append((name, value))
1038
+
1039
+ for name, value in form_data.items():
1040
+ param_def = known_fields.get(name)
1041
+ if param_def:
1042
+ if param_def.get("type") == "file" or is_multipart:
1043
+ add_file(name, value)
1044
+ else:
1045
+ data[name] = value
1046
+ else:
1047
+ # Unknown field — treat it as a file (safe default under multipart/form-data)
1048
+ add_file(name, value)
1043
1049
  # `None` is the default value for `files` and `data` arguments in `requests.request`
1044
1050
  return files or None, data or None
1045
1051
 
@@ -1202,14 +1208,19 @@ class OpenApi30(SwaggerV20):
1202
1208
  break
1203
1209
  else:
1204
1210
  raise InternalError("No 'multipart/form-data' media type found in the schema")
1205
- for name, property_schema in (schema or {}).get("properties", {}).items():
1206
- if name in form_data:
1207
- if isinstance(form_data[name], list):
1208
- files.extend([(name, item) for item in form_data[name]])
1211
+ for name, value in form_data.items():
1212
+ property_schema = (schema or {}).get("properties", {}).get(name)
1213
+ if property_schema:
1214
+ if isinstance(value, list):
1215
+ files.extend([(name, item) for item in value])
1209
1216
  elif property_schema.get("format") in ("binary", "base64"):
1210
- files.append((name, form_data[name]))
1217
+ files.append((name, value))
1211
1218
  else:
1212
- files.append((name, (None, form_data[name])))
1219
+ files.append((name, (None, value)))
1220
+ elif isinstance(value, list):
1221
+ files.extend([(name, item) for item in value])
1222
+ else:
1223
+ files.append((name, (None, value)))
1213
1224
  # `None` is the default value for `files` and `data` arguments in `requests.request`
1214
1225
  return files or None, None
1215
1226
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 4.0.0a6
3
+ Version: 4.0.0a8
4
4
  Summary: Property-based testing framework for Open API and GraphQL based apps
5
5
  Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
6
6
  Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
@@ -1,18 +1,18 @@
1
- schemathesis/__init__.py,sha256=ggp1CxctLo__wwFwlDhvtrexxDXGSbRjFKzXw_Twi7k,1139
1
+ schemathesis/__init__.py,sha256=S9MD8cGyXWihyQikye9mSBpvrfUJbOItD5yr65vkx6A,1263
2
2
  schemathesis/auths.py,sha256=t-YuPyoLqL7jlRUH-45JxO7Ir3pYxpe31CRmNIJh7rI,15423
3
3
  schemathesis/checks.py,sha256=B5-ROnjvvwpaqgj_iQ7eCjGqvRRVT30eWNPLKmwdrM8,5084
4
4
  schemathesis/errors.py,sha256=VSZ-h9Bt7QvrvywOGB-MoHCshR8OWJegYlBxfVh5Vuw,899
5
5
  schemathesis/filters.py,sha256=CzVPnNSRLNgvLlU5_WssPEC0wpdQi0dMvDpHSQbAlkE,13577
6
- schemathesis/hooks.py,sha256=jTdN5GJbxHRMshxgcuI_th9ouuL32CN4m2Jt0pmT_bs,13148
6
+ schemathesis/hooks.py,sha256=ZSGEnsLJ7UVezf4CcaJebVkjEpvwgJolJFZo5fjQNDc,13153
7
7
  schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- schemathesis/schemas.py,sha256=Hs2pJTUa2Od3x5YRprrKYIfE7ndpKbAqeDylAWkB6yM,27407
8
+ schemathesis/schemas.py,sha256=A2qAs1PY9wbRWk6PFnslWyIqzchAhu5oo_MsKL7uF8w,27952
9
9
  schemathesis/cli/__init__.py,sha256=U9gjzWWpiFhaqevPjZbwyTNjABdpvXETI4HgwdGKnvs,877
10
10
  schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
11
11
  schemathesis/cli/constants.py,sha256=rUixnqorraUFDtOu3Nmm1x_k0qbgmW9xW96kQB_fBCQ,338
12
12
  schemathesis/cli/core.py,sha256=Qm5xvpIIMwJDTeR3N3TjKhMCHV5d5Rp0UstVS2GjWgw,459
13
13
  schemathesis/cli/hooks.py,sha256=vTrA8EN99whRns5K5AnExViQ6WL9cak5RGsC-ZBEiJM,1458
14
14
  schemathesis/cli/commands/__init__.py,sha256=FFalEss3D7mnCRO0udtYb65onXSjQCCOv8sOSjqvTTM,1059
15
- schemathesis/cli/commands/run/__init__.py,sha256=pLuXYxgO0z1CLZH_2P_uW8xeGOy1S2VtxLnNqKXanHk,23138
15
+ schemathesis/cli/commands/run/__init__.py,sha256=ACIRF3eP-Za56sY5OMSdLdbrmopPvJblDel4Jl-vBtw,23745
16
16
  schemathesis/cli/commands/run/checks.py,sha256=lLtBCt6NhhQisrWo8aC6i0M3dSXlbjGWTTlOyjzatks,3278
17
17
  schemathesis/cli/commands/run/context.py,sha256=pUwSlS7UwW2cq1nJXfKZFEaWDipsQAElCO4tdv1qYJA,7739
18
18
  schemathesis/cli/commands/run/events.py,sha256=Dj-xvIr-Hkms8kvh4whNwKSk1Q2Hx4NIENi_4A8nQO8,1224
@@ -21,7 +21,7 @@ schemathesis/cli/commands/run/filters.py,sha256=MdymOZtzOolvXCNBIdfHbBbWEXVF7Se0
21
21
  schemathesis/cli/commands/run/hypothesis.py,sha256=hdEHim_Hc2HwCGxAiRTf4t2OfQf0IeCUhyjNT_btB1o,2553
22
22
  schemathesis/cli/commands/run/loaders.py,sha256=VedoeIE1tgFBqVokWxOoUReAjBl-Zhx87RjCEBtCVfs,4840
23
23
  schemathesis/cli/commands/run/reports.py,sha256=OjyakiV0lpNDBZb1xsb_2HmLtcqhTThPYMpJGXyNNO8,2147
24
- schemathesis/cli/commands/run/validation.py,sha256=7fvLeDREQ9FTV8ZMJRnCdycD858j21k7j56ow4_iIcY,12789
24
+ schemathesis/cli/commands/run/validation.py,sha256=cpGG5hFc4lHVemXrQXRvrlNlqBmMqtvx9yUwbOhc2TI,13008
25
25
  schemathesis/cli/commands/run/handlers/__init__.py,sha256=TPZ3KdGi8m0fjlN0GjA31MAXXn1qI7uU4FtiDwroXZI,1915
26
26
  schemathesis/cli/commands/run/handlers/base.py,sha256=yDsTtCiztLksfk7cRzg8JlaAVOfS-zwK3tsJMOXAFyc,530
27
27
  schemathesis/cli/commands/run/handlers/cassettes.py,sha256=SVk13xPhsQduCpgvvBwzEMDNTju-SHQCW90xTQ6iL1U,18525
@@ -40,7 +40,7 @@ schemathesis/core/control.py,sha256=IzwIc8HIAEMtZWW0Q0iXI7T1niBpjvcLlbuwOSmy5O8,
40
40
  schemathesis/core/curl.py,sha256=yuaCe_zHLGwUjEeloQi6W3tOA3cGdnHDNI17-5jia0o,1723
41
41
  schemathesis/core/deserialization.py,sha256=ygIj4fNaOd0mJ2IvTsn6bsabBt_2AbSLCz-z9UqfpdQ,2406
42
42
  schemathesis/core/errors.py,sha256=97Fk3udsMaS5xZrco7ZaShqe4W6g2aZ55J7d58HPRac,15881
43
- schemathesis/core/failures.py,sha256=jbbxOXB8LDYoLI97YrLCKi9XLuSqVqFJSLeeixVJPRU,8828
43
+ schemathesis/core/failures.py,sha256=nt_KJAQnachw4Ey-rZ__P8q6nGJ_YekZiSLc6-PfFW0,8833
44
44
  schemathesis/core/fs.py,sha256=ItQT0_cVwjDdJX9IiI7EnU75NI2H3_DCEyyUjzg_BgI,472
45
45
  schemathesis/core/lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
46
46
  schemathesis/core/loaders.py,sha256=SQQ-8m64-D2FaOgvwKZLyTtLJuzP3RPo7Ud2BERK1c0,3404
@@ -63,24 +63,24 @@ schemathesis/engine/core.py,sha256=DfulRMVTivmZj-wwLekIhuSzLsFnuVPtSg7j9HyWdz0,5
63
63
  schemathesis/engine/errors.py,sha256=8PHYsuq2qIEJHm2FDf_UnWa4IDc-DRFTPckLAr22yhE,16895
64
64
  schemathesis/engine/events.py,sha256=gslRAWQKMPqBCQzLDS4wAbsKcVuONSy5SPqimJJJYT4,6250
65
65
  schemathesis/engine/recorder.py,sha256=K3HfMARrT5mPWXPnYebjjcq5CcsBRhMrtZwEL9_Lvtg,8432
66
- schemathesis/engine/phases/__init__.py,sha256=3p_T3JYBFOtrwtgmMM7J-6a41QJgk83dUtm7NKcVl3o,2490
66
+ schemathesis/engine/phases/__init__.py,sha256=CuTBMaQIsGdtWw400maiwqfIbMyVv5_vHXV-SY5A5NI,2495
67
67
  schemathesis/engine/phases/probes.py,sha256=3M9g3E7CXbDDK_8inuvkRZibCCcoO2Ce5U3lnyTeWXQ,5131
68
68
  schemathesis/engine/phases/stateful/__init__.py,sha256=lWo2RLrutNblHvohTzofQqL22GORwBRA8bf6jvLuGPg,2391
69
69
  schemathesis/engine/phases/stateful/_executor.py,sha256=m1ZMqFUPc4Hdql10l0gF3tpP4JOImSA-XeBd4jg3Ll8,12443
70
70
  schemathesis/engine/phases/stateful/context.py,sha256=SKWsok-tlWbUDagiUmP7cLNW6DsgFDc_Afv0vQfWv6c,2964
71
- schemathesis/engine/phases/unit/__init__.py,sha256=LcBQpGNPeEFB9XPGpcHBcH-C7nF-e8bZNPop9PIfiKA,7861
71
+ schemathesis/engine/phases/unit/__init__.py,sha256=QmtzIgP9KWLo-IY1kMyBqYXPMxFQz-WF2eVTWewqUfI,8174
72
72
  schemathesis/engine/phases/unit/_executor.py,sha256=buMEr7e01SFSeNuEQNGMf4hoiLxX9_sp0JhH4LBAk9M,12928
73
73
  schemathesis/engine/phases/unit/_pool.py,sha256=9OgmFd-ov1AAvcZGquK40PXkGLp7f2qCjZoPZuoZl4A,2529
74
74
  schemathesis/experimental/__init__.py,sha256=jYY3Mq6okqTRTMudPzcaT0JVjzJW5IN_ZVJdGU0stBs,2011
75
- schemathesis/generation/__init__.py,sha256=2htA0TlQee6AvQmLl1VNxEptRDqvPjksXKJLMVLAJng,1580
75
+ schemathesis/generation/__init__.py,sha256=sWTRPTh-qDNkSfpM9rYI3v8zskH8_wFKUuPRg18fZI8,1627
76
76
  schemathesis/generation/case.py,sha256=Rt5MCUtPVYVQzNyjUx8magocPJpHV1svyuqQSTwUE-I,7306
77
- schemathesis/generation/coverage.py,sha256=hyDb465tBoCWE7nI-ZJjhTUzk7f2WDufaadWdSAkdr0,39276
77
+ schemathesis/generation/coverage.py,sha256=vv2dbj_KAaqo8PCwMdyDWLyyAssk-YL5oTU3aCkgB1s,41185
78
78
  schemathesis/generation/meta.py,sha256=36h6m4E7jzLGa8TCvl7eBl_xUWLiRul3qxzexl5cB58,2515
79
79
  schemathesis/generation/modes.py,sha256=t_EvKr2aOXYMsEfdMu4lLF4KCGcX1LVVyvzTkcpJqhk,663
80
80
  schemathesis/generation/overrides.py,sha256=FhqcFoliEvgW6MZyFPYemfLgzKt3Miy8Cud7OMOCb7g,3045
81
81
  schemathesis/generation/targets.py,sha256=_rN2qgxTE2EfvygiN-Fy3WmDnRH0ERohdx3sKRDaYhU,2120
82
82
  schemathesis/generation/hypothesis/__init__.py,sha256=Rl7QwvMBMJI7pBqTydplX6bXC420n0EGQHVm-vZgaYQ,1204
83
- schemathesis/generation/hypothesis/builder.py,sha256=lAxBePbfqGsp6iPjjXeDL-X8RnOgUhsgpYQpeKc1VKg,29292
83
+ schemathesis/generation/hypothesis/builder.py,sha256=cSgFWQG-apIHNdW-PpIBDBjLw4RorihfZ4e_Ln3j2-w,30341
84
84
  schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
85
85
  schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
86
86
  schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
@@ -99,7 +99,7 @@ schemathesis/pytest/__init__.py,sha256=7W0q-Thcw03IAQfXE_Mo8JPZpUdHJzfu85fjK1Zdf
99
99
  schemathesis/pytest/control_flow.py,sha256=F8rAPsPeNv_sJiJgbZYtTpwKWjauZmqFUaKroY2GmQI,217
100
100
  schemathesis/pytest/lazy.py,sha256=g7DpOeQNsjXC03FCG5e1L65iz3zE48qAyaqG81HzCZY,12028
101
101
  schemathesis/pytest/loaders.py,sha256=oQJ78yyuIm3Ye9X7giVjDB1vYfaW5UY5YuhaTLm_ZFU,266
102
- schemathesis/pytest/plugin.py,sha256=RDOuT25Uotese7W-SD3Pu-nb7zdnaPbyPOoJSkJKSoQ,12379
102
+ schemathesis/pytest/plugin.py,sha256=TxbESQy9JPZBaIwUP4BHiIGFzPd2oMWwq_4VqFS_UfI,12067
103
103
  schemathesis/python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
104
  schemathesis/python/asgi.py,sha256=5PyvuTBaivvyPUEi3pwJni91K1kX5Zc0u9c6c1D8a1Q,287
105
105
  schemathesis/python/wsgi.py,sha256=uShAgo_NChbfYaV1117e6UHp0MTg7jaR0Sy_to3Jmf8,219
@@ -113,28 +113,28 @@ schemathesis/specs/graphql/validation.py,sha256=-W1Noc1MQmTb4RX-gNXMeU2qkgso4mzV
113
113
  schemathesis/specs/openapi/__init__.py,sha256=C5HOsfuDJGq_3mv8CRBvRvb0Diy1p0BFdqyEXMS-loE,238
114
114
  schemathesis/specs/openapi/_cache.py,sha256=HpglmETmZU0RCHxp3DO_sg5_B_nzi54Zuw9vGzzYCxY,4295
115
115
  schemathesis/specs/openapi/_hypothesis.py,sha256=n_39iyz1rt2EdSe-Lyr-3sOIEyJIthnCVR4tGUUvH1c,21328
116
- schemathesis/specs/openapi/checks.py,sha256=m3n5N3_iZcS7inJojW47FF6dfbUQzrBH-bXwsCAOyhM,27737
116
+ schemathesis/specs/openapi/checks.py,sha256=i4tVVkK1wLthdmG-zu7EaQLkBxJ2T3FkuHqw0dA4qlA,27742
117
117
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
118
118
  schemathesis/specs/openapi/converter.py,sha256=lil8IewM5j8tvt4lpA9g_KITvIwx1M96i45DNSHNjoc,3505
119
119
  schemathesis/specs/openapi/definitions.py,sha256=8htclglV3fW6JPBqs59lgM4LnA25Mm9IptXBPb_qUT0,93949
120
120
  schemathesis/specs/openapi/examples.py,sha256=Xvjp60QUcLaeGsJRbi2i6XM15_4uO0ceVoClIaJehiE,21062
121
121
  schemathesis/specs/openapi/formats.py,sha256=ViVF3aFeFI1ctwGQbiRDXhU3so82P0BCaF2aDDbUUm8,2816
122
122
  schemathesis/specs/openapi/media_types.py,sha256=ADedOaNWjbAtAekyaKmNj9fY6zBTeqcNqBEjN0EWNhI,1014
123
- schemathesis/specs/openapi/parameters.py,sha256=hv1reNpSjVuzFbtMpSTwWZ75zcWTOy5ZE0ah6AVEqAo,14565
123
+ schemathesis/specs/openapi/parameters.py,sha256=tVL61gDe9A8_jwoVKZZvpXKPerMyq7vkAvwdMsi44TI,14622
124
124
  schemathesis/specs/openapi/patterns.py,sha256=NLnGybcana_kYLVKVEjkEyAzdClAV0xKe4Oy4NVayMI,12834
125
- schemathesis/specs/openapi/references.py,sha256=YjD1xMlaYS7xLt6PrrVS20R72ZWHuFZFTa8Llzf54Rg,8808
126
- schemathesis/specs/openapi/schemas.py,sha256=VSeacEAVJJ6EKJ-llwOaX4aalzUTXyWP8s4wbxTqtWc,54720
125
+ schemathesis/specs/openapi/references.py,sha256=c8Ufa8hp6Dyf-gPn5lpmyqF_GtqXIBWoKkj3bk3WaPA,8871
126
+ schemathesis/specs/openapi/schemas.py,sha256=zfGPFWnaI9_W8F8E8qCTzuYQRE5yDuGx7WGW4EH-QgI,55020
127
127
  schemathesis/specs/openapi/security.py,sha256=6UWYMhL-dPtkTineqqBFNKca1i4EuoTduw-EOLeE0aQ,7149
128
128
  schemathesis/specs/openapi/serialization.py,sha256=VdDLmeHqxlWM4cxQQcCkvrU6XurivolwEEaT13ohelA,11972
129
129
  schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
130
130
  schemathesis/specs/openapi/expressions/__init__.py,sha256=hfuRtXD75tQFhzSo6QgDZ3zByyWeZRKevB8edszAVj4,2272
131
131
  schemathesis/specs/openapi/expressions/errors.py,sha256=YLVhps-sYcslgVaahfcUYxUSHlIfWL-rQMeT5PZSMZ8,219
132
132
  schemathesis/specs/openapi/expressions/extractors.py,sha256=Py3of3_vBACP4ljiZIcgd-xQCrWIpcMsfQFc0EtAUoA,470
133
- schemathesis/specs/openapi/expressions/lexer.py,sha256=LeVE6fgYT9-fIsXrv0-YrRHnI4VPisbwsexyh9Q5YU0,3982
134
- schemathesis/specs/openapi/expressions/nodes.py,sha256=YvpbAi8OFdb6RqqrqReGBeADpAmFaoyWN-lGiyYOXTc,4072
133
+ schemathesis/specs/openapi/expressions/lexer.py,sha256=KFA8Z-Kh1IYUpKgwAnDtEucN9YLLpnFR1GQl8KddWlA,3987
134
+ schemathesis/specs/openapi/expressions/nodes.py,sha256=63LC4mQHy3a0_tKiGIVWaUHu9L9IWilq6R004GLpjyY,4077
135
135
  schemathesis/specs/openapi/expressions/parser.py,sha256=e-ZxshrGE_5CVbgcZLYgdGSjdifgyzgKkLQp0dI0cJY,4503
136
136
  schemathesis/specs/openapi/negative/__init__.py,sha256=60QqVBTXPTsAojcf7GDs7v8WbOE_k3g_VC_DBeQUqBw,3749
137
- schemathesis/specs/openapi/negative/mutations.py,sha256=7jTjD9rt5vxWSVBL5Hx8Avj4WhTA63frDQiFMKysrUU,19248
137
+ schemathesis/specs/openapi/negative/mutations.py,sha256=MIFVSWbZHW92KhpWruJT3XLisgc-rFnvYasRtwMmExs,19253
138
138
  schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
139
139
  schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
140
140
  schemathesis/specs/openapi/stateful/__init__.py,sha256=0pu_iGjRiKuqUDN3ewz1zUOt6f1SdvSxVtHC5uK-CYw,14750
@@ -146,8 +146,8 @@ schemathesis/transport/prepare.py,sha256=qQ6zXBw5NN2AIM0bzLAc5Ryc3dmMb0R6xN14lnR
146
146
  schemathesis/transport/requests.py,sha256=j5wI1Uo_PnVuP1eV8l6ddsXosyxAPQ1mLSyWEZmTI9I,8747
147
147
  schemathesis/transport/serialization.py,sha256=jIMra1LqRGav0OX3Hx7mvORt38ll4cd2DKit2D58FN0,10531
148
148
  schemathesis/transport/wsgi.py,sha256=RWSuUXPrl91GxAy8a4jyNNozOWVMRBxKx_tljlWA_Lo,5697
149
- schemathesis-4.0.0a6.dist-info/METADATA,sha256=W8GlnQVnH1VOEqPqS-ElR_yk0RvdB9TOXTxHy_p3zTU,10427
150
- schemathesis-4.0.0a6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
151
- schemathesis-4.0.0a6.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
152
- schemathesis-4.0.0a6.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
153
- schemathesis-4.0.0a6.dist-info/RECORD,,
149
+ schemathesis-4.0.0a8.dist-info/METADATA,sha256=hdJd_ASqgZ75dZ1EW27p3mL6CAG179nTb-cm1zVtQqk,10427
150
+ schemathesis-4.0.0a8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
151
+ schemathesis-4.0.0a8.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
152
+ schemathesis-4.0.0a8.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
153
+ schemathesis-4.0.0a8.dist-info/RECORD,,