diff --git a/src/openai/_files.py b/src/openai/_files.py index 1a2cc77478..76f71238ff 100644 --- a/src/openai/_files.py +++ b/src/openai/_files.py @@ -27,13 +27,11 @@ def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: def is_file_content(obj: object) -> TypeGuard[FileContent]: - return ( - isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) - ) + return isinstance(obj, bytes) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) def assert_is_file_content(obj: object, *, key: str | None = None) -> None: - if not is_file_content(obj): + if not is_file_content(obj) and not is_tuple_t(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/openai/openai-python/tree/main#file-uploads" diff --git a/tests/test_files.py b/tests/test_files.py index 56445fb550..180643e312 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -22,6 +22,18 @@ def test_tuple_input() -> None: assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) +def test_file_tuple_with_pathlike_content() -> None: + result = to_httpx_files({"file": ("custom-name.md", readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("custom-name.md", IsBytes())}) + + +def test_file_tuple_with_pathlike_content_and_metadata() -> None: + result = to_httpx_files({"file": ("custom-name.md", readme_path, "text/markdown")}) + print(result) + assert result == IsDict({"file": IsTuple("custom-name.md", IsBytes(), "text/markdown")}) + + @pytest.mark.asyncio async def test_async_pathlib_includes_file_name() -> None: result = await async_to_httpx_files({"file": readme_path}) @@ -43,6 +55,20 @@ async def test_async_tuple_input() -> None: assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) +@pytest.mark.asyncio +async def test_async_file_tuple_with_pathlike_content() -> None: + result = await async_to_httpx_files({"file": ("custom-name.md", readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("custom-name.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_file_tuple_with_pathlike_content_and_metadata() -> None: + result = await async_to_httpx_files({"file": ("custom-name.md", readme_path, "text/markdown")}) + print(result) + assert result == IsDict({"file": IsTuple("custom-name.md", IsBytes(), "text/markdown")}) + + def test_string_not_allowed() -> None: with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): to_httpx_files( @@ -117,6 +143,17 @@ def test_extract_files_does_not_mutate_original_top_level(self) -> None: assert original == {"file": file_bytes, "other": "value"} assert copied == {"other": "value"} + def test_extract_files_accepts_file_tuple(self) -> None: + file_tuple = ("custom-name.jsonl", b"contents", "application/jsonl") + original = {"file": file_tuple, "purpose": "batch"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_tuple)] + assert original == {"file": file_tuple, "purpose": "batch"} + assert copied == {"purpose": "batch"} + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: file1 = b"f1" file2 = b"f2"