From 41c3481aaad9f55ba91d0e420bd131530cc4fdfc Mon Sep 17 00:00:00 2001 From: Chilin Chiou Date: Sun, 9 Mar 2025 01:38:35 +0800 Subject: [PATCH 1/7] Add hint to display full message for missing dependencies in pandas/init.py --- pandas/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/__init__.py b/pandas/__init__.py index c570fb8d70204..2298769c40792 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -14,7 +14,10 @@ if _missing_dependencies: # pragma: no cover raise ImportError( - "Unable to import required dependencies:\n" + "\n".join(_missing_dependencies) + "Unable to import required dependencies:\n" + + "\n".join(_missing_dependencies) + + "\n\nTo see the full error message, " + + "try importing the missing dependencies directly." ) del _hard_dependencies, _dependency, _missing_dependencies From 0267293886b9544a6248410b1e4a95f2e5bed834 Mon Sep 17 00:00:00 2001 From: Chilin Chiou Date: Tue, 11 Mar 2025 16:35:38 +0800 Subject: [PATCH 2/7] ENH: Improve import error handling to preserve original traceback --- pandas/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pandas/__init__.py b/pandas/__init__.py index 2298769c40792..b5b9e9f5baae2 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -10,14 +10,13 @@ try: __import__(_dependency) except ImportError as _e: # pragma: no cover - _missing_dependencies.append(f"{_dependency}: {_e}") + raise ImportError( + f"Unable to import required dependency {_dependency} ..." + ) from _e if _missing_dependencies: # pragma: no cover raise ImportError( - "Unable to import required dependencies:\n" - + "\n".join(_missing_dependencies) - + "\n\nTo see the full error message, " - + "try importing the missing dependencies directly." + "Unable to import required dependencies:\n" + "\n".join(_missing_dependencies) ) del _hard_dependencies, _dependency, _missing_dependencies From 51826c087ae146e08b1a7aa66773f2a1e089bbed Mon Sep 17 00:00:00 2001 From: Chilin Chiou Date: Tue, 11 Mar 2025 22:04:20 +0800 Subject: [PATCH 3/7] TST: refactor testing for hard dependency package --- pandas/__init__.py | 7 +--- pandas/tests/test_downstream.py | 57 ++++++++++++++------------------- 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/pandas/__init__.py b/pandas/__init__.py index b5b9e9f5baae2..8fae3dd4c3cc4 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -4,7 +4,6 @@ # Let users know if they're missing any of our hard dependencies _hard_dependencies = ("numpy", "dateutil") -_missing_dependencies = [] for _dependency in _hard_dependencies: try: @@ -14,11 +13,7 @@ f"Unable to import required dependency {_dependency} ..." ) from _e -if _missing_dependencies: # pragma: no cover - raise ImportError( - "Unable to import required dependencies:\n" + "\n".join(_missing_dependencies) - ) -del _hard_dependencies, _dependency, _missing_dependencies +del _hard_dependencies, _dependency try: # numpy compat diff --git a/pandas/tests/test_downstream.py b/pandas/tests/test_downstream.py index 76fad35304fe6..8d7d869829fc8 100644 --- a/pandas/tests/test_downstream.py +++ b/pandas/tests/test_downstream.py @@ -4,6 +4,7 @@ import array from functools import partial +import importlib import subprocess import sys @@ -186,41 +187,31 @@ def test_yaml_dump(df): tm.assert_frame_equal(df, loaded2) -@pytest.mark.single_cpu -def test_missing_required_dependency(): - # GH 23868 - # To ensure proper isolation, we pass these flags - # -S : disable site-packages - # -s : disable user site-packages - # -E : disable PYTHON* env vars, especially PYTHONPATH - # https://github.com/MacPython/pandas-wheels/pull/50 - - pyexe = sys.executable.replace("\\", "/") - - # We skip this test if pandas is installed as a site package. We first - # import the package normally and check the path to the module before - # executing the test which imports pandas with site packages disabled. - call = [pyexe, "-c", "import pandas;print(pandas.__file__)"] - output = subprocess.check_output(call).decode() - if "site-packages" in output: - pytest.skip("pandas installed as site package") - - # This test will fail if pandas is installed as a site package. The flags - # prevent pandas being imported and the test will report Failed: DID NOT - # RAISE - call = [pyexe, "-sSE", "-c", "import pandas"] - - msg = ( - rf"Command '\['{pyexe}', '-sSE', '-c', 'import pandas'\]' " - "returned non-zero exit status 1." - ) +@pytest.mark.parametrize("dependency", ["numpy", "dateutil"]) +def test_missing_required_dependency(monkeypatch, dependency): + # GH#61030 + # test pandas raises appropriate error when a required dependency is missing + real_module = sys.modules.get(dependency) + mock_error = ImportError(f"Mock error for {dependency}") - with pytest.raises(subprocess.CalledProcessError, match=msg) as exc: - subprocess.check_output(call, stderr=subprocess.STDOUT) + def mock_import(name, *args, **kwargs): + if name == dependency: + raise mock_error + return importlib.import_module(name) + + try: + monkeypatch.setattr("builtins.__import__", mock_import) - output = exc.value.stdout.decode() - for name in ["numpy", "dateutil"]: - assert name in output + if dependency in sys.modules: + del sys.modules[dependency] + + with pytest.raises(ImportError) as excinfo: + importlib.reload(importlib.import_module("pandas")) + + assert dependency in str(excinfo.value) + finally: + if real_module is not None: + sys.modules[dependency] = real_module def test_frame_setitem_dask_array_into_new_col(request): From 2fa208dade7dfa43f5a34f4109b9c767c34693e3 Mon Sep 17 00:00:00 2001 From: ChiLin Chiu Date: Thu, 13 Mar 2025 06:35:34 +0800 Subject: [PATCH 4/7] Update pandas/__init__.py Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- pandas/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/__init__.py b/pandas/__init__.py index 8fae3dd4c3cc4..232e3810f7f43 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -10,7 +10,7 @@ __import__(_dependency) except ImportError as _e: # pragma: no cover raise ImportError( - f"Unable to import required dependency {_dependency} ..." + f"Unable to import required dependency {_dependency}. Please see the traceback for details." ) from _e del _hard_dependencies, _dependency From 734a7070fe690b1208c93d98808ab9835e37c9c0 Mon Sep 17 00:00:00 2001 From: Chilin Chiou Date: Thu, 13 Mar 2025 07:11:08 +0800 Subject: [PATCH 5/7] Refactor prevent statement too long --- pandas/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/__init__.py b/pandas/__init__.py index 232e3810f7f43..5dc6a8c3bc50c 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -10,7 +10,8 @@ __import__(_dependency) except ImportError as _e: # pragma: no cover raise ImportError( - f"Unable to import required dependency {_dependency}. Please see the traceback for details." + f"Unable to import required dependency {_dependency}. " + "Please see the traceback for details." ) from _e del _hard_dependencies, _dependency From 5f5ab6a33481f5509dfe620e5737f91671ee2709 Mon Sep 17 00:00:00 2001 From: Chilin Chiou Date: Sat, 5 Apr 2025 23:21:09 +0800 Subject: [PATCH 6/7] ENH: change unittest to verify ImportError is raised when required dependencies are missing --- pandas/tests/test_downstream.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/pandas/tests/test_downstream.py b/pandas/tests/test_downstream.py index 8d7d869829fc8..afc9716b79ddc 100644 --- a/pandas/tests/test_downstream.py +++ b/pandas/tests/test_downstream.py @@ -190,28 +190,20 @@ def test_yaml_dump(df): @pytest.mark.parametrize("dependency", ["numpy", "dateutil"]) def test_missing_required_dependency(monkeypatch, dependency): # GH#61030 - # test pandas raises appropriate error when a required dependency is missing - real_module = sys.modules.get(dependency) + original_import = __import__ mock_error = ImportError(f"Mock error for {dependency}") def mock_import(name, *args, **kwargs): if name == dependency: raise mock_error - return importlib.import_module(name) + return original_import(name, *args, **kwargs) - try: - monkeypatch.setattr("builtins.__import__", mock_import) - - if dependency in sys.modules: - del sys.modules[dependency] + monkeypatch.setattr("builtins.__import__", mock_import) - with pytest.raises(ImportError) as excinfo: - importlib.reload(importlib.import_module("pandas")) + with pytest.raises(ImportError) as excinfo: + importlib.reload(importlib.import_module("pandas")) - assert dependency in str(excinfo.value) - finally: - if real_module is not None: - sys.modules[dependency] = real_module + assert dependency in str(excinfo.value) def test_frame_setitem_dask_array_into_new_col(request): From 05a5fb8601b52a3e315bd441310521e7438f2ea9 Mon Sep 17 00:00:00 2001 From: Chilin Chiou Date: Fri, 11 Apr 2025 23:30:46 +0800 Subject: [PATCH 7/7] TST: Use pytest.raises match parameter in test_missing_required_dependency --- pandas/tests/test_downstream.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pandas/tests/test_downstream.py b/pandas/tests/test_downstream.py index afc9716b79ddc..6282aecdfe977 100644 --- a/pandas/tests/test_downstream.py +++ b/pandas/tests/test_downstream.py @@ -200,11 +200,9 @@ def mock_import(name, *args, **kwargs): monkeypatch.setattr("builtins.__import__", mock_import) - with pytest.raises(ImportError) as excinfo: + with pytest.raises(ImportError, match=dependency): importlib.reload(importlib.import_module("pandas")) - assert dependency in str(excinfo.value) - def test_frame_setitem_dask_array_into_new_col(request): # GH#47128