diff --git a/pandas/io/excel/_base.py b/pandas/io/excel/_base.py index 44254f54cbc7a..d4570498e3fc6 100644 --- a/pandas/io/excel/_base.py +++ b/pandas/io/excel/_base.py @@ -892,6 +892,12 @@ def sheet_names(self): def close(self): """close io if necessary""" + if self.engine == "openpyxl": + # https://stackoverflow.com/questions/31416842/ + # openpyxl-does-not-close-excel-workbook-in-read-only-mode + wb = self.book + wb._archive.close() + if hasattr(self.io, "close"): self.io.close() @@ -900,3 +906,7 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): self.close() + + def __del__(self): + # Ensure we don't leak file descriptors + self.close() diff --git a/pandas/tests/io/excel/test_readers.py b/pandas/tests/io/excel/test_readers.py index e4b7d683b4c3b..480407664285f 100644 --- a/pandas/tests/io/excel/test_readers.py +++ b/pandas/tests/io/excel/test_readers.py @@ -543,6 +543,7 @@ def test_read_from_pathlib_path(self, read_ext): tm.assert_frame_equal(expected, actual) @td.skip_if_no("py.path") + @td.check_file_leaks def test_read_from_py_localpath(self, read_ext): # GH12655 @@ -881,6 +882,7 @@ def test_excel_passes_na_filter(self, read_ext, na_filter): tm.assert_frame_equal(parsed, expected) @pytest.mark.parametrize("arg", ["sheet", "sheetname", "parse_cols"]) + @td.check_file_leaks def test_unexpected_kwargs_raises(self, read_ext, arg): # gh-17964 kwarg = {arg: "Sheet1"} diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py index b1be0a1a2fece..e0cb75b0a6c99 100644 --- a/pandas/tests/io/excel/test_writers.py +++ b/pandas/tests/io/excel/test_writers.py @@ -809,6 +809,7 @@ def test_to_excel_unicode_filename(self, ext, path): ) tm.assert_frame_equal(result, expected) + # FIXME: dont leave commented-out # def test_to_excel_header_styling_xls(self, engine, ext): # import StringIO diff --git a/pandas/util/_test_decorators.py b/pandas/util/_test_decorators.py index b9c165140aaad..7669e729995d0 100644 --- a/pandas/util/_test_decorators.py +++ b/pandas/util/_test_decorators.py @@ -24,6 +24,7 @@ def test_foo(): For more information, refer to the ``pytest`` documentation on ``skipif``. """ from distutils.version import LooseVersion +from functools import wraps import locale from typing import Callable, Optional @@ -230,3 +231,24 @@ def documented_fixture(fixture): return fixture return documented_fixture + + +def check_file_leaks(func): + """ + Decorate a test function tot check that we are not leaking file descriptors. + """ + psutil = safe_import("psutil") + if not psutil: + return func + + @wraps(func) + def new_func(*args, **kwargs): + proc = psutil.Process() + flist = proc.open_files() + + func(*args, **kwargs) + + flist2 = proc.open_files() + assert flist2 == flist + + return new_func