Skip to content

Commit

Permalink
Allow passing arguments containing spaces into pygmt functions (Gener…
Browse files Browse the repository at this point in the history
…icMappingTools#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]>
  • Loading branch information
2 people authored and Josh Sixsmith committed Dec 21, 2022
1 parent ec12d7c commit 3ee75a1
Show file tree
Hide file tree
Showing 16 changed files with 117 additions and 24 deletions.
2 changes: 1 addition & 1 deletion examples/gallery/embellishments/legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
pen="faint",
label="Apples",
)
fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label='"My lines"')
fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label="My lines")
fig.plot(data="@Table_5_11.txt", style="t0.15i", color="orange", label="Oranges")

fig.legend(position="JTR+jTR+o0.2c", box=True)
Expand Down
13 changes: 10 additions & 3 deletions pygmt/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,17 @@ def psconvert(self, icc_gray=False, **kwargs):
kwargs["N"] = "+i"
else:
kwargs["N"] += "+i"
# allow for spaces in figure name
kwargs["F"] = f'"{kwargs.get("F")}"' if kwargs.get("F") else None

# Manually handle prefix -F argument so spaces aren't converted to \040
# by build_arg_string function. For more information, see
# https://github.com/GenericMappingTools/pygmt/pull/1487
try:
prefix_arg = f'-F"{kwargs.pop("F")}"'
except KeyError as err:
raise GMTInvalidInput("The 'prefix' must be specified.") from err

with Session() as lib:
lib.call_module("psconvert", build_arg_string(kwargs))
lib.call_module("psconvert", f"{prefix_arg} {build_arg_string(kwargs)}")

def savefig(
self, fname, transparent=False, crop=True, anti_alias=True, show=False, **kwargs
Expand Down
33 changes: 28 additions & 5 deletions pygmt/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def dummy_context(arg):


def build_arg_string(kwargs):
"""
r"""
Transform keyword arguments into a GMT argument string.
Make sure all arguments have been previously converted to a string
Expand All @@ -131,6 +131,11 @@ def build_arg_string(kwargs):
same command line argument. For example, the kwargs entry ``'B': ['xa',
'yaf']`` will be converted to ``-Bxa -Byaf`` in the argument string.
Note that spaces `` `` in arguments are converted to the equivalent octal
code ``\040``, except in the case of -J (projection) arguments where PROJ4
strings (e.g. "+proj=longlat +datum=WGS84") will have their spaces removed.
See https://github.com/GenericMappingTools/pygmt/pull/1487 for more info.
Parameters
----------
kwargs : dict
Expand All @@ -151,7 +156,7 @@ def build_arg_string(kwargs):
... A=True,
... B=False,
... E=200,
... J="X4c",
... J="+proj=longlat +datum=WGS84",
... P="",
... R="1/2/3/4",
... X=None,
Expand All @@ -160,7 +165,7 @@ def build_arg_string(kwargs):
... )
... )
... )
-A -E200 -JX4c -P -R1/2/3/4 -Z0
-A -E200 -J+proj=longlat+datum=WGS84 -P -R1/2/3/4 -Z0
>>> print(
... build_arg_string(
... dict(
Expand All @@ -176,6 +181,16 @@ def build_arg_string(kwargs):
Traceback (most recent call last):
...
pygmt.exceptions.GMTInvalidInput: Unrecognized parameter 'watre'.
>>> print(
... build_arg_string(
... dict(
... B=["af", "WSne+tBlank Space"],
... F='+t"Empty Spaces"',
... l="'Void Space'",
... ),
... )
... )
-BWSne+tBlank\040Space -Baf -F+t"Empty\040\040Spaces" -l'Void\040Space'
"""
gmt_args = []

Expand All @@ -185,11 +200,19 @@ def build_arg_string(kwargs):
if kwargs[key] is None or kwargs[key] is False:
pass # Exclude arguments that are None and False
elif is_nonstr_iter(kwargs[key]):
gmt_args.extend(f"-{key}{value}" for value in kwargs[key])
for value in kwargs[key]:
_value = str(value).replace(" ", r"\040")
gmt_args.append(rf"-{key}{_value}")
elif kwargs[key] is True:
gmt_args.append(f"-{key}")
else:
gmt_args.append(f"-{key}{kwargs[key]}")
if key != "J": # non-projection parameters
_value = str(kwargs[key]).replace(" ", r"\040")
else:
# special handling if key == "J" (projection)
# remove any spaces in PROJ4 string
_value = str(kwargs[key]).replace(" ", "")
gmt_args.append(rf"-{key}{_value}")
return " ".join(sorted(gmt_args))


Expand Down
2 changes: 1 addition & 1 deletion pygmt/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def __init__(self, **kwargs):
self.old_defaults[key] = lib.get_default(key)

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

Expand Down
6 changes: 0 additions & 6 deletions pygmt/src/subplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,6 @@ def subplot(self, nrows=1, ncols=1, **kwargs):
{XY}
"""
kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access
# allow for spaces in string without needing double quotes
if isinstance(kwargs.get("A"), str):
kwargs["A"] = f'"{kwargs.get("A")}"'
kwargs["T"] = f'"{kwargs.get("T")}"' if kwargs.get("T") else None

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

Expand Down
2 changes: 1 addition & 1 deletion pygmt/src/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def text_(
kwargs["F"] += f"+j{justify}"

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

extra_arrays = []
# If an array of transparency is given, GMT will read it from
Expand Down
4 changes: 4 additions & 0 deletions pygmt/tests/baseline/test_basemap_utm_projection.png.dvc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
outs:
- md5: e6984efed2a94673754cc7f1f1d74832
size: 9069
path: test_basemap_utm_projection.png
4 changes: 4 additions & 0 deletions pygmt/tests/baseline/test_config_format_date_map.png.dvc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
outs:
- md5: 3619720cdfcd857cbdbb49ed7fe6e930
size: 1392
path: test_config_format_date_map.png
4 changes: 2 additions & 2 deletions pygmt/tests/baseline/test_rose_no_sectors.png.dvc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
outs:
- md5: 8e1c47b1cf6001dad3b3c0875af4562e
size: 150390
- md5: ce2d5cd1415b7c7bbeea5bf6ff39c480
size: 150288
path: test_rose_no_sectors.png
23 changes: 23 additions & 0 deletions pygmt/tests/test_basemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ def test_basemap_winkel_tripel():
return fig


@pytest.mark.mpl_image_compare(filename="test_basemap_utm_projection.png")
@pytest.mark.parametrize(
"projection",
[
"EPSG_32723 +width=5",
"+proj=utm +zone=23 +south +datum=WGS84 +units=m +no_defs +width=5",
],
)
def test_basemap_utm_projection(projection):
"""
Create a Universal Transverse Mercator (Zone 23S) basemap plot.
Also check that providing the projection as an EPSG code or PROJ4 string
works.
"""
projection = projection.replace(
"EPSG_", "EPSG:" # workaround Windows not allowing colons in filenames
)
fig = Figure()
fig.basemap(region=[-52, -50, -12, -11], projection=projection, frame="afg")
return fig


@pytest.mark.mpl_image_compare
def test_basemap_rose():
"""
Expand Down
19 changes: 19 additions & 0 deletions pygmt/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@ def test_config_font_annot():
return fig


@pytest.mark.mpl_image_compare
def test_config_format_date_map():
"""
Test that setting FORMAT_DATE_MAP config changes how the output date string
is plotted.
Note the space in 'o dd', this acts as a regression test for
https://github.com/GenericMappingTools/pygmt/issues/247.
"""
fig = Figure()
with config(FORMAT_DATE_MAP="o dd"):
fig.basemap(
region=["1969-7-21T", "1969-7-23T", 0, 1],
projection="X2.5c/0.1c",
frame=["sxa1D", "S"],
)
return fig


@pytest.mark.mpl_image_compare
def test_config_format_time_map():
"""
Expand Down
3 changes: 2 additions & 1 deletion pygmt/tests/test_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ def test_figure_savefig_filename_with_spaces():
fig = Figure()
fig.basemap(region=[0, 1, 0, 1], projection="X1c/1c", frame=True)
with GMTTempFile(prefix="pygmt-filename with spaces", suffix=".png") as imgfile:
fig.savefig(imgfile.name)
fig.savefig(fname=imgfile.name)
assert r"\040" not in os.path.abspath(imgfile.name)
assert os.path.exists(imgfile.name)


Expand Down
11 changes: 9 additions & 2 deletions pygmt/tests/test_grdproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,20 @@ def test_grdproject_file_out(grid, expected_grid):
xr.testing.assert_allclose(a=temp_grid, b=expected_grid)


def test_grdproject_no_outgrid(grid, expected_grid):
@pytest.mark.parametrize(
"projection",
["M10c", "EPSG:3395 +width=10", "+proj=merc +ellps=WGS84 +units=m +width=10"],
)
def test_grdproject_no_outgrid(grid, projection, expected_grid):
"""
Test grdproject with no set outgrid.
Also check that providing the projection as an EPSG code or PROJ4 string
works.
"""
assert grid.gmt.gtype == 1 # Geographic grid
result = grdproject(
grid=grid, projection="M10c", spacing=3, region=[-53, -51, -20, -17]
grid=grid, projection=projection, spacing=3, region=[-53, -51, -20, -17]
)
assert result.gmt.gtype == 0 # Rectangular grid
assert result.gmt.registration == 1 # Pixel registration
Expand Down
2 changes: 1 addition & 1 deletion pygmt/tests/test_legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_legend_entries():
pen="faint",
label="Apples",
)
fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label='"My lines"')
fig.plot(data="@Table_5_11.txt", pen="1.5p,gray", label="My lines")
fig.plot(data="@Table_5_11.txt", style="t0.15i", color="orange", label="Oranges")
fig.legend(position="JTR+jTR")

Expand Down
11 changes: 11 additions & 0 deletions pygmt/tests/test_psconvert.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"""
import os

import pytest
from pygmt import Figure
from pygmt.exceptions import GMTInvalidInput


def test_psconvert():
Expand Down Expand Up @@ -36,3 +38,12 @@ def test_psconvert_twice():
fname = prefix + ".png"
assert os.path.exists(fname)
os.remove(fname)


def test_psconvert_without_prefix():
"""
Call psconvert without the 'prefix' option.
"""
fig = Figure()
with pytest.raises(GMTInvalidInput):
fig.psconvert(fmt="g")
2 changes: 1 addition & 1 deletion pygmt/tests/test_rose.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def test_rose_no_sectors(data_fractures_compilation):
region=[0, 500, 0, 360],
diameter="10c",
labels="180/0/90/270",
frame=["xg100", "yg45", "+t'Windrose diagram'"],
frame=["xg100", "yg45", "+tWindrose diagram"],
pen="1.5p,red3",
transparency=40,
scale=0.5,
Expand Down

0 comments on commit 3ee75a1

Please sign in to comment.