Skip to content

Commit 4d82e5d

Browse files
jbrockmendelproost
authored andcommitted
BUG: file leak in read_excel (pandas-dev#30096)
1 parent 91aad05 commit 4d82e5d

File tree

4 files changed

+35
-0
lines changed

4 files changed

+35
-0
lines changed

pandas/io/excel/_base.py

+10
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,12 @@ def sheet_names(self):
892892

893893
def close(self):
894894
"""close io if necessary"""
895+
if self.engine == "openpyxl":
896+
# https://stackoverflow.com/questions/31416842/
897+
# openpyxl-does-not-close-excel-workbook-in-read-only-mode
898+
wb = self.book
899+
wb._archive.close()
900+
895901
if hasattr(self.io, "close"):
896902
self.io.close()
897903

@@ -900,3 +906,7 @@ def __enter__(self):
900906

901907
def __exit__(self, exc_type, exc_value, traceback):
902908
self.close()
909+
910+
def __del__(self):
911+
# Ensure we don't leak file descriptors
912+
self.close()

pandas/tests/io/excel/test_readers.py

+2
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ def test_read_from_pathlib_path(self, read_ext):
543543
tm.assert_frame_equal(expected, actual)
544544

545545
@td.skip_if_no("py.path")
546+
@td.check_file_leaks
546547
def test_read_from_py_localpath(self, read_ext):
547548

548549
# GH12655
@@ -881,6 +882,7 @@ def test_excel_passes_na_filter(self, read_ext, na_filter):
881882
tm.assert_frame_equal(parsed, expected)
882883

883884
@pytest.mark.parametrize("arg", ["sheet", "sheetname", "parse_cols"])
885+
@td.check_file_leaks
884886
def test_unexpected_kwargs_raises(self, read_ext, arg):
885887
# gh-17964
886888
kwarg = {arg: "Sheet1"}

pandas/tests/io/excel/test_writers.py

+1
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,7 @@ def test_to_excel_unicode_filename(self, ext, path):
809809
)
810810
tm.assert_frame_equal(result, expected)
811811

812+
# FIXME: dont leave commented-out
812813
# def test_to_excel_header_styling_xls(self, engine, ext):
813814

814815
# import StringIO

pandas/util/_test_decorators.py

+22
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def test_foo():
2424
For more information, refer to the ``pytest`` documentation on ``skipif``.
2525
"""
2626
from distutils.version import LooseVersion
27+
from functools import wraps
2728
import locale
2829
from typing import Callable, Optional
2930

@@ -230,3 +231,24 @@ def documented_fixture(fixture):
230231
return fixture
231232

232233
return documented_fixture
234+
235+
236+
def check_file_leaks(func):
237+
"""
238+
Decorate a test function tot check that we are not leaking file descriptors.
239+
"""
240+
psutil = safe_import("psutil")
241+
if not psutil:
242+
return func
243+
244+
@wraps(func)
245+
def new_func(*args, **kwargs):
246+
proc = psutil.Process()
247+
flist = proc.open_files()
248+
249+
func(*args, **kwargs)
250+
251+
flist2 = proc.open_files()
252+
assert flist2 == flist
253+
254+
return new_func

0 commit comments

Comments
 (0)