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

Let kwargs_to_strings work with default values and positional arguments #2826

Merged
merged 9 commits into from
Dec 15, 2023
102 changes: 70 additions & 32 deletions pygmt/helpers/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,6 @@

Examples
--------

>>> @kwargs_to_strings(
... R="sequence", i="sequence_comma", files="sequence_space"
... )
Expand Down Expand Up @@ -686,58 +685,97 @@
... ]
... )
{'R': '2005-01-01T08:00:00.000000000/2015-01-01T12:00:00.123456'}
>>> # Here is a more realistic example
>>> # See https://github.com/GenericMappingTools/pygmt/issues/2361
>>> @kwargs_to_strings(
... files="sequence_space",
... offset="sequence",
... R="sequence",
... i="sequence_comma",
... )
... def module(files, offset=("-54p", "-54p"), **kwargs):
... "A module that prints the arguments it received"
... print(files, end=" ")
... print(offset, end=" ")
... print("{", end="")
... print(
... ", ".join(f"'{k}': {repr(kwargs[k])}" for k in sorted(kwargs)),
... end="",
... )
... print("}")
>>> module(files=["data1.txt", "data2.txt"])
data1.txt data2.txt -54p/-54p {}
>>> module(["data1.txt", "data2.txt"])
data1.txt data2.txt -54p/-54p {}
>>> module(files=["data1.txt", "data2.txt"], offset=("20p", "20p"))
data1.txt data2.txt 20p/20p {}
>>> module(["data1.txt", "data2.txt"], ("20p", "20p"))
data1.txt data2.txt 20p/20p {}
>>> module(["data1.txt", "data2.txt"], ("20p", "20p"), R=[1, 2, 3, 4])
data1.txt data2.txt 20p/20p {'R': '1/2/3/4'}
"""
valid_conversions = [
"sequence",
"sequence_comma",
"sequence_plus",
"sequence_space",
]

for arg, fmt in conversions.items():
if fmt not in valid_conversions:
raise GMTInvalidInput(
f"Invalid conversion type '{fmt}' for argument '{arg}'."
)

separators = {
"sequence": "/",
"sequence_comma": ",",
"sequence_plus": "+",
"sequence_space": " ",
}

for arg, fmt in conversions.items():
if fmt not in separators:
raise GMTInvalidInput(
f"Invalid conversion type '{fmt}' for argument '{arg}'."
)

# Make the actual decorator function
def converter(module_func):
"""
The decorator that creates our new function with the conversions.
"""
sig = signature(module_func)

@functools.wraps(module_func)
def new_module(*args, **kwargs):
"""
New module instance that converts the arguments first.
"""
# Inspired by https://stackoverflow.com/a/69170441
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()

for arg, fmt in conversions.items():
if arg in kwargs:
value = kwargs[arg]
issequence = fmt in separators
if issequence and is_nonstr_iter(value):
for index, item in enumerate(value):
try:
# check if there is a space " " when converting
# a pandas.Timestamp/xr.DataArray to a string.
# If so, use np.datetime_as_string instead.
assert " " not in str(item)
except AssertionError:
# convert datetime-like item to ISO 8601
# string format like YYYY-MM-DDThh:mm:ss.ffffff
value[index] = np.datetime_as_string(
np.asarray(item, dtype=np.datetime64)
)
kwargs[arg] = separators[fmt].join(f"{item}" for item in value)
# The arg may be in args or kwargs
if arg in bound.arguments:
value = bound.arguments[arg]
elif arg in bound.arguments.get("kwargs"):
value = bound.arguments["kwargs"][arg]
else:
continue

Check warning on line 753 in pygmt/helpers/decorators.py

View check run for this annotation

Codecov / codecov/patch

pygmt/helpers/decorators.py#L753

Added line #L753 was not covered by tests

issequence = fmt in separators
if issequence and is_nonstr_iter(value):
for index, item in enumerate(value):
try:
# check if there is a space " " when converting
seisman marked this conversation as resolved.
Show resolved Hide resolved
# a pandas.Timestamp/xr.DataArray to a string.
# If so, use np.datetime_as_string instead.
assert " " not in str(item)
except AssertionError:
# convert datetime-like item to ISO 8601
seisman marked this conversation as resolved.
Show resolved Hide resolved
# string format like YYYY-MM-DDThh:mm:ss.ffffff
seisman marked this conversation as resolved.
Show resolved Hide resolved
value[index] = np.datetime_as_string(
np.asarray(item, dtype=np.datetime64)
)
newvalue = separators[fmt].join(f"{item}" for item in value)
# Changes in bound.arguments will reflect in bound.args
# and bound.kwargs.
if arg in bound.arguments:
bound.arguments[arg] = newvalue
elif arg in bound.arguments.get("kwargs"):
bound.arguments["kwargs"][arg] = newvalue

# Execute the original function and return its output
return module_func(*args, **kwargs)
return module_func(*bound.args, **bound.kwargs)

return new_module

Expand Down
4 changes: 1 addition & 3 deletions pygmt/src/subplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from pygmt.helpers import (
build_arg_string,
fmt_docstring,
is_nonstr_iter,
kwargs_to_strings,
use_alias,
)
Expand Down Expand Up @@ -172,6 +171,7 @@ def subplot(self, nrows=1, ncols=1, **kwargs):
@fmt_docstring
@contextlib.contextmanager
@use_alias(A="fixedlabel", C="clearance", V="verbose")
@kwargs_to_strings(panel="sequence_comma")
def set_panel(self, panel=None, **kwargs):
r"""
Set the current subplot panel to plot on.
Expand Down Expand Up @@ -221,8 +221,6 @@ def set_panel(self, panel=None, **kwargs):
{verbose}
"""
kwargs = self._preprocess(**kwargs)
# convert tuple or list to comma-separated str
panel = ",".join(map(str, panel)) if is_nonstr_iter(panel) else panel

with Session() as lib:
arg_str = " ".join(["set", f"{panel}", build_arg_string(kwargs)])
Expand Down
12 changes: 5 additions & 7 deletions pygmt/src/timestamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

from packaging.version import Version
from pygmt.clib import Session, __gmt_version__
from pygmt.helpers import build_arg_string, is_nonstr_iter
from pygmt.helpers import build_arg_string, kwargs_to_strings

__doctest_skip__ = ["timestamp"]


@kwargs_to_strings(offset="sequence")
def timestamp(
self,
text=None,
Expand Down Expand Up @@ -83,14 +84,11 @@ def timestamp(
kwdict["U"] += f"{label}"
kwdict["U"] += f"+j{justification}"

if is_nonstr_iter(offset): # given a tuple
kwdict["U"] += "+o" + "/".join(f"{item}" for item in offset)
elif "/" not in offset and Version(__gmt_version__) <= Version("6.4.0"):
if "/" not in offset and Version(__gmt_version__) <= Version("6.4.0"):
# Giving a single offset doesn't work in GMT <= 6.4.0.
# See https://github.com/GenericMappingTools/gmt/issues/7107.
kwdict["U"] += f"+o{offset}/{offset}"
else:
kwdict["U"] += f"+o{offset}"
offset = f"{offset}/{offset}"
kwdict["U"] += f"+o{offset}"

# The +t modifier was added in GMT 6.5.0.
# See https://github.com/GenericMappingTools/gmt/pull/7127.
Expand Down
6 changes: 2 additions & 4 deletions pygmt/src/which.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
GMTTempFile,
build_arg_string,
fmt_docstring,
is_nonstr_iter,
kwargs_to_strings,
use_alias,
)


@fmt_docstring
@use_alias(G="download", V="verbose")
@kwargs_to_strings(fname="sequence_space")
def which(fname, **kwargs):
r"""
Find the full path to specified files.
Expand Down Expand Up @@ -62,9 +63,6 @@ def which(fname, **kwargs):
FileNotFoundError
If the file is not found.
"""
if is_nonstr_iter(fname): # Got a list of files
fname = " ".join(fname)

with GMTTempFile() as tmpfile:
with Session() as lib:
lib.call_module(
Expand Down
Loading