Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d023f6e
Add to API doc
seisman Jul 15, 2025
19d3df2
Add new exception GMTParameterError for missing required parameters
seisman Jul 21, 2025
d553a25
Use GMTParameterError for missing required parameters
seisman Jul 21, 2025
11fd4a0
Use GMTParameteError for mutally exclusive parameters
seisman Jul 21, 2025
e51bb31
Use GMTParameterError for parameters where at least one is required
seisman Jul 21, 2025
b14dccf
Merge branch 'main' into exception/parametererror
seisman Jul 26, 2025
170d11b
More fixes
seisman Jul 26, 2025
c2c81d7
Improve exception
seisman Jul 26, 2025
c42c587
Fix typing issue
seisman Jul 26, 2025
eb93ba4
Merge branch 'main' into exception/parametererror
seisman Jul 29, 2025
f558ed5
Merge branch 'main' into exception/parametererror
seisman Aug 7, 2025
c6b6652
Merge branch 'main' into exception/parametererror
seisman Aug 8, 2025
a02a089
Merge branch 'main' into exception/parametererror
seisman Aug 10, 2025
b4829b6
Migrate more exceptions to GMTParameterError
seisman Aug 10, 2025
5a18415
Merge branch 'main' into exception/parametererror
seisman Aug 21, 2025
8d9a122
Fix the order in doc/api
seisman Aug 21, 2025
2d6fc06
Merge branch 'main' into exception/parametererror
seisman Aug 23, 2025
c8b7a7d
Merge branch 'main' into exception/parametererror
seisman Sep 6, 2025
7924399
Merge branch 'main' into exception/parametererror
seisman Sep 16, 2025
9bf8c07
Merge branch 'main' into exception/parametererror
seisman Sep 24, 2025
e1580cc
Merge branch 'main' into exception/parametererror
seisman Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
47 changes: 46 additions & 1 deletion pygmt/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
All exceptions derive from GMTError.
"""

from collections.abc import Iterable
from collections.abc import Iterable, Set
from typing import Any


Expand Down Expand Up @@ -130,3 +130,48 @@ 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 = (
"Required parameter(s) are missing: "
f"{', '.join(repr(par) for par in required)}."
)
Comment on lines 160 to 164
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this case where the parameter(s) is required/compulsory, I'm wondering if we can move towards having the error being thrown natively by Python, instead of writing code like:

if kwargs.get("F") is None:
    raise GMTParameterError(required={"filter_type"})

Should we revisit #2896 which mentions PEP692, and also think about using PEP655's typing.Required qualifier (https://typing.python.org/en/latest/spec/callables.html#required-and-non-required-keys)? Maybe a step towards #262 is to disallow single character arguments for required parameters, so that we can have native Python errors?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main issue is that, with the new alias system #4000 implemented, **kwargs will no longer be needed. We will still keep **kwargs to support single-letter option flags, but other parameters won't be passed via kwargs anymore.

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: "
f"{', '.join(repr(par) for par in exclusive)}."
)
if reason:
msg += f" {reason}"
super().__init__(msg)
17 changes: 12 additions & 5 deletions pygmt/src/coast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))

Expand Down
8 changes: 2 additions & 6 deletions pygmt/src/dimfilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions pygmt/src/filter1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 2 additions & 3 deletions pygmt/src/grd2cpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions pygmt/src/grdclip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down
13 changes: 6 additions & 7 deletions pygmt/src/grdfill.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand All @@ -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 "
Expand Down
9 changes: 3 additions & 6 deletions pygmt/src/grdgradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions pygmt/src/grdlandmask.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down
5 changes: 2 additions & 3 deletions pygmt/src/grdproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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 (
Expand Down
14 changes: 7 additions & 7 deletions pygmt/src/grdtrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 2 additions & 3 deletions pygmt/src/makecpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions pygmt/src/meca.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)


Expand Down
8 changes: 3 additions & 5 deletions pygmt/src/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -223,14 +223,12 @@ 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)
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)

Expand Down
Loading
Loading