schemathesis 4.3.2__py3-none-any.whl → 4.3.3__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.

Potentially problematic release.


This version of schemathesis might be problematic. Click here for more details.

schemathesis/checks.py CHANGED
@@ -81,7 +81,7 @@ class CheckContext:
81
81
 
82
82
  def _record_case(self, *, parent_id: str, case: Case) -> None:
83
83
  if self._recorder is not None:
84
- self._recorder.record_case(parent_id=parent_id, transition=None, case=case)
84
+ self._recorder.record_case(parent_id=parent_id, case=case, transition=None, is_transition_applied=False)
85
85
 
86
86
  def _record_response(self, *, case_id: str, response: Response) -> None:
87
87
  if self._recorder is not None:
@@ -1048,7 +1048,11 @@ class OutputHandler(EventHandler):
1048
1048
  and event.status not in (Status.INTERRUPTED, Status.SKIP, None)
1049
1049
  ):
1050
1050
  assert self.stateful_tests_manager is not None
1051
- links_seen = {case.transition.id for case in event.recorder.cases.values() if case.transition is not None}
1051
+ links_seen = {
1052
+ case.transition.id
1053
+ for case in event.recorder.cases.values()
1054
+ if case.transition is not None and case.is_transition_applied
1055
+ }
1052
1056
  self.stateful_tests_manager.update(links_seen, event.status)
1053
1057
  self._check_stateful_warnings(ctx, event)
1054
1058
 
@@ -385,7 +385,7 @@ def test_func(
385
385
  transport_kwargs: dict[str, Any],
386
386
  continue_on_failure: bool,
387
387
  ) -> None:
388
- recorder.record_case(parent_id=None, transition=None, case=case)
388
+ recorder.record_case(parent_id=None, case=case, transition=None, is_transition_applied=False)
389
389
  try:
390
390
  response = case.call(**transport_kwargs)
391
391
  except (requests.Timeout, requests.ConnectionError, ChunkedEncodingError) as error:
@@ -41,9 +41,16 @@ class ScenarioRecorder:
41
41
  self.checks = {}
42
42
  self.interactions = {}
43
43
 
44
- def record_case(self, *, parent_id: str | None, transition: Transition | None, case: Case) -> None:
44
+ def record_case(
45
+ self, *, parent_id: str | None, case: Case, transition: Transition | None, is_transition_applied: bool
46
+ ) -> None:
45
47
  """Record a test case and its relationship to a parent, if applicable."""
46
- self.cases[case.id] = CaseNode(value=case, parent_id=parent_id, transition=transition)
48
+ self.cases[case.id] = CaseNode(
49
+ value=case,
50
+ parent_id=parent_id,
51
+ transition=transition,
52
+ is_transition_applied=is_transition_applied,
53
+ )
47
54
 
48
55
  def record_response(self, *, case_id: str, response: Response) -> None:
49
56
  """Record the API response for a given test case."""
@@ -139,8 +146,9 @@ class CaseNode:
139
146
  # Transition may be absent if `parent_id` is present for cases when a case is derived inside a check
140
147
  # and outside of the implemented transition logic (e.g. Open API links)
141
148
  transition: Transition | None
149
+ is_transition_applied: bool
142
150
 
143
- __slots__ = ("value", "parent_id", "transition")
151
+ __slots__ = ("value", "parent_id", "transition", "is_transition_applied")
144
152
 
145
153
 
146
154
  @dataclass
@@ -37,12 +37,16 @@ class StepInput:
37
37
 
38
38
  case: Case
39
39
  transition: Transition | None # None for initial steps
40
+ # Whether this transition was actually applied
41
+ # Data extraction failures can prevent it, as well as transitions can be skipped in some cases
42
+ # to improve discovery of bugs triggered by non-stateful inputs during stateful testing
43
+ is_applied: bool
40
44
 
41
- __slots__ = ("case", "transition")
45
+ __slots__ = ("case", "transition", "is_applied")
42
46
 
43
47
  @classmethod
44
48
  def initial(cls, case: Case) -> StepInput:
45
- return cls(case=case, transition=None)
49
+ return cls(case=case, transition=None, is_applied=False)
46
50
 
47
51
 
48
52
  @dataclass
@@ -49,6 +49,8 @@ class OpenAPIStateMachine(APIStateMachine):
49
49
 
50
50
  # The proportion of negative tests generated for "root" transitions
51
51
  NEGATIVE_TEST_CASES_THRESHOLD = 10
52
+ # How often some transition is skipped
53
+ USE_TRANSITION_THRESHOLD = 85
52
54
 
53
55
 
54
56
  @dataclass
@@ -226,7 +228,7 @@ def classify_root_transitions(operations: list[APIOperation], transitions: ApiTr
226
228
  if not operation_transitions or not operation_transitions.outgoing:
227
229
  continue
228
230
 
229
- if is_likely_root_transition(operation, operation_transitions):
231
+ if is_likely_root_transition(operation):
230
232
  roots.reliable.add(operation.label)
231
233
  else:
232
234
  roots.fallback.add(operation.label)
@@ -234,7 +236,7 @@ def classify_root_transitions(operations: list[APIOperation], transitions: ApiTr
234
236
  return roots
235
237
 
236
238
 
237
- def is_likely_root_transition(operation: APIOperation, transitions: OperationTransitions) -> bool:
239
+ def is_likely_root_transition(operation: APIOperation) -> bool:
238
240
  """Check if operation is likely to succeed as a root transition."""
239
241
  # POST operations with request bodies are likely to create resources
240
242
  if operation.method == "post" and operation.body:
@@ -269,8 +271,12 @@ def into_step_input(
269
271
  and isinstance(transition.request_body.value, Ok)
270
272
  and transition.request_body.value.ok() is not UNRESOLVABLE
271
273
  and not link.merge_body
274
+ and draw(st.integers(min_value=0, max_value=99)) < USE_TRANSITION_THRESHOLD
272
275
  ):
273
276
  kwargs["body"] = transition.request_body.value.ok()
277
+
278
+ is_applied = bool(kwargs)
279
+
274
280
  cases = strategies.combine(
275
281
  [target.as_strategy(generation_mode=mode, phase=TestPhase.STATEFUL, **kwargs) for mode in modes]
276
282
  )
@@ -280,24 +286,26 @@ def into_step_input(
280
286
  and isinstance(transition.request_body.value, Ok)
281
287
  and transition.request_body.value.ok() is not UNRESOLVABLE
282
288
  and link.merge_body
289
+ and draw(st.integers(min_value=0, max_value=99)) < USE_TRANSITION_THRESHOLD
283
290
  ):
284
291
  new = transition.request_body.value.ok()
285
292
  if isinstance(case.body, dict) and isinstance(new, dict):
286
293
  case.body = {**case.body, **new}
287
294
  else:
288
295
  case.body = new
296
+ is_applied = True
289
297
  if case.meta and case.meta.generation.mode == GenerationMode.NEGATIVE:
290
298
  # It is possible that the new body is now valid and the whole test case could be valid too
291
299
  for alternative in case.operation.body:
292
300
  if alternative.media_type == case.media_type:
293
301
  schema = alternative.optimized_schema
294
- if jsonschema.validators.validator_for(schema)(schema).is_valid(new):
302
+ if jsonschema.validators.validator_for(schema)(schema).is_valid(case.body):
295
303
  case.meta.components[ParameterLocation.BODY] = ComponentInfo(
296
304
  mode=GenerationMode.POSITIVE
297
305
  )
298
306
  if all(info.mode == GenerationMode.POSITIVE for info in case.meta.components.values()):
299
307
  case.meta.generation.mode = GenerationMode.POSITIVE
300
- return StepInput(case=case, transition=transition)
308
+ return StepInput(case=case, transition=transition, is_applied=is_applied)
301
309
 
302
310
  return inner(output=_output)
303
311
 
@@ -322,10 +330,13 @@ def transition(*, name: str, target: Bundle, input: st.SearchStrategy[StepInput]
322
330
  def step_function(self: OpenAPIStateMachine, input: StepInput) -> StepOutput | None:
323
331
  if input.transition is not None:
324
332
  self.recorder.record_case(
325
- parent_id=input.transition.parent_id, transition=input.transition, case=input.case
333
+ parent_id=input.transition.parent_id,
334
+ transition=input.transition,
335
+ case=input.case,
336
+ is_transition_applied=input.is_applied,
326
337
  )
327
338
  else:
328
- self.recorder.record_case(parent_id=None, transition=None, case=input.case)
339
+ self.recorder.record_case(parent_id=None, case=input.case, transition=None, is_transition_applied=False)
329
340
  self.control.record_step(input, self.recorder)
330
341
  return APIStateMachine._step(self, input=input)
331
342
 
@@ -2,6 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Iterator
4
4
 
5
+ from schemathesis.core import media_types
6
+ from schemathesis.core.errors import MalformedMediaType
7
+ from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
5
8
  from schemathesis.core.parameters import ParameterLocation
6
9
  from schemathesis.specs.openapi.stateful.dependencies import naming
7
10
  from schemathesis.specs.openapi.stateful.dependencies.models import (
@@ -16,6 +19,7 @@ from schemathesis.specs.openapi.stateful.dependencies.resources import extract_r
16
19
 
17
20
  if TYPE_CHECKING:
18
21
  from schemathesis.core.compat import RefResolver
22
+ from schemathesis.specs.openapi.adapter.parameters import OpenApiBody
19
23
  from schemathesis.specs.openapi.schemas import APIOperation
20
24
 
21
25
 
@@ -32,7 +36,6 @@ def extract_inputs(
32
36
  Connects each parameter (e.g., `userId`) to its resource definition (`User`),
33
37
  creating placeholder resources if not yet discovered from their schemas.
34
38
  """
35
- # Note: Currently limited to path parameters. Query / header / body will be supported in future releases.
36
39
  for param in operation.path_parameters:
37
40
  input_slot = _resolve_parameter_dependency(
38
41
  parameter_name=param.name,
@@ -46,6 +49,13 @@ def extract_inputs(
46
49
  if input_slot is not None:
47
50
  yield input_slot
48
51
 
52
+ for body in operation.body:
53
+ try:
54
+ if media_types.is_json(body.media_type):
55
+ yield from _resolve_body_dependencies(body=body, operation=operation, resources=resources)
56
+ except MalformedMediaType:
57
+ continue
58
+
49
59
 
50
60
  def _resolve_parameter_dependency(
51
61
  *,
@@ -152,6 +162,52 @@ def _find_resource_in_responses(
152
162
  return None
153
163
 
154
164
 
165
+ def _resolve_body_dependencies(
166
+ *, body: OpenApiBody, operation: APIOperation, resources: ResourceMap
167
+ ) -> Iterator[InputSlot]:
168
+ schema = body.raw_schema
169
+ if not isinstance(schema, dict):
170
+ return
171
+
172
+ # Right now, the body schema comes bundled to dependency analysis
173
+ if BUNDLE_STORAGE_KEY in schema and "$ref" in schema:
174
+ schema_key = schema["$ref"].split("/")[-1]
175
+ resolved = schema[BUNDLE_STORAGE_KEY][schema_key]
176
+ else:
177
+ resolved = schema
178
+
179
+ # Inspect each property that could be a part of some other resource
180
+ properties = resolved.get("properties", {})
181
+ path = operation.path
182
+ for property_name in properties:
183
+ resource_name = naming.from_parameter(property_name, path)
184
+ if resource_name is None:
185
+ continue
186
+ resource = resources.get(resource_name)
187
+ if resource is None:
188
+ resource = ResourceDefinition.inferred_from_parameter(
189
+ name=resource_name,
190
+ parameter_name=property_name,
191
+ )
192
+ resources[resource_name] = resource
193
+ field = property_name
194
+ else:
195
+ field = (
196
+ naming.find_matching_field(
197
+ parameter=property_name,
198
+ resource=resource_name,
199
+ fields=resource.fields,
200
+ )
201
+ or "id"
202
+ )
203
+ yield InputSlot(
204
+ resource=resource,
205
+ resource_field=field,
206
+ parameter_name=property_name,
207
+ parameter_location=ParameterLocation.BODY,
208
+ )
209
+
210
+
155
211
  def update_input_field_bindings(resource_name: str, operations: OperationMap) -> None:
156
212
  """Update input slots field bindings after resource definition was upgraded.
157
213
 
@@ -9,6 +9,7 @@ from typing_extensions import TypeAlias
9
9
 
10
10
  from schemathesis.core.parameters import ParameterLocation
11
11
  from schemathesis.core.transforms import encode_pointer
12
+ from schemathesis.specs.openapi.stateful.links import SCHEMATHESIS_LINK_EXTENSION
12
13
 
13
14
 
14
15
  @dataclass
@@ -61,12 +62,21 @@ class DependencyGraph:
61
62
  output_slot.pointer, input_slot.resource_field, output_slot.cardinality
62
63
  )
63
64
  link_name = f"{consumer.method.capitalize()}{input_slot.resource.name}"
65
+ parameters = {}
66
+ request_body = {}
67
+ # Data is extracted from response body
68
+ if input_slot.parameter_location == ParameterLocation.BODY:
69
+ request_body = {
70
+ input_slot.parameter_name: f"$response.body#{body_pointer}",
71
+ }
72
+ else:
73
+ parameters = {
74
+ f"{input_slot.parameter_location.value}.{input_slot.parameter_name}": f"$response.body#{body_pointer}",
75
+ }
64
76
  links[link_name] = LinkDefinition(
65
77
  operation_ref=f"#/paths/{consumer_path}/{consumer.method}",
66
- parameters={
67
- # Data is extracted from response body
68
- f"{input_slot.parameter_location.value}.{input_slot.parameter_name}": f"$response.body#{body_pointer}",
69
- },
78
+ parameters=parameters,
79
+ request_body=request_body,
70
80
  )
71
81
  if links:
72
82
  yield ResponseLinks(
@@ -136,14 +146,22 @@ class LinkDefinition:
136
146
  parameters: dict[str, str]
137
147
  """Parameter mappings (e.g., {'path.id': '$response.body#/id'})"""
138
148
 
139
- __slots__ = ("operation_ref", "parameters")
149
+ request_body: dict[str, str]
150
+ """Request body (e.g., {'path.id': '$response.body#/id'})"""
151
+
152
+ __slots__ = ("operation_ref", "parameters", "request_body")
140
153
 
141
154
  def to_openapi(self) -> dict[str, Any]:
142
155
  """Convert to OpenAPI Links format."""
143
- return {
156
+ links: dict[str, Any] = {
144
157
  "operationRef": self.operation_ref,
145
- "parameters": self.parameters,
146
158
  }
159
+ if self.parameters:
160
+ links["parameters"] = self.parameters
161
+ if self.request_body:
162
+ links["requestBody"] = self.request_body
163
+ links[SCHEMATHESIS_LINK_EXTENSION] = {"merge_body": True}
164
+ return links
147
165
 
148
166
 
149
167
  @dataclass
@@ -27,76 +27,122 @@ def from_path(path: str) -> str | None:
27
27
 
28
28
 
29
29
  IRREGULAR_TO_PLURAL = {
30
- "echo": "echoes",
31
- "dingo": "dingoes",
32
- "volcano": "volcanoes",
33
- "tornado": "tornadoes",
34
- "torpedo": "torpedoes",
35
- "genus": "genera",
36
- "viscus": "viscera",
37
- "stigma": "stigmata",
38
- "stoma": "stomata",
39
- "dogma": "dogmata",
40
- "lemma": "lemmata",
30
+ "abuse": "abuses",
31
+ "alias": "aliases",
32
+ "analysis": "analyses",
41
33
  "anathema": "anathemata",
42
- "ox": "oxen",
43
34
  "axe": "axes",
35
+ "base": "bases",
36
+ "bookshelf": "bookshelves",
37
+ "cache": "caches",
38
+ "canvas": "canvases",
39
+ "carve": "carves",
40
+ "case": "cases",
41
+ "cause": "causes",
42
+ "child": "children",
43
+ "course": "courses",
44
+ "criterion": "criteria",
45
+ "database": "databases",
46
+ "defense": "defenses",
47
+ "diagnosis": "diagnoses",
44
48
  "die": "dice",
45
- "yes": "yeses",
46
- "foot": "feet",
49
+ "dingo": "dingoes",
50
+ "disease": "diseases",
51
+ "dogma": "dogmata",
52
+ "dose": "doses",
47
53
  "eave": "eaves",
54
+ "echo": "echoes",
55
+ "enterprise": "enterprises",
56
+ "ephemeris": "ephemerides",
57
+ "excuse": "excuses",
58
+ "expense": "expenses",
59
+ "foot": "feet",
60
+ "franchise": "franchises",
61
+ "genus": "genera",
48
62
  "goose": "geese",
49
- "tooth": "teeth",
50
- "quiz": "quizzes",
51
- "human": "humans",
52
- "proof": "proofs",
53
- "carve": "carves",
54
- "valve": "valves",
55
- "looey": "looies",
56
- "thief": "thieves",
57
63
  "groove": "grooves",
58
- "pickaxe": "pickaxes",
59
- "passerby": "passersby",
60
- "canvas": "canvases",
61
- "use": "uses",
62
- "case": "cases",
63
- "vase": "vases",
64
+ "half": "halves",
65
+ "horse": "horses",
64
66
  "house": "houses",
67
+ "human": "humans",
68
+ "hypothesis": "hypotheses",
69
+ "index": "indices",
70
+ "knife": "knives",
71
+ "lemma": "lemmata",
72
+ "license": "licenses",
73
+ "life": "lives",
74
+ "loaf": "loaves",
75
+ "looey": "looies",
76
+ "man": "men",
77
+ "matrix": "matrices",
65
78
  "mouse": "mice",
66
- "reuse": "reuses",
67
- "abuse": "abuses",
68
- "excuse": "excuses",
69
- "cause": "causes",
79
+ "movie": "movies",
80
+ "nose": "noses",
81
+ "oasis": "oases",
82
+ "ox": "oxen",
83
+ "passerby": "passersby",
70
84
  "pause": "pauses",
71
- "base": "bases",
85
+ "person": "people",
72
86
  "phase": "phases",
73
- "rose": "roses",
74
- "dose": "doses",
75
- "nose": "noses",
76
- "horse": "horses",
77
- "course": "courses",
87
+ "phenomenon": "phenomena",
88
+ "pickaxe": "pickaxes",
89
+ "proof": "proofs",
90
+ "purchase": "purchases",
78
91
  "purpose": "purposes",
92
+ "quiz": "quizzes",
93
+ "radius": "radii",
94
+ "release": "releases",
79
95
  "response": "responses",
96
+ "reuse": "reuses",
97
+ "rose": "roses",
98
+ "scarf": "scarves",
99
+ "self": "selves",
80
100
  "sense": "senses",
101
+ "shelf": "shelves",
102
+ "size": "sizes",
103
+ "snooze": "snoozes",
104
+ "stigma": "stigmata",
105
+ "stoma": "stomata",
106
+ "synopsis": "synopses",
81
107
  "tense": "tenses",
82
- "expense": "expenses",
83
- "license": "licenses",
84
- "defense": "defenses",
108
+ "thief": "thieves",
109
+ "tooth": "teeth",
110
+ "tornado": "tornadoes",
111
+ "torpedo": "torpedoes",
112
+ "use": "uses",
113
+ "valve": "valves",
114
+ "vase": "vases",
115
+ "verse": "verses",
116
+ "viscus": "viscera",
117
+ "volcano": "volcanoes",
118
+ "warehouse": "warehouses",
119
+ "wave": "waves",
120
+ "wife": "wives",
121
+ "wolf": "wolves",
122
+ "woman": "women",
123
+ "yes": "yeses",
124
+ "vie": "vies",
85
125
  }
86
126
  IRREGULAR_TO_SINGULAR = {v: k for k, v in IRREGULAR_TO_PLURAL.items()}
87
127
  UNCOUNTABLE = frozenset(
88
128
  [
129
+ "access",
130
+ "address",
89
131
  "adulthood",
90
132
  "advice",
91
133
  "agenda",
92
134
  "aid",
93
135
  "aircraft",
94
136
  "alcohol",
137
+ "alias",
95
138
  "ammo",
139
+ "analysis",
96
140
  "analytics",
97
141
  "anime",
142
+ "anonymous",
98
143
  "athletics",
99
144
  "audio",
145
+ "bias",
100
146
  "bison",
101
147
  "blood",
102
148
  "bream",
@@ -104,22 +150,31 @@ UNCOUNTABLE = frozenset(
104
150
  "butter",
105
151
  "carp",
106
152
  "cash",
153
+ "chaos",
107
154
  "chassis",
108
155
  "chess",
109
156
  "clothing",
110
157
  "cod",
111
158
  "commerce",
159
+ "compass",
160
+ "consensus",
112
161
  "cooperation",
113
162
  "corps",
163
+ "data",
114
164
  "debris",
165
+ "deer",
115
166
  "diabetes",
167
+ "diagnosis",
116
168
  "digestion",
117
169
  "elk",
118
170
  "energy",
171
+ "ephemeris",
119
172
  "equipment",
173
+ "eries",
120
174
  "excretion",
121
175
  "expertise",
122
176
  "firmware",
177
+ "fish",
123
178
  "flounder",
124
179
  "fun",
125
180
  "gallows",
@@ -141,12 +196,15 @@ UNCOUNTABLE = frozenset(
141
196
  "machinery",
142
197
  "mackerel",
143
198
  "mail",
199
+ "manga",
200
+ "means",
144
201
  "media",
202
+ "metadata",
145
203
  "mews",
204
+ "money",
146
205
  "moose",
147
- "music",
148
206
  "mud",
149
- "manga",
207
+ "music",
150
208
  "news",
151
209
  "only",
152
210
  "personnel",
@@ -156,6 +214,9 @@ UNCOUNTABLE = frozenset(
156
214
  "police",
157
215
  "pollution",
158
216
  "premises",
217
+ "progress",
218
+ "prometheus",
219
+ "radius",
159
220
  "rain",
160
221
  "research",
161
222
  "rice",
@@ -164,10 +225,13 @@ UNCOUNTABLE = frozenset(
164
225
  "series",
165
226
  "sewage",
166
227
  "shambles",
228
+ "sheep",
167
229
  "shrimp",
168
230
  "software",
231
+ "species",
169
232
  "staff",
170
233
  "swine",
234
+ "synopsis",
171
235
  "tennis",
172
236
  "traffic",
173
237
  "transportation",
@@ -178,22 +242,37 @@ UNCOUNTABLE = frozenset(
178
242
  "whiting",
179
243
  "wildebeest",
180
244
  "wildlife",
245
+ "wireless",
181
246
  "you",
182
- "sheep",
183
- "deer",
184
- "species",
185
- "series",
186
- "means",
187
247
  ]
188
248
  )
189
249
 
190
250
 
251
+ def _is_word_like(s: str) -> bool:
252
+ """Check if string looks like a word (not a path, technical term, etc)."""
253
+ # Skip empty or very short
254
+ if not s or len(s) < 2:
255
+ return False
256
+ # Skip if contains non-word characters (except underscore and hyphen)
257
+ if not all(c.isalpha() or c in ("_", "-") for c in s):
258
+ return False
259
+ # Skip if has numbers
260
+ return not any(c.isdigit() for c in s)
261
+
262
+
191
263
  def to_singular(word: str) -> str:
192
- if word in UNCOUNTABLE:
264
+ if not _is_word_like(word):
265
+ return word
266
+ if word.lower() in UNCOUNTABLE:
267
+ return word
268
+ known_lower = IRREGULAR_TO_SINGULAR.get(word.lower())
269
+ if known_lower is not None:
270
+ # Preserve case: if input was capitalized, capitalize result
271
+ if word[0].isupper():
272
+ return known_lower.capitalize()
273
+ return known_lower
274
+ if word.endswith(("ss", "us")):
193
275
  return word
194
- known = IRREGULAR_TO_SINGULAR.get(word)
195
- if known is not None:
196
- return known
197
276
  if word.endswith("ies") and len(word) > 3 and word[-4] not in "aeiou":
198
277
  return word[:-3] + "y"
199
278
  if word.endswith("sses"):
@@ -211,11 +290,18 @@ def to_singular(word: str) -> str:
211
290
 
212
291
 
213
292
  def to_plural(word: str) -> str:
214
- if word in UNCOUNTABLE:
293
+ if not _is_word_like(word):
294
+ return word
295
+ if word.lower() in UNCOUNTABLE:
215
296
  return word
216
297
  known = IRREGULAR_TO_PLURAL.get(word)
217
298
  if known is not None:
218
299
  return known
300
+ known_lower = IRREGULAR_TO_PLURAL.get(word.lower())
301
+ if known_lower is not None:
302
+ if word[0].isupper():
303
+ return known_lower.capitalize()
304
+ return known_lower
219
305
  # Only change y -> ies after consonants (party -> parties, not day -> days)
220
306
  if word.endswith("y") and len(word) > 1 and word[-2] not in "aeiou":
221
307
  return word[:-1] + "ies"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 4.3.2
3
+ Version: 4.3.3
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
@@ -1,6 +1,6 @@
1
1
  schemathesis/__init__.py,sha256=QqVUCBQr-RDEstgCZLsxzIa9HJslVSeijrm9gES4b_0,1423
2
2
  schemathesis/auths.py,sha256=JdEwPRS9WKmPcxzGXYYz9pjlIUMQYCfif7ZJU0Kde-I,16400
3
- schemathesis/checks.py,sha256=GTdejjXDooAOuq66nvCK3i-AMPBuU-_-aNeSeL9JIlc,6561
3
+ schemathesis/checks.py,sha256=yyR219TQjLAieop5O2waEVLILF6_i10B8mkutd6QaCs,6590
4
4
  schemathesis/errors.py,sha256=K3irHIZkrBH2-9LIjlgXlm8RNC41Nffd39ncfwagUvw,1053
5
5
  schemathesis/filters.py,sha256=IevPA5A04GfRLLjmkFLZ0CLhjNO3RmpZq_yw6MqjLIA,13515
6
6
  schemathesis/hooks.py,sha256=q2wqYNgpMCO8ImSBkbrWDSwN0BSELelqJMgAAgGvv2M,14836
@@ -23,7 +23,7 @@ schemathesis/cli/commands/run/handlers/__init__.py,sha256=TPZ3KdGi8m0fjlN0GjA31M
23
23
  schemathesis/cli/commands/run/handlers/base.py,sha256=qUtDvtr3F6were_BznfnaPpMibGJMnQ5CA9aEzcIUBc,1306
24
24
  schemathesis/cli/commands/run/handlers/cassettes.py,sha256=LzvQp--Ub5MXF7etet7fQD0Ufloh1R0j2X1o9dT8Z4k,19253
25
25
  schemathesis/cli/commands/run/handlers/junitxml.py,sha256=qiFvM4-SlM67sep003SkLqPslzaEb4nOm3bkzw-DO-Q,2602
26
- schemathesis/cli/commands/run/handlers/output.py,sha256=TwK82zNpIZ7Q76ggTp8gcW2clzrw0WBmHFJMcvYL1nE,63927
26
+ schemathesis/cli/commands/run/handlers/output.py,sha256=jWrqEkEQPO2kgzxOffZacqxH6r7dkDmAx0ep9GA3NU8,64020
27
27
  schemathesis/cli/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  schemathesis/cli/ext/fs.py,sha256=dHQYBjQozQmuSSfXVp-2KWFK0ESOb_w-lV2SptfMfco,461
29
29
  schemathesis/cli/ext/groups.py,sha256=kQ37t6qeArcKaY2y5VxyK3_KwAkBKCVm58IYV8gewds,2720
@@ -82,14 +82,14 @@ schemathesis/engine/core.py,sha256=qlPHnZVq2RrUe93fOciXd1hC3E1gVyF2BIWMPMeLIj8,6
82
82
  schemathesis/engine/errors.py,sha256=FlpEk44WRLzRkdK9m37z93EQuY3kbeMIQRGwU5e3Qm4,19005
83
83
  schemathesis/engine/events.py,sha256=jpCtMkWWfNe2jUeZh_Ly_wfZEF44EOodL-I_W4C9rgg,6594
84
84
  schemathesis/engine/observations.py,sha256=T-5R8GeVIqvxpCMxc6vZ04UUxUTx3w7689r3Dc6bIcE,1416
85
- schemathesis/engine/recorder.py,sha256=K3HfMARrT5mPWXPnYebjjcq5CcsBRhMrtZwEL9_Lvtg,8432
85
+ schemathesis/engine/recorder.py,sha256=KWyWkGkZxIwSDU92jNWCJXU4G4E5WqfhLM6G1Yi7Jyo,8636
86
86
  schemathesis/engine/phases/__init__.py,sha256=7Yp7dQbd6-K9pavIJeURg6jiNeMpW8UU-Iiikr668ts,3278
87
87
  schemathesis/engine/phases/probes.py,sha256=YogjJcZJcTMS8sMdGnG4oXKmMUj_4r_J7MY-BBJtCRU,5690
88
88
  schemathesis/engine/phases/stateful/__init__.py,sha256=Lz1rgNqCfUSIz173XqCGsiMuUI5bh4L-RIFexU1-c_Q,2461
89
89
  schemathesis/engine/phases/stateful/_executor.py,sha256=yRpUJqKLTKMVRy7hEXPwmI23CtgGIprz341lCJwvTrU,15613
90
90
  schemathesis/engine/phases/stateful/context.py,sha256=A7X1SLDOWFpCvFN9IiIeNVZM0emjqatmJL_k9UsO7vM,2946
91
91
  schemathesis/engine/phases/unit/__init__.py,sha256=9dDcxyj887pktnE9YDIPNaR-vc7iqKQWIrFr77SbUTQ,8786
92
- schemathesis/engine/phases/unit/_executor.py,sha256=UkOcmR23rl5PSIuNMjthZxadbAOHYSmTWft7Wa91yLs,17206
92
+ schemathesis/engine/phases/unit/_executor.py,sha256=YDibV3lkC2UMHLvh1FSmnlaQ-SJS-R0MU2qEF4NBbf0,17235
93
93
  schemathesis/engine/phases/unit/_pool.py,sha256=iU0hdHDmohPnEv7_S1emcabuzbTf-Cznqwn0pGQ5wNQ,2480
94
94
  schemathesis/generation/__init__.py,sha256=tvNO2FLiY8z3fZ_kL_QJhSgzXfnT4UqwSXMHCwfLI0g,645
95
95
  schemathesis/generation/case.py,sha256=Qc2_5JrWuUkCzAFTTgnVqNUJ2sioslmINTXiY7nHHgA,12326
@@ -105,7 +105,7 @@ schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5
105
105
  schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
106
106
  schemathesis/generation/hypothesis/strategies.py,sha256=RurE81E06d99YKG48dizy9346ayfNswYTt38zewmGgw,483
107
107
  schemathesis/generation/stateful/__init__.py,sha256=s7jiJEnguIj44IsRyMi8afs-8yjIUuBbzW58bH5CHjs,1042
108
- schemathesis/generation/stateful/state_machine.py,sha256=3oAGpn46adNJx3sp5ym9e30SyYOjJGiEqenDZ5gWtBY,8803
108
+ schemathesis/generation/stateful/state_machine.py,sha256=DJjIxeTFpVfBXqZYUNnfDZSUXXt0ydQqOe75lWLmqlk,9098
109
109
  schemathesis/graphql/__init__.py,sha256=_eO6MAPHGgiADVGRntnwtPxmuvk666sAh-FAU4cG9-0,326
110
110
  schemathesis/graphql/checks.py,sha256=IADbxiZjgkBWrC5yzHDtohRABX6zKXk5w_zpWNwdzYo,3186
111
111
  schemathesis/graphql/loaders.py,sha256=2tgG4HIvFmjHLr_KexVXnT8hSBM-dKG_fuXTZgE97So,9445
@@ -160,14 +160,14 @@ schemathesis/specs/openapi/negative/__init__.py,sha256=B78vps314fJOMZwlPdv7vUHo7
160
160
  schemathesis/specs/openapi/negative/mutations.py,sha256=9U352xJsdZBR-Zfy1V7_X3a5i91LIUS9Zqotrzp3BLA,21000
161
161
  schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
162
162
  schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
163
- schemathesis/specs/openapi/stateful/__init__.py,sha256=nD5f9pP2Rx2DKIeXtbc_KqUukC4Nf2834cHeOp52byM,16247
163
+ schemathesis/specs/openapi/stateful/__init__.py,sha256=CQx2WJ3mKn5qmYRc90DqsG9w3Gx7DrB60S9HFz81STY,16663
164
164
  schemathesis/specs/openapi/stateful/control.py,sha256=QaXLSbwQWtai5lxvvVtQV3BLJ8n5ePqSKB00XFxp-MA,3695
165
165
  schemathesis/specs/openapi/stateful/inference.py,sha256=9o9V-UUpphW7u_Kqz5MCp1_JXS2H_rcAZwz0bwJnmbI,9637
166
166
  schemathesis/specs/openapi/stateful/links.py,sha256=G6vqW6JFOdhF044ZjG6PsSwAHU1yP4E3FolcNFE55NM,7918
167
167
  schemathesis/specs/openapi/stateful/dependencies/__init__.py,sha256=epBYtVw7q9mkV-UtlJNbfJQgwAs9d5jkOJYkyEeUMvE,3348
168
- schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=5niLSz7-wl-sP9cJdnYBItwEeLuzJr3nw4XIMT3yt98,6764
169
- schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=4MXJzCat1bU-tmwAM7OH2dFBys_YHCJw9wTgd9Hib3c,9728
170
- schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=1aaa8vc56tsyOAKAvD8oPk55S-qbzrCBYe1kCk3Y9VY,9052
168
+ schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=DJDDCq73OYvCIPMxLKXJGTQGloNf6z6mgxjzjD0kJHA,8739
169
+ schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=BkKSUK_irj-peBjQplvau-tyGbBKRJdKhzNkOTJ51l4,10650
170
+ schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=MGoyh1bfw2SoKzdbzpHxed9LHMjokPJTU_YErZaF-Ls,11396
171
171
  schemathesis/specs/openapi/stateful/dependencies/outputs.py,sha256=zvVUfQWNIuhMkKDpz5hsVGkkvkefLt1EswpJAnHajOw,1186
172
172
  schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=7E2Z6LvomSRrp_0vCD_adzoux0wBLEjKi_EiSqiN43U,9664
173
173
  schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=pNV2GibNW8042KrdfUQBdJEkGj_dd84bTHbqunba48k,13976
@@ -179,8 +179,8 @@ schemathesis/transport/prepare.py,sha256=erYXRaxpQokIDzaIuvt_csHcw72iHfCyNq8VNEz
179
179
  schemathesis/transport/requests.py,sha256=wriRI9fprTplE_qEZLEz1TerX6GwkE3pwr6ZnU2o6vQ,10648
180
180
  schemathesis/transport/serialization.py,sha256=GwO6OAVTmL1JyKw7HiZ256tjV4CbrRbhQN0ep1uaZwI,11157
181
181
  schemathesis/transport/wsgi.py,sha256=kQtasFre6pjdJWRKwLA_Qb-RyQHCFNpaey9ubzlFWKI,5907
182
- schemathesis-4.3.2.dist-info/METADATA,sha256=t4XVoDj7Tst0-FT19wxnLdQ3wVtaveCp18GMoa1ZzU4,8540
183
- schemathesis-4.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
184
- schemathesis-4.3.2.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
185
- schemathesis-4.3.2.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
186
- schemathesis-4.3.2.dist-info/RECORD,,
182
+ schemathesis-4.3.3.dist-info/METADATA,sha256=VZo592TRmLn636zc-i_sq0FJh0d_zGmZoEZysc9bRe0,8540
183
+ schemathesis-4.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
184
+ schemathesis-4.3.3.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
185
+ schemathesis-4.3.3.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
186
+ schemathesis-4.3.3.dist-info/RECORD,,