diff --git a/doc/api/index.rst b/doc/api/index.rst index 7828a225652..c35341f65a1 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -285,7 +285,8 @@ All custom exceptions are derived from :class:`pygmt.exceptions.GMTError`. exceptions.GMTCLibNotFoundError exceptions.GMTTypeError exceptions.GMTValueError - + exceptions.GMTTypeError + exceptions.GMTParameterError .. currentmodule:: pygmt diff --git a/pygmt/datasets/load_remote_dataset.py b/pygmt/datasets/load_remote_dataset.py index 2d97ab0c305..f23de9228d4 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,10 @@ 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"Required for {dataset.description} resolution {resolution!r} with tiled grids.", ) - raise GMTInvalidInput(msg) fname = f"@{prefix}_{resolution}_{reg}" grid = xr.load_dataarray( diff --git a/pygmt/exceptions.py b/pygmt/exceptions.py index dedb64db7fa..b275067f6e7 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,51 @@ 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. + require_any + Names of parameters where at least one must be specified. + exclusive + Names of mutually exclusive parameters. + reason + Detailed reason why the parameters are invalid. + """ + + def __init__( + self, + *, + required: Set[str] | None = None, + require_any: Set[str] | None = None, + exclusive: Set[str] | None = None, + reason: str | None = None, + ): + msg = [] + if required: + msg.append( + "Required parameter(s) are missing: " + f"{', '.join(repr(par) for par in required)}." + ) + + if require_any: + msg.append( + "At least one of the following parameters must be specified: " + f"{', '.join(repr(par) for par in require_any)}." + ) + + if exclusive: + msg.append( + "Mutually exclusive parameter(s) are specified: " + f"{', '.join(repr(par) for par in exclusive)}." + ) + + if reason: + msg.append(reason) + super().__init__(" ".join(msg)) diff --git a/pygmt/src/coast.py b/pygmt/src/coast.py index 497869faeed..03665b2fd75 100644 --- a/pygmt/src/coast.py +++ b/pygmt/src/coast.py @@ -6,7 +6,7 @@ from pygmt.alias import Alias, AliasSystem 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, @@ -207,11 +207,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) aliasdict = AliasSystem( D=Alias( 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/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/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/grdclip.py b/pygmt/src/grdclip.py index e1d0bb8bd41..8e6e04d0cbc 100644 --- a/pygmt/src/grdclip.py +++ b/pygmt/src/grdclip.py @@ -8,7 +8,7 @@ from pygmt._typing import PathLike from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, deprecate_parameter, @@ -109,11 +109,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"}) aliasdict = AliasSystem( Sa=Alias(above, name="above", sep="/", size=2), diff --git a/pygmt/src/grdfill.py b/pygmt/src/grdfill.py index a649ea85a25..d420f95e4d1 100644 --- a/pygmt/src/grdfill.py +++ b/pygmt/src/grdfill.py @@ -9,7 +9,7 @@ from pygmt._typing import PathLike from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, deprecate_parameter, @@ -38,22 +38,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 ... + pygmt.exceptions.GMTParameterError: ... """ - _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) @@ -62,14 +62,15 @@ 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 " - "'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) @fmt_docstring diff --git a/pygmt/src/grdgradient.py b/pygmt/src/grdgradient.py index f2a87a4dc8a..7d40f969ea9 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 GMTParameterError from pygmt.helpers import ( args_in_kwargs, build_arg_list, @@ -162,14 +162,13 @@ 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) - 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 GMTParameterError( + required={"normalize"}, + reason="Must specify 'normalize' if 'tiles' is specified.", ) - raise GMTInvalidInput(msg) + if not args_in_kwargs(args=["A", "D", "E"], kwargs=kwargs): + 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/grdimage.py b/pygmt/src/grdimage.py index 8a5ebdb76c6..46180bc89b4 100644 --- a/pygmt/src/grdimage.py +++ b/pygmt/src/grdimage.py @@ -6,7 +6,6 @@ from pygmt._typing import PathLike from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -165,7 +164,7 @@ def grdimage(self, grid: PathLike | xr.DataArray, projection=None, **kwargs): "Parameter 'img_out'/'A' is not implemented. " "Please consider submitting a feature request to us." ) - raise GMTInvalidInput(msg) + raise NotImplementedError(msg) aliasdict = AliasSystem( J=Alias(projection, name="projection"), diff --git a/pygmt/src/grdlandmask.py b/pygmt/src/grdlandmask.py index b96a5e8c551..5c912eda1f7 100644 --- a/pygmt/src/grdlandmask.py +++ b/pygmt/src/grdlandmask.py @@ -9,7 +9,7 @@ from pygmt._typing import PathLike from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -114,8 +114,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"}) aliasdict = AliasSystem( D=Alias( diff --git a/pygmt/src/grdproject.py b/pygmt/src/grdproject.py index abe1f48fc20..8594c1dcff3 100644 --- a/pygmt/src/grdproject.py +++ b/pygmt/src/grdproject.py @@ -6,7 +6,7 @@ from pygmt._typing import PathLike from pygmt.alias import Alias, AliasSystem 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"] @@ -110,8 +110,7 @@ def grdproject( >>> new_grid = pygmt.grdproject(grid=grid, projection="M10c", region=region) """ if projection is None: - msg = "The projection must be specified." - raise GMTInvalidInput(msg) + raise GMTParameterError(required={"projection"}) aliasdict = AliasSystem( J=Alias(projection, name="projection"), diff --git a/pygmt/src/grdtrack.py b/pygmt/src/grdtrack.py index 23106a10217..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 +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -292,16 +292,16 @@ 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'." - raise GMTInvalidInput(msg) + raise GMTParameterError(require_any={"points", "profile"}) 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/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/meca.py b/pygmt/src/meca.py index 0df6b32b3d8..898fc2796fa 100644 --- a/pygmt/src/meca.py +++ b/pygmt/src/meca.py @@ -10,7 +10,7 @@ from pygmt._typing import PathLike, TableLike from pygmt.alias import Alias, AliasSystem 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, @@ -31,8 +31,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/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/project.py b/pygmt/src/project.py index 73317598328..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 +from pygmt.exceptions import GMTParameterError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -223,14 +223,14 @@ 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) + 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: - 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/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/subplot.py b/pygmt/src/subplot.py index 4003538df44..d7f29f210a5 100644 --- a/pygmt/src/subplot.py +++ b/pygmt/src/subplot.py @@ -6,7 +6,7 @@ from pygmt.alias import Alias, AliasSystem 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, @@ -157,8 +157,7 @@ def subplot(self, nrows=1, ncols=1, projection=None, **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"}) aliasdict = AliasSystem( J=Alias(projection, name="projection"), 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/src/velo.py b/pygmt/src/velo.py index c65c82e500f..31ad591d8ec 100644 --- a/pygmt/src/velo.py +++ b/pygmt/src/velo.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, fmt_docstring, @@ -242,11 +242,8 @@ def velo(self, data: PathLike | TableLike | None = None, projection=None, **kwar """ 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 f3e7053551e..27d08fcfcb6 100644 --- a/pygmt/src/xyz2grd.py +++ b/pygmt/src/xyz2grd.py @@ -6,7 +6,7 @@ from pygmt._typing import PathLike, TableLike from pygmt.alias import Alias, AliasSystem 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"] @@ -151,8 +151,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"}) aliasdict = AliasSystem( J=Alias(projection, name="projection"), diff --git a/pygmt/tests/test_coast.py b/pygmt/tests/test_coast.py index 78650014a80..24d00f48025 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_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_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_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_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_grdfill.py b/pygmt/tests/test_grdfill.py index ed81a934cfa..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 +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) @@ -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_grdgradient.py b/pygmt/tests/test_grdgradient.py index 5749b237aea..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 +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -80,8 +80,8 @@ 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): + 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_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..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, GMTTypeError +from pygmt.exceptions import 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) @@ -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) @@ -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) @@ -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_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_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_project.py b/pygmt/tests/test_project.py index 36917368c63..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 +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile @@ -81,12 +81,12 @@ 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): + with pytest.raises(GMTParameterError): # 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_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_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 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 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)