Skip to content

Commit

Permalink
Merge pull request #1454 from dopplershift/improve-tests
Browse files Browse the repository at this point in the history
Improve testing framework and infrastructure
  • Loading branch information
dopplershift authored Aug 26, 2020
2 parents 931c114 + 2c77a74 commit cdeb8c7
Show file tree
Hide file tree
Showing 17 changed files with 279 additions and 129 deletions.
32 changes: 22 additions & 10 deletions .github/workflows/tests-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,25 @@ jobs:
# Run all tests on Linux using standard PyPI packages, including min and pre-releases
#
PyPITests:
name: ${{ matrix.python-version }} ${{ matrix.dep-versions }}
name: ${{ matrix.python-version }} ${{ matrix.dep-versions }} ${{ matrix.no-extras }}
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
continue-on-error: ${{ matrix.dep-versions == 'Prerelease' }}
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8]
dep-versions: [Current.txt]
git-versions: ['']
experimental: [false]
no-extras: ['']
include:
- python-version: 3.6
dep-versions: Minimum
git-versions: ''
experimental: false
no-extras: ''
- python-version: 3.8
dep-versions: Current.txt
no-extras: 'No Extras'
- python-version: 3.8
dep-versions: Prerelease
git-versions: 'git+git://github.com/hgrecco/pint@master#egg=pint git+git://github.com/pydata/xarray@master#egg=xarray'
experimental: true
no-extras: ''

steps:
# We check out only a limited depth and then pull tags to save time
Expand Down Expand Up @@ -63,6 +63,16 @@ jobs:
pip-tests-${{ runner.os }}-
pip-tests-
- name: Add extras to requirements
if: ${{ matrix.no-extras != 'No Extras' }}
run: cat ci/extra_requirements.txt >> ci/test_requirements.txt

- name: Add git versions to requirements
if: ${{ matrix.dep-versions == 'Prerelease' }}
run: |
echo git+git://github.com/hgrecco/pint@master#egg=pint >> ci/test_requirements.txt
echo git+git://github.com/pydata/xarray@master#egg=xarray >> ci/test_requirements.txt
# This installs the stuff needed to build and install Shapely and CartoPy from source.
# Need to install numpy first to make CartoPy happy.
- name: Install dependencies
Expand All @@ -71,14 +81,16 @@ jobs:
python -m pip install --upgrade pip setuptools
python -m pip install --no-binary :all: shapely
python -m pip install -c ci/${{ matrix.dep-versions }} numpy
python -m pip install -r ci/test_requirements.txt -r ci/extra_requirements.txt -c ci/${{ matrix.dep-versions }} ${{ matrix.git-versions }}
python -m pip install -r ci/test_requirements.txt -c ci/${{ matrix.dep-versions }}
# This imports CartoPy to find its map data cache directory
- name: Get CartoPy maps dir
if: ${{ matrix.no-extras != 'No Extras' }}
id: cartopy-cache
run: echo "::set-output name=dir::$(python -c 'import cartopy;print(cartopy.config["data_dir"])')"

- name: Setup mapdata caching
if: ${{ steps.cartopy-cache.outputs.dir != '' }}
uses: actions/cache@v2
env:
# Increase to reset cache of map data
Expand All @@ -98,7 +110,7 @@ jobs:
python -m pytest --mpl -W error::metpy.deprecation.MetpyDeprecationWarning --cov=metpy --cov=tests --cov-report=xml
- name: Run doctests
if: ${{ matrix.dep-versions == 'Current.txt' }}
if: ${{ matrix.dep-versions == 'Current.txt' && matrix.no-extras != 'No Extras' }}
env:
PY_IGNORE_IMPORTMISMATCH: 1
run: python -m pytest --doctest-modules -k "not test" src;
Expand Down
20 changes: 20 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,23 @@ def doctest_available_modules(doctest_namespace):
doctest_namespace['metpy'] = metpy
doctest_namespace['metpy.calc'] = metpy.calc
doctest_namespace['plt'] = matplotlib.pyplot


@pytest.fixture()
def ccrs():
"""Provide access to the ``cartopy.crs`` module through a global fixture.
Any testing function/fixture that needs access to ``cartopy.crs`` can simply add this to
their parameter list.
"""
return pytest.importorskip('cartopy.crs')


@pytest.fixture
def cfeature():
"""Provide access to the ``cartopy.feature`` module through a global fixture.
Any testing function/fixture that needs access to ``cartopy.feature`` can simply add this
to their parameter list.
"""
return pytest.importorskip('cartopy.feature')
97 changes: 59 additions & 38 deletions src/metpy/plots/cartopy_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,75 @@
# SPDX-License-Identifier: BSD-3-Clause
"""Cartopy specific mapping utilities."""

import cartopy.crs as ccrs
import cartopy.feature as cfeature
try:
import cartopy.feature as cfeature

from ..cbook import get_test_data
from ..cbook import get_test_data

class MetPyMapFeature(cfeature.Feature):
"""A simple interface to MetPy-included shapefiles."""

class MetPyMapFeature(cfeature.Feature):
"""A simple interface to MetPy-included shapefiles."""
def __init__(self, name, scale, **kwargs):
"""Create MetPyMapFeature instance."""
import cartopy.crs as ccrs
super().__init__(ccrs.PlateCarree(), **kwargs)
self.name = name

def __init__(self, name, scale, **kwargs):
"""Create MetPyMapFeature instance."""
super().__init__(ccrs.PlateCarree(), **kwargs)
self.name = name
if isinstance(scale, str):
scale = cfeature.Scaler(scale)
self.scaler = scale

if isinstance(scale, str):
scale = cfeature.Scaler(scale)
self.scaler = scale
def geometries(self):
"""Return an iterator of (shapely) geometries for this feature."""
import cartopy.io.shapereader as shapereader
# Ensure that the associated files are in the cache
fname = f'{self.name}_{self.scaler.scale}'
for extension in ['.dbf', '.shx']:
get_test_data(fname + extension)
path = get_test_data(fname + '.shp', as_file_obj=False)
return iter(tuple(shapereader.Reader(path).geometries()))

def geometries(self):
"""Return an iterator of (shapely) geometries for this feature."""
import cartopy.io.shapereader as shapereader
# Ensure that the associated files are in the cache
fname = f'{self.name}_{self.scaler.scale}'
for extension in ['.dbf', '.shx']:
get_test_data(fname + extension)
path = get_test_data(fname + '.shp', as_file_obj=False)
return iter(tuple(shapereader.Reader(path).geometries()))
def intersecting_geometries(self, extent):
"""Return geometries that intersect the extent."""
self.scaler.scale_from_extent(extent)
return super().intersecting_geometries(extent)

def intersecting_geometries(self, extent):
"""Return geometries that intersect the extent."""
self.scaler.scale_from_extent(extent)
return super().intersecting_geometries(extent)
def with_scale(self, new_scale):
"""
Return a copy of the feature with a new scale.
def with_scale(self, new_scale):
"""
Return a copy of the feature with a new scale.
Parameters
----------
new_scale
The new dataset scale, i.e. one of '500k', '5m', or '20m'.
Corresponding to 1:500,000, 1:5,000,000, and 1:20,000,000
respectively.
Parameters
----------
new_scale
The new dataset scale, i.e. one of '500k', '5m', or '20m'.
Corresponding to 1:500,000, 1:5,000,000, and 1:20,000,000
respectively.
"""
return MetPyMapFeature(self.name, new_scale, **self.kwargs)

"""
return MetPyMapFeature(self.name, new_scale, **self.kwargs)
USCOUNTIES = MetPyMapFeature('us_counties', '20m', facecolor='None', edgecolor='black')

USSTATES = MetPyMapFeature('us_states', '20m', facecolor='None', edgecolor='black')
except ImportError:
pass

USCOUNTIES = MetPyMapFeature('us_counties', '20m', facecolor='None', edgecolor='black')

USSTATES = MetPyMapFeature('us_states', '20m', facecolor='None', edgecolor='black')
def import_cartopy():
"""Import CartoPy; return a stub if unable.
This allows code requiring CartoPy to fail at use time rather than import time.
"""
try:
import cartopy.crs as ccrs
return ccrs
except ImportError:
return CartopyStub()


class CartopyStub:
"""Fail if a CartoPy attribute is accessed."""

def __getattr__(self, item):
"""Raise an error on any attribute access."""
raise RuntimeError(f'CartoPy is required to use this feature ({item}).')
19 changes: 8 additions & 11 deletions src/metpy/plots/declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@

from datetime import datetime, timedelta

try:
import cartopy.crs as ccrs
DEFAULT_LAT_LON = ccrs.PlateCarree()
except ImportError:
DEFAULT_LAT_LON = None
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
Expand All @@ -18,11 +13,13 @@

from . import ctables
from . import wx_symbols
from .cartopy_utils import import_cartopy
from .station_plot import StationPlot
from ..calc import reduce_point_density
from ..package_tools import Exporter
from ..units import units

ccrs = import_cartopy()
exporter = Exporter(globals())

_areas = {
Expand Down Expand Up @@ -741,7 +738,7 @@ def draw(self):
# Otherwise, assume we have a tuple to use as the extent
else:
area = self.area
self.ax.set_extent(area, DEFAULT_LAT_LON)
self.ax.set_extent(area, ccrs.PlateCarree())

# Draw all of the plots.
for p in self.plots:
Expand Down Expand Up @@ -953,7 +950,7 @@ def plotdata(self):
y = self.griddata.metpy.y

if 'degree' in x.units:
x, y, _ = self.griddata.metpy.cartopy_crs.transform_points(DEFAULT_LAT_LON,
x, y, _ = self.griddata.metpy.cartopy_crs.transform_points(ccrs.PlateCarree(),
*np.meshgrid(x, y)).T
x = x[:, 0] % 360
y = y[0, :]
Expand Down Expand Up @@ -1233,14 +1230,14 @@ def plotdata(self):
y = self.griddata[0].metpy.y

if self.earth_relative:
x, y, _ = DEFAULT_LAT_LON.transform_points(self.griddata[0].metpy.cartopy_crs,
*np.meshgrid(x, y)).T
x, y, _ = ccrs.PlateCarree().transform_points(self.griddata[0].metpy.cartopy_crs,
*np.meshgrid(x, y)).T
x = x.T
y = y.T
else:
if 'degree' in x.units:
x, y, _ = self.griddata[0].metpy.cartopy_crs.transform_points(
DEFAULT_LAT_LON, *np.meshgrid(x, y)).T
ccrs.PlateCarree(), *np.meshgrid(x, y)).T
x = x.T % 360
y = y.T

Expand Down Expand Up @@ -1281,7 +1278,7 @@ def _build(self):
"""Build the plot by calling needed plotting methods as necessary."""
x, y, u, v = self.plotdata
if self.earth_relative:
transform = DEFAULT_LAT_LON
transform = ccrs.PlateCarree()
else:
transform = u.metpy.cartopy_crs

Expand Down
10 changes: 8 additions & 2 deletions src/metpy/plots/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
Currently this includes tools for working with CartoPy projections.
"""
import cartopy.crs as ccrs

from ..cbook import Registry
from ..plots.cartopy_utils import import_cartopy

ccrs = import_cartopy()


class CFProjection:
Expand Down Expand Up @@ -60,6 +61,11 @@ def cartopy_globe(self):

return ccrs.Globe(**kwargs)

@property
def cartopy_geodetic(self):
"""Make a `cartopy.crs.Geodetic` instance from the appropriate `cartopy.crs.Globe`."""
return ccrs.Geodetic(self.cartopy_globe)

def to_cartopy(self):
"""Convert to a CartoPy projection."""
globe = self.cartopy_globe
Expand Down
26 changes: 26 additions & 0 deletions src/metpy/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,32 @@
from .units import units


def needs_cartopy(test_func):
"""Decorate a test function or fixture as requiring CartoPy.
Will skip the decorated test, or any test using the decorated fixture, if ``cartopy`` is
unable to be imported.
"""
@functools.wraps(test_func)
def wrapped(*args, **kwargs):
pytest.importorskip('cartopy')
return test_func(*args, **kwargs)
return wrapped


def needs_pyproj(test_func):
"""Decorate a test function or fixture as requiring PyProj.
Will skip the decorated test, or any test using the decorated fixture, if ``pyproj`` is
unable to be imported.
"""
@functools.wraps(test_func)
def wrapped(*args, **kwargs):
pytest.importorskip('pyproj')
return test_func(*args, **kwargs)
return wrapped


def get_upper_air_data(date, station):
"""Get upper air observations from the test data cache.
Expand Down
11 changes: 7 additions & 4 deletions src/metpy/xarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import re
import warnings

import cartopy.crs as ccrs
import numpy as np
import xarray as xr

Expand Down Expand Up @@ -227,6 +226,11 @@ def cartopy_globe(self):
"""Return the globe belonging to the coordinate reference system (CRS)."""
return self.crs.cartopy_globe

@property
def cartopy_geodetic(self):
"""Return the Geodetic CRS associated with the native CRS globe."""
return self.crs.cartopy_geodetic

def _fixup_coordinate_map(self, coord_map):
"""Ensure sure we have coordinate variables in map, not coordinate names."""
new_coord_map = {}
Expand Down Expand Up @@ -997,8 +1001,7 @@ def _build_latitude_longitude(da):
"""Build latitude/longitude coordinates from DataArray's y/x coordinates."""
y, x = da.metpy.coordinates('y', 'x')
xx, yy = np.meshgrid(x.values, y.values)
lonlats = ccrs.Geodetic(globe=da.metpy.cartopy_globe).transform_points(
da.metpy.cartopy_crs, xx, yy)
lonlats = da.metpy.cartopy_geodetic.transform_points(da.metpy.cartopy_crs, xx, yy)
longitude = xr.DataArray(lonlats[..., 0], dims=(y.name, x.name),
coords={y.name: y, x.name: x},
attrs={'units': 'degrees_east', 'standard_name': 'longitude'})
Expand All @@ -1019,7 +1022,7 @@ def _build_y_x(da, tolerance):
'must be 2D')

# Convert to projected y/x
xxyy = da.metpy.cartopy_crs.transform_points(ccrs.Geodetic(da.metpy.cartopy_globe),
xxyy = da.metpy.cartopy_crs.transform_points(da.metpy.cartopy_geodetic,
longitude.values,
latitude.values)

Expand Down
Loading

0 comments on commit cdeb8c7

Please sign in to comment.