diff --git a/pygmt/alias.py b/pygmt/alias.py index 958a08a4607..f9aa3f69109 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -7,7 +7,7 @@ from collections.abc import Mapping, Sequence from typing import Any, Literal -from pygmt.exceptions import GMTInvalidInput, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTValueError from pygmt.helpers.utils import is_nonstr_iter, sequence_join @@ -404,27 +404,28 @@ def merge(self, kwargs: Mapping[str, Any]): # Long-form parameters exist. aliases = self.aliasdict.get(short_param) - if not isinstance(aliases, Sequence): # Single Alias object. - _msg_long = f"Use long-form parameter {aliases.name!r} instead." - else: # Sequence of Alias objects. - _params = [f"{v.name!r}" for v in aliases if not v.prefix] - _modifiers = [f"{v.name!r} ({v.prefix})" for v in aliases if v.prefix] - _msg_long = ( - f"Use long-form parameters {', '.join(_params)}, " - f"with optional parameters {', '.join(_modifiers)} instead." - ) + if not isinstance(aliases, Sequence): + aliases = [aliases] + + long_params = [v.name for v in aliases] + long_params_text = ", ".join( + [ + f"{v.name!r} ({v.prefix})" + if v.prefix.startswith("+") + else f"{v.name!r}" + for v in aliases + ] + ) + msg = ( + f"Short-form parameter {short_param!r} is not recommended. " + f"Use long-form parameter(s) {long_params_text} instead." + ) - # Long-form parameters are already specified. if long_param_given: - msg = ( - f"Short-form parameter {short_param!r} conflicts with long-form " - f"parameters and is not recommended. {_msg_long}" + raise GMTParameterError( + conflicts_with=(short_param, long_params), reason=msg ) - raise GMTInvalidInput(msg) # Long-form parameters are not specified. - msg = ( - f"Short-form parameter {short_param!r} is not recommended. {_msg_long}" - ) warnings.warn(msg, category=SyntaxWarning, stacklevel=2) return self diff --git a/pygmt/exceptions.py b/pygmt/exceptions.py index fbff80dacd5..dbffb77d2c0 100644 --- a/pygmt/exceptions.py +++ b/pygmt/exceptions.py @@ -145,6 +145,9 @@ class GMTParameterError(GMTError): at_most_one A collection of mutually exclusive parameter names, of which at most one can be specified. + conflicts_with + A tuple with the parameter name and a collection of conflicting parameter names, + indicating which parameters cannot be used together. reason Detailed reason why the parameters are invalid. """ @@ -155,6 +158,7 @@ def __init__( required: str | Iterable[str] | None = None, at_least_one: Iterable[str] | None = None, at_most_one: Iterable[str] | None = None, + conflicts_with: tuple[str, Iterable[str]] | None = None, reason: str | None = None, ): msg = [] @@ -177,6 +181,12 @@ def __init__( f"{', '.join(repr(par) for par in at_most_one)}. " "Specify at most one of them." ) + if conflicts_with: + param, conflicts = conflicts_with + msg.append( + f"Conflicting parameters: {param!r} cannot be used with " + f"{', '.join(repr(c) for c in conflicts)}." + ) if reason: msg.append(reason) super().__init__(" ".join(msg)) diff --git a/pygmt/src/_common.py b/pygmt/src/_common.py index e6bb2ebf275..69066e0d26d 100644 --- a/pygmt/src/_common.py +++ b/pygmt/src/_common.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Any, ClassVar, Literal -from pygmt.exceptions import GMTInvalidInput, GMTTypeError, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTTypeError, GMTValueError from pygmt.params.position import Position from pygmt.src.which import which @@ -328,7 +328,7 @@ def _parse_position( ... ) Traceback (most recent call last): ... - pygmt.exceptions.GMTInvalidInput: Parameter 'position' is given with a raw GMT... + pygmt.exceptions.GMTParameterError: ... >>> _parse_position( ... 123, @@ -364,12 +364,10 @@ def _parse_position( position = Position(position, cstype="inside") elif kwdict is not None: # Raw GMT command string with potential conflicts. if any(v is not None and v is not False for v in kwdict.values()): - msg = ( - "Parameter 'position' is given with a raw GMT command string, " - "and conflicts with parameters " - f"{', '.join(repr(c) for c in kwdict)}." + raise GMTParameterError( + conflicts_with=("position", kwdict.keys()), + reason="'position' is specified using the unrecommended GMT command string syntax.", ) - raise GMTInvalidInput(msg) else: # No conflicting parameters to check, indicating it's a new function. # The string must be an anchor code. diff --git a/pygmt/src/grdview.py b/pygmt/src/grdview.py index f628378f9e4..d67cb1b7659 100644 --- a/pygmt/src/grdview.py +++ b/pygmt/src/grdview.py @@ -10,7 +10,7 @@ from pygmt._typing import PathLike from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session, __gmt_version__ -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTInvalidInput, GMTParameterError from pygmt.helpers import build_arg_list, deprecate_parameter, fmt_docstring, use_alias from pygmt.src.grdinfo import grdinfo @@ -71,11 +71,13 @@ def _alias_option_Q( # noqa: N802 v is not None and v is not False for v in (dpi, mesh_fill, monochrome, nan_transparent) ): - msg = ( - "Parameter 'surftype' is given with a raw GMT command string, and conflicts " - "with parameters 'dpi', 'mesh_fill', 'monochrome', or 'nan_transparent'." + raise GMTParameterError( + conflicts_with=( + "surftype", + ["dpi", "mesh_fill", "monochrome", "nan_transparent"], + ), + reason="'surftype' is specified using the unrecommended GMT command string syntax.", ) - raise GMTInvalidInput(msg) if dpi is not None and surftype != "image": msg = "Parameter 'dpi' can only be used when 'surftype' is 'image'." diff --git a/pygmt/tests/test_alias_system.py b/pygmt/tests/test_alias_system.py index e7eb5683442..f430ded16aa 100644 --- a/pygmt/tests/test_alias_system.py +++ b/pygmt/tests/test_alias_system.py @@ -4,7 +4,7 @@ import pytest from pygmt.alias import Alias, AliasSystem -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list @@ -65,21 +65,17 @@ def test_alias_system_one_alias_short_form(): """ Test that the alias system works when short-form parameters coexist. """ + _msg_conflict = "Conflicting parameters: 'J' cannot be used with 'projection'." + _msg_reason = r"Short-form parameter 'J' is not recommended. Use long-form parameter\(s\) 'projection' instead." # Long-form does not exist. assert func(A="abc") == ["-Aabc"] # Long-form exists but is not given, and short-form is given. - with pytest.warns( - SyntaxWarning, - match=r"Short-form parameter 'J' is not recommended. Use long-form parameter 'projection' instead.", - ): + with pytest.warns(SyntaxWarning, match=_msg_reason): assert func(J="X10c") == ["-JX10c"] # Coexistence of long-form and short-form parameters. - with pytest.raises( - GMTInvalidInput, - match=r"Short-form parameter 'J' conflicts with long-form parameters and is not recommended. Use long-form parameter 'projection' instead.", - ): + with pytest.raises(GMTParameterError, match=rf"{_msg_conflict} {_msg_reason}"): func(projection="X10c", J="H10c") @@ -88,18 +84,19 @@ def test_alias_system_multiple_aliases_short_form(): Test that the alias system works with multiple aliases when short-form parameters are used. """ - _msg_long = r"Use long-form parameters 'label', with optional parameters 'text' \(\+t\), 'offset' \(\+o\) instead." + _msg_conflict = ( + "Conflicting parameters: 'U' cannot be used with 'label', 'text', 'offset'." + ) + _msg_reason = r"Short-form parameter 'U' is not recommended. Use long-form parameter\(s\) 'label', 'text' \(\+t\), 'offset' \(\+o\) instead." # Long-form exists but is not given, and short-form is given. - msg = rf"Short-form parameter 'U' is not recommended. {_msg_long}" - with pytest.warns(SyntaxWarning, match=msg): + with pytest.warns(SyntaxWarning, match=_msg_reason): assert func(U="abcd+tefg") == ["-Uabcd+tefg"] # Coexistence of long-form and short-form parameters. - msg = rf"Short-form parameter 'U' conflicts with long-form parameters and is not recommended. {_msg_long}" - with pytest.raises(GMTInvalidInput, match=msg): + with pytest.raises(GMTParameterError, match=rf"{_msg_conflict} {_msg_reason}"): func(label="abcd", U="efg") - with pytest.raises(GMTInvalidInput, match=msg): + with pytest.raises(GMTParameterError, match=rf"{_msg_conflict} {_msg_reason}"): func(text="efg", U="efg") diff --git a/pygmt/tests/test_coast.py b/pygmt/tests/test_coast.py index 24d00f48025..e03ce819344 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, GMTParameterError +from pygmt.exceptions import GMTParameterError @pytest.mark.benchmark @@ -82,7 +82,7 @@ def test_coast_resolution_long_short_form_conflict(): using the long form. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.coast( region=[-180, 180, -80, 80], projection="M15c", diff --git a/pygmt/tests/test_colorbar.py b/pygmt/tests/test_colorbar.py index 3024ba38341..5cd121eada5 100644 --- a/pygmt/tests/test_colorbar.py +++ b/pygmt/tests/test_colorbar.py @@ -4,7 +4,7 @@ import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.params.position import Position @@ -50,21 +50,21 @@ def test_image_position_mixed_syntax(): Test that mixing deprecated GMT CLI syntax string with new parameters. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.colorbar(cmap="gmt/rainbow", position="x0/0", length="4c") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.colorbar(cmap="gmt/rainbow", position="x0/0", width="0.5c") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.colorbar(cmap="gmt/rainbow", position="x0/0", orientation="horizontal") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.colorbar(cmap="gmt/rainbow", position="x0/0", reverse=True) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.colorbar(cmap="gmt/rainbow", position="x0/0", nan=True) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.colorbar( cmap="gmt/rainbow", position="x0/0", fg_triangle=True, bg_triangle=True ) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.colorbar(cmap="gmt/rainbow", position="x0/0", move_text="label") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.colorbar(cmap="gmt/rainbow", position="x0/0", label_as_column=True) diff --git a/pygmt/tests/test_grdview.py b/pygmt/tests/test_grdview.py index 4e84fdcfebb..6bb5e5b6594 100644 --- a/pygmt/tests/test_grdview.py +++ b/pygmt/tests/test_grdview.py @@ -4,7 +4,7 @@ import pytest from pygmt import Figure, grdcut -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 @@ -343,13 +343,13 @@ def test_grdview_mixed_syntax(gridfile): Run grdview using grid as a file and drapegrid as an xarray.DataArray. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.grdview(grid=gridfile, cmap="SCM/oleron", surftype="i", dpi=300) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.grdview(grid=gridfile, cmap="SCM/oleron", surftype="m", mesh_fill="red") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.grdview(grid=gridfile, cmap="SCM/oleron", surftype="s", monochrome=True) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.grdview( grid=gridfile, cmap="SCM/oleron", surftype="i", nan_transparent=True ) diff --git a/pygmt/tests/test_image.py b/pygmt/tests/test_image.py index d48836a0539..b9370a3560b 100644 --- a/pygmt/tests/test_image.py +++ b/pygmt/tests/test_image.py @@ -4,7 +4,7 @@ import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.params import Box, Position @@ -66,11 +66,11 @@ def test_image_position_mixed_syntax(): and conflicts with other parameters. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.image(imagefile="@circuit.png", position="x0/0", width="4c") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.image(imagefile="@circuit.png", position="x0/0", height="3c") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.image(imagefile="@circuit.png", position="x0/0", dpi="300") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.image(imagefile="@circuit.png", position="x0/0", replicate=(2, 1)) diff --git a/pygmt/tests/test_inset.py b/pygmt/tests/test_inset.py index a5c219bf02a..8b4aceaad59 100644 --- a/pygmt/tests/test_inset.py +++ b/pygmt/tests/test_inset.py @@ -4,7 +4,7 @@ import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.params import Box, Position @@ -94,6 +94,6 @@ def test_inset_invalid_inputs(): with fig.inset(position=Position("TL"), height="5c"): pass # Old position syntax conflicts with width/height - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): with fig.inset(position="jTL+w3.5c", width="3.5c"): pass diff --git a/pygmt/tests/test_legend.py b/pygmt/tests/test_legend.py index e43b1b39cad..c5af43fa994 100644 --- a/pygmt/tests/test_legend.py +++ b/pygmt/tests/test_legend.py @@ -7,7 +7,7 @@ import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput, GMTTypeError +from pygmt.exceptions import GMTParameterError, GMTTypeError from pygmt.helpers import GMTTempFile @@ -174,9 +174,9 @@ def test_legend_position_mixed_syntax(legend_spec): spec = io.StringIO(legend_spec) fig = Figure() fig.basemap(projection="x6i", region=[0, 1, 0, 1], frame=True) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.legend(spec, position="jTL", width="5i") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.legend(spec, position="jTL", height="5i") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.legend(spec, position="jTL", line_spacing=2.0) diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index 95fc5fa8534..8772c9803ac 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, GMTParameterError +from pygmt.exceptions import GMTParameterError from pygmt.params import Position @@ -66,7 +66,7 @@ def test_logo_position_mixed_syntax(): Test that an error is raised when mixing new and deprecated syntax in 'position'. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.logo(position="jTL", width="5c") - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.logo(position="jTL", height="6c") diff --git a/pygmt/tests/test_solar.py b/pygmt/tests/test_solar.py index b63b6b40899..b4ddf78016d 100644 --- a/pygmt/tests/test_solar.py +++ b/pygmt/tests/test_solar.py @@ -6,7 +6,7 @@ import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput, GMTValueError +from pygmt.exceptions import GMTParameterError, GMTValueError @pytest.mark.mpl_image_compare @@ -84,7 +84,7 @@ def test_invalid_parameter(): arguments for 'terminator' and 'terminator_datetime'. """ fig = Figure() - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): # Use single-letter parameter 'T' for testing fig.solar( region="d", projection="W0/15c", frame="a", T="d+d1990-02-17T04:25:00" diff --git a/pygmt/tests/test_wiggle.py b/pygmt/tests/test_wiggle.py index 8e4ba6718d4..3e76d128d60 100644 --- a/pygmt/tests/test_wiggle.py +++ b/pygmt/tests/test_wiggle.py @@ -5,7 +5,7 @@ import numpy as np import pytest from pygmt import Figure -from pygmt.exceptions import GMTInvalidInput +from pygmt.exceptions import GMTParameterError from pygmt.params import Position @@ -136,11 +136,11 @@ def test_wiggle_mixed_syntax(data): "pen": "1.0p", "track": "0.5p", } - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.wiggle(position="jMR+w2+lnT", length=2, **kwargs) - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fig.wiggle(position="jMR+w2+lnT", label="nT", **kwargs) - with pytest.raises(GMTInvalidInput): - fig.wiggle(position="jMR+w2+lnT", length_alignment="left", **kwargs) + with pytest.raises(GMTParameterError): + fig.wiggle(position="jMR+w2+lnT", label_alignment="left", **kwargs)