diff --git a/ci/deps/actions-310.yaml b/ci/deps/actions-310.yaml index e442beda102eb..e2bfe6e57d216 100644 --- a/ci/deps/actions-310.yaml +++ b/ci/deps/actions-310.yaml @@ -12,7 +12,6 @@ dependencies: - pytest>=7.0.0 - pytest-cov - pytest-xdist>=2.2.0 - - psutil - pytest-asyncio>=0.17 - boto3 diff --git a/ci/deps/actions-311.yaml b/ci/deps/actions-311.yaml index 85103276fa6a6..237924b5c6f0b 100644 --- a/ci/deps/actions-311.yaml +++ b/ci/deps/actions-311.yaml @@ -12,7 +12,6 @@ dependencies: - pytest>=7.0 - pytest-cov - pytest-xdist>=2.2.0 - - psutil - pytest-asyncio>=0.17 - boto3 diff --git a/ci/deps/actions-38-downstream_compat.yaml b/ci/deps/actions-38-downstream_compat.yaml index e313d4efbf8a7..9b62b25a15740 100644 --- a/ci/deps/actions-38-downstream_compat.yaml +++ b/ci/deps/actions-38-downstream_compat.yaml @@ -13,7 +13,6 @@ dependencies: - pytest>=7.0.0 - pytest-cov - pytest-xdist>=2.2.0 - - psutil - pytest-asyncio>=0.17 - boto3 diff --git a/ci/deps/actions-38-minimum_versions.yaml b/ci/deps/actions-38-minimum_versions.yaml index 7652b6347ad4f..f3ff36a1b2ada 100644 --- a/ci/deps/actions-38-minimum_versions.yaml +++ b/ci/deps/actions-38-minimum_versions.yaml @@ -14,7 +14,6 @@ dependencies: - pytest>=7.0.0 - pytest-cov - pytest-xdist>=2.2.0 - - psutil - pytest-asyncio>=0.17 - boto3 diff --git a/ci/deps/actions-38.yaml b/ci/deps/actions-38.yaml index 233cd651a85af..95bab9897ac63 100644 --- a/ci/deps/actions-38.yaml +++ b/ci/deps/actions-38.yaml @@ -12,7 +12,6 @@ dependencies: - pytest>=7.0.0 - pytest-cov - pytest-xdist>=2.2.0 - - psutil - pytest-asyncio>=0.17 - boto3 diff --git a/ci/deps/actions-39.yaml b/ci/deps/actions-39.yaml index 0a56337dd3bbd..9d95e28ae9fb6 100644 --- a/ci/deps/actions-39.yaml +++ b/ci/deps/actions-39.yaml @@ -12,7 +12,6 @@ dependencies: - pytest>=7.0.0 - pytest-cov - pytest-xdist>=2.2.0 - - psutil - pytest-asyncio>=0.17 - boto3 diff --git a/ci/deps/circle-38-arm64.yaml b/ci/deps/circle-38-arm64.yaml index 40cfe7b605278..1548eb3d4929d 100644 --- a/ci/deps/circle-38-arm64.yaml +++ b/ci/deps/circle-38-arm64.yaml @@ -12,7 +12,6 @@ dependencies: - pytest>=7.0.0 - pytest-cov - pytest-xdist>=2.2.0 - - psutil - pytest-asyncio>=0.17 - boto3 diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index ac7d30310be9e..f5445c567f59b 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -1331,6 +1331,7 @@ I/O - Bug in :meth:`DataFrame.to_html` with ``na_rep`` set when the :class:`DataFrame` contains non-scalar data (:issue:`47103`) - Bug in :func:`read_xml` where file-like objects failed when iterparse is used (:issue:`50641`) - Bug in :func:`read_xml` ignored repeated elements when iterparse is used (:issue:`51183`) +- Bug in :class:`ExcelWriter` leaving file handles open if an exception occurred during instantiation (:issue:`51443`) Period ^^^^^^ diff --git a/environment.yml b/environment.yml index 04ded62262483..41c93de50bff3 100644 --- a/environment.yml +++ b/environment.yml @@ -14,7 +14,6 @@ dependencies: - pytest>=7.0.0 - pytest-cov - pytest-xdist>=2.2.0 - - psutil - pytest-asyncio>=0.17 - coverage diff --git a/pandas/_testing/_warnings.py b/pandas/_testing/_warnings.py index 69ef57d9f450d..0d1076f235b1d 100644 --- a/pandas/_testing/_warnings.py +++ b/pandas/_testing/_warnings.py @@ -5,7 +5,6 @@ nullcontext, ) import re -import sys from typing import ( Generator, Literal, @@ -164,22 +163,6 @@ def _assert_caught_no_extra_warnings( for actual_warning in caught_warnings: if _is_unexpected_warning(actual_warning, expected_warning): - # GH#38630 pytest.filterwarnings does not suppress these. - if actual_warning.category == ResourceWarning: - # GH 44732: Don't make the CI flaky by filtering SSL-related - # ResourceWarning from dependencies - unclosed_ssl = ( - "unclosed transport str: """ @@ -1617,11 +1617,3 @@ def __exit__( traceback: TracebackType | None, ) -> None: self.close() - - def __del__(self) -> None: - # Ensure we don't leak file descriptors, but put in try/except in case - # attributes are already deleted - try: - self.close() - except AttributeError: - pass diff --git a/pandas/io/excel/_odswriter.py b/pandas/io/excel/_odswriter.py index 5ea3d8d3e43f4..6f1d62111e5b4 100644 --- a/pandas/io/excel/_odswriter.py +++ b/pandas/io/excel/_odswriter.py @@ -48,6 +48,9 @@ def __init__( if mode == "a": raise ValueError("Append mode is not supported with odf!") + engine_kwargs = combine_kwargs(engine_kwargs, kwargs) + self._book = OpenDocumentSpreadsheet(**engine_kwargs) + super().__init__( path, mode=mode, @@ -56,9 +59,6 @@ def __init__( engine_kwargs=engine_kwargs, ) - engine_kwargs = combine_kwargs(engine_kwargs, kwargs) - - self._book = OpenDocumentSpreadsheet(**engine_kwargs) self._style_dict: dict[str, str] = {} @property diff --git a/pandas/io/excel/_openpyxl.py b/pandas/io/excel/_openpyxl.py index 69ddadc58f10b..594813fe0c1ac 100644 --- a/pandas/io/excel/_openpyxl.py +++ b/pandas/io/excel/_openpyxl.py @@ -70,11 +70,19 @@ def __init__( if "r+" in self._mode: # Load from existing workbook from openpyxl import load_workbook - self._book = load_workbook(self._handles.handle, **engine_kwargs) + try: + self._book = load_workbook(self._handles.handle, **engine_kwargs) + except TypeError: + self._handles.handle.close() + raise self._handles.handle.seek(0) else: # Create workbook object with default optimized_write=True. - self._book = Workbook(**engine_kwargs) + try: + self._book = Workbook(**engine_kwargs) + except TypeError: + self._handles.handle.close() + raise if self.book.worksheets: self.book.remove(self.book.worksheets[0]) diff --git a/pandas/tests/frame/test_api.py b/pandas/tests/frame/test_api.py index 4ffb4b6b355b6..e5787a7f16a35 100644 --- a/pandas/tests/frame/test_api.py +++ b/pandas/tests/frame/test_api.py @@ -7,7 +7,6 @@ from pandas._config.config import option_context -import pandas.util._test_decorators as td from pandas.util._test_decorators import ( async_mark, skip_if_no, @@ -293,7 +292,6 @@ def _check_f(base, f): _check_f(d.copy(), f) @async_mark() - @td.check_file_leaks async def test_tab_complete_warning(self, ip, frame_or_series): # GH 16409 pytest.importorskip("IPython", minversion="6.0.0") diff --git a/pandas/tests/io/excel/conftest.py b/pandas/tests/io/excel/conftest.py index 4ce06c01892d9..15ff52d5bea48 100644 --- a/pandas/tests/io/excel/conftest.py +++ b/pandas/tests/io/excel/conftest.py @@ -1,8 +1,5 @@ import pytest -from pandas.compat import is_platform_windows -import pandas.util._test_decorators as td - import pandas._testing as tm from pandas.io.parsers import read_csv @@ -42,26 +39,3 @@ def read_ext(request): Valid extensions for reading Excel files. """ return request.param - - -# Checking for file leaks can hang on Windows CI -@pytest.fixture(autouse=not is_platform_windows()) -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/test_readers.py b/pandas/tests/io/excel/test_readers.py index 5fdd0e4311d88..b36f3fcee16ca 100644 --- a/pandas/tests/io/excel/test_readers.py +++ b/pandas/tests/io/excel/test_readers.py @@ -909,7 +909,6 @@ 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 from py.path import local as LocalPath @@ -922,7 +921,6 @@ def test_read_from_py_localpath(self, read_ext): tm.assert_frame_equal(expected, actual) - @td.check_file_leaks def test_close_from_py_localpath(self, read_ext): # GH31467 str_path = os.path.join("test1" + read_ext) diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py index af117af0c8d3d..fae29c124df71 100644 --- a/pandas/tests/io/formats/test_format.py +++ b/pandas/tests/io/formats/test_format.py @@ -26,7 +26,6 @@ IS64, is_platform_windows, ) -import pandas.util._test_decorators as td import pandas as pd from pandas import ( @@ -3400,7 +3399,6 @@ def test_format_percentiles_integer_idx(): assert result == expected -@td.check_file_leaks def test_repr_html_ipython_config(ip): code = textwrap.dedent( """\ diff --git a/pandas/tests/io/parser/common/test_file_buffer_url.py b/pandas/tests/io/parser/common/test_file_buffer_url.py index 3f1d013c5471d..c11a59a8b4660 100644 --- a/pandas/tests/io/parser/common/test_file_buffer_url.py +++ b/pandas/tests/io/parser/common/test_file_buffer_url.py @@ -13,7 +13,6 @@ import pytest -from pandas.compat import is_ci_environment from pandas.errors import ( EmptyDataError, ParserError, @@ -404,25 +403,14 @@ def test_context_manageri_user_provided(all_parsers, datapath): assert not reader.handles.handle.closed -def test_file_descriptor_leak(all_parsers, using_copy_on_write, request): +def test_file_descriptor_leak(all_parsers, using_copy_on_write): # GH 31488 - if using_copy_on_write and is_ci_environment(): - mark = pytest.mark.xfail( - reason="2023-02-12 frequent-but-flaky failures", strict=False - ) - request.node.add_marker(mark) - parser = all_parsers with tm.ensure_clean() as path: - - def test(): - with pytest.raises(EmptyDataError, match="No columns to parse from file"): - parser.read_csv(path) - - td.check_file_leaks(test)() + with pytest.raises(EmptyDataError, match="No columns to parse from file"): + parser.read_csv(path) -@td.check_file_leaks def test_memory_map(all_parsers, csv_dir_path): mmap_file = os.path.join(csv_dir_path, "test_mmap.csv") parser = all_parsers diff --git a/pandas/tests/io/parser/common/test_read_errors.py b/pandas/tests/io/parser/common/test_read_errors.py index f5a7eb4ccd2fa..817daad9849c0 100644 --- a/pandas/tests/io/parser/common/test_read_errors.py +++ b/pandas/tests/io/parser/common/test_read_errors.py @@ -17,7 +17,6 @@ EmptyDataError, ParserError, ) -import pandas.util._test_decorators as td from pandas import DataFrame import pandas._testing as tm @@ -204,7 +203,6 @@ def test_null_byte_char(request, all_parsers): parser.read_csv(StringIO(data), names=names) -@td.check_file_leaks def test_open_file(request, all_parsers): # GH 39024 parser = all_parsers diff --git a/pandas/tests/io/sas/test_xport.py b/pandas/tests/io/sas/test_xport.py index 2046427deeaf0..766c9c37d55b9 100644 --- a/pandas/tests/io/sas/test_xport.py +++ b/pandas/tests/io/sas/test_xport.py @@ -1,8 +1,6 @@ import numpy as np import pytest -import pandas.util._test_decorators as td - import pandas as pd import pandas._testing as tm @@ -21,11 +19,6 @@ def numeric_as_float(data): class TestXport: - @pytest.fixture(autouse=True) - def setup_method(self): - with td.file_leak_context(): - yield - @pytest.fixture def file01(self, datapath): return datapath("io", "sas", "data", "DEMO_G.xpt") @@ -138,10 +131,9 @@ def test2_binary(self, file02): numeric_as_float(data_csv) with open(file02, "rb") as fd: - with td.file_leak_context(): - # GH#35693 ensure that if we pass an open file, we - # dont incorrectly close it in read_sas - data = read_sas(fd, format="xport") + # GH#35693 ensure that if we pass an open file, we + # dont incorrectly close it in read_sas + data = read_sas(fd, format="xport") tm.assert_frame_equal(data, data_csv) diff --git a/pandas/tests/resample/test_resampler_grouper.py b/pandas/tests/resample/test_resampler_grouper.py index 2aa2b272547bc..425eef69c52ae 100644 --- a/pandas/tests/resample/test_resampler_grouper.py +++ b/pandas/tests/resample/test_resampler_grouper.py @@ -3,7 +3,6 @@ import numpy as np import pytest -import pandas.util._test_decorators as td from pandas.util._test_decorators import async_mark import pandas as pd @@ -24,7 +23,6 @@ @async_mark() -@td.check_file_leaks async def test_tab_complete_ipython6_warning(ip): from IPython.core.completer import provisionalcompleter diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py index 855b8d52aa84d..afb4dd7422114 100644 --- a/pandas/tests/scalar/timestamp/test_timestamp.py +++ b/pandas/tests/scalar/timestamp/test_timestamp.py @@ -10,7 +10,10 @@ import time import unicodedata -from dateutil.tz import tzutc +from dateutil.tz import ( + tzlocal, + tzutc, +) import numpy as np import pytest import pytz @@ -23,6 +26,7 @@ maybe_get_tz, tz_compare, ) +from pandas.compat import IS64 from pandas.errors import OutOfBoundsDatetime import pandas.util._test_decorators as td @@ -152,6 +156,11 @@ def test_names(self, data, time_locale): def test_is_leap_year(self, tz_naive_fixture): tz = tz_naive_fixture + if not IS64 and tz == tzlocal(): + # https://github.com/dateutil/dateutil/issues/197 + pytest.skip( + "tzlocal() on a 32 bit platform causes internal overflow errors" + ) # GH 13727 dt = Timestamp("2000-01-01 00:00:00", tz=tz) assert dt.is_leap_year diff --git a/pandas/util/_test_decorators.py b/pandas/util/_test_decorators.py index de100dba8144d..dfe48d994cb0e 100644 --- a/pandas/util/_test_decorators.py +++ b/pandas/util/_test_decorators.py @@ -25,13 +25,8 @@ def test_foo(): """ from __future__ import annotations -from contextlib import contextmanager -import gc import locale -from typing import ( - Callable, - Generator, -) +from typing import Callable import numpy as np import pytest @@ -233,43 +228,6 @@ def documented_fixture(fixture): return documented_fixture -def check_file_leaks(func) -> Callable: - """ - Decorate a test function to check that we are not leaking file descriptors. - """ - with file_leak_context(): - return func - - -@contextmanager -def file_leak_context() -> Generator[None, None, None]: - """ - ContextManager analogue to check_file_leaks. - """ - psutil = safe_import("psutil") - if not psutil or is_platform_windows(): - # Checking for file leaks can hang on Windows CI - yield - else: - proc = psutil.Process() - flist = proc.open_files() - conns = proc.connections() - - try: - yield - finally: - gc.collect() - flist2 = proc.open_files() - # on some builds open_files includes file position, which we _dont_ - # expect to remain unchanged, so we need to compare excluding that - flist_ex = [(x.path, x.fd) for x in flist] - flist2_ex = [(x.path, x.fd) for x in flist2] - assert set(flist2_ex) <= set(flist_ex), (flist2, flist) - - conns2 = proc.connections() - assert conns2 == conns, (conns2, conns) - - def async_mark(): try: import_optional_dependency("pytest_asyncio") diff --git a/pyproject.toml b/pyproject.toml index 511cc6a4d46eb..9d4166b033128 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -395,6 +395,11 @@ doctest_optionflags = [ "ELLIPSIS", ] filterwarnings = [ + "error::ResourceWarning", + "error::pytest.PytestUnraisableExceptionWarning", + "ignore:.*ssl.SSLSocket:pytest.PytestUnraisableExceptionWarning", + "ignore:unclosed =7.0.0 pytest-cov pytest-xdist>=2.2.0 -psutil pytest-asyncio>=0.17 coverage python-dateutil