Skip to content

Commit 438edce

Browse files
seismanCopilot
andauthored
Add a helper function sequence_join to join a sequence by a separator (#3961)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 490d9c4 commit 438edce

File tree

3 files changed

+123
-91
lines changed

3 files changed

+123
-91
lines changed

pygmt/helpers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@
2323
is_nonstr_iter,
2424
launch_external_viewer,
2525
non_ascii_to_octal,
26+
sequence_join,
2627
)
2728
from pygmt.helpers.validators import validate_output_table_type

pygmt/helpers/utils.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,3 +711,120 @@ def args_in_kwargs(args: Sequence[str], kwargs: dict[str, Any]) -> bool:
711711
return any(
712712
kwargs.get(arg) is not None and kwargs.get(arg) is not False for arg in args
713713
)
714+
715+
716+
def sequence_join(
717+
value: Any,
718+
separator: str = "/",
719+
size: int | Sequence[int] | None = None,
720+
ndim: int = 1,
721+
name: str | None = None,
722+
) -> str | list[str] | None | Any:
723+
"""
724+
Join a sequence of values into a string separated by a separator.
725+
726+
A 1-D sequence will be joined into a single string. A 2-D sequence will be joined
727+
into a list of strings. Non-sequence values will be returned as is.
728+
729+
Parameters
730+
----------
731+
value
732+
The 1-D or 2-D sequence of values to join.
733+
separator
734+
The separator to join the values.
735+
size
736+
Expected size of the 1-D sequence. It can be either an integer or a sequence of
737+
integers. If an integer, it is the expected size of the 1-D sequence. If it is a
738+
sequence, it is the allowed sizes of the 1-D sequence.
739+
ndim
740+
The expected maximum number of dimensions of the sequence.
741+
name
742+
The name of the parameter to be used in the error message.
743+
744+
Returns
745+
-------
746+
joined_value
747+
The joined string or list of strings.
748+
749+
Examples
750+
--------
751+
>>> sequence_join("1/2/3/4")
752+
'1/2/3/4'
753+
>>> sequence_join(None)
754+
>>> sequence_join(True)
755+
True
756+
>>> sequence_join(False)
757+
False
758+
759+
>>> sequence_join([])
760+
Traceback (most recent call last):
761+
...
762+
pygmt.exceptions.GMTInvalidInput: Expected a sequence but got an empty sequence.
763+
764+
>>> sequence_join([1, 2, 3, 4])
765+
'1/2/3/4'
766+
>>> sequence_join([1, 2, 3, 4], separator=",")
767+
'1,2,3,4'
768+
>>> sequence_join([1, 2, 3, 4], separator="/", size=4)
769+
'1/2/3/4'
770+
>>> sequence_join([1, 2, 3, 4], separator="/", size=[2, 4])
771+
'1/2/3/4'
772+
>>> sequence_join([1, 2, 3, 4], separator="/", size=[2, 4], ndim=2)
773+
'1/2/3/4'
774+
>>> sequence_join([1, 2, 3, 4], separator="/", size=2)
775+
Traceback (most recent call last):
776+
...
777+
pygmt.exceptions.GMTInvalidInput: Expected a sequence of 2 values, but got 4 values.
778+
>>> sequence_join([1, 2, 3, 4, 5], separator="/", size=[2, 4], name="parname")
779+
Traceback (most recent call last):
780+
...
781+
pygmt.exceptions.GMTInvalidInput: Parameter 'parname': Expected ...
782+
783+
>>> sequence_join([[1, 2], [3, 4]], separator="/")
784+
Traceback (most recent call last):
785+
...
786+
pygmt.exceptions.GMTInvalidInput: Expected a 1-D ..., but a 2-D sequence is given.
787+
>>> sequence_join([[1, 2], [3, 4]], separator="/", ndim=2)
788+
['1/2', '3/4']
789+
>>> sequence_join([[1, 2], [3, 4]], separator="/", size=2, ndim=2)
790+
['1/2', '3/4']
791+
>>> sequence_join([[1, 2], [3, 4]], separator="/", size=4, ndim=2)
792+
Traceback (most recent call last):
793+
...
794+
pygmt.exceptions.GMTInvalidInput: Expected a sequence of 4 values.
795+
>>> sequence_join([[1, 2], [3, 4]], separator="/", size=[2, 4], ndim=2)
796+
['1/2', '3/4']
797+
"""
798+
# Return the original value if it is not a sequence (e.g., None, bool, or str).
799+
if not is_nonstr_iter(value):
800+
return value
801+
# Now it must be a sequence.
802+
803+
# Change size to a list to simplify the checks.
804+
size = [size] if isinstance(size, int) else size
805+
errmsg = {
806+
"name": f"Parameter '{name}': " if name else "",
807+
"sizes": ", ".join(str(s) for s in size) if size is not None else "",
808+
}
809+
810+
if len(value) == 0:
811+
msg = f"{errmsg['name']}Expected a sequence but got an empty sequence."
812+
raise GMTInvalidInput(msg)
813+
814+
if not is_nonstr_iter(value[0]): # 1-D sequence.
815+
if size is not None and len(value) not in size:
816+
msg = (
817+
f"{errmsg['name']}Expected a sequence of {errmsg['sizes']} values, "
818+
f"but got {len(value)} values."
819+
)
820+
raise GMTInvalidInput(msg)
821+
return separator.join(str(v) for v in value)
822+
823+
# Now it must be a 2-D sequence.
824+
if ndim == 1:
825+
msg = f"{errmsg['name']}Expected a 1-D sequence, but a 2-D sequence is given."
826+
raise GMTInvalidInput(msg)
827+
if size is not None and any(len(i) not in size for i in value):
828+
msg = f"{errmsg['name']}Expected a sequence of {errmsg['sizes']} values."
829+
raise GMTInvalidInput(msg)
830+
return [separator.join(str(j) for j in sub) for sub in value]

pygmt/src/grdclip.py

Lines changed: 5 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -12,100 +12,14 @@
1212
build_arg_list,
1313
deprecate_parameter,
1414
fmt_docstring,
15-
is_nonstr_iter,
1615
kwargs_to_strings,
16+
sequence_join,
1717
use_alias,
1818
)
1919

2020
__doctest_skip__ = ["grdclip"]
2121

2222

23-
def _parse_sequence(name, value, separator="/", size=2, ndim=1):
24-
"""
25-
Parse a 1-D or 2-D sequence of values and join them by a separator.
26-
27-
Parameters
28-
----------
29-
name
30-
The parameter name.
31-
value
32-
The 1-D or 2-D sequence of values to parse.
33-
separator
34-
The separator to join the values.
35-
size
36-
The number of values in the sequence.
37-
ndim
38-
The expected maximum number of dimensions of the sequence.
39-
40-
Returns
41-
-------
42-
str
43-
The parsed sequence.
44-
45-
Examples
46-
--------
47-
>>> _parse_sequence("above_or_below", [1000, 0], size=2, ndim=1)
48-
'1000/0'
49-
>>> _parse_sequence("between", [1000, 1500, 10000], size=3, ndim=2)
50-
'1000/1500/10000'
51-
>>> _parse_sequence("between", [[1000, 1500, 10000]], size=3, ndim=2)
52-
['1000/1500/10000']
53-
>>> _parse_sequence(
54-
... "between", [[1000, 1500, 10000], [1500, 2000, 20000]], size=3, ndim=2
55-
... )
56-
['1000/1500/10000', '1500/2000/20000']
57-
>>> _parse_sequence("replace", [1000, 0], size=2, ndim=2)
58-
'1000/0'
59-
>>> _parse_sequence("replace", [[1000, 0]], size=2, ndim=2)
60-
['1000/0']
61-
>>> _parse_sequence("replace", [[1000, 0], [1500, 10000]], size=2, ndim=2)
62-
['1000/0', '1500/10000']
63-
>>> _parse_sequence("any", "1000/100")
64-
'1000/100'
65-
>>> _parse_sequence("any", None)
66-
>>> _parse_sequence("any", [])
67-
[]
68-
>>> _parse_sequence("above_or_below", [[100, 1000], [1500, 2000]], size=2, ndim=1)
69-
Traceback (most recent call last):
70-
...
71-
pygmt.exceptions.GMTInvalidInput: Parameter ... must be a 1-D sequence...
72-
>>> _parse_sequence("above_or_below", [100, 200, 300], size=2, ndim=1)
73-
Traceback (most recent call last):
74-
...
75-
pygmt.exceptions.GMTInvalidInput: Parameter ... must be a 1-D sequence ...
76-
>>> _parse_sequence("between", [[100, 200, 300], [500, 600]], size=3, ndim=2)
77-
Traceback (most recent call last):
78-
...
79-
pygmt.exceptions.GMTInvalidInput: Parameter ... must be a 2-D sequence with ...
80-
"""
81-
# Return the value as is if not a sequence (e.g., str or None) or empty.
82-
if not is_nonstr_iter(value) or len(value) == 0:
83-
return value
84-
85-
# 1-D sequence
86-
if not is_nonstr_iter(value[0]):
87-
if len(value) != size:
88-
msg = (
89-
f"Parameter '{name}' must be a 1-D sequence of {size} values, "
90-
f"but got {len(value)} values."
91-
)
92-
raise GMTInvalidInput(msg)
93-
return separator.join(str(i) for i in value)
94-
95-
# 2-D sequence
96-
if ndim == 1:
97-
msg = f"Parameter '{name}' must be a 1-D sequence, not a 2-D sequence."
98-
raise GMTInvalidInput(msg)
99-
100-
if any(len(i) != size for i in value):
101-
msg = (
102-
f"Parameter '{name}' must be a 2-D sequence with each sub-sequence "
103-
f"having {size} values."
104-
)
105-
raise GMTInvalidInput(msg)
106-
return [separator.join(str(j) for j in value[i]) for i in range(len(value))]
107-
108-
10923
# TODO(PyGMT>=0.19.0): Remove the deprecated "new" parameter.
11024
@fmt_docstring
11125
@deprecate_parameter("new", "replace", "v0.15.0", remove_version="v0.19.0")
@@ -198,10 +112,10 @@ def grdclip(
198112
raise GMTInvalidInput(msg)
199113

200114
# Parse the -S option.
201-
kwargs["Sa"] = _parse_sequence("above", above, size=2, ndim=1)
202-
kwargs["Sb"] = _parse_sequence("below", below, size=2, ndim=1)
203-
kwargs["Si"] = _parse_sequence("between", between, size=3, ndim=2)
204-
kwargs["Sr"] = _parse_sequence("replace", replace, size=2, ndim=2)
115+
kwargs["Sa"] = sequence_join(above, size=2, name="above")
116+
kwargs["Sb"] = sequence_join(below, size=2, name="below")
117+
kwargs["Si"] = sequence_join(between, size=3, ndim=2, name="between")
118+
kwargs["Sr"] = sequence_join(replace, size=2, ndim=2, name="replace")
205119

206120
with Session() as lib:
207121
with (

0 commit comments

Comments
 (0)