diff --git a/examples/gallery/embellishments/legend.py b/examples/gallery/embellishments/legend.py index 65f8c615b00..6463b6ef0d5 100644 --- a/examples/gallery/embellishments/legend.py +++ b/examples/gallery/embellishments/legend.py @@ -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) diff --git a/pygmt/figure.py b/pygmt/figure.py index f6111fff54f..b922705d752 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -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 diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index b3df51558b4..ea7a89873e7 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -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 @@ -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 @@ -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, @@ -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( @@ -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 = [] @@ -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)) diff --git a/pygmt/src/config.py b/pygmt/src/config.py index 7c347e78758..7e4df67d3be 100644 --- a/pygmt/src/config.py +++ b/pygmt/src/config.py @@ -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) diff --git a/pygmt/src/subplot.py b/pygmt/src/subplot.py index 1e713afb189..19a9e6ecfd6 100644 --- a/pygmt/src/subplot.py +++ b/pygmt/src/subplot.py @@ -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.") @@ -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 diff --git a/pygmt/src/text.py b/pygmt/src/text.py index 3c7d0c2fb31..4e74d47f1b7 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -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 diff --git a/pygmt/tests/baseline/test_basemap_utm_projection.png.dvc b/pygmt/tests/baseline/test_basemap_utm_projection.png.dvc new file mode 100644 index 00000000000..c12f0d4026a --- /dev/null +++ b/pygmt/tests/baseline/test_basemap_utm_projection.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: e6984efed2a94673754cc7f1f1d74832 + size: 9069 + path: test_basemap_utm_projection.png diff --git a/pygmt/tests/baseline/test_config_format_date_map.png.dvc b/pygmt/tests/baseline/test_config_format_date_map.png.dvc new file mode 100644 index 00000000000..7bc946d104f --- /dev/null +++ b/pygmt/tests/baseline/test_config_format_date_map.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: 3619720cdfcd857cbdbb49ed7fe6e930 + size: 1392 + path: test_config_format_date_map.png diff --git a/pygmt/tests/baseline/test_rose_no_sectors.png.dvc b/pygmt/tests/baseline/test_rose_no_sectors.png.dvc index 0eddeaf6fee..9e0184a9caf 100644 --- a/pygmt/tests/baseline/test_rose_no_sectors.png.dvc +++ b/pygmt/tests/baseline/test_rose_no_sectors.png.dvc @@ -1,4 +1,4 @@ outs: -- md5: 8e1c47b1cf6001dad3b3c0875af4562e - size: 150390 +- md5: ce2d5cd1415b7c7bbeea5bf6ff39c480 + size: 150288 path: test_rose_no_sectors.png diff --git a/pygmt/tests/test_basemap.py b/pygmt/tests/test_basemap.py index 726661ab165..7c05e84973a 100644 --- a/pygmt/tests/test_basemap.py +++ b/pygmt/tests/test_basemap.py @@ -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(): """ diff --git a/pygmt/tests/test_config.py b/pygmt/tests/test_config.py index ba39ab53b79..067ee1b7289 100644 --- a/pygmt/tests/test_config.py +++ b/pygmt/tests/test_config.py @@ -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(): """ diff --git a/pygmt/tests/test_figure.py b/pygmt/tests/test_figure.py index 05dcb467c0e..2d5949eaddb 100644 --- a/pygmt/tests/test_figure.py +++ b/pygmt/tests/test_figure.py @@ -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) diff --git a/pygmt/tests/test_grdproject.py b/pygmt/tests/test_grdproject.py index 26b2aba86ab..6e90f061c34 100644 --- a/pygmt/tests/test_grdproject.py +++ b/pygmt/tests/test_grdproject.py @@ -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 diff --git a/pygmt/tests/test_legend.py b/pygmt/tests/test_legend.py index b93bafc401b..66eadd3fc68 100644 --- a/pygmt/tests/test_legend.py +++ b/pygmt/tests/test_legend.py @@ -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") diff --git a/pygmt/tests/test_psconvert.py b/pygmt/tests/test_psconvert.py index af610cf86bb..a18b14883f2 100644 --- a/pygmt/tests/test_psconvert.py +++ b/pygmt/tests/test_psconvert.py @@ -3,7 +3,9 @@ """ import os +import pytest from pygmt import Figure +from pygmt.exceptions import GMTInvalidInput def test_psconvert(): @@ -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") diff --git a/pygmt/tests/test_rose.py b/pygmt/tests/test_rose.py index bb7c3de0fe7..b330fb43a54 100644 --- a/pygmt/tests/test_rose.py +++ b/pygmt/tests/test_rose.py @@ -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,