From 2fa025195d9618fdbbdda9e7e4d442a2ee877a24 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 29 Oct 2024 13:37:02 -0700 Subject: [PATCH 1/6] fix logic --- hvplot/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index bf2044e0e..7a81c1c07 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -370,7 +370,7 @@ def __init__(self, data, **params): pass geo_params = GEO_KEYS + ['geo'] - if not gv_available and any(p in params for p in geo_params): + if not gv_available and any(p in params and params[p] for p in geo_params): raise ImportError('GeoViews must be installed to enable the geographic options.') super().__init__(data, **params) if not gv_available: From c3c5a2a68329014207029dca42c26c734193027f Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 29 Oct 2024 13:38:55 -0700 Subject: [PATCH 2/6] simplify --- hvplot/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 7a81c1c07..808cb4f95 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -370,7 +370,7 @@ def __init__(self, data, **params): pass geo_params = GEO_KEYS + ['geo'] - if not gv_available and any(p in params and params[p] for p in geo_params): + if not gv_available and any(params.get(p) for p in geo_params): raise ImportError('GeoViews must be installed to enable the geographic options.') super().__init__(data, **params) if not gv_available: From b441bdab5b396d1e5ae23682c72e5f6f41a559ca Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 29 Oct 2024 13:48:01 -0700 Subject: [PATCH 3/6] add test --- hvplot/tests/testui.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index ccbbb3cca..2cd9fb624 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -415,3 +415,8 @@ def test_max_rows_sample(): ui = hvplot.explorer(df, x='x', y='y', by=['#'], kind='scatter') assert len(ui._data) == MAX_ROWS assert not ui._data.equals(df.head(MAX_ROWS)) + + +def test_explorer_geo_no_import_error_when_false(): + da = ds_air_temperature['air'].isel(time=0) + assert hvplot.explorer(da, x='lon', y='lat', geo=False) From 681fa4f3d4a1c5d95d74b672d610478322d17d20 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 30 Oct 2024 11:36:01 -0700 Subject: [PATCH 4/6] refactor into util and test --- hvplot/converter.py | 16 +++++----------- hvplot/tests/testui.py | 4 +++- hvplot/tests/testutil.py | 21 +++++++++++++++++++++ hvplot/ui.py | 14 +++----------- hvplot/util.py | 18 ++++++++++++++++++ 5 files changed, 50 insertions(+), 23 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 8d5f3f035..aecb0227a 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -76,6 +76,7 @@ process_derived_datetime_pandas, _convert_col_names_to_str, import_datashader, + geoviews_is_available, ) from .utilities import hvplot_extension @@ -646,6 +647,10 @@ def __init__( self.dynamic = dynamic self.geo = any([geo, crs, global_extent, projection, project, coastline, features]) + # Try importing geoviews if geo-features requested + if self.geo or self.datatype == 'geopandas': + geoviews_is_available(raise_error=True) + self.crs = self._process_crs(data, crs) if self.geo else None self.output_projection = self.crs self.project = project @@ -655,17 +660,6 @@ def __init__( self.tiles_opts = tiles_opts or {} self.sort_date = sort_date - # Import geoviews if geo-features requested - if self.geo or self.datatype == 'geopandas': - try: - import geoviews # noqa - except ImportError: - raise ImportError( - 'In order to use geo-related features ' - 'the geoviews library must be available. ' - 'It can be installed with:\n conda ' - 'install geoviews' - ) if self.geo: if self.kind not in self._geo_types: param.main.param.warning( diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 2cd9fb624..68bc46cd4 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -1,5 +1,6 @@ import re from textwrap import dedent +from unittest.mock import patch import numpy as np import holoviews as hv @@ -419,4 +420,5 @@ def test_max_rows_sample(): def test_explorer_geo_no_import_error_when_false(): da = ds_air_temperature['air'].isel(time=0) - assert hvplot.explorer(da, x='lon', y='lat', geo=False) + with patch('hvplot.util.geoviews_is_available', return_value=False): + assert hvplot.explorer(da, x='lon', y='lat', geo=False) diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py index 4f9d39ef8..71dc7c42a 100644 --- a/hvplot/tests/testutil.py +++ b/hvplot/tests/testutil.py @@ -2,6 +2,7 @@ Tests utilities to convert data and projections """ +from unittest.mock import patch import numpy as np import pandas as pd import panel as pn @@ -18,6 +19,7 @@ from hvplot.util import ( check_crs, + geoviews_is_available, is_list_like, process_crs, process_xarray, @@ -383,3 +385,22 @@ def test_is_geodataframe_spatialpandas_dask(): def test_is_geodataframe_classic_dataframe(): df = pd.DataFrame({'geometry': [None, None], 'name': ['A', 'B']}) assert not is_geodataframe(df) + + +@pytest.mark.geo +def test_geoviews_is_available(): + assert geoviews_is_available(raise_error=True) + + +def test_geoviews_is_available_no_raise(): + with patch('hvplot.util.geoviews_is_available', side_effect=ImportError): + result = geoviews_is_available(raise_error=False) + assert result is False + + +def test_geoviews_is_available_with_raise(): + with patch('hvplot.util.geoviews_is_available', side_effect=ImportError): + with pytest.raises( + ImportError, match='GeoViews must be installed to enable the geographic options.' + ): + geoviews_is_available(raise_error=True) diff --git a/hvplot/ui.py b/hvplot/ui.py index 808cb4f95..b6256ecef 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -10,7 +10,7 @@ from .converter import HoloViewsConverter as _hvConverter from .plotting import hvPlot as _hvPlot -from .util import is_geodataframe, is_xarray, instantiate_crs_str +from .util import is_geodataframe, is_xarray, instantiate_crs_str, geoviews_is_available # Defaults KINDS = { @@ -361,17 +361,9 @@ class Geographic(Controls): _widgets_kwargs = {'geo': {'type': pn.widgets.Toggle}} def __init__(self, data, **params): - gv_available = False - try: - import geoviews # noqa - - gv_available = True - except ImportError: - pass - geo_params = GEO_KEYS + ['geo'] - if not gv_available and any(params.get(p) for p in geo_params): - raise ImportError('GeoViews must be installed to enable the geographic options.') + gv_available = geoviews_is_available(raise_error=any(params.get(p) for p in geo_params)) + super().__init__(data, **params) if not gv_available: for p in geo_params: diff --git a/hvplot/util.py b/hvplot/util.py index e0392e300..6a86cab7f 100644 --- a/hvplot/util.py +++ b/hvplot/util.py @@ -750,3 +750,21 @@ def relabel_redim(hv_obj, relabel_kwargs, redim_kwargs): if redim_kwargs: hv_obj = hv_obj.redim(**redim_kwargs) return hv_obj + + +def geoviews_is_available(raise_error: bool = False): + """ + Check if GeoViews is available and raise an ImportError if not. + """ + try: + import geoviews # noqa + + gv_available = True + except ImportError: + gv_available = False + + if not raise_error: + return gv_available + + if not gv_available and raise_error: + raise ImportError('GeoViews must be installed to enable the geographic options.') From bae3bd715beab08114966e7897d76743d5083dc4 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 30 Oct 2024 11:52:15 -0700 Subject: [PATCH 5/6] use findspec properly mock --- hvplot/tests/testutil.py | 4 ++-- hvplot/ui.py | 4 ++-- hvplot/util.py | 12 +++--------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py index 71dc7c42a..2dc7cac67 100644 --- a/hvplot/tests/testutil.py +++ b/hvplot/tests/testutil.py @@ -393,13 +393,13 @@ def test_geoviews_is_available(): def test_geoviews_is_available_no_raise(): - with patch('hvplot.util.geoviews_is_available', side_effect=ImportError): + with patch('hvplot.util.find_spec', return_value=None): result = geoviews_is_available(raise_error=False) assert result is False def test_geoviews_is_available_with_raise(): - with patch('hvplot.util.geoviews_is_available', side_effect=ImportError): + with patch('hvplot.util.find_spec', return_value=None): with pytest.raises( ImportError, match='GeoViews must be installed to enable the geographic options.' ): diff --git a/hvplot/ui.py b/hvplot/ui.py index b6256ecef..ad9b19023 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -665,8 +665,8 @@ def _plot(self): if kwargs.get('geo'): if 'crs' not in kwargs: xmax = np.max(np.abs(self.xlim())) - self.geographic.crs = 'PlateCarree' if xmax <= 360 else 'GOOGLE_MERCATOR' - kwargs['crs'] = self.geographic.crs + # self.geographic.crs = 'PlateCarree' if xmax <= 360 else 'GOOGLE_MERCATOR' + kwargs['crs'] = 'PlateCarree' if xmax <= 360 else 'GOOGLE_MERCATOR' for key in ['crs', 'projection']: if key not in kwargs: continue diff --git a/hvplot/util.py b/hvplot/util.py index 6a86cab7f..33dcb336c 100644 --- a/hvplot/util.py +++ b/hvplot/util.py @@ -2,6 +2,7 @@ Provides utilities to convert data and projections """ +from importlib.util import find_spec import sys from collections.abc import Hashable @@ -756,15 +757,8 @@ def geoviews_is_available(raise_error: bool = False): """ Check if GeoViews is available and raise an ImportError if not. """ - try: - import geoviews # noqa - - gv_available = True - except ImportError: - gv_available = False - - if not raise_error: - return gv_available + gv_available = find_spec('geoviews') is not None if not gv_available and raise_error: raise ImportError('GeoViews must be installed to enable the geographic options.') + return gv_available From 0054eead2a5fafeab6584d9e34874dd568c3c509 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 30 Oct 2024 13:09:29 -0700 Subject: [PATCH 6/6] add back --- hvplot/ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index ad9b19023..b6256ecef 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -665,8 +665,8 @@ def _plot(self): if kwargs.get('geo'): if 'crs' not in kwargs: xmax = np.max(np.abs(self.xlim())) - # self.geographic.crs = 'PlateCarree' if xmax <= 360 else 'GOOGLE_MERCATOR' - kwargs['crs'] = 'PlateCarree' if xmax <= 360 else 'GOOGLE_MERCATOR' + self.geographic.crs = 'PlateCarree' if xmax <= 360 else 'GOOGLE_MERCATOR' + kwargs['crs'] = self.geographic.crs for key in ['crs', 'projection']: if key not in kwargs: continue