From 9e66a9807e1657ac5d6d08fc7da9414c243f26ef Mon Sep 17 00:00:00 2001 From: Ryan May Date: Tue, 22 Jun 2021 15:36:03 -0600 Subject: [PATCH] ENH: Improve handling of optional Cartopy map features Use the module-level __getattr__ support in Python 3.7 to dynamically handle access to the CartoPy map features and only warn *upon use* if CartoPy isn't installed. --- src/metpy/plots/__init__.py | 21 +++++++++++++++----- src/metpy/plots/cartopy_utils.py | 3 +++ tests/plots/test_cartopy_utils.py | 32 ++++++++++++++++++++++--------- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/metpy/plots/__init__.py b/src/metpy/plots/__init__.py index 1af06cce648..4c557bb064e 100644 --- a/src/metpy/plots/__init__.py +++ b/src/metpy/plots/__init__.py @@ -7,6 +7,7 @@ # Trigger matplotlib wrappers from . import _mpl # noqa: F401 +from . import cartopy_utils from ._util import (add_metpy_logo, add_timestamp, add_unidata_logo, # noqa: F401 convert_gempak_color) from .ctables import * # noqa: F403 @@ -25,10 +26,20 @@ __all__.extend(wx_symbols.__all__) # pylint: disable=undefined-variable __all__.extend(['add_metpy_logo', 'add_timestamp', 'add_unidata_logo', 'convert_gempak_color']) -try: - from .cartopy_utils import USCOUNTIES, USSTATES # noqa: F401 - __all__.extend(['USCOUNTIES', 'USSTATES']) -except ImportError: - logger.warning('Cannot import USCOUNTIES and USSTATES without Cartopy installed.') set_module(globals()) + + +def __getattr__(name): + """Handle warning if Cartopy map features are not available.""" + if name in cartopy_utils.__all__: + try: + return getattr(cartopy_utils, name) + except AttributeError: + logger.warning(f'Cannot use {name} without Cartopy installed.') + + raise AttributeError(f'module {__name__!r} has no attribute {name!r}') + + +def __dir__(): + return __all__ + cartopy_utils.__all__ diff --git a/src/metpy/plots/cartopy_utils.py b/src/metpy/plots/cartopy_utils.py index 8841f025b32..55101052c5e 100644 --- a/src/metpy/plots/cartopy_utils.py +++ b/src/metpy/plots/cartopy_utils.py @@ -54,8 +54,11 @@ def with_scale(self, new_scale): USSTATES = MetPyMapFeature('us_states', '20m', facecolor='None', edgecolor='black') except ImportError: + # If no Cartopy is present, we just don't have map features pass +__all__ = ['USCOUNTIES', 'USSTATES'] + def import_cartopy(): """Import CartoPy; return a stub if unable. diff --git a/tests/plots/test_cartopy_utils.py b/tests/plots/test_cartopy_utils.py index 5ec295ff4f9..29c3b7b4042 100644 --- a/tests/plots/test_cartopy_utils.py +++ b/tests/plots/test_cartopy_utils.py @@ -2,15 +2,14 @@ # Distributed under the terms of the BSD 3-Clause License. # SPDX-License-Identifier: BSD-3-Clause """Test the cartopy utilities.""" -import contextlib +import logging import matplotlib import matplotlib.pyplot as plt import pytest -with contextlib.suppress(ImportError): - from metpy.plots import USCOUNTIES, USSTATES -from metpy.plots.cartopy_utils import import_cartopy +import metpy.plots as mpplots +from metpy.plots import cartopy_utils # Fixture to make sure we have the right backend from metpy.testing import set_agg_backend # noqa: F401, I202 @@ -26,7 +25,7 @@ def test_us_county_defaults(ccrs): fig = plt.figure(figsize=(12, 9)) ax = fig.add_subplot(1, 1, 1, projection=proj) ax.set_extent([270.25, 270.9, 38.15, 38.75], ccrs.Geodetic()) - ax.add_feature(USCOUNTIES) + ax.add_feature(mpplots.USCOUNTIES) return fig @@ -43,7 +42,7 @@ def test_us_county_scales(ccrs): for scale, axis in zip(['20m', '5m', '500k'], [ax1, ax2, ax3]): axis.set_extent([270.25, 270.9, 38.15, 38.75], ccrs.Geodetic()) - axis.add_feature(USCOUNTIES.with_scale(scale)) + axis.add_feature(mpplots.USCOUNTIES.with_scale(scale)) return fig @@ -55,7 +54,7 @@ def test_us_states_defaults(ccrs): fig = plt.figure(figsize=(12, 9)) ax = fig.add_subplot(1, 1, 1, projection=proj) ax.set_extent([270, 280, 28, 39], ccrs.Geodetic()) - ax.add_feature(USSTATES) + ax.add_feature(mpplots.USSTATES) return fig @@ -72,7 +71,7 @@ def test_us_states_scales(ccrs): for scale, axis in zip(['20m', '5m', '500k'], [ax1, ax2, ax3]): axis.set_extent([270, 280, 28, 39], ccrs.Geodetic()) - axis.add_feature(USSTATES.with_scale(scale)) + axis.add_feature(mpplots.USSTATES.with_scale(scale)) return fig @@ -82,6 +81,21 @@ def test_cartopy_stub(monkeypatch): # This makes sure that cartopy is not found monkeypatch.setitem(sys.modules, 'cartopy.crs', None) - ccrs = import_cartopy() + ccrs = cartopy_utils.import_cartopy() with pytest.raises(RuntimeError, match='CartoPy is required'): ccrs.PlateCarree() + + +def test_plots_getattr(monkeypatch, caplog): + """Ensure the module-level getattr works.""" + # Make sure the feature is missing + caplog.set_level(logging.WARNING, 'metpy.plots') + monkeypatch.delattr(cartopy_utils, 'USSTATES') + with pytest.raises(AttributeError): + assert not mpplots.USSTATES # Should fail on attribute lookup before assert + assert 'Cannot use USSTATES without Cartopy' in caplog.text + + +def test_plots_dir(): + """Ensure dir() on metpy.plots works.""" + assert 'USSTATES' in dir(mpplots)