Skip to content

Commit bbf909d

Browse files
seismanyvonnefroehlichmichaelgrund
authored
Session.call_module: Support passing a list of argument strings (#3139)
Co-authored-by: Yvonne Fröhlich <[email protected]> Co-authored-by: Michael Grund <[email protected]>
1 parent 85d4ed2 commit bbf909d

File tree

2 files changed

+78
-18
lines changed

2 files changed

+78
-18
lines changed

pygmt/clib/session.py

+43-13
Original file line numberDiff line numberDiff line change
@@ -592,25 +592,36 @@ def get_common(self, option):
592592
# the function return value (i.e., 'status')
593593
return status
594594

595-
def call_module(self, module, args):
595+
def call_module(self, module: str, args: str | list[str]):
596596
"""
597597
Call a GMT module with the given arguments.
598598
599-
Makes a call to ``GMT_Call_Module`` from the C API using mode
600-
``GMT_MODULE_CMD`` (arguments passed as a single string).
599+
Wraps ``GMT_Call_Module``.
601600
602-
Most interactions with the C API are done through this function.
601+
The ``GMT_Call_Module`` API function supports passing module arguments in three
602+
different ways:
603+
604+
1. Pass a single string that contains whitespace-separated module arguments.
605+
2. Pass a list of strings and each string contains a module argument.
606+
3. Pass a list of ``GMT_OPTION`` data structure.
607+
608+
Both options 1 and 2 are implemented in this function, but option 2 is preferred
609+
because it can correctly handle special characters like whitespaces and
610+
quotation marks in module arguments.
603611
604612
Parameters
605613
----------
606-
module : str
607-
Module name (``'coast'``, ``'basemap'``, etc).
608-
args : str
609-
String with the command line arguments that will be passed to the
610-
module (for example, ``'-R0/5/0/10 -JM'``).
614+
module
615+
The GMT module name to be called (``"coast"``, ``"basemap"``, etc).
616+
args
617+
Module arguments that will be passed to the GMT module. It can be either
618+
a single string (e.g., ``"-R0/5/0/10 -JX10c -BWSen+t'My Title'"``) or a list
619+
of strings (e.g., ``["-R0/5/0/10", "-JX10c", "-BWSEN+tMy Title"]``).
611620
612621
Raises
613622
------
623+
GMTInvalidInput
624+
If the ``args`` argument is not a string or a list of strings.
614625
GMTCLibError
615626
If the returned status code of the function is non-zero.
616627
"""
@@ -620,10 +631,29 @@ def call_module(self, module, args):
620631
restype=ctp.c_int,
621632
)
622633

623-
mode = self["GMT_MODULE_CMD"]
624-
status = c_call_module(
625-
self.session_pointer, module.encode(), mode, args.encode()
626-
)
634+
# 'args' can be (1) a single string or (2) a list of strings.
635+
argv: bytes | ctp.Array[ctp.c_char_p] | None
636+
if isinstance(args, str):
637+
# 'args' is a single string that contains whitespace-separated arguments.
638+
# In this way, we need to correctly handle option arguments that contain
639+
# whitespaces or quotation marks. It's used in PyGMT <= v0.11.0 but is no
640+
# longer recommended.
641+
mode = self["GMT_MODULE_CMD"]
642+
argv = args.encode()
643+
elif isinstance(args, list):
644+
# 'args' is a list of strings and each string contains a module argument.
645+
# In this way, GMT can correctly handle option arguments with whitespaces or
646+
# quotation marks. This is the preferred way to pass arguments to the GMT
647+
# API and is used for PyGMT >= v0.12.0.
648+
mode = len(args) # 'mode' is the number of arguments.
649+
# Pass a null pointer if no arguments are specified.
650+
argv = strings_to_ctypes_array(args) if mode != 0 else None
651+
else:
652+
raise GMTInvalidInput(
653+
"'args' must be either a string or a list of strings."
654+
)
655+
656+
status = c_call_module(self.session_pointer, module.encode(), mode, argv)
627657
if status != 0:
628658
raise GMTCLibError(
629659
f"Module '{module}' failed with status code {status}:\n{self._error_message}"

pygmt/tests/test_clib.py

+35-5
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,20 @@ def test_destroy_session_fails():
133133
@pytest.mark.benchmark
134134
def test_call_module():
135135
"""
136-
Run a command to see if call_module works.
136+
Call a GMT module by passing a list of arguments.
137+
"""
138+
with clib.Session() as lib:
139+
with GMTTempFile() as out_fname:
140+
lib.call_module("info", [str(POINTS_DATA), "-C", f"->{out_fname.name}"])
141+
assert Path(out_fname.name).stat().st_size > 0
142+
output = out_fname.read().strip()
143+
assert output == "11.5309 61.7074 -2.9289 7.8648 0.1412 0.9338"
144+
145+
146+
def test_call_module_argument_string():
147+
"""
148+
Call a GMT module by passing a single argument string.
137149
"""
138-
out_fname = "test_call_module.txt"
139150
with clib.Session() as lib:
140151
with GMTTempFile() as out_fname:
141152
lib.call_module("info", f"{POINTS_DATA} -C ->{out_fname.name}")
@@ -144,9 +155,28 @@ def test_call_module():
144155
assert output == "11.5309 61.7074 -2.9289 7.8648 0.1412 0.9338"
145156

146157

158+
def test_call_module_empty_argument():
159+
"""
160+
call_module should work if an empty string or an empty list is passed as argument.
161+
"""
162+
with clib.Session() as lib:
163+
lib.call_module("defaults", "")
164+
with clib.Session() as lib:
165+
lib.call_module("defaults", [])
166+
167+
168+
def test_call_module_invalid_argument_type():
169+
"""
170+
call_module only accepts a string or a list of strings as module arguments.
171+
"""
172+
with clib.Session() as lib:
173+
with pytest.raises(GMTInvalidInput):
174+
lib.call_module("get", ("FONT_TITLE", "FONT_TAG"))
175+
176+
147177
def test_call_module_invalid_arguments():
148178
"""
149-
Fails for invalid module arguments.
179+
call_module should fail for invalid module arguments.
150180
"""
151181
with clib.Session() as lib:
152182
with pytest.raises(GMTCLibError):
@@ -155,7 +185,7 @@ def test_call_module_invalid_arguments():
155185

156186
def test_call_module_invalid_name():
157187
"""
158-
Fails when given bad input.
188+
call_module should fail when an invalid module name is given.
159189
"""
160190
with clib.Session() as lib:
161191
with pytest.raises(GMTCLibError):
@@ -164,7 +194,7 @@ def test_call_module_invalid_name():
164194

165195
def test_call_module_error_message():
166196
"""
167-
Check is the GMT error message was captured.
197+
Check if the GMT error message was captured when calling a module.
168198
"""
169199
with clib.Session() as lib:
170200
with pytest.raises(GMTCLibError) as exc_info:

0 commit comments

Comments
 (0)