Skip to content

Commit b173c69

Browse files
weiji14seisman
andauthored
Allow passing arguments containing spaces into pygmt functions (#1487)
* Replace spaces in arguments with octal code 040 Modifying build_arg_string function to replace blank space characters with octal code 040, and added a doctest to check various combinations with single and double quotes included. * Remove workarounds for spaces in fig.subplot's autolabel and title args Supersedes workaround for subplot's autolabel (-A) and title (-T) parameters in a9d167d, 4126c16, and eadb847. * Remove workaround for spaces in fig.text's -F argument * Remove double quotes around legend label test examples * Edit test_rose_no_sectors to remove single quotes from title * Remove workaround for spaces in fig.psconvert prefix Doesn't work yet, as the filename will contain the 040 octal code, but committing to have the diff available for review. * Ensure spaces in pygmt.config arguments can work Also added a regression test for FORMAT_DATE_MAP="o dd". * Manually handle prefix -F in psconvert So that fig.savefig won't insert `\040` characters when saving filenames with spaces. Resolves problem mentioned in https://github.com/GenericMappingTools/pygmt/pull/1487/files#r703116544 * Handle PROJ4 strings with spaces Instead of converting spaces to \040 in proj4 strings, just remove them directly. Added parametrized unit tests to basemap and grdproject to check that it works. * Use Modifier Letter Colon instead of regular colon to fix WIndows tests Adapted from https://stackoverflow.com/questions/10386344/how-to-get-a-file-in-windows-with-a-colon-in-the-filename/25477235#25477235. * Try using underscore instead of Modifier Letter Colon * Raise GMTInvalidInput if no prefix argument is passed to psconvert Co-authored-by: Dongdong Tian <[email protected]>
1 parent cc44040 commit b173c69

16 files changed

+117
-24
lines changed

examples/gallery/embellishments/legend.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
pen="faint",
2020
label="Apples",
2121
)
22-
fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label='"My lines"')
22+
fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label="My lines")
2323
fig.plot(data="@Table_5_11.txt", style="t0.15i", color="orange", label="Oranges")
2424

2525
fig.legend(position="JTR+jTR+o0.2c", box=True)

pygmt/figure.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,17 @@ def psconvert(self, icc_gray=False, **kwargs):
235235
kwargs["N"] = "+i"
236236
else:
237237
kwargs["N"] += "+i"
238-
# allow for spaces in figure name
239-
kwargs["F"] = f'"{kwargs.get("F")}"' if kwargs.get("F") else None
238+
239+
# Manually handle prefix -F argument so spaces aren't converted to \040
240+
# by build_arg_string function. For more information, see
241+
# https://github.com/GenericMappingTools/pygmt/pull/1487
242+
try:
243+
prefix_arg = f'-F"{kwargs.pop("F")}"'
244+
except KeyError as err:
245+
raise GMTInvalidInput("The 'prefix' must be specified.") from err
246+
240247
with Session() as lib:
241-
lib.call_module("psconvert", build_arg_string(kwargs))
248+
lib.call_module("psconvert", f"{prefix_arg} {build_arg_string(kwargs)}")
242249

243250
def savefig(
244251
self, fname, transparent=False, crop=True, anti_alias=True, show=False, **kwargs

pygmt/helpers/utils.py

+28-5
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def dummy_context(arg):
120120

121121

122122
def build_arg_string(kwargs):
123-
"""
123+
r"""
124124
Transform keyword arguments into a GMT argument string.
125125
126126
Make sure all arguments have been previously converted to a string
@@ -131,6 +131,11 @@ def build_arg_string(kwargs):
131131
same command line argument. For example, the kwargs entry ``'B': ['xa',
132132
'yaf']`` will be converted to ``-Bxa -Byaf`` in the argument string.
133133
134+
Note that spaces `` `` in arguments are converted to the equivalent octal
135+
code ``\040``, except in the case of -J (projection) arguments where PROJ4
136+
strings (e.g. "+proj=longlat +datum=WGS84") will have their spaces removed.
137+
See https://github.com/GenericMappingTools/pygmt/pull/1487 for more info.
138+
134139
Parameters
135140
----------
136141
kwargs : dict
@@ -151,7 +156,7 @@ def build_arg_string(kwargs):
151156
... A=True,
152157
... B=False,
153158
... E=200,
154-
... J="X4c",
159+
... J="+proj=longlat +datum=WGS84",
155160
... P="",
156161
... R="1/2/3/4",
157162
... X=None,
@@ -160,7 +165,7 @@ def build_arg_string(kwargs):
160165
... )
161166
... )
162167
... )
163-
-A -E200 -JX4c -P -R1/2/3/4 -Z0
168+
-A -E200 -J+proj=longlat+datum=WGS84 -P -R1/2/3/4 -Z0
164169
>>> print(
165170
... build_arg_string(
166171
... dict(
@@ -176,6 +181,16 @@ def build_arg_string(kwargs):
176181
Traceback (most recent call last):
177182
...
178183
pygmt.exceptions.GMTInvalidInput: Unrecognized parameter 'watre'.
184+
>>> print(
185+
... build_arg_string(
186+
... dict(
187+
... B=["af", "WSne+tBlank Space"],
188+
... F='+t"Empty Spaces"',
189+
... l="'Void Space'",
190+
... ),
191+
... )
192+
... )
193+
-BWSne+tBlank\040Space -Baf -F+t"Empty\040\040Spaces" -l'Void\040Space'
179194
"""
180195
gmt_args = []
181196

@@ -185,11 +200,19 @@ def build_arg_string(kwargs):
185200
if kwargs[key] is None or kwargs[key] is False:
186201
pass # Exclude arguments that are None and False
187202
elif is_nonstr_iter(kwargs[key]):
188-
gmt_args.extend(f"-{key}{value}" for value in kwargs[key])
203+
for value in kwargs[key]:
204+
_value = str(value).replace(" ", r"\040")
205+
gmt_args.append(rf"-{key}{_value}")
189206
elif kwargs[key] is True:
190207
gmt_args.append(f"-{key}")
191208
else:
192-
gmt_args.append(f"-{key}{kwargs[key]}")
209+
if key != "J": # non-projection parameters
210+
_value = str(kwargs[key]).replace(" ", r"\040")
211+
else:
212+
# special handling if key == "J" (projection)
213+
# remove any spaces in PROJ4 string
214+
_value = str(kwargs[key]).replace(" ", "")
215+
gmt_args.append(rf"-{key}{_value}")
193216
return " ".join(sorted(gmt_args))
194217

195218

pygmt/src/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def __init__(self, **kwargs):
5555
self.old_defaults[key] = lib.get_default(key)
5656

5757
# call gmt set to change GMT defaults
58-
arg_str = " ".join([f"{key}={value}" for key, value in kwargs.items()])
58+
arg_str = " ".join([f'{key}="{value}"' for key, value in kwargs.items()])
5959
with Session() as lib:
6060
lib.call_module("set", arg_str)
6161

pygmt/src/subplot.py

-6
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,6 @@ def subplot(self, nrows=1, ncols=1, **kwargs):
148148
{XY}
149149
"""
150150
kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access
151-
# allow for spaces in string without needing double quotes
152-
if isinstance(kwargs.get("A"), str):
153-
kwargs["A"] = f'"{kwargs.get("A")}"'
154-
kwargs["T"] = f'"{kwargs.get("T")}"' if kwargs.get("T") else None
155151

156152
if nrows < 1 or ncols < 1:
157153
raise GMTInvalidInput("Please ensure that both 'nrows'>=1 and 'ncols'>=1.")
@@ -222,8 +218,6 @@ def set_panel(self, panel=None, **kwargs):
222218
{V}
223219
"""
224220
kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access
225-
# allow for spaces in string with needing double quotes
226-
kwargs["A"] = f'"{kwargs.get("A")}"' if kwargs.get("A") is not None else None
227221
# convert tuple or list to comma-separated str
228222
panel = ",".join(map(str, panel)) if is_nonstr_iter(panel) else panel
229223

pygmt/src/text.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def text_(
210210
kwargs["F"] += f"+j{justify}"
211211

212212
if isinstance(position, str):
213-
kwargs["F"] += f'+c{position}+t"{text}"'
213+
kwargs["F"] += f"+c{position}+t{text}"
214214

215215
extra_arrays = []
216216
# If an array of transparency is given, GMT will read it from
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
outs:
2+
- md5: e6984efed2a94673754cc7f1f1d74832
3+
size: 9069
4+
path: test_basemap_utm_projection.png
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
outs:
2+
- md5: 3619720cdfcd857cbdbb49ed7fe6e930
3+
size: 1392
4+
path: test_config_format_date_map.png
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
outs:
2-
- md5: 8e1c47b1cf6001dad3b3c0875af4562e
3-
size: 150390
2+
- md5: ce2d5cd1415b7c7bbeea5bf6ff39c480
3+
size: 150288
44
path: test_rose_no_sectors.png

pygmt/tests/test_basemap.py

+23
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@ def test_basemap_winkel_tripel():
7373
return fig
7474

7575

76+
@pytest.mark.mpl_image_compare(filename="test_basemap_utm_projection.png")
77+
@pytest.mark.parametrize(
78+
"projection",
79+
[
80+
"EPSG_32723 +width=5",
81+
"+proj=utm +zone=23 +south +datum=WGS84 +units=m +no_defs +width=5",
82+
],
83+
)
84+
def test_basemap_utm_projection(projection):
85+
"""
86+
Create a Universal Transverse Mercator (Zone 23S) basemap plot.
87+
88+
Also check that providing the projection as an EPSG code or PROJ4 string
89+
works.
90+
"""
91+
projection = projection.replace(
92+
"EPSG_", "EPSG:" # workaround Windows not allowing colons in filenames
93+
)
94+
fig = Figure()
95+
fig.basemap(region=[-52, -50, -12, -11], projection=projection, frame="afg")
96+
return fig
97+
98+
7699
@pytest.mark.mpl_image_compare
77100
def test_basemap_rose():
78101
"""

pygmt/tests/test_config.py

+19
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@ def test_config_font_annot():
6464
return fig
6565

6666

67+
@pytest.mark.mpl_image_compare
68+
def test_config_format_date_map():
69+
"""
70+
Test that setting FORMAT_DATE_MAP config changes how the output date string
71+
is plotted.
72+
73+
Note the space in 'o dd', this acts as a regression test for
74+
https://github.com/GenericMappingTools/pygmt/issues/247.
75+
"""
76+
fig = Figure()
77+
with config(FORMAT_DATE_MAP="o dd"):
78+
fig.basemap(
79+
region=["1969-7-21T", "1969-7-23T", 0, 1],
80+
projection="X2.5c/0.1c",
81+
frame=["sxa1D", "S"],
82+
)
83+
return fig
84+
85+
6786
@pytest.mark.mpl_image_compare
6887
def test_config_format_time_map():
6988
"""

pygmt/tests/test_figure.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ def test_figure_savefig_filename_with_spaces():
138138
fig = Figure()
139139
fig.basemap(region=[0, 1, 0, 1], projection="X1c/1c", frame=True)
140140
with GMTTempFile(prefix="pygmt-filename with spaces", suffix=".png") as imgfile:
141-
fig.savefig(imgfile.name)
141+
fig.savefig(fname=imgfile.name)
142+
assert r"\040" not in os.path.abspath(imgfile.name)
142143
assert os.path.exists(imgfile.name)
143144

144145

pygmt/tests/test_grdproject.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,20 @@ def test_grdproject_file_out(grid, expected_grid):
5858
xr.testing.assert_allclose(a=temp_grid, b=expected_grid)
5959

6060

61-
def test_grdproject_no_outgrid(grid, expected_grid):
61+
@pytest.mark.parametrize(
62+
"projection",
63+
["M10c", "EPSG:3395 +width=10", "+proj=merc +ellps=WGS84 +units=m +width=10"],
64+
)
65+
def test_grdproject_no_outgrid(grid, projection, expected_grid):
6266
"""
6367
Test grdproject with no set outgrid.
68+
69+
Also check that providing the projection as an EPSG code or PROJ4 string
70+
works.
6471
"""
6572
assert grid.gmt.gtype == 1 # Geographic grid
6673
result = grdproject(
67-
grid=grid, projection="M10c", spacing=3, region=[-53, -51, -20, -17]
74+
grid=grid, projection=projection, spacing=3, region=[-53, -51, -20, -17]
6875
)
6976
assert result.gmt.gtype == 0 # Rectangular grid
7077
assert result.gmt.registration == 1 # Pixel registration

pygmt/tests/test_legend.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_legend_entries():
5656
pen="faint",
5757
label="Apples",
5858
)
59-
fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label='"My lines"')
59+
fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label="My lines")
6060
fig.plot(data="@Table_5_11.txt", style="t0.15i", color="orange", label="Oranges")
6161
fig.legend(position="JTR+jTR")
6262

pygmt/tests/test_psconvert.py

+11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
"""
44
import os
55

6+
import pytest
67
from pygmt import Figure
8+
from pygmt.exceptions import GMTInvalidInput
79

810

911
def test_psconvert():
@@ -36,3 +38,12 @@ def test_psconvert_twice():
3638
fname = prefix + ".png"
3739
assert os.path.exists(fname)
3840
os.remove(fname)
41+
42+
43+
def test_psconvert_without_prefix():
44+
"""
45+
Call psconvert without the 'prefix' option.
46+
"""
47+
fig = Figure()
48+
with pytest.raises(GMTInvalidInput):
49+
fig.psconvert(fmt="g")

pygmt/tests/test_rose.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def test_rose_no_sectors(data_fractures_compilation):
152152
region=[0, 500, 0, 360],
153153
diameter="10c",
154154
labels="180/0/90/270",
155-
frame=["xg100", "yg45", "+t'Windrose diagram'"],
155+
frame=["xg100", "yg45", "+tWindrose diagram"],
156156
pen="1.5p,red3",
157157
transparency=40,
158158
scale=0.5,

0 commit comments

Comments
 (0)