Skip to content

Add new exception GMTParameterError for invalid parameters #4003

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
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)}."
)
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