Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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