From 0c82b3cad9c3afffd4f389653eff67f797a9f12c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 3 Oct 2024 14:42:07 +0800 Subject: [PATCH 01/18] data_kind: Refactor the if-else statements into if-return statements --- pygmt/helpers/utils.py | 67 ++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 24a938df518..59928482091 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -187,7 +187,7 @@ def _check_encoding( return "ISOLatin1+" -def data_kind( +def data_kind( # noqa: PLR0911 data: Any = None, required: bool = True ) -> Literal[ "arg", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors" @@ -195,15 +195,18 @@ def data_kind( r""" Check the kind of data that is provided to a module. - The ``data`` argument can be in any type, but only following types are supported: + The ``data`` argument can be in any type. Following data kinds are recognized: - - a string or a :class:`pathlib.PurePath` object or a sequence of them, representing - a file name or a list of file names - - a 2-D or 3-D :class:`xarray.DataArray` object - - a 2-D matrix - - None, bool, int or float type representing an optional arguments - - a geo-like Python object that implements ``__geo_interface__`` (e.g., - geopandas.GeoDataFrame or shapely.geometry) + - ``"arg"``: data is ``None`` and ``required=False``, or bool, int, float, + representing an optional argument, used for dealing with optional virtual files + - ``"file"``: a string or a :class:`pathlib.PurePath` object or a sequence of them, + representing one or more file names + - ``"geojson"``: a geo-like Python object that implements ``__geo_interface__`` + (e.g., geopandas.GeoDataFrame or shapely.geometry) + - ``"grid"``: a :class:`xarray.DataArray` object that is not 3-D + - ``"image"``: a 3-D :class:`xarray.DataArray` object + - ``"matrix"``: anything that is not None + - ``"vectors"``: data is ``None`` and ``required=True`` Parameters ---------- @@ -287,30 +290,36 @@ def data_kind( >>> data_kind(data=None) 'vectors' """ - kind: Literal[ - "arg", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors" - ] + # One file or a list/tuple of files. if isinstance(data, str | pathlib.PurePath) or ( isinstance(data, list | tuple) and all(isinstance(_file, str | pathlib.PurePath) for _file in data) ): - # One or more files - kind = "file" - elif isinstance(data, bool | int | float) or (data is None and not required): - kind = "arg" - elif isinstance(data, io.StringIO): - kind = "stringio" - elif isinstance(data, xr.DataArray): - kind = "image" if len(data.dims) == 3 else "grid" - elif hasattr(data, "__geo_interface__"): - # geo-like Python object that implements ``__geo_interface__`` - # (geopandas.GeoDataFrame or shapely.geometry) - kind = "geojson" - elif data is not None: - kind = "matrix" - else: - kind = "vectors" - return kind + return "file" + + # A StringIO object. + if isinstance(data, io.StringIO): + return "stringio" + + # An option argument, mainly for dealing optional virtual files. + if isinstance(data, bool | int | float) or (data is None and not required): + return "arg" + + # An xarray.DataArray object, representing a grid or an image. + if isinstance(data, xr.DataArray): + return "image" if len(data.dims) == 3 else "grid" + + # Geo-like Python object that implements ``__geo_interface__`` (e.g., + # geopandas.GeoDataFrame or shapely.geometry). + # Reference: https://gist.github.com/sgillies/2217756 + if hasattr(data, "__geo_interface__"): + return "geojson" + + # Any not-None is considered as a matrix. + if data is not None: + return "matrix" + + return "vectors" def non_ascii_to_octal( From 808755d7747dcbcd93b8a0d495b11990d10deb6f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 3 Oct 2024 17:21:14 +0800 Subject: [PATCH 02/18] data_kind: Now 'matrix' represents a 2-D numpy array and unrecognizd data types fall back to 'vectors' --- pygmt/clib/session.py | 36 ++++++++++++++++++------------------ pygmt/helpers/utils.py | 35 +++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index 376d441746a..42edfe44bc8 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -1790,10 +1790,7 @@ def virtualfile_in( # noqa: PLR0912 "grid": self.virtualfile_from_grid, "image": tempfile_from_image, "stringio": self.virtualfile_from_stringio, - # Note: virtualfile_from_matrix is not used because a matrix can be - # converted to vectors instead, and using vectors allows for better - # handling of string type inputs (e.g. for datetime data types) - "matrix": self.virtualfile_from_vectors, + "matrix": self.virtualfile_from_matrix, "vectors": self.virtualfile_from_vectors, }[kind] @@ -1810,29 +1807,32 @@ def virtualfile_in( # noqa: PLR0912 warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) _data = (data,) if not isinstance(data, pathlib.PurePath) else (str(data),) elif kind == "vectors": - _data = [np.atleast_1d(x), np.atleast_1d(y)] - if z is not None: - _data.append(np.atleast_1d(z)) - if extra_arrays: - _data.extend(extra_arrays) - elif kind == "matrix": # turn 2-D arrays into list of vectors - if hasattr(data, "items") and not hasattr(data, "to_frame"): + if data is None: + # data is None, so data must be given via x/y/z. + _data = [np.atleast_1d(x), np.atleast_1d(y)] + if z is not None: + _data.append(np.atleast_1d(z)) + if extra_arrays: + _data.extend(extra_arrays) + elif hasattr(data, "items") and not hasattr(data, "to_frame"): # pandas.DataFrame or xarray.Dataset types. # pandas.Series will be handled below like a 1-D numpy.ndarray. _data = [array for _, array in data.items()] - elif hasattr(data, "ndim") and data.ndim == 2 and data.dtype.kind in "iuf": - # Just use virtualfile_from_matrix for 2-D numpy.ndarray - # which are signed integer (i), unsigned integer (u) or - # floating point (f) types - _virtualfile_from = self.virtualfile_from_matrix - _data = (data,) else: # Python list, tuple, numpy.ndarray, and pandas.Series types _data = np.atleast_2d(np.asanyarray(data).T) + elif kind == "matrix": + # GMT can only accept a 2-D matrix which are signed integer (i), unsigned + # integer (u) or floating point (f) types. For other data types, we need to + # use virtualfile_from_vectors instead, which turns the matrix into list of + # vectors and allows for better handling of string type inputs (e.g. for + # datetime data types). + _data = (data,) + if data.dtype.kind not in "iuf": + _virtualfile_from = self.virtualfile_from_vectors # Finally create the virtualfile from the data, to be passed into GMT file_context = _virtualfile_from(*_data) - return file_context def virtualfile_from_data( diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 59928482091..8b5aa12dcc2 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -14,6 +14,7 @@ from collections.abc import Iterable, Sequence from typing import Any, Literal +import numpy as np import xarray as xr from pygmt.encodings import charset from pygmt.exceptions import GMTInvalidInput @@ -205,8 +206,10 @@ def data_kind( # noqa: PLR0911 (e.g., geopandas.GeoDataFrame or shapely.geometry) - ``"grid"``: a :class:`xarray.DataArray` object that is not 3-D - ``"image"``: a 3-D :class:`xarray.DataArray` object - - ``"matrix"``: anything that is not None - - ``"vectors"``: data is ``None`` and ``required=True`` + - ``"matrix"``: a 2-D :class:`numpy.ndarray` object + - ``"vectors"``: fallback to ``"vectors"`` for any unrecognized data. Common data + types include, a :class:`pandas.DataFrame` object, a dictionary with array-like + values, a 1-D/3-D :class:`numpy.ndarray` object, or array-like objects. Parameters ---------- @@ -266,27 +269,27 @@ def data_kind( # noqa: PLR0911 The "matrix"`` kind: - >>> data_kind(data=np.arange(10)) # 1-D numpy.ndarray - 'matrix' >>> data_kind(data=np.arange(10).reshape((5, 2))) # 2-D numpy.ndarray 'matrix' + + The "vectors" kind: + + >>> data_kind(data=np.arange(10)) # 1-D numpy.ndarray + 'vectors' >>> data_kind(data=np.arange(60).reshape((3, 4, 5))) # 3-D numpy.ndarray - 'matrix' + 'vectors' >>> data_kind(xr.DataArray(np.arange(12), name="x").to_dataset()) # xarray.Dataset - 'matrix' + 'vectors' >>> data_kind(data=[1, 2, 3]) # 1-D sequence - 'matrix' + 'vectors' >>> data_kind(data=[[1, 2, 3], [4, 5, 6]]) # sequence of sequences - 'matrix' + 'vectors' >>> data_kind(data={"x": [1, 2, 3], "y": [4, 5, 6]}) # dictionary - 'matrix' + 'vectors' >>> data_kind(data=pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})) # pd.DataFrame - 'matrix' + 'vectors' >>> data_kind(data=pd.Series([1, 2, 3], name="x")) # pd.Series - 'matrix' - - The "vectors" kind: - + 'vectors' >>> data_kind(data=None) 'vectors' """ @@ -315,8 +318,8 @@ def data_kind( # noqa: PLR0911 if hasattr(data, "__geo_interface__"): return "geojson" - # Any not-None is considered as a matrix. - if data is not None: + # A 2-D numpy.ndarray. + if isinstance(data, np.ndarray) and data.ndim == 2: return "matrix" return "vectors" From 0eb4f8fcc3b7e29d33ee4d9dad75d8edc5e5a8a8 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 3 Oct 2024 17:27:16 +0800 Subject: [PATCH 03/18] Make 'data' a required parameter --- pygmt/helpers/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 59928482091..0fe90a886b3 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -188,7 +188,7 @@ def _check_encoding( def data_kind( # noqa: PLR0911 - data: Any = None, required: bool = True + data: Any, required: bool = True ) -> Literal[ "arg", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors" ]: From 9891b2c687c0ff2f12bfe6b5c357a3f99bb97082 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 3 Oct 2024 17:37:03 +0800 Subject: [PATCH 04/18] Fix x2sys_cross as pd.DataFrame is 'vectors' kind now --- pygmt/src/x2sys_cross.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/x2sys_cross.py b/pygmt/src/x2sys_cross.py index 79daf523fec..382f560f6f7 100644 --- a/pygmt/src/x2sys_cross.py +++ b/pygmt/src/x2sys_cross.py @@ -195,7 +195,7 @@ def x2sys_cross( match data_kind(track): case "file": file_contexts.append(contextlib.nullcontext(track)) - case "matrix": + case "vectors": # find suffix (-E) of trackfiles used (e.g. xyz, csv, etc) from # $X2SYS_HOME/TAGNAME/TAGNAME.tag file tagfile = Path( From a9d094cfe0b777abc679701f361c31f9f1716590 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 3 Oct 2024 17:41:17 +0800 Subject: [PATCH 05/18] Fix legend as now 'vectors' doesn't mean data is None --- pygmt/src/legend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/legend.py b/pygmt/src/legend.py index ed34bc0d797..ddc26cbd2eb 100644 --- a/pygmt/src/legend.py +++ b/pygmt/src/legend.py @@ -91,7 +91,7 @@ def legend( kwargs["F"] = box kind = data_kind(spec) - if kind not in {"vectors", "file", "stringio"}: # kind="vectors" means spec is None + if spec is not None and kind not in {"file", "stringio"}: raise GMTInvalidInput(f"Unrecognized data type: {type(spec)}") if kind == "file" and is_nonstr_iter(spec): raise GMTInvalidInput("Only one legend specification file is allowed.") From a0e18482500a475caa2141b6e52f8101918f4620 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 3 Oct 2024 18:06:36 +0800 Subject: [PATCH 06/18] data_kind: data is None and required now returns the 'none' kind --- pygmt/clib/session.py | 19 ++++++++++--------- pygmt/helpers/utils.py | 13 +++++++++++-- pygmt/src/legend.py | 2 +- pygmt/src/plot.py | 2 +- pygmt/src/plot3d.py | 2 +- pygmt/src/text.py | 4 ++-- 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index 42edfe44bc8..bd14b6d9397 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -1776,7 +1776,7 @@ def virtualfile_in( # noqa: PLR0912 if check_kind == "raster": valid_kinds += ("grid", "image") elif check_kind == "vector": - valid_kinds += ("matrix", "vectors", "geojson") + valid_kinds += ("none", "matrix", "vectors", "geojson") if kind not in valid_kinds: raise GMTInvalidInput( f"Unrecognized data type for {check_kind}: {type(data)}" @@ -1791,6 +1791,7 @@ def virtualfile_in( # noqa: PLR0912 "image": tempfile_from_image, "stringio": self.virtualfile_from_stringio, "matrix": self.virtualfile_from_matrix, + "none": self.virtualfile_from_vectors, "vectors": self.virtualfile_from_vectors, }[kind] @@ -1806,15 +1807,15 @@ def virtualfile_in( # noqa: PLR0912 ) warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) _data = (data,) if not isinstance(data, pathlib.PurePath) else (str(data),) + elif kind == "none": + # data is None, so data must be given via x/y/z. + _data = [np.atleast_1d(x), np.atleast_1d(y)] + if z is not None: + _data.append(np.atleast_1d(z)) + if extra_arrays: + _data.extend(extra_arrays) elif kind == "vectors": - if data is None: - # data is None, so data must be given via x/y/z. - _data = [np.atleast_1d(x), np.atleast_1d(y)] - if z is not None: - _data.append(np.atleast_1d(z)) - if extra_arrays: - _data.extend(extra_arrays) - elif hasattr(data, "items") and not hasattr(data, "to_frame"): + if hasattr(data, "items") and not hasattr(data, "to_frame"): # pandas.DataFrame or xarray.Dataset types. # pandas.Series will be handled below like a 1-D numpy.ndarray. _data = [array for _, array in data.items()] diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 8b5aa12dcc2..79701bbd297 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -191,13 +191,15 @@ def _check_encoding( def data_kind( # noqa: PLR0911 data: Any = None, required: bool = True ) -> Literal[ - "arg", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors" + "arg", "file", "geojson", "grid", "image", "matrix", "none", "stringio", "vectors" ]: r""" Check the kind of data that is provided to a module. The ``data`` argument can be in any type. Following data kinds are recognized: + - ``"none"`: data is ``None`` and ``required=True``. It means the data is given via + a series of vectors like x/y/z - ``"arg"``: data is ``None`` and ``required=False``, or bool, int, float, representing an optional argument, used for dealing with optional virtual files - ``"file"``: a string or a :class:`pathlib.PurePath` object or a sequence of them, @@ -290,9 +292,16 @@ def data_kind( # noqa: PLR0911 'vectors' >>> data_kind(data=pd.Series([1, 2, 3], name="x")) # pd.Series 'vectors' + + The "none" kind: + >>> data_kind(data=None) - 'vectors' + 'none' """ + # data should be given via a series of vectors like x/y/z + if data is None and required: + return "none" + # One file or a list/tuple of files. if isinstance(data, str | pathlib.PurePath) or ( isinstance(data, list | tuple) diff --git a/pygmt/src/legend.py b/pygmt/src/legend.py index ddc26cbd2eb..197dad9b347 100644 --- a/pygmt/src/legend.py +++ b/pygmt/src/legend.py @@ -91,7 +91,7 @@ def legend( kwargs["F"] = box kind = data_kind(spec) - if spec is not None and kind not in {"file", "stringio"}: + if kind not in {"none", "file", "stringio"}: raise GMTInvalidInput(f"Unrecognized data type: {type(spec)}") if kind == "file" and is_nonstr_iter(spec): raise GMTInvalidInput("Only one legend specification file is allowed.") diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py index 61db357bfde..655870f8904 100644 --- a/pygmt/src/plot.py +++ b/pygmt/src/plot.py @@ -206,7 +206,7 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs): kind = data_kind(data) extra_arrays = [] - if kind == "vectors": # Add more columns for vectors input + if kind == "none": # Add more columns for vectors input # Parameters for vector styles if ( kwargs.get("S") is not None diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py index 7c7a78f2ab3..2dde128e412 100644 --- a/pygmt/src/plot3d.py +++ b/pygmt/src/plot3d.py @@ -184,7 +184,7 @@ def plot3d( kind = data_kind(data) extra_arrays = [] - if kind == "vectors": # Add more columns for vectors input + if kind == "none": # Add more columns for vectors input # Parameters for vector styles if ( kwargs.get("S") is not None diff --git a/pygmt/src/text.py b/pygmt/src/text.py index 8a12031069b..c11c9643a60 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -193,7 +193,7 @@ def text_( # noqa: PLR0912 raise GMTInvalidInput("'text' can't be None or array when 'position' is given.") if textfiles is not None and text is not None: raise GMTInvalidInput("'text' can't be specified when 'textfiles' is given.") - if kind == "vectors" and text is None: + if kind == "none" and text is None: raise GMTInvalidInput("Must provide text with x/y pairs.") # Arguments that can accept arrays. @@ -217,7 +217,7 @@ def text_( # noqa: PLR0912 extra_arrays = [] confdict = {} - if kind == "vectors": + if kind == "none": for arg, flag, name in array_args: if is_nonstr_iter(arg): kwargs["F"] += flag From 3d8be4d5982008a58c38cfd1aa879c9f64a524e2 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 3 Oct 2024 19:26:12 +0800 Subject: [PATCH 07/18] Add docstrings for stringio --- pygmt/helpers/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 0fe90a886b3..4c2451762ce 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -195,7 +195,7 @@ def data_kind( # noqa: PLR0911 r""" Check the kind of data that is provided to a module. - The ``data`` argument can be in any type. Following data kinds are recognized: + The ``data`` argument can be in any types. Following data kinds are recognized: - ``"arg"``: data is ``None`` and ``required=False``, or bool, int, float, representing an optional argument, used for dealing with optional virtual files @@ -205,6 +205,7 @@ def data_kind( # noqa: PLR0911 (e.g., geopandas.GeoDataFrame or shapely.geometry) - ``"grid"``: a :class:`xarray.DataArray` object that is not 3-D - ``"image"``: a 3-D :class:`xarray.DataArray` object + - ``"stringio"``: a :class:`io.StringIO` object - ``"matrix"``: anything that is not None - ``"vectors"``: data is ``None`` and ``required=True`` From 6954c5d47f81852a72eaeb75913500bfd96f14c9 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 7 Oct 2024 13:43:59 +0800 Subject: [PATCH 08/18] Fix docstrings --- pygmt/helpers/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index c505c45be81..2885ca3878a 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -209,9 +209,10 @@ def data_kind( - ``"image"``: a 3-D :class:`xarray.DataArray` object - ``"stringio"``: a :class:`io.StringIO` object - ``"matrix"``: a 2-D :class:`numpy.ndarray` object - - ``"vectors"``: fallback to ``"vectors"`` for any unrecognized data. Common data - types include, a :class:`pandas.DataFrame` object, a dictionary with array-like - values, a 1-D/3-D :class:`numpy.ndarray` object, or array-like objects. + - ``"vectors"``: ``data`` is ``None`` and ``required=True``, or any unrecognized + data. Common data types include, a :class:`pandas.DataFrame` object, a dictionary + with array-like values, a 1-D/3-D :class:`numpy.ndarray` object, or array-like + objects. Parameters ---------- From a1e67d3432c0818fcc1cf2e3eb299a7a83e515b2 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 11 Oct 2024 08:26:46 +0800 Subject: [PATCH 09/18] Rename 'none' kind to 'empty' --- pygmt/clib/session.py | 8 ++++---- pygmt/helpers/utils.py | 18 +++++++++--------- pygmt/src/legend.py | 2 +- pygmt/src/plot.py | 2 +- pygmt/src/plot3d.py | 2 +- pygmt/src/text.py | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index b369d0f2a47..fde15176c69 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -1775,7 +1775,7 @@ def virtualfile_in( # noqa: PLR0912 if check_kind == "raster": valid_kinds += ("grid", "image") elif check_kind == "vector": - valid_kinds += ("none", "matrix", "vectors", "geojson") + valid_kinds += ("empty", "matrix", "vectors", "geojson") if kind not in valid_kinds: raise GMTInvalidInput( f"Unrecognized data type for {check_kind}: {type(data)}" @@ -1783,14 +1783,14 @@ def virtualfile_in( # noqa: PLR0912 # Decide which virtualfile_from_ function to use _virtualfile_from = { - "file": contextlib.nullcontext, "arg": contextlib.nullcontext, + "empty": self.virtualfile_from_vectors, + "file": contextlib.nullcontext, "geojson": tempfile_from_geojson, "grid": self.virtualfile_from_grid, "image": tempfile_from_image, "stringio": self.virtualfile_from_stringio, "matrix": self.virtualfile_from_matrix, - "none": self.virtualfile_from_vectors, "vectors": self.virtualfile_from_vectors, }[kind] @@ -1806,7 +1806,7 @@ def virtualfile_in( # noqa: PLR0912 ) warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) _data = (data,) if not isinstance(data, pathlib.PurePath) else (str(data),) - elif kind == "none": + elif kind == "empty": # data is None, so data must be given via x/y/z. _data = [x, y] if z is not None: diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 586710f8ac4..74e2655dc67 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -191,7 +191,7 @@ def _check_encoding( def data_kind( data: Any, required: bool = True ) -> Literal[ - "arg", "file", "geojson", "grid", "image", "matrix", "none", "stringio", "vectors" + "arg", "empty", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors" ]: r""" Check the kind of data that is provided to a module. @@ -199,10 +199,10 @@ def data_kind( The argument passed to the ``data`` parameter can have any data type. The following data kinds are recognized and returned as ``kind``: - - ``"none"`: ``data`` is ``None`` and ``required=True``. It means the data is given - via a series of vectors like x/y/z - ``"arg"``: ``data`` is ``None`` and ``required=False``, or bool, int, float, representing an optional argument, used for dealing with optional virtual files + - ``"empty"`: ``data`` is ``None`` and ``required=True``. It means the data is given + via a series of vectors like x/y/z - ``"file"``: a string or a :class:`pathlib.PurePath` object or a sequence of them, representing one or more file names - ``"geojson"``: a geo-like Python object that implements ``__geo_interface__`` @@ -244,6 +244,11 @@ def data_kind( >>> data_kind(data=None, required=False) 'arg' + The "empty" kind: + + >>> data_kind(data=None) + 'empty' + The "file" kind: >>> [data_kind(data=data) for data in ("file.txt", ("file1.txt", "file2.txt"))] @@ -295,15 +300,10 @@ def data_kind( 'vectors' >>> data_kind(data=pd.Series([1, 2, 3], name="x")) # pd.Series 'vectors' - - The "none" kind: - - >>> data_kind(data=None) - 'none' """ match data: case None if required: # No data provided and required=True. - kind = "none" + kind = "empty" case str() | pathlib.PurePath(): # One file. kind = "file" case list() | tuple() if all( diff --git a/pygmt/src/legend.py b/pygmt/src/legend.py index 197dad9b347..f6e2d61f34f 100644 --- a/pygmt/src/legend.py +++ b/pygmt/src/legend.py @@ -91,7 +91,7 @@ def legend( kwargs["F"] = box kind = data_kind(spec) - if kind not in {"none", "file", "stringio"}: + if kind not in {"empty", "file", "stringio"}: raise GMTInvalidInput(f"Unrecognized data type: {type(spec)}") if kind == "file" and is_nonstr_iter(spec): raise GMTInvalidInput("Only one legend specification file is allowed.") diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py index 655870f8904..4565d02e0db 100644 --- a/pygmt/src/plot.py +++ b/pygmt/src/plot.py @@ -206,7 +206,7 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs): kind = data_kind(data) extra_arrays = [] - if kind == "none": # Add more columns for vectors input + if kind == "empty": # Add more columns for vectors input # Parameters for vector styles if ( kwargs.get("S") is not None diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py index 2dde128e412..31695a82464 100644 --- a/pygmt/src/plot3d.py +++ b/pygmt/src/plot3d.py @@ -184,7 +184,7 @@ def plot3d( kind = data_kind(data) extra_arrays = [] - if kind == "none": # Add more columns for vectors input + if kind == "empty": # Add more columns for vectors input # Parameters for vector styles if ( kwargs.get("S") is not None diff --git a/pygmt/src/text.py b/pygmt/src/text.py index b59eebcd34a..9f0f80ccde1 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -193,7 +193,7 @@ def text_( # noqa: PLR0912 raise GMTInvalidInput("'text' can't be None or array when 'position' is given.") if textfiles is not None and text is not None: raise GMTInvalidInput("'text' can't be specified when 'textfiles' is given.") - if kind == "none" and text is None: + if kind == "empty" and text is None: raise GMTInvalidInput("Must provide text with x/y pairs.") # Arguments that can accept arrays. @@ -217,7 +217,7 @@ def text_( # noqa: PLR0912 extra_arrays = [] confdict = {} - if kind == "none": + if kind == "empty": for arg, flag, name in array_args: if is_nonstr_iter(arg): kwargs["F"] += flag From ea9ddaa8ffb4d18d81e40d42647a13d01745d426 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 14 Oct 2024 23:09:08 +0800 Subject: [PATCH 10/18] Update pygmt/helpers/utils.py Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> --- pygmt/helpers/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index ee3410bfe9a..9e9a5fb542a 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -246,7 +246,7 @@ def data_kind( The "empty" kind: - >>> data_kind(data=None) + >>> data_kind(data=None, required=True) 'empty' The "file" kind: From 6f55375c7c36aa5447cf881ad9c7f43109f14e78 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 11 Oct 2024 08:05:23 +0800 Subject: [PATCH 11/18] clib: Switch the order of if-conditions to improve the Session.call_module performance (#3502) Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> --- pygmt/clib/session.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index fde15176c69..29e008d5e03 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -629,14 +629,7 @@ def call_module(self, module: str, args: str | list[str]): # 'args' can be (1) a single string or (2) a list of strings. argv: bytes | ctp.Array[ctp.c_char_p] | None - if isinstance(args, str): - # 'args' is a single string that contains whitespace-separated arguments. - # In this way, we need to correctly handle option arguments that contain - # whitespaces or quotation marks. It's used in PyGMT <= v0.11.0 but is no - # longer recommended. - mode = self["GMT_MODULE_CMD"] - argv = args.encode() - elif isinstance(args, list): + if isinstance(args, list): # 'args' is a list of strings and each string contains a module argument. # In this way, GMT can correctly handle option arguments with whitespaces or # quotation marks. This is the preferred way to pass arguments to the GMT @@ -644,16 +637,21 @@ def call_module(self, module: str, args: str | list[str]): mode = len(args) # 'mode' is the number of arguments. # Pass a null pointer if no arguments are specified. argv = strings_to_ctypes_array(args) if mode != 0 else None + elif isinstance(args, str): + # 'args' is a single string that contains whitespace-separated arguments. + # In this way, we need to correctly handle option arguments that contain + # whitespaces or quotation marks. It's used in PyGMT <= v0.11.0 but is no + # longer recommended. + mode = self["GMT_MODULE_CMD"] + argv = args.encode() else: - raise GMTInvalidInput( - "'args' must be either a string or a list of strings." - ) + msg = "'args' must either be a list of strings (recommended) or a string." + raise GMTInvalidInput(msg) status = c_call_module(self.session_pointer, module.encode(), mode, argv) if status != 0: - raise GMTCLibError( - f"Module '{module}' failed with status code {status}:\n{self._error_message}" - ) + msg = f"Module '{module}' failed with status code {status}:\n{self._error_message}" + raise GMTCLibError(msg) def create_data( self, From 3252988f70df71c33bf6c6aa64deb40a970c7c4f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 11 Oct 2024 13:59:22 +0800 Subject: [PATCH 12/18] Fix the conversion error for pandas.Series with missing values in pandas<=2.1 (#3505) Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> --- pygmt/clib/conversion.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pygmt/clib/conversion.py b/pygmt/clib/conversion.py index 0739d767567..ee80ec1a187 100644 --- a/pygmt/clib/conversion.py +++ b/pygmt/clib/conversion.py @@ -7,6 +7,8 @@ from collections.abc import Sequence import numpy as np +import pandas as pd +from packaging.version import Version from pygmt.exceptions import GMTInvalidInput @@ -178,6 +180,10 @@ def vectors_to_arrays(vectors): >>> [i.ndim for i in data] # Check that they are 1-D arrays [1, 1, 1] + >>> series = pd.Series(data=[0, 4, pd.NA, 8, 6], dtype=pd.Int32Dtype()) + >>> vectors_to_arrays([series]) + [array([ 0., 4., nan, 8., 6.])] + >>> import datetime >>> import pytest >>> pa = pytest.importorskip("pyarrow") @@ -205,8 +211,20 @@ def vectors_to_arrays(vectors): } arrays = [] for vector in vectors: - vec_dtype = str(getattr(vector, "dtype", "")) - arrays.append(np.ascontiguousarray(vector, dtype=dtypes.get(vec_dtype))) + 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 From 003d8a189109a459d3baf224b10ee2f02e8c379e Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 11 Oct 2024 14:05:33 +0800 Subject: [PATCH 13/18] Add type hints for GMT anchor codes (#3459) --- pygmt/_typing.py | 8 ++++++++ pygmt/helpers/utils.py | 4 ++-- pygmt/src/text.py | 13 ++++++++----- pygmt/src/timestamp.py | 3 ++- 4 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 pygmt/_typing.py diff --git a/pygmt/_typing.py b/pygmt/_typing.py new file mode 100644 index 00000000000..bbc7d596c65 --- /dev/null +++ b/pygmt/_typing.py @@ -0,0 +1,8 @@ +""" +Type aliases for type hints. +""" + +from typing import Literal + +# Anchor codes +AnchorCode = Literal["TL", "TC", "TR", "ML", "MC", "MR", "BL", "BC", "BR"] diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 74e2655dc67..ee3410bfe9a 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -11,7 +11,7 @@ import sys import time import webbrowser -from collections.abc import Iterable, Sequence +from collections.abc import Iterable, Mapping, Sequence from typing import Any, Literal import numpy as np @@ -406,7 +406,7 @@ def non_ascii_to_octal( def build_arg_list( # noqa: PLR0912 kwdict: dict[str, Any], - confdict: dict[str, str] | None = None, + confdict: Mapping[str, Any] | None = None, infile: str | pathlib.PurePath | Sequence[str | pathlib.PurePath] | None = None, outfile: str | pathlib.PurePath | None = None, ) -> list[str]: diff --git a/pygmt/src/text.py b/pygmt/src/text.py index 9f0f80ccde1..24835fc1881 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -2,7 +2,10 @@ text - Plot text on a figure. """ +from collections.abc import Sequence + import numpy as np +from pygmt._typing import AnchorCode from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import ( @@ -44,11 +47,11 @@ def text_( # noqa: PLR0912 textfiles=None, x=None, y=None, - position=None, + position: AnchorCode | None = None, text=None, angle=None, font=None, - justify=None, + justify: bool | None | AnchorCode | Sequence[AnchorCode] = None, **kwargs, ): r""" @@ -90,14 +93,14 @@ def text_( # noqa: PLR0912 x/y : float or 1-D arrays The x and y coordinates, or an array of x and y coordinates to plot the text. - position : str + position Set reference point on the map for the text by using x, y coordinates extracted from ``region`` instead of providing them through ``x``/``y``. Specify with a two-letter (order independent) code, chosen from: - * Horizontal: **L**\ (eft), **C**\ (entre), **R**\ (ight) * Vertical: **T**\ (op), **M**\ (iddle), **B**\ (ottom) + * Horizontal: **L**\ (eft), **C**\ (entre), **R**\ (ight) For example, ``position="TL"`` plots the text at the Top Left corner of the map. @@ -116,7 +119,7 @@ def text_( # noqa: PLR0912 font. If no font info is explicitly given (i.e. ``font=True``), then the input to ``textfiles`` must have this information in one of its columns. - justify : str, bool or list of str + justify Set the alignment which refers to the part of the text string that will be mapped onto the (x, y) point. Choose a two-letter combination of **L**, **C**, **R** (for left, center, or right) and diff --git a/pygmt/src/timestamp.py b/pygmt/src/timestamp.py index 9818a289974..3db9ff694d9 100644 --- a/pygmt/src/timestamp.py +++ b/pygmt/src/timestamp.py @@ -6,6 +6,7 @@ from collections.abc import Sequence from packaging.version import Version +from pygmt._typing import AnchorCode from pygmt.clib import Session, __gmt_version__ from pygmt.helpers import build_arg_list, kwargs_to_strings @@ -17,7 +18,7 @@ def timestamp( self, text: str | None = None, label: str | None = None, - justify: str = "BL", + justify: AnchorCode = "BL", offset: float | str | Sequence[float | str] = ("-54p", "-54p"), font: str = "Helvetica,black", timefmt: str = "%Y %b %d %H:%M:%S", From edf80c037d2689e15b527dddc18a60ca6ef84e7c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 11 Oct 2024 14:06:18 +0800 Subject: [PATCH 14/18] clib.Session: Add type hints and reformat docstrings (part 1) (#3504) --- pygmt/clib/session.py | 56 +++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index 29e008d5e03..5fef545af51 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -105,33 +105,28 @@ class Session: """ A GMT API session where most operations involving the C API happen. - Works as a context manager (for use in a ``with`` block) to create a GMT C - API session and destroy it in the end to clean up memory. + Works as a context manager (for use in a ``with`` block) to create a GMT C API + session and destroy it in the end to clean up memory. - Functions of the shared library are exposed as methods of this class. Most - methods MUST be used with an open session (inside a ``with`` block). If - creating GMT data structures to communicate data, put that code inside the - same ``with`` block as the API calls that will use the data. + Functions of the shared library are exposed as methods of this class. Most methods + MUST be used with an open session (inside a ``with`` block). If creating GMT data + structures to communicate data, put that code inside the same ``with`` block as the + API calls that will use the data. - By default, will let :mod:`ctypes` try to find the GMT shared library - (``libgmt``). If the environment variable :term:`GMT_LIBRARY_PATH` is set, will - look for the shared library in the directory specified by it. + By default, will let :mod:`ctypes` try to find the GMT shared library (``libgmt``). + If the environment variable :term:`GMT_LIBRARY_PATH` is set, will look for the + shared library in the directory specified by it. - A ``GMTVersionError`` exception will be raised if the GMT shared library - reports a version older than the required minimum GMT version. - - The ``session_pointer`` attribute holds a ctypes pointer to the currently - open session. + The ``session_pointer`` attribute holds a ctypes pointer to the currently open + session. Raises ------ GMTCLibNotFoundError - If there was any problem loading the library (couldn't find it or - couldn't access the functions). + If there was any problem loading the library (couldn't find it or couldn't + access the functions). GMTCLibNoSessionError - If you try to call a method outside of a 'with' block. - GMTVersionError - If the minimum required version of GMT is not found. + If you try to call a method outside of a ``with`` block. Examples -------- @@ -141,45 +136,44 @@ class Session: >>> grid = load_static_earth_relief() >>> type(grid) - >>> # Create a session and destroy it automatically when exiting the "with" - >>> # block. - >>> with Session() as ses: + >>> # Create a session and destroy it automatically when exiting the "with" block. + >>> with Session() as lib: ... # Create a virtual file and link to the memory block of the grid. - ... with ses.virtualfile_from_grid(grid) as fin: + ... with lib.virtualfile_from_grid(grid) as fin: ... # Create a temp file to use as output. ... with GMTTempFile() as fout: - ... # Call the grdinfo module with the virtual file as input - ... # and the temp file as output. - ... ses.call_module("grdinfo", [fin, "-C", f"->{fout.name}"]) + ... # Call the grdinfo module with the virtual file as input and the + ... # temp file as output. + ... lib.call_module("grdinfo", [fin, "-C", f"->{fout.name}"]) ... # Read the contents of the temp file before it's deleted. ... print(fout.read().strip()) -55 -47 -24 -10 190 981 1 1 8 14 1 1 """ @property - def session_pointer(self): + def session_pointer(self) -> ctp.c_void_p: """ The :class:`ctypes.c_void_p` pointer to the current open GMT session. Raises ------ GMTCLibNoSessionError - If trying to access without a currently open GMT session (i.e., - outside of the context manager). + If trying to access without a currently open GMT session (i.e., outside of + the context manager). """ if not hasattr(self, "_session_pointer") or self._session_pointer is None: raise GMTCLibNoSessionError("No currently open GMT API session.") return self._session_pointer @session_pointer.setter - def session_pointer(self, session): + def session_pointer(self, session: ctp.c_void_p): """ Set the session void pointer. """ self._session_pointer = session @property - def info(self): + def info(self) -> dict[str, str]: """ Dictionary with the GMT version and default paths and parameters. """ From fde7901c353d96276a5bd490b8bff00a57c72605 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 11 Oct 2024 14:29:44 +0800 Subject: [PATCH 15/18] clib.conversion: Add type hints and improve docstrings for dataarray_to_matrix/vectors_to_arrays/array_to_datetime (#3496) --- pygmt/clib/conversion.py | 94 ++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/pygmt/clib/conversion.py b/pygmt/clib/conversion.py index ee80ec1a187..07f8756adcd 100644 --- a/pygmt/clib/conversion.py +++ b/pygmt/clib/conversion.py @@ -5,41 +5,45 @@ import ctypes as ctp import warnings from collections.abc import Sequence +from typing import Any import numpy as np import pandas as pd +import xarray as xr from packaging.version import Version from pygmt.exceptions import GMTInvalidInput -def dataarray_to_matrix(grid): +def dataarray_to_matrix( + grid: xr.DataArray, +) -> tuple[np.ndarray, list[float], list[float]]: """ - Transform an xarray.DataArray into a data 2-D array and metadata. + Transform an xarray.DataArray into a 2-D numpy array and metadata. - Use this to extract the underlying numpy array of data and the region and - increment for the grid. + Use this to extract the underlying numpy array of data and the region and increment + for the grid. - Only allows grids with two dimensions and constant grid spacing (GMT - doesn't allow variable grid spacing). If the latitude and/or longitude - increments of the input grid are negative, the output matrix will be - sorted by the DataArray coordinates to yield positive increments. + Only allows grids with two dimensions and constant grid spacings (GMT doesn't allow + variable grid spacings). If the latitude and/or longitude increments of the input + grid are negative, the output matrix will be sorted by the DataArray coordinates to + yield positive increments. - If the underlying data array is not C contiguous, for example if it's a - slice of a larger grid, a copy will need to be generated. + If the underlying data array is not C contiguous, for example, if it's a slice of a + larger grid, a copy will need to be generated. Parameters ---------- - grid : xarray.DataArray - The input grid as a DataArray instance. Information is retrieved from - the coordinate arrays, not from headers. + grid + The input grid as a DataArray instance. Information is retrieved from the + coordinate arrays, not from headers. Returns ------- - matrix : 2-D array + matrix The 2-D array of data from the grid. - region : list + region The West, East, South, North boundaries of the grid. - inc : list + inc The grid spacing in East-West and North-South, respectively. Raises @@ -64,8 +68,8 @@ def dataarray_to_matrix(grid): (180, 360) >>> matrix.flags.c_contiguous True - >>> # Using a slice of the grid, the matrix will be copied to guarantee - >>> # that it's C-contiguous in memory. The increment should be unchanged. + >>> # Using a slice of the grid, the matrix will be copied to guarantee that it's + >>> # C-contiguous in memory. The increment should be unchanged. >>> matrix, region, inc = dataarray_to_matrix(grid[10:41, 30:101]) >>> matrix.flags.c_contiguous True @@ -75,7 +79,7 @@ def dataarray_to_matrix(grid): [-150.0, -79.0, -80.0, -49.0] >>> print(inc) [1.0, 1.0] - >>> # but not if only taking every other grid point. + >>> # The increment should change accordingly if taking every other grid point. >>> matrix, region, inc = dataarray_to_matrix(grid[10:41:2, 30:101:2]) >>> matrix.flags.c_contiguous True @@ -87,21 +91,19 @@ def dataarray_to_matrix(grid): [2.0, 2.0] """ if len(grid.dims) != 2: - raise GMTInvalidInput( - f"Invalid number of grid dimensions '{len(grid.dims)}'. Must be 2." - ) + msg = f"Invalid number of grid dimensions 'len({grid.dims})'. Must be 2." + raise GMTInvalidInput(msg) + # Extract region and inc from the grid - region = [] - inc = [] - # Reverse the dims because it is rows, columns ordered. In geographic - # grids, this would be North-South, East-West. GMT's region and inc are - # East-West, North-South. + region, inc = [], [] + # Reverse the dims because it is rows, columns ordered. In geographic grids, this + # would be North-South, East-West. GMT's region and inc are East-West, North-South. for dim in grid.dims[::-1]: coord = grid.coords[dim].to_numpy() - coord_incs = coord[1:] - coord[0:-1] + coord_incs = coord[1:] - coord[:-1] coord_inc = coord_incs[0] if not np.allclose(coord_incs, coord_inc): - # calculate the increment if irregular spacing is found + # Calculate the increment if irregular spacing is found. coord_inc = (coord[-1] - coord[0]) / (coord.size - 1) msg = ( f"Grid may have irregular spacing in the '{dim}' dimension, " @@ -110,9 +112,8 @@ def dataarray_to_matrix(grid): ) warnings.warn(msg, category=RuntimeWarning, stacklevel=2) if coord_inc == 0: - raise GMTInvalidInput( - f"Grid has a zero increment in the '{dim}' dimension." - ) + msg = f"Grid has a zero increment in the '{dim}' dimension." + raise GMTInvalidInput(msg) region.extend( [ coord.min() - coord_inc / 2 * grid.gmt.registration, @@ -131,26 +132,25 @@ def dataarray_to_matrix(grid): return matrix, region, inc -def vectors_to_arrays(vectors): +def vectors_to_arrays(vectors: Sequence[Any]) -> list[np.ndarray]: """ - Convert 1-D vectors (lists, arrays, or pandas.Series) to C contiguous 1-D arrays. + Convert 1-D vectors (scalars, lists, or array-like) to C contiguous 1-D arrays. - Arrays must be in C contiguous order for us to pass their memory pointers - to GMT. If any are not, convert them to C order (which requires copying the - memory). This usually happens when vectors are columns of a 2-D array or - have been sliced. + Arrays must be in C contiguous order for us to pass their memory pointers to GMT. + If any are not, convert them to C order (which requires copying the memory). This + usually happens when vectors are columns of a 2-D array or have been sliced. - If a vector is a list or pandas.Series, get the underlying numpy array. + The returned arrays are guaranteed to be C contiguous and at least 1-D. Parameters ---------- - vectors : list of lists, 1-D arrays, or pandas.Series + vectors The vectors that must be converted. Returns ------- - arrays : list of 1-D arrays - The converted numpy arrays + arrays + List of converted numpy arrays. Examples -------- @@ -307,16 +307,15 @@ def strings_to_ctypes_array(strings: Sequence[str]) -> ctp.Array: return (ctp.c_char_p * len(strings))(*[s.encode() for s in strings]) -def array_to_datetime(array): +def array_to_datetime(array: Sequence[Any]) -> np.ndarray: """ Convert a 1-D datetime array from various types into numpy.datetime64. - If the input array is not in legal datetime formats, raise a ValueError - exception. + If the input array is not in legal datetime formats, raise a ValueError exception. Parameters ---------- - array : list or 1-D array + array The input datetime array in various formats. Supported types: @@ -328,7 +327,8 @@ def array_to_datetime(array): Returns ------- - array : 1-D datetime array in numpy.datetime64 + array + 1-D datetime array in numpy.datetime64. Raises ------ From 78fdfb186c8ce5c112cf04cebdc7656d567edf5a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 14 Oct 2024 23:09:08 +0800 Subject: [PATCH 16/18] Update pygmt/helpers/utils.py Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> --- pygmt/helpers/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index ee3410bfe9a..9e9a5fb542a 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -246,7 +246,7 @@ def data_kind( The "empty" kind: - >>> data_kind(data=None) + >>> data_kind(data=None, required=True) 'empty' The "file" kind: From a469acf2de5b5a6fe5cf73728bfc27a95aa5bc24 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 16 Oct 2024 10:16:08 +0800 Subject: [PATCH 17/18] Fix docstrings --- pygmt/clib/session.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index 00487550247..ba6f87763d9 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -1822,9 +1822,9 @@ def virtualfile_in( # noqa: PLR0912 elif kind == "matrix": # GMT can only accept a 2-D matrix which are signed integer (i), unsigned # integer (u) or floating point (f) types. For other data types, we need to - # use virtualfile_from_vectors instead, which turns the matrix into list of - # vectors and allows for better handling of string type inputs (e.g. for - # datetime data types). + # use virtualfile_from_vectors instead, which turns the matrix into a list + # of vectors and allows for better handling of non-integer/float type inputs + # (e.g. for string or datetime data types). _data = (data,) if data.dtype.kind not in "iuf": _virtualfile_from = self.virtualfile_from_vectors From 74bc1613c0011891d03ac570c00798b0886564e2 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 16 Oct 2024 10:17:42 +0800 Subject: [PATCH 18/18] Remove duplicated doctest --- pygmt/helpers/utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 5b59102cb94..ace5b6fe01f 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -299,9 +299,6 @@ def data_kind( 'vectors' >>> data_kind(data=pd.Series([1, 2, 3], name="x")) # pd.Series 'vectors' - - >>> data_kind(data=None, required=True) - 'empty' """ match data: case None if required: # No data provided and required=True.