Skip to content
This repository was archived by the owner on Nov 19, 2023. It is now read-only.

Commit c281ffc

Browse files
Na'aman HirschfeldNa'aman Hirschfeld
authored andcommitted
fix/208: resolved indeterminism
1 parent e89a5cc commit c281ffc

File tree

4 files changed

+33
-32
lines changed

4 files changed

+33
-32
lines changed

openapi_tester/schema_tester.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
""" Schema Tester """
2-
from itertools import combinations
2+
from itertools import chain
33
from typing import Any, Callable, Dict, List, Optional, Union, cast
44

55
from django.conf import settings
@@ -19,7 +19,7 @@
1919
)
2020
from openapi_tester.exceptions import DocumentationError, UndocumentedSchemaSectionError
2121
from openapi_tester.loaders import DrfSpectacularSchemaLoader, DrfYasgSchemaLoader, StaticSchemaLoader
22-
from openapi_tester.utils import merge_objects, normalize_schema_section
22+
from openapi_tester.utils import lazy_combinations, normalize_schema_section
2323
from openapi_tester.validators import (
2424
validate_enum,
2525
validate_format,
@@ -154,7 +154,7 @@ def get_response_schema_section(self, response: td.Response) -> Dict[str, Any]:
154154

155155
def handle_one_of(self, schema_section: dict, data: Any, reference: str, **kwargs: Any):
156156
matches = 0
157-
for option in [normalize_schema_section(entry) for entry in schema_section["oneOf"]]:
157+
for option in schema_section["oneOf"]:
158158
try:
159159
self.test_schema_section(schema_section=option, data=data, reference=f"{reference}.oneOf", **kwargs)
160160
matches += 1
@@ -164,10 +164,8 @@ def handle_one_of(self, schema_section: dict, data: Any, reference: str, **kwarg
164164
raise DocumentationError(f"{VALIDATE_ONE_OF_ERROR.format(matches=matches)}\n\nReference: {reference}.oneOf")
165165

166166
def handle_any_of(self, schema_section: dict, data: Any, reference: str, **kwargs: Any):
167-
any_of: List[Dict[str, Any]] = [normalize_schema_section(entry) for entry in schema_section.get("anyOf", [])]
168-
for i in range(2, len(any_of) + 1):
169-
any_of.extend([merge_objects(combination) for combination in combinations(any_of, i)])
170-
for schema in any_of:
167+
any_of: List[Dict[str, Any]] = schema_section.get("anyOf", [])
168+
for schema in chain(any_of, lazy_combinations(any_of)):
171169
try:
172170
self.test_schema_section(schema_section=schema, data=data, reference=f"{reference}.anyOf", **kwargs)
173171
return

openapi_tester/utils.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,47 @@
11
""" Utils Module - this file contains utility functions used in multiple places """
2-
from typing import Any, Dict, Sequence
2+
from copy import deepcopy
3+
from itertools import chain, combinations
4+
from typing import Any, Dict, Iterator, Sequence
35

46

57
def merge_objects(dictionaries: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
68
""" helper function to deep merge objects """
79
output: Dict[str, Any] = {}
810
for dictionary in dictionaries:
911
for key, value in dictionary.items():
10-
if isinstance(value, dict) and "allOf" in value:
11-
all_of = merge_objects(value.pop("allOf"))
12-
value = merge_objects([value, all_of])
1312
if key not in output:
1413
output[key] = value
1514
continue
1615
current_value = output[key]
1716
if isinstance(current_value, list) and isinstance(value, list):
18-
output[key] = [*output[key], *value]
17+
output[key] = list(chain(output[key], value))
18+
continue
1919
if isinstance(current_value, dict) and isinstance(value, dict):
2020
output[key] = merge_objects([current_value, value])
21+
continue
2122
return output
2223

2324

2425
def normalize_schema_section(schema_section: dict) -> dict:
2526
""" helper method to remove allOf and handle edge uses of oneOf"""
26-
output: Dict[str, Any] = {**schema_section}
27-
if "allOf" in schema_section:
28-
all_of = schema_section.pop("allOf")
29-
schema_section = {**schema_section, **merge_objects(all_of)}
30-
if schema_section.get("oneOf") and all(item.get("enum") for item in schema_section["oneOf"]):
27+
output: Dict[str, Any] = deepcopy(schema_section)
28+
if output.get("allOf"):
29+
all_of = output.pop("allOf")
30+
output = {**output, **merge_objects(all_of)}
31+
if output.get("oneOf") and all(item.get("enum") for item in output["oneOf"]):
3132
# handle the way drf-spectacular is doing enums
32-
one_of = schema_section.pop("oneOf")
33-
schema_section = {**schema_section, **merge_objects(one_of)}
33+
one_of = output.pop("oneOf")
34+
output = {**output, **merge_objects(one_of)}
3435
for key, value in output.items():
3536
if isinstance(value, dict):
3637
output[key] = normalize_schema_section(value)
3738
elif isinstance(value, list):
3839
output[key] = [normalize_schema_section(entry) if isinstance(entry, dict) else entry for entry in value]
39-
return schema_section
40+
return output
41+
42+
43+
def lazy_combinations(options_list: Sequence[Dict[str, Any]]) -> Iterator[dict]:
44+
""" helper to lazy evaluate possible permutations of possible combinations """
45+
for i in range(2, len(options_list) + 1):
46+
for combination in combinations(options_list, i):
47+
yield merge_objects(combination)

tests/schema_converter.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,15 @@ def __init__(self, schema: dict):
2525

2626
def convert_schema(self, schema: Dict[str, Any]) -> Any:
2727
schema_type = schema.get("type", "object")
28-
sample: List[Dict[str, Any]] = []
2928
schema = normalize_schema_section(schema)
3029
if "oneOf" in schema:
3130
one_of = schema.pop("oneOf")
32-
while not sample:
33-
sample = random.sample(one_of, 1)
34-
return self.convert_schema({**schema, **sample[0]})
31+
return self.convert_schema({**schema, **random.sample(one_of, 1)[0]})
3532
if "anyOf" in schema:
3633
any_of = schema.pop("anyOf")
37-
while not sample:
38-
sample = random.sample(any_of, random.randint(1, len(any_of)))
39-
sample = [normalize_schema_section(item) for item in sample]
40-
return self.convert_schema({**schema, **merge_objects(sample)})
34+
return self.convert_schema(
35+
{**schema, **merge_objects(random.sample(any_of, random.randint(1, len(any_of))))}
36+
)
4137
if schema_type == "array":
4238
return self.convert_schema_array_to_list(schema)
4339
if schema_type == "object":
@@ -77,9 +73,9 @@ def schema_type_to_mock_value(self, schema_object: Dict[str, Any]) -> Any:
7773
return random.sample(enum, 1)[0]
7874
if schema_type in ["integer", "number"] and (minimum is not None or maximum is not None):
7975
if minimum is not None:
80-
minimum += 1 if schema_object.get("excludeMinimum") else 0
76+
minimum += 1 if schema_object.get("exclusiveMinimum") else 0
8177
if maximum is not None:
82-
maximum -= 1 if schema_object.get("excludeMaximum") else 0
78+
maximum -= 1 if schema_object.get("exclusiveMaximum") else 0
8379
if minimum is not None or maximum is not None:
8480
minimum = minimum or 0
8581
maximum = maximum or minimum * 2

tests/test_utils.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ def test_merge_objects():
2323
test_schemas = [
2424
object_1,
2525
object_2,
26-
{"type": "object", "properties": {"key3": {"allOf": [object_1, object_2]}}},
2726
]
2827
expected = {
2928
"type": "object",
3029
"required": ["key1", "key2"],
31-
"properties": {"key1": {"type": "string"}, "key2": {"type": "string"}, "key3": merged_object},
30+
"properties": {"key1": {"type": "string"}, "key2": {"type": "string"}},
3231
}
3332
assert sort_object(merge_objects(test_schemas)) == sort_object(expected)

0 commit comments

Comments
 (0)