From 2433f3d9e1ae4c11001408dfffe336997713ca98 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 13 Jul 2026 11:43:19 -0700 Subject: [PATCH 1/7] Lazy load optional dependencies Reduce import time of Harmonica by lazily loading optional dependencies like `pyvista`, `vtk`, and `numba_progress`. Add comments to ignore Ruff complains about having import statements inside functions. --- src/harmonica/_forward/utils.py | 16 ++++------------ src/harmonica/visualization/_prism.py | 17 ++++------------- test/test_visualizations.py | 22 ---------------------- 3 files changed, 8 insertions(+), 47 deletions(-) diff --git a/src/harmonica/_forward/utils.py b/src/harmonica/_forward/utils.py index aa1b46264..7509097da 100644 --- a/src/harmonica/_forward/utils.py +++ b/src/harmonica/_forward/utils.py @@ -14,12 +14,6 @@ from numba import jit from scipy.spatial.transform import Rotation -# Attempt to import numba_progress -try: - from numba_progress import ProgressBar -except ImportError: # pragma: no cover - ProgressBar = None - def distance(point_p, point_q, coordinate_system="cartesian", ellipsoid=None): """ @@ -384,12 +378,10 @@ def initialize_progressbar(total, use_progressbar): # Return None if progressbar is not desired if not use_progressbar: return contextlib.nullcontext() - # Raise error if numba_progress is not installed - if ProgressBar is None: - msg = ( - "Missing optional dependency 'numba_progress' required if progressbar=True" - ) - raise ImportError(msg) + + # Lazily load optional dependency + from numba_progress import ProgressBar # noqa: PLC0415 + return ProgressBar(total=total) diff --git a/src/harmonica/visualization/_prism.py b/src/harmonica/visualization/_prism.py index 68ccf35ce..7a4451b0a 100644 --- a/src/harmonica/visualization/_prism.py +++ b/src/harmonica/visualization/_prism.py @@ -10,13 +10,6 @@ import numpy as np -try: - import pyvista -except ImportError: # pragma: no cover - pyvista = None -else: - import vtk - def prism_to_pyvista(prisms, properties=None): """ @@ -71,12 +64,10 @@ def prism_to_pyvista(prisms, properties=None): >>> pv_grid.plot() # doctest: +SKIP """ - # Check if pyvista are installed - if pyvista is None: - msg = ( - "Missing optional dependency 'pyvista' required for building pyvista grids." - ) - raise ImportError(msg) + # Lazily import optional dependencies + import pyvista # noqa: PLC0415 + import vtk # noqa: PLC0415 + # Get prisms and number of prisms prisms = np.atleast_2d(prisms) n_prisms = prisms.shape[0] diff --git a/test/test_visualizations.py b/test/test_visualizations.py index 31338c619..84b598b94 100644 --- a/test/test_visualizations.py +++ b/test/test_visualizations.py @@ -9,7 +9,6 @@ """ import re -from unittest.mock import patch import numpy as np import numpy.testing as npt @@ -24,17 +23,6 @@ pyvista = None -@patch("harmonica.visualization._prism.pyvista", None) -def test_prism_to_pyvista_missing_pyvista(): - """ - Check error raise after calling prism_to_pyvista when pyvista is missing. - """ - prism = [0, 1, 0, 1, 0, 1] - with pytest.raises(ImportError) as exception: - prism_to_pyvista(prism) - assert "'pyvista'" in str(exception.value) - - @pytest.fixture(name="prisms") def fixture_prisms(): """ @@ -124,13 +112,3 @@ def test_prism_to_pyvista_error_2d_property(prisms, density): ) with pytest.raises(ValueError, match=msg): prism_to_pyvista(prisms, properties={name: density_2d}) - - -@patch("harmonica.visualization._prism.pyvista", None) -def test_prisms_pyvista_missing_error(prisms, density): - """ - Check if prism_to_pyvista raises error if pyvista is missing. - """ - # Check if error is raised - with pytest.raises(ImportError): - prism_to_pyvista(prisms, properties={"density": density}) From ecb82e0ecc20f65061663c178ef1ce9d28689672 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 13 Jul 2026 11:50:43 -0700 Subject: [PATCH 2/7] Lazy import pyvista in `Ellipsoid` method --- src/harmonica/_forward/ellipsoids/ellipsoids.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/harmonica/_forward/ellipsoids/ellipsoids.py b/src/harmonica/_forward/ellipsoids/ellipsoids.py index 174e034e5..80d49a686 100644 --- a/src/harmonica/_forward/ellipsoids/ellipsoids.py +++ b/src/harmonica/_forward/ellipsoids/ellipsoids.py @@ -16,11 +16,6 @@ from ..utils import get_rotation_matrix -try: - import pyvista -except ImportError: # pragma: no cover - pyvista = None - class Ellipsoid: """ @@ -313,12 +308,9 @@ def to_pyvista(self, **kwargs): ellipsoid : pyvista.PolyData A PyVista's parametric ellipsoid. """ - if pyvista is None: - msg = ( - "Missing optional dependency 'pyvista' required for " - "exporting ellipsoids to PyVista." - ) - raise ImportError(msg) + # Lazy load optional dependency + import pyvista # noqa: PLC0415 + ellipsoid = pyvista.ParametricEllipsoid( xradius=self.a, yradius=self.b, zradius=self.c, **kwargs ) From 4c9ddef6a92b164c6dc55857375be76573a3098c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 13 Jul 2026 11:56:42 -0700 Subject: [PATCH 3/7] Remove tests that checked for custom ImportErrors Remove tests that used to check if custom `ImportError`s were being raised. Since we are importing inside the functions themselves, it's best to just let the original `ImportError` to be raised. --- test/ellipsoids/test_ellipsoids.py | 9 --------- test/test_forward_utils.py | 14 -------------- test/test_prism.py | 13 ------------- test/test_prism_layer.py | 19 ------------------- test/test_tesseroid.py | 13 ------------- test/test_tesseroid_layer.py | 19 ------------------- test/test_tesseroid_variable_density.py | 12 ------------ 7 files changed, 99 deletions(-) diff --git a/test/ellipsoids/test_ellipsoids.py b/test/ellipsoids/test_ellipsoids.py index 1fd09092e..dd56daa1d 100644 --- a/test/ellipsoids/test_ellipsoids.py +++ b/test/ellipsoids/test_ellipsoids.py @@ -9,7 +9,6 @@ """ import re -from unittest.mock import patch import numpy as np import pytest @@ -212,14 +211,6 @@ def ellipsoid(self): center = (43.0, -72.0, 105) return Ellipsoid(a, b, c, yaw=yaw, pitch=pitch, roll=roll, center=center) - @patch("harmonica._forward.ellipsoids.ellipsoids.pyvista", None) - def test_pyvista_missing_error(self, ellipsoid): - """ - Check if error is raised when pyvista is not installed. - """ - with pytest.raises(ImportError): - ellipsoid.to_pyvista() - def test_pyvista_object(self, ellipsoid): """ Check if method works as expected. diff --git a/test/test_forward_utils.py b/test/test_forward_utils.py index 2d8a69b8d..f34e2ca68 100644 --- a/test/test_forward_utils.py +++ b/test/test_forward_utils.py @@ -8,7 +8,6 @@ Test utils functions for forward modelling. """ -from unittest.mock import patch import boule as bl import numpy as np @@ -131,16 +130,3 @@ def test_initialize_progressbar(use_progressbar): assert isinstance(progress_proxy, ProgressBar) else: assert progress_proxy is None - - -@patch("harmonica._forward.utils.ProgressBar", None) -@pytest.mark.parametrize("use_progressbar", [True, False]) -def test_initialize_progressbar_import_error(use_progressbar): - """Test if it raises ImportError when numba_progress is missing.""" - if use_progressbar: - with pytest.raises(ImportError): # noqa: SIM117 - with initialize_progressbar(3, use_progressbar) as progress_proxy: - pass # pragma: no cover (won't reach this code) - else: - with initialize_progressbar(3, use_progressbar) as progress_proxy: - assert progress_proxy is None diff --git a/test/test_prism.py b/test/test_prism.py index c6330ae64..38463f50e 100644 --- a/test/test_prism.py +++ b/test/test_prism.py @@ -9,7 +9,6 @@ """ import re -from unittest.mock import patch import bordado as bd import numpy as np @@ -377,18 +376,6 @@ def test_progress_bar(self, coordinates, prisms, densities, field): ) npt.assert_allclose(result_progress_true, result_progress_false) - @patch("harmonica._forward.utils.ProgressBar", None) - def test_numba_progress_missing_error(self, coordinates, prisms, densities): - """ - Check if error is raised when progresbar=True and numba_progress - package is not installed. - """ - # Check if error is raised - with pytest.raises(ImportError): - prism_gravity( - coordinates, prisms, densities, field="potential", progressbar=True - ) - class TestSingularPoints: """ diff --git a/test/test_prism_layer.py b/test/test_prism_layer.py index 60620a128..9ee5141ff 100644 --- a/test/test_prism_layer.py +++ b/test/test_prism_layer.py @@ -9,7 +9,6 @@ """ import re -from unittest.mock import patch import bordado as bd import numpy as np @@ -420,24 +419,6 @@ def test_progress_bar(dummy_layer): npt.assert_allclose(result_progress_true, result_progress_false) -@patch("harmonica._forward.utils.ProgressBar", None) -def test_numba_progress_missing_error(dummy_layer): - """ - Check if error is raised when progressbar=True and numba_progress package - is not installed. - """ - coordinates = bd.grid_coordinates( - (1, 3, 7, 10), spacing=1, non_dimensional_coords=30.0 - ) - (easting, northing), surface, reference, density = dummy_layer - layer = prism_layer( - (easting, northing), surface, reference, properties={"density": density} - ) - # Check if error is raised - with pytest.raises(ImportError): - layer.prism_layer.gravity(coordinates, field="g_z", progressbar=True) - - def test_gravity_discarded_thin_prisms(dummy_layer): """ Check if gravity of prism layer after discarding thin prisms is correct. diff --git a/test/test_tesseroid.py b/test/test_tesseroid.py index 9f4e2865e..80d18158a 100644 --- a/test/test_tesseroid.py +++ b/test/test_tesseroid.py @@ -9,7 +9,6 @@ """ import re -from unittest.mock import patch import bordado as bd import boule @@ -808,15 +807,3 @@ def test_progress_bar(self, coordinates, tesseroids, densities, field): coordinates, tesseroids, densities, field=field, progressbar=False ) npt.assert_allclose(result_progress_true, result_progress_false) - - @patch("harmonica._forward.utils.ProgressBar", None) - def test_numba_progress_missing_error(self, coordinates, tesseroids, densities): - """ - Check if error is raised when progresbar=True and numba_progress - package is not installed. - """ - # Check if error is raised - with pytest.raises(ImportError): - tesseroid_gravity( - coordinates, tesseroids, densities, field="potential", progressbar=True - ) diff --git a/test/test_tesseroid_layer.py b/test/test_tesseroid_layer.py index 6504aed27..40f18badc 100644 --- a/test/test_tesseroid_layer.py +++ b/test/test_tesseroid_layer.py @@ -9,7 +9,6 @@ """ import warnings -from unittest.mock import patch import bordado as bd import boule @@ -493,24 +492,6 @@ def test_progress_bar(dummy_layer): npt.assert_allclose(result_progress_true, result_progress_false) -@patch("harmonica._forward.utils.ProgressBar", None) -def test_numba_progress_missing_error(dummy_layer): - """ - Check if error is raised when progressbar=True and numba_progress package - is not installed. - """ - (longitude, latitude), surface, reference, density = dummy_layer - coordinates = bd.grid_coordinates( - (-10, 10, -10, 10), spacing=7, non_dimensional_coords=(surface[0] + 10e3) - ) - layer = tesseroid_layer( - (longitude, latitude), surface, reference, properties={"density": density} - ) - # Check if error is raised - with pytest.raises(ImportError): - layer.tesseroid_layer.gravity(coordinates, field="g_z", progressbar=True) - - def test_gravity_discarded_thin_tesseroids(dummy_layer): """ Check if gravity of tesseroid layer after discarding thin tesseroids is correct. diff --git a/test/test_tesseroid_variable_density.py b/test/test_tesseroid_variable_density.py index 4707028b9..8c23a2b2c 100644 --- a/test/test_tesseroid_variable_density.py +++ b/test/test_tesseroid_variable_density.py @@ -8,7 +8,6 @@ Test forward modelling for tesseroids with variable density. """ -from unittest.mock import patch import bordado as bd import numpy as np @@ -570,14 +569,3 @@ def test_progress_bar(self, coordinates, tesseroids, densities, field): ) npt.assert_allclose(result_progress_true, result_progress_false) - @patch("harmonica._forward.utils.ProgressBar", None) - def test_numba_progress_missing_error(self, coordinates, tesseroids, densities): - """ - Check if error is raised when progresbar=True and numba_progress - package is not installed. - """ - # Check if error is raised - with pytest.raises(ImportError): - tesseroid_gravity( - coordinates, tesseroids, densities, field="potential", progressbar=True - ) From de4939775fc0f5390c2cd55590e1554a0ffecfd9 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 13 Jul 2026 12:04:48 -0700 Subject: [PATCH 4/7] Add custom error when initializing the progressbar Raise a custom error instructing users to install `numba_progress` upon import. --- src/harmonica/_forward/utils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/harmonica/_forward/utils.py b/src/harmonica/_forward/utils.py index 7509097da..5608aebd3 100644 --- a/src/harmonica/_forward/utils.py +++ b/src/harmonica/_forward/utils.py @@ -380,7 +380,14 @@ def initialize_progressbar(total, use_progressbar): return contextlib.nullcontext() # Lazily load optional dependency - from numba_progress import ProgressBar # noqa: PLC0415 + try: + from numba_progress import ProgressBar # noqa: PLC0415 + except ImportError as original: + error = ImportError( + "Cannot import the optional dependency 'numba_progress'. " + "It has to be installed in order to be able to show a progressbar." + ) # pragma: nocover + raise error from original # pragma: nocover return ProgressBar(total=total) From 5d75dc0e60db374e29f4ea61bce4ee1e2cd148fb Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 13 Jul 2026 12:05:35 -0700 Subject: [PATCH 5/7] Autoformat --- src/harmonica/_forward/utils.py | 2 +- test/test_forward_utils.py | 1 - test/test_tesseroid_variable_density.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/harmonica/_forward/utils.py b/src/harmonica/_forward/utils.py index 5608aebd3..3848e0963 100644 --- a/src/harmonica/_forward/utils.py +++ b/src/harmonica/_forward/utils.py @@ -386,7 +386,7 @@ def initialize_progressbar(total, use_progressbar): error = ImportError( "Cannot import the optional dependency 'numba_progress'. " "It has to be installed in order to be able to show a progressbar." - ) # pragma: nocover + ) # pragma: nocover raise error from original # pragma: nocover return ProgressBar(total=total) diff --git a/test/test_forward_utils.py b/test/test_forward_utils.py index f34e2ca68..1e20d7891 100644 --- a/test/test_forward_utils.py +++ b/test/test_forward_utils.py @@ -8,7 +8,6 @@ Test utils functions for forward modelling. """ - import boule as bl import numpy as np import numpy.testing as npt diff --git a/test/test_tesseroid_variable_density.py b/test/test_tesseroid_variable_density.py index 8c23a2b2c..f7f38065f 100644 --- a/test/test_tesseroid_variable_density.py +++ b/test/test_tesseroid_variable_density.py @@ -8,7 +8,6 @@ Test forward modelling for tesseroids with variable density. """ - import bordado as bd import numpy as np import numpy.testing as npt @@ -568,4 +567,3 @@ def test_progress_bar(self, coordinates, tesseroids, densities, field): coordinates, tesseroids, densities, field=field, progressbar=False ) npt.assert_allclose(result_progress_true, result_progress_false) - From ff7589c3c718b5a8f23231cedc2ccb760881e498 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 13 Jul 2026 12:06:13 -0700 Subject: [PATCH 6/7] Improve wording in error message --- src/harmonica/_forward/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/harmonica/_forward/utils.py b/src/harmonica/_forward/utils.py index 3848e0963..7dc4092d4 100644 --- a/src/harmonica/_forward/utils.py +++ b/src/harmonica/_forward/utils.py @@ -385,7 +385,7 @@ def initialize_progressbar(total, use_progressbar): except ImportError as original: error = ImportError( "Cannot import the optional dependency 'numba_progress'. " - "It has to be installed in order to be able to show a progressbar." + "It must be installed to be able to show a progressbar." ) # pragma: nocover raise error from original # pragma: nocover From e6ca2bafca82c6f2a6757a227efce1acf9e5bf67 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 13 Jul 2026 12:53:13 -0700 Subject: [PATCH 7/7] Add nocover to except line --- src/harmonica/_forward/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/harmonica/_forward/utils.py b/src/harmonica/_forward/utils.py index 7dc4092d4..6270b3632 100644 --- a/src/harmonica/_forward/utils.py +++ b/src/harmonica/_forward/utils.py @@ -382,7 +382,7 @@ def initialize_progressbar(total, use_progressbar): # Lazily load optional dependency try: from numba_progress import ProgressBar # noqa: PLC0415 - except ImportError as original: + except ImportError as original: # pragma: nocover error = ImportError( "Cannot import the optional dependency 'numba_progress'. " "It must be installed to be able to show a progressbar."