Skip to content

Commit

Permalink
ENH: Improve handling of optional Cartopy map features
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dopplershift committed Jun 23, 2021
1 parent acd279b commit 9e66a98
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 14 deletions.
21 changes: 16 additions & 5 deletions src/metpy/plots/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__
3 changes: 3 additions & 0 deletions src/metpy/plots/cartopy_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
32 changes: 23 additions & 9 deletions tests/plots/test_cartopy_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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


Expand All @@ -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


Expand All @@ -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


Expand All @@ -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


Expand All @@ -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)

0 comments on commit 9e66a98

Please sign in to comment.