schemathesis 3.39.13__py3-none-any.whl → 3.39.15__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
@@ -6,6 +6,7 @@ from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runn
6
6
  from ._lazy_import import lazy_import
7
7
  from .constants import SCHEMATHESIS_VERSION
8
8
  from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig
9
+ from .hooks import HookContext
9
10
  from .models import Case
10
11
  from .specs import openapi
11
12
 
@@ -65,6 +66,7 @@ __all__ = [
65
66
  "register_check",
66
67
  "register_target",
67
68
  "register_string_format",
69
+ "HookContext",
68
70
  ]
69
71
 
70
72
 
@@ -74,7 +76,13 @@ def _load_generic_response() -> Any:
74
76
  return GenericResponse
75
77
 
76
78
 
77
- _imports = {"GenericResponse": _load_generic_response}
79
+ def _load_base_schema() -> Any:
80
+ from .schemas import BaseSchema
81
+
82
+ return BaseSchema
83
+
84
+
85
+ _imports = {"GenericResponse": _load_generic_response, "BaseSchema": _load_base_schema}
78
86
 
79
87
 
80
88
  def __getattr__(name: str) -> Any:
@@ -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)
@@ -398,45 +398,62 @@ def find_matching_in_responses(examples: dict[str, list], param: str) -> Iterato
398
398
  if not isinstance(example, dict):
399
399
  continue
400
400
  # Unwrapping example from `{"item": [{...}]}`
401
- if isinstance(example, dict) and len(example) == 1 and list(example)[0].lower() == schema_name.lower():
402
- inner = list(example.values())[0]
403
- if isinstance(inner, list):
404
- for sub_example in inner:
405
- found = _find_matching_in_responses(sub_example, schema_name, param, normalized, is_id_param)
406
- if found is not NOT_FOUND:
407
- yield found
408
- continue
409
- if isinstance(inner, dict):
410
- example = inner
411
- found = _find_matching_in_responses(example, schema_name, param, normalized, is_id_param)
412
- if found is not NOT_FOUND:
413
- yield found
401
+ if isinstance(example, dict):
402
+ inner = next((value for key, value in example.items() if key.lower() == schema_name.lower()), None)
403
+ if inner is not None:
404
+ if isinstance(inner, list):
405
+ for sub_example in inner:
406
+ if isinstance(sub_example, dict):
407
+ for found in _find_matching_in_responses(
408
+ sub_example, schema_name, param, normalized, is_id_param
409
+ ):
410
+ if found is not NOT_FOUND:
411
+ yield found
412
+ continue
413
+ if isinstance(inner, dict):
414
+ example = inner
415
+ for found in _find_matching_in_responses(example, schema_name, param, normalized, is_id_param):
416
+ if found is not NOT_FOUND:
417
+ yield found
414
418
 
415
419
 
416
420
  def _find_matching_in_responses(
417
421
  example: dict[str, Any], schema_name: str, param: str, normalized: str, is_id_param: bool
418
- ) -> Any:
422
+ ) -> Iterator[Any]:
419
423
  # Check for exact match
420
424
  if param in example:
421
- return example[param]
425
+ yield example[param]
426
+ return
422
427
  if is_id_param and param[:-2] in example:
423
- return example[param[:-2]]
428
+ value = example[param[:-2]]
429
+ if isinstance(value, list):
430
+ for sub_example in value:
431
+ for found in _find_matching_in_responses(sub_example, schema_name, param, normalized, is_id_param):
432
+ if found is not NOT_FOUND:
433
+ yield found
434
+ return
435
+ else:
436
+ yield value
437
+ return
424
438
 
425
439
  # Check for case-insensitive match
426
440
  for key in example:
427
441
  if key.lower() == normalized:
428
- return example[key]
442
+ yield example[key]
443
+ return
429
444
  else:
430
445
  # If no match found and it's an ID parameter, try additional checks
431
446
  if is_id_param:
432
447
  # Check for 'id' if parameter is '{something}Id'
433
448
  if "id" in example:
434
- return example["id"]
449
+ yield example["id"]
450
+ return
435
451
  # Check for '{schemaName}Id' or '{schemaName}_id'
436
452
  if normalized == "id" or normalized.startswith(schema_name.lower()):
437
453
  for key in (schema_name, schema_name.lower()):
438
454
  for suffix in ("_id", "Id"):
439
455
  with_suffix = f"{key}{suffix}"
440
456
  if with_suffix in example:
441
- return example[with_suffix]
442
- return NOT_FOUND
457
+ yield example[with_suffix]
458
+ return
459
+ yield NOT_FOUND
@@ -132,6 +132,7 @@ class OpenAPI20Parameter(OpenAPIParameter):
132
132
  "multipleOf",
133
133
  "example",
134
134
  "examples",
135
+ "default",
135
136
  )
136
137
 
137
138
 
@@ -176,6 +177,7 @@ class OpenAPI30Parameter(OpenAPIParameter):
176
177
  "format",
177
178
  "example",
178
179
  "examples",
180
+ "default",
179
181
  )
180
182
 
181
183
  def from_open_api_to_json_schema(self, operation: APIOperation, open_api_schema: dict[str, Any]) -> dict[str, Any]:
@@ -228,6 +230,7 @@ class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
228
230
  "additionalProperties",
229
231
  "example",
230
232
  "examples",
233
+ "default",
231
234
  )
232
235
  # NOTE. For Open API 2.0 bodies, we still give `x-example` precedence over the schema-level `example` field to keep
233
236
  # the precedence rules consistent.
@@ -141,7 +141,10 @@ class ConvertingResolver(InliningResolver):
141
141
  def resolve(self, ref: str) -> tuple[str, Any]:
142
142
  url, document = super().resolve(ref)
143
143
  document = to_json_schema_recursive(
144
- document, nullable_name=self.nullable_name, is_response_schema=self.is_response_schema
144
+ document,
145
+ nullable_name=self.nullable_name,
146
+ is_response_schema=self.is_response_schema,
147
+ update_quantifiers=False,
145
148
  )
146
149
  return url, document
147
150
 
@@ -1013,24 +1013,30 @@ class SwaggerV20(BaseOpenAPISchema):
1013
1013
  content_types = self.get_request_payload_content_types(operation)
1014
1014
  is_multipart = "multipart/form-data" in content_types
1015
1015
 
1016
- def add_file(file_value: Any) -> None:
1017
- if isinstance(file_value, list):
1018
- for item in file_value:
1019
- files.append((name, (None, item)))
1020
- else:
1021
- files.append((name, file_value))
1016
+ known_fields: dict[str, dict] = {}
1022
1017
 
1023
1018
  for parameter in operation.body:
1024
1019
  if isinstance(parameter, OpenAPI20CompositeBody):
1025
1020
  for form_parameter in parameter.definition:
1026
- name = form_parameter.name
1027
- # It might be not in `form_data`, if the parameter is optional
1028
- if name in form_data:
1029
- value = form_data[name]
1030
- if form_parameter.definition.get("type") == "file" or is_multipart:
1031
- add_file(value)
1032
- else:
1033
- data[name] = value
1021
+ known_fields[form_parameter.name] = form_parameter.definition
1022
+
1023
+ def add_file(name: str, value: Any) -> None:
1024
+ if isinstance(value, list):
1025
+ for item in value:
1026
+ files.append((name, (None, item)))
1027
+ else:
1028
+ files.append((name, value))
1029
+
1030
+ for name, value in form_data.items():
1031
+ param_def = known_fields.get(name)
1032
+ if param_def:
1033
+ if param_def.get("type") == "file" or is_multipart:
1034
+ add_file(name, value)
1035
+ else:
1036
+ data[name] = value
1037
+ else:
1038
+ # Unknown field — treat it as a file (safe default under multipart/form-data)
1039
+ add_file(name, value)
1034
1040
  # `None` is the default value for `files` and `data` arguments in `requests.request`
1035
1041
  return files or None, data or None
1036
1042
 
@@ -1188,14 +1194,19 @@ class OpenApi30(SwaggerV20):
1188
1194
  break
1189
1195
  else:
1190
1196
  raise InternalError("No 'multipart/form-data' media type found in the schema")
1191
- for name, property_schema in (schema or {}).get("properties", {}).items():
1192
- if name in form_data:
1193
- if isinstance(form_data[name], list):
1194
- files.extend([(name, item) for item in form_data[name]])
1197
+ for name, value in form_data.items():
1198
+ property_schema = (schema or {}).get("properties", {}).get(name)
1199
+ if property_schema:
1200
+ if isinstance(value, list):
1201
+ files.extend([(name, item) for item in value])
1195
1202
  elif property_schema.get("format") in ("binary", "base64"):
1196
- files.append((name, form_data[name]))
1203
+ files.append((name, value))
1197
1204
  else:
1198
- files.append((name, (None, form_data[name])))
1205
+ files.append((name, (None, value)))
1206
+ elif isinstance(value, list):
1207
+ files.extend([(name, item) for item in value])
1208
+ else:
1209
+ files.append((name, (None, value)))
1199
1210
  # `None` is the default value for `files` and `data` arguments in `requests.request`
1200
1211
  return files or None, None
1201
1212
 
@@ -14,6 +14,7 @@ from .. import failures
14
14
  from .._dependency_versions import IS_WERKZEUG_ABOVE_3
15
15
  from ..constants import DEFAULT_RESPONSE_TIMEOUT, NOT_SET
16
16
  from ..exceptions import get_timeout_error
17
+ from ..internal.copy import fast_deepcopy
17
18
  from ..serializers import SerializerContext
18
19
  from ..types import Cookies, NotSet, RequestCert
19
20
 
@@ -126,12 +127,21 @@ class RequestsTransport:
126
127
  # Additional headers, needed for the serializer
127
128
  for key, value in additional_headers.items():
128
129
  final_headers.setdefault(key, value)
130
+
131
+ p = case.query
132
+
133
+ # Replace empty dictionaries with empty strings, so the parameters actually present in the query string
134
+ if any(value == {} for value in (p or {}).values()):
135
+ p = fast_deepcopy(p)
136
+ for k, v in p.items():
137
+ if v == {}:
138
+ p[k] = ""
129
139
  data = {
130
140
  "method": case.method,
131
141
  "url": url,
132
142
  "cookies": case.cookies,
133
143
  "headers": final_headers,
134
- "params": case.query,
144
+ "params": p,
135
145
  **extra,
136
146
  }
137
147
  if params is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 3.39.13
3
+ Version: 3.39.15
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,4 +1,4 @@
1
- schemathesis/__init__.py,sha256=UW2Bq8hDDkcBeAAA7PzpBFXkOOxkmHox-mfQwzHDjL0,1914
1
+ schemathesis/__init__.py,sha256=AGpMI329waZiHwU05CW7cB1cxqQsXvRPB5vs4N7ktB4,2090
2
2
  schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
3
3
  schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
4
4
  schemathesis/_hypothesis.py,sha256=CEfWX38CsPy-RzwMGdKuJD9mY_AV8fIq_ZhabGp4tW0,30759
@@ -61,7 +61,7 @@ schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZE
61
61
  schemathesis/generation/__init__.py,sha256=PClFLK3bu-8Gsy71rgdD0ULMqySrzX-Um8Tan77x_5A,1628
62
62
  schemathesis/generation/_hypothesis.py,sha256=74fzLPHugZgMQXerWYFAMqCAjtAXz5E4gek7Gnkhli4,1756
63
63
  schemathesis/generation/_methods.py,sha256=r8oVlJ71_gXcnEhU-byw2E0R2RswQQFm8U7yGErSqbw,1204
64
- schemathesis/generation/coverage.py,sha256=1CilQSe2DIdMdeWA6RL22so2bZULPRwc0CQBRxcLRFs,39370
64
+ schemathesis/generation/coverage.py,sha256=bE93UnTpp8FsILzbupgqcc-1pQL5Y2DykuIlwWUweM4,41279
65
65
  schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
66
66
  schemathesis/internal/checks.py,sha256=ZPvsPJ7gWwK0IpzBFgOMaq4L2e0yfeC8qxPrnpauVFA,2741
67
67
  schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
@@ -111,15 +111,15 @@ schemathesis/specs/openapi/checks.py,sha256=cuHTZsoHV2fdUz23_F99-mLelT1xtvaiS9Ec
111
111
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
112
112
  schemathesis/specs/openapi/converter.py,sha256=Yxw9lS_JKEyi-oJuACT07fm04bqQDlAu-iHwzkeDvE4,3546
113
113
  schemathesis/specs/openapi/definitions.py,sha256=WTkWwCgTc3OMxfKsqh6YDoGfZMTThSYrHGp8h0vLAK0,93935
114
- schemathesis/specs/openapi/examples.py,sha256=yBK0hjq5ROjk7BCLe7BO2dr7raijeZ6_KlZEol-cU-E,20401
114
+ schemathesis/specs/openapi/examples.py,sha256=fpZ8gzc1wxgpcUeOpIYaS_WOC-O0oPCLI4tcqmgNpog,21082
115
115
  schemathesis/specs/openapi/formats.py,sha256=3KtEC-8nQRwMErS-WpMadXsr8R0O-NzYwFisZqMuc-8,2761
116
116
  schemathesis/specs/openapi/links.py,sha256=C4Uir2P_EcpqME8ee_a1vdUM8Tm3ZcKNn2YsGjZiMUQ,17935
117
117
  schemathesis/specs/openapi/loaders.py,sha256=jlTYLoG5sVRh8xycIF2M2VDCZ44M80Sct07a_ycg1Po,25698
118
118
  schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
119
- schemathesis/specs/openapi/parameters.py,sha256=X_3PKqUScIiN_vbSFEauPYyxASyFv-_9lZ_9QEZRLqo,14655
119
+ schemathesis/specs/openapi/parameters.py,sha256=fP4BupY_1wFbjL9n0lTtpQZY0YBt2mCjrG598LF8ZOI,14712
120
120
  schemathesis/specs/openapi/patterns.py,sha256=L99UtslPvwObCVf5ndq3vL2YjQ7H1nMb-ZNMcyz_Qvk,12677
121
- schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
122
- schemathesis/specs/openapi/schemas.py,sha256=JA9SiBnwYg75kYnd4_0CWOuQv_XTfYwuDeGmFe4RtVo,53724
121
+ schemathesis/specs/openapi/references.py,sha256=0-gqbAxvBfrvFXA7YqmcNh7zRY7V_pKEnak0ncwQljI,9894
122
+ schemathesis/specs/openapi/schemas.py,sha256=bgtDwlHcXtw27bYx4mCj1GVnjO2ZazdMaWAxXESyo2I,54024
123
123
  schemathesis/specs/openapi/security.py,sha256=Z-6pk2Ga1PTUtBe298KunjVHsNh5A-teegeso7zcPIE,7138
124
124
  schemathesis/specs/openapi/serialization.py,sha256=rcZfqQbWer_RELedu4Sh5h_RhKYPWTfUjnmLwpP2R_A,11842
125
125
  schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
@@ -147,14 +147,14 @@ schemathesis/stateful/sink.py,sha256=bHYlgh-fMwg1Srxk_XGs0-WV34YccotwH9PGrxCK57A
147
147
  schemathesis/stateful/state_machine.py,sha256=EE1T0L21vBU0UHGiCmfPfIfnhU1WptB16h0t1iNVro0,13037
148
148
  schemathesis/stateful/statistic.py,sha256=2-uU5xpT9CbMulKgJWLZN6MUpC0Fskf5yXTt4ef4NFA,542
149
149
  schemathesis/stateful/validation.py,sha256=23qSZjC1_xRmtCX4OqsyG6pGxdlo6IZYid695ZpDQyU,3747
150
- schemathesis/transports/__init__.py,sha256=k35qBp-657qnHE9FfCowqO3rqOgCwSUnrdl2vAV3hnQ,12951
150
+ schemathesis/transports/__init__.py,sha256=9ahByCU8HNCWX8zc6ngGFrkW4kAHJZKGc1fpajKGYJU,13308
151
151
  schemathesis/transports/asgi.py,sha256=bwW9vMd1h89Jh7I4jHJVwSNUQzHvc7-JOD5u4hSHZd8,212
152
152
  schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZwo,1143
153
153
  schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
154
154
  schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
155
155
  schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
156
- schemathesis-3.39.13.dist-info/METADATA,sha256=rFdsmXi833eMqVorQ59g1gYgoPeuhQCnripqRiMlFKg,11901
157
- schemathesis-3.39.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
158
- schemathesis-3.39.13.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
- schemathesis-3.39.13.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
- schemathesis-3.39.13.dist-info/RECORD,,
156
+ schemathesis-3.39.15.dist-info/METADATA,sha256=_4VLl1QXjO3WQhtKtojjaD68tz6wGveA2s1CMwWteUE,11901
157
+ schemathesis-3.39.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
158
+ schemathesis-3.39.15.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
+ schemathesis-3.39.15.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
+ schemathesis-3.39.15.dist-info/RECORD,,