diff --git a/src/openai/_models.py b/src/openai/_models.py index ed4c1f82d6..77ce1c6956 100644 --- a/src/openai/_models.py +++ b/src/openai/_models.py @@ -657,7 +657,9 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] if not is_mapping(value): return value - _, items_type = get_args(type_) # Dict[_, items_type] + # bare, unparameterised `dict` annotations don't have any type arguments, + # in which case the values are treated as if they were annotated with `Any` + items_type = args[1] if len(args) > 1 else object # Dict[_, items_type] return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} if ( @@ -678,7 +680,8 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] if not is_list(value): return value - inner_type = args[0] # List[inner_type] + # as with `dict` above, a bare `list` annotation has no type arguments + inner_type = args[0] if args else object # List[inner_type] return [construct_type(value=entry, type_=inner_type) for entry in value] if origin == float: diff --git a/src/openai/_utils/_transform.py b/src/openai/_utils/_transform.py index 414f38c340..01ee2a1333 100644 --- a/src/openai/_utils/_transform.py +++ b/src/openai/_utils/_transform.py @@ -23,7 +23,6 @@ from ._typing import ( is_list_type, is_union_type, - extract_type_arg, is_iterable_type, is_required_type, is_sequence_type, @@ -151,6 +150,20 @@ def _no_transform_needed(annotation: type) -> bool: return annotation == float or annotation == int +def _extract_container_arg(typ: type, index: int) -> type: + """Return the type argument at the given index for a container type. + + Bare, unparameterised containers, e.g. `dict` instead of `dict[str, int]`, don't have + any type arguments, in which case the contained values are treated as if they were + annotated with `Any`. + """ + args = get_args(typ) + if index >= len(args): + return cast(type, object) + + return cast(type, args[index]) + + def _transform_recursive( data: object, *, @@ -180,7 +193,7 @@ def _transform_recursive( return _transform_typeddict(data, stripped_type) if origin == dict and is_mapping(data): - items_type = get_args(stripped_type)[1] + items_type = _extract_container_arg(stripped_type, 1) return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} if ( @@ -196,7 +209,7 @@ def _transform_recursive( if isinstance(data, dict): return cast(object, data) - inner_type = extract_type_arg(stripped_type, 0) + inner_type = _extract_container_arg(stripped_type, 0) if _no_transform_needed(inner_type): # for some types there is no need to transform anything, so we can get a small # perf boost from skipping that work. @@ -346,7 +359,7 @@ async def _async_transform_recursive( return await _async_transform_typeddict(data, stripped_type) if origin == dict and is_mapping(data): - items_type = get_args(stripped_type)[1] + items_type = _extract_container_arg(stripped_type, 1) return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} if ( @@ -362,7 +375,7 @@ async def _async_transform_recursive( if isinstance(data, dict): return cast(object, data) - inner_type = extract_type_arg(stripped_type, 0) + inner_type = _extract_container_arg(stripped_type, 0) if _no_transform_needed(inner_type): # for some types there is no need to transform anything, so we can get a small # perf boost from skipping that work. diff --git a/tests/test_models.py b/tests/test_models.py index cc204bac1d..2181391cda 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -359,6 +359,36 @@ class Model(BaseModel): assert cast(Any, m.items[1]) == 156 +def test_bare_dict_annotation() -> None: + # a bare `dict` has no type arguments so the values are left as-is + class Model(BaseModel): + metadata: dict # type: ignore[type-arg] + + m = Model.construct(metadata={"key": "value"}) + assert m.metadata == {"key": "value"} # pyright: ignore[reportUnknownMemberType] + + assert construct_type(value={"key": "value"}, type_=dict) == {"key": "value"} + assert construct_type(value={"key": "value"}, type_=Dict) == {"key": "value"} + + # non-mapping values are still passed through unchanged + assert construct_type(value="not a dict", type_=dict) == "not a dict" + + +def test_bare_list_annotation() -> None: + # a bare `list` has no type arguments so the entries are left as-is + class Model(BaseModel): + items: list # type: ignore[type-arg] + + m = Model.construct(items=[{"key": "value"}, 1]) + assert m.items == [{"key": "value"}, 1] # pyright: ignore[reportUnknownMemberType] + + assert construct_type(value=[1, 2], type_=list) == [1, 2] + assert construct_type(value=[1, 2], type_=List) == [1, 2] + + # non-list values are still passed through unchanged + assert construct_type(value="not a list", type_=list) == "not a list" + + def test_dict_of_union() -> None: class SubModel1(BaseModel): name: str diff --git a/tests/test_transform.py b/tests/test_transform.py index 5af84df7fd..ea1d81cc11 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -397,6 +397,32 @@ class DictItems(TypedDict): assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} +@parametrize +@pytest.mark.asyncio +async def test_bare_dict_annotation(use_async: bool) -> None: + """bare `dict` annotations have no type arguments so the values are left as-is""" + + class BareDict(TypedDict): + metadata: dict # type: ignore[type-arg] + + assert await transform({"metadata": {"key": "value"}}, BareDict, use_async) == {"metadata": {"key": "value"}} + assert await transform({"key": "value"}, dict, use_async) == {"key": "value"} + assert await transform({"key": "value"}, Dict, use_async) == {"key": "value"} + + +@parametrize +@pytest.mark.asyncio +async def test_bare_list_annotation(use_async: bool) -> None: + """bare `list` annotations have no type arguments so the entries are left as-is""" + + class BareList(TypedDict): + items: list # type: ignore[type-arg] + + assert await transform({"items": [{"foo_baz": "bar"}]}, BareList, use_async) == {"items": [{"foo_baz": "bar"}]} + assert await transform([1, 2, 3], list, use_async) == [1, 2, 3] + assert await transform([1, 2, 3], List, use_async) == [1, 2, 3] + + class TypedDictIterableUnionStr(TypedDict): foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")]