From 043a49c903707b86c84db2c8d7b31585bc2a51b7 Mon Sep 17 00:00:00 2001 From: Michael Grund <23025878+michaelgrund@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:10:01 +0100 Subject: [PATCH 01/11] DOC: Make PyGMT naming in install.md consistent (#3578) --- doc/install.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/install.md b/doc/install.md index 0283a95086c..e5bbf018e82 100644 --- a/doc/install.md +++ b/doc/install.md @@ -231,7 +231,7 @@ python -m pip install pygmt ``` ::: {tip} -You can also run `python -m pip install pygmt[all]` to install pygmt with all of its +You can also run `python -m pip install pygmt[all]` to install PyGMT with all of its optional dependencies. ::: @@ -303,8 +303,8 @@ C:\Users\USERNAME\Miniforge3\envs\pygmt\Library\bin\ ### `ModuleNotFoundError` in Jupyter notebook environment -If you can successfully import pygmt in a Python interpreter or IPython, but get a -`ModuleNotFoundError` when importing pygmt in Jupyter, you may need to activate your +If you can successfully import PyGMT in a Python interpreter or IPython, but get a +`ModuleNotFoundError` when importing PyGMT in Jupyter, you may need to activate your `pygmt` virtual environment (using `mamba activate pygmt` or `conda activate pygmt`) and install a `pygmt` kernel following the commands below: ``` From f41974d9a5e02c56dfe60f6c5b06afbb000bcbd8 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 5 Nov 2024 03:13:59 +0800 Subject: [PATCH 02/11] Refactor to specify numpy dtypes in a consistent way (#3577) --- examples/gallery/symbols/datetime_inputs.py | 4 +++- examples/tutorials/advanced/date_time_charts.py | 4 +++- pygmt/clib/session.py | 6 +++--- pygmt/datatypes/dataset.py | 2 +- pygmt/helpers/tempfile.py | 4 +--- pygmt/src/info.py | 2 +- pygmt/src/text.py | 4 ++-- pygmt/tests/test_clib_dataarray_to_matrix.py | 6 +++--- pygmt/tests/test_clib_put_strings.py | 4 ++-- pygmt/tests/test_clib_put_vector.py | 14 +++++++------- pygmt/tests/test_geopandas.py | 3 ++- pygmt/tests/test_info.py | 4 ++-- pygmt/tests/test_plot.py | 2 +- pygmt/tests/test_triangulate.py | 4 ++-- 14 files changed, 33 insertions(+), 30 deletions(-) diff --git a/examples/gallery/symbols/datetime_inputs.py b/examples/gallery/symbols/datetime_inputs.py index 640a075107d..939e90ccf1d 100644 --- a/examples/gallery/symbols/datetime_inputs.py +++ b/examples/gallery/symbols/datetime_inputs.py @@ -40,7 +40,9 @@ ) # numpy.datetime64 types -x = np.array(["2010-06-01", "2011-06-01T12", "2012-01-01T12:34:56"], dtype="datetime64") +x = np.array( + ["2010-06-01", "2011-06-01T12", "2012-01-01T12:34:56"], dtype=np.datetime64 +) y = [1, 2, 3] fig.plot(x=x, y=y, style="c0.4c", pen="1p", fill="red3") diff --git a/examples/tutorials/advanced/date_time_charts.py b/examples/tutorials/advanced/date_time_charts.py index 3b91339fbe9..1fcea100384 100644 --- a/examples/tutorials/advanced/date_time_charts.py +++ b/examples/tutorials/advanced/date_time_charts.py @@ -222,7 +222,9 @@ # before passing it as an argument. However, ``np.array`` objects use less memory and # allow developers to specify data types. -x = np.array(["2010-06-01", "2011-06-01T12", "2012-01-01T12:34:56"], dtype="datetime64") +x = np.array( + ["2010-06-01", "2011-06-01T12", "2012-01-01T12:34:56"], dtype=np.datetime64 +) y = [2, 7, 5] fig = pygmt.Figure() diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index bdd8a13a1d3..81afe6efb7c 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -910,12 +910,12 @@ def _check_dtype_and_dim(self, array: np.ndarray, ndim: int) -> int: Examples -------- >>> import numpy as np - >>> data = np.array([1, 2, 3], dtype="float64") + >>> data = np.array([1, 2, 3], dtype=np.float64) >>> with Session() as lib: ... gmttype = lib._check_dtype_and_dim(data, ndim=1) ... gmttype == lib["GMT_DOUBLE"] True - >>> data = np.ones((5, 2), dtype="float32") + >>> data = np.ones((5, 2), dtype=np.float32) >>> with Session() as lib: ... gmttype = lib._check_dtype_and_dim(data, ndim=2) ... gmttype == lib["GMT_FLOAT"] @@ -1505,7 +1505,7 @@ def virtualfile_from_vectors( strings = np.array( [" ".join(vals) for vals in zip(*string_arrays, strict=True)] ) - strings = np.asanyarray(a=strings, dtype=str) + strings = np.asanyarray(a=strings, dtype=np.str_) self.put_strings( dataset, family="GMT_IS_VECTOR|GMT_IS_DUPLICATE", strings=strings ) diff --git a/pygmt/datatypes/dataset.py b/pygmt/datatypes/dataset.py index 3f2202052e0..354eefe543e 100644 --- a/pygmt/datatypes/dataset.py +++ b/pygmt/datatypes/dataset.py @@ -168,7 +168,7 @@ def to_strings(self) -> np.ndarray[Any, np.dtype[np.str_]]: ) warnings.warn(msg, category=RuntimeWarning, stacklevel=1) textvector = [item if item is not None else b"" for item in textvector] - return np.char.decode(textvector) if textvector else np.array([], dtype=str) + return np.char.decode(textvector) if textvector else np.array([], dtype=np.str_) def to_dataframe( self, diff --git a/pygmt/helpers/tempfile.py b/pygmt/helpers/tempfile.py index 2c56d3847c6..b32e422f28d 100644 --- a/pygmt/helpers/tempfile.py +++ b/pygmt/helpers/tempfile.py @@ -50,7 +50,7 @@ class GMTTempFile: ... np.savetxt(tmpfile.name, (x, y, z), fmt="%.1f") ... lines = tmpfile.read() ... print(lines) - ... nx, ny, nz = tmpfile.loadtxt(unpack=True, dtype=float) + ... nx, ny, nz = tmpfile.loadtxt(unpack=True, dtype=np.float64) ... print(nx, ny, nz) 0.0 1.0 2.0 0.0 1.0 2.0 @@ -143,9 +143,7 @@ def tempfile_from_geojson(geojson): # 32-bit integer overflow issue. Related issues: # https://github.com/geopandas/geopandas/issues/967#issuecomment-842877704 # https://github.com/GenericMappingTools/pygmt/issues/2497 - int32_info = np.iinfo(np.int32) - if Version(gpd.__version__).major < 1: # GeoPandas v0.x # The default engine 'fiona' supports the 'schema' parameter. if geojson.index.name is None: diff --git a/pygmt/src/info.py b/pygmt/src/info.py index f9fa73c62f7..308a84256bf 100644 --- a/pygmt/src/info.py +++ b/pygmt/src/info.py @@ -98,6 +98,6 @@ def info(data, **kwargs): result = np.loadtxt(result.splitlines()) except ValueError: # Load non-numerical outputs in str type, e.g. for datetime - result = np.loadtxt(result.splitlines(), dtype="str") + result = np.loadtxt(result.splitlines(), dtype=np.str_) return result diff --git a/pygmt/src/text.py b/pygmt/src/text.py index 24835fc1881..2ed475c9ac2 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -228,7 +228,7 @@ def text_( # noqa: PLR0912 if name == "angle": extra_arrays.append(arg) else: - extra_arrays.append(np.asarray(arg, dtype=str)) + extra_arrays.append(np.asarray(arg, dtype=np.str_)) # If an array of transparency is given, GMT will read it from the last numerical # column per data record. @@ -237,7 +237,7 @@ def text_( # noqa: PLR0912 kwargs["t"] = True # Append text to the last column. Text must be passed in as str type. - text = np.asarray(text, dtype=str) + text = np.asarray(text, dtype=np.str_) encoding = _check_encoding("".join(text.flatten())) if encoding != "ascii": text = np.vectorize(non_ascii_to_octal, excluded="encoding")( diff --git a/pygmt/tests/test_clib_dataarray_to_matrix.py b/pygmt/tests/test_clib_dataarray_to_matrix.py index fdaf44ec06a..ec8e5e27e5a 100644 --- a/pygmt/tests/test_clib_dataarray_to_matrix.py +++ b/pygmt/tests/test_clib_dataarray_to_matrix.py @@ -76,7 +76,7 @@ def test_dataarray_to_matrix_dims_fails(): Check that it fails for > 2 dims. """ # Make a 3-D regular grid - data = np.ones((10, 12, 11), dtype="float32") + data = np.ones((10, 12, 11), dtype=np.float32) x = np.arange(11) y = np.arange(12) z = np.arange(10) @@ -90,7 +90,7 @@ def test_dataarray_to_matrix_irregular_inc_warning(): Check that it warns for variable increments, see also https://github.com/GenericMappingTools/pygmt/issues/1468. """ - data = np.ones((4, 5), dtype="float64") + data = np.ones((4, 5), dtype=np.float64) x = np.linspace(0, 1, 5) y = np.logspace(2, 3, 4) grid = xr.DataArray(data, coords=[("y", y), ("x", x)]) @@ -103,7 +103,7 @@ def test_dataarray_to_matrix_zero_inc_fails(): """ Check that dataarray_to_matrix fails for zero increments grid. """ - data = np.ones((5, 5), dtype="float32") + data = np.ones((5, 5), dtype=np.float32) x = np.linspace(0, 1, 5) y = np.zeros_like(x) grid = xr.DataArray(data, coords=[("y", y), ("x", x)]) diff --git a/pygmt/tests/test_clib_put_strings.py b/pygmt/tests/test_clib_put_strings.py index 171c6c2b60e..81b27b06117 100644 --- a/pygmt/tests/test_clib_put_strings.py +++ b/pygmt/tests/test_clib_put_strings.py @@ -24,7 +24,7 @@ def test_put_strings(): ) x = np.array([1, 2, 3, 4, 5], dtype=np.int32) y = np.array([6, 7, 8, 9, 10], dtype=np.int32) - strings = np.array(["a", "bc", "defg", "hijklmn", "opqrst"], dtype=str) + strings = np.array(["a", "bc", "defg", "hijklmn", "opqrst"], dtype=np.str_) lib.put_vector(dataset, column=lib["GMT_X"], vector=x) lib.put_vector(dataset, column=lib["GMT_Y"], vector=y) lib.put_strings( @@ -60,5 +60,5 @@ def test_put_strings_fails(): lib.put_strings( dataset=None, family="GMT_IS_VECTOR|GMT_IS_DUPLICATE", - strings=np.empty(shape=(3,), dtype=str), + strings=np.empty(shape=(3,), dtype=np.str_), ) diff --git a/pygmt/tests/test_clib_put_vector.py b/pygmt/tests/test_clib_put_vector.py index 7e1dd0d9310..46b199727f0 100644 --- a/pygmt/tests/test_clib_put_vector.py +++ b/pygmt/tests/test_clib_put_vector.py @@ -208,11 +208,11 @@ def test_put_vector_invalid_dtype(): for dtype in [ np.bool_, np.bytes_, - np.csingle, - np.cdouble, - np.clongdouble, - np.half, + np.float16, np.longdouble, + np.complex64, + np.complex128, + np.clongdouble, np.object_, ]: with clib.Session() as lib: @@ -224,7 +224,7 @@ def test_put_vector_invalid_dtype(): ) data = np.array([37, 12, 556], dtype=dtype) with pytest.raises(GMTInvalidInput, match="Unsupported numpy data type"): - lib.put_vector(dataset, column=1, vector=data) + lib.put_vector(dataset, column=0, vector=data) def test_put_vector_wrong_column(): @@ -238,7 +238,7 @@ def test_put_vector_wrong_column(): mode="GMT_CONTAINER_ONLY", dim=[1, 3, 0, 0], # ncolumns, nrows, dtype, unused ) - data = np.array([37, 12, 556], dtype="float32") + data = np.array([37, 12, 556], dtype=np.float32) with pytest.raises(GMTCLibError): lib.put_vector(dataset, column=1, vector=data) @@ -254,6 +254,6 @@ def test_put_vector_2d_fails(): mode="GMT_CONTAINER_ONLY", dim=[1, 6, 0, 0], # ncolumns, nrows, dtype, unused ) - data = np.array([[37, 12, 556], [37, 12, 556]], dtype="int32") + data = np.array([[37, 12, 556], [37, 12, 556]], dtype=np.int32) with pytest.raises(GMTInvalidInput): lib.put_vector(dataset, column=0, vector=data) diff --git a/pygmt/tests/test_geopandas.py b/pygmt/tests/test_geopandas.py index 336a076d64c..a98f1d3a014 100644 --- a/pygmt/tests/test_geopandas.py +++ b/pygmt/tests/test_geopandas.py @@ -2,6 +2,7 @@ Test integration with geopandas. """ +import numpy as np import numpy.testing as npt import pandas as pd import pytest @@ -221,7 +222,7 @@ def test_geopandas_plot_int64_as_float(gdf_ridge): gdf = gdf_ridge.copy() factor = 2**32 # Convert NPOINTS column to int64 type and make big integers - gdf["NPOINTS"] = gdf.NPOINTS.astype(dtype="int64") + gdf["NPOINTS"] = gdf.NPOINTS.astype(dtype=np.int64) gdf["NPOINTS"] *= factor # Make sure the column is bigger than the largest 32-bit integer diff --git a/pygmt/tests/test_info.py b/pygmt/tests/test_info.py index a14030cb772..3ac9f27c4e1 100644 --- a/pygmt/tests/test_info.py +++ b/pygmt/tests/test_info.py @@ -76,7 +76,7 @@ def test_info_2d_list(): @pytest.mark.parametrize( "dtype", - ["int64", pytest.param("int64[pyarrow]", marks=skip_if_no(package="pyarrow"))], + [np.int64, pytest.param("int64[pyarrow]", marks=skip_if_no(package="pyarrow"))], ) def test_info_series(dtype): """ @@ -90,7 +90,7 @@ def test_info_series(dtype): @pytest.mark.parametrize( "dtype", [ - "float64", + np.float64, pytest.param("float64[pyarrow]", marks=skip_if_no(package="pyarrow")), ], ) diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index 48608cf51ce..721b7841307 100644 --- a/pygmt/tests/test_plot.py +++ b/pygmt/tests/test_plot.py @@ -446,7 +446,7 @@ def test_plot_datetime(): # numpy.datetime64 types x = np.array( - ["2010-06-01", "2011-06-01T12", "2012-01-01T12:34:56"], dtype="datetime64" + ["2010-06-01", "2011-06-01T12", "2012-01-01T12:34:56"], dtype=np.datetime64 ) y = [1.0, 2.0, 3.0] fig.plot(x=x, y=y, style="c0.2c", pen="1p") diff --git a/pygmt/tests/test_triangulate.py b/pygmt/tests/test_triangulate.py index 97c29a273e6..f0a47c6e4ae 100644 --- a/pygmt/tests/test_triangulate.py +++ b/pygmt/tests/test_triangulate.py @@ -45,7 +45,7 @@ def fixture_expected_dataframe(): [3, 4, 2], [9, 3, 8], ], - dtype=float, + dtype=np.float64, ) @@ -117,7 +117,7 @@ def test_delaunay_triples_outfile(dataframe, expected_dataframe): assert result is None # return value is None assert Path(tmpfile.name).stat().st_size > 0 temp_df = pd.read_csv( - filepath_or_buffer=tmpfile.name, sep="\t", header=None, dtype=float + filepath_or_buffer=tmpfile.name, sep="\t", header=None, dtype=np.float64 ) pd.testing.assert_frame_equal(left=temp_df, right=expected_dataframe) From 3ad94d9bf4c0a73c776d047932c54119ffb7807d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 5 Nov 2024 11:03:15 +0800 Subject: [PATCH 03/11] Figure.savefig: Add a new test for the show parameter and simplify existing tests. (#3568) * Get rid of try-except in test_figure_savefig_geotiff * Simplify test_figure_savefig_unknown_extension * Simplify test_figure_savefig with unittest.mock * Add one more test for Figure.savefig(show=True) --- pygmt/tests/test_figure.py | 96 ++++++++++++-------------------------- 1 file changed, 30 insertions(+), 66 deletions(-) diff --git a/pygmt/tests/test_figure.py b/pygmt/tests/test_figure.py index 3118f675232..f91509dd8f2 100644 --- a/pygmt/tests/test_figure.py +++ b/pygmt/tests/test_figure.py @@ -17,6 +17,7 @@ from pygmt.helpers import GMTTempFile _HAS_IPYTHON = bool(importlib.util.find_spec("IPython")) +_HAS_RIOXARRAY = bool(importlib.util.find_spec("rioxarray")) def test_figure_region(): @@ -100,7 +101,7 @@ def test_figure_savefig_geotiff(): geofname = Path("test_figure_savefig_geotiff.tiff") fig.savefig(geofname) assert geofname.exists() - # The .pgw should not exist + # The .pgw file should not exist assert not geofname.with_suffix(".pgw").exists() # Save as TIFF @@ -109,7 +110,7 @@ def test_figure_savefig_geotiff(): assert fname.exists() # Check if a TIFF is georeferenced or not - try: + if _HAS_RIOXARRAY: import rioxarray from rasterio.errors import NotGeoreferencedWarning from rasterio.transform import Affine @@ -147,8 +148,6 @@ def test_figure_savefig_geotiff(): a=1.0, b=0.0, c=0.0, d=0.0, e=1.0, f=0.0 ) assert len(record) == 1 - except ImportError: - pass geofname.unlink() fname.unlink() @@ -170,9 +169,7 @@ def test_figure_savefig_unknown_extension(): """ fig = Figure() fig.basemap(region="10/70/-300/800", projection="X3i/5i", frame="af") - prefix = "test_figure_savefig_unknown_extension" - fmt = "test" - fname = f"{prefix}.{fmt}" + fname = "test_figure_savefig_unknown_extension.test" with pytest.raises(GMTInvalidInput, match="Unknown extension '.test'."): fig.savefig(fname) @@ -223,69 +220,23 @@ def test_figure_savefig(): """ Check if the arguments being passed to psconvert are correct. """ - kwargs_saved = [] - - def mock_psconvert(*args, **kwargs): # noqa: ARG001 - """ - Just record the arguments. - """ - kwargs_saved.append(kwargs) - - fig = Figure() - fig.psconvert = mock_psconvert - prefix = "test_figure_savefig" - - fname = f"{prefix}.png" - fig.savefig(fname) - assert kwargs_saved[-1] == { - "prefix": prefix, - "fmt": "g", - "crop": True, - "Qt": 2, - "Qg": 2, - } - - fname = f"{prefix}.pdf" - fig.savefig(fname) - assert kwargs_saved[-1] == { - "prefix": prefix, - "fmt": "f", - "crop": True, - "Qt": 2, - "Qg": 2, + common_kwargs = {"prefix": prefix, "crop": True, "Qt": 2, "Qg": 2} + expected_kwargs = { + "png": {"fmt": "g", **common_kwargs}, + "pdf": {"fmt": "f", **common_kwargs}, + "eps": {"fmt": "e", **common_kwargs}, + "kml": {"fmt": "g", "W": "+k", **common_kwargs}, } - fname = f"{prefix}.png" - fig.savefig(fname, transparent=True) - assert kwargs_saved[-1] == { - "prefix": prefix, - "fmt": "G", - "crop": True, - "Qt": 2, - "Qg": 2, - } - - fname = f"{prefix}.eps" - fig.savefig(fname) - assert kwargs_saved[-1] == { - "prefix": prefix, - "fmt": "e", - "crop": True, - "Qt": 2, - "Qg": 2, - } + with patch.object(Figure, "psconvert") as mock_psconvert: + fig = Figure() + for fmt, expected in expected_kwargs.items(): + fig.savefig(f"{prefix}.{fmt}") + mock_psconvert.assert_called_with(**expected) - fname = f"{prefix}.kml" - fig.savefig(fname) - assert kwargs_saved[-1] == { - "prefix": prefix, - "fmt": "g", - "crop": True, - "Qt": 2, - "Qg": 2, - "W": "+k", - } + fig.savefig(f"{prefix}.png", transparent=True) + mock_psconvert.assert_called_with(fmt="G", **common_kwargs) def test_figure_savefig_worldfile(): @@ -309,6 +260,19 @@ def test_figure_savefig_worldfile(): fig.savefig(fname=imgfile.name, worldfile=True) +def test_figure_savefig_show(): + """ + Check if the external viewer is launched when the show parameter is specified. + """ + fig = Figure() + fig.basemap(region=[0, 1, 0, 1], projection="X1c/1c", frame=True) + prefix = "test_figure_savefig_show" + with patch("pygmt.figure.launch_external_viewer") as mock_viewer: + with GMTTempFile(prefix=prefix, suffix=".png") as imgfile: + fig.savefig(imgfile.name, show=True) + assert mock_viewer.call_count == 1 + + @pytest.mark.skipif(not _HAS_IPYTHON, reason="run when IPython is installed") def test_figure_show(): """ From f91475d6611999e84c1e65a702db1d65b3e254a8 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 5 Nov 2024 11:16:06 +0800 Subject: [PATCH 04/11] Support 1-D/2-D numpy arrays with longlong and ulonglong dtype (#3566) --- pygmt/clib/session.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index 81afe6efb7c..10c8770adaa 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -88,10 +88,12 @@ np.int16: "GMT_SHORT", np.int32: "GMT_INT", np.int64: "GMT_LONG", + np.longlong: "GMT_LONG", np.uint8: "GMT_UCHAR", np.uint16: "GMT_USHORT", np.uint32: "GMT_UINT", np.uint64: "GMT_ULONG", + np.ulonglong: "GMT_ULONG", np.float32: "GMT_FLOAT", np.float64: "GMT_DOUBLE", np.timedelta64: "GMT_LONG", @@ -948,8 +950,9 @@ def put_vector(self, dataset: ctp.c_void_p, column: int, vector: np.ndarray): The dataset must be created by :meth:`pygmt.clib.Session.create_data` first with ``family="GMT_IS_DATASET|GMT_VIA_VECTOR"``. - Not all numpy dtypes are supported, only: int8, int16, int32, int64, uint8, - uint16, uint32, uint64, float32, float64, str\_, and datetime64. + Not all numpy dtypes are supported, only: int8, int16, int32, int64, longlong, + uint8, uint16, uint32, uint64, ulonglong, float32, float64, str\_, datetime64, + and timedelta64. .. warning:: The numpy array must be C contiguous in memory. Use @@ -1060,8 +1063,8 @@ def put_matrix(self, dataset: ctp.c_void_p, matrix: np.ndarray, pad: int = 0): The dataset must be created by :meth:`pygmt.clib.Session.create_data` first with ``family="GMT_IS_DATASET|GMT_VIA_MATRIX"``. - Not all numpy dtypes are supported, only: int8, int16, int32, int64, uint8, - uint16, uint32, uint64, float32, and float64. + Not all numpy dtypes are supported, only: int8, int16, int32, int64, longlong, + uint8, uint16, uint32, uint64, ulonglong, float32, and float64. .. warning:: The numpy array must be C contiguous in memory. Use From 1a62c7244b91fc41ce8b134bd48db190d2496419 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 5 Nov 2024 17:14:58 +0800 Subject: [PATCH 05/11] Add the private _to_numpy function to convert anything to a numpy array (#3581) --- pygmt/clib/conversion.py | 68 +++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/pygmt/clib/conversion.py b/pygmt/clib/conversion.py index 40d90ed71c4..af8eb3458d4 100644 --- a/pygmt/clib/conversion.py +++ b/pygmt/clib/conversion.py @@ -132,6 +132,52 @@ def dataarray_to_matrix( return matrix, region, inc +def _to_numpy(data: Any) -> np.ndarray: + """ + Convert an array-like object to a C contiguous NumPy array. + + The function aims to convert any array-like objects (e.g., Python lists or tuples, + NumPy arrays with various dtypes, pandas.Series with NumPy/pandas/PyArrow dtypes, + PyArrow arrays with various dtypes) to a NumPy array. + + The function is internally used in the ``vectors_to_arrays`` function, which is + responsible for converting a sequence of vectors to a list of C contiguous NumPy + arrays. Thus, the function uses the :numpy:func:`numpy.ascontiguousarray` function + rather than the :numpy:func:`numpy.asarray`/:numpy::func:`numpy.asanyarray` + functions, to ensure the returned NumPy array is C contiguous. + + Parameters + ---------- + data + The array-like object to convert. + + Returns + ------- + array + The C contiguous NumPy array. + """ + # Mapping of unsupported dtypes to the expected NumPy dtype. + dtypes: dict[str, type] = { + "date32[day][pyarrow]": np.datetime64, + "date64[ms][pyarrow]": np.datetime64, + } + + if ( + hasattr(data, "isna") + and data.isna().any() + and Version(pd.__version__) < Version("2.2") + ): + # Workaround for dealing with pd.NA with pandas < 2.2. + # Bug report at: https://github.com/GenericMappingTools/pygmt/issues/2844 + # Following SPEC0, pandas 2.1 will be dropped in 2025 Q3, so it's likely + # we can remove the workaround in PyGMT v0.17.0. + array = np.ascontiguousarray(data.astype(float)) + else: + vec_dtype = str(getattr(data, "dtype", "")) + array = np.ascontiguousarray(data, dtype=dtypes.get(vec_dtype)) + return array + + def vectors_to_arrays(vectors: Sequence[Any]) -> list[np.ndarray]: """ Convert 1-D vectors (scalars, lists, or array-like) to C contiguous 1-D arrays. @@ -171,27 +217,7 @@ def vectors_to_arrays(vectors: Sequence[Any]) -> list[np.ndarray]: >>> all(i.ndim == 1 for i in arrays) True """ - dtypes = { - "date32[day][pyarrow]": np.datetime64, - "date64[ms][pyarrow]": np.datetime64, - } - arrays = [] - for vector in vectors: - if ( - hasattr(vector, "isna") - and vector.isna().any() - and Version(pd.__version__) < Version("2.2") - ): - # Workaround for dealing with pd.NA with pandas < 2.2. - # Bug report at: https://github.com/GenericMappingTools/pygmt/issues/2844 - # Following SPEC0, pandas 2.1 will be dropped in 2025 Q3, so it's likely - # we can remove the workaround in PyGMT v0.17.0. - array = np.ascontiguousarray(vector.astype(float)) - else: - vec_dtype = str(getattr(vector, "dtype", "")) - array = np.ascontiguousarray(vector, dtype=dtypes.get(vec_dtype)) - arrays.append(array) - return arrays + return [_to_numpy(vector) for vector in vectors] def sequence_to_ctypes_array( From 7f20520c30781cbe89a014524e72fd3567712aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yvonne=20Fr=C3=B6hlich?= <94163266+yvonnefroehlich@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:21:23 +0100 Subject: [PATCH 06/11] DOC/installation guide: Fix links to examples (#3586) --- doc/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/install.md b/doc/install.md index e5bbf018e82..fc9251d5a1e 100644 --- a/doc/install.md +++ b/doc/install.md @@ -57,8 +57,8 @@ import pygmt pygmt.show_versions() ``` -You are now ready to make your first figure! Start by looking at our [Intro](examples/intro/index.rst), -[Tutorials](examples/tutorials/index.rst), and [Gallery](examples/gallery/index.rst). Good luck! +You are now ready to make your first figure! Start by looking at our [Intro](intro/index.rst), +[Tutorials](tutorials/index.rst), and [Gallery](gallery/index.rst). Good luck! :::{note} The sections below provide more detailed, step by step instructions to install and test From dd713dd37cc649b52bd33429ee6b65127c3e8aae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 08:22:05 +0800 Subject: [PATCH 07/11] Build(deps): Bump pypa/gh-action-pypi-publish from 1.10.3 to 1.12.0 (#3588) Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.10.3 to 1.12.0. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.10.3...v1.12.0) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish-to-pypi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 85e866fabe7..b04619fccf8 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -75,10 +75,10 @@ jobs: ls -lh dist/ - name: Publish to Test PyPI - uses: pypa/gh-action-pypi-publish@v1.10.3 + uses: pypa/gh-action-pypi-publish@v1.12.0 with: repository-url: https://test.pypi.org/legacy/ - name: Publish to PyPI if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@v1.10.3 + uses: pypa/gh-action-pypi-publish@v1.12.0 From 897ee399a0f250a6a2fdce2a6e9ec9dbe6735dce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 08:23:35 +0800 Subject: [PATCH 08/11] Build(deps): Bump CodSpeedHQ/action from 3.0.1 to 3.1.0 (#3589) Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 3.0.1 to 3.1.0. - [Release notes](https://github.com/codspeedhq/action/releases) - [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codspeedhq/action/compare/v3.0.1...v3.1.0) --- updated-dependencies: - dependency-name: CodSpeedHQ/action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/benchmarks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 1e4d693c236..f8f6b9d6bb0 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -87,7 +87,7 @@ jobs: # Run the benchmark tests - name: Run benchmarks - uses: CodSpeedHQ/action@v3.0.1 + uses: CodSpeedHQ/action@v3.1.0 with: # 'bash -el -c' is needed to use the custom shell. # See https://github.com/CodSpeedHQ/action/issues/65. From 0169b9d8625f26cda9f2cc5f9f702a96e7bd8555 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 6 Nov 2024 10:31:51 +0800 Subject: [PATCH 09/11] Test timedelta64 dtype arrays with various date/time units (#3567) --- pygmt/tests/test_clib_put_vector.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pygmt/tests/test_clib_put_vector.py b/pygmt/tests/test_clib_put_vector.py index 46b199727f0..968e09bf87c 100644 --- a/pygmt/tests/test_clib_put_vector.py +++ b/pygmt/tests/test_clib_put_vector.py @@ -172,10 +172,12 @@ def test_put_vector_string_dtype(): def test_put_vector_timedelta64_dtype(): """ - Passing timedelta64 type vectors with various time units (year, month, - week, day, hour, minute, second, millisecond, microsecond) to a dataset. + Passing timedelta64 type vectors with various date/time units to a dataset. + + Valid date/time units can be found at + https://numpy.org/devdocs/reference/arrays.datetime.html#datetime-units. """ - for unit in ["Y", "M", "W", "D", "h", "m", "s", "ms", "μs"]: + for unit in ["Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns", "ps", "fs", "as"]: with clib.Session() as lib, GMTTempFile() as tmp_file: dataset = lib.create_data( family="GMT_IS_DATASET|GMT_VIA_VECTOR", From 38694be57b209405a292440684ed54cfa4c4c7e9 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 6 Nov 2024 11:39:26 +0800 Subject: [PATCH 10/11] Figure.show: Raise ImportError instead of GMTError if IPython is not installed but required (#3580) --- pygmt/figure.py | 16 +++++++++------- pygmt/tests/test_figure.py | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pygmt/figure.py b/pygmt/figure.py index 9c0a7a7a763..0b2faea00d0 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -17,7 +17,7 @@ import numpy as np from pygmt.clib import Session -from pygmt.exceptions import GMTError, GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import launch_external_viewer, unique_name @@ -331,11 +331,12 @@ def show( match method: case "notebook": if not _HAS_IPYTHON: - raise GMTError( + msg = ( "Notebook display is selected, but IPython is not available. " "Make sure you have IPython installed, " "or run the script in a Jupyter notebook." ) + raise ImportError(msg) png = self._preview( fmt="png", dpi=dpi, anti_alias=True, as_bytes=True, **kwargs ) @@ -344,14 +345,15 @@ def show( pdf = self._preview( fmt="pdf", dpi=dpi, anti_alias=False, as_bytes=False, **kwargs ) - launch_external_viewer(pdf, waiting=waiting) # type: ignore[arg-type] + launch_external_viewer(pdf, waiting=waiting) case "none": pass # Do nothing case _: - raise GMTInvalidInput( - f"Invalid display method '{method}'. Valid values are 'external', " - "'notebook', 'none' or None." + msg = ( + f"Invalid display method '{method}'. " + "Valid values are 'external', 'notebook', 'none' or None." ) + raise GMTInvalidInput(msg) def _preview(self, fmt: str, dpi: int, as_bytes: bool = False, **kwargs): """ @@ -400,7 +402,7 @@ def _repr_html_(self): html = '' return html.format(image=base64_png.decode("utf-8"), width=500) - from pygmt.src import ( # type: ignore [misc] + from pygmt.src import ( # type: ignore[misc] basemap, coast, colorbar, diff --git a/pygmt/tests/test_figure.py b/pygmt/tests/test_figure.py index f91509dd8f2..10076b69437 100644 --- a/pygmt/tests/test_figure.py +++ b/pygmt/tests/test_figure.py @@ -12,7 +12,7 @@ import numpy.testing as npt import pytest from pygmt import Figure, set_display -from pygmt.exceptions import GMTError, GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput from pygmt.figure import SHOW_CONFIG, _get_default_display_method from pygmt.helpers import GMTTempFile @@ -321,7 +321,7 @@ def test_figure_show_notebook_error_without_ipython(): """ fig = Figure() fig.basemap(region=[0, 1, 2, 3], frame=True) - with pytest.raises(GMTError): + with pytest.raises(ImportError): fig.show(method="notebook") @@ -361,7 +361,7 @@ def test_set_display(self): assert mock_viewer.call_count == 0 assert mock_display.call_count == 1 else: - with pytest.raises(GMTError): + with pytest.raises(ImportError): fig.show() # Test the "external" display method From 40edcf7123537754376f7e1960cdd64a6e3cf8ac Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 6 Nov 2024 11:39:42 +0800 Subject: [PATCH 11/11] Figure.savefig: Clarify that 'transparent' also works for the PNG file associated with the KML format (#3579) --- pygmt/figure.py | 32 +++++++++++++++----------------- pygmt/tests/test_figure.py | 11 ++++++++++- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/pygmt/figure.py b/pygmt/figure.py index 0b2faea00d0..4163ab52eb1 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -135,7 +135,7 @@ def region(self) -> np.ndarray: wesn = lib.extract_region() return wesn - def savefig( # noqa: PLR0912 + def savefig( self, fname: str | PurePath, transparent: bool = False, @@ -177,7 +177,8 @@ def savefig( # noqa: PLR0912 The desired figure file name, including the extension. See the list of supported formats and their extensions above. transparent - Use a transparent background for the figure. Only valid for PNG format. + Use a transparent background for the figure. Only valid for PNG format and + the PNG file asscoiated with KML format. crop Crop the figure canvas (page) to the plot area. anti_alias @@ -203,9 +204,9 @@ def savefig( # noqa: PLR0912 "bmp": "b", "eps": "e", "jpg": "j", - "kml": "g", + "kml": "G" if transparent is True else "g", "pdf": "f", - "png": "g", + "png": "G" if transparent is True else "g", "ppm": "m", "tif": "t", "tiff": None, # GeoTIFF doesn't need the -T option @@ -226,14 +227,12 @@ def savefig( # noqa: PLR0912 msg = "Extension '.ps' is not supported. Use '.eps' or '.pdf' instead." raise GMTInvalidInput(msg) case ext if ext not in fmts: - raise GMTInvalidInput(f"Unknown extension '.{ext}'.") - - fmt = fmts[ext] - if transparent: - if fmt != "g": - msg = f"Transparency unavailable for '{ext}', only for png." + msg = f"Unknown extension '.{ext}'." raise GMTInvalidInput(msg) - fmt = fmt.upper() + + if transparent and ext not in {"kml", "png"}: + msg = f"Transparency unavailable for '{ext}', only for png and kml." + raise GMTInvalidInput(msg) if anti_alias: kwargs["Qt"] = 2 kwargs["Qg"] = 2 @@ -244,18 +243,17 @@ def savefig( # noqa: PLR0912 raise GMTInvalidInput(msg) kwargs["W"] = True - # pytest-mpl v0.17.0 added the "metadata" parameter to Figure.savefig, which - # is not recognized. So remove it before calling Figure.psconvert. + # pytest-mpl v0.17.0 added the "metadata" parameter to Figure.savefig, which is + # not recognized. So remove it before calling Figure.psconvert. kwargs.pop("metadata", None) - self.psconvert(prefix=prefix, fmt=fmt, crop=crop, **kwargs) + self.psconvert(prefix=prefix, fmt=fmts[ext], crop=crop, **kwargs) - # Remove the .pgw world file if exists. - # Not necessary after GMT 6.5.0. + # Remove the .pgw world file if exists. Not necessary after GMT 6.5.0. # See upstream fix https://github.com/GenericMappingTools/gmt/pull/7865 if ext == "tiff": fname.with_suffix(".pgw").unlink(missing_ok=True) - # Rename if file extension doesn't match the input file suffix + # Rename if file extension doesn't match the input file suffix. if ext != suffix[1:]: fname.with_suffix("." + ext).rename(fname) diff --git a/pygmt/tests/test_figure.py b/pygmt/tests/test_figure.py index 10076b69437..1831961ced4 100644 --- a/pygmt/tests/test_figure.py +++ b/pygmt/tests/test_figure.py @@ -196,12 +196,21 @@ def test_figure_savefig_transparent(): fname = f"{prefix}.{fmt}" with pytest.raises(GMTInvalidInput): fig.savefig(fname, transparent=True) - # png should not raise an error + + # PNG should support transparency and should not raise an error. fname = Path(f"{prefix}.png") fig.savefig(fname, transparent=True) assert fname.exists() fname.unlink() + # The companion PNG file with KML format should also support transparency. + fname = Path(f"{prefix}.kml") + fig.savefig(fname, transparent=True) + assert fname.exists() + fname.unlink() + assert fname.with_suffix(".png").exists() + fname.with_suffix(".png").unlink() + def test_figure_savefig_filename_with_spaces(): """