From 0bc1c96ba5f4800fcaf29818801aed75636a5a57 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 9 Dec 2019 10:27:56 -0800 Subject: [PATCH 1/9] Make fixture to check for file leaks --- ci/deps/azure-36-minimum_versions.yaml | 1 + pandas/conftest.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/ci/deps/azure-36-minimum_versions.yaml b/ci/deps/azure-36-minimum_versions.yaml index ff1095005fa85..8331c8ab23891 100644 --- a/ci/deps/azure-36-minimum_versions.yaml +++ b/ci/deps/azure-36-minimum_versions.yaml @@ -11,6 +11,7 @@ dependencies: - pytest-xdist>=1.21 - hypothesis>=3.58.0 - pytest-azurepipelines + - psutil # pandas dependencies - beautifulsoup4=4.6.0 diff --git a/pandas/conftest.py b/pandas/conftest.py index 3553a411a27f8..6de5061e5d9c5 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -881,3 +881,19 @@ def index_or_series(request): See GH#29725 """ return request.param + + +@pytest.fixture(autouse=True) +def check_for_file_leaks(): + """ + Fixture to run around every test to ensure that we are not leaing files. + """ + psutil = td.safe_import("psutil") + if psutil is None: + return + + proc = psutil.Process() + flist = proc.open_files() + yield + flist2 = proc.open_files() + assert flist == flist2 From c9fb6ec48c10bea270055d28acdf734fa13561b5 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 9 Dec 2019 10:52:37 -0800 Subject: [PATCH 2/9] docstring --- pandas/conftest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 6de5061e5d9c5..1772e1d29ab9a 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -886,7 +886,11 @@ def index_or_series(request): @pytest.fixture(autouse=True) def check_for_file_leaks(): """ - Fixture to run around every test to ensure that we are not leaing files. + Fixture to run around every test to ensure that we are not leaking files. + + See also + -------- + _test_decorators.check_file_leaks """ psutil = td.safe_import("psutil") if psutil is None: From a20b31f8c385c6770f85efe4e8be5cf540d9806c Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 9 Dec 2019 12:25:20 -0800 Subject: [PATCH 3/9] checks on all tests in test_readers --- pandas/conftest.py | 20 ----------- pandas/tests/io/excel/test_readers.py | 51 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 1772e1d29ab9a..3553a411a27f8 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -881,23 +881,3 @@ def index_or_series(request): See GH#29725 """ return request.param - - -@pytest.fixture(autouse=True) -def check_for_file_leaks(): - """ - Fixture to run around every test to ensure that we are not leaking files. - - See also - -------- - _test_decorators.check_file_leaks - """ - psutil = td.safe_import("psutil") - if psutil is None: - return - - proc = psutil.Process() - flist = proc.open_files() - yield - flist2 = proc.open_files() - assert flist == flist2 diff --git a/pandas/tests/io/excel/test_readers.py b/pandas/tests/io/excel/test_readers.py index 480407664285f..62bc8371ef9aa 100644 --- a/pandas/tests/io/excel/test_readers.py +++ b/pandas/tests/io/excel/test_readers.py @@ -84,6 +84,7 @@ def cd_and_set_engine(self, engine, datapath, monkeypatch, read_ext): monkeypatch.chdir(datapath("io", "data", "excel")) monkeypatch.setattr(pd, "read_excel", func) + @td.check_file_leaks def test_usecols_int(self, read_ext, df_ref): df_ref = df_ref.reindex(columns=["A", "B", "C"]) @@ -100,6 +101,7 @@ def test_usecols_int(self, read_ext, df_ref): "test1" + read_ext, "Sheet2", skiprows=[1], index_col=0, usecols=3 ) + @td.check_file_leaks def test_usecols_list(self, read_ext, df_ref): df_ref = df_ref.reindex(columns=["B", "C"]) @@ -114,6 +116,7 @@ def test_usecols_list(self, read_ext, df_ref): tm.assert_frame_equal(df1, df_ref, check_names=False) tm.assert_frame_equal(df2, df_ref, check_names=False) + @td.check_file_leaks def test_usecols_str(self, read_ext, df_ref): df1 = df_ref.reindex(columns=["A", "B", "C"]) @@ -143,6 +146,7 @@ def test_usecols_str(self, read_ext, df_ref): tm.assert_frame_equal(df2, df1, check_names=False) tm.assert_frame_equal(df3, df1, check_names=False) + @td.check_file_leaks @pytest.mark.parametrize( "usecols", [[0, 1, 3], [0, 3, 1], [1, 0, 3], [1, 3, 0], [3, 0, 1], [3, 1, 0]] ) @@ -153,6 +157,7 @@ def test_usecols_diff_positional_int_columns_order(self, read_ext, usecols, df_r ) tm.assert_frame_equal(result, expected, check_names=False) + @td.check_file_leaks @pytest.mark.parametrize("usecols", [["B", "D"], ["D", "B"]]) def test_usecols_diff_positional_str_columns_order(self, read_ext, usecols, df_ref): expected = df_ref[["B", "D"]] @@ -161,11 +166,13 @@ def test_usecols_diff_positional_str_columns_order(self, read_ext, usecols, df_r result = pd.read_excel("test1" + read_ext, "Sheet1", usecols=usecols) tm.assert_frame_equal(result, expected, check_names=False) + @td.check_file_leaks def test_read_excel_without_slicing(self, read_ext, df_ref): expected = df_ref result = pd.read_excel("test1" + read_ext, "Sheet1", index_col=0) tm.assert_frame_equal(result, expected, check_names=False) + @td.check_file_leaks def test_usecols_excel_range_str(self, read_ext, df_ref): expected = df_ref[["C", "D"]] result = pd.read_excel( @@ -173,12 +180,14 @@ def test_usecols_excel_range_str(self, read_ext, df_ref): ) tm.assert_frame_equal(result, expected, check_names=False) + @td.check_file_leaks def test_usecols_excel_range_str_invalid(self, read_ext): msg = "Invalid column name: E1" with pytest.raises(ValueError, match=msg): pd.read_excel("test1" + read_ext, "Sheet1", usecols="D:E1") + @td.check_file_leaks def test_index_col_label_error(self, read_ext): msg = "list indices must be integers.*, not str" @@ -187,6 +196,7 @@ def test_index_col_label_error(self, read_ext): "test1" + read_ext, "Sheet1", index_col=["A"], usecols=["A", "C"] ) + @td.check_file_leaks def test_index_col_empty(self, read_ext): # see gh-9208 result = pd.read_excel("test1" + read_ext, "Sheet3", index_col=["A", "B", "C"]) @@ -196,6 +206,7 @@ def test_index_col_empty(self, read_ext): ) tm.assert_frame_equal(result, expected) + @td.check_file_leaks @pytest.mark.parametrize("index_col", [None, 2]) def test_index_col_with_unnamed(self, read_ext, index_col): # see gh-18792 @@ -208,6 +219,7 @@ def test_index_col_with_unnamed(self, read_ext, index_col): tm.assert_frame_equal(result, expected) + @td.check_file_leaks def test_usecols_pass_non_existent_column(self, read_ext): msg = ( "Usecols do not match columns, " @@ -217,6 +229,7 @@ def test_usecols_pass_non_existent_column(self, read_ext): with pytest.raises(ValueError, match=msg): pd.read_excel("test1" + read_ext, usecols=["E"]) + @td.check_file_leaks def test_usecols_wrong_type(self, read_ext): msg = ( "'usecols' must either be list-like of " @@ -226,18 +239,21 @@ def test_usecols_wrong_type(self, read_ext): with pytest.raises(ValueError, match=msg): pd.read_excel("test1" + read_ext, usecols=["E1", 0]) + @td.check_file_leaks def test_excel_stop_iterator(self, read_ext): parsed = pd.read_excel("test2" + read_ext, "Sheet1") expected = DataFrame([["aaaa", "bbbbb"]], columns=["Test", "Test1"]) tm.assert_frame_equal(parsed, expected) + @td.check_file_leaks def test_excel_cell_error_na(self, read_ext): parsed = pd.read_excel("test3" + read_ext, "Sheet1") expected = DataFrame([[np.nan]], columns=["Test"]) tm.assert_frame_equal(parsed, expected) + @td.check_file_leaks def test_excel_table(self, read_ext, df_ref): df1 = pd.read_excel("test1" + read_ext, "Sheet1", index_col=0) @@ -249,6 +265,7 @@ def test_excel_table(self, read_ext, df_ref): df3 = pd.read_excel("test1" + read_ext, "Sheet1", index_col=0, skipfooter=1) tm.assert_frame_equal(df3, df1.iloc[:-1]) + @td.check_file_leaks def test_reader_special_dtypes(self, read_ext): expected = DataFrame.from_dict( @@ -310,6 +327,7 @@ def test_reader_special_dtypes(self, read_ext): tm.assert_frame_equal(actual, no_convert_float) # GH8212 - support for converters and missing values + @td.check_file_leaks def test_reader_converters(self, read_ext): basename = "test_converters" @@ -337,6 +355,7 @@ def test_reader_converters(self, read_ext): actual = pd.read_excel(basename + read_ext, "Sheet1", converters=converters) tm.assert_frame_equal(actual, expected) + @td.check_file_leaks def test_reader_dtype(self, read_ext): # GH 8212 basename = "testdtype" @@ -365,6 +384,7 @@ def test_reader_dtype(self, read_ext): with pytest.raises(ValueError): pd.read_excel(basename + read_ext, dtype={"d": "int64"}) + @td.check_file_leaks @pytest.mark.parametrize( "dtype,expected", [ @@ -399,6 +419,7 @@ def test_reader_dtype_str(self, read_ext, dtype, expected): actual = pd.read_excel(basename + read_ext, dtype=dtype) tm.assert_frame_equal(actual, expected) + @td.check_file_leaks def test_reading_all_sheets(self, read_ext): # Test reading all sheetnames by setting sheetname to None, # Ensure a dict is returned. @@ -412,6 +433,7 @@ def test_reading_all_sheets(self, read_ext): # Ensure sheet order is preserved assert expected_keys == list(dfs.keys()) + @td.check_file_leaks def test_reading_multiple_specific_sheets(self, read_ext): # Test reading specific sheetnames by specifying a mixed list # of integers and strings, and confirm that duplicated sheet @@ -426,6 +448,7 @@ def test_reading_multiple_specific_sheets(self, read_ext): tm.assert_contains_all(expected_keys, dfs.keys()) assert len(expected_keys) == len(dfs.keys()) + @td.check_file_leaks def test_reading_all_sheets_with_blank(self, read_ext): # Test reading all sheetnames by setting sheetname to None, # In the case where some sheets are blank. @@ -436,15 +459,18 @@ def test_reading_all_sheets_with_blank(self, read_ext): tm.assert_contains_all(expected_keys, dfs.keys()) # GH6403 + @td.check_file_leaks def test_read_excel_blank(self, read_ext): actual = pd.read_excel("blank" + read_ext, "Sheet1") tm.assert_frame_equal(actual, DataFrame()) + @td.check_file_leaks def test_read_excel_blank_with_header(self, read_ext): expected = DataFrame(columns=["col_1", "col_2"]) actual = pd.read_excel("blank_with_header" + read_ext, "Sheet1") tm.assert_frame_equal(actual, expected) + @td.check_file_leaks def test_date_conversion_overflow(self, read_ext): # GH 10001 : pandas.ExcelFile ignore parse_dates=False expected = pd.DataFrame( @@ -462,6 +488,7 @@ def test_date_conversion_overflow(self, read_ext): result = pd.read_excel("testdateoverflow" + read_ext) tm.assert_frame_equal(result, expected) + @td.check_file_leaks def test_sheet_name(self, read_ext, df_ref): filename = "test1" sheet_name = "Sheet1" @@ -475,6 +502,7 @@ def test_sheet_name(self, read_ext, df_ref): tm.assert_frame_equal(df1, df_ref, check_names=False) tm.assert_frame_equal(df2, df_ref, check_names=False) + @td.check_file_leaks def test_excel_read_buffer(self, read_ext): pth = "test1" + read_ext @@ -483,12 +511,14 @@ def test_excel_read_buffer(self, read_ext): actual = pd.read_excel(f, "Sheet1", index_col=0) tm.assert_frame_equal(expected, actual) + @td.check_file_leaks def test_bad_engine_raises(self, read_ext): bad_engine = "foo" with pytest.raises(ValueError, match="Unknown engine: foo"): pd.read_excel("", engine=bad_engine) @tm.network + @td.check_file_leaks def test_read_from_http_url(self, read_ext): url = ( @@ -499,6 +529,7 @@ def test_read_from_http_url(self, read_ext): local_table = pd.read_excel("test1" + read_ext) tm.assert_frame_equal(url_table, local_table) + @td.check_file_leaks @td.skip_if_not_us_locale def test_read_from_s3_url(self, read_ext, s3_resource): # Bucket "pandas-test" created in tests/io/conftest.py @@ -513,6 +544,7 @@ def test_read_from_s3_url(self, read_ext, s3_resource): @pytest.mark.slow # ignore warning from old xlrd @pytest.mark.filterwarnings("ignore:This metho:PendingDeprecationWarning") + @td.check_file_leaks def test_read_from_file_url(self, read_ext, datapath): # FILE @@ -529,6 +561,7 @@ def test_read_from_file_url(self, read_ext, datapath): tm.assert_frame_equal(url_table, local_table) + @td.check_file_leaks def test_read_from_pathlib_path(self, read_ext): # GH12655 @@ -557,6 +590,7 @@ def test_read_from_py_localpath(self, read_ext): tm.assert_frame_equal(expected, actual) + @td.check_file_leaks def test_reader_seconds(self, read_ext): # Test reading times with and without milliseconds. GH5945. @@ -584,6 +618,7 @@ def test_reader_seconds(self, read_ext): actual = pd.read_excel("times_1904" + read_ext, "Sheet1") tm.assert_frame_equal(actual, expected) + @td.check_file_leaks def test_read_excel_multiindex(self, read_ext): # see gh-4679 mi = MultiIndex.from_product([["foo", "bar"], ["a", "b"]]) @@ -649,6 +684,7 @@ def test_read_excel_multiindex(self, read_ext): ) tm.assert_frame_equal(actual, expected) + @td.check_file_leaks def test_read_excel_multiindex_header_only(self, read_ext): # see gh-11733. # @@ -660,6 +696,7 @@ def test_read_excel_multiindex_header_only(self, read_ext): expected = DataFrame([[1, 2, 3, 4]] * 2, columns=exp_columns) tm.assert_frame_equal(result, expected) + @td.check_file_leaks def test_excel_old_index_format(self, read_ext): # see gh-4679 filename = "test_index_name_pre17" + read_ext @@ -732,17 +769,20 @@ def test_excel_old_index_format(self, read_ext): actual = pd.read_excel(filename, "multi_no_names", index_col=[0, 1]) tm.assert_frame_equal(actual, expected, check_names=False) + @td.check_file_leaks def test_read_excel_bool_header_arg(self, read_ext): # GH 6114 for arg in [True, False]: with pytest.raises(TypeError): pd.read_excel("test1" + read_ext, header=arg) + @td.check_file_leaks def test_read_excel_chunksize(self, read_ext): # GH 8011 with pytest.raises(NotImplementedError): pd.read_excel("test1" + read_ext, chunksize=100) + @td.check_file_leaks def test_read_excel_skiprows_list(self, read_ext): # GH 4903 actual = pd.read_excel( @@ -764,6 +804,7 @@ def test_read_excel_skiprows_list(self, read_ext): ) tm.assert_frame_equal(actual, expected) + @td.check_file_leaks def test_read_excel_nrows(self, read_ext): # GH 16645 num_rows_to_pull = 5 @@ -772,6 +813,7 @@ def test_read_excel_nrows(self, read_ext): expected = expected[:num_rows_to_pull] tm.assert_frame_equal(actual, expected) + @td.check_file_leaks def test_read_excel_nrows_greater_than_nrows_in_file(self, read_ext): # GH 16645 expected = pd.read_excel("test1" + read_ext) @@ -780,12 +822,14 @@ def test_read_excel_nrows_greater_than_nrows_in_file(self, read_ext): actual = pd.read_excel("test1" + read_ext, nrows=num_rows_to_pull) tm.assert_frame_equal(actual, expected) + @td.check_file_leaks def test_read_excel_nrows_non_integer_parameter(self, read_ext): # GH 16645 msg = "'nrows' must be an integer >=0" with pytest.raises(ValueError, match=msg): pd.read_excel("test1" + read_ext, nrows="5") + @td.check_file_leaks def test_read_excel_squeeze(self, read_ext): # GH 12157 f = "test_squeeze" + read_ext @@ -821,6 +865,7 @@ def cd_and_set_engine(self, engine, datapath, monkeypatch, read_ext): monkeypatch.chdir(datapath("io", "data", "excel")) monkeypatch.setattr(pd, "ExcelFile", func) + @td.check_file_leaks def test_excel_passes_na(self, read_ext): with pd.ExcelFile("test4" + read_ext) as excel: @@ -860,6 +905,7 @@ def test_excel_passes_na(self, read_ext): ) tm.assert_frame_equal(parsed, expected) + @td.check_file_leaks @pytest.mark.parametrize("na_filter", [None, True, False]) def test_excel_passes_na_filter(self, read_ext, na_filter): # gh-25453 @@ -892,6 +938,7 @@ def test_unexpected_kwargs_raises(self, read_ext, arg): with pytest.raises(TypeError, match=msg): pd.read_excel(excel, **kwarg) + @td.check_file_leaks def test_excel_table_sheet_by_index(self, read_ext, df_ref): with pd.ExcelFile("test1" + read_ext) as excel: @@ -915,6 +962,7 @@ def test_excel_table_sheet_by_index(self, read_ext, df_ref): tm.assert_frame_equal(df3, df1.iloc[:-1]) + @td.check_file_leaks def test_sheet_name(self, read_ext, df_ref): filename = "test1" sheet_name = "Sheet1" @@ -928,6 +976,7 @@ def test_sheet_name(self, read_ext, df_ref): tm.assert_frame_equal(df1_parse, df_ref, check_names=False) tm.assert_frame_equal(df2_parse, df_ref, check_names=False) + @td.check_file_leaks def test_excel_read_buffer(self, engine, read_ext): pth = "test1" + read_ext expected = pd.read_excel(pth, "Sheet1", index_col=0, engine=engine) @@ -938,6 +987,7 @@ def test_excel_read_buffer(self, engine, read_ext): tm.assert_frame_equal(expected, actual) + @td.check_file_leaks def test_reader_closes_file(self, engine, read_ext): f = open("test1" + read_ext, "rb") with pd.ExcelFile(f) as xlsx: @@ -946,6 +996,7 @@ def test_reader_closes_file(self, engine, read_ext): assert f.closed + @td.check_file_leaks def test_conflicting_excel_engines(self, read_ext): # GH 26566 msg = "Engine should not be specified when passing an ExcelFile" From 3c6366e044734c37a6288214e3d4f984f62c3fd5 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 9 Dec 2019 13:32:23 -0800 Subject: [PATCH 4/9] BUG: close oepn files --- pandas/io/excel/_base.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pandas/io/excel/_base.py b/pandas/io/excel/_base.py index d4570498e3fc6..7a9d4926b1a46 100644 --- a/pandas/io/excel/_base.py +++ b/pandas/io/excel/_base.py @@ -896,7 +896,7 @@ def close(self): # https://stackoverflow.com/questions/31416842/ # openpyxl-does-not-close-excel-workbook-in-read-only-mode wb = self.book - wb._archive.close() + wb.close() if hasattr(self.io, "close"): self.io.close() @@ -910,3 +910,13 @@ def __exit__(self, exc_type, exc_value, traceback): def __del__(self): # Ensure we don't leak file descriptors self.close() + if self.engine == "openpyxl": + # openpyxl 2.5.5 and earlier could leave open files behind + # https://bitbucket.org/openpyxl/openpyxl/issues/832 + import openpyxl + from distutils.version import LooseVersion + + if LooseVersion(openpyxl.__version__) <= LooseVersion("2.5.5"): + import gc + + gc.collect() From 322ede30b4900449e767a5c313af0c7833f28344 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 9 Dec 2019 15:01:59 -0800 Subject: [PATCH 5/9] fixturize --- pandas/tests/io/conftest.py | 23 ++++++++++++ pandas/tests/io/excel/test_readers.py | 51 --------------------------- 2 files changed, 23 insertions(+), 51 deletions(-) diff --git a/pandas/tests/io/conftest.py b/pandas/tests/io/conftest.py index 3f034107ef24f..ec6cfc92f19fe 100644 --- a/pandas/tests/io/conftest.py +++ b/pandas/tests/io/conftest.py @@ -2,6 +2,8 @@ import pytest +import pandas.util._test_decorators as td + import pandas.util.testing as tm from pandas.io.parsers import read_csv @@ -81,3 +83,24 @@ def add_tips_files(bucket_name): yield conn finally: s3.stop() + + +@pytest.fixture(autouse=True) +def check_for_file_leaks(): + """ + Fixture to run around every test to ensure that we are not leaking files. + + See also + -------- + _test_decorators.check_file_leaks + """ + # GH#30162 + psutil = td.safe_import("psutil") + if not psutil: + return + + proc = psutil.Process() + flist = proc.open_files() + yield + flist2 = proc.open_files() + assert flist == flist2 diff --git a/pandas/tests/io/excel/test_readers.py b/pandas/tests/io/excel/test_readers.py index 62bc8371ef9aa..480407664285f 100644 --- a/pandas/tests/io/excel/test_readers.py +++ b/pandas/tests/io/excel/test_readers.py @@ -84,7 +84,6 @@ def cd_and_set_engine(self, engine, datapath, monkeypatch, read_ext): monkeypatch.chdir(datapath("io", "data", "excel")) monkeypatch.setattr(pd, "read_excel", func) - @td.check_file_leaks def test_usecols_int(self, read_ext, df_ref): df_ref = df_ref.reindex(columns=["A", "B", "C"]) @@ -101,7 +100,6 @@ def test_usecols_int(self, read_ext, df_ref): "test1" + read_ext, "Sheet2", skiprows=[1], index_col=0, usecols=3 ) - @td.check_file_leaks def test_usecols_list(self, read_ext, df_ref): df_ref = df_ref.reindex(columns=["B", "C"]) @@ -116,7 +114,6 @@ def test_usecols_list(self, read_ext, df_ref): tm.assert_frame_equal(df1, df_ref, check_names=False) tm.assert_frame_equal(df2, df_ref, check_names=False) - @td.check_file_leaks def test_usecols_str(self, read_ext, df_ref): df1 = df_ref.reindex(columns=["A", "B", "C"]) @@ -146,7 +143,6 @@ def test_usecols_str(self, read_ext, df_ref): tm.assert_frame_equal(df2, df1, check_names=False) tm.assert_frame_equal(df3, df1, check_names=False) - @td.check_file_leaks @pytest.mark.parametrize( "usecols", [[0, 1, 3], [0, 3, 1], [1, 0, 3], [1, 3, 0], [3, 0, 1], [3, 1, 0]] ) @@ -157,7 +153,6 @@ def test_usecols_diff_positional_int_columns_order(self, read_ext, usecols, df_r ) tm.assert_frame_equal(result, expected, check_names=False) - @td.check_file_leaks @pytest.mark.parametrize("usecols", [["B", "D"], ["D", "B"]]) def test_usecols_diff_positional_str_columns_order(self, read_ext, usecols, df_ref): expected = df_ref[["B", "D"]] @@ -166,13 +161,11 @@ def test_usecols_diff_positional_str_columns_order(self, read_ext, usecols, df_r result = pd.read_excel("test1" + read_ext, "Sheet1", usecols=usecols) tm.assert_frame_equal(result, expected, check_names=False) - @td.check_file_leaks def test_read_excel_without_slicing(self, read_ext, df_ref): expected = df_ref result = pd.read_excel("test1" + read_ext, "Sheet1", index_col=0) tm.assert_frame_equal(result, expected, check_names=False) - @td.check_file_leaks def test_usecols_excel_range_str(self, read_ext, df_ref): expected = df_ref[["C", "D"]] result = pd.read_excel( @@ -180,14 +173,12 @@ def test_usecols_excel_range_str(self, read_ext, df_ref): ) tm.assert_frame_equal(result, expected, check_names=False) - @td.check_file_leaks def test_usecols_excel_range_str_invalid(self, read_ext): msg = "Invalid column name: E1" with pytest.raises(ValueError, match=msg): pd.read_excel("test1" + read_ext, "Sheet1", usecols="D:E1") - @td.check_file_leaks def test_index_col_label_error(self, read_ext): msg = "list indices must be integers.*, not str" @@ -196,7 +187,6 @@ def test_index_col_label_error(self, read_ext): "test1" + read_ext, "Sheet1", index_col=["A"], usecols=["A", "C"] ) - @td.check_file_leaks def test_index_col_empty(self, read_ext): # see gh-9208 result = pd.read_excel("test1" + read_ext, "Sheet3", index_col=["A", "B", "C"]) @@ -206,7 +196,6 @@ def test_index_col_empty(self, read_ext): ) tm.assert_frame_equal(result, expected) - @td.check_file_leaks @pytest.mark.parametrize("index_col", [None, 2]) def test_index_col_with_unnamed(self, read_ext, index_col): # see gh-18792 @@ -219,7 +208,6 @@ def test_index_col_with_unnamed(self, read_ext, index_col): tm.assert_frame_equal(result, expected) - @td.check_file_leaks def test_usecols_pass_non_existent_column(self, read_ext): msg = ( "Usecols do not match columns, " @@ -229,7 +217,6 @@ def test_usecols_pass_non_existent_column(self, read_ext): with pytest.raises(ValueError, match=msg): pd.read_excel("test1" + read_ext, usecols=["E"]) - @td.check_file_leaks def test_usecols_wrong_type(self, read_ext): msg = ( "'usecols' must either be list-like of " @@ -239,21 +226,18 @@ def test_usecols_wrong_type(self, read_ext): with pytest.raises(ValueError, match=msg): pd.read_excel("test1" + read_ext, usecols=["E1", 0]) - @td.check_file_leaks def test_excel_stop_iterator(self, read_ext): parsed = pd.read_excel("test2" + read_ext, "Sheet1") expected = DataFrame([["aaaa", "bbbbb"]], columns=["Test", "Test1"]) tm.assert_frame_equal(parsed, expected) - @td.check_file_leaks def test_excel_cell_error_na(self, read_ext): parsed = pd.read_excel("test3" + read_ext, "Sheet1") expected = DataFrame([[np.nan]], columns=["Test"]) tm.assert_frame_equal(parsed, expected) - @td.check_file_leaks def test_excel_table(self, read_ext, df_ref): df1 = pd.read_excel("test1" + read_ext, "Sheet1", index_col=0) @@ -265,7 +249,6 @@ def test_excel_table(self, read_ext, df_ref): df3 = pd.read_excel("test1" + read_ext, "Sheet1", index_col=0, skipfooter=1) tm.assert_frame_equal(df3, df1.iloc[:-1]) - @td.check_file_leaks def test_reader_special_dtypes(self, read_ext): expected = DataFrame.from_dict( @@ -327,7 +310,6 @@ def test_reader_special_dtypes(self, read_ext): tm.assert_frame_equal(actual, no_convert_float) # GH8212 - support for converters and missing values - @td.check_file_leaks def test_reader_converters(self, read_ext): basename = "test_converters" @@ -355,7 +337,6 @@ def test_reader_converters(self, read_ext): actual = pd.read_excel(basename + read_ext, "Sheet1", converters=converters) tm.assert_frame_equal(actual, expected) - @td.check_file_leaks def test_reader_dtype(self, read_ext): # GH 8212 basename = "testdtype" @@ -384,7 +365,6 @@ def test_reader_dtype(self, read_ext): with pytest.raises(ValueError): pd.read_excel(basename + read_ext, dtype={"d": "int64"}) - @td.check_file_leaks @pytest.mark.parametrize( "dtype,expected", [ @@ -419,7 +399,6 @@ def test_reader_dtype_str(self, read_ext, dtype, expected): actual = pd.read_excel(basename + read_ext, dtype=dtype) tm.assert_frame_equal(actual, expected) - @td.check_file_leaks def test_reading_all_sheets(self, read_ext): # Test reading all sheetnames by setting sheetname to None, # Ensure a dict is returned. @@ -433,7 +412,6 @@ def test_reading_all_sheets(self, read_ext): # Ensure sheet order is preserved assert expected_keys == list(dfs.keys()) - @td.check_file_leaks def test_reading_multiple_specific_sheets(self, read_ext): # Test reading specific sheetnames by specifying a mixed list # of integers and strings, and confirm that duplicated sheet @@ -448,7 +426,6 @@ def test_reading_multiple_specific_sheets(self, read_ext): tm.assert_contains_all(expected_keys, dfs.keys()) assert len(expected_keys) == len(dfs.keys()) - @td.check_file_leaks def test_reading_all_sheets_with_blank(self, read_ext): # Test reading all sheetnames by setting sheetname to None, # In the case where some sheets are blank. @@ -459,18 +436,15 @@ def test_reading_all_sheets_with_blank(self, read_ext): tm.assert_contains_all(expected_keys, dfs.keys()) # GH6403 - @td.check_file_leaks def test_read_excel_blank(self, read_ext): actual = pd.read_excel("blank" + read_ext, "Sheet1") tm.assert_frame_equal(actual, DataFrame()) - @td.check_file_leaks def test_read_excel_blank_with_header(self, read_ext): expected = DataFrame(columns=["col_1", "col_2"]) actual = pd.read_excel("blank_with_header" + read_ext, "Sheet1") tm.assert_frame_equal(actual, expected) - @td.check_file_leaks def test_date_conversion_overflow(self, read_ext): # GH 10001 : pandas.ExcelFile ignore parse_dates=False expected = pd.DataFrame( @@ -488,7 +462,6 @@ def test_date_conversion_overflow(self, read_ext): result = pd.read_excel("testdateoverflow" + read_ext) tm.assert_frame_equal(result, expected) - @td.check_file_leaks def test_sheet_name(self, read_ext, df_ref): filename = "test1" sheet_name = "Sheet1" @@ -502,7 +475,6 @@ def test_sheet_name(self, read_ext, df_ref): tm.assert_frame_equal(df1, df_ref, check_names=False) tm.assert_frame_equal(df2, df_ref, check_names=False) - @td.check_file_leaks def test_excel_read_buffer(self, read_ext): pth = "test1" + read_ext @@ -511,14 +483,12 @@ def test_excel_read_buffer(self, read_ext): actual = pd.read_excel(f, "Sheet1", index_col=0) tm.assert_frame_equal(expected, actual) - @td.check_file_leaks def test_bad_engine_raises(self, read_ext): bad_engine = "foo" with pytest.raises(ValueError, match="Unknown engine: foo"): pd.read_excel("", engine=bad_engine) @tm.network - @td.check_file_leaks def test_read_from_http_url(self, read_ext): url = ( @@ -529,7 +499,6 @@ def test_read_from_http_url(self, read_ext): local_table = pd.read_excel("test1" + read_ext) tm.assert_frame_equal(url_table, local_table) - @td.check_file_leaks @td.skip_if_not_us_locale def test_read_from_s3_url(self, read_ext, s3_resource): # Bucket "pandas-test" created in tests/io/conftest.py @@ -544,7 +513,6 @@ def test_read_from_s3_url(self, read_ext, s3_resource): @pytest.mark.slow # ignore warning from old xlrd @pytest.mark.filterwarnings("ignore:This metho:PendingDeprecationWarning") - @td.check_file_leaks def test_read_from_file_url(self, read_ext, datapath): # FILE @@ -561,7 +529,6 @@ def test_read_from_file_url(self, read_ext, datapath): tm.assert_frame_equal(url_table, local_table) - @td.check_file_leaks def test_read_from_pathlib_path(self, read_ext): # GH12655 @@ -590,7 +557,6 @@ def test_read_from_py_localpath(self, read_ext): tm.assert_frame_equal(expected, actual) - @td.check_file_leaks def test_reader_seconds(self, read_ext): # Test reading times with and without milliseconds. GH5945. @@ -618,7 +584,6 @@ def test_reader_seconds(self, read_ext): actual = pd.read_excel("times_1904" + read_ext, "Sheet1") tm.assert_frame_equal(actual, expected) - @td.check_file_leaks def test_read_excel_multiindex(self, read_ext): # see gh-4679 mi = MultiIndex.from_product([["foo", "bar"], ["a", "b"]]) @@ -684,7 +649,6 @@ def test_read_excel_multiindex(self, read_ext): ) tm.assert_frame_equal(actual, expected) - @td.check_file_leaks def test_read_excel_multiindex_header_only(self, read_ext): # see gh-11733. # @@ -696,7 +660,6 @@ def test_read_excel_multiindex_header_only(self, read_ext): expected = DataFrame([[1, 2, 3, 4]] * 2, columns=exp_columns) tm.assert_frame_equal(result, expected) - @td.check_file_leaks def test_excel_old_index_format(self, read_ext): # see gh-4679 filename = "test_index_name_pre17" + read_ext @@ -769,20 +732,17 @@ def test_excel_old_index_format(self, read_ext): actual = pd.read_excel(filename, "multi_no_names", index_col=[0, 1]) tm.assert_frame_equal(actual, expected, check_names=False) - @td.check_file_leaks def test_read_excel_bool_header_arg(self, read_ext): # GH 6114 for arg in [True, False]: with pytest.raises(TypeError): pd.read_excel("test1" + read_ext, header=arg) - @td.check_file_leaks def test_read_excel_chunksize(self, read_ext): # GH 8011 with pytest.raises(NotImplementedError): pd.read_excel("test1" + read_ext, chunksize=100) - @td.check_file_leaks def test_read_excel_skiprows_list(self, read_ext): # GH 4903 actual = pd.read_excel( @@ -804,7 +764,6 @@ def test_read_excel_skiprows_list(self, read_ext): ) tm.assert_frame_equal(actual, expected) - @td.check_file_leaks def test_read_excel_nrows(self, read_ext): # GH 16645 num_rows_to_pull = 5 @@ -813,7 +772,6 @@ def test_read_excel_nrows(self, read_ext): expected = expected[:num_rows_to_pull] tm.assert_frame_equal(actual, expected) - @td.check_file_leaks def test_read_excel_nrows_greater_than_nrows_in_file(self, read_ext): # GH 16645 expected = pd.read_excel("test1" + read_ext) @@ -822,14 +780,12 @@ def test_read_excel_nrows_greater_than_nrows_in_file(self, read_ext): actual = pd.read_excel("test1" + read_ext, nrows=num_rows_to_pull) tm.assert_frame_equal(actual, expected) - @td.check_file_leaks def test_read_excel_nrows_non_integer_parameter(self, read_ext): # GH 16645 msg = "'nrows' must be an integer >=0" with pytest.raises(ValueError, match=msg): pd.read_excel("test1" + read_ext, nrows="5") - @td.check_file_leaks def test_read_excel_squeeze(self, read_ext): # GH 12157 f = "test_squeeze" + read_ext @@ -865,7 +821,6 @@ def cd_and_set_engine(self, engine, datapath, monkeypatch, read_ext): monkeypatch.chdir(datapath("io", "data", "excel")) monkeypatch.setattr(pd, "ExcelFile", func) - @td.check_file_leaks def test_excel_passes_na(self, read_ext): with pd.ExcelFile("test4" + read_ext) as excel: @@ -905,7 +860,6 @@ def test_excel_passes_na(self, read_ext): ) tm.assert_frame_equal(parsed, expected) - @td.check_file_leaks @pytest.mark.parametrize("na_filter", [None, True, False]) def test_excel_passes_na_filter(self, read_ext, na_filter): # gh-25453 @@ -938,7 +892,6 @@ def test_unexpected_kwargs_raises(self, read_ext, arg): with pytest.raises(TypeError, match=msg): pd.read_excel(excel, **kwarg) - @td.check_file_leaks def test_excel_table_sheet_by_index(self, read_ext, df_ref): with pd.ExcelFile("test1" + read_ext) as excel: @@ -962,7 +915,6 @@ def test_excel_table_sheet_by_index(self, read_ext, df_ref): tm.assert_frame_equal(df3, df1.iloc[:-1]) - @td.check_file_leaks def test_sheet_name(self, read_ext, df_ref): filename = "test1" sheet_name = "Sheet1" @@ -976,7 +928,6 @@ def test_sheet_name(self, read_ext, df_ref): tm.assert_frame_equal(df1_parse, df_ref, check_names=False) tm.assert_frame_equal(df2_parse, df_ref, check_names=False) - @td.check_file_leaks def test_excel_read_buffer(self, engine, read_ext): pth = "test1" + read_ext expected = pd.read_excel(pth, "Sheet1", index_col=0, engine=engine) @@ -987,7 +938,6 @@ def test_excel_read_buffer(self, engine, read_ext): tm.assert_frame_equal(expected, actual) - @td.check_file_leaks def test_reader_closes_file(self, engine, read_ext): f = open("test1" + read_ext, "rb") with pd.ExcelFile(f) as xlsx: @@ -996,7 +946,6 @@ def test_reader_closes_file(self, engine, read_ext): assert f.closed - @td.check_file_leaks def test_conflicting_excel_engines(self, read_ext): # GH 26566 msg = "Engine should not be specified when passing an ExcelFile" From 127dfeeb8224190b6ca74aca3748bc3c05ccc8c2 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 9 Dec 2019 16:38:39 -0800 Subject: [PATCH 6/9] troubleshoot yield behavior --- pandas/tests/io/conftest.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pandas/tests/io/conftest.py b/pandas/tests/io/conftest.py index ec6cfc92f19fe..9402d99a018a2 100644 --- a/pandas/tests/io/conftest.py +++ b/pandas/tests/io/conftest.py @@ -97,10 +97,11 @@ def check_for_file_leaks(): # GH#30162 psutil = td.safe_import("psutil") if not psutil: - return - - proc = psutil.Process() - flist = proc.open_files() - yield - flist2 = proc.open_files() - assert flist == flist2 + yield + + else: + proc = psutil.Process() + flist = proc.open_files() + yield + flist2 = proc.open_files() + assert flist == flist2 From 013cdd77e1e1a621d755cdafb0f47db9548d4f11 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 9 Dec 2019 17:34:19 -0800 Subject: [PATCH 7/9] check file leaks only in excel for now --- pandas/tests/io/conftest.py | 24 ------------------------ pandas/tests/io/excel/conftest.py | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pandas/tests/io/conftest.py b/pandas/tests/io/conftest.py index 9402d99a018a2..3f034107ef24f 100644 --- a/pandas/tests/io/conftest.py +++ b/pandas/tests/io/conftest.py @@ -2,8 +2,6 @@ import pytest -import pandas.util._test_decorators as td - import pandas.util.testing as tm from pandas.io.parsers import read_csv @@ -83,25 +81,3 @@ def add_tips_files(bucket_name): yield conn finally: s3.stop() - - -@pytest.fixture(autouse=True) -def check_for_file_leaks(): - """ - Fixture to run around every test to ensure that we are not leaking files. - - See also - -------- - _test_decorators.check_file_leaks - """ - # GH#30162 - psutil = td.safe_import("psutil") - if not psutil: - yield - - else: - proc = psutil.Process() - flist = proc.open_files() - yield - flist2 = proc.open_files() - assert flist == flist2 diff --git a/pandas/tests/io/excel/conftest.py b/pandas/tests/io/excel/conftest.py index 6ec2f477a442d..4495ba9b80b67 100644 --- a/pandas/tests/io/excel/conftest.py +++ b/pandas/tests/io/excel/conftest.py @@ -1,5 +1,7 @@ import pytest +import pandas.util._test_decorators as td + import pandas.util.testing as tm from pandas.io.parsers import read_csv @@ -39,3 +41,25 @@ def read_ext(request): Valid extensions for reading Excel files. """ return request.param + + +@pytest.fixture(autouse=True) +def check_for_file_leaks(): + """ + Fixture to run around every test to ensure that we are not leaking files. + + See also + -------- + _test_decorators.check_file_leaks + """ + # GH#30162 + psutil = td.safe_import("psutil") + if not psutil: + yield + + else: + proc = psutil.Process() + flist = proc.open_files() + yield + flist2 = proc.open_files() + assert flist == flist2 From a637e5091a9224f2e1956e6d9f330b45e32a6bde Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 10 Dec 2019 16:23:02 -0800 Subject: [PATCH 8/9] bump openpyxl deep to 2.5.6 --- ci/deps/azure-36-locale_slow.yaml | 2 +- ci/deps/azure-36-minimum_versions.yaml | 2 +- doc/source/getting_started/install.rst | 2 +- doc/source/whatsnew/v1.0.0.rst | 2 +- pandas/compat/_optional.py | 2 +- pandas/io/excel/_base.py | 12 +----------- 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/ci/deps/azure-36-locale_slow.yaml b/ci/deps/azure-36-locale_slow.yaml index c3c94e365c259..b9979122427e1 100644 --- a/ci/deps/azure-36-locale_slow.yaml +++ b/ci/deps/azure-36-locale_slow.yaml @@ -18,7 +18,7 @@ dependencies: - lxml - matplotlib=2.2.2 - numpy=1.14.* - - openpyxl=2.4.8 + - openpyxl=2.5.6 - python-dateutil - python-blosc - pytz=2017.2 diff --git a/ci/deps/azure-36-minimum_versions.yaml b/ci/deps/azure-36-minimum_versions.yaml index 8331c8ab23891..b2152938512ac 100644 --- a/ci/deps/azure-36-minimum_versions.yaml +++ b/ci/deps/azure-36-minimum_versions.yaml @@ -19,7 +19,7 @@ dependencies: - jinja2=2.8 - numexpr=2.6.2 - numpy=1.13.3 - - openpyxl=2.4.8 + - openpyxl=2.5.6 - pytables=3.4.2 - python-dateutil=2.6.1 - pytz=2017.2 diff --git a/doc/source/getting_started/install.rst b/doc/source/getting_started/install.rst index 9f3ab22496ae7..3e5080715f2a6 100644 --- a/doc/source/getting_started/install.rst +++ b/doc/source/getting_started/install.rst @@ -255,7 +255,7 @@ gcsfs 0.2.2 Google Cloud Storage access html5lib HTML parser for read_html (see :ref:`note `) lxml 3.8.0 HTML parser for read_html (see :ref:`note `) matplotlib 2.2.2 Visualization -openpyxl 2.4.8 Reading / writing for xlsx files +openpyxl 2.5.6 Reading / writing for xlsx files pandas-gbq 0.8.0 Google Big Query access psycopg2 PostgreSQL engine for sqlalchemy pyarrow 0.12.0 Parquet and feather reading / writing diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index 3e72072eae303..31ae3158d49c6 100644 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -424,7 +424,7 @@ Optional libraries below the lowest tested version may still work, but are not c +-----------------+-----------------+---------+ | matplotlib | 2.2.2 | | +-----------------+-----------------+---------+ -| openpyxl | 2.4.8 | | +| openpyxl | 2.5.6 | X | +-----------------+-----------------+---------+ | pyarrow | 0.12.0 | X | +-----------------+-----------------+---------+ diff --git a/pandas/compat/_optional.py b/pandas/compat/_optional.py index 0be201daea425..07b5b52199811 100644 --- a/pandas/compat/_optional.py +++ b/pandas/compat/_optional.py @@ -14,7 +14,7 @@ "matplotlib": "2.2.2", "numexpr": "2.6.2", "odfpy": "1.3.0", - "openpyxl": "2.4.8", + "openpyxl": "2.5.6", "pandas_gbq": "0.8.0", "pyarrow": "0.12.0", "pytables": "3.4.2", diff --git a/pandas/io/excel/_base.py b/pandas/io/excel/_base.py index 7a9d4926b1a46..d4570498e3fc6 100644 --- a/pandas/io/excel/_base.py +++ b/pandas/io/excel/_base.py @@ -896,7 +896,7 @@ def close(self): # https://stackoverflow.com/questions/31416842/ # openpyxl-does-not-close-excel-workbook-in-read-only-mode wb = self.book - wb.close() + wb._archive.close() if hasattr(self.io, "close"): self.io.close() @@ -910,13 +910,3 @@ def __exit__(self, exc_type, exc_value, traceback): def __del__(self): # Ensure we don't leak file descriptors self.close() - if self.engine == "openpyxl": - # openpyxl 2.5.5 and earlier could leave open files behind - # https://bitbucket.org/openpyxl/openpyxl/issues/832 - import openpyxl - from distutils.version import LooseVersion - - if LooseVersion(openpyxl.__version__) <= LooseVersion("2.5.5"): - import gc - - gc.collect() From a041e876dc249faf0293c48c35cbb6db674d1507 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 10 Dec 2019 19:16:35 -0800 Subject: [PATCH 9/9] bump to 2.5.7 --- ci/deps/azure-36-locale_slow.yaml | 2 +- ci/deps/azure-36-minimum_versions.yaml | 2 +- doc/source/getting_started/install.rst | 2 +- doc/source/whatsnew/v1.0.0.rst | 2 +- pandas/compat/_optional.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ci/deps/azure-36-locale_slow.yaml b/ci/deps/azure-36-locale_slow.yaml index b9979122427e1..2bb2b00319382 100644 --- a/ci/deps/azure-36-locale_slow.yaml +++ b/ci/deps/azure-36-locale_slow.yaml @@ -18,7 +18,7 @@ dependencies: - lxml - matplotlib=2.2.2 - numpy=1.14.* - - openpyxl=2.5.6 + - openpyxl=2.5.7 - python-dateutil - python-blosc - pytz=2017.2 diff --git a/ci/deps/azure-36-minimum_versions.yaml b/ci/deps/azure-36-minimum_versions.yaml index b2152938512ac..8bf4f70d18aec 100644 --- a/ci/deps/azure-36-minimum_versions.yaml +++ b/ci/deps/azure-36-minimum_versions.yaml @@ -19,7 +19,7 @@ dependencies: - jinja2=2.8 - numexpr=2.6.2 - numpy=1.13.3 - - openpyxl=2.5.6 + - openpyxl=2.5.7 - pytables=3.4.2 - python-dateutil=2.6.1 - pytz=2017.2 diff --git a/doc/source/getting_started/install.rst b/doc/source/getting_started/install.rst index 3e5080715f2a6..ec4c0a3c4a8e8 100644 --- a/doc/source/getting_started/install.rst +++ b/doc/source/getting_started/install.rst @@ -255,7 +255,7 @@ gcsfs 0.2.2 Google Cloud Storage access html5lib HTML parser for read_html (see :ref:`note `) lxml 3.8.0 HTML parser for read_html (see :ref:`note `) matplotlib 2.2.2 Visualization -openpyxl 2.5.6 Reading / writing for xlsx files +openpyxl 2.5.7 Reading / writing for xlsx files pandas-gbq 0.8.0 Google Big Query access psycopg2 PostgreSQL engine for sqlalchemy pyarrow 0.12.0 Parquet and feather reading / writing diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index 31ae3158d49c6..86347f9530c56 100644 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -424,7 +424,7 @@ Optional libraries below the lowest tested version may still work, but are not c +-----------------+-----------------+---------+ | matplotlib | 2.2.2 | | +-----------------+-----------------+---------+ -| openpyxl | 2.5.6 | X | +| openpyxl | 2.5.7 | X | +-----------------+-----------------+---------+ | pyarrow | 0.12.0 | X | +-----------------+-----------------+---------+ diff --git a/pandas/compat/_optional.py b/pandas/compat/_optional.py index 07b5b52199811..412293f029fa5 100644 --- a/pandas/compat/_optional.py +++ b/pandas/compat/_optional.py @@ -14,7 +14,7 @@ "matplotlib": "2.2.2", "numexpr": "2.6.2", "odfpy": "1.3.0", - "openpyxl": "2.5.6", + "openpyxl": "2.5.7", "pandas_gbq": "0.8.0", "pyarrow": "0.12.0", "pytables": "3.4.2",