From d023f6e4a4879ee88745882fe543c255c61800c1 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 15 Jul 2025 16:18:43 +0800 Subject: [PATCH 01/10] Add to API doc --- doc/api/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/index.rst b/doc/api/index.rst index 7828a225652..76263498f7d 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -285,6 +285,7 @@ All custom exceptions are derived from :class:`pygmt.exceptions.GMTError`. exceptions.GMTCLibNotFoundError exceptions.GMTTypeError exceptions.GMTValueError + exceptions.GMTTypeError .. currentmodule:: pygmt From 19d3df2f2c0689698c11c84616d4c0cf5c40a0f7 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 21 Jul 2025 14:04:46 +0800 Subject: [PATCH 02/10] Add new exception GMTParameterError for missing required parameters --- doc/api/index.rst | 2 +- pygmt/exceptions.py | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/doc/api/index.rst b/doc/api/index.rst index 76263498f7d..c35341f65a1 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -286,7 +286,7 @@ All custom exceptions are derived from :class:`pygmt.exceptions.GMTError`. exceptions.GMTTypeError exceptions.GMTValueError exceptions.GMTTypeError - + exceptions.GMTParameterError .. currentmodule:: pygmt diff --git a/pygmt/exceptions.py b/pygmt/exceptions.py index dedb64db7fa..da956787821 100644 --- a/pygmt/exceptions.py +++ b/pygmt/exceptions.py @@ -4,7 +4,7 @@ All exceptions derive from GMTError. """ -from collections.abc import Iterable +from collections.abc import Iterable, Set from typing import Any @@ -130,3 +130,40 @@ def __init__(self, dtype: object, /, reason: str | None = None): if reason: msg += f" {reason}" super().__init__(msg) + + +class GMTParameterError(GMTError): + """ + Raised when parameters are missing or invalid. + + Parameters + ---------- + required + Names of required parameters. + exclusive + Names of mutually exclusive parameters. + reason + Detailed reason why the parameters are invalid. + """ + + def __init__( + self, + *, + required: Set[str] | None = None, + exclusive: Set[str] | None = None, + reason: str | None = None, + ): + msg = "" + if required: + msg = ( + "Required parameter(s) are missing: " + f"{', '.join(repr(par) for par in required)}." + ) + if exclusive: + msg = ( + "Mutually exclusive parameter(s) are specified: " + f"{', '.join(repr(par) for par in exclusive)}." + ) + if reason: + msg += f" {reason}" + super().__init__(msg) From d553a25b300a7550f9a1b71627c09a7c8d10d6b1 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 21 Jul 2025 14:56:04 +0800 Subject: [PATCH 03/10] Use GMTParameterError for missing required parameters --- pygmt/src/filter1d.py | 5 ++--- pygmt/src/grdlandmask.py | 5 ++--- pygmt/src/grdproject.py | 5 ++--- pygmt/src/grdtrack.py | 8 +++++--- pygmt/src/meca.py | 5 ++--- pygmt/src/project.py | 5 ++--- pygmt/src/sphdistance.py | 6 +++--- pygmt/src/velo.py | 9 +++------ pygmt/src/xyz2grd.py | 5 ++--- pygmt/tests/test_grdlandmask.py | 4 ++-- pygmt/tests/test_grdproject.py | 4 ++-- pygmt/tests/test_grdtrack.py | 4 ++-- pygmt/tests/test_meca.py | 4 ++-- pygmt/tests/test_project.py | 4 ++-- pygmt/tests/test_sphdistance.py | 4 ++-- pygmt/tests/test_velo.py | 4 ++-- pygmt/tests/test_xyz2grd.py | 8 ++++---- 17 files changed, 41 insertions(+), 48 deletions(-) diff --git a/pygmt/src/filter1d.py b/pygmt/src/filter1d.py index e0ed391f096..b5be771fa3b 100644 --- a/pygmt/src/filter1d.py +++ b/pygmt/src/filter1d.py @@ -8,7 +8,7 @@ import pandas as pd from pygmt._typing import PathLike, TableLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -112,8 +112,7 @@ def filter1d( (depends on ``output_type``) """ if kwargs.get("F") is None: - msg = "Pass a required argument to 'filter_type'." - raise GMTInvalidInput(msg) + raise GMTParameterError(required={"filter_type"}) output_type = validate_output_table_type(output_type, outfile=outfile) diff --git a/pygmt/src/grdlandmask.py b/pygmt/src/grdlandmask.py index f2fdae251d9..18dd37dfe81 100644 --- a/pygmt/src/grdlandmask.py +++ b/pygmt/src/grdlandmask.py @@ -8,7 +8,7 @@ import xarray as xr from pygmt._typing import PathLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -115,8 +115,7 @@ def grdlandmask( >>> landmask = pygmt.grdlandmask(spacing=1, region=[125, 130, 30, 35]) """ if kwargs.get("I") is None or kwargs.get("R") is None: - msg = "Both 'region' and 'spacing' must be specified." - raise GMTInvalidInput(msg) + raise GMTParameterError(required={"spacing", "region"}) kwargs["D"] = kwargs.get("D", _parse_coastline_resolution(resolution)) kwargs["N"] = sequence_join(maskvalues, size=(2, 5), name="maskvalues") diff --git a/pygmt/src/grdproject.py b/pygmt/src/grdproject.py index 41a11b3c875..4d3bc7e865a 100644 --- a/pygmt/src/grdproject.py +++ b/pygmt/src/grdproject.py @@ -5,7 +5,7 @@ import xarray as xr from pygmt._typing import PathLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias __doctest_skip__ = ["grdproject"] @@ -106,8 +106,7 @@ def grdproject( >>> new_grid = pygmt.grdproject(grid=grid, projection="M10c", region=region) """ if kwargs.get("J") is None: - msg = "The projection must be specified." - raise GMTInvalidInput(msg) + raise GMTParameterError(required={"projection"}) with Session() as lib: with ( diff --git a/pygmt/src/grdtrack.py b/pygmt/src/grdtrack.py index 23106a10217..4ca6831addb 100644 --- a/pygmt/src/grdtrack.py +++ b/pygmt/src/grdtrack.py @@ -9,7 +9,7 @@ import xarray as xr from pygmt._typing import PathLike, TableLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput, GMTParameterError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -300,8 +300,10 @@ def grdtrack( raise GMTInvalidInput(msg) if hasattr(points, "columns") and newcolname is None: - msg = "Please pass in a str to 'newcolname'." - raise GMTInvalidInput(msg) + raise GMTParameterError( + required={"newcolname"}, + reason="Parameter 'newcolname' is required when 'points' is a pandas.DataFrame object.", + ) output_type = validate_output_table_type(output_type, outfile=outfile) diff --git a/pygmt/src/meca.py b/pygmt/src/meca.py index 629911f9097..8657c8b90a5 100644 --- a/pygmt/src/meca.py +++ b/pygmt/src/meca.py @@ -9,7 +9,7 @@ import pandas as pd from pygmt._typing import PathLike, TableLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTValueError from pygmt.helpers import ( build_arg_list, data_kind, @@ -30,8 +30,7 @@ def _get_focal_convention(spec, convention, component) -> _FocalMechanismConvent # Determine the convention from the 'convention' parameter. if convention is None: - msg = "Parameter 'convention' must be specified." - raise GMTInvalidInput(msg) + raise GMTParameterError(required={"convention"}) return _FocalMechanismConvention(convention=convention, component=component) diff --git a/pygmt/src/project.py b/pygmt/src/project.py index 73317598328..924945446f9 100644 --- a/pygmt/src/project.py +++ b/pygmt/src/project.py @@ -8,7 +8,7 @@ import pandas as pd from pygmt._typing import PathLike, TableLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput, GMTParameterError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -223,8 +223,7 @@ def project( (depends on ``output_type``) """ if kwargs.get("C") is None: - msg = "The 'center' parameter must be specified." - raise GMTInvalidInput(msg) + raise GMTParameterError(required={"center"}) if kwargs.get("G") is None and data is None: msg = "The 'data' parameter must be specified unless 'generate' is used." raise GMTInvalidInput(msg) diff --git a/pygmt/src/sphdistance.py b/pygmt/src/sphdistance.py index bec132b3ed8..eda4f410478 100644 --- a/pygmt/src/sphdistance.py +++ b/pygmt/src/sphdistance.py @@ -6,7 +6,7 @@ import xarray as xr from pygmt._typing import PathLike, TableLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias __doctest_skip__ = ["sphdistance"] @@ -115,8 +115,8 @@ def sphdistance( ... ) """ if kwargs.get("I") is None or kwargs.get("R") is None: - msg = "Both 'region' and 'spacing' must be specified." - raise GMTInvalidInput(msg) + raise GMTParameterError(required={"spacing", "region"}) + with Session() as lib: with ( lib.virtualfile_in(check_kind="vector", data=data, x=x, y=y) as vintbl, diff --git a/pygmt/src/velo.py b/pygmt/src/velo.py index d5484238009..0722ef39028 100644 --- a/pygmt/src/velo.py +++ b/pygmt/src/velo.py @@ -6,7 +6,7 @@ import pandas as pd from pygmt._typing import PathLike, TableLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput, GMTTypeError +from pygmt.exceptions import GMTParameterError, GMTTypeError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -241,11 +241,8 @@ def velo(self, data: PathLike | TableLike | None = None, **kwargs): """ self._activate_figure() - if kwargs.get("S") is None or ( - kwargs.get("S") is not None and not isinstance(kwargs["S"], str) - ): - msg = "The parameter 'spec' is required and has to be a string." - raise GMTInvalidInput(msg) + if kwargs.get("S") is None: + raise GMTParameterError(required={"spec"}) if isinstance(data, np.ndarray) and not pd.api.types.is_numeric_dtype(data): raise GMTTypeError( diff --git a/pygmt/src/xyz2grd.py b/pygmt/src/xyz2grd.py index cab13131dec..70eba96a5db 100644 --- a/pygmt/src/xyz2grd.py +++ b/pygmt/src/xyz2grd.py @@ -5,7 +5,7 @@ import xarray as xr from pygmt._typing import PathLike, TableLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias __doctest_skip__ = ["xyz2grd"] @@ -149,8 +149,7 @@ def xyz2grd( ... ) """ if kwargs.get("I") is None or kwargs.get("R") is None: - msg = "Both 'region' and 'spacing' must be specified." - raise GMTInvalidInput(msg) + raise GMTParameterError(required={"spacing", "region"}) with Session() as lib: with ( diff --git a/pygmt/tests/test_grdlandmask.py b/pygmt/tests/test_grdlandmask.py index 21b45d8d8c5..9155890c414 100644 --- a/pygmt/tests/test_grdlandmask.py +++ b/pygmt/tests/test_grdlandmask.py @@ -8,7 +8,7 @@ import xarray as xr from pygmt import grdlandmask from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile @@ -64,5 +64,5 @@ def test_grdlandmask_fails(): """ Check that grdlandmask fails correctly when region and spacing are not given. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdlandmask() diff --git a/pygmt/tests/test_grdproject.py b/pygmt/tests/test_grdproject.py index 8d4a1f70a21..f985b1df1ed 100644 --- a/pygmt/tests/test_grdproject.py +++ b/pygmt/tests/test_grdproject.py @@ -8,7 +8,7 @@ import xarray as xr from pygmt import grdproject from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -85,5 +85,5 @@ def test_grdproject_fails(grid): """ Check that grdproject fails correctly. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdproject(grid=grid) diff --git a/pygmt/tests/test_grdtrack.py b/pygmt/tests/test_grdtrack.py index 1b63dae041a..dfe5bd15f3c 100644 --- a/pygmt/tests/test_grdtrack.py +++ b/pygmt/tests/test_grdtrack.py @@ -9,7 +9,7 @@ import pandas as pd import pytest from pygmt import grdtrack -from pygmt.exceptions import GMTInvalidInput, GMTTypeError +from pygmt.exceptions import GMTInvalidInput, GMTParameterError, GMTTypeError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -146,7 +146,7 @@ def test_grdtrack_without_newcolname_setting(dataarray, dataframe): """ Run grdtrack by not passing in newcolname parameter setting. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdtrack(points=dataframe, grid=dataarray) diff --git a/pygmt/tests/test_meca.py b/pygmt/tests/test_meca.py index eaa9213fd5e..d24546ae848 100644 --- a/pygmt/tests/test_meca.py +++ b/pygmt/tests/test_meca.py @@ -10,7 +10,7 @@ from packaging.version import Version from pygmt import Figure from pygmt.clib import __gmt_version__ -from pygmt.exceptions import GMTInvalidInput, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTValueError from pygmt.helpers import GMTTempFile @@ -308,7 +308,7 @@ def test_meca_spec_ndarray_no_convention(): """ fig = Figure() fig.basemap(region=[-125, -122, 47, 49], projection="M6c", frame=True) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.meca(spec=np.array([[-124, 48, 12.0, 330, 30, 90, 3]]), scale="1c") diff --git a/pygmt/tests/test_project.py b/pygmt/tests/test_project.py index 36917368c63..8d58b6cd0fc 100644 --- a/pygmt/tests/test_project.py +++ b/pygmt/tests/test_project.py @@ -10,7 +10,7 @@ import pytest import xarray as xr from pygmt import project -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput, GMTParameterError from pygmt.helpers import GMTTempFile @@ -81,7 +81,7 @@ def test_project_incorrect_parameters(): Run project by providing incorrect parameters such as 1) no `center`; 2) no `data` or `generate`; and 3) `generate` with `convention`. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError, match="center"): # No `center` project(azimuth=45) with pytest.raises(GMTInvalidInput): diff --git a/pygmt/tests/test_sphdistance.py b/pygmt/tests/test_sphdistance.py index f96f14530e5..a4ad9fb2cdf 100644 --- a/pygmt/tests/test_sphdistance.py +++ b/pygmt/tests/test_sphdistance.py @@ -9,7 +9,7 @@ import pytest from pygmt import sphdistance from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile @@ -69,5 +69,5 @@ def test_sphdistance_fails(array): """ Check that sphdistance fails correctly when neither increment nor region is given. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): sphdistance(data=array) diff --git a/pygmt/tests/test_velo.py b/pygmt/tests/test_velo.py index ae5d44b62dd..a5675c6bc43 100644 --- a/pygmt/tests/test_velo.py +++ b/pygmt/tests/test_velo.py @@ -5,7 +5,7 @@ import pandas as pd import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput, GMTTypeError +from pygmt.exceptions import GMTParameterError, GMTTypeError @pytest.fixture(scope="module", name="dataframe") @@ -60,7 +60,7 @@ def test_velo_without_spec(dataframe): Check that velo fails when the spec parameter is not given. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError, match="spec"): fig.velo(data=dataframe) diff --git a/pygmt/tests/test_xyz2grd.py b/pygmt/tests/test_xyz2grd.py index 9f70c73cf8c..bf7fe854233 100644 --- a/pygmt/tests/test_xyz2grd.py +++ b/pygmt/tests/test_xyz2grd.py @@ -10,7 +10,7 @@ from pygmt import xyz2grd from pygmt.datasets import load_sample_data from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile @@ -75,9 +75,9 @@ def test_xyz2grd_missing_region_spacing(ship_data): """ Test xyz2grd raise an exception if region or spacing is missing. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): xyz2grd(data=ship_data) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): xyz2grd(data=ship_data, region=[245, 255, 20, 30]) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): xyz2grd(data=ship_data, spacing=5) From 11fd4a0abef02062d42824180a215444d88affd3 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 21 Jul 2025 15:49:56 +0800 Subject: [PATCH 04/10] Use GMTParameteError for mutally exclusive parameters --- pygmt/src/grd2cpt.py | 5 ++--- pygmt/src/grdfill.py | 13 ++++++------- pygmt/src/grdtrack.py | 3 +-- pygmt/src/makecpt.py | 5 ++--- pygmt/src/project.py | 3 +-- pygmt/src/subplot.py | 5 ++--- pygmt/tests/test_grd2cpt.py | 4 ++-- pygmt/tests/test_grdfill.py | 4 ++-- pygmt/tests/test_grdtrack.py | 2 +- pygmt/tests/test_makecpt.py | 4 ++-- pygmt/tests/test_project.py | 2 +- pygmt/tests/test_subplot.py | 4 ++-- 12 files changed, 24 insertions(+), 30 deletions(-) diff --git a/pygmt/src/grd2cpt.py b/pygmt/src/grd2cpt.py index 2b6f798f4bd..902d91689eb 100644 --- a/pygmt/src/grd2cpt.py +++ b/pygmt/src/grd2cpt.py @@ -5,7 +5,7 @@ import xarray as xr from pygmt._typing import PathLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias __doctest_skip__ = ["grd2cpt"] @@ -184,8 +184,7 @@ def grd2cpt(grid: PathLike | xr.DataArray, **kwargs): >>> fig.show() """ if kwargs.get("W") is not None and kwargs.get("Ww") is not None: - msg = "Set only 'categorical' or 'cyclic' to True, not both." - raise GMTInvalidInput(msg) + raise GMTParameterError(exclusive={"categorical", "cyclic"}) if (output := kwargs.pop("H", None)) is not None: kwargs["H"] = True diff --git a/pygmt/src/grdfill.py b/pygmt/src/grdfill.py index 87341a8122b..8bb41e0bdc8 100644 --- a/pygmt/src/grdfill.py +++ b/pygmt/src/grdfill.py @@ -8,7 +8,7 @@ import xarray as xr from pygmt._typing import PathLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput, GMTParameterError from pygmt.helpers import ( build_arg_list, deprecate_parameter, @@ -37,22 +37,22 @@ def _validate_params( >>> _validate_params(constantfill=20.0, gridfill="bggrid.nc") Traceback (most recent call last): ... - pygmt.exceptions.GMTInvalidInput: Parameters ... are mutually exclusive. + pygmt.exceptions.GMTParameterError: Mutually exclusive parameter... >>> _validate_params(constantfill=20.0, inquire=True) Traceback (most recent call last): ... - pygmt.exceptions.GMTInvalidInput: Parameters ... are mutually exclusive. + pygmt.exceptions.GMTParameterError: Mutually exclusive parameter... >>> _validate_params() Traceback (most recent call last): ... pygmt.exceptions.GMTInvalidInput: Need to specify parameter ... """ - _fill_params = "'constantfill'/'gridfill'/'neighborfill'/'splinefill'" + _fill_params = {"constantfill", "gridfill", "neighborfill", "splinefill"} # The deprecated 'mode' parameter is given. if mode is not None: msg = ( "The 'mode' parameter is deprecated since v0.15.0 and will be removed in " - f"v0.19.0. Use {_fill_params} instead." + f"v0.19.0. Use {', '.join(repr(par) for par in _fill_params)} instead." ) warnings.warn(msg, FutureWarning, stacklevel=2) @@ -61,8 +61,7 @@ def _validate_params( for param in [constantfill, gridfill, neighborfill, splinefill, inquire, mode] ) if n_given > 1: # More than one mutually exclusive parameter is given. - msg = f"Parameters {_fill_params}/'inquire'/'mode' are mutually exclusive." - raise GMTInvalidInput(msg) + raise GMTParameterError(exclusive=[*_fill_params, "inquire", "mode"]) if n_given == 0: # No parameters are given. msg = ( f"Need to specify parameter {_fill_params} for filling holes or " diff --git a/pygmt/src/grdtrack.py b/pygmt/src/grdtrack.py index 4ca6831addb..40f293b15e9 100644 --- a/pygmt/src/grdtrack.py +++ b/pygmt/src/grdtrack.py @@ -292,8 +292,7 @@ def grdtrack( ... ) """ if points is not None and kwargs.get("E") is not None: - msg = "Can't set both 'points' and 'profile'." - raise GMTInvalidInput(msg) + raise GMTParameterError(exclusive={"points", "profile"}) if points is None and kwargs.get("E") is None: msg = "Must give 'points' or set 'profile'." diff --git a/pygmt/src/makecpt.py b/pygmt/src/makecpt.py index 92f3ca26e5a..481689fb4c1 100644 --- a/pygmt/src/makecpt.py +++ b/pygmt/src/makecpt.py @@ -3,7 +3,7 @@ """ from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias @@ -156,8 +156,7 @@ def makecpt(**kwargs): ``categorical=True``. """ if kwargs.get("W") is not None and kwargs.get("Ww") is not None: - msg = "Set only categorical or cyclic to True, not both." - raise GMTInvalidInput(msg) + raise GMTParameterError(exclusive={"categorical", "cyclic"}) if (output := kwargs.pop("H", None)) is not None: kwargs["H"] = True diff --git a/pygmt/src/project.py b/pygmt/src/project.py index 924945446f9..6709f6dc19d 100644 --- a/pygmt/src/project.py +++ b/pygmt/src/project.py @@ -228,8 +228,7 @@ def project( msg = "The 'data' parameter must be specified unless 'generate' is used." raise GMTInvalidInput(msg) if kwargs.get("G") is not None and kwargs.get("F") is not None: - msg = "The 'convention' parameter is not allowed with 'generate'." - raise GMTInvalidInput(msg) + raise GMTParameterError(exclusive={"generate", "convention"}) output_type = validate_output_table_type(output_type, outfile=outfile) diff --git a/pygmt/src/subplot.py b/pygmt/src/subplot.py index b2fd9c273df..6a3b9f1f4b7 100644 --- a/pygmt/src/subplot.py +++ b/pygmt/src/subplot.py @@ -5,7 +5,7 @@ import contextlib from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTValueError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -156,8 +156,7 @@ def subplot(self, nrows=1, ncols=1, **kwargs): ) if kwargs.get("Ff") and kwargs.get("Fs"): - msg = "Please provide either one of 'figsize' or 'subsize' only." - raise GMTInvalidInput(msg) + raise GMTParameterError(exclusive={"figsize", "subsize"}) # Need to use separate sessions for "subplot begin" and "subplot end". # Otherwise, "subplot end" will use the last session, which may cause diff --git a/pygmt/tests/test_grd2cpt.py b/pygmt/tests/test_grd2cpt.py index db8eecb73a4..5430742054d 100644 --- a/pygmt/tests/test_grd2cpt.py +++ b/pygmt/tests/test_grd2cpt.py @@ -6,7 +6,7 @@ import pytest from pygmt import Figure, grd2cpt -from pygmt.exceptions import GMTInvalidInput, GMTTypeError, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTTypeError, GMTValueError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -70,5 +70,5 @@ def test_grd2cpt_categorical_and_cyclic(grid): """ Use incorrect setting by setting both categorical and cyclic to True. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grd2cpt(grid=grid, cmap="batlow", categorical=True, cyclic=True) diff --git a/pygmt/tests/test_grdfill.py b/pygmt/tests/test_grdfill.py index ed81a934cfa..4043b3dec15 100644 --- a/pygmt/tests/test_grdfill.py +++ b/pygmt/tests/test_grdfill.py @@ -10,7 +10,7 @@ import xarray as xr from pygmt import grdfill from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput, GMTParameterError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -136,7 +136,7 @@ def test_grdfill_inquire_and_fill(grid): """ Test that grdfill fails if both inquire and fill parameters are given. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdfill(grid=grid, inquire=True, constantfill=20) diff --git a/pygmt/tests/test_grdtrack.py b/pygmt/tests/test_grdtrack.py index dfe5bd15f3c..1766792008e 100644 --- a/pygmt/tests/test_grdtrack.py +++ b/pygmt/tests/test_grdtrack.py @@ -170,5 +170,5 @@ def test_grdtrack_set_points_and_profile(dataarray, dataframe): """ Run grdtrack but set both 'points' and 'profile'. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdtrack(grid=dataarray, points=dataframe, profile="BL/TR") diff --git a/pygmt/tests/test_makecpt.py b/pygmt/tests/test_makecpt.py index 6b55c21b27c..cc1013cdf18 100644 --- a/pygmt/tests/test_makecpt.py +++ b/pygmt/tests/test_makecpt.py @@ -7,7 +7,7 @@ import numpy as np import pytest from pygmt import Figure, makecpt -from pygmt.exceptions import GMTInvalidInput, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTValueError from pygmt.helpers import GMTTempFile POINTS_DATA = Path(__file__).parent / "data" / "points.txt" @@ -166,5 +166,5 @@ def test_makecpt_categorical_and_cyclic(): """ Use incorrect setting by setting both categorical and cyclic to True. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): makecpt(cmap="batlow", categorical=True, cyclic=True) diff --git a/pygmt/tests/test_project.py b/pygmt/tests/test_project.py index 8d58b6cd0fc..4a66c2ff17e 100644 --- a/pygmt/tests/test_project.py +++ b/pygmt/tests/test_project.py @@ -87,6 +87,6 @@ def test_project_incorrect_parameters(): with pytest.raises(GMTInvalidInput): # No `data` or `generate` project(center=[0, -1], azimuth=45, flat_earth=True) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): # Using `generate` with `convention` project(center=[0, -1], generate=0.5, convention="xypqrsz") diff --git a/pygmt/tests/test_subplot.py b/pygmt/tests/test_subplot.py index 118b0306900..8b3ea551ea7 100644 --- a/pygmt/tests/test_subplot.py +++ b/pygmt/tests/test_subplot.py @@ -4,7 +4,7 @@ import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTValueError @pytest.mark.benchmark @@ -89,7 +89,7 @@ def test_subplot_figsize_and_subsize_error(): into subplot. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): with fig.subplot(figsize=("2c", "1c"), subsize=("2c", "1c")): pass From e51bb31786dbe5819e75126499cbb1cce689903a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 21 Jul 2025 16:34:54 +0800 Subject: [PATCH 05/10] Use GMTParameterError for parameters where at least one is required --- pygmt/exceptions.py | 8 ++++++++ pygmt/src/coast.py | 17 ++++++++++++----- pygmt/src/dimfilter.py | 8 ++------ pygmt/src/grdclip.py | 8 ++------ pygmt/src/grdgradient.py | 9 +++------ pygmt/src/grdtrack.py | 5 ++--- pygmt/tests/test_coast.py | 4 ++-- pygmt/tests/test_dimfilter.py | 4 ++-- pygmt/tests/test_grdclip.py | 4 ++-- pygmt/tests/test_grdgradient.py | 4 ++-- pygmt/tests/test_grdtrack.py | 2 +- 11 files changed, 38 insertions(+), 35 deletions(-) diff --git a/pygmt/exceptions.py b/pygmt/exceptions.py index da956787821..ad50c1a3c11 100644 --- a/pygmt/exceptions.py +++ b/pygmt/exceptions.py @@ -140,6 +140,8 @@ class GMTParameterError(GMTError): ---------- required Names of required parameters. + require_any + Names of parameters where at least one must be specified. exclusive Names of mutually exclusive parameters. reason @@ -150,6 +152,7 @@ def __init__( self, *, required: Set[str] | None = None, + require_any: Set[str] | None = None, exclusive: Set[str] | None = None, reason: str | None = None, ): @@ -159,6 +162,11 @@ def __init__( "Required parameter(s) are missing: " f"{', '.join(repr(par) for par in required)}." ) + if require_any: + msg = ( + "At least one of the following parameters must be specified: " + f"{', '.join(repr(par) for par in require_any)}." + ) if exclusive: msg = ( "Mutually exclusive parameter(s) are specified: " diff --git a/pygmt/src/coast.py b/pygmt/src/coast.py index 1d68c411d8b..9f97712fa7b 100644 --- a/pygmt/src/coast.py +++ b/pygmt/src/coast.py @@ -5,7 +5,7 @@ from typing import Literal from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( args_in_kwargs, build_arg_list, @@ -206,11 +206,18 @@ def coast( """ self._activate_figure() if not args_in_kwargs(args=["C", "G", "S", "I", "N", "E", "Q", "W"], kwargs=kwargs): - msg = ( - "At least one of the following parameters must be specified: " - "lakes, land, water, rivers, borders, dcw, Q, or shorelines." + raise GMTParameterError( + require_any={ + "lakes", + "land", + "water", + "rivers", + "borders", + "dcw", + "Q", + "shorelines", + } ) - raise GMTInvalidInput(msg) kwargs["D"] = kwargs.get("D", _parse_coastline_resolution(resolution)) diff --git a/pygmt/src/dimfilter.py b/pygmt/src/dimfilter.py index 28a83584c8d..1e91bc18dfb 100644 --- a/pygmt/src/dimfilter.py +++ b/pygmt/src/dimfilter.py @@ -5,7 +5,7 @@ import xarray as xr from pygmt._typing import PathLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias __doctest_skip__ = ["dimfilter"] @@ -138,11 +138,7 @@ def dimfilter( ... ) """ if not all(arg in kwargs for arg in ["D", "F", "N"]) and "Q" not in kwargs: - msg = ( - "At least one of the following parameters must be specified: " - "distance, filters, or sectors." - ) - raise GMTInvalidInput(msg) + raise GMTParameterError(require_any={"distance", "filter", "sectors"}) with Session() as lib: with ( lib.virtualfile_in(check_kind="raster", data=grid) as vingrd, diff --git a/pygmt/src/grdclip.py b/pygmt/src/grdclip.py index a5f8af9e540..bc863eec79b 100644 --- a/pygmt/src/grdclip.py +++ b/pygmt/src/grdclip.py @@ -7,7 +7,7 @@ import xarray as xr from pygmt._typing import PathLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, deprecate_parameter, @@ -107,11 +107,7 @@ def grdclip( [0.0, 10000.0] """ if all(v is None for v in (above, below, between, replace)): - msg = ( - "Must specify at least one of the following parameters: ", - "'above', 'below', 'between', or 'replace'.", - ) - raise GMTInvalidInput(msg) + raise GMTParameterError(require_any={"above", "below", "between", "replace"}) # Parse the -S option. kwargs["Sa"] = sequence_join(above, size=2, name="above") diff --git a/pygmt/src/grdgradient.py b/pygmt/src/grdgradient.py index f2a87a4dc8a..a33a3230f89 100644 --- a/pygmt/src/grdgradient.py +++ b/pygmt/src/grdgradient.py @@ -5,7 +5,7 @@ import xarray as xr from pygmt._typing import PathLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput, GMTParameterError from pygmt.helpers import ( args_in_kwargs, build_arg_list, @@ -165,11 +165,8 @@ def grdgradient( msg = "Must specify normalize if tiles is specified." raise GMTInvalidInput(msg) if not args_in_kwargs(args=["A", "D", "E"], kwargs=kwargs): - msg = ( - "At least one of the following parameters must be specified: " - "azimuth, direction, or radiance." - ) - raise GMTInvalidInput(msg) + raise GMTParameterError(require_any={"azimuth", "direction", "radiance"}) + with Session() as lib: with ( lib.virtualfile_in(check_kind="raster", data=grid) as vingrd, diff --git a/pygmt/src/grdtrack.py b/pygmt/src/grdtrack.py index 40f293b15e9..afece769643 100644 --- a/pygmt/src/grdtrack.py +++ b/pygmt/src/grdtrack.py @@ -9,7 +9,7 @@ import xarray as xr from pygmt._typing import PathLike, TableLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -295,8 +295,7 @@ def grdtrack( raise GMTParameterError(exclusive={"points", "profile"}) if points is None and kwargs.get("E") is None: - msg = "Must give 'points' or set 'profile'." - raise GMTInvalidInput(msg) + raise GMTParameterError(require_any={"points", "profile"}) if hasattr(points, "columns") and newcolname is None: raise GMTParameterError( diff --git a/pygmt/tests/test_coast.py b/pygmt/tests/test_coast.py index 61ff8fcb405..b313f459857 100644 --- a/pygmt/tests/test_coast.py +++ b/pygmt/tests/test_coast.py @@ -4,7 +4,7 @@ import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput, GMTParameterError @pytest.mark.benchmark @@ -40,7 +40,7 @@ def test_coast_required_args(): Test if fig.coast fails when not given required arguments. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.coast(region="EG") diff --git a/pygmt/tests/test_dimfilter.py b/pygmt/tests/test_dimfilter.py index ca1f5e120e1..24717e9322f 100644 --- a/pygmt/tests/test_dimfilter.py +++ b/pygmt/tests/test_dimfilter.py @@ -8,7 +8,7 @@ import xarray as xr from pygmt import dimfilter from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -80,5 +80,5 @@ def test_dimfilter_fails(grid): Check that dimfilter fails correctly when not all of sectors, filters, and distance are specified. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): dimfilter(grid=grid, sectors="l6", distance=4) diff --git a/pygmt/tests/test_grdclip.py b/pygmt/tests/test_grdclip.py index a8824467131..1a8284dfaaa 100644 --- a/pygmt/tests/test_grdclip.py +++ b/pygmt/tests/test_grdclip.py @@ -11,7 +11,7 @@ from pygmt import grdclip from pygmt.datasets import load_earth_mask from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -110,5 +110,5 @@ def test_grdclip_missing_required_parameter(grid): """ Test that grdclip raises a ValueError if the required parameter is missing. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdclip(grid=grid) diff --git a/pygmt/tests/test_grdgradient.py b/pygmt/tests/test_grdgradient.py index 5749b237aea..0b1b37a69e4 100644 --- a/pygmt/tests/test_grdgradient.py +++ b/pygmt/tests/test_grdgradient.py @@ -8,7 +8,7 @@ import xarray as xr from pygmt import grdgradient from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput, GMTParameterError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -80,7 +80,7 @@ def test_grdgradient_fails(grid): Check that grdgradient fails correctly when `tiles` is specified but normalize is not. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdgradient(grid=grid) # fails without required arguments with pytest.raises(GMTInvalidInput): # fails when tiles is specified but not normalize diff --git a/pygmt/tests/test_grdtrack.py b/pygmt/tests/test_grdtrack.py index 1766792008e..edc5b41fc8e 100644 --- a/pygmt/tests/test_grdtrack.py +++ b/pygmt/tests/test_grdtrack.py @@ -162,7 +162,7 @@ def test_grdtrack_no_points_and_profile(dataarray): """ Run grdtrack but don't set 'points' and 'profile'. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdtrack(grid=dataarray) From 170d11bafafd59cf0a9c09e2f7006553246c43bb Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 26 Jul 2025 22:13:25 +0800 Subject: [PATCH 06/10] More fixes --- pygmt/datasets/load_remote_dataset.py | 12 +++++++----- pygmt/src/grdfill.py | 14 ++++++++------ pygmt/src/grdgradient.py | 8 +++++--- pygmt/src/grdimage.py | 3 +-- pygmt/src/project.py | 8 +++++--- pygmt/tests/test_datasets_load_remote_datasets.py | 4 ++-- pygmt/tests/test_grdfill.py | 4 ++-- pygmt/tests/test_grdgradient.py | 4 ++-- pygmt/tests/test_grdimage.py | 6 +++--- pygmt/tests/test_grdtrack.py | 4 ++-- pygmt/tests/test_project.py | 4 ++-- 11 files changed, 39 insertions(+), 32 deletions(-) diff --git a/pygmt/datasets/load_remote_dataset.py b/pygmt/datasets/load_remote_dataset.py index d20a8b4ac05..27783cfc741 100644 --- a/pygmt/datasets/load_remote_dataset.py +++ b/pygmt/datasets/load_remote_dataset.py @@ -7,7 +7,7 @@ from typing import Any, Literal, NamedTuple import xarray as xr -from pygmt.exceptions import GMTInvalidInput, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTValueError with contextlib.suppress(ImportError): # rioxarray is needed to register the rio accessor @@ -572,11 +572,13 @@ def _load_remote_dataset( reg = registration[0] if resinfo.tiled and region is None: - msg = ( - f"The 'region' parameter is required for {dataset.description} " - f"resolution '{resolution}'." + raise GMTParameterError( + required=["region"], + reason=( + f"Parameter 'region' is required for {dataset.description} resolution " + f"{resolution!r} with tiled grids." + ), ) - raise GMTInvalidInput(msg) fname = f"@{prefix}_{resolution}_{reg}" grid = xr.load_dataarray( diff --git a/pygmt/src/grdfill.py b/pygmt/src/grdfill.py index 8bb41e0bdc8..60f88732592 100644 --- a/pygmt/src/grdfill.py +++ b/pygmt/src/grdfill.py @@ -8,7 +8,7 @@ import xarray as xr from pygmt._typing import PathLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, deprecate_parameter, @@ -45,7 +45,7 @@ def _validate_params( >>> _validate_params() Traceback (most recent call last): ... - pygmt.exceptions.GMTInvalidInput: Need to specify parameter ... + pygmt.exceptions.GMTParameterError: ... """ _fill_params = {"constantfill", "gridfill", "neighborfill", "splinefill"} # The deprecated 'mode' parameter is given. @@ -63,11 +63,13 @@ def _validate_params( if n_given > 1: # More than one mutually exclusive parameter is given. raise GMTParameterError(exclusive=[*_fill_params, "inquire", "mode"]) if n_given == 0: # No parameters are given. - msg = ( - f"Need to specify parameter {_fill_params} for filling holes or " - "'inquire' for inquiring the bounds of each hole." + raise GMTParameterError( + required=_fill_params, + reason=( + f"Need to specify parameter {_fill_params!r} for filling holes or " + "'inquire' for inquiring the bounds of each hole." + ), ) - raise GMTInvalidInput(msg) def _parse_fill_mode( diff --git a/pygmt/src/grdgradient.py b/pygmt/src/grdgradient.py index a33a3230f89..b91f56bc893 100644 --- a/pygmt/src/grdgradient.py +++ b/pygmt/src/grdgradient.py @@ -5,7 +5,7 @@ import xarray as xr from pygmt._typing import PathLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( args_in_kwargs, build_arg_list, @@ -162,8 +162,10 @@ def grdgradient( >>> new_grid = pygmt.grdgradient(grid=grid, azimuth=10) """ if kwargs.get("Q") is not None and kwargs.get("N") is None: - msg = "Must specify normalize if tiles is specified." - raise GMTInvalidInput(msg) + raise GMTParameterError( + required=["normalize"], + reason="Must specify 'normalize' if 'tiles' is specified.", + ) if not args_in_kwargs(args=["A", "D", "E"], kwargs=kwargs): raise GMTParameterError(require_any={"azimuth", "direction", "radiance"}) diff --git a/pygmt/src/grdimage.py b/pygmt/src/grdimage.py index 2b61d87b945..ac3b118ceb2 100644 --- a/pygmt/src/grdimage.py +++ b/pygmt/src/grdimage.py @@ -5,7 +5,6 @@ import xarray as xr from pygmt._typing import PathLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -164,7 +163,7 @@ def grdimage(self, grid: PathLike | xr.DataArray, **kwargs): "Parameter 'img_out'/'A' is not implemented. " "Please consider submitting a feature request to us." ) - raise GMTInvalidInput(msg) + raise NotImplementedError(msg) with Session() as lib: with ( diff --git a/pygmt/src/project.py b/pygmt/src/project.py index 6709f6dc19d..624b4f68e67 100644 --- a/pygmt/src/project.py +++ b/pygmt/src/project.py @@ -8,7 +8,7 @@ import pandas as pd from pygmt._typing import PathLike, TableLike from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -225,8 +225,10 @@ def project( if kwargs.get("C") is None: raise GMTParameterError(required={"center"}) if kwargs.get("G") is None and data is None: - msg = "The 'data' parameter must be specified unless 'generate' is used." - raise GMTInvalidInput(msg) + raise GMTParameterError( + required={"data"}, + reason="Parameter 'data' must be specified unless 'generate' is used.", + ) if kwargs.get("G") is not None and kwargs.get("F") is not None: raise GMTParameterError(exclusive={"generate", "convention"}) diff --git a/pygmt/tests/test_datasets_load_remote_datasets.py b/pygmt/tests/test_datasets_load_remote_datasets.py index e2cef8f8ac2..dfa7fc5bf75 100644 --- a/pygmt/tests/test_datasets_load_remote_datasets.py +++ b/pygmt/tests/test_datasets_load_remote_datasets.py @@ -5,7 +5,7 @@ import pytest from pygmt.datasets.load_remote_dataset import _load_remote_dataset from pygmt.enums import GridRegistration -from pygmt.exceptions import GMTInvalidInput, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTValueError def load_remote_dataset_wrapper(resolution="01d", region=None, registration=None): @@ -61,7 +61,7 @@ def test_load_remote_dataset_tiled_grid_without_region(): Make sure _load_remote_dataset fails when trying to load a tiled grid without specifying a region. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): load_remote_dataset_wrapper(resolution="01m") diff --git a/pygmt/tests/test_grdfill.py b/pygmt/tests/test_grdfill.py index 4043b3dec15..50fa8af7355 100644 --- a/pygmt/tests/test_grdfill.py +++ b/pygmt/tests/test_grdfill.py @@ -10,7 +10,7 @@ import xarray as xr from pygmt import grdfill from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -128,7 +128,7 @@ def test_grdfill_required_args(grid): """ Test that grdfill fails without filling parameters or 'inquire'. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdfill(grid=grid) diff --git a/pygmt/tests/test_grdgradient.py b/pygmt/tests/test_grdgradient.py index 0b1b37a69e4..40b27414c17 100644 --- a/pygmt/tests/test_grdgradient.py +++ b/pygmt/tests/test_grdgradient.py @@ -8,7 +8,7 @@ import xarray as xr from pygmt import grdgradient from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -82,6 +82,6 @@ def test_grdgradient_fails(grid): """ with pytest.raises(GMTParameterError): grdgradient(grid=grid) # fails without required arguments - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): # fails when tiles is specified but not normalize grdgradient(grid=grid, azimuth=10, direction="c", tiles="c") diff --git a/pygmt/tests/test_grdimage.py b/pygmt/tests/test_grdimage.py index 8df8aab6df9..b0d11c4bdd6 100644 --- a/pygmt/tests/test_grdimage.py +++ b/pygmt/tests/test_grdimage.py @@ -10,7 +10,7 @@ from pygmt.clib import __gmt_version__ from pygmt.datasets import load_earth_relief from pygmt.enums import GridRegistration, GridType -from pygmt.exceptions import GMTInvalidInput, GMTTypeError +from pygmt.exceptions import GMTTypeError from pygmt.helpers.testing import check_figures_equal @@ -252,9 +252,9 @@ def test_grdimage_imgout_fails(grid): Test that an exception is raised if img_out/A is given. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(NotImplementedError): fig.grdimage(grid, img_out="out.png") - with pytest.raises(GMTInvalidInput): + with pytest.raises(NotImplementedError): fig.grdimage(grid, A="out.png") diff --git a/pygmt/tests/test_grdtrack.py b/pygmt/tests/test_grdtrack.py index edc5b41fc8e..4bedac10a24 100644 --- a/pygmt/tests/test_grdtrack.py +++ b/pygmt/tests/test_grdtrack.py @@ -9,7 +9,7 @@ import pandas as pd import pytest from pygmt import grdtrack -from pygmt.exceptions import GMTInvalidInput, GMTParameterError, GMTTypeError +from pygmt.exceptions import GMTParameterError, GMTTypeError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -154,7 +154,7 @@ def test_grdtrack_without_outfile_setting(dataarray, dataframe): """ Run grdtrack by not passing in outfile parameter setting. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdtrack(points=dataframe, grid=dataarray) diff --git a/pygmt/tests/test_project.py b/pygmt/tests/test_project.py index 4a66c2ff17e..ea032d15e30 100644 --- a/pygmt/tests/test_project.py +++ b/pygmt/tests/test_project.py @@ -10,7 +10,7 @@ import pytest import xarray as xr from pygmt import project -from pygmt.exceptions import GMTInvalidInput, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile @@ -84,7 +84,7 @@ def test_project_incorrect_parameters(): with pytest.raises(GMTParameterError, match="center"): # No `center` project(azimuth=45) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): # No `data` or `generate` project(center=[0, -1], azimuth=45, flat_earth=True) with pytest.raises(GMTParameterError): From c2c81d7e1454f5e9b7ac7150dc1a23b5f1879de0 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 26 Jul 2025 22:26:48 +0800 Subject: [PATCH 07/10] Improve exception --- pygmt/exceptions.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pygmt/exceptions.py b/pygmt/exceptions.py index ad50c1a3c11..b275067f6e7 100644 --- a/pygmt/exceptions.py +++ b/pygmt/exceptions.py @@ -156,22 +156,25 @@ def __init__( exclusive: Set[str] | None = None, reason: str | None = None, ): - msg = "" + msg = [] if required: - msg = ( + msg.append( "Required parameter(s) are missing: " f"{', '.join(repr(par) for par in required)}." ) + if require_any: - msg = ( + msg.append( "At least one of the following parameters must be specified: " f"{', '.join(repr(par) for par in require_any)}." ) + if exclusive: - msg = ( + msg.append( "Mutually exclusive parameter(s) are specified: " f"{', '.join(repr(par) for par in exclusive)}." ) + if reason: - msg += f" {reason}" - super().__init__(msg) + msg.append(reason) + super().__init__(" ".join(msg)) From c42c58772aba245b2a88687da875db8ce356bcf8 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 26 Jul 2025 22:45:12 +0800 Subject: [PATCH 08/10] Fix typing issue --- pygmt/datasets/load_remote_dataset.py | 7 ++----- pygmt/src/grdgradient.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pygmt/datasets/load_remote_dataset.py b/pygmt/datasets/load_remote_dataset.py index 27783cfc741..2805c54d1bf 100644 --- a/pygmt/datasets/load_remote_dataset.py +++ b/pygmt/datasets/load_remote_dataset.py @@ -573,11 +573,8 @@ def _load_remote_dataset( if resinfo.tiled and region is None: raise GMTParameterError( - required=["region"], - reason=( - f"Parameter 'region' is required for {dataset.description} resolution " - f"{resolution!r} with tiled grids." - ), + required={"region"}, + reason=f"Required for {dataset.description} resolution {resolution!r} with tiled grids.", ) fname = f"@{prefix}_{resolution}_{reg}" diff --git a/pygmt/src/grdgradient.py b/pygmt/src/grdgradient.py index b91f56bc893..7d40f969ea9 100644 --- a/pygmt/src/grdgradient.py +++ b/pygmt/src/grdgradient.py @@ -163,7 +163,7 @@ def grdgradient( """ if kwargs.get("Q") is not None and kwargs.get("N") is None: raise GMTParameterError( - required=["normalize"], + required={"normalize"}, reason="Must specify 'normalize' if 'tiles' is specified.", ) if not args_in_kwargs(args=["A", "D", "E"], kwargs=kwargs): From b4829b68dc4343cd45370b1846967a0a6c72a79f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 10 Aug 2025 16:04:08 +0800 Subject: [PATCH 09/10] Migrate more exceptions to GMTParameterError --- pygmt/src/plot.py | 8 +++++--- pygmt/src/plot3d.py | 8 +++++--- pygmt/src/text.py | 30 ++++++++++++++++++++---------- pygmt/tests/test_plot.py | 10 +++++----- pygmt/tests/test_plot3d.py | 6 +++--- pygmt/tests/test_text.py | 16 ++++++++-------- 6 files changed, 46 insertions(+), 32 deletions(-) diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py index 5595474afd8..83f661bd56e 100644 --- a/pygmt/src/plot.py +++ b/pygmt/src/plot.py @@ -7,7 +7,7 @@ from pygmt._typing import PathLike, TableLike from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput, GMTTypeError +from pygmt.exceptions import GMTParameterError, GMTTypeError from pygmt.helpers import ( build_arg_list, data_kind, @@ -263,8 +263,10 @@ def plot( # noqa: PLR0912 data["symbol"] = symbol else: if any(v is not None for v in (x, y)): - msg = "Too much data. Use either data or x/y/z." - raise GMTInvalidInput(msg) + raise GMTParameterError( + exclusive={"data", "x/y/z"}, + reason="Too much data. Use either data or x/y/z.", + ) for name, value in [ ("direction", direction), ("fill", kwargs.get("G")), diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py index 8cd20079764..4cd1ba21307 100644 --- a/pygmt/src/plot3d.py +++ b/pygmt/src/plot3d.py @@ -7,7 +7,7 @@ from pygmt._typing import PathLike, TableLike from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput, GMTTypeError +from pygmt.exceptions import GMTParameterError, GMTTypeError from pygmt.helpers import ( build_arg_list, data_kind, @@ -241,8 +241,10 @@ def plot3d( # noqa: PLR0912 data["symbol"] = symbol else: if any(v is not None for v in (x, y, z)): - msg = "Too much data. Use either data or x/y/z." - raise GMTInvalidInput(msg) + raise GMTParameterError( + exclusive={"data", "x/y/z"}, + reason="Too much data. Use either data or x/y/z.", + ) for name, value in [ ("direction", direction), diff --git a/pygmt/src/text.py b/pygmt/src/text.py index cd8e788e691..1fce51199a5 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -8,7 +8,7 @@ from pygmt._typing import AnchorCode, PathLike, StringArrayTypes, TableLike from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput, GMTTypeError +from pygmt.exceptions import GMTParameterError, GMTTypeError from pygmt.helpers import ( _check_encoding, build_arg_list, @@ -190,21 +190,31 @@ def text_( # noqa: PLR0912 + (position is not None) + (x is not None or y is not None) ) != 1: - msg = "Provide either 'textfiles', 'x'/'y'/'text', or 'position'/'text'." - raise GMTInvalidInput(msg) + raise GMTParameterError( + exclusive={"textfiles", "x/y/text", "position/text"}, + reason="Provide either 'textfiles', 'x'/'y'/'text', or 'position'/'text'.", + ) data_is_required = position is None kind = data_kind(textfiles, required=data_is_required) - if position is not None and (text is None or is_nonstr_iter(text)): - msg = "'text' can't be None or array when 'position' is given." - raise GMTInvalidInput(msg) + if position is not None: + if text is None: + raise GMTParameterError( + required={"text"}, + reason="Parameter 'text' is required when 'position' is given.", + ) + if is_nonstr_iter(text): + raise GMTTypeError( + type(text), + reason="Parameter 'text' can't be a sequence when 'position' is given.", + ) if textfiles is not None and text is not None: - msg = "'text' can't be specified when 'textfiles' is given." - raise GMTInvalidInput(msg) + raise GMTParameterError(exclusive={"text", "textfiles"}) if kind == "empty" and text is None: - msg = "Must provide text with x/y pairs." - raise GMTInvalidInput(msg) + raise GMTParameterError( + required={"text"}, reason="Must provide text with x/y pairs." + ) # Arguments that can accept arrays. array_args = [ diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index c38f195b958..6c589ae6876 100644 --- a/pygmt/tests/test_plot.py +++ b/pygmt/tests/test_plot.py @@ -10,7 +10,7 @@ import pytest import xarray as xr from pygmt import Figure, which -from pygmt.exceptions import GMTInvalidInput, GMTTypeError +from pygmt.exceptions import GMTParameterError, GMTTypeError from pygmt.helpers import GMTTempFile POINTS_DATA = Path(__file__).parent / "data" / "points.txt" @@ -55,11 +55,11 @@ def test_plot_fail_no_data(data, region): Plot should raise an exception if no data is given. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.plot( region=region, projection="X10c", style="c0.2c", fill="red", frame="afg" ) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.plot( x=data[:, 0], region=region, @@ -68,7 +68,7 @@ def test_plot_fail_no_data(data, region): fill="red", frame="afg", ) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.plot( y=data[:, 0], region=region, @@ -78,7 +78,7 @@ def test_plot_fail_no_data(data, region): frame="afg", ) # Should also fail if given too much data - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.plot( x=data[:, 0], y=data[:, 1], diff --git a/pygmt/tests/test_plot3d.py b/pygmt/tests/test_plot3d.py index 69237e02447..dd14f17ea1b 100644 --- a/pygmt/tests/test_plot3d.py +++ b/pygmt/tests/test_plot3d.py @@ -7,7 +7,7 @@ import numpy as np import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput, GMTTypeError +from pygmt.exceptions import GMTParameterError, GMTTypeError from pygmt.helpers import GMTTempFile POINTS_DATA = Path(__file__).parent / "data" / "points.txt" @@ -93,11 +93,11 @@ def test_plot3d_fail_no_data(data, region): Should raise an exception if data is not enough or too much. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.plot3d( style="c0.2c", x=data[0], y=data[1], region=region, projection="X10c" ) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.plot3d( style="c0.2c", data=data, x=data[0], region=region, projection="X10c" ) diff --git a/pygmt/tests/test_text.py b/pygmt/tests/test_text.py index b77c9ff36e1..db72d6a3eb2 100644 --- a/pygmt/tests/test_text.py +++ b/pygmt/tests/test_text.py @@ -7,7 +7,7 @@ import numpy as np import pytest from pygmt import Figure, config -from pygmt.exceptions import GMTCLibError, GMTInvalidInput +from pygmt.exceptions import GMTCLibError, GMTParameterError, GMTTypeError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import skip_if_no @@ -85,7 +85,7 @@ def test_text_without_text_input(region, projection): Run text by passing in x and y, but no text. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.text(region=region, projection=projection, x=1.2, y=2.4) @@ -146,20 +146,20 @@ def test_text_invalid_inputs(region): Run text by providing invalid combinations of inputs. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.text( region=region, projection="x1c", x=1.2, y=2.4, position="MC", text="text" ) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.text(region=region, projection="x1c", textfiles="file.txt", text="text") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.text(region=region, projection="x1c", position="MC", text=None) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): + fig.text(region=region, projection="x1c", textfiles="file.txt", x=1.2, y=2.4) + with pytest.raises(GMTTypeError): fig.text( region=region, projection="x1c", position="MC", text=["text1", "text2"] ) - with pytest.raises(GMTInvalidInput): - fig.text(region=region, projection="x1c", textfiles="file.txt", x=1.2, y=2.4) @pytest.mark.mpl_image_compare From 8d9a122da8668a615d42b893b95015fdc03f341c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 21 Aug 2025 21:19:57 +0800 Subject: [PATCH 10/10] Fix the order in doc/api --- doc/api/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/api/index.rst b/doc/api/index.rst index c35341f65a1..4aa071bafd7 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -283,10 +283,9 @@ All custom exceptions are derived from :class:`pygmt.exceptions.GMTError`. exceptions.GMTCLibError exceptions.GMTCLibNoSessionError exceptions.GMTCLibNotFoundError + exceptions.GMTParameterError exceptions.GMTTypeError exceptions.GMTValueError - exceptions.GMTTypeError - exceptions.GMTParameterError .. currentmodule:: pygmt