Skip to content

Commit 16836e9

Browse files
committed
Add tests for parsing ambiguous ref unions
1 parent 7bd7c69 commit 16836e9

File tree

1 file changed

+76
-0
lines changed

1 file changed

+76
-0
lines changed

tests/test_schema/test_noisy_refs.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# If a field may be reference (`Union[Reference, OtherType]`) and the dictionary
2+
# being processed for it contains "$ref", it seems like it should preferentially
3+
# be parsed as a `Reference`[1]. Since the models are defined with
4+
# `extra="allow"`, Pydantic won't guarantee this parse if the dictionary is in
5+
# an unspecified sense a "better match" for `OtherType`[2], e.g., perhaps if it
6+
# has several more fields matching that type versus the single match for `$ref`.
7+
#
8+
# We can use a discriminated union to force parsing these dictionaries as
9+
# `Reference`s.
10+
#
11+
# References:
12+
# [1] https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object
13+
# [2] https://docs.pydantic.dev/latest/concepts/unions/#smart-mode
14+
import json
15+
from typing import Annotated, TypeVar, Union, get_args, get_origin
16+
17+
import pytest
18+
from pydantic import TypeAdapter
19+
20+
from openapi_python_client.schema.openapi_schema_pydantic import Callback, Example, Header, Link, Parameter, PathItem, Reference, RequestBody, Response, Schema, SecurityScheme
21+
22+
23+
try:
24+
from openapi_python_client.schema.openapi_schema_pydantic.reference import ReferenceOr
25+
except ImportError:
26+
T = TypeVar("T")
27+
ReferenceOr = Union[Reference, T]
28+
29+
30+
def get_example(base_type):
31+
schema = base_type.model_json_schema()
32+
print(json.dumps(schema.get("examples", []), indent=4))
33+
if "examples" in schema:
34+
return schema["examples"][0]
35+
if "$defs" in schema:
36+
return schema["$defs"][base_type.__name__]["examples"][0]
37+
assert False, f"No example found for {base_type.__name__}"
38+
39+
def deannotate_type(t):
40+
while get_origin(t) == Annotated:
41+
t = get_args(t)[0]
42+
return t
43+
44+
45+
# The following types occur in various models, so we want to make sure they
46+
# parse properly. They are verified to /fail/ to parse as of commit 3bd12f86.
47+
48+
@pytest.mark.parametrize(("ref_or_type", "get_example_fn"), [
49+
(ReferenceOr[Callback], lambda t: {"test1": get_example(PathItem),
50+
"test2": get_example(PathItem)}),
51+
(ReferenceOr[Example], get_example),
52+
(ReferenceOr[Header], get_example),
53+
(ReferenceOr[Link], get_example),
54+
(ReferenceOr[Parameter], get_example),
55+
(ReferenceOr[RequestBody], get_example),
56+
(ReferenceOr[Response], get_example),
57+
(ReferenceOr[Schema], get_example),
58+
(ReferenceOr[SecurityScheme], get_example),
59+
])
60+
def test_type(ref_or_type, get_example_fn):
61+
base_type = None
62+
print(deannotate_type(ref_or_type))
63+
for base_type in get_args(deannotate_type(ref_or_type)):
64+
base_type = deannotate_type(base_type)
65+
if base_type != Reference:
66+
break
67+
assert base_type is not None
68+
69+
example = get_example_fn(base_type)
70+
71+
parsed = TypeAdapter(ref_or_type).validate_python(example)
72+
assert type(parsed) == get_origin(base_type) or base_type
73+
74+
example["$ref"] = "ref"
75+
parsed = TypeAdapter(ref_or_type).validate_python(example)
76+
assert type(parsed) == Reference

0 commit comments

Comments
 (0)