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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ All notable changes to Fair Code are documented here, newest first.

## [Unreleased]

Nothing yet. Changes made after 2.3.0 are listed here until the next release.
### Fixed

- `get_xlsx_sheet_info` now closes the workbook it opens to list sheet names, rather than leaking the file handle. The unclosed handle raised a `ResourceWarning` on garbage collection and, on Windows, kept the `.xlsx` locked against deletion or overwrite ([#1051](https://github.com/yakew7/Fair-Code/issues/1051)).

## [2.3.0] - 17 Oct 2026

Expand Down
7 changes: 4 additions & 3 deletions faircode/loaders_extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,13 @@ def get_xlsx_sheet_info(path: str) -> tuple[str, list[str]] | None:
if Path(path).suffix.lower() != ".xlsx":
return None
try:
book = pd.ExcelFile(path)
with pd.ExcelFile(path) as book:
sheet_names = book.sheet_names
except Exception:
return None
if not book.sheet_names:
if not sheet_names:
return None
return book.sheet_names[0], book.sheet_names[1:]
return sheet_names[0], sheet_names[1:]


# Binary table formats that never decode as text, so an explicit text encoding
Expand Down
26 changes: 26 additions & 0 deletions tests/test_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
Run from the repo root: pytest tests/ -q
"""

import gc
import importlib.util
import json
import warnings

import pandas as pd
import pytest
Expand Down Expand Up @@ -204,6 +206,30 @@ def test_get_xlsx_sheet_info_returns_none_for_a_corrupt_xlsx_file(tmp_path):
assert get_xlsx_sheet_info(str(path)) is None


@requires_openpyxl
def test_get_xlsx_sheet_info_closes_the_workbook(tmp_path):
# Listing sheet names must not leak the workbook's file handle: an unclosed
# handle raises a ResourceWarning on garbage collection and, on Windows,
# keeps the file locked against deletion or overwrite (#1051).
path = tmp_path / "multi.xlsx"
with pd.ExcelWriter(path) as writer:
pd.DataFrame(ROWS).to_excel(writer, sheet_name="First", index=False)
pd.DataFrame(ROWS).to_excel(writer, sheet_name="Second", index=False)

with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
info = get_xlsx_sheet_info(str(path))
gc.collect()

assert info == ("First", ["Second"])
unclosed = [
w
for w in caught
if issubclass(w.category, ResourceWarning) and "unclosed" in str(w.message)
]
assert not unclosed, f"workbook left open: {[str(w.message) for w in unclosed]}"


def test_read_table_unknown_extension_sniffs_tabs(tmp_path):
path = tmp_path / "data.txt"
_write_csv(path, sep="\t")
Expand Down
Loading