Skip to content
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

Session.call_module: Support passing a list of argument strings #3139

Merged
merged 11 commits into from
Mar 31, 2024
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 contains
seisman marked this conversation as resolved.
Show resolved Hide resolved
# whitepsaces or quotation marks. It's used in PyGMT <= v0.11.0 but is no
seisman marked this conversation as resolved.
Show resolved Hide resolved
# 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 GMT API
seisman marked this conversation as resolved.
Show resolved Hide resolved
# 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."
seisman marked this conversation as resolved.
Show resolved Hide resolved
)

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 given an invalid module name.
seisman marked this conversation as resolved.
Show resolved Hide resolved
"""
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 is the GMT error message was captured when calling a module.
seisman marked this conversation as resolved.
Show resolved Hide resolved
"""
with clib.Session() as lib:
with pytest.raises(GMTCLibError) as exc_info:
Expand Down