Skip to content

Commit 2909662

Browse files
committed
Merge branch 'master' into stringio_input
2 parents d79a0c8 + 4856d64 commit 2909662

21 files changed

+224
-83
lines changed

.github/workflows/cache_data.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
# Install GMT
2424
- name: Install GMT
2525
shell: bash -l {0}
26-
run: conda install -c conda-forge gmt=6.1.0
26+
run: conda install -c conda-forge gmt=6.1.1
2727

2828
# Download remote files
2929
- name: Download remote data

.github/workflows/ci_tests.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,11 @@ jobs:
6565

6666
# Setup Miniconda
6767
- name: Setup Miniconda
68-
uses: goanpeca/setup-miniconda@v1.6.0
68+
uses: conda-incubator/setup-miniconda@v1.7.0
6969
with:
7070
python-version: ${{ matrix.python-version }}
7171
channels: conda-forge
72+
miniconda-version: "latest"
7273

7374
# Install GMT and other required dependencies from conda-forge
7475
- name: Install GMT and required dependencies
@@ -77,7 +78,7 @@ jobs:
7778
requirements_file=full-conda-requirements.txt
7879
cat requirements.txt requirements-dev.txt > $requirements_file
7980
cat << EOF >> $requirements_file
80-
gmt=6.1.0
81+
gmt=6.1.1
8182
make
8283
codecov
8384
EOF

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ env:
2626
# The file with the listed requirements to be installed by conda
2727
- CONDA_REQUIREMENTS=requirements.txt
2828
- CONDA_REQUIREMENTS_DEV=requirements-dev.txt
29-
- CONDA_INSTALL_EXTRA="codecov twine gmt=6.1.0"
29+
- CONDA_INSTALL_EXTRA="codecov twine gmt=6.1.1"
3030
# These variables control which actions are performed in a build
3131
- DEPLOY=false
3232

CONTRIBUTING.md

+32-2
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,38 @@ Leave a comment in the PR and we'll help you out.
310310

311311
### Testing plots
312312

313-
We use the [pytest-mpl](https://github.com/matplotlib/pytest-mpl) plug-in to test plot
314-
generating code.
313+
Writing an image-based test is only slightly more difficult than a simple test.
314+
The main consideration is that you must specify the "baseline" or reference
315+
image, and compare it with a "generated" or test image. This is handled using
316+
the *decorator* functions `@check_figures_equal` and
317+
`@pytest.mark.mpl_image_compare` whose usage are further described below.
318+
319+
#### Using check_figures_equal
320+
321+
This approach draws the same figure using two different methods (the reference
322+
method and the tested method), and checks that both of them are the same.
323+
It takes two `pygmt.Figure` objects ('fig_ref' and 'fig_test'), generates a png
324+
image, and checks for the Root Mean Square (RMS) error between the two.
325+
Here's an example:
326+
327+
```python
328+
@check_figures_equal()
329+
def test_my_plotting_case(fig_ref, fig_test):
330+
"Test that my plotting function works"
331+
fig_ref.grdimage("@earth_relief_01d_g", projection="W120/15c", cmap="geo")
332+
fig_test.grdimage(grid, projection="W120/15c", cmap="geo")
333+
```
334+
335+
Note: This is the recommended way to test plots whenever possible, such as when
336+
we want to compare a reference GMT plot created from NetCDF files with one
337+
generated by PyGMT that passes through several layers of virtualfile machinery.
338+
Using this method will help save space in the git repository by not having to
339+
store baseline images as with the other method below.
340+
341+
#### Using mpl_image_compare
342+
343+
This method uses the [pytest-mpl](https://github.com/matplotlib/pytest-mpl)
344+
plug-in to test plot generating code.
315345
Every time the tests are run, `pytest-mpl` compares the generated plots with known
316346
correct ones stored in `pygmt/tests/baseline`.
317347
If your test created a `pygmt.Figure` object, you can test it by adding a *decorator* and

Makefile

+1-5
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,7 @@ test:
2929
@echo ""
3030
@cd $(TESTDIR); python -c "import $(PROJECT); $(PROJECT).show_versions()"
3131
@echo ""
32-
# There are two steps to the test here because `test_grdimage_over_dateline`
33-
# passes only when it runs before the other tests.
34-
# See also https://github.com/GenericMappingTools/pygmt/pull/476
35-
cd $(TESTDIR); pytest -m runfirst $(PYTEST_ARGS) $(PROJECT)
36-
cd $(TESTDIR); pytest -m 'not runfirst' $(PYTEST_ARGS) $(PROJECT)
32+
cd $(TESTDIR); pytest $(PYTEST_ARGS) $(PROJECT)
3733
cp $(TESTDIR)/coverage.xml .
3834
cp -r $(TESTDIR)/htmlcov .
3935
rm -r $(TESTDIR)

doc/install.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Which GMT?
3131
PyGMT requires Generic Mapping Tools (GMT) version 6 as a minimum, which is the latest
3232
released version that can be found at
3333
the `GMT official site <https://www.generic-mapping-tools.org>`__.
34-
We need the latest GMT (>=6.1.0) since there are many changes being made to GMT itself in
34+
We need the latest GMT (>=6.1.1) since there are many changes being made to GMT itself in
3535
response to the development of PyGMT, mainly the new
3636
`modern execution mode <https://docs.generic-mapping-tools.org/latest/cookbook/introduction.html#modern-and-classic-mode>`__.
3737

environment.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ channels:
55
dependencies:
66
- python=3.7
77
- pip
8-
- gmt=6.1.0
8+
- gmt=6.1.1
99
- numpy
1010
- pandas
1111
- xarray

examples/gallery/grid/track_sampling.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
fig = pygmt.Figure()
2626
# Plot the earth relief grid on Cylindrical Stereographic projection, masking land areas
27-
fig.basemap(region="d", frame=True, projection="Cyl_stere/8i")
27+
fig.basemap(region="g", frame=True, projection="Cyl_stere/150/-20/8i")
2828
fig.grdimage(grid=grid, cmap="gray")
2929
fig.coast(land="#666666")
3030
# Plot using circles (c) of 0.15cm, the sampled bathymetry points

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"scripts": {
33
"build:miniconda": "curl -o ~/miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && bash ~/miniconda.sh -b -p $HOME/miniconda",
4-
"build:pygmt": "conda env create -f environment.yml && source activate pygmt && conda install -c conda-forge -y gmt==6.1.0 && make install",
4+
"build:pygmt": "conda env create -f environment.yml && source activate pygmt && conda install -c conda-forge -y gmt==6.1.1 && make install",
55
"build:docs": "source activate pygmt && cd doc && make all && mv _build/html ../public",
66
"build": "export PATH=$HOME/miniconda/bin:$PATH && npm run build:miniconda && npm run build:pygmt && npm run build:docs"
77
}

pygmt/base_plotting.py

+10-24
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
Does not define any special non-GMT methods (savefig, show, etc).
44
"""
55
import contextlib
6-
import csv
7-
86
import numpy as np
97
import pandas as pd
108

@@ -993,28 +991,16 @@ def text(
993991
if position is not None and isinstance(position, str):
994992
kwargs["F"] += f'+c{position}+t"{text}"'
995993

996-
with GMTTempFile(suffix=".txt") as tmpfile:
997-
with Session() as lib:
998-
fname = textfiles if kind == "file" else ""
999-
if kind == "vectors":
1000-
if position is not None:
1001-
fname = ""
1002-
else:
1003-
pd.DataFrame.from_dict(
1004-
{
1005-
"x": np.atleast_1d(x),
1006-
"y": np.atleast_1d(y),
1007-
"text": np.atleast_1d(text),
1008-
}
1009-
).to_csv(
1010-
tmpfile.name,
1011-
sep="\t",
1012-
header=False,
1013-
index=False,
1014-
quoting=csv.QUOTE_NONE,
1015-
)
1016-
fname = tmpfile.name
1017-
994+
with Session() as lib:
995+
file_context = dummy_context(textfiles) if kind == "file" else ""
996+
if kind == "vectors":
997+
if position is not None:
998+
file_context = dummy_context("")
999+
else:
1000+
file_context = lib.virtualfile_from_vectors(
1001+
np.atleast_1d(x), np.atleast_1d(y), np.atleast_1d(text)
1002+
)
1003+
with file_context as fname:
10181004
arg_str = " ".join([fname, build_arg_string(kwargs)])
10191005
lib.call_module("text", arg_str)
10201006

pygmt/clib/session.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ class Session:
119119
"""
120120

121121
# The minimum version of GMT required
122-
required_version = "6.1.0"
122+
required_version = "6.1.1"
123123

124124
@property
125125
def session_pointer(self):

pygmt/exceptions.py

+6
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,9 @@ class GMTVersionError(GMTError):
4444
"""
4545
Raised when an incompatible version of GMT is being used.
4646
"""
47+
48+
49+
class GMTImageComparisonFailure(AssertionError):
50+
"""
51+
Raised when a comparison between two images fails.
52+
"""

pygmt/figure.py

-8
Original file line numberDiff line numberDiff line change
@@ -316,14 +316,6 @@ def shift_origin(self, xshift=None, yshift=None):
316316
Shift plot origin in x direction.
317317
yshift : str
318318
Shift plot origin in y direction.
319-
320-
Notes
321-
-----
322-
For GMT 6.1.0, this function can't be used as the first plotting
323-
function of :meth:`pygmt.Figure`, since it relies the *region* and
324-
*projection* settings from previous commands.
325-
326-
.. TODO: Remove the notes when PyGMT bumps to GMT>=6.1.1.
327319
"""
328320
self._preprocess()
329321
args = ["-T"]

pygmt/helpers/testing.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
Helper functions for testing.
3+
"""
4+
5+
import inspect
6+
import os
7+
8+
from matplotlib.testing.compare import compare_images
9+
10+
from ..exceptions import GMTImageComparisonFailure
11+
from ..figure import Figure
12+
13+
14+
def check_figures_equal(*, tol=0.0, result_dir="result_images"):
15+
"""
16+
Decorator for test cases that generate and compare two figures.
17+
18+
The decorated function must take two arguments, *fig_ref* and *fig_test*,
19+
and draw the reference and test images on them. After the function
20+
returns, the figures are saved and compared.
21+
22+
This decorator is practically identical to matplotlib's check_figures_equal
23+
function, but adapted for PyGMT figures. See also the original code at
24+
https://matplotlib.org/3.3.1/api/testing_api.html#
25+
matplotlib.testing.decorators.check_figures_equal
26+
27+
Parameters
28+
----------
29+
tol : float
30+
The RMS threshold above which the test is considered failed.
31+
result_dir : str
32+
The directory where the figures will be stored.
33+
34+
Examples
35+
--------
36+
37+
>>> import pytest
38+
>>> import shutil
39+
40+
>>> @check_figures_equal(result_dir="tmp_result_images")
41+
... def test_check_figures_equal(fig_ref, fig_test):
42+
... fig_ref.basemap(projection="X5c", region=[0, 5, 0, 5], frame=True)
43+
... fig_test.basemap(projection="X5c", region=[0, 5, 0, 5], frame="af")
44+
>>> test_check_figures_equal()
45+
>>> assert len(os.listdir("tmp_result_images")) == 0
46+
>>> shutil.rmtree(path="tmp_result_images") # cleanup folder if tests pass
47+
48+
>>> @check_figures_equal(result_dir="tmp_result_images")
49+
... def test_check_figures_unequal(fig_ref, fig_test):
50+
... fig_ref.basemap(projection="X5c", region=[0, 5, 0, 5], frame=True)
51+
... fig_test.basemap(projection="X5c", region=[0, 3, 0, 3], frame=True)
52+
>>> with pytest.raises(GMTImageComparisonFailure):
53+
... test_check_figures_unequal()
54+
>>> for suffix in ["", "-expected", "-failed-diff"]:
55+
... assert os.path.exists(
56+
... os.path.join(
57+
... "tmp_result_images",
58+
... f"test_check_figures_unequal{suffix}.png",
59+
... )
60+
... )
61+
>>> shutil.rmtree(path="tmp_result_images") # cleanup folder if tests pass
62+
"""
63+
64+
def decorator(func):
65+
66+
os.makedirs(result_dir, exist_ok=True)
67+
old_sig = inspect.signature(func)
68+
69+
def wrapper(*args, **kwargs):
70+
try:
71+
fig_ref = Figure()
72+
fig_test = Figure()
73+
func(*args, fig_ref=fig_ref, fig_test=fig_test, **kwargs)
74+
ref_image_path = os.path.join(
75+
result_dir, func.__name__ + "-expected.png"
76+
)
77+
test_image_path = os.path.join(result_dir, func.__name__ + ".png")
78+
fig_ref.savefig(ref_image_path)
79+
fig_test.savefig(test_image_path)
80+
81+
# Code below is adapted for PyGMT, and is originally based on
82+
# matplotlib.testing.decorators._raise_on_image_difference
83+
err = compare_images(
84+
expected=ref_image_path,
85+
actual=test_image_path,
86+
tol=tol,
87+
in_decorator=True,
88+
)
89+
if err is None: # Images are the same
90+
os.remove(ref_image_path)
91+
os.remove(test_image_path)
92+
else: # Images are not the same
93+
for key in ["actual", "expected", "diff"]:
94+
err[key] = os.path.relpath(err[key])
95+
raise GMTImageComparisonFailure(
96+
"images not close (RMS %(rms).3f):\n\t%(actual)s\n\t%(expected)s "
97+
% err
98+
)
99+
finally:
100+
del fig_ref
101+
del fig_test
102+
103+
parameters = [
104+
param
105+
for param in old_sig.parameters.values()
106+
if param.name not in {"fig_test", "fig_ref"}
107+
]
108+
new_sig = old_sig.replace(parameters=parameters)
109+
wrapper.__signature__ = new_sig
110+
111+
return wrapper
112+
113+
return decorator

pygmt/helpers/utils.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def data_kind(data, x=None, y=None, z=None):
2121
Possible types:
2222
2323
* a file name provided as 'data'
24+
* an xarray.DataArray provided as 'data'
2425
* a matrix provided as 'data'
2526
* 1D arrays x and y (and z, optionally)
2627
@@ -29,8 +30,8 @@ def data_kind(data, x=None, y=None, z=None):
2930
3031
Parameters
3132
----------
32-
data : str, 2d array, or None
33-
Data file name or numpy array.
33+
data : str, xarray.DataArray, 2d array, or None
34+
Data file name, xarray.DataArray or numpy array.
3435
x/y : 1d arrays or None
3536
x and y columns as numpy arrays.
3637
z : 1d array or None
@@ -40,18 +41,21 @@ def data_kind(data, x=None, y=None, z=None):
4041
Returns
4142
-------
4243
kind : str
43-
One of: ``'file'``, ``'matrix'``, ``'vectors'``.
44+
One of: ``'file'``, ``'grid'``, ``'matrix'``, ``'vectors'``.
4445
4546
Examples
4647
--------
4748
4849
>>> import numpy as np
50+
>>> import xarray as xr
4951
>>> data_kind(data=None, x=np.array([1, 2, 3]), y=np.array([4, 5, 6]))
5052
'vectors'
5153
>>> data_kind(data=np.arange(10).reshape((5, 2)), x=None, y=None)
5254
'matrix'
5355
>>> data_kind(data='my-data-file.txt', x=None, y=None)
5456
'file'
57+
>>> data_kind(data=xr.DataArray(np.random.rand(4, 3)))
58+
'grid'
5559
5660
"""
5761
if data is None and x is None and y is None:

pygmt/tests/test_clib.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -402,10 +402,6 @@ def test_virtualfile_from_vectors():
402402
assert output == expected
403403

404404

405-
@pytest.mark.xfail(
406-
condition=gmt_version < Version("6.1.1"),
407-
reason="GMT_Put_Strings only works for GMT 6.1.1 and above",
408-
)
409405
def test_virtualfile_from_vectors_one_string_column():
410406
"Test passing in one column with string dtype into virtual file dataset"
411407
size = 5
@@ -421,10 +417,6 @@ def test_virtualfile_from_vectors_one_string_column():
421417
assert output == expected
422418

423419

424-
@pytest.mark.xfail(
425-
condition=gmt_version < Version("6.1.1"),
426-
reason="GMT_Put_Strings only works for GMT 6.1.1 and above",
427-
)
428420
def test_virtualfile_from_vectors_two_string_columns():
429421
"Test passing in two columns of string dtype into virtual file dataset"
430422
size = 5
@@ -688,7 +680,7 @@ def test_get_default():
688680
with clib.Session() as lib:
689681
assert lib.get_default("API_GRID_LAYOUT") in ["rows", "columns"]
690682
assert int(lib.get_default("API_CORES")) >= 1
691-
assert Version(lib.get_default("API_VERSION")) >= Version("6.1.0")
683+
assert Version(lib.get_default("API_VERSION")) >= Version("6.1.1")
692684

693685

694686
def test_get_default_fails():

0 commit comments

Comments
 (0)