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 +1 -1
- schemathesis/cli/commands/run/handlers/output.py +5 -1
- schemathesis/engine/phases/unit/_executor.py +1 -1
- schemathesis/engine/recorder.py +11 -3
- schemathesis/generation/stateful/state_machine.py +6 -2
- schemathesis/specs/openapi/stateful/__init__.py +17 -6
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +57 -1
- schemathesis/specs/openapi/stateful/dependencies/models.py +25 -7
- schemathesis/specs/openapi/stateful/dependencies/naming.py +139 -53
- {schemathesis-4.3.2.dist-info → schemathesis-4.3.3.dist-info}/METADATA +1 -1
- {schemathesis-4.3.2.dist-info → schemathesis-4.3.3.dist-info}/RECORD +14 -14
- {schemathesis-4.3.2.dist-info → schemathesis-4.3.3.dist-info}/WHEEL +0 -0
- {schemathesis-4.3.2.dist-info → schemathesis-4.3.3.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.3.2.dist-info → schemathesis-4.3.3.dist-info}/licenses/LICENSE +0 -0
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,
|
|
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 = {
|
|
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,
|
|
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:
|
schemathesis/engine/recorder.py
CHANGED
|
@@ -41,9 +41,16 @@ class ScenarioRecorder:
|
|
|
41
41
|
self.checks = {}
|
|
42
42
|
self.interactions = {}
|
|
43
43
|
|
|
44
|
-
def record_case(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
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
|
-
"
|
|
46
|
-
"
|
|
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
|
-
"
|
|
59
|
-
"
|
|
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
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
79
|
+
"movie": "movies",
|
|
80
|
+
"nose": "noses",
|
|
81
|
+
"oasis": "oases",
|
|
82
|
+
"ox": "oxen",
|
|
83
|
+
"passerby": "passersby",
|
|
70
84
|
"pause": "pauses",
|
|
71
|
-
"
|
|
85
|
+
"person": "people",
|
|
72
86
|
"phase": "phases",
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
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
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
169
|
-
schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=
|
|
170
|
-
schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=
|
|
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.
|
|
183
|
-
schemathesis-4.3.
|
|
184
|
-
schemathesis-4.3.
|
|
185
|
-
schemathesis-4.3.
|
|
186
|
-
schemathesis-4.3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|