Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion haystack/components/converters/file_to_file_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,12 @@ def run(
continue

base64_data = base64.b64encode(bytestream.data).decode("utf-8")
# ``normalize_metadata`` returns the same dict object for every source when ``extra`` is a
# single dict (or ``None``), so give each FileContent its own copy. Otherwise mutating one
# file's ``extra`` downstream would leak into all the others. The other converters avoid this
# implicitly by merging ``extra`` into a fresh ``{**bytestream.meta, ...}`` dict.
file_content = FileContent(
base64_data=base64_data, mime_type=bytestream.mime_type, filename=filename, extra=extra_dict
base64_data=base64_data, mime_type=bytestream.mime_type, filename=filename, extra=dict(extra_dict)
)
file_contents.append(file_content)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
fixes:
- |
Fixed ``FileToFileContent`` sharing a single ``extra`` dictionary across all produced ``FileContent``
objects when ``extra`` is passed as one dict (or left as ``None``). Each ``FileContent`` now gets its
own copy, so mutating one file's ``extra`` no longer leaks into the others.
14 changes: 14 additions & 0 deletions test/components/converters/test_file_to_file_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,20 @@ def test_run_with_extra_list(self) -> None:
assert results["file_contents"][0].extra == {"key": "value1"}
assert results["file_contents"][1].extra == {"key": "value2"}

def test_run_with_extra_dict_does_not_share_reference(self) -> None:
# A single ``extra`` dict is applied to every source; each FileContent must get its own copy
# so that mutating one file's ``extra`` downstream does not leak into the others.
converter = FileToFileContent()
sources = ["./test/test_files/txt/doc_1.txt", "./test/test_files/txt/doc_2.txt"]
results = converter.run(sources=sources, extra={"tenant": "acme"})

file_contents = results["file_contents"]
assert len(file_contents) == 2
assert file_contents[0].extra is not file_contents[1].extra

file_contents[0].extra["page"] = 1
assert "page" not in file_contents[1].extra

def test_run_skips_empty_files_among_valid(self, caplog) -> None:
byte_stream_empty = ByteStream(data=b"")
valid_source = "./test/test_files/txt/doc_1.txt"
Expand Down
Loading