diff --git a/pygmt/exceptions.py b/pygmt/exceptions.py index 7fcfa4d6fb2..ae76cc12ff5 100644 --- a/pygmt/exceptions.py +++ b/pygmt/exceptions.py @@ -142,6 +142,9 @@ class GMTParameterError(GMTError): Name or a set of names of required parameters. at_least_one A set of parameter names, of which at least one must be specified. + at_most_one + A set of mutually exclusive parameter names, of which at most one can be + specified. reason Detailed reason why the parameters are invalid. """ @@ -151,6 +154,7 @@ def __init__( *, required: str | set[str] | None = None, at_least_one: set[str] | None = None, + at_most_one: set[str] | None = None, reason: str | None = None, ): msg = [] @@ -167,6 +171,12 @@ def __init__( "Missing parameter: requires at least one of " f"{', '.join(repr(par) for par in at_least_one)}." ) + if at_most_one: + msg.append( + "Mutually exclusive parameters: " + f"{', '.join(repr(par) for par in at_most_one)}. " + "Specify at most one of them." + ) if reason: msg.append(reason) super().__init__(" ".join(msg)) diff --git a/pygmt/src/grd2cpt.py b/pygmt/src/grd2cpt.py index a09ac3aa4c2..0295c71f300 100644 --- a/pygmt/src/grd2cpt.py +++ b/pygmt/src/grd2cpt.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, kwargs_to_strings, use_alias __doctest_skip__ = ["grd2cpt"] @@ -199,8 +199,7 @@ def grd2cpt( >>> 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(at_most_one={"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 4825d934828..adaa50c17ca 100644 --- a/pygmt/src/grdfill.py +++ b/pygmt/src/grdfill.py @@ -10,7 +10,7 @@ from pygmt._typing import PathLike from pygmt.alias import Alias, AliasSystem 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, fmt_docstring, use_alias __doctest_skip__ = ["grdfill"] @@ -31,41 +31,31 @@ def _validate_params( >>> _validate_params(constant_fill=20.0, grid_fill="bggrid.nc") Traceback (most recent call last): ... - pygmt.exceptions.GMTInvalidInput: Parameters ... are mutually exclusive. + pygmt.exceptions.GMTParameterError: Mutually exclusive parameters: ... >>> _validate_params(constant_fill=20.0, inquire=True) Traceback (most recent call last): ... - pygmt.exceptions.GMTInvalidInput: Parameters ... are mutually exclusive. + pygmt.exceptions.GMTParameterError: Mutually exclusive parameters: ... >>> _validate_params() Traceback (most recent call last): ... pygmt.exceptions.GMTParameterError: Missing parameter: requires at least one ... """ - _fill_params = "'constant_fill'/'grid_fill'/'neighbor_fill'/'spline_fill'" - - n_given = sum( - param is not None and param is not False - for param in [ - constant_fill, - grid_fill, - neighbor_fill, - spline_fill, - inquire, - ] - ) - if n_given > 1: # More than one mutually exclusive parameter is given. - msg = f"Parameters {_fill_params}/'inquire' are mutually exclusive." - raise GMTInvalidInput(msg) - if n_given == 0: # No parameters are given. - raise GMTParameterError( - at_least_one={ - "constant_fill", - "grid_fill", - "neighbor_fill", - "spline_fill", - "inquire", - } - ) + params = { + "constant_fill": constant_fill, + "grid_fill": grid_fill, + "neighbor_fill": neighbor_fill, + "spline_fill": spline_fill, + "inquire": inquire, + } + n_given = sum(param is not None and param is not False for param in params.values()) + match n_given: + case 0: + raise GMTParameterError(at_least_one=params) + case 1: + pass + case _: + raise GMTParameterError(at_most_one=params) @fmt_docstring diff --git a/pygmt/src/grdproject.py b/pygmt/src/grdproject.py index 6a5290ef5e8..9ef2ced1cb2 100644 --- a/pygmt/src/grdproject.py +++ b/pygmt/src/grdproject.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, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list, fmt_docstring, use_alias __doctest_skip__ = ["grdproject"] @@ -121,8 +121,7 @@ def grdproject( # noqa: PLR0913 raise GMTParameterError(required="projection") if kwargs.get("M", unit) is not None and kwargs.get("F", scaling) is not False: - msg = "Cannot use both 'unit' and 'scaling'." - raise GMTInvalidInput(msg) + raise GMTParameterError(at_most_one={"unit", "scaling"}) aliasdict = AliasSystem( C=Alias(center, name="center", sep="/", size=2), diff --git a/pygmt/src/grdsample.py b/pygmt/src/grdsample.py index ff96c31e7e1..7b799829e5d 100644 --- a/pygmt/src/grdsample.py +++ b/pygmt/src/grdsample.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, @@ -100,10 +100,8 @@ def grdsample( >>> # and set both x- and y-spacings to 0.5 arc-degrees >>> new_grid = pygmt.grdsample(grid=grid, toggle=True, spacing=[0.5, 0.5]) """ - # Enforce mutual exclusivity between -T (toggle) and -r (registration) if kwargs.get("T", toggle) and kwargs.get("r", registration): - msg = "Parameters 'toggle' and 'registration' cannot be used together." - raise GMTInvalidInput(msg) + raise GMTParameterError(at_most_one={"toggle", "registration"}) aliasdict = AliasSystem( I=Alias(spacing, name="spacing", sep="/", size=2), diff --git a/pygmt/src/grdtrack.py b/pygmt/src/grdtrack.py index ed4c04bcb94..0286ea9690e 100644 --- a/pygmt/src/grdtrack.py +++ b/pygmt/src/grdtrack.py @@ -11,7 +11,7 @@ from pygmt._typing import PathLike, TableLike from pygmt.alias import AliasSystem 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, @@ -299,12 +299,10 @@ 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(at_most_one={"points", "profile"}) if points is None and kwargs.get("E") is None: - msg = "Must give 'points' or set 'profile'." - raise GMTInvalidInput(msg) + raise GMTParameterError(at_least_one={"points", "profile"}) if hasattr(points, "columns") and newcolname is None: raise GMTParameterError( diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index c12bb1bbf05..3b220ee41c8 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -8,7 +8,7 @@ from pygmt._typing import AnchorCode 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 from pygmt.params import Box, Position from pygmt.src._common import _parse_position @@ -103,10 +103,8 @@ def logo( # noqa: PLR0913 kwdict={"width": width, "height": height}, ) - # width and height are mutually exclusive. if width is not None and height is not None: - msg = "Cannot specify both 'width' and 'height'." - raise GMTInvalidInput(msg) + raise GMTParameterError(at_most_one={"width", "height"}) aliasdict = AliasSystem( D=[ diff --git a/pygmt/src/makecpt.py b/pygmt/src/makecpt.py index 4b9d3392927..1f6e504e053 100644 --- a/pygmt/src/makecpt.py +++ b/pygmt/src/makecpt.py @@ -7,7 +7,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 build_arg_list, fmt_docstring, kwargs_to_strings, use_alias @@ -169,8 +169,7 @@ def makecpt( ``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(at_most_one={"categorical", "cyclic"}) if (output := kwargs.pop("H", None)) is not None: kwargs["H"] = True diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py index f7c92bebc6e..54740704586 100644 --- a/pygmt/src/plot.py +++ b/pygmt/src/plot.py @@ -8,7 +8,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, @@ -272,8 +272,7 @@ def plot( # noqa: PLR0912, PLR0913 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(at_most_one={"data", "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 d4ce2940488..3f4e4b5cb5d 100644 --- a/pygmt/src/plot3d.py +++ b/pygmt/src/plot3d.py @@ -8,7 +8,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, @@ -252,8 +252,7 @@ def plot3d( # noqa: PLR0912, PLR0913 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(at_most_one={"data", "x/y/z"}) for name, value in [ ("direction", direction), diff --git a/pygmt/src/subplot.py b/pygmt/src/subplot.py index 0ef2ed37e34..62e37438be8 100644 --- a/pygmt/src/subplot.py +++ b/pygmt/src/subplot.py @@ -8,7 +8,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, kwargs_to_strings, use_alias @@ -166,8 +166,7 @@ def subplot( ) if kwargs.get("Ff") and kwargs.get("Fs"): - msg = "Please provide either one of 'figsize' or 'subsize' only." - raise GMTInvalidInput(msg) + raise GMTParameterError(at_most_one={"figsize", "subsize"}) aliasdict = AliasSystem( M=Alias(margins, name="margins", sep="/", size=(2, 4)), diff --git a/pygmt/tests/test_grd2cpt.py b/pygmt/tests/test_grd2cpt.py index 38e35a919a2..b50522475fb 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="SCM/batlow", categorical=True, cyclic=True) diff --git a/pygmt/tests/test_grdfill.py b/pygmt/tests/test_grdfill.py index d52f3261f7f..66e7dc552d8 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 @@ -154,5 +154,5 @@ 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, constant_fill=20) diff --git a/pygmt/tests/test_grdproject.py b/pygmt/tests/test_grdproject.py index 4207b4a4521..e1def13d90d 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, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -86,7 +86,7 @@ def test_grdproject_unit_scaling(grid): Test that the input validation to prevent passing both 'unit' and 'scaling' is performed. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdproject( grid=grid, projection="M10c", diff --git a/pygmt/tests/test_grdsample.py b/pygmt/tests/test_grdsample.py index 43411573508..ce14ef99bb0 100644 --- a/pygmt/tests/test_grdsample.py +++ b/pygmt/tests/test_grdsample.py @@ -8,7 +8,7 @@ import xarray as xr from pygmt import grdsample 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 @@ -101,5 +101,5 @@ def test_grdsample_toggle_and_registration_mutually_exclusive(grid): """ Raise an exception if toggle and registration are both set. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): grdsample(grid=grid, toggle=True, registration="pixel") diff --git a/pygmt/tests/test_grdtrack.py b/pygmt/tests/test_grdtrack.py index c6d45958094..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 @@ -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_logo.py b/pygmt/tests/test_logo.py index ad70acf62cd..95fc5fa8534 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -4,7 +4,7 @@ import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput, GMTParameterError from pygmt.params import Position @@ -57,7 +57,7 @@ def test_logo_width_and_height(): Test that an error is raised when both width and height are specified. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.logo(width="5c", height="5c") diff --git a/pygmt/tests/test_makecpt.py b/pygmt/tests/test_makecpt.py index 5f9193e31e4..2f0d86bcaf4 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="SCM/batlow", categorical=True, cyclic=True) diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index 06d816457c9..20da1814cac 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 GMTInvalidInput, GMTParameterError, GMTTypeError from pygmt.helpers import GMTTempFile POINTS_DATA = Path(__file__).parent / "data" / "points.txt" @@ -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 f15f9101fad..caf9e3cdddd 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 GMTInvalidInput, GMTParameterError, GMTTypeError from pygmt.helpers import GMTTempFile POINTS_DATA = Path(__file__).parent / "data" / "points.txt" @@ -97,7 +97,7 @@ def test_plot3d_fail_no_data(data, region): 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_subplot.py b/pygmt/tests/test_subplot.py index b6143c1ac21..5fd96cea60d 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 from pygmt.params import Position @@ -90,7 +90,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