Skip to content

Commit

Permalink
Session.call_module: Support passing a list of argument strings (#3139)
Browse files Browse the repository at this point in the history
Co-authored-by: Yvonne Fröhlich <[email protected]>
Co-authored-by: Michael Grund <[email protected]>
  • Loading branch information
3 people authored Mar 31, 2024
1 parent 85d4ed2 commit bbf909d
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 18 deletions.
56 changes: 43 additions & 13 deletions pygmt/clib/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,25 +592,36 @@ def get_common(self, option):
# the function return value (i.e., 'status')
return status

def call_module(self, module, args):
def call_module(self, module: str, args: str | list[str]):
"""
Call a GMT module with the given arguments.
Makes a call to ``GMT_Call_Module`` from the C API using mode
``GMT_MODULE_CMD`` (arguments passed as a single string).
Wraps ``GMT_Call_Module``.
Most interactions with the C API are done through this function.
The ``GMT_Call_Module`` API function supports passing module arguments in three
different ways:
1. Pass a single string that contains whitespace-separated module arguments.
2. Pass a list of strings and each string contains a module argument.
3. Pass a list of ``GMT_OPTION`` data structure.
Both options 1 and 2 are implemented in this function, but option 2 is preferred
because it can correctly handle special characters like whitespaces and
quotation marks in module arguments.
Parameters
----------
module : str
Module name (``'coast'``, ``'basemap'``, etc).
args : str
String with the command line arguments that will be passed to the
module (for example, ``'-R0/5/0/10 -JM'``).
module
The GMT module name to be called (``"coast"``, ``"basemap"``, etc).
args
Module arguments that will be passed to the GMT module. It can be either
a single string (e.g., ``"-R0/5/0/10 -JX10c -BWSen+t'My Title'"``) or a list
of strings (e.g., ``["-R0/5/0/10", "-JX10c", "-BWSEN+tMy Title"]``).
Raises
------
GMTInvalidInput
If the ``args`` argument is not a string or a list of strings.
GMTCLibError
If the returned status code of the function is non-zero.
"""
Expand All @@ -620,10 +631,29 @@ def call_module(self, module, args):
restype=ctp.c_int,
)

mode = self["GMT_MODULE_CMD"]
status = c_call_module(
self.session_pointer, module.encode(), mode, args.encode()
)
# 'args' can be (1) a single string or (2) a list of strings.
argv: bytes | ctp.Array[ctp.c_char_p] | None
if isinstance(args, str):
# 'args' is a single string that contains whitespace-separated arguments.
# In this way, we need to correctly handle option arguments that contain
# whitespaces or quotation marks. It's used in PyGMT <= v0.11.0 but is no
# longer recommended.
mode = self["GMT_MODULE_CMD"]
argv = args.encode()
elif isinstance(args, list):
# 'args' is a list of strings and each string contains a module argument.
# In this way, GMT can correctly handle option arguments with whitespaces or
# quotation marks. This is the preferred way to pass arguments to the GMT
# API and is used for PyGMT >= v0.12.0.
mode = len(args) # 'mode' is the number of arguments.
# Pass a null pointer if no arguments are specified.
argv = strings_to_ctypes_array(args) if mode != 0 else None
else:
raise GMTInvalidInput(
"'args' must be either a string or a list of strings."
)

status = c_call_module(self.session_pointer, module.encode(), mode, argv)
if status != 0:
raise GMTCLibError(
f"Module '{module}' failed with status code {status}:\n{self._error_message}"
Expand Down
40 changes: 35 additions & 5 deletions pygmt/tests/test_clib.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,20 @@ def test_destroy_session_fails():
@pytest.mark.benchmark
def test_call_module():
"""
Run a command to see if call_module works.
Call a GMT module by passing a list of arguments.
"""
with clib.Session() as lib:
with GMTTempFile() as out_fname:
lib.call_module("info", [str(POINTS_DATA), "-C", f"->{out_fname.name}"])
assert Path(out_fname.name).stat().st_size > 0
output = out_fname.read().strip()
assert output == "11.5309 61.7074 -2.9289 7.8648 0.1412 0.9338"


def test_call_module_argument_string():
"""
Call a GMT module by passing a single argument string.
"""
out_fname = "test_call_module.txt"
with clib.Session() as lib:
with GMTTempFile() as out_fname:
lib.call_module("info", f"{POINTS_DATA} -C ->{out_fname.name}")
Expand All @@ -144,9 +155,28 @@ def test_call_module():
assert output == "11.5309 61.7074 -2.9289 7.8648 0.1412 0.9338"


def test_call_module_empty_argument():
"""
call_module should work if an empty string or an empty list is passed as argument.
"""
with clib.Session() as lib:
lib.call_module("defaults", "")
with clib.Session() as lib:
lib.call_module("defaults", [])


def test_call_module_invalid_argument_type():
"""
call_module only accepts a string or a list of strings as module arguments.
"""
with clib.Session() as lib:
with pytest.raises(GMTInvalidInput):
lib.call_module("get", ("FONT_TITLE", "FONT_TAG"))


def test_call_module_invalid_arguments():
"""
Fails for invalid module arguments.
call_module should fail for invalid module arguments.
"""
with clib.Session() as lib:
with pytest.raises(GMTCLibError):
Expand All @@ -155,7 +185,7 @@ def test_call_module_invalid_arguments():

def test_call_module_invalid_name():
"""
Fails when given bad input.
call_module should fail when an invalid module name is given.
"""
with clib.Session() as lib:
with pytest.raises(GMTCLibError):
Expand All @@ -164,7 +194,7 @@ def test_call_module_invalid_name():

def test_call_module_error_message():
"""
Check is the GMT error message was captured.
Check if the GMT error message was captured when calling a module.
"""
with clib.Session() as lib:
with pytest.raises(GMTCLibError) as exc_info:
Expand Down

0 comments on commit bbf909d

Please sign in to comment.