Skip to content

Commit b0a2c44

Browse files
authored
Let kwargs_to_strings work with default values and postional arguments (#2826)
1 parent 022a91f commit b0a2c44

File tree

4 files changed

+78
-46
lines changed

4 files changed

+78
-46
lines changed

pygmt/helpers/decorators.py

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,6 @@ def kwargs_to_strings(**conversions):
641641
642642
Examples
643643
--------
644-
645644
>>> @kwargs_to_strings(
646645
... R="sequence", i="sequence_comma", files="sequence_space"
647646
... )
@@ -691,58 +690,97 @@ def kwargs_to_strings(**conversions):
691690
... ]
692691
... )
693692
{'R': '2005-01-01T08:00:00.000000000/2015-01-01T12:00:00.123456'}
693+
>>> # Here is a more realistic example
694+
>>> # See https://github.com/GenericMappingTools/pygmt/issues/2361
695+
>>> @kwargs_to_strings(
696+
... files="sequence_space",
697+
... offset="sequence",
698+
... R="sequence",
699+
... i="sequence_comma",
700+
... )
701+
... def module(files, offset=("-54p", "-54p"), **kwargs):
702+
... "A module that prints the arguments it received"
703+
... print(files, end=" ")
704+
... print(offset, end=" ")
705+
... print("{", end="")
706+
... print(
707+
... ", ".join(f"'{k}': {repr(kwargs[k])}" for k in sorted(kwargs)),
708+
... end="",
709+
... )
710+
... print("}")
711+
>>> module(files=["data1.txt", "data2.txt"])
712+
data1.txt data2.txt -54p/-54p {}
713+
>>> module(["data1.txt", "data2.txt"])
714+
data1.txt data2.txt -54p/-54p {}
715+
>>> module(files=["data1.txt", "data2.txt"], offset=("20p", "20p"))
716+
data1.txt data2.txt 20p/20p {}
717+
>>> module(["data1.txt", "data2.txt"], ("20p", "20p"))
718+
data1.txt data2.txt 20p/20p {}
719+
>>> module(["data1.txt", "data2.txt"], ("20p", "20p"), R=[1, 2, 3, 4])
720+
data1.txt data2.txt 20p/20p {'R': '1/2/3/4'}
694721
"""
695-
valid_conversions = [
696-
"sequence",
697-
"sequence_comma",
698-
"sequence_plus",
699-
"sequence_space",
700-
]
701-
702-
for arg, fmt in conversions.items():
703-
if fmt not in valid_conversions:
704-
raise GMTInvalidInput(
705-
f"Invalid conversion type '{fmt}' for argument '{arg}'."
706-
)
707-
708722
separators = {
709723
"sequence": "/",
710724
"sequence_comma": ",",
711725
"sequence_plus": "+",
712726
"sequence_space": " ",
713727
}
714728

729+
for arg, fmt in conversions.items():
730+
if fmt not in separators:
731+
raise GMTInvalidInput(
732+
f"Invalid conversion type '{fmt}' for argument '{arg}'."
733+
)
734+
715735
# Make the actual decorator function
716736
def converter(module_func):
717737
"""
718738
The decorator that creates our new function with the conversions.
719739
"""
740+
sig = signature(module_func)
720741

721742
@functools.wraps(module_func)
722743
def new_module(*args, **kwargs):
723744
"""
724745
New module instance that converts the arguments first.
725746
"""
747+
# Inspired by https://stackoverflow.com/a/69170441
748+
bound = sig.bind(*args, **kwargs)
749+
bound.apply_defaults()
750+
726751
for arg, fmt in conversions.items():
727-
if arg in kwargs:
728-
value = kwargs[arg]
729-
issequence = fmt in separators
730-
if issequence and is_nonstr_iter(value):
731-
for index, item in enumerate(value):
732-
try:
733-
# check if there is a space " " when converting
734-
# a pandas.Timestamp/xr.DataArray to a string.
735-
# If so, use np.datetime_as_string instead.
736-
assert " " not in str(item)
737-
except AssertionError:
738-
# convert datetime-like item to ISO 8601
739-
# string format like YYYY-MM-DDThh:mm:ss.ffffff
740-
value[index] = np.datetime_as_string(
741-
np.asarray(item, dtype=np.datetime64)
742-
)
743-
kwargs[arg] = separators[fmt].join(f"{item}" for item in value)
752+
# The arg may be in args or kwargs
753+
if arg in bound.arguments:
754+
value = bound.arguments[arg]
755+
elif arg in bound.arguments.get("kwargs"):
756+
value = bound.arguments["kwargs"][arg]
757+
else:
758+
continue
759+
760+
issequence = fmt in separators
761+
if issequence and is_nonstr_iter(value):
762+
for index, item in enumerate(value):
763+
try:
764+
# Check if there is a space " " when converting
765+
# a pandas.Timestamp/xr.DataArray to a string.
766+
# If so, use np.datetime_as_string instead.
767+
assert " " not in str(item)
768+
except AssertionError:
769+
# Convert datetime-like item to ISO 8601
770+
# string format like YYYY-MM-DDThh:mm:ss.ffffff.
771+
value[index] = np.datetime_as_string(
772+
np.asarray(item, dtype=np.datetime64)
773+
)
774+
newvalue = separators[fmt].join(f"{item}" for item in value)
775+
# Changes in bound.arguments will reflect in bound.args
776+
# and bound.kwargs.
777+
if arg in bound.arguments:
778+
bound.arguments[arg] = newvalue
779+
elif arg in bound.arguments.get("kwargs"):
780+
bound.arguments["kwargs"][arg] = newvalue
781+
744782
# Execute the original function and return its output
745-
return module_func(*args, **kwargs)
783+
return module_func(*bound.args, **bound.kwargs)
746784

747785
return new_module
748786

pygmt/src/subplot.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from pygmt.helpers import (
99
build_arg_string,
1010
fmt_docstring,
11-
is_nonstr_iter,
1211
kwargs_to_strings,
1312
use_alias,
1413
)
@@ -172,6 +171,7 @@ def subplot(self, nrows=1, ncols=1, **kwargs):
172171
@fmt_docstring
173172
@contextlib.contextmanager
174173
@use_alias(A="fixedlabel", C="clearance", V="verbose")
174+
@kwargs_to_strings(panel="sequence_comma")
175175
def set_panel(self, panel=None, **kwargs):
176176
r"""
177177
Set the current subplot panel to plot on.
@@ -221,8 +221,6 @@ def set_panel(self, panel=None, **kwargs):
221221
{verbose}
222222
"""
223223
kwargs = self._preprocess(**kwargs)
224-
# convert tuple or list to comma-separated str
225-
panel = ",".join(map(str, panel)) if is_nonstr_iter(panel) else panel
226224

227225
with Session() as lib:
228226
arg_str = " ".join(["set", f"{panel}", build_arg_string(kwargs)])

pygmt/src/timestamp.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55

66
from packaging.version import Version
77
from pygmt.clib import Session, __gmt_version__
8-
from pygmt.helpers import build_arg_string, is_nonstr_iter
8+
from pygmt.helpers import build_arg_string, kwargs_to_strings
99

1010
__doctest_skip__ = ["timestamp"]
1111

1212

13+
@kwargs_to_strings(offset="sequence")
1314
def timestamp(
1415
self,
1516
text=None,
@@ -83,14 +84,11 @@ def timestamp(
8384
kwdict["U"] += f"{label}"
8485
kwdict["U"] += f"+j{justification}"
8586

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

9593
# The +t modifier was added in GMT 6.5.0.
9694
# See https://github.com/GenericMappingTools/gmt/pull/7127.

pygmt/src/which.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
GMTTempFile,
77
build_arg_string,
88
fmt_docstring,
9-
is_nonstr_iter,
9+
kwargs_to_strings,
1010
use_alias,
1111
)
1212

1313

1414
@fmt_docstring
1515
@use_alias(G="download", V="verbose")
16+
@kwargs_to_strings(fname="sequence_space")
1617
def which(fname, **kwargs):
1718
r"""
1819
Find the full path to specified files.
@@ -62,9 +63,6 @@ def which(fname, **kwargs):
6263
FileNotFoundError
6364
If the file is not found.
6465
"""
65-
if is_nonstr_iter(fname): # Got a list of files
66-
fname = " ".join(fname)
67-
6866
with GMTTempFile() as tmpfile:
6967
with Session() as lib:
7068
lib.call_module(

0 commit comments

Comments
 (0)