|
| 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