diff --git a/pygmt/helpers/__init__.py b/pygmt/helpers/__init__.py index 0d074061ba1..8cebc43cb77 100644 --- a/pygmt/helpers/__init__.py +++ b/pygmt/helpers/__init__.py @@ -23,5 +23,6 @@ is_nonstr_iter, launch_external_viewer, non_ascii_to_octal, + sequence_join, ) from pygmt.helpers.validators import validate_output_table_type diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 9e8858faec3..387d61e03b3 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -711,3 +711,120 @@ def args_in_kwargs(args: Sequence[str], kwargs: dict[str, Any]) -> bool: return any( kwargs.get(arg) is not None and kwargs.get(arg) is not False for arg in args ) + + +def sequence_join( + value: Any, + separator: str = "/", + size: int | Sequence[int] | None = None, + ndim: int = 1, + name: str | None = None, +) -> str | list[str] | None | Any: + """ + Join a sequence of values into a string separated by a separator. + + A 1-D sequence will be joined into a single string. A 2-D sequence will be joined + into a list of strings. Non-sequence values will be returned as is. + + Parameters + ---------- + value + The 1-D or 2-D sequence of values to join. + separator + The separator to join the values. + size + Expected size of the 1-D sequence. It can be either an integer or a sequence of + integers. If an integer, it is the expected size of the 1-D sequence. If it is a + sequence, it is the allowed sizes of the 1-D sequence. + ndim + The expected maximum number of dimensions of the sequence. + name + The name of the parameter to be used in the error message. + + Returns + ------- + joined_value + The joined string or list of strings. + + Examples + -------- + >>> sequence_join("1/2/3/4") + '1/2/3/4' + >>> sequence_join(None) + >>> sequence_join(True) + True + >>> sequence_join(False) + False + + >>> sequence_join([]) + Traceback (most recent call last): + ... + pygmt.exceptions.GMTInvalidInput: Expected a sequence but got an empty sequence. + + >>> sequence_join([1, 2, 3, 4]) + '1/2/3/4' + >>> sequence_join([1, 2, 3, 4], separator=",") + '1,2,3,4' + >>> sequence_join([1, 2, 3, 4], separator="/", size=4) + '1/2/3/4' + >>> sequence_join([1, 2, 3, 4], separator="/", size=[2, 4]) + '1/2/3/4' + >>> sequence_join([1, 2, 3, 4], separator="/", size=[2, 4], ndim=2) + '1/2/3/4' + >>> sequence_join([1, 2, 3, 4], separator="/", size=2) + Traceback (most recent call last): + ... + pygmt.exceptions.GMTInvalidInput: Expected a sequence of 2 values, but got 4 values. + >>> sequence_join([1, 2, 3, 4, 5], separator="/", size=[2, 4], name="parname") + Traceback (most recent call last): + ... + pygmt.exceptions.GMTInvalidInput: Parameter 'parname': Expected ... + + >>> sequence_join([[1, 2], [3, 4]], separator="/") + Traceback (most recent call last): + ... + pygmt.exceptions.GMTInvalidInput: Expected a 1-D ..., but a 2-D sequence is given. + >>> sequence_join([[1, 2], [3, 4]], separator="/", ndim=2) + ['1/2', '3/4'] + >>> sequence_join([[1, 2], [3, 4]], separator="/", size=2, ndim=2) + ['1/2', '3/4'] + >>> sequence_join([[1, 2], [3, 4]], separator="/", size=4, ndim=2) + Traceback (most recent call last): + ... + pygmt.exceptions.GMTInvalidInput: Expected a sequence of 4 values. + >>> sequence_join([[1, 2], [3, 4]], separator="/", size=[2, 4], ndim=2) + ['1/2', '3/4'] + """ + # Return the original value if it is not a sequence (e.g., None, bool, or str). + if not is_nonstr_iter(value): + return value + # Now it must be a sequence. + + # Change size to a list to simplify the checks. + size = [size] if isinstance(size, int) else size + errmsg = { + "name": f"Parameter '{name}': " if name else "", + "sizes": ", ".join(str(s) for s in size) if size is not None else "", + } + + if len(value) == 0: + msg = f"{errmsg['name']}Expected a sequence but got an empty sequence." + raise GMTInvalidInput(msg) + + if not is_nonstr_iter(value[0]): # 1-D sequence. + if size is not None and len(value) not in size: + msg = ( + f"{errmsg['name']}Expected a sequence of {errmsg['sizes']} values, " + f"but got {len(value)} values." + ) + raise GMTInvalidInput(msg) + return separator.join(str(v) for v in value) + + # Now it must be a 2-D sequence. + if ndim == 1: + msg = f"{errmsg['name']}Expected a 1-D sequence, but a 2-D sequence is given." + raise GMTInvalidInput(msg) + if size is not None and any(len(i) not in size for i in value): + msg = f"{errmsg['name']}Expected a sequence of {errmsg['sizes']} values." + raise GMTInvalidInput(msg) + return [separator.join(str(j) for j in sub) for sub in value] diff --git a/pygmt/src/grdclip.py b/pygmt/src/grdclip.py index 58ce6bd1e70..8bd32400148 100644 --- a/pygmt/src/grdclip.py +++ b/pygmt/src/grdclip.py @@ -12,100 +12,14 @@ build_arg_list, deprecate_parameter, fmt_docstring, - is_nonstr_iter, kwargs_to_strings, + sequence_join, use_alias, ) __doctest_skip__ = ["grdclip"] -def _parse_sequence(name, value, separator="/", size=2, ndim=1): - """ - Parse a 1-D or 2-D sequence of values and join them by a separator. - - Parameters - ---------- - name - The parameter name. - value - The 1-D or 2-D sequence of values to parse. - separator - The separator to join the values. - size - The number of values in the sequence. - ndim - The expected maximum number of dimensions of the sequence. - - Returns - ------- - str - The parsed sequence. - - Examples - -------- - >>> _parse_sequence("above_or_below", [1000, 0], size=2, ndim=1) - '1000/0' - >>> _parse_sequence("between", [1000, 1500, 10000], size=3, ndim=2) - '1000/1500/10000' - >>> _parse_sequence("between", [[1000, 1500, 10000]], size=3, ndim=2) - ['1000/1500/10000'] - >>> _parse_sequence( - ... "between", [[1000, 1500, 10000], [1500, 2000, 20000]], size=3, ndim=2 - ... ) - ['1000/1500/10000', '1500/2000/20000'] - >>> _parse_sequence("replace", [1000, 0], size=2, ndim=2) - '1000/0' - >>> _parse_sequence("replace", [[1000, 0]], size=2, ndim=2) - ['1000/0'] - >>> _parse_sequence("replace", [[1000, 0], [1500, 10000]], size=2, ndim=2) - ['1000/0', '1500/10000'] - >>> _parse_sequence("any", "1000/100") - '1000/100' - >>> _parse_sequence("any", None) - >>> _parse_sequence("any", []) - [] - >>> _parse_sequence("above_or_below", [[100, 1000], [1500, 2000]], size=2, ndim=1) - Traceback (most recent call last): - ... - pygmt.exceptions.GMTInvalidInput: Parameter ... must be a 1-D sequence... - >>> _parse_sequence("above_or_below", [100, 200, 300], size=2, ndim=1) - Traceback (most recent call last): - ... - pygmt.exceptions.GMTInvalidInput: Parameter ... must be a 1-D sequence ... - >>> _parse_sequence("between", [[100, 200, 300], [500, 600]], size=3, ndim=2) - Traceback (most recent call last): - ... - pygmt.exceptions.GMTInvalidInput: Parameter ... must be a 2-D sequence with ... - """ - # Return the value as is if not a sequence (e.g., str or None) or empty. - if not is_nonstr_iter(value) or len(value) == 0: - return value - - # 1-D sequence - if not is_nonstr_iter(value[0]): - if len(value) != size: - msg = ( - f"Parameter '{name}' must be a 1-D sequence of {size} values, " - f"but got {len(value)} values." - ) - raise GMTInvalidInput(msg) - return separator.join(str(i) for i in value) - - # 2-D sequence - if ndim == 1: - msg = f"Parameter '{name}' must be a 1-D sequence, not a 2-D sequence." - raise GMTInvalidInput(msg) - - if any(len(i) != size for i in value): - msg = ( - f"Parameter '{name}' must be a 2-D sequence with each sub-sequence " - f"having {size} values." - ) - raise GMTInvalidInput(msg) - return [separator.join(str(j) for j in value[i]) for i in range(len(value))] - - # TODO(PyGMT>=0.19.0): Remove the deprecated "new" parameter. @fmt_docstring @deprecate_parameter("new", "replace", "v0.15.0", remove_version="v0.19.0") @@ -198,10 +112,10 @@ def grdclip( raise GMTInvalidInput(msg) # Parse the -S option. - kwargs["Sa"] = _parse_sequence("above", above, size=2, ndim=1) - kwargs["Sb"] = _parse_sequence("below", below, size=2, ndim=1) - kwargs["Si"] = _parse_sequence("between", between, size=3, ndim=2) - kwargs["Sr"] = _parse_sequence("replace", replace, size=2, ndim=2) + kwargs["Sa"] = sequence_join(above, size=2, name="above") + kwargs["Sb"] = sequence_join(below, size=2, name="below") + kwargs["Si"] = sequence_join(between, size=3, ndim=2, name="between") + kwargs["Sr"] = sequence_join(replace, size=2, ndim=2, name="replace") with Session() as lib: with (