schemathesis 4.0.3__py3-none-any.whl → 4.0.4__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.
@@ -421,6 +421,7 @@ def clear_hypothesis_notes(exc: Exception) -> None:
421
421
  def is_unrecoverable_network_error(exc: Exception) -> bool:
422
422
  from http.client import RemoteDisconnected
423
423
 
424
+ import requests
424
425
  from urllib3.exceptions import ProtocolError
425
426
 
426
427
  def has_connection_reset(inner: BaseException) -> bool:
@@ -433,6 +434,8 @@ def is_unrecoverable_network_error(exc: Exception) -> bool:
433
434
 
434
435
  return False
435
436
 
437
+ if isinstance(exc, requests.Timeout):
438
+ return True
436
439
  if isinstance(exc.__context__, ProtocolError):
437
440
  if len(exc.__context__.args) == 2 and isinstance(exc.__context__.args[1], RemoteDisconnected):
438
441
  return True
@@ -442,14 +445,16 @@ def is_unrecoverable_network_error(exc: Exception) -> bool:
442
445
  return has_connection_reset(exc)
443
446
 
444
447
 
445
- @dataclass()
448
+ @dataclass
446
449
  class UnrecoverableNetworkError:
447
- error: requests.ConnectionError | ChunkedEncodingError
450
+ error: requests.ConnectionError | ChunkedEncodingError | requests.Timeout
448
451
  code_sample: str
449
452
 
450
453
  __slots__ = ("error", "code_sample")
451
454
 
452
- def __init__(self, error: requests.ConnectionError | ChunkedEncodingError, code_sample: str) -> None:
455
+ def __init__(
456
+ self, error: requests.ConnectionError | ChunkedEncodingError | requests.Timeout, code_sample: str
457
+ ) -> None:
453
458
  self.error = error
454
459
  self.code_sample = code_sample
455
460
 
@@ -133,9 +133,9 @@ def execute_state_machine_loop(
133
133
  ctx.step_failed()
134
134
  raise
135
135
  except Exception as exc:
136
- if isinstance(exc, (requests.ConnectionError, ChunkedEncodingError)) and is_unrecoverable_network_error(
137
- exc
138
- ):
136
+ if isinstance(
137
+ exc, (requests.ConnectionError, ChunkedEncodingError, requests.Timeout)
138
+ ) and is_unrecoverable_network_error(exc):
139
139
  transport_kwargs = engine.get_transport_kwargs(operation=input.case.operation)
140
140
  if exc.request is not None:
141
141
  headers = {key: value[0] for key, value in exc.request.headers.items()}
@@ -327,9 +327,9 @@ def cached_test_func(f: Callable) -> Callable:
327
327
  except (KeyboardInterrupt, Failure):
328
328
  raise
329
329
  except Exception as exc:
330
- if isinstance(exc, (requests.ConnectionError, ChunkedEncodingError)) and is_unrecoverable_network_error(
331
- exc
332
- ):
330
+ if isinstance(
331
+ exc, (requests.ConnectionError, ChunkedEncodingError, requests.Timeout)
332
+ ) and is_unrecoverable_network_error(exc):
333
333
  # Server likely has crashed and does not accept any connections at all
334
334
  # Don't report these error - only the original crash should be reported
335
335
  if exc.request is not None:
@@ -109,19 +109,22 @@ def cached_draw(strategy: st.SearchStrategy) -> Any:
109
109
  class CoverageContext:
110
110
  generation_modes: list[GenerationMode]
111
111
  location: str
112
+ is_required: bool
112
113
  path: list[str | int]
113
114
 
114
- __slots__ = ("location", "generation_modes", "path")
115
+ __slots__ = ("location", "generation_modes", "is_required", "path")
115
116
 
116
117
  def __init__(
117
118
  self,
118
119
  *,
119
120
  location: str,
120
121
  generation_modes: list[GenerationMode] | None = None,
122
+ is_required: bool,
121
123
  path: list[str | int] | None = None,
122
124
  ) -> None:
123
125
  self.location = location
124
126
  self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
127
+ self.is_required = is_required
125
128
  self.path = path or []
126
129
 
127
130
  @contextmanager
@@ -140,6 +143,7 @@ class CoverageContext:
140
143
  return CoverageContext(
141
144
  location=self.location,
142
145
  generation_modes=[GenerationMode.POSITIVE],
146
+ is_required=self.is_required,
143
147
  path=self.path,
144
148
  )
145
149
 
@@ -147,6 +151,7 @@ class CoverageContext:
147
151
  return CoverageContext(
148
152
  location=self.location,
149
153
  generation_modes=[GenerationMode.NEGATIVE],
154
+ is_required=self.is_required,
150
155
  path=self.path,
151
156
  )
152
157
 
@@ -157,6 +162,16 @@ class CoverageContext:
157
162
  return not is_invalid_path_parameter(value)
158
163
  return True
159
164
 
165
+ def leads_to_negative_test_case(self, value: Any) -> bool:
166
+ if self.location == "query":
167
+ # Some values will not be serialized into the query string
168
+ if isinstance(value, list) and not self.is_required:
169
+ # Optional parameters should be present
170
+ return any(item not in [{}, []] for item in value)
171
+ if isinstance(value, dict) and not self.is_required:
172
+ return bool(value)
173
+ return True
174
+
160
175
  def generate_from(self, strategy: st.SearchStrategy) -> Any:
161
176
  return cached_draw(strategy)
162
177
 
@@ -955,11 +970,13 @@ def _negative_items(ctx: CoverageContext, schema: dict[str, Any] | bool) -> Gene
955
970
  """Arrays not matching the schema."""
956
971
  nctx = ctx.with_negative()
957
972
  for value in cover_schema_iter(nctx, schema):
958
- yield NegativeValue(
959
- [value.value],
960
- description=f"Array with invalid items: {value.description}",
961
- location=nctx.current_path,
962
- )
973
+ items = [value.value]
974
+ if ctx.leads_to_negative_test_case(items):
975
+ yield NegativeValue(
976
+ items,
977
+ description=f"Array with invalid items: {value.description}",
978
+ location=nctx.current_path,
979
+ )
963
980
 
964
981
 
965
982
  def _not_matching_pattern(value: str, pattern: re.Pattern) -> bool:
@@ -1027,6 +1044,9 @@ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generat
1027
1044
  # Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
1028
1045
  without_format = {k: v for k, v in schema.items() if k != "format"}
1029
1046
  without_format.setdefault("type", "string")
1047
+ if ctx.location == "path":
1048
+ # Empty path parameters are invalid
1049
+ without_format["minLength"] = 1
1030
1050
  strategy = from_schema(without_format)
1031
1051
  if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
1032
1052
  if format == "hostname":
@@ -1060,7 +1080,7 @@ def _negative_type(
1060
1080
  strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
1061
1081
  for strategy in strategies.values():
1062
1082
  value = ctx.generate_from(strategy)
1063
- if seen.insert(value):
1083
+ if seen.insert(value) and ctx.is_valid_for_location(value):
1064
1084
  yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
1065
1085
 
1066
1086
 
@@ -474,7 +474,10 @@ def _iter_coverage_cases(
474
474
  for value in find_matching_in_responses(responses, parameter.name):
475
475
  schema.setdefault("examples", []).append(value)
476
476
  gen = coverage.cover_schema_iter(
477
- coverage.CoverageContext(location=location, generation_modes=generation_modes), schema
477
+ coverage.CoverageContext(
478
+ location=location, generation_modes=generation_modes, is_required=parameter.is_required
479
+ ),
480
+ schema,
478
481
  )
479
482
  value = next(gen, NOT_SET)
480
483
  if isinstance(value, NotSet):
@@ -492,7 +495,10 @@ def _iter_coverage_cases(
492
495
  if examples:
493
496
  schema.setdefault("examples", []).extend(examples)
494
497
  gen = coverage.cover_schema_iter(
495
- coverage.CoverageContext(location="body", generation_modes=generation_modes), schema
498
+ coverage.CoverageContext(
499
+ location="body", generation_modes=generation_modes, is_required=body.is_required
500
+ ),
501
+ schema,
496
502
  )
497
503
  value = next(gen, NOT_SET)
498
504
  if isinstance(value, NotSet):
@@ -712,11 +718,13 @@ def _iter_coverage_cases(
712
718
  }
713
719
 
714
720
  def _yield_negative(
715
- subschema: dict[str, Any], _location: str, _container_name: str
721
+ subschema: dict[str, Any], _location: str, _container_name: str, is_required: bool
716
722
  ) -> Generator[Case, None, None]:
717
723
  iterator = iter(
718
724
  coverage.cover_schema_iter(
719
- coverage.CoverageContext(location=_location, generation_modes=[GenerationMode.NEGATIVE]),
725
+ coverage.CoverageContext(
726
+ location=_location, generation_modes=[GenerationMode.NEGATIVE], is_required=is_required
727
+ ),
720
728
  subschema,
721
729
  )
722
730
  )
@@ -751,7 +759,7 @@ def _iter_coverage_cases(
751
759
  )
752
760
  if GenerationMode.NEGATIVE in generation_modes:
753
761
  subschema = _combination_schema(only_required, required, parameter_set)
754
- for case in _yield_negative(subschema, location, container_name):
762
+ for case in _yield_negative(subschema, location, container_name, is_required=bool(required)):
755
763
  kwargs = _case_to_kwargs(case)
756
764
  if not seen_negative.insert(kwargs):
757
765
  continue
@@ -778,7 +786,7 @@ def _iter_coverage_cases(
778
786
  )
779
787
  if GenerationMode.NEGATIVE in generation_modes:
780
788
  subschema = _combination_schema(combo, required, parameter_set)
781
- for case in _yield_negative(subschema, location, container_name):
789
+ for case in _yield_negative(subschema, location, container_name, is_required=bool(required)):
782
790
  assert case.meta is not None
783
791
  assert isinstance(case.meta.phase.data, CoveragePhaseData)
784
792
  # Already generated in one of the blocks above
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 4.0.3
3
+ Version: 4.0.4
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://github.com/schemathesis/schemathesis/blob/master/CHANGELOG.md
@@ -72,26 +72,26 @@ schemathesis/engine/__init__.py,sha256=QaFE-FinaTAaarteADo2RRMJ-Sz6hZB9TzD5KjMin
72
72
  schemathesis/engine/context.py,sha256=x-I9KX6rO6hdCvvN8FEdzIZBqIcNaxdNYHgQjcXbZhM,3931
73
73
  schemathesis/engine/control.py,sha256=FXzP8dxL47j1Giqpy2-Bsr_MdMw9YiATSK_UfpFwDtk,1348
74
74
  schemathesis/engine/core.py,sha256=5jfAqFH0XSD7NVgoSXuUPW-dooItscneAzUNq1RBh1E,5712
75
- schemathesis/engine/errors.py,sha256=cWKuwj0Kzr2BHdVCHACnniUJ8sFVJ0Nqckc3iggZS1o,18800
75
+ schemathesis/engine/errors.py,sha256=HRtFFg-TQ68VmGAM3p6VLOimTU7VaFnv6iKD9-ucjaw,18932
76
76
  schemathesis/engine/events.py,sha256=VV6epicFIJnX4c87fVNSd0ibDccX3gryDv52OUGa3FI,6370
77
77
  schemathesis/engine/recorder.py,sha256=K3HfMARrT5mPWXPnYebjjcq5CcsBRhMrtZwEL9_Lvtg,8432
78
78
  schemathesis/engine/phases/__init__.py,sha256=jUIfb_9QoUo4zmJEVU0z70PgXPYjt8CIqp4qP_HlYHg,3146
79
79
  schemathesis/engine/phases/probes.py,sha256=SEtWKPdkLfRTKV0_tbiNHTK3sJsUUPZ0jZQ9Nv4qUi8,5678
80
80
  schemathesis/engine/phases/stateful/__init__.py,sha256=Lz1rgNqCfUSIz173XqCGsiMuUI5bh4L-RIFexU1-c_Q,2461
81
- schemathesis/engine/phases/stateful/_executor.py,sha256=CV4jUuXpV4uSXVJqDI4btnLR8dpzOQVqbv2qVCgE4_s,15182
81
+ schemathesis/engine/phases/stateful/_executor.py,sha256=_303Yqflx1iFNTQI2EfjSp_2T21YvzJJgMSazhpv5JQ,15200
82
82
  schemathesis/engine/phases/stateful/context.py,sha256=A7X1SLDOWFpCvFN9IiIeNVZM0emjqatmJL_k9UsO7vM,2946
83
83
  schemathesis/engine/phases/unit/__init__.py,sha256=BvZh39LZmXg90Cy_Tn0cQY5y7eWzYvAEmJ43fGKFAt8,8715
84
- schemathesis/engine/phases/unit/_executor.py,sha256=jay_D7fmmBTjZigifmY30RiVP5Jb0OlK450fknSWZ_I,16471
84
+ schemathesis/engine/phases/unit/_executor.py,sha256=9MmZoKSBVSPk0LWwN3PZ3iaO9nzpT1Z70yzdEE48YYw,16489
85
85
  schemathesis/engine/phases/unit/_pool.py,sha256=iU0hdHDmohPnEv7_S1emcabuzbTf-Cznqwn0pGQ5wNQ,2480
86
86
  schemathesis/generation/__init__.py,sha256=tvNO2FLiY8z3fZ_kL_QJhSgzXfnT4UqwSXMHCwfLI0g,645
87
87
  schemathesis/generation/case.py,sha256=MuqnKsJBpGm2gaqDFdJi1yGSWgBhqJUwtYaX97kfXgo,11820
88
- schemathesis/generation/coverage.py,sha256=bKP0idU5-eiK4VwhH4kjxDPtCZzMg81mbN1tEDuT6EA,47913
88
+ schemathesis/generation/coverage.py,sha256=SlPD8WfrRXca4A9p6P894JXBAqjdCVAto0V4qQrceOE,48825
89
89
  schemathesis/generation/meta.py,sha256=adkoMuCfzSjHJ9ZDocQn0GnVldSCkLL3eVR5A_jafwM,2552
90
90
  schemathesis/generation/metrics.py,sha256=cZU5HdeAMcLFEDnTbNE56NuNq4P0N4ew-g1NEz5-kt4,2836
91
91
  schemathesis/generation/modes.py,sha256=Q1fhjWr3zxabU5qdtLvKfpMFZJAwlW9pnxgenjeXTyU,481
92
92
  schemathesis/generation/overrides.py,sha256=OBWqDQPreiliaf2M-oyXppVKHoJkCRzxtwSJx1b6AFw,3759
93
93
  schemathesis/generation/hypothesis/__init__.py,sha256=SVwM-rx07jPZzms0idWYACgUtWAxh49HRuTnaQ__zf0,1549
94
- schemathesis/generation/hypothesis/builder.py,sha256=xc5U3LfJ1lQHKFFW_9W8n3KFutwA2mv954A4jLhB9vg,33105
94
+ schemathesis/generation/hypothesis/builder.py,sha256=JtKh9hzob2byQrAtf0IXOgKX1c17mLiMQ8f030Hae2Y,33414
95
95
  schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
96
96
  schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
97
97
  schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
@@ -157,8 +157,8 @@ schemathesis/transport/prepare.py,sha256=iiB8KTAqnnuqjWzblIPiGVdkGIF7Yr1SAEz-KZz
157
157
  schemathesis/transport/requests.py,sha256=rziZTrZCVMAqgy6ldB8iTwhkpAsnjKSgK8hj5Sq3ThE,10656
158
158
  schemathesis/transport/serialization.py,sha256=igUXKZ_VJ9gV7P0TUc5PDQBJXl_s0kK9T3ljGWWvo6E,10339
159
159
  schemathesis/transport/wsgi.py,sha256=KoAfvu6RJtzyj24VGB8e-Iaa9smpgXJ3VsM8EgAz2tc,6152
160
- schemathesis-4.0.3.dist-info/METADATA,sha256=hm5ioN50c0UyShRq7AN-9qzDnKGLMYSxQwXw1Lw7jTc,8471
161
- schemathesis-4.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
162
- schemathesis-4.0.3.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
163
- schemathesis-4.0.3.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
164
- schemathesis-4.0.3.dist-info/RECORD,,
160
+ schemathesis-4.0.4.dist-info/METADATA,sha256=rRUbFXvyFsf72j7N6smvPu4mMvQYJLnDCvjPwf3XPOY,8471
161
+ schemathesis-4.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
162
+ schemathesis-4.0.4.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
163
+ schemathesis-4.0.4.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
164
+ schemathesis-4.0.4.dist-info/RECORD,,