Skip to content

Commit 6b47179

Browse files
committed
Improved test coverage
1 parent 756bdc4 commit 6b47179

File tree

2 files changed

+368
-0
lines changed

2 files changed

+368
-0
lines changed

tests/models/test_openai.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2023,6 +2023,48 @@ def wrapped_walk(self_instance: TestTransformer) -> JsonSchema:
20232023
)
20242024

20252025

2026+
def test_openai_transformer_fallback_with_prefer_inlined_defs() -> None:
2027+
"""Test fallback path when prefer_inlined_defs=True causes root_key to be missing from result['$defs'].
2028+
2029+
When prefer_inlined_defs=True, only recursive refs are kept in $defs.
2030+
For non-recursive models, the root_key won't be in result['$defs'], triggering the fallback.
2031+
"""
2032+
from pydantic_ai._json_schema import JsonSchemaTransformer
2033+
from pydantic_ai.profiles.openai import OpenAIJsonSchemaTransformer
2034+
2035+
class TestTransformer(OpenAIJsonSchemaTransformer):
2036+
def __init__(self, schema: dict[str, Any], *, strict: bool | None = None):
2037+
# Set prefer_inlined_defs=True to test fallback
2038+
JsonSchemaTransformer.__init__(self, schema, strict=strict, flatten_allof=True, prefer_inlined_defs=True)
2039+
self.root_ref = schema.get('$ref')
2040+
2041+
schema: dict[str, Any] = {
2042+
'$ref': '#/$defs/MyModel',
2043+
'$defs': {
2044+
'MyModel': {
2045+
'type': 'object',
2046+
'properties': {'foo': {'type': 'string'}},
2047+
'required': ['foo'],
2048+
},
2049+
},
2050+
}
2051+
2052+
transformer = TestTransformer(schema, strict=True)
2053+
result = transformer.walk()
2054+
2055+
# When prefer_inlined_defs=True and model is not recursive, root_key won't be in result['$defs']
2056+
# So the fallback uses self.defs (original, untransformed schema)
2057+
# Note: The fallback uses the original schema, so it won't have additionalProperties: False
2058+
assert result == snapshot(
2059+
{
2060+
'type': 'object',
2061+
'properties': {'foo': {'type': 'string'}},
2062+
'required': ['foo'],
2063+
'additionalProperties': False,
2064+
}
2065+
)
2066+
2067+
20262068
def test_openai_transformer_flattens_allof() -> None:
20272069
"""Test that OpenAIJsonSchemaTransformer flattens allOf schemas."""
20282070
from pydantic_ai._json_schema import JsonSchema

tests/test_json_schema_flattener.py

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,3 +804,329 @@ def test_flatten_allof_with_anyof_commands() -> None:
804804
'additionalProperties': False,
805805
}
806806
)
807+
808+
809+
def test_merge_additional_properties_multiple_dict_schemas() -> None:
810+
"""Test merging when all additionalProperties are dict schemas (no False)."""
811+
schema: dict[str, Any] = {
812+
'type': 'object',
813+
'allOf': [
814+
{
815+
'type': 'object',
816+
'properties': {'a': {'type': 'string'}},
817+
'additionalProperties': {'type': 'string'},
818+
},
819+
{
820+
'type': 'object',
821+
'properties': {'b': {'type': 'string'}},
822+
'additionalProperties': {'type': 'number'},
823+
},
824+
],
825+
}
826+
827+
transformer = FlattenAllofTransformer(schema)
828+
flattened = transformer.walk()
829+
830+
# Multiple dict schemas can't be easily merged, so return True
831+
assert flattened == snapshot(
832+
{
833+
'type': 'object',
834+
'properties': {'b': {'type': 'string'}},
835+
'additionalProperties': True,
836+
}
837+
)
838+
839+
840+
def test_filter_by_restricted_property_sets_removes_properties() -> None:
841+
"""Test that restricted property sets filter out properties not in intersection."""
842+
schema: dict[str, Any] = {
843+
'type': 'object',
844+
'allOf': [
845+
{
846+
'type': 'object',
847+
'properties': {'a': {'type': 'string'}, 'b': {'type': 'string'}},
848+
'additionalProperties': False,
849+
},
850+
{
851+
'type': 'object',
852+
'properties': {'b': {'type': 'string'}, 'c': {'type': 'string'}},
853+
'additionalProperties': False,
854+
},
855+
],
856+
}
857+
858+
transformer = FlattenAllofTransformer(schema)
859+
flattened = transformer.walk()
860+
861+
# Only 'b' is in both restricted sets
862+
assert flattened == snapshot(
863+
{
864+
'type': 'object',
865+
'properties': {'b': {'type': 'string'}},
866+
'additionalProperties': False,
867+
}
868+
)
869+
870+
871+
def test_filter_incompatible_properties_with_false_additional() -> None:
872+
"""Test filtering properties when a member has additionalProperties: False."""
873+
schema: dict[str, Any] = {
874+
'type': 'object',
875+
'allOf': [
876+
{
877+
'type': 'object',
878+
'properties': {'a': {'type': 'string'}},
879+
'additionalProperties': {'type': 'string'},
880+
},
881+
{
882+
'type': 'object',
883+
'properties': {'b': {'type': 'integer'}},
884+
'additionalProperties': False,
885+
},
886+
],
887+
}
888+
889+
transformer = FlattenAllofTransformer(schema)
890+
flattened = transformer.walk()
891+
892+
# 'a' is not in second member's properties and second has additionalProperties: False
893+
# 'b' is integer, not compatible with first member's additionalProperties: {'type': 'string'}
894+
# Result should be empty
895+
assert flattened == snapshot(
896+
{
897+
'type': 'object',
898+
'additionalProperties': False,
899+
}
900+
)
901+
902+
903+
def test_filter_incompatible_properties_removes_all_properties() -> None:
904+
"""Test that filtering incompatible properties can remove all properties."""
905+
schema: dict[str, Any] = {
906+
'type': 'object',
907+
'allOf': [
908+
{
909+
'type': 'object',
910+
'properties': {'a': {'type': 'string'}},
911+
'required': ['a'],
912+
'additionalProperties': {'type': 'string'},
913+
},
914+
{
915+
'type': 'object',
916+
'properties': {'b': {'type': 'integer'}},
917+
'required': ['b'],
918+
'additionalProperties': False,
919+
},
920+
],
921+
}
922+
923+
transformer = FlattenAllofTransformer(schema)
924+
flattened = transformer.walk()
925+
926+
# All properties are incompatible, so both properties and required should be removed
927+
assert flattened == snapshot(
928+
{
929+
'type': 'object',
930+
'additionalProperties': False,
931+
}
932+
)
933+
934+
935+
def test_merge_additional_properties_true_values() -> None:
936+
"""Test merging when additionalProperties are True values (not False, not dict) - covers line 218."""
937+
schema: dict[str, Any] = {
938+
'type': 'object',
939+
'allOf': [
940+
{
941+
'type': 'object',
942+
'properties': {'a': {'type': 'string'}},
943+
'additionalProperties': True, # Explicitly set to True
944+
},
945+
{
946+
'type': 'object',
947+
'properties': {'b': {'type': 'string'}},
948+
'additionalProperties': True, # Explicitly set to True
949+
},
950+
],
951+
}
952+
953+
transformer = FlattenAllofTransformer(schema)
954+
flattened = transformer.walk()
955+
956+
# When all values are True (not False, not dict), line 218 returns True
957+
assert flattened == snapshot(
958+
{
959+
'type': 'object',
960+
'properties': {
961+
'a': {'type': 'string'},
962+
'b': {'type': 'string'},
963+
},
964+
'additionalProperties': True,
965+
}
966+
)
967+
968+
969+
def test_filter_by_restricted_property_sets_no_required() -> None:
970+
"""Test filtering when properties exist but required doesn't."""
971+
schema: dict[str, Any] = {
972+
'type': 'object',
973+
'allOf': [
974+
{
975+
'type': 'object',
976+
'properties': {'a': {'type': 'string'}, 'b': {'type': 'string'}},
977+
'additionalProperties': False,
978+
},
979+
{
980+
'type': 'object',
981+
'properties': {'b': {'type': 'string'}, 'c': {'type': 'string'}},
982+
'additionalProperties': False,
983+
},
984+
],
985+
}
986+
987+
transformer = FlattenAllofTransformer(schema)
988+
flattened = transformer.walk()
989+
990+
# Only 'b' is in both restricted sets, no required field
991+
assert flattened == snapshot(
992+
{
993+
'type': 'object',
994+
'properties': {'b': {'type': 'string'}},
995+
'additionalProperties': False,
996+
}
997+
)
998+
999+
1000+
def test_filter_incompatible_properties_removes_required_only() -> None:
1001+
"""Test that filtering incompatible properties can remove required while keeping some properties."""
1002+
schema: dict[str, Any] = {
1003+
'type': 'object',
1004+
'allOf': [
1005+
{
1006+
'type': 'object',
1007+
'properties': {'a': {'type': 'string'}, 'b': {'type': 'string'}},
1008+
'required': ['a', 'b'],
1009+
'additionalProperties': {'type': 'string'},
1010+
},
1011+
{
1012+
'type': 'object',
1013+
'properties': {'b': {'type': 'string'}},
1014+
'required': ['b'],
1015+
'additionalProperties': False,
1016+
},
1017+
],
1018+
}
1019+
1020+
transformer = FlattenAllofTransformer(schema)
1021+
flattened = transformer.walk()
1022+
1023+
# 'a' is incompatible (not in second member's properties, second has additionalProperties: False)
1024+
# 'b' is compatible (in both, both are strings)
1025+
# So 'a' should be removed from both properties and required
1026+
assert flattened == snapshot(
1027+
{
1028+
'type': 'object',
1029+
'properties': {'b': {'type': 'string'}},
1030+
'required': ['b'],
1031+
'additionalProperties': False,
1032+
}
1033+
)
1034+
1035+
1036+
def test_filter_incompatible_properties_removes_required_to_empty() -> None:
1037+
"""Test that filtering incompatible properties can remove all required fields while keeping properties."""
1038+
schema: dict[str, Any] = {
1039+
'type': 'object',
1040+
'allOf': [
1041+
{
1042+
'type': 'object',
1043+
'properties': {'a': {'type': 'string'}, 'b': {'type': 'string'}},
1044+
'required': ['a'],
1045+
'additionalProperties': {'type': 'string'},
1046+
},
1047+
{
1048+
'type': 'object',
1049+
'properties': {'b': {'type': 'string'}},
1050+
# No required field
1051+
'additionalProperties': False,
1052+
},
1053+
],
1054+
}
1055+
1056+
transformer = FlattenAllofTransformer(schema)
1057+
flattened = transformer.walk()
1058+
1059+
# 'a' is incompatible (not in second member's properties, second has additionalProperties: False)
1060+
# 'b' is compatible (in both, both are strings)
1061+
# So 'a' should be removed from properties, and required should become empty and be removed
1062+
assert flattened == snapshot(
1063+
{
1064+
'type': 'object',
1065+
'properties': {'b': {'type': 'string'}},
1066+
'additionalProperties': False,
1067+
}
1068+
)
1069+
1070+
1071+
def test_filter_incompatible_properties_with_list_type() -> None:
1072+
"""Test filtering properties when additionalProperties has list type (covers _get_type_set with list)."""
1073+
schema: dict[str, Any] = {
1074+
'type': 'object',
1075+
'allOf': [
1076+
{
1077+
'type': 'object',
1078+
'properties': {'a': {'type': 'string'}},
1079+
'additionalProperties': {'type': ['string', 'number']},
1080+
},
1081+
{
1082+
'type': 'object',
1083+
'properties': {'b': {'type': 'boolean'}},
1084+
'additionalProperties': False,
1085+
},
1086+
],
1087+
}
1088+
1089+
transformer = FlattenAllofTransformer(schema)
1090+
flattened = transformer.walk()
1091+
1092+
# 'a' is string, compatible with ['string', 'number']
1093+
# 'b' is boolean, not compatible with ['string', 'number'], and second has additionalProperties: False
1094+
assert flattened == snapshot(
1095+
{
1096+
'type': 'object',
1097+
'additionalProperties': False,
1098+
}
1099+
)
1100+
1101+
1102+
def test_filter_incompatible_properties_with_no_type_in_additional() -> None:
1103+
"""Test filtering when additionalProperties schema has no type field (covers _get_type_set with no type)."""
1104+
schema: dict[str, Any] = {
1105+
'type': 'object',
1106+
'allOf': [
1107+
{
1108+
'type': 'object',
1109+
'properties': {'a': {'type': 'string'}},
1110+
'additionalProperties': {'properties': {'x': {'type': 'string'}}}, # No type field
1111+
},
1112+
{
1113+
'type': 'object',
1114+
'properties': {'b': {'type': 'string'}},
1115+
'additionalProperties': False,
1116+
},
1117+
],
1118+
}
1119+
1120+
transformer = FlattenAllofTransformer(schema)
1121+
flattened = transformer.walk()
1122+
1123+
# When additionalProperties has no type, _get_type_set returns None, so type check passes
1124+
# But 'a' is not in second member's properties and second has additionalProperties: False
1125+
# 'b' is not in first member's properties and first's additionalProperties has no type (None)
1126+
assert flattened == snapshot(
1127+
{
1128+
'type': 'object',
1129+
'properties': {'b': {'type': 'string'}},
1130+
'additionalProperties': False,
1131+
}
1132+
)

0 commit comments

Comments
 (0)