Skip to content

Commit 0c1bbe5

Browse files
committed
Merge branch 'main' into doc/ecosystem
2 parents db22486 + 1080ade commit 0c1bbe5

File tree

6 files changed

+97
-50
lines changed

6 files changed

+97
-50
lines changed

.github/workflows/ci_tests_dev.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
strategy:
3737
fail-fast: false
3838
matrix:
39-
os: [ubuntu-24.04, macos-14, windows-2022]
39+
os: [ubuntu-24.04, macos-15, windows-2022]
4040
gmt_git_ref: [master]
4141
timeout-minutes: 30
4242
defaults:

doc/_templates/footer.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
{%- block extrafooter %}
55
<p>
6-
Built with <a href="https://sphinx-doc.org/">Sphinx</a>
6+
Built with <a href="https://www.sphinx-doc.org/">Sphinx</a>
77
using a <a href="https://github.com/rtfd/sphinx_rtd_theme">theme</a>
88
provided by <a href="https://readthedocs.org">Read the Docs</a>
99
</p>

pygmt/accessors.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
GMT accessor for :class:`xarray.DataArray`.
33
"""
44

5+
import contextlib
56
from pathlib import Path
67

78
import xarray as xr
@@ -115,22 +116,17 @@ class GMTDataArrayAccessor:
115116

116117
def __init__(self, xarray_obj):
117118
self._obj = xarray_obj
118-
119-
self._source = self._obj.encoding.get("source")
120-
if self._source is not None and Path(self._source).exists():
121-
try:
122-
# Get grid registration and grid type from the last two columns
123-
# of the shortened summary information of `grdinfo`.
119+
# Default to Gridline registration and Cartesian grid type
120+
self._registration = 0
121+
self._gtype = 0
122+
123+
# If the source file exists, get grid registration and grid type from the last
124+
# two columns of the shortened summary information of grdinfo.
125+
if (_source := self._obj.encoding.get("source")) and Path(_source).exists():
126+
with contextlib.suppress(ValueError):
124127
self._registration, self._gtype = map(
125-
int, grdinfo(self._source, per_column="n").split()[-2:]
128+
int, grdinfo(_source, per_column="n").split()[-2:]
126129
)
127-
except ValueError:
128-
self._registration = 0 # Default to Gridline registration
129-
self._gtype = 0 # Default to Cartesian grid type
130-
else:
131-
self._registration = 0 # Default to Gridline registration
132-
self._gtype = 0 # Default to Cartesian grid type
133-
del self._source
134130

135131
@property
136132
def registration(self):

pygmt/helpers/utils.py

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -188,22 +188,27 @@ def _check_encoding(
188188

189189

190190
def data_kind(
191-
data: Any = None, required: bool = True
191+
data: Any, required: bool = True
192192
) -> Literal[
193193
"arg", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors"
194194
]:
195195
r"""
196196
Check the kind of data that is provided to a module.
197197
198-
The ``data`` argument can be in any type, but only following types are supported:
199-
200-
- a string or a :class:`pathlib.PurePath` object or a sequence of them, representing
201-
a file name or a list of file names
202-
- a 2-D or 3-D :class:`xarray.DataArray` object
203-
- a 2-D matrix
204-
- None, bool, int or float type representing an optional arguments
205-
- a geo-like Python object that implements ``__geo_interface__`` (e.g.,
206-
geopandas.GeoDataFrame or shapely.geometry)
198+
The argument passed to the ``data`` parameter can have any data type. The
199+
following data kinds are recognized and returned as ``kind``:
200+
201+
- ``"arg"``: ``data`` is ``None`` and ``required=False``, or bool, int, float,
202+
representing an optional argument, used for dealing with optional virtual files
203+
- ``"file"``: a string or a :class:`pathlib.PurePath` object or a sequence of them,
204+
representing one or more file names
205+
- ``"geojson"``: a geo-like Python object that implements ``__geo_interface__``
206+
(e.g., geopandas.GeoDataFrame or shapely.geometry)
207+
- ``"grid"``: a :class:`xarray.DataArray` object that is not 3-D
208+
- ``"image"``: a 3-D :class:`xarray.DataArray` object
209+
- ``"stringio"``: a :class:`io.StringIO` object
210+
- ``"matrix"``: anything else that is not ``None``
211+
- ``"vectors"``: ``data`` is ``None`` and ``required=True``
207212
208213
Parameters
209214
----------
@@ -287,30 +292,31 @@ def data_kind(
287292
>>> data_kind(data=None)
288293
'vectors'
289294
"""
290-
kind: Literal[
291-
"arg", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors"
292-
]
293-
if isinstance(data, str | pathlib.PurePath) or (
294-
isinstance(data, list | tuple)
295-
and all(isinstance(_file, str | pathlib.PurePath) for _file in data)
296-
):
297-
# One or more files
298-
kind = "file"
299-
elif isinstance(data, bool | int | float) or (data is None and not required):
300-
kind = "arg"
301-
elif isinstance(data, io.StringIO):
302-
kind = "stringio"
303-
elif isinstance(data, xr.DataArray):
304-
kind = "image" if len(data.dims) == 3 else "grid"
305-
elif hasattr(data, "__geo_interface__"):
306-
# geo-like Python object that implements ``__geo_interface__``
307-
# (geopandas.GeoDataFrame or shapely.geometry)
308-
kind = "geojson"
309-
elif data is not None:
310-
kind = "matrix"
311-
else:
312-
kind = "vectors"
313-
return kind
295+
match data:
296+
case str() | pathlib.PurePath(): # One file.
297+
kind = "file"
298+
case list() | tuple() if all(
299+
isinstance(_file, str | pathlib.PurePath) for _file in data
300+
): # A list/tuple of files.
301+
kind = "file"
302+
case io.StringIO():
303+
kind = "stringio"
304+
case (bool() | int() | float()) | None if not required:
305+
# An option argument, mainly for dealing with optional virtual files.
306+
kind = "arg"
307+
case xr.DataArray():
308+
# An xarray.DataArray object, representing either a grid or an image.
309+
kind = "image" if len(data.dims) == 3 else "grid"
310+
case x if hasattr(x, "__geo_interface__"):
311+
# Geo-like Python object that implements ``__geo_interface__`` (e.g.,
312+
# geopandas.GeoDataFrame or shapely.geometry).
313+
# Reference: https://gist.github.com/sgillies/2217756
314+
kind = "geojson"
315+
case x if x is not None: # Any not-None is considered as a matrix.
316+
kind = "matrix"
317+
case _: # Fall back to "vectors" if data is None and required=True.
318+
kind = "vectors"
319+
return kind # type: ignore[return-value]
314320

315321

316322
def non_ascii_to_octal(
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 6145622653eaedd2b4845400aa09ac75
3+
size: 239431
4+
hash: md5
5+
path: test_grdimage_grid_no_redunant_360.png

pygmt/tests/test_grdimage.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import numpy as np
66
import pytest
77
import xarray as xr
8+
from packaging.version import Version
89
from pygmt import Figure
10+
from pygmt.clib import __gmt_version__
911
from pygmt.datasets import load_earth_relief
1012
from pygmt.exceptions import GMTInvalidInput
1113
from pygmt.helpers.testing import check_figures_equal
@@ -252,3 +254,41 @@ def test_grdimage_imgout_fails(grid):
252254
fig.grdimage(grid, img_out="out.png")
253255
with pytest.raises(GMTInvalidInput):
254256
fig.grdimage(grid, A="out.png")
257+
258+
259+
@pytest.mark.xfail(
260+
condition=Version(__gmt_version__) <= Version("6.5.0"),
261+
reason="Upstream bug fixed in https://github.com/GenericMappingTools/gmt/pull/8554",
262+
)
263+
@pytest.mark.mpl_image_compare()
264+
def test_grdimage_grid_no_redunant_360():
265+
"""
266+
Test that global grids with and without redundant 360/0 longitude values work.
267+
268+
Test for https://github.com/GenericMappingTools/pygmt/issues/3331.
269+
"""
270+
# Global grid [-180, 180, -90, 90] with redundant longitude at 180/-180
271+
da1 = load_earth_relief(region=[-180, 180, -90, 90])
272+
# Global grid [0, 360, -90, 90] with redundant longitude at 360/0
273+
da2 = load_earth_relief(region=[0, 360, -90, 90])
274+
275+
# Global grid [-180, 179, -90, 90] without redundant longitude at 180/-180
276+
da3 = da1[:, 0:360]
277+
da3.gmt.registration, da3.gmt.gtype = 0, 1
278+
assert da3.shape == (181, 360)
279+
assert da3.lon.to_numpy().min() == -180.0
280+
assert da3.lon.to_numpy().max() == 179.0
281+
282+
# Global grid [0, 359, -90, 90] without redundant longitude at 360/0
283+
da4 = da2[:, 0:360]
284+
da4.gmt.registration, da4.gmt.gtype = 0, 1
285+
assert da4.shape == (181, 360)
286+
assert da4.lon.to_numpy().min() == 0.0
287+
assert da4.lon.to_numpy().max() == 359.0
288+
289+
fig = Figure()
290+
kwdict = {"projection": "W120/10c", "region": "g", "frame": "+tlon=120"}
291+
fig.grdimage(da3, **kwdict)
292+
fig.shift_origin(xshift="w+2c")
293+
fig.grdimage(da4, **kwdict)
294+
return fig

0 commit comments

Comments
 (0)