From cd1ffcd9b2234597b5aecbd69df4af54317f4935 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 10:36:44 +0800 Subject: [PATCH 01/53] Implement the new alias system --- pygmt/alias.py | 267 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 pygmt/alias.py diff --git a/pygmt/alias.py b/pygmt/alias.py new file mode 100644 index 00000000000..e285e915a73 --- /dev/null +++ b/pygmt/alias.py @@ -0,0 +1,267 @@ +""" +Alias system that converts PyGMT parameters to GMT short-form options. +""" + +import dataclasses +import inspect +from collections import defaultdict +from collections.abc import Mapping, Sequence +from typing import Any + +from pygmt.helpers.utils import is_nonstr_iter + + +def value_to_string( + value: Any, + prefix: str = "", # Default to an empty string to simplify the code logic. + separator: str | None = None, + mapping: bool | Mapping = False, +) -> str | Sequence[str] | None: + """ + Convert any value to a string, a sequence of strings or None. + + ``None`` or ``False`` will be converted to ``None``. + + ``True`` will be converted to an empty string. If the value is a sequence and a + separator is provided, the sequence will be joined by the separator. Otherwise, each + item in the sequence will be converted to a string and a sequence of strings will be + returned. Any other value will be converted to a string if possible. It also tried + to convert PyGMT's long-form arguments into GMT's short-form arguments by using a + mapping dictionary or simply using the first letter of the long-form arguments. + + An optional prefix (e.g., `"+o"`) can be added to the beginning of the converted + string. + + Parameters + ---------- + value + The value to convert. + prefix + The string to add as a prefix to the value. + separator + The separator to use if the value is a sequence. + mapping + Map long-form arguments to GMT's short-form arguments. If ``True``, will use the + first letter of the long-form arguments. + + Examples + -------- + >>> value_to_string("text") + 'text' + >>> value_to_string(12) + '12' + >>> value_to_string((12, 34), separator="/") + '12/34' + >>> value_to_string(("12p", "34p"), separator=",") + '12p,34p' + >>> value_to_string(("12p", "34p"), prefix="+o", separator="/") + '+o12p/34p' + >>> value_to_string(True) + '' + >>> value_to_string(True, prefix="+a") + '+a' + >>> value_to_string(False) + >>> value_to_string(None) + >>> value_to_string(["xaf", "yaf", "WSen"]) + ['xaf', 'yaf', 'WSen'] + >>> value_to_string("high", mapping=True) + 'h' + >>> value_to_string("low", mapping=True) + 'l' + >>> value_to_string("mean", mapping={"mean": "a", "mad": "d", "full": "g"}) + 'a' + >>> value_to_string("invalid", mapping={"mean": "a", "mad": "d", "full": "g"}) + 'invalid' + """ + # None or False means the parameter is not specified, returns None. + if value is None or value is False: + return None + # True means the parameter is specified, returns an empty string with the optional + # prefix ('prefix' defaults to an empty string!). + if value is True: + return f"{prefix}" + + # Convert any value to a string or a sequence of strings + if is_nonstr_iter(value): # Is a sequence + value = [str(item) for item in value] # Convert to a sequence of strings + if separator is None: + # A sequence is given but separator is not specified. In this case, return + # a sequence of strings, which is used to support repeated GMT options like + # '-B'. 'prefix' makes no sense here, so ignored. + return value + value = separator.join(value) # Join the sequence by the specified separator. + if mapping: # Mapping long-form arguments to short-form arguments + value = value[0] if mapping is True else mapping.get(value, value) + return f"{prefix}{value}" + + +@dataclasses.dataclass +class Alias: + """ + Class for aliasing a PyGMT parameter to a GMT option or a modifier. + + Attributes + ---------- + name + Parameter name. + prefix + String to add at the beginning of the value. + separator + Separator to use if the value is a sequence. + mapping + Map long-form arguments to GMT's short-form arguments. If ``True``, will use the + first letter of the long-form arguments. + value + Value of the parameter. + + Examples + -------- + >>> par = Alias("offset", prefix="+o", separator="/") + >>> par.value = (2.0, 2.0) + >>> par.value + '+o2.0/2.0' + >>> par = Alias("frame") + >>> par.value = ("xaf", "yaf", "WSen") + >>> par.value + ['xaf', 'yaf', 'WSen'] + """ + + name: str + prefix: str = "" # Default to an empty string to simplify code logic. + separator: str | None = None + mapping: bool | Mapping = False + _value: Any = None + + @property + def value(self) -> str | Sequence[str] | None: + """ + Get the value of the parameter. + """ + return self._value + + @value.setter + def value(self, new_value: Any): + """ + Set the value of the parameter. + + Internally, the value is converted to a string, a sequence of strings or None. + """ + self._value = value_to_string( + new_value, self.prefix, self.separator, self.mapping + ) + + +class AliasSystem: + """ + Alias system to convert PyGMT parameter into a keyword dictionary for GMT options. + + The AliasSystem class is initialized by keyword arguments where the key is the GMT + single-letter option flag and the value is one or a list of ``Alias`` objects. + + The ``kwdict`` property is a keyword dictionary that stores the current parameter + values. The key of the dictionary is the GMT single-letter option flag, and the + value is the corresponding value of the option. The value can be a string or a + sequence of strings, or None. The keyword dictionary can be passed to the + ``build_arg_list`` function. + + Need to note that the ``kwdict`` property is dynamically computed from the current + values of parameters. So, don't change it and avoid accessing it multiple times. + + Examples + -------- + >>> from pygmt.alias import Alias, AliasSystem + >>> from pygmt.helpers import build_arg_list + >>> + >>> def func( + ... par0, + ... par1=None, + ... par2=None, + ... par3=None, + ... par4=None, + ... frame=False, + ... panel=None, + ... **kwargs, + ... ): + ... alias = AliasSystem( + ... A=[ + ... Alias("par1"), + ... Alias("par2", prefix="+j"), + ... Alias("par3", prefix="+o", separator="/"), + ... ], + ... B=Alias("frame"), + ... c=Alias("panel", separator=","), + ... ) + ... return build_arg_list(alias.kwdict) + >>> func( + ... "infile", + ... par1="mytext", + ... par3=(12, 12), + ... frame=True, + ... panel=(1, 2), + ... J="X10c/10c", + ... ) + ['-Amytext+o12/12', '-B', '-JX10c/10c', '-c1,2'] + """ + + def __init__(self, **kwargs): + """ + Initialize as a dictionary of GMT options and their aliases. + """ + self.options = {} + for option, aliases in kwargs.items(): + if isinstance(aliases, list): + self.options[option] = aliases + elif isinstance(aliases, str): # Support shorthand like 'J="projection"' + self.options[option] = [Alias(aliases)] + else: + self.options[option] = [aliases] + + @property + def kwdict(self): + """ + A keyword dictionary that stores the current parameter values. + """ + # Get the local variables from the calling function. + p_locals = inspect.currentframe().f_back.f_locals + # Get parameters/arguments from **kwargs of the calling function. + p_kwargs = p_locals.pop("kwargs", {}) + + params = p_locals | p_kwargs + # Default value is an empty string to simplify code logic. + kwdict = defaultdict(str) + for option, aliases in self.options.items(): + for alias in aliases: + alias.value = params.get(alias.name) + # value can be a string, a sequence of strings or None. + if alias.value is None: + continue + + # Special handing of repeatable parameter like -B/frame. + if is_nonstr_iter(alias.value): + kwdict[option] = alias.value + # A repeatable option should have only one alias, so break. + break + + kwdict[option] += alias.value + + # Support short-form parameter names specified in kwargs. + # Short-form parameters can be either one-letter (e.g., '-B'), or two-letters + # (e.g., '-Td'). + for option, value in p_kwargs.items(): + # Here, we assume that long-form parameters specified in kwargs are longer + # than two characters. Sometimes, we may use parameter like 'az', but it's + # not specified in kwargs. So, the assumption is still valid. + if len(option) > 2: + continue + + # Two cases for short-form parameters: + # + # If it has an alias and the long-form parameter is also specified, (e.g., + # 'projection="X10c", J="X10c"'), then we silently ignore the short-form + # parameter. + # + # If it has an alias but the long-form parameter is not specified, or it + # doesn't has an alias, then we use the value of the short-form parameter. + if option not in self.options or option not in kwdict: + kwdict[option] = value + return kwdict From 492f2c2efc3bd73d2c6d28d2bc0f6d06a332976c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 10:39:55 +0800 Subject: [PATCH 02/53] Implement the BaseParam class as a base class for PyGMT class-like parameters --- pygmt/params/base.py | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 pygmt/params/base.py diff --git a/pygmt/params/base.py b/pygmt/params/base.py new file mode 100644 index 00000000000..d5ff0ef59d8 --- /dev/null +++ b/pygmt/params/base.py @@ -0,0 +1,55 @@ +""" +Base class for PyGMT common parameters. +""" + + +class BaseParam: + """ + Base class for PyGMT common parameters. + + Examples + -------- + >>> from typing import Any + >>> import dataclasses + >>> from pygmt.params.base import BaseParam + >>> from pygmt.alias import Alias + >>> + >>> @dataclasses.dataclass(repr=False) + ... class Test(BaseParam): + ... par1: Any = None + ... par2: Any = None + ... par3: Any = None + ... + ... _aliases = [ + ... Alias("par1"), + ... Alias("par2", prefix="+a"), + ... Alias("par3", prefix="+b", separator="/"), + ... ] + >>> var = Test(par1="val1") + >>> str(var) + 'val1' + >>> repr(var) + "Test(par1='val1')" + """ + + def __str__(self): + """ + String representation of the object that can be passed to GMT directly. + """ + for alias in self._aliases: + alias.value = getattr(self, alias.name) + return "".join( + [alias.value for alias in self._aliases if alias.value is not None] + ) + + def __repr__(self): + """ + String representation of the object. + """ + string = [] + for alias in self._aliases: + value = getattr(self, alias.name) + if value is None or value is False: + continue + string.append(f"{alias.name}={value!r}") + return f"{self.__class__.__name__}({', '.join(string)})" From ae9006d9f1b94d32821814651d13a6f6dc439cfa Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 10:40:44 +0800 Subject: [PATCH 03/53] Add the Box class for specifying the box parameter --- pygmt/params/__init__.py | 5 +++ pygmt/params/box.py | 68 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 pygmt/params/__init__.py create mode 100644 pygmt/params/box.py diff --git a/pygmt/params/__init__.py b/pygmt/params/__init__.py new file mode 100644 index 00000000000..cc6be04b2db --- /dev/null +++ b/pygmt/params/__init__.py @@ -0,0 +1,5 @@ +""" +Classes for PyGMT common parameters. +""" + +from pygmt.params.box import Box diff --git a/pygmt/params/box.py b/pygmt/params/box.py new file mode 100644 index 00000000000..48cb66dd560 --- /dev/null +++ b/pygmt/params/box.py @@ -0,0 +1,68 @@ +""" +The box parameter. +""" + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import ClassVar + +from pygmt.alias import Alias +from pygmt.params.base import BaseParam + + +@dataclass(repr=False) +class Box(BaseParam): + """ + Class for the box around GMT embellishments. + + Attributes + ---------- + clearance + Set clearances between the embellishment and the box border. Can be either a + scalar value or a list of two/four values. + + - a scalar value means a uniform clearance in all four directions. + - a list of two values means separate clearances in x- and y- directions. + - a list of four values means separate clearances for left/right/bottom/top. + fill + Fill for the box. None means no fill. + + Examples + -------- + >>> from pygmt.params import Box + >>> str(Box(fill="red@20")) + '+gred@20' + >>> str(Box(clearance=(0.2, 0.2), fill="red@20", pen="blue")) + '+c0.2/0.2+gred@20+pblue' + >>> str(Box(clearance=(0.2, 0.2), pen="blue", radius=True)) + '+c0.2/0.2+pblue+r' + >>> str(Box(clearance=(0.1, 0.2, 0.3, 0.4), pen="blue", radius="10p")) + '+c0.1/0.2/0.3/0.4+pblue+r10p' + >>> str( + ... Box( + ... clearance=0.2, + ... pen="blue", + ... radius="10p", + ... shading=("5p", "5p", "lightred"), + ... ) + ... ) + '+c0.2+pblue+r10p+s5p/5p/lightred' + >>> str(Box(clearance=0.2, innerborder=("2p", "1p,red"), pen="blue")) + '+c0.2+i2p/1p,red+pblue' + """ + + clearance: float | str | Sequence[float | str] | None = None + fill: str | None = None + innerborder: str | Sequence | None = None + pen: str | None = None + radius: float | bool | None = False + shading: str | Sequence | None = None + + _aliases: ClassVar = [ + Alias("clearance", prefix="+c", separator="/"), + Alias("fill", prefix="+g"), + Alias("innerborder", prefix="+i", separator="/"), + Alias("pen", prefix="+p"), + Alias("radius", prefix="+r"), + Alias("shading", prefix="+s", separator="/"), + ] From 34c3dd79ab983d2ede861537b8039dae2e023fac Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 10:41:34 +0800 Subject: [PATCH 04/53] Add the Frame/Axes/Axis class for the frame parameter --- pygmt/params/__init__.py | 1 + pygmt/params/frame.py | 93 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 pygmt/params/frame.py diff --git a/pygmt/params/__init__.py b/pygmt/params/__init__.py index cc6be04b2db..14326517cde 100644 --- a/pygmt/params/__init__.py +++ b/pygmt/params/__init__.py @@ -3,3 +3,4 @@ """ from pygmt.params.box import Box +from pygmt.params.frame import Axes, Axis, Frame diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py new file mode 100644 index 00000000000..4593b4ca575 --- /dev/null +++ b/pygmt/params/frame.py @@ -0,0 +1,93 @@ +""" +The box parameter. +""" + +from dataclasses import dataclass +from typing import Any, ClassVar + +from pygmt.alias import Alias +from pygmt.params.base import BaseParam + + +@dataclass(repr=False) +class Axes(BaseParam): + """ + Examples + -------- + >>> from pygmt.params import Axes + >>> str(Axes("WSen", title="My Plot Title", fill="lightred")) + 'WSen+glightred+tMy Plot Title' + """ + + axes: Any = None + fill: Any = None + title: Any = None + + _aliases: ClassVar = [ + Alias("axes"), + Alias("fill", prefix="+g"), + Alias("title", prefix="+t"), + ] + + +@dataclass(repr=False) +class Axis(BaseParam): + """ + >>> from pygmt.params import Axis + >>> str(Axis(10, angle=30, label="X axis", unit="km")) + '10+a30+lX axis+ukm' + """ + + interval: float | str + angle: float | str | None = None + label: str | None = None + unit: str | None = None + + _aliases: ClassVar = [ + Alias("interval"), + Alias("angle", prefix="+a"), + Alias("label", prefix="+l"), + Alias("unit", prefix="+u"), + ] + + +@dataclass(repr=False) +class Frame(BaseParam): + """ + >>> from pygmt.alias import AliasSystem, Alias + >>> from pygmt.params import Frame, Axes, Axis + >>> frame = Frame( + ... axes=Axes("WSen", title="My Plot Title", fill="lightred"), + ... xaxis=Axis(10, angle=30, label="X axis", unit="km"), + ... ) + >>> def func(frame): + ... alias = AliasSystem(B="frame") + ... return alias.kwdict + >>> dict(func(frame)) + {'B': ['WSen+glightred+tMy Plot Title', 'x10+a30+lX axis+ukm']} + """ + + axes: Any = None + xaxis: Any = None + yaxis: Any = None + zaxis: Any = None + + _aliases: ClassVar = [ + Alias("axes"), + Alias("xaxis", prefix="x"), + Alias("yaxis", prefix="y"), + Alias("zaxis", prefix="z"), + ] + + def __iter__(self): + """ + Iterate over the aliases of the class. + + Yields + ------ + The value of each alias in the class. None are excluded. + """ + for alias in self._aliases: + alias.value = getattr(self, alias.name) + if alias.value is not None: + yield alias.value From 85edc006e9d1fbe9f914c9c38f9695fc28a85ea1 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 10:42:15 +0800 Subject: [PATCH 05/53] Add Figure.scalebar for plotting a scale bar --- pygmt/figure.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/scalebar.py | 66 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 pygmt/src/scalebar.py diff --git a/pygmt/figure.py b/pygmt/figure.py index 5190c4acf77..4a9223f5f7a 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -532,6 +532,7 @@ def _repr_html_(self): plot, plot3d, rose, + scalebar, set_panel, shift_origin, solar, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 1d113e5dd30..44da6c24b8b 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -41,6 +41,7 @@ from pygmt.src.plot3d import plot3d from pygmt.src.project import project from pygmt.src.rose import rose +from pygmt.src.scalebar import scalebar from pygmt.src.select import select from pygmt.src.shift_origin import shift_origin from pygmt.src.solar import solar diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py new file mode 100644 index 00000000000..de149857ae7 --- /dev/null +++ b/pygmt/src/scalebar.py @@ -0,0 +1,66 @@ +""" +scalebar - Add a scale bar. +""" + +from pygmt.alias import Alias, AliasSystem +from pygmt.clib import Session +from pygmt.helpers import build_arg_list + + +# ruff: noqa: ARG001 +def scalebar( # noqa: PLR0913 + self, + position, + length, + label_alignment=None, + scale_position=None, + fancy=None, + justify=None, + label=None, + offset=None, + unit=None, + vertical=None, + box=None, +): + """ + Add a scale bar. + + Parameters + ---------- + TODO + + Examples + -------- + >>> import pygmt + >>> from pygmt.params import Box + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True) + >>> fig.scalebar( + ... "g10/10", + ... length=1000, + ... fancy=True, + ... label="Scale", + ... unit=True, + ... box=Box(pen=0.5, fill="lightblue"), + ... ) + >>> fig.show() + """ + alias = AliasSystem( + L=[ + Alias("position", separator="/"), + Alias("length", prefix="+w"), + Alias("label_alignment", prefix="+a"), + Alias("scale_position", prefix="+c", separator="/"), + Alias("fancy", prefix="+f"), + Alias("justify", prefix="+j"), + Alias("label", prefix="+l"), + Alias("offset", prefix="+o", separator="/"), + Alias("unit", prefix="+u"), + Alias("vertical", prefix="+v"), + ], + F="box", + ) + + self._preprocess() + with Session() as lib: + lib.call_module(module="basemap", args=build_arg_list(alias.kwdict)) From b712840ff2063e0874ee3d98387aea11dca1dd4f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 10:43:19 +0800 Subject: [PATCH 06/53] Figure.basemap: Refactor to use the new alias system --- pygmt/src/basemap.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pygmt/src/basemap.py b/pygmt/src/basemap.py index 6355e2e02f7..061b2f89d9c 100644 --- a/pygmt/src/basemap.py +++ b/pygmt/src/basemap.py @@ -2,6 +2,7 @@ basemap - Plot base maps and frames for the figure. """ +from pygmt.alias import AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias @@ -12,7 +13,6 @@ J="projection", Jz="zscale", JZ="zsize", - B="frame", L="map_scale", F="box", Td="rose", @@ -82,6 +82,9 @@ def basemap(self, **kwargs): {perspective} {transparency} """ + alias = AliasSystem( + B="frame", + ) kwargs = self._preprocess(**kwargs) with Session() as lib: - lib.call_module(module="basemap", args=build_arg_list(kwargs)) + lib.call_module(module="basemap", args=build_arg_list(alias.kwdict)) From 1b480a0cb3a90e825715fbf2850a9f21ad168a61 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 10:43:42 +0800 Subject: [PATCH 07/53] pygmt.dimfilter: Refactor to use the new alias system --- pygmt/src/dimfilter.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/pygmt/src/dimfilter.py b/pygmt/src/dimfilter.py index 0498fb02e7c..fcbcd0a61f0 100644 --- a/pygmt/src/dimfilter.py +++ b/pygmt/src/dimfilter.py @@ -2,23 +2,15 @@ dimfilter - Directional filtering of grids in the space domain. """ +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput -from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias +from pygmt.helpers import build_arg_list, fmt_docstring __doctest_skip__ = ["dimfilter"] @fmt_docstring -@use_alias( - D="distance", - F="filter", - I="spacing", - N="sectors", - R="region", - V="verbose", -) -@kwargs_to_strings(I="sequence", R="sequence") def dimfilter(grid, outgrid: str | None = None, **kwargs): r""" Filter a grid by dividing the filter circle. @@ -41,8 +33,6 @@ def dimfilter(grid, outgrid: str | None = None, **kwargs): Full option list at :gmt-docs:`dimfilter.html` - {aliases} - Parameters ---------- {grid} @@ -135,19 +125,34 @@ def dimfilter(grid, outgrid: str | None = None, **kwargs): ... region=[-55, -51, -24, -19], ... ) """ - if not all(arg in kwargs for arg in ["D", "F", "N"]) and "Q" not in kwargs: + alias = AliasSystem( + D=Alias("distance"), + G=Alias("outgrid"), + F=Alias("filter"), + I=Alias("spacing", separator="/"), + N=Alias("sectors"), + R=Alias("region", separator="/"), + V=Alias("verbose"), + ) + + if ( + not all(arg in kwargs for arg in ["distance", "filter", "sectors"]) + and "Q" not in kwargs + ): raise GMTInvalidInput( """At least one of the following parameters must be specified: - distance, filters, or sectors.""" + distance, filter, or sectors.""" ) + kwdict = alias.kwdict with Session() as lib: with ( lib.virtualfile_in(check_kind="raster", data=grid) as vingrd, lib.virtualfile_out(kind="grid", fname=outgrid) as voutgrd, ): - kwargs["G"] = voutgrd + kwdict["G"] = voutgrd lib.call_module( - module="dimfilter", args=build_arg_list(kwargs, infile=vingrd) + module="dimfilter", + args=build_arg_list(kwdict, infile=vingrd), ) return lib.virtualfile_to_raster(vfname=voutgrd, outgrid=outgrid) From 075d75943d9055eba22c7692f65750a5b523b85e Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 10:44:08 +0800 Subject: [PATCH 08/53] Figure.image: Refactor to use the new alias system --- pygmt/src/image.py | 35 ++++++++++++++++++----------------- pygmt/tests/test_image.py | 3 ++- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/pygmt/src/image.py b/pygmt/src/image.py index 601c09d7eff..6ccd7e51286 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -2,24 +2,12 @@ image - Plot an image. """ +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias +from pygmt.helpers import build_arg_list, fmt_docstring @fmt_docstring -@use_alias( - D="position", - F="box", - G="bitcolor", - J="projection", - M="monochrome", - R="region", - V="verbose", - c="panel", - p="perspective", - t="transparency", -) -@kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") def image(self, imagefile, **kwargs): r""" Place images or EPS files on maps. @@ -29,8 +17,6 @@ def image(self, imagefile, **kwargs): Full option list at :gmt-docs:`image.html` - {aliases} - Parameters ---------- imagefile : str @@ -67,6 +53,21 @@ def image(self, imagefile, **kwargs): {perspective} {transparency} """ + alias = AliasSystem( + R=Alias("region", separator="/"), + J="projection", + D="position", + F="box", + G="bitcolor", + M="monochrome", + V="verbose", + c=Alias("panel", separator=","), + p=Alias("perspective", separator="/"), + t="transparency", + ) + kwargs = self._preprocess(**kwargs) with Session() as lib: - lib.call_module(module="image", args=build_arg_list(kwargs, infile=imagefile)) + lib.call_module( + module="image", args=build_arg_list(alias.kwdict, infile=imagefile) + ) diff --git a/pygmt/tests/test_image.py b/pygmt/tests/test_image.py index 5bcc8c28145..e69a8d71938 100644 --- a/pygmt/tests/test_image.py +++ b/pygmt/tests/test_image.py @@ -4,6 +4,7 @@ import pytest from pygmt import Figure +from pygmt.params import Box @pytest.mark.mpl_image_compare @@ -12,5 +13,5 @@ def test_image(): Place images on map. """ fig = Figure() - fig.image(imagefile="@circuit.png", position="x0/0+w2c", box="+pthin,blue") + fig.image(imagefile="@circuit.png", position="x0/0+w2c", box=Box(pen="thin,blue")) return fig From 0b0d5b9a29a87cd003e0ba1739536836eb2693e5 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 10:44:28 +0800 Subject: [PATCH 09/53] Figure.logo: Refactor to use the new alias system --- pygmt/src/logo.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index bab9c5dcd36..6221cdedab8 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -2,6 +2,7 @@ logo - Plot the GMT logo """ +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias @@ -11,7 +12,6 @@ R="region", J="projection", D="position", - F="box", S="style", V="verbose", c="panel", @@ -54,6 +54,9 @@ def logo(self, **kwargs): {panel} {transparency} """ + alias = AliasSystem( + F=Alias("box"), + ) kwargs = self._preprocess(**kwargs) with Session() as lib: - lib.call_module(module="logo", args=build_arg_list(kwargs)) + lib.call_module(module="logo", args=build_arg_list(alias.kwdict)) From c25ef4a7fa931462532cf7d4081d74e478b18c10 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 10:44:59 +0800 Subject: [PATCH 10/53] Figure.timestamp: Refactor to use the new alias system --- pygmt/src/timestamp.py | 65 +++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/pygmt/src/timestamp.py b/pygmt/src/timestamp.py index d091b8e300c..9876db84011 100644 --- a/pygmt/src/timestamp.py +++ b/pygmt/src/timestamp.py @@ -2,23 +2,18 @@ timestamp - Plot the GMT timestamp logo. """ -from __future__ import annotations - import warnings -from typing import TYPE_CHECKING +from collections.abc import Sequence from packaging.version import Version +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session, __gmt_version__ -from pygmt.helpers import build_arg_list, kwargs_to_strings - -if TYPE_CHECKING: - from collections.abc import Sequence - +from pygmt.helpers import build_arg_list, is_nonstr_iter __doctest_skip__ = ["timestamp"] -@kwargs_to_strings(offset="sequence") +# ruff: noqa: ARG001 def timestamp( self, text: str | None = None, @@ -81,35 +76,39 @@ def timestamp( >>> fig.timestamp(label="Powered by PyGMT") >>> fig.show() """ - self._preprocess() + alias = AliasSystem( + U=[ + Alias("label"), + Alias("justify", prefix="+j"), + Alias("offset", prefix="+o", separator="/"), + Alias("text", prefix="+t"), + ] + ) - # Build the options passed to the "plot" module - kwdict: dict = {"T": True, "U": ""} - if label is not None: - kwdict["U"] += f"{label}" - kwdict["U"] += f"+j{justify}" + self._preprocess() - if Version(__gmt_version__) <= Version("6.4.0") and "/" not in str(offset): - # Giving a single offset doesn't work in GMT <= 6.4.0. + # Workarounds for bugs/missing features for GMT<=6.4.0 + if Version(__gmt_version__) <= Version("6.4.0"): + # Giving a single offset doesn't work. # See https://github.com/GenericMappingTools/gmt/issues/7107. - 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. - if text is not None: - if len(str(text)) > 64: - msg = ( - "Argument of 'text' must be no longer than 64 characters. " - "The given text string will be truncated to 64 characters." - ) - warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) - if Version(__gmt_version__) <= Version("6.4.0"): - # workaround for GMT<=6.4.0 by overriding the 'timefmt' parameter + if (is_nonstr_iter(offset) and len(offset) == 1) or "/" not in str(offset): # type: ignore[arg-type] + offset = f"{offset}/{offset}" + # The +t modifier was added in GMT 6.5.0. + # See https://github.com/GenericMappingTools/gmt/pull/7127. + if text is not None: + # Overriding the 'timefmt' parameter and set 'text' to None timefmt = text[:64] - else: - kwdict["U"] += f"+t{text}" + text = None + + if text is not None and len(text) > 64: + msg = ( + "Argument of 'text' must be no longer than 64 characters. " + "The given text string will be truncated to 64 characters." + ) + warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) + text = text[:64] + kwdict: dict = {"T": True, "U": True} | alias.kwdict with Session() as lib: lib.call_module( module="plot", From ce74b850749c3640efb9d43164067dac88dcd376 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 18 Jan 2024 16:47:07 +0800 Subject: [PATCH 11/53] Figure.coast: Make the 'resolution' parameter more Pythonic --- pygmt/src/coast.py | 36 ++++++++++++++++++++++++++++-------- pygmt/tests/test_coast.py | 2 +- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/pygmt/src/coast.py b/pygmt/src/coast.py index aef178ea74c..6697a6638d5 100644 --- a/pygmt/src/coast.py +++ b/pygmt/src/coast.py @@ -2,6 +2,9 @@ coast - Plot land and water. """ +from typing import Literal + +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import ( @@ -20,7 +23,6 @@ A="area_thresh", B="frame", C="lakes", - D="resolution", E="dcw", F="box", G="land", @@ -37,7 +39,13 @@ t="transparency", ) @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") -def coast(self, **kwargs): +def coast( + self, + resolution: Literal[ # noqa: ARG001 + "auto", "full", "high", "intermediate", "low", "crude" + ] = "auto", + **kwargs, +): r""" Plot continents, shorelines, rivers, and borders on maps. @@ -75,11 +83,19 @@ def coast(self, **kwargs): parameter. Optionally, specify separate fills by appending **+l** for lakes or **+r** for river-lakes, and passing multiple strings in a list. - resolution : str - **f**\|\ **h**\|\ **i**\|\ **l**\|\ **c**. - Select the resolution of the data set to: (**f**\ )ull, - (**h**\ )igh, (**i**\ )ntermediate, (**l**\ )ow, - and (**c**\ )rude. + resolution + Select the resolution of the GSHHG coastline data set to use. The available + resolutions from highest to lowest are: + + - ``"full"`` - Full resolution (may be very slow for large regions). + - ``"high"`` - High resolution (may be slow for large regions). + - ``"intermediate"`` - Intermediate resolution. + - ``"low"`` - Low resolution. + - ``"crude"`` - Crude resolution, for tasks that need crude continent outlines + only. + + The default is ``"auto"`` to automatically select the best resolution given the + chosen map scale. land : str Select filling or clipping of "dry" areas. rivers : int, str, or list @@ -220,11 +236,15 @@ def coast(self, **kwargs): >>> # Show the plot >>> fig.show() """ + alias = AliasSystem( + D=Alias("resolution", mapping=True), + ) kwargs = self._preprocess(**kwargs) if not args_in_kwargs(args=["C", "G", "S", "I", "N", "E", "Q", "W"], kwargs=kwargs): raise GMTInvalidInput( """At least one of the following parameters must be specified: lakes, land, water, rivers, borders, dcw, Q, or shorelines""" ) + with Session() as lib: - lib.call_module(module="coast", args=build_arg_list(kwargs)) + lib.call_module(module="coast", args=build_arg_list(alias.kwdict)) diff --git a/pygmt/tests/test_coast.py b/pygmt/tests/test_coast.py index 780ec63cace..64153e8e36e 100644 --- a/pygmt/tests/test_coast.py +++ b/pygmt/tests/test_coast.py @@ -29,7 +29,7 @@ def test_coast_world_mercator(): projection="M15c", frame="af", land="#aaaaaa", - resolution="c", + resolution="crude", water="white", ) return fig From c6ea4b89039055394688984f7beddc3e7b5fb48a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 10 May 2024 11:22:41 +0800 Subject: [PATCH 12/53] pygmt.binstats: Make the 'statistic' parameter more Pythonic --- pygmt/src/binstats.py | 44 ++++++++++++++++++++++++++++++++---- pygmt/tests/test_binstats.py | 4 ++-- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/pygmt/src/binstats.py b/pygmt/src/binstats.py index d60a337d8b2..4764b6263f6 100644 --- a/pygmt/src/binstats.py +++ b/pygmt/src/binstats.py @@ -2,13 +2,13 @@ binstats - Bin spatial data and determine statistics per bin """ +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias @fmt_docstring @use_alias( - C="statistic", E="empty", I="spacing", N="normalize", @@ -23,7 +23,13 @@ r="registration", ) @kwargs_to_strings(I="sequence", R="sequence", i="sequence_comma") -def binstats(data, outgrid: str | None = None, **kwargs): +def binstats( + data, + outgrid: str | None = None, + statistic=None, + quantile_value=50, + **kwargs, # noqa: ARG001 +): r""" Bin spatial data and determine statistics per bin. @@ -69,6 +75,8 @@ def binstats(data, outgrid: str | None = None, **kwargs): - **u** for maximum (upper) - **U** for maximum of negative values only - **z** for the sum + quantile_value : float + The quantile value if ``statistic="quantile". empty : float Set the value assigned to empty nodes [Default is NaN]. normalize : bool @@ -102,13 +110,41 @@ def binstats(data, outgrid: str | None = None, **kwargs): - None if ``outgrid`` is set (grid output will be stored in file set by ``outgrid``) """ + alias = AliasSystem( + C=Alias( + "statistic", + mapping={ + "mean": "a", + "mad": "d", + "full": "g", + "interquartile": "i", + "min": "l", + "minpos": "L", + "median": "m", + "number": "n", + "lms": "o", + "mode": "p", + "quantile": "q", + "rms": "r", + "stddev": "s", + "max": "u", + "maxneg": "U", + "sum": "z", + }, + ), + G="outgrid", + ) + if statistic == "quantile": + statistic += str(quantile_value) + + kwdict = alias.kwdict with Session() as lib: with ( lib.virtualfile_in(check_kind="vector", data=data) as vintbl, lib.virtualfile_out(kind="grid", fname=outgrid) as voutgrd, ): - kwargs["G"] = voutgrd + kwdict["G"] = voutgrd lib.call_module( - module="binstats", args=build_arg_list(kwargs, infile=vintbl) + module="binstats", args=build_arg_list(kwdict, infile=vintbl) ) return lib.virtualfile_to_raster(vfname=voutgrd, outgrid=outgrid) diff --git a/pygmt/tests/test_binstats.py b/pygmt/tests/test_binstats.py index b8a1a39e8ad..beca8b00c0a 100644 --- a/pygmt/tests/test_binstats.py +++ b/pygmt/tests/test_binstats.py @@ -19,7 +19,7 @@ def test_binstats_outgrid(): data="@capitals.gmt", outgrid=tmpfile.name, spacing=5, - statistic="z", + statistic="sum", search_radius="1000k", aspatial="2=population", region="g", @@ -36,7 +36,7 @@ def test_binstats_no_outgrid(): temp_grid = binstats( data="@capitals.gmt", spacing=5, - statistic="z", + statistic="sum", search_radius="1000k", aspatial="2=population", region="g", From 477e7cbec366d0f4e14dcb91c45e62defacfd862 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 27 Sep 2024 18:48:21 +0800 Subject: [PATCH 13/53] Fix styling issues --- pygmt/src/binstats.py | 2 +- pygmt/src/dimfilter.py | 4 ++-- pygmt/src/timestamp.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pygmt/src/binstats.py b/pygmt/src/binstats.py index 5a106d46383..811561cadc7 100644 --- a/pygmt/src/binstats.py +++ b/pygmt/src/binstats.py @@ -2,8 +2,8 @@ binstats - Bin spatial data and determine statistics per bin """ -from pygmt.alias import Alias, AliasSystem import xarray as xr +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias diff --git a/pygmt/src/dimfilter.py b/pygmt/src/dimfilter.py index 97b95e7610a..2f660f2566d 100644 --- a/pygmt/src/dimfilter.py +++ b/pygmt/src/dimfilter.py @@ -2,8 +2,8 @@ dimfilter - Directional filtering of grids in the space domain. """ -from pygmt.alias import Alias, AliasSystem import xarray as xr +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import build_arg_list, fmt_docstring @@ -12,7 +12,7 @@ @fmt_docstring -def dimfilter(grid, outgrid: str | None = None, **kwargs)-> xr.DataArray | None: +def dimfilter(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | None: r""" Filter a grid by dividing the filter circle. diff --git a/pygmt/src/timestamp.py b/pygmt/src/timestamp.py index dba4993fb77..2f2972e026b 100644 --- a/pygmt/src/timestamp.py +++ b/pygmt/src/timestamp.py @@ -8,7 +8,7 @@ from packaging.version import Version from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session, __gmt_version__ -from pygmt.helpers import build_arg_list, kwargs_to_strings, is_nonstr_iter +from pygmt.helpers import build_arg_list, is_nonstr_iter __doctest_skip__ = ["timestamp"] From a13182f280ae742172b92f124d1bb17ddf97a559 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 3 Jan 2025 19:37:59 +0800 Subject: [PATCH 14/53] Improve the docstrings of the value_to_string function --- pygmt/alias.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index e285e915a73..a43748286f1 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -1,5 +1,5 @@ """ -Alias system that converts PyGMT parameters to GMT short-form options. +Alias system converting PyGMT parameters to GMT short-form options. """ import dataclasses @@ -20,14 +20,18 @@ def value_to_string( """ Convert any value to a string, a sequence of strings or None. - ``None`` or ``False`` will be converted to ``None``. + - ``None`` or ``False`` will be converted to ``None``. + - ``True`` will be converted to an empty string. + - A sequence will be joined by the separator if a separator is provided. Otherwise, + each item in the sequence will be converted to a string and a sequence of strings + will be returned. + - Any other value will be converted to a string if possible. - ``True`` will be converted to an empty string. If the value is a sequence and a - separator is provided, the sequence will be joined by the separator. Otherwise, each - item in the sequence will be converted to a string and a sequence of strings will be - returned. Any other value will be converted to a string if possible. It also tried - to convert PyGMT's long-form arguments into GMT's short-form arguments by using a - mapping dictionary or simply using the first letter of the long-form arguments. + If a mapping dictionary is provided, the value will be converted to the short-form + value that GMT accepts (e.g., mapping PyGMT long-form argument ``high`` to GMT's + short-form argument ``h``). For values not in the mapping dictionary, the original + value will be returned. If ``mapping`` is set to ``True``, the first letter of the + long-form argument will be used as the short-form argument. An optional prefix (e.g., `"+o"`) can be added to the beginning of the converted string. @@ -37,12 +41,17 @@ def value_to_string( value The value to convert. prefix - The string to add as a prefix to the value. + The string to add as a prefix to the returned value. separator The separator to use if the value is a sequence. mapping - Map long-form arguments to GMT's short-form arguments. If ``True``, will use the - first letter of the long-form arguments. + A mapping dictionary or ``True`` to map long-form arguments to GMT's short-form + arguments. If ``True``, will use the first letter of the long-form arguments. + + Returns + ------- + ret + The converted value. Examples -------- @@ -66,8 +75,6 @@ def value_to_string( ['xaf', 'yaf', 'WSen'] >>> value_to_string("high", mapping=True) 'h' - >>> value_to_string("low", mapping=True) - 'l' >>> value_to_string("mean", mapping={"mean": "a", "mad": "d", "full": "g"}) 'a' >>> value_to_string("invalid", mapping={"mean": "a", "mad": "d", "full": "g"}) @@ -77,11 +84,11 @@ def value_to_string( if value is None or value is False: return None # True means the parameter is specified, returns an empty string with the optional - # prefix ('prefix' defaults to an empty string!). + # prefix. We don't have to check 'prefix' since it defaults to an empty string! if value is True: return f"{prefix}" - # Convert any value to a string or a sequence of strings + # Convert any value to a string or a sequence of strings. if is_nonstr_iter(value): # Is a sequence value = [str(item) for item in value] # Convert to a sequence of strings if separator is None: @@ -89,8 +96,8 @@ def value_to_string( # a sequence of strings, which is used to support repeated GMT options like # '-B'. 'prefix' makes no sense here, so ignored. return value - value = separator.join(value) # Join the sequence by the specified separator. - if mapping: # Mapping long-form arguments to short-form arguments + value = separator.join(value) # Join the sequence with the separator. + elif mapping: # Mapping long-form arguments to short-form arguments. value = value[0] if mapping is True else mapping.get(value, value) return f"{prefix}{value}" From 44805815d7a4a8dbad1ba275417c08a0e5cf942d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 10 Mar 2025 19:10:08 +0800 Subject: [PATCH 15/53] Add more comments to the value_to_string function --- pygmt/alias.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index a43748286f1..ef62e055be8 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -80,16 +80,16 @@ def value_to_string( >>> value_to_string("invalid", mapping={"mean": "a", "mad": "d", "full": "g"}) 'invalid' """ - # None or False means the parameter is not specified, returns None. + # Return None if the value is None or False. if value is None or value is False: return None - # True means the parameter is specified, returns an empty string with the optional - # prefix. We don't have to check 'prefix' since it defaults to an empty string! + # Return an empty string if the value is True. We don't have to check 'prefix' since + # it defaults to an empty string! if value is True: return f"{prefix}" # Convert any value to a string or a sequence of strings. - if is_nonstr_iter(value): # Is a sequence + if is_nonstr_iter(value): # Is a sequence. value = [str(item) for item in value] # Convert to a sequence of strings if separator is None: # A sequence is given but separator is not specified. In this case, return @@ -99,6 +99,7 @@ def value_to_string( value = separator.join(value) # Join the sequence with the separator. elif mapping: # Mapping long-form arguments to short-form arguments. value = value[0] if mapping is True else mapping.get(value, value) + # Return the final string with the optional prefix. return f"{prefix}{value}" From da7cea4bd1d12b926b80ae15ea18b918d817e226 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 10 Mar 2025 19:29:42 +0800 Subject: [PATCH 16/53] Refactor to allow passing an initial value to an alias --- pygmt/alias.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index ef62e055be8..3de13a5947d 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -124,21 +124,34 @@ class Alias: Examples -------- + >>> par = Alias("offset", prefix="+o", separator="/", value=(3.0, 3.0)) + >>> par.value + '+o3.0/3.0' + >>> par = Alias("offset", prefix="+o", separator="/") >>> par.value = (2.0, 2.0) >>> par.value '+o2.0/2.0' + >>> par = Alias("frame") >>> par.value = ("xaf", "yaf", "WSen") >>> par.value ['xaf', 'yaf', 'WSen'] """ - name: str - prefix: str = "" # Default to an empty string to simplify code logic. - separator: str | None = None - mapping: bool | Mapping = False - _value: Any = None + def __init__( + self, + name: str, + prefix: str = "", + separator: str | None = None, + mapping: bool | Mapping = False, + value: Any = None, + ): + self.name = name + self.prefix = prefix + self.separator = separator + self.mapping = mapping + self.value = value @property def value(self) -> str | Sequence[str] | None: From bf5622830d2981fe3cd453878616e00dead7176d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 12 Mar 2025 17:13:17 +0800 Subject: [PATCH 17/53] Fix for Python 3.13 --- pygmt/alias.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index 3de13a5947d..6b81ec8f03b 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -245,7 +245,7 @@ def kwdict(self): # Get the local variables from the calling function. p_locals = inspect.currentframe().f_back.f_locals # Get parameters/arguments from **kwargs of the calling function. - p_kwargs = p_locals.pop("kwargs", {}) + p_kwargs = p_locals.get("kwargs", {}) params = p_locals | p_kwargs # Default value is an empty string to simplify code logic. From e73cbf772a947086170695f9080239e228836c40 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 29 Mar 2025 00:20:50 +0800 Subject: [PATCH 18/53] Update docstrings --- pygmt/alias.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index 6b81ec8f03b..160d47ab8f0 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -1,12 +1,12 @@ """ -Alias system converting PyGMT parameters to GMT short-form options. +PyGMT's alias system for converting PyGMT parameters to GMT short-form options. """ import dataclasses import inspect from collections import defaultdict from collections.abc import Mapping, Sequence -from typing import Any +from typing import Any, Literal from pygmt.helpers.utils import is_nonstr_iter @@ -14,7 +14,7 @@ def value_to_string( value: Any, prefix: str = "", # Default to an empty string to simplify the code logic. - separator: str | None = None, + separator: Literal["/", ","] | None = None, mapping: bool | Mapping = False, ) -> str | Sequence[str] | None: """ @@ -28,14 +28,19 @@ def value_to_string( - Any other value will be converted to a string if possible. If a mapping dictionary is provided, the value will be converted to the short-form - value that GMT accepts (e.g., mapping PyGMT long-form argument ``high`` to GMT's - short-form argument ``h``). For values not in the mapping dictionary, the original - value will be returned. If ``mapping`` is set to ``True``, the first letter of the - long-form argument will be used as the short-form argument. + string that GMT accepts (e.g., mapping PyGMT long-form argument ``"high"`` to GMT's + short-form argument ``"h"``). If the value is not in the mapping dictionary, the + original value will be returned. If ``mapping`` is set to ``True``, the first letter + of the long-form argument will be used as the short-form argument. An optional prefix (e.g., `"+o"`) can be added to the beginning of the converted string. + Need to note that this function doesn't check if the given parameters are valid, to + avoid the overhead of checking. For example, if ``value`` is a sequence but + ``separator`` is not specified, a sequence of strings will be returned. ``prefix`` + makes no sense here, but this function won't check it. + Parameters ---------- value @@ -92,11 +97,11 @@ def value_to_string( if is_nonstr_iter(value): # Is a sequence. value = [str(item) for item in value] # Convert to a sequence of strings if separator is None: - # A sequence is given but separator is not specified. In this case, return - # a sequence of strings, which is used to support repeated GMT options like - # '-B'. 'prefix' makes no sense here, so ignored. + # A sequence is given but separator is not specified. Return a sequence of + # strings, to support repeated GMT options like '-B'. 'prefix' makes no + # sense and is ignored. return value - value = separator.join(value) # Join the sequence with the separator. + value = separator.join(value) # Join the sequence by the separator. elif mapping: # Mapping long-form arguments to short-form arguments. value = value[0] if mapping is True else mapping.get(value, value) # Return the final string with the optional prefix. @@ -143,7 +148,7 @@ def __init__( self, name: str, prefix: str = "", - separator: str | None = None, + separator: Literal["/", ","] | None = None, mapping: bool | Mapping = False, value: Any = None, ): From ee072df6dc58b49f930d4f507fc4358ea6dbb1c7 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 29 Mar 2025 23:57:15 +0800 Subject: [PATCH 19/53] Improve the Box class --- pygmt/params/box.py | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/pygmt/params/box.py b/pygmt/params/box.py index 48cb66dd560..1ab27d12bc5 100644 --- a/pygmt/params/box.py +++ b/pygmt/params/box.py @@ -43,25 +43,60 @@ class Box(BaseParam): ... clearance=0.2, ... pen="blue", ... radius="10p", - ... shading=("5p", "5p", "lightred"), + ... shading_offset=("5p", "5p"), + ... shading_fill="lightred", ... ) ... ) '+c0.2+pblue+r10p+s5p/5p/lightred' - >>> str(Box(clearance=0.2, innerborder=("2p", "1p,red"), pen="blue")) + >>> str(Box(clearance=0.2, inner_gap="2p", inner_pen="1p,red", pen="blue")) '+c0.2+i2p/1p,red+pblue' + >>> str(Box(clearance=0.2, shading_offset=("5p", "5p"), shading_fill="lightred")) + '+c0.2+s5p/5p/lightred' """ + """ + The GMT syntax: + + [+c] + [+g] + [+i[[/]]] + [+p[]] + [+r[]] + [+s[//][]] + """ clearance: float | str | Sequence[float | str] | None = None fill: str | None = None - innerborder: str | Sequence | None = None + inner_gap: float | str | None = None + inner_pen: str | None = None pen: str | None = None radius: float | bool | None = False - shading: str | Sequence | None = None + shading_offset: Sequence[float | str] | None = None + shading_fill: str | None = None + + @property + def innerborder(self) -> str | None: + """ + innerborder="{inner_gap}/{inner_pen}" + """ + args = [self.inner_gap, self.inner_pen] + return "/".join([v for v in args if v is not None]) or None + + @property + def shading(self) -> str | None: + """ + shading="{shading_offset}/{shading_fill}" + """ + args = ( + [*self.shading_offset, self.shading_fill] + if self.shading_offset + else [self.shading_fill] + ) + return "/".join([v for v in args if v is not None]) or None _aliases: ClassVar = [ Alias("clearance", prefix="+c", separator="/"), Alias("fill", prefix="+g"), - Alias("innerborder", prefix="+i", separator="/"), + Alias("innerborder", prefix="+i"), Alias("pen", prefix="+p"), Alias("radius", prefix="+r"), Alias("shading", prefix="+s", separator="/"), From 6aca13d6afa12d673aec4ad7c6b8921e565b14de Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 30 Mar 2025 00:31:57 +0800 Subject: [PATCH 20/53] Avoid using inspect --- pygmt/alias.py | 74 +++++++++++++++++++----------------------- pygmt/src/basemap.py | 8 ++--- pygmt/src/binstats.py | 7 ++-- pygmt/src/coast.py | 2 +- pygmt/src/dimfilter.py | 30 +++++++++++------ pygmt/src/image.py | 34 +++++++++++++------ pygmt/src/logo.py | 2 +- pygmt/src/timestamp.py | 19 ++++++----- 8 files changed, 97 insertions(+), 79 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index 160d47ab8f0..581fd3d8393 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -3,7 +3,6 @@ """ import dataclasses -import inspect from collections import defaultdict from collections.abc import Mapping, Sequence from typing import Any, Literal @@ -210,14 +209,14 @@ class AliasSystem: ... ): ... alias = AliasSystem( ... A=[ - ... Alias("par1"), - ... Alias("par2", prefix="+j"), - ... Alias("par3", prefix="+o", separator="/"), + ... Alias("par1", value=par1), + ... Alias("par2", prefix="+j", value=par2), + ... Alias("par3", prefix="+o", separator="/", value=par3), ... ], - ... B=Alias("frame"), - ... c=Alias("panel", separator=","), + ... B=Alias("frame", value=frame), + ... c=Alias("panel", separator=",", value=panel), ... ) - ... return build_arg_list(alias.kwdict) + ... return build_arg_list(alias.kwdict | kwargs) >>> func( ... "infile", ... par1="mytext", @@ -235,33 +234,26 @@ def __init__(self, **kwargs): """ self.options = {} for option, aliases in kwargs.items(): - if isinstance(aliases, list): - self.options[option] = aliases - elif isinstance(aliases, str): # Support shorthand like 'J="projection"' - self.options[option] = [Alias(aliases)] - else: - self.options[option] = [aliases] + match aliases: + case list(): + self.options[option] = aliases + case str(): # Support shorthand like 'J="projection"' + self.options[option] = [Alias(aliases)] + case _: + self.options[option] = [aliases] @property def kwdict(self): """ A keyword dictionary that stores the current parameter values. """ - # Get the local variables from the calling function. - p_locals = inspect.currentframe().f_back.f_locals - # Get parameters/arguments from **kwargs of the calling function. - p_kwargs = p_locals.get("kwargs", {}) - - params = p_locals | p_kwargs # Default value is an empty string to simplify code logic. kwdict = defaultdict(str) for option, aliases in self.options.items(): for alias in aliases: - alias.value = params.get(alias.name) # value can be a string, a sequence of strings or None. if alias.value is None: continue - # Special handing of repeatable parameter like -B/frame. if is_nonstr_iter(alias.value): kwdict[option] = alias.value @@ -270,24 +262,24 @@ def kwdict(self): kwdict[option] += alias.value - # Support short-form parameter names specified in kwargs. - # Short-form parameters can be either one-letter (e.g., '-B'), or two-letters - # (e.g., '-Td'). - for option, value in p_kwargs.items(): - # Here, we assume that long-form parameters specified in kwargs are longer - # than two characters. Sometimes, we may use parameter like 'az', but it's - # not specified in kwargs. So, the assumption is still valid. - if len(option) > 2: - continue - - # Two cases for short-form parameters: - # - # If it has an alias and the long-form parameter is also specified, (e.g., - # 'projection="X10c", J="X10c"'), then we silently ignore the short-form - # parameter. - # - # If it has an alias but the long-form parameter is not specified, or it - # doesn't has an alias, then we use the value of the short-form parameter. - if option not in self.options or option not in kwdict: - kwdict[option] = value + # # Support short-form parameter names specified in kwargs. + # # Short-form parameters can be either one-letter (e.g., '-B'), or two-letters + # # (e.g., '-Td'). + # for option, value in self.options.items(): + # # Here, we assume that long-form parameters specified in kwargs are longer + # # than two characters. Sometimes, we may use parameter like 'az', but it's + # # not specified in kwargs. So, the assumption is still valid. + # if len(option) > 2: + # continue + + # # Two cases for short-form parameters: + # # + # # If it has an alias and the long-form parameter is also specified, (e.g., + # # 'projection="X10c", J="X10c"'), then we silently ignore the short-form + # # parameter. + # # + # # If it has an alias but the long-form parameter is not specified, or it + # # doesn't has an alias, then we use the value of the short-form parameter. + # if option not in self.options or option not in kwdict: + # kwdict[option] = value return kwdict diff --git a/pygmt/src/basemap.py b/pygmt/src/basemap.py index b03044bdb94..f62480b0a4d 100644 --- a/pygmt/src/basemap.py +++ b/pygmt/src/basemap.py @@ -2,7 +2,7 @@ basemap - Plot base maps and frames. """ -from pygmt.alias import AliasSystem +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias @@ -24,7 +24,7 @@ t="transparency", ) @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") -def basemap(self, **kwargs): +def basemap(self, frame=None, **kwargs): r""" Plot base maps and frames. @@ -83,8 +83,8 @@ def basemap(self, **kwargs): {transparency} """ alias = AliasSystem( - B="frame", + B=Alias("frame", value=frame), ) kwargs = self._preprocess(**kwargs) with Session() as lib: - lib.call_module(module="basemap", args=build_arg_list(alias.kwdict)) + lib.call_module(module="basemap", args=build_arg_list(alias.kwdict | kwargs)) diff --git a/pygmt/src/binstats.py b/pygmt/src/binstats.py index 25f9ee4fa9b..38750bfa2ac 100644 --- a/pygmt/src/binstats.py +++ b/pygmt/src/binstats.py @@ -29,7 +29,7 @@ def binstats( outgrid: str | None = None, statistic=None, quantile_value=50, - **kwargs, # noqa: ARG001 + **kwargs, ) -> xr.DataArray | None: r""" Bin spatial data and determine statistics per bin. @@ -130,13 +130,14 @@ def binstats( "maxneg": "U", "sum": "z", }, + value=statistic, ), - G="outgrid", + G=Alias("outgrid", value=outgrid), ) if statistic == "quantile": statistic += str(quantile_value) - kwdict = alias.kwdict + kwdict = alias.kwdict | kwargs with Session() as lib: with ( lib.virtualfile_in(check_kind="vector", data=data) as vintbl, diff --git a/pygmt/src/coast.py b/pygmt/src/coast.py index 039619759d5..c03ce0008fd 100644 --- a/pygmt/src/coast.py +++ b/pygmt/src/coast.py @@ -221,4 +221,4 @@ def coast( ) raise GMTInvalidInput(msg) with Session() as lib: - lib.call_module(module="coast", args=build_arg_list(alias.kwdict)) + lib.call_module(module="coast", args=build_arg_list(alias.kwdict | kwargs)) diff --git a/pygmt/src/dimfilter.py b/pygmt/src/dimfilter.py index 61ef10d8db3..88b6f5f1e84 100644 --- a/pygmt/src/dimfilter.py +++ b/pygmt/src/dimfilter.py @@ -12,7 +12,17 @@ @fmt_docstring -def dimfilter(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | None: +def dimfilter( + grid, + outgrid: str | None = None, + distance: int | str | None = None, + filter: str | None = None, + sectors: str | None = None, + spacing: str | list | None = None, + region: str | list | None = None, + verbose: bool | None = None, + **kwargs, +) -> xr.DataArray | None: r""" Directional filtering of grids in the space domain. @@ -125,17 +135,17 @@ def dimfilter(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | None ... ) """ alias = AliasSystem( - D=Alias("distance"), - G=Alias("outgrid"), - F=Alias("filter"), - I=Alias("spacing", separator="/"), - N=Alias("sectors"), - R=Alias("region", separator="/"), - V=Alias("verbose"), + D=Alias("distance", value=distance), + G=Alias("outgrid", value=outgrid), + F=Alias("filter", value=filter), + I=Alias("spacing", separator="/", value=spacing), + N=Alias("sectors", value=sectors), + R=Alias("region", separator="/", value=region), + V=Alias("verbose", value=verbose), ) if ( - not all(arg in kwargs for arg in ["distance", "filter", "sectors"]) + not all(v is not None for v in [distance, filter, sectors]) and "Q" not in kwargs ): msg = ( @@ -143,7 +153,7 @@ def dimfilter(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | None "distance, filters, or sectors." ) raise GMTInvalidInput(msg) - kwdict = alias.kwdict + kwdict = alias.kwdict | kwargs with Session() as lib: with ( lib.virtualfile_in(check_kind="raster", data=grid) as vingrd, diff --git a/pygmt/src/image.py b/pygmt/src/image.py index b93977fe977..3767ae4266e 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -8,7 +8,21 @@ @fmt_docstring -def image(self, imagefile, **kwargs): +def image( + self, + imagefile, + region=None, + projection=None, + position=None, + box=None, + bitcolor=None, + monochrome=None, + verbose=None, + panel=None, + perspective=None, + transparency=None, + **kwargs, +): r""" Plot raster or EPS images. @@ -54,20 +68,20 @@ def image(self, imagefile, **kwargs): {transparency} """ alias = AliasSystem( - R=Alias("region", separator="/"), - J="projection", - D="position", - F="box", - G="bitcolor", - M="monochrome", - V="verbose", + R=Alias("region", separator="/", value=region), + J=Alias("projection", value=projection), + D=Alias("position", value=position), + F=Alias("box", value=box), + G=Alias("bitcolor", value=bitcolor), + M=Alias("monochrome", value=monochrome), + V=Alias("verbose", value=verbose), c=Alias("panel", separator=","), p=Alias("perspective", separator="/"), - t="transparency", + t=Alias("transparency", value=transparency), ) kwargs = self._preprocess(**kwargs) with Session() as lib: lib.call_module( - module="image", args=build_arg_list(alias.kwdict, infile=imagefile) + module="image", args=build_arg_list(alias.kwdict | kwargs, infile=imagefile) ) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 3afa208ff16..6a1edaef006 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -59,4 +59,4 @@ def logo(self, **kwargs): ) kwargs = self._preprocess(**kwargs) with Session() as lib: - lib.call_module(module="logo", args=build_arg_list(alias.kwdict)) + lib.call_module(module="logo", args=build_arg_list(alias.kwdict | kwargs)) diff --git a/pygmt/src/timestamp.py b/pygmt/src/timestamp.py index fd6fbc44a72..3adc5c23038 100644 --- a/pygmt/src/timestamp.py +++ b/pygmt/src/timestamp.py @@ -77,14 +77,6 @@ def timestamp( >>> fig.timestamp(label="Powered by PyGMT") >>> fig.show() """ - alias = AliasSystem( - U=[ - Alias("label"), - Alias("justify", prefix="+j"), - Alias("offset", prefix="+o", separator="/"), - Alias("text", prefix="+t"), - ] - ) self._preprocess() @@ -109,7 +101,16 @@ def timestamp( warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) text = text[:64] - kwdict: dict = {"T": True, "U": True} | alias.kwdict + alias = AliasSystem( + U=[ + Alias("label", value=label), + Alias("justify", prefix="+j", value=justify), + Alias("offset", prefix="+o", separator="/", value=offset), + Alias("text", prefix="+t", value=text), + ] + ) + + kwdict = {"T": True} | alias.kwdict with Session() as lib: lib.call_module( module="plot", From 0655efecc0246b6febe7af572b6a2278ee20d585 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 30 Mar 2025 14:01:12 +0800 Subject: [PATCH 21/53] Fix --- pygmt/params/frame.py | 2 +- pygmt/src/logo.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py index 4593b4ca575..f390b9d480c 100644 --- a/pygmt/params/frame.py +++ b/pygmt/params/frame.py @@ -61,7 +61,7 @@ class Frame(BaseParam): ... xaxis=Axis(10, angle=30, label="X axis", unit="km"), ... ) >>> def func(frame): - ... alias = AliasSystem(B="frame") + ... alias = AliasSystem(B=Alias("frame", value=frame)) ... return alias.kwdict >>> dict(func(frame)) {'B': ['WSen+glightred+tMy Plot Title', 'x10+a30+lX axis+ukm']} diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 6a1edaef006..f2b9d31fb54 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -18,7 +18,7 @@ t="transparency", ) @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") -def logo(self, **kwargs): +def logo(self, box=None, **kwargs): r""" Plot the GMT logo. @@ -55,7 +55,7 @@ def logo(self, **kwargs): {transparency} """ alias = AliasSystem( - F=Alias("box"), + F=Alias("box", value=box), ) kwargs = self._preprocess(**kwargs) with Session() as lib: From 5067a3aada6a81b43083cf01a1e8225ff2597c1c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 30 Mar 2025 14:14:20 +0800 Subject: [PATCH 22/53] Add to documentation --- doc/api/index.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/api/index.rst b/doc/api/index.rst index 25de6d44adf..dabd13f4270 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -197,6 +197,17 @@ Getting metadata from tabular or grid data: info grdinfo +Common Parameters +----------------- + +.. currentmodule:: pygmt.params + +.. autosummary:: + :toctree: generated + + Box + Frame + Enums ----- From 38bc8073dd186dc484669cd050ec121d0834d931 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 30 Mar 2025 14:48:29 +0800 Subject: [PATCH 23/53] Remove commented codes --- pygmt/alias.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index 581fd3d8393..5735bc797cf 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -261,25 +261,4 @@ def kwdict(self): break kwdict[option] += alias.value - - # # Support short-form parameter names specified in kwargs. - # # Short-form parameters can be either one-letter (e.g., '-B'), or two-letters - # # (e.g., '-Td'). - # for option, value in self.options.items(): - # # Here, we assume that long-form parameters specified in kwargs are longer - # # than two characters. Sometimes, we may use parameter like 'az', but it's - # # not specified in kwargs. So, the assumption is still valid. - # if len(option) > 2: - # continue - - # # Two cases for short-form parameters: - # # - # # If it has an alias and the long-form parameter is also specified, (e.g., - # # 'projection="X10c", J="X10c"'), then we silently ignore the short-form - # # parameter. - # # - # # If it has an alias but the long-form parameter is not specified, or it - # # doesn't has an alias, then we use the value of the short-form parameter. - # if option not in self.options or option not in kwdict: - # kwdict[option] = value return kwdict From ee6ba4035f8ccbe83c4de064dcf1f036aff366e3 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 30 Mar 2025 16:25:59 +0800 Subject: [PATCH 24/53] Fix styling --- doc/api/index.rst | 2 +- pygmt/src/image.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/index.rst b/doc/api/index.rst index dabd13f4270..07edb1b0768 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -204,7 +204,7 @@ Common Parameters .. autosummary:: :toctree: generated - + Box Frame diff --git a/pygmt/src/image.py b/pygmt/src/image.py index 3767ae4266e..6680b2d6625 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -75,8 +75,8 @@ def image( G=Alias("bitcolor", value=bitcolor), M=Alias("monochrome", value=monochrome), V=Alias("verbose", value=verbose), - c=Alias("panel", separator=","), - p=Alias("perspective", separator="/"), + c=Alias("panel", separator=",", value=panel), + p=Alias("perspective", separator="/", value=perspective), t=Alias("transparency", value=transparency), ) From c02fdef52c8993ee1e286342bfc25f0fe1f0dace Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 31 Mar 2025 08:04:19 +0800 Subject: [PATCH 25/53] pygmt.grdclip: Deprecate parameter 'new' to 'replace' (remove in v0.19.0) (#3884) --- pygmt/src/grdclip.py | 14 +++++++++++--- pygmt/tests/test_grdclip.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/pygmt/src/grdclip.py b/pygmt/src/grdclip.py index 87dcf71f8c3..e780b904308 100644 --- a/pygmt/src/grdclip.py +++ b/pygmt/src/grdclip.py @@ -4,18 +4,26 @@ import xarray as xr from pygmt.clib import Session -from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias +from pygmt.helpers import ( + build_arg_list, + deprecate_parameter, + fmt_docstring, + kwargs_to_strings, + use_alias, +) __doctest_skip__ = ["grdclip"] +# TODO(PyGMT>=0.19.0): Remove the deprecated "new" parameter. @fmt_docstring +@deprecate_parameter("new", "replace", "v0.15.0", remove_version="v0.19.0") @use_alias( R="region", Sa="above", Sb="below", Si="between", - Sr="new", + Sr="replace", V="verbose", ) @kwargs_to_strings( @@ -55,7 +63,7 @@ def grdclip(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | None: between : str or list [*low*, *high*, *between*]. Set all data[i] >= *low* and <= *high* to *between*. - new : str or list + replace : str or list [*old*, *new*]. Set all data[i] == *old* to *new*. This is mostly useful when your data are known to be integer values. diff --git a/pygmt/tests/test_grdclip.py b/pygmt/tests/test_grdclip.py index d759e28eb91..f32a1cfb264 100644 --- a/pygmt/tests/test_grdclip.py +++ b/pygmt/tests/test_grdclip.py @@ -4,9 +4,12 @@ from pathlib import Path +import numpy as np +import numpy.testing as npt import pytest import xarray as xr from pygmt import grdclip, load_dataarray +from pygmt.datasets import load_earth_mask from pygmt.enums import GridRegistration, GridType from pygmt.helpers import GMTTempFile from pygmt.helpers.testing import load_static_earth_relief @@ -69,3 +72,19 @@ def test_grdclip_no_outgrid(grid, expected_grid): assert temp_grid.gmt.gtype == GridType.GEOGRAPHIC assert temp_grid.gmt.registration == GridRegistration.PIXEL xr.testing.assert_allclose(a=temp_grid, b=expected_grid) + + +def test_grdclip_replace(): + """ + Test the replace parameter for grdclip. + """ + grid = load_earth_mask(region=[0, 10, 0, 10]) + npt.assert_array_equal(np.unique(grid), [0, 1]) # Only have 0 and 1 + grid = grdclip(grid=grid, replace=[0, 2]) # Replace 0 with 2 + npt.assert_array_equal(np.unique(grid), [1, 2]) + + # Test for the deprecated 'new' parameter + # TODO(PyGMT>=0.19.0): Remove this test below for the 'new' parameter + with pytest.warns(FutureWarning): + grid = grdclip(grid=grid, new=[1, 3]) # Replace 1 with 3 + npt.assert_array_equal(np.unique(grid), [2, 3]) From a240fd40a53636e664e2368f3b09cc42cf7c8a3a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 31 Mar 2025 18:24:57 +0800 Subject: [PATCH 26/53] Changelog entry for v0.15.0 (#3866) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> Co-authored-by: Michael Grund <23025878+michaelgrund@users.noreply.github.com> Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> --- CITATION.cff | 6 +-- README.md | 10 ++--- doc/_static/version_switch.js | 1 + doc/changes.md | 73 ++++++++++++++++++++++++++++++----- doc/minversions.md | 1 + 5 files changed, 74 insertions(+), 17 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 94300414610..826218b6cd2 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -76,9 +76,9 @@ authors: family-names: Wessel affiliation: University of Hawaiʻi at Mānoa, USA orcid: https://orcid.org/0000-0001-5708-7336 -date-released: 2025-02-15 -doi: 10.5281/zenodo.14868324 +date-released: 2025-03-31 +doi: 10.5281/zenodo.15071586 license: BSD-3-Clause repository-code: https://github.com/GenericMappingTools/pygmt type: software -version: 0.14.2 +version: 0.15.0 diff --git a/README.md b/README.md index 389c5cdea90..5ee75a6e0e5 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Feel free to cite our work in your research using the following BibTeX: ``` @software{ - pygmt_2025_14868324, + pygmt_2025_15071586, author = {Tian, Dongdong and Uieda, Leonardo and Leong, Wei Ji and @@ -157,12 +157,12 @@ Feel free to cite our work in your research using the following BibTeX: Quinn, Jamie and Wessel, Paul}, title = {{PyGMT: A Python interface for the Generic Mapping Tools}}, - month = feb, + month = mar, year = 2025, publisher = {Zenodo}, - version = {0.14.2}, - doi = {10.5281/zenodo.14868324}, - url = {https://doi.org/10.5281/zenodo.14868324} + version = {0.15.0}, + doi = {10.5281/zenodo.15071586}, + url = {https://doi.org/10.5281/zenodo.15071586} } ``` diff --git a/doc/_static/version_switch.js b/doc/_static/version_switch.js index 3bab800591b..f3c47e68b40 100644 --- a/doc/_static/version_switch.js +++ b/doc/_static/version_switch.js @@ -12,6 +12,7 @@ var all_versions = { 'latest': 'latest', 'dev': 'dev', + 'v0.15.0': 'v0.15.0', 'v0.14.2': 'v0.14.2', 'v0.14.1': 'v0.14.1', 'v0.14.0': 'v0.14.0', diff --git a/doc/changes.md b/doc/changes.md index 6fa9d658024..1ebcb03d77a 100644 --- a/doc/changes.md +++ b/doc/changes.md @@ -1,5 +1,60 @@ # Changelog +## Release v0.15.0 (2025/03/31) + +[![Digital Object Identifier for PyGMT v0.15.0](https://zenodo.org/badge/DOI/10.5281/zenodo.15071586.svg)](https://doi.org/10.5281/zenodo.15071586) + +### Highlights + +* 🎉 **Fifteenth minor release of PyGMT** 🎉 +* One new gallery example and two new tutorials +* Figure.shift_origin: Support shifting origins temporarily when used as a context manager ([#2509](https://github.com/GenericMappingTools/pygmt/pull/2509)) +* Documentation as HTML ZIP archive and in PDF format for offline reference + +### Enhancements + +* **BREAKING** Support typesetting apostrophe (') and backtick (`) ([#3105](https://github.com/GenericMappingTools/pygmt/pull/3105)) +* **BREAKING** pygmt.grdcut: Refactor to store output in virtualfiles for grids ([#3115](https://github.com/GenericMappingTools/pygmt/pull/3115)) +* GMTDataArrayAccessor: Support passing values using enums GridRegistration and GridType for grid registration and type ([#3696](https://github.com/GenericMappingTools/pygmt/pull/3696)) +* pygmt.grdfill: Add new parameters 'constantfill'/'gridfill'/'neighborfill'/'splinefill' for filling holes ([#3855](https://github.com/GenericMappingTools/pygmt/pull/3855)) +* pygmt.grdfill: Add new parameter 'inquire' to inquire the bounds of holes ([#3880](https://github.com/GenericMappingTools/pygmt/pull/3880)) +* pygmt.grdfill: Add alias 'coltypes' (-f) ([#3869](https://github.com/GenericMappingTools/pygmt/pull/3869)) + +### Deprecations + +* pygmt.grdfill: Deprecate parameter 'no_data' to 'hole' (remove in v0.19.0) ([#3852](https://github.com/GenericMappingTools/pygmt/pull/3852)) +* pygmt.grdfill: Deprecate parameter 'mode', use parameters 'constantfill'/'gridfill'/'neighborfill'/'splinefill' instead (remove in v0.19.0) ([#3855](https://github.com/GenericMappingTools/pygmt/pull/3855)) +* pygmt.grdclip: Deprecate parameter 'new' to 'replace' (remove in v0.19.0) ([#3884](https://github.com/GenericMappingTools/pygmt/pull/3884)) +* clib.Session: Remove deprecated open_virtual_file method, use open_virtualfile instead (Deprecated since v0.11.0) ([#3738](https://github.com/GenericMappingTools/pygmt/pull/3738)) +* clib.Session: Remove deprecated virtualfile_from_data method, use virtualfile_in instead (Deprecated since v0.13.0) ([#3739](https://github.com/GenericMappingTools/pygmt/pull/3739)) + +### Documentation + +* Add an advanced tutorial for plotting focal mechanisms (beachballs) ([#2550](https://github.com/GenericMappingTools/pygmt/pull/2550)) +* Add an advanced tutorial for creating legends ([#3594](https://github.com/GenericMappingTools/pygmt/pull/3594)) +* Add a gallery example for Figure.hlines and Figure.vlines ([#3755](https://github.com/GenericMappingTools/pygmt/pull/3755)) + +### Maintenance + +* Use the 'release-branch-semver' version scheme for setuptools_scm ([#3828](https://github.com/GenericMappingTools/pygmt/pull/3828)) +* Rename _GMT_DATASET.to_dataframe to .to_pandas and _GMT_GRID.to_dataarray/_GMT_IMAGE.to_dataarray to .to_xarray ([#3798](https://github.com/GenericMappingTools/pygmt/pull/3798)) +* Bump to ruff 0.9.0, apply ruff 2025 style, and ignore A005 (stdlib-module-shadowing) violations ([#3763](https://github.com/GenericMappingTools/pygmt/pull/3763)) +* Use well-known labels in project URLs following PEP753 ([#3743](https://github.com/GenericMappingTools/pygmt/pull/3743)) +* clib.conversion: Remove the unused array_to_datetime function ([#3507](https://github.com/GenericMappingTools/pygmt/pull/3507)) +* CI: Test on Linux arm64 runners ([#3778](https://github.com/GenericMappingTools/pygmt/pull/3778)) +* CI: Build PDF documentation using tectonic ([#3765](https://github.com/GenericMappingTools/pygmt/pull/3765)) + +**Full Changelog**: + +### Contributors + +* [Dongdong Tian](https://github.com/seisman) +* [Yvonne Fröhlich](https://github.com/yvonnefroehlich) +* [Wei Ji Leong](https://github.com/weiji14) +* [Michael Grund](https://github.com/michaelgrund) + +--- + ## Release v0.14.2 (2025/02/15) [![Digital Object Identifier for PyGMT v0.14.2](https://zenodo.org/badge/DOI/10.5281/zenodo.14868324.svg)](https://doi.org/10.5281/zenodo.14868324) @@ -7,8 +62,8 @@ ### Bug Fixes -- **Patch release fixing a critical bug introduced in PyGMT v0.14.1** -- Fix the bug for passing text strings with numeric values ([#3804](https://github.com/GenericMappingTools/pygmt/pull/3804)) +* **Patch release fixing a critical bug introduced in PyGMT v0.14.1** +* Fix the bug for passing text strings with numeric values ([#3804](https://github.com/GenericMappingTools/pygmt/pull/3804)) **Full Changelog**: @@ -24,16 +79,16 @@ ### Highlights -- **Patch release fixing critical bugs in PyGMT v0.14.0** -- Fix the bug of converting Python sequence of datetime-like objects ([#3760](https://github.com/GenericMappingTools/pygmt/pull/3760)) +* **Patch release fixing critical bugs in PyGMT v0.14.0** +* Fix the bug of converting Python sequence of datetime-like objects ([#3760](https://github.com/GenericMappingTools/pygmt/pull/3760)) ### Maintenance -- CI: Separate jobs for publishing to TestPyPI and PyPI ([#3742](https://github.com/GenericMappingTools/pygmt/pull/3742)) -- clib.conversion._to_numpy: Add tests for Python sequence of datetime-like objects ([#3758](https://github.com/GenericMappingTools/pygmt/pull/3758)) -- Fix an image in README.md (broken on PyPI) and rewrap to 88 characters ([#3740](https://github.com/GenericMappingTools/pygmt/pull/3740)) -- Fix the dataset link in the RGB image gallery example ([#3781](https://github.com/GenericMappingTools/pygmt/pull/3781)) -- Update License year to 2025 ([#3737](https://github.com/GenericMappingTools/pygmt/pull/3737)) +* CI: Separate jobs for publishing to TestPyPI and PyPI ([#3742](https://github.com/GenericMappingTools/pygmt/pull/3742)) +* clib.conversion._to_numpy: Add tests for Python sequence of datetime-like objects ([#3758](https://github.com/GenericMappingTools/pygmt/pull/3758)) +* Fix an image in README.md (broken on PyPI) and rewrap to 88 characters ([#3740](https://github.com/GenericMappingTools/pygmt/pull/3740)) +* Fix the dataset link in the RGB image gallery example ([#3781](https://github.com/GenericMappingTools/pygmt/pull/3781)) +* Update License year to 2025 ([#3737](https://github.com/GenericMappingTools/pygmt/pull/3737)) **Full Changelog**: diff --git a/doc/minversions.md b/doc/minversions.md index 21da2ee058e..c070b858e79 100644 --- a/doc/minversions.md +++ b/doc/minversions.md @@ -47,6 +47,7 @@ compatibility reasons. | PyGMT Version | Documentation | GMT | Python | NumPy | pandas | Xarray | |---|---|---|---|---|---|---| | [Dev][]* | , [HTML+ZIP](doc:dev/pygmt-docs.zip), [PDF](doc:dev/pygmt-docs.pdf) | {{ requires.gmt }} | {{ requires.python }} | {{ requires.numpy }} | {{ requires.pandas }} | {{ requires.xarray }} | +| | , , | >=6.4.0 | >=3.11 | >=1.25 | >=2.0 | >=2023.04 | | | , | >=6.4.0 | >=3.11 | >=1.25 | >=2.0 | >=2023.04 | | | , | >=6.4.0 | >=3.11 | >=1.25 | >=2.0 | >=2023.04 | | | , | >=6.4.0 | >=3.11 | >=1.25 | >=2.0 | >=2023.04 | From 397ac10a0ea83e57968577ed17e7cf6e323c071c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yvonne=20Fr=C3=B6hlich?= <94163266+yvonnefroehlich@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:00:34 +0200 Subject: [PATCH 27/53] DOC: Update URLs (Link Checker Report on 2025-03-30) (#3886) --- .github/workflows/check-links.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index f395ee1e77c..072e073ff74 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -66,11 +66,8 @@ jobs: --exclude "^https://www.pygmt.org/%7B%7Bpath%7D%7D" --exclude "^https://www.researchgate.net/" --exclude "^https://zenodo.org/badge/DOI/" - --exclude-path "repository/doc/**/*.rst" - --exclude-path "repository/doc/**/*.md" --verbose - "repository/**/*.rst" - "repository/**/*.md" + "repository/*.md" "repository/**/*.py" "documentation/dev/**/*.html" From 4100aceee8e6202753e9c28e696c13d1a170654e Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 1 Apr 2025 00:35:19 +0800 Subject: [PATCH 28/53] Some fixes --- pygmt/params/box.py | 7 +++---- pygmt/src/dimfilter.py | 2 +- pygmt/src/image.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pygmt/params/box.py b/pygmt/params/box.py index 1ab27d12bc5..c785dbc20a7 100644 --- a/pygmt/params/box.py +++ b/pygmt/params/box.py @@ -78,8 +78,7 @@ def innerborder(self) -> str | None: """ innerborder="{inner_gap}/{inner_pen}" """ - args = [self.inner_gap, self.inner_pen] - return "/".join([v for v in args if v is not None]) or None + return [v for v in (self.inner_gap, self.inner_pen) if v is not None] or None @property def shading(self) -> str | None: @@ -91,12 +90,12 @@ def shading(self) -> str | None: if self.shading_offset else [self.shading_fill] ) - return "/".join([v for v in args if v is not None]) or None + return [v for v in args if v is not None] or None _aliases: ClassVar = [ Alias("clearance", prefix="+c", separator="/"), Alias("fill", prefix="+g"), - Alias("innerborder", prefix="+i"), + Alias("innerborder", prefix="+i", separator="/"), Alias("pen", prefix="+p"), Alias("radius", prefix="+r"), Alias("shading", prefix="+s", separator="/"), diff --git a/pygmt/src/dimfilter.py b/pygmt/src/dimfilter.py index 88b6f5f1e84..03119b9d7da 100644 --- a/pygmt/src/dimfilter.py +++ b/pygmt/src/dimfilter.py @@ -16,7 +16,7 @@ def dimfilter( grid, outgrid: str | None = None, distance: int | str | None = None, - filter: str | None = None, + filter: str | None = None, # noqa: A002 sectors: str | None = None, spacing: str | list | None = None, region: str | list | None = None, diff --git a/pygmt/src/image.py b/pygmt/src/image.py index 6680b2d6625..3411587c31d 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -8,7 +8,7 @@ @fmt_docstring -def image( +def image( # noqa: PLR0913 self, imagefile, region=None, From 165412a40d5eee7acc9e49bc653b74db44149ea7 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 1 Apr 2025 11:09:51 +0800 Subject: [PATCH 29/53] Remove the 'name' attribute from the Alias class --- pygmt/alias.py | 47 +++++++++------------------------------- pygmt/params/base.py | 27 +++++++++++------------ pygmt/params/box.py | 19 ++++++++-------- pygmt/params/frame.py | 49 ++++++++++++++++++++++-------------------- pygmt/src/basemap.py | 2 +- pygmt/src/binstats.py | 5 ++--- pygmt/src/coast.py | 4 ++-- pygmt/src/dimfilter.py | 14 ++++++------ pygmt/src/image.py | 20 ++++++++--------- pygmt/src/logo.py | 2 +- pygmt/src/scalebar.py | 22 +++++++++---------- pygmt/src/timestamp.py | 8 +++---- 12 files changed, 97 insertions(+), 122 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index 5735bc797cf..4e3c7683a4b 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -128,51 +128,24 @@ class Alias: Examples -------- - >>> par = Alias("offset", prefix="+o", separator="/", value=(3.0, 3.0)) + >>> par = Alias((3.0, 3.0), prefix="+o", separator="/") >>> par.value '+o3.0/3.0' - >>> par = Alias("offset", prefix="+o", separator="/") - >>> par.value = (2.0, 2.0) - >>> par.value - '+o2.0/2.0' - - >>> par = Alias("frame") - >>> par.value = ("xaf", "yaf", "WSen") + >>> par = Alias(["xaf", "yaf", "WSen"]) >>> par.value ['xaf', 'yaf', 'WSen'] """ def __init__( self, - name: str, + value: Any, prefix: str = "", separator: Literal["/", ","] | None = None, mapping: bool | Mapping = False, - value: Any = None, ): - self.name = name - self.prefix = prefix - self.separator = separator - self.mapping = mapping - self.value = value - - @property - def value(self) -> str | Sequence[str] | None: - """ - Get the value of the parameter. - """ - return self._value - - @value.setter - def value(self, new_value: Any): - """ - Set the value of the parameter. - - Internally, the value is converted to a string, a sequence of strings or None. - """ - self._value = value_to_string( - new_value, self.prefix, self.separator, self.mapping + self.value = value_to_string( + value=value, prefix=prefix, separator=separator, mapping=mapping ) @@ -209,12 +182,12 @@ class AliasSystem: ... ): ... alias = AliasSystem( ... A=[ - ... Alias("par1", value=par1), - ... Alias("par2", prefix="+j", value=par2), - ... Alias("par3", prefix="+o", separator="/", value=par3), + ... Alias(par1), + ... Alias(par2, prefix="+j"), + ... Alias(par3, prefix="+o", separator="/"), ... ], - ... B=Alias("frame", value=frame), - ... c=Alias("panel", separator=",", value=panel), + ... B=Alias(frame), + ... c=Alias(panel, separator=","), ... ) ... return build_arg_list(alias.kwdict | kwargs) >>> func( diff --git a/pygmt/params/base.py b/pygmt/params/base.py index d5ff0ef59d8..dba049b0e27 100644 --- a/pygmt/params/base.py +++ b/pygmt/params/base.py @@ -20,11 +20,13 @@ class BaseParam: ... par2: Any = None ... par3: Any = None ... - ... _aliases = [ - ... Alias("par1"), - ... Alias("par2", prefix="+a"), - ... Alias("par3", prefix="+b", separator="/"), - ... ] + ... @property + ... def _aliases(self): + ... return [ + ... Alias(self.par1), + ... Alias(self.par2, prefix="+a"), + ... Alias(self.par3, prefix="+b", separator="/"), + ... ] >>> var = Test(par1="val1") >>> str(var) 'val1' @@ -36,8 +38,6 @@ def __str__(self): """ String representation of the object that can be passed to GMT directly. """ - for alias in self._aliases: - alias.value = getattr(self, alias.name) return "".join( [alias.value for alias in self._aliases if alias.value is not None] ) @@ -46,10 +46,9 @@ def __repr__(self): """ String representation of the object. """ - string = [] - for alias in self._aliases: - value = getattr(self, alias.name) - if value is None or value is False: - continue - string.append(f"{alias.name}={value!r}") - return f"{self.__class__.__name__}({', '.join(string)})" + params = ", ".join( + f"{k}={v!r}" + for k, v in vars(self).items() + if v is not None and v is not False + ) + return f"{self.__class__.__name__}({params})" diff --git a/pygmt/params/box.py b/pygmt/params/box.py index c785dbc20a7..b84ef5f2475 100644 --- a/pygmt/params/box.py +++ b/pygmt/params/box.py @@ -4,7 +4,6 @@ from collections.abc import Sequence from dataclasses import dataclass -from typing import ClassVar from pygmt.alias import Alias from pygmt.params.base import BaseParam @@ -92,11 +91,13 @@ def shading(self) -> str | None: ) return [v for v in args if v is not None] or None - _aliases: ClassVar = [ - Alias("clearance", prefix="+c", separator="/"), - Alias("fill", prefix="+g"), - Alias("innerborder", prefix="+i", separator="/"), - Alias("pen", prefix="+p"), - Alias("radius", prefix="+r"), - Alias("shading", prefix="+s", separator="/"), - ] + @property + def _aliases(self): + return [ + Alias(self.clearance, prefix="+c", separator="/"), + Alias(self.fill, prefix="+g"), + Alias(self.innerborder, prefix="+i", separator="/"), + Alias(self.pen, prefix="+p"), + Alias(self.radius, prefix="+r"), + Alias(self.shading, prefix="+s", separator="/"), + ] diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py index f390b9d480c..6613959ca12 100644 --- a/pygmt/params/frame.py +++ b/pygmt/params/frame.py @@ -3,7 +3,7 @@ """ from dataclasses import dataclass -from typing import Any, ClassVar +from typing import Any from pygmt.alias import Alias from pygmt.params.base import BaseParam @@ -23,11 +23,13 @@ class Axes(BaseParam): fill: Any = None title: Any = None - _aliases: ClassVar = [ - Alias("axes"), - Alias("fill", prefix="+g"), - Alias("title", prefix="+t"), - ] + @property + def _aliases(self): + return [ + Alias(self.axes), + Alias(self.fill, prefix="+g"), + Alias(self.title, prefix="+t"), + ] @dataclass(repr=False) @@ -43,12 +45,14 @@ class Axis(BaseParam): label: str | None = None unit: str | None = None - _aliases: ClassVar = [ - Alias("interval"), - Alias("angle", prefix="+a"), - Alias("label", prefix="+l"), - Alias("unit", prefix="+u"), - ] + @property + def _aliases(self): + return [ + Alias(self.interval), + Alias(self.angle, prefix="+a"), + Alias(self.label, prefix="+l"), + Alias(self.unit, prefix="+u"), + ] @dataclass(repr=False) @@ -61,7 +65,7 @@ class Frame(BaseParam): ... xaxis=Axis(10, angle=30, label="X axis", unit="km"), ... ) >>> def func(frame): - ... alias = AliasSystem(B=Alias("frame", value=frame)) + ... alias = AliasSystem(B=Alias(frame)) ... return alias.kwdict >>> dict(func(frame)) {'B': ['WSen+glightred+tMy Plot Title', 'x10+a30+lX axis+ukm']} @@ -72,12 +76,14 @@ class Frame(BaseParam): yaxis: Any = None zaxis: Any = None - _aliases: ClassVar = [ - Alias("axes"), - Alias("xaxis", prefix="x"), - Alias("yaxis", prefix="y"), - Alias("zaxis", prefix="z"), - ] + @property + def _aliases(self): + return [ + Alias(self.axes), + Alias(self.xaxis, prefix="x"), + Alias(self.yaxis, prefix="y"), + Alias(self.zaxis, prefix="z"), + ] def __iter__(self): """ @@ -87,7 +93,4 @@ def __iter__(self): ------ The value of each alias in the class. None are excluded. """ - for alias in self._aliases: - alias.value = getattr(self, alias.name) - if alias.value is not None: - yield alias.value + yield from (alias.value for alias in self._aliases if alias.value is not None) diff --git a/pygmt/src/basemap.py b/pygmt/src/basemap.py index f62480b0a4d..4beb0bb323f 100644 --- a/pygmt/src/basemap.py +++ b/pygmt/src/basemap.py @@ -83,7 +83,7 @@ def basemap(self, frame=None, **kwargs): {transparency} """ alias = AliasSystem( - B=Alias("frame", value=frame), + B=Alias(frame), ) kwargs = self._preprocess(**kwargs) with Session() as lib: diff --git a/pygmt/src/binstats.py b/pygmt/src/binstats.py index 38750bfa2ac..61b0633e684 100644 --- a/pygmt/src/binstats.py +++ b/pygmt/src/binstats.py @@ -111,7 +111,7 @@ def binstats( """ alias = AliasSystem( C=Alias( - "statistic", + statistic, mapping={ "mean": "a", "mad": "d", @@ -130,9 +130,8 @@ def binstats( "maxneg": "U", "sum": "z", }, - value=statistic, ), - G=Alias("outgrid", value=outgrid), + G=Alias(outgrid), ) if statistic == "quantile": statistic += str(quantile_value) diff --git a/pygmt/src/coast.py b/pygmt/src/coast.py index c03ce0008fd..f812b1bca35 100644 --- a/pygmt/src/coast.py +++ b/pygmt/src/coast.py @@ -41,7 +41,7 @@ @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") def coast( self, - resolution: Literal[ # noqa: ARG001 + resolution: Literal[ "auto", "full", "high", "intermediate", "low", "crude" ] = "auto", **kwargs, @@ -211,7 +211,7 @@ def coast( >>> fig.show() """ alias = AliasSystem( - D=Alias("resolution", mapping=True), + D=Alias(resolution, mapping=True), ) kwargs = self._preprocess(**kwargs) if not args_in_kwargs(args=["C", "G", "S", "I", "N", "E", "Q", "W"], kwargs=kwargs): diff --git a/pygmt/src/dimfilter.py b/pygmt/src/dimfilter.py index 03119b9d7da..c31abdca469 100644 --- a/pygmt/src/dimfilter.py +++ b/pygmt/src/dimfilter.py @@ -135,13 +135,13 @@ def dimfilter( ... ) """ alias = AliasSystem( - D=Alias("distance", value=distance), - G=Alias("outgrid", value=outgrid), - F=Alias("filter", value=filter), - I=Alias("spacing", separator="/", value=spacing), - N=Alias("sectors", value=sectors), - R=Alias("region", separator="/", value=region), - V=Alias("verbose", value=verbose), + D=Alias(distance), + G=Alias(outgrid), + F=Alias(filter), + I=Alias(spacing, separator="/"), + N=Alias(sectors), + R=Alias(region, separator="/"), + V=Alias(verbose), ) if ( diff --git a/pygmt/src/image.py b/pygmt/src/image.py index 3411587c31d..9cb17e844bc 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -68,16 +68,16 @@ def image( # noqa: PLR0913 {transparency} """ alias = AliasSystem( - R=Alias("region", separator="/", value=region), - J=Alias("projection", value=projection), - D=Alias("position", value=position), - F=Alias("box", value=box), - G=Alias("bitcolor", value=bitcolor), - M=Alias("monochrome", value=monochrome), - V=Alias("verbose", value=verbose), - c=Alias("panel", separator=",", value=panel), - p=Alias("perspective", separator="/", value=perspective), - t=Alias("transparency", value=transparency), + R=Alias(region, separator="/"), + J=Alias(projection), + D=Alias(position), + F=Alias(box), + G=Alias(bitcolor), + M=Alias(monochrome), + V=Alias(verbose), + c=Alias(panel, separator=","), + p=Alias(perspective, separator="/"), + t=Alias(transparency), ) kwargs = self._preprocess(**kwargs) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index f2b9d31fb54..b2bb6f66aec 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -55,7 +55,7 @@ def logo(self, box=None, **kwargs): {transparency} """ alias = AliasSystem( - F=Alias("box", value=box), + F=Alias(box), ) kwargs = self._preprocess(**kwargs) with Session() as lib: diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index fcf791ce355..3f9dfb06404 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -47,18 +47,18 @@ def scalebar( # noqa: PLR0913 """ alias = AliasSystem( L=[ - Alias("position", separator="/", value=position), - Alias("length", prefix="+w", value=length), - Alias("label_alignment", prefix="+a", value=label_alignment), - Alias("scale_position", prefix="+c", separator="/", value=scale_position), - Alias("fancy", prefix="+f", value=fancy), - Alias("justify", prefix="+j", value=justify), - Alias("label", prefix="+l", value=label), - Alias("offset", prefix="+o", separator="/"), - Alias("unit", prefix="+u"), - Alias("vertical", prefix="+v"), + Alias(position, separator="/"), + Alias(length, prefix="+w"), + Alias(label_alignment, prefix="+a"), + Alias(scale_position, prefix="+c", separator="/"), + Alias(fancy, prefix="+f"), + Alias(justify, prefix="+j"), + Alias(label, prefix="+l"), + Alias(offset, prefix="+o", separator="/"), + Alias(unit, prefix="+u"), + Alias(vertical, prefix="+v"), ], - F="box", + F=Alias(box), ) self._preprocess() diff --git a/pygmt/src/timestamp.py b/pygmt/src/timestamp.py index 3adc5c23038..74c5d21b0ac 100644 --- a/pygmt/src/timestamp.py +++ b/pygmt/src/timestamp.py @@ -103,10 +103,10 @@ def timestamp( alias = AliasSystem( U=[ - Alias("label", value=label), - Alias("justify", prefix="+j", value=justify), - Alias("offset", prefix="+o", separator="/", value=offset), - Alias("text", prefix="+t", value=text), + Alias(label), + Alias(justify, prefix="+j"), + Alias(offset, prefix="+o", separator="/"), + Alias(text, prefix="+t"), ] ) From 79a0d91a1bb1322c559696673bbc0d90b25f40b8 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 1 Apr 2025 12:22:02 +0800 Subject: [PATCH 30/53] Simplify Figure.timestamp --- pygmt/src/timestamp.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pygmt/src/timestamp.py b/pygmt/src/timestamp.py index 74c5d21b0ac..cf9eeb3ea31 100644 --- a/pygmt/src/timestamp.py +++ b/pygmt/src/timestamp.py @@ -77,7 +77,6 @@ def timestamp( >>> fig.timestamp(label="Powered by PyGMT") >>> fig.show() """ - self._preprocess() # TODO(GMT>=6.5.0): Remove the patch for upstream bug fixed in GMT 6.5.0. @@ -101,16 +100,15 @@ def timestamp( warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) text = text[:64] - alias = AliasSystem( + kwdict = AliasSystem( U=[ Alias(label), Alias(justify, prefix="+j"), Alias(offset, prefix="+o", separator="/"), Alias(text, prefix="+t"), ] - ) + ).kwdict | {"T": True} - kwdict = {"T": True} | alias.kwdict with Session() as lib: lib.call_module( module="plot", From a2a3e3d9bd9a623d16a262151ade646ae00935b7 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 1 Apr 2025 12:46:31 +0800 Subject: [PATCH 31/53] Improve Figure.scalebar --- pygmt/src/scalebar.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 3f9dfb06404..1be826f8f96 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -2,6 +2,8 @@ scalebar - Add a scale bar. """ +from typing import Literal + from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list @@ -12,6 +14,7 @@ def scalebar( # noqa: PLR0913 self, position, length, + position_type: Literal["g", "j", "J", "n", "x"] = "g", label_alignment=None, scale_position=None, fancy=None, @@ -36,7 +39,8 @@ def scalebar( # noqa: PLR0913 >>> fig = pygmt.Figure() >>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True) >>> fig.scalebar( - ... "g10/10", + ... position=(10, 10), + ... position_type="g", ... length=1000, ... fancy=True, ... label="Scale", @@ -45,9 +49,9 @@ def scalebar( # noqa: PLR0913 ... ) >>> fig.show() """ - alias = AliasSystem( + kwdict = AliasSystem( L=[ - Alias(position, separator="/"), + Alias(position, separator="/", prefix=position_type), Alias(length, prefix="+w"), Alias(label_alignment, prefix="+a"), Alias(scale_position, prefix="+c", separator="/"), @@ -59,8 +63,8 @@ def scalebar( # noqa: PLR0913 Alias(vertical, prefix="+v"), ], F=Alias(box), - ) + ).kwdict self._preprocess() with Session() as lib: - lib.call_module(module="basemap", args=build_arg_list(alias.kwdict)) + lib.call_module(module="basemap", args=build_arg_list(kwdict)) From e2b448ca4d89526ea4ed459a1648f5282d3eff13 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 1 Apr 2025 12:50:01 +0800 Subject: [PATCH 32/53] Improve Figure.logo --- pygmt/src/logo.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index b2bb6f66aec..b2ad7609594 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -18,7 +18,16 @@ t="transparency", ) @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") -def logo(self, box=None, **kwargs): +def logo( + self, + position=None, + position_type=None, + length=None, + height=None, + offset=None, + box=None, + **kwargs, +): r""" Plot the GMT logo. @@ -55,6 +64,12 @@ def logo(self, box=None, **kwargs): {transparency} """ alias = AliasSystem( + D=[ + Alias(position, separator="/", prefix=position_type), + Alias(length, prefix="+w"), + Alias(height, prefix="+h"), + Alias(offset, prefix="+o", separator="/"), + ], F=Alias(box), ) kwargs = self._preprocess(**kwargs) From 86fb3af9f7ec5474fd0006925dd034c8cda71f47 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 1 Apr 2025 12:56:09 +0800 Subject: [PATCH 33/53] Update Figure.image --- pygmt/src/image.py | 13 ++++++++++++- pygmt/tests/test_image.py | 8 +++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/pygmt/src/image.py b/pygmt/src/image.py index 9cb17e844bc..730bedfce90 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -14,6 +14,11 @@ def image( # noqa: PLR0913 region=None, projection=None, position=None, + position_type=None, + dimension=None, + repeat=None, + offset=None, + dpi=None, box=None, bitcolor=None, monochrome=None, @@ -70,7 +75,13 @@ def image( # noqa: PLR0913 alias = AliasSystem( R=Alias(region, separator="/"), J=Alias(projection), - D=Alias(position), + D=[ + Alias(position, separator="/", prefix=position_type), + Alias(dimension, prefix="+w", separator="/"), + Alias(repeat, prefix="+n", separator="/"), + Alias(offset, prefix="+o", separator="/"), + Alias(dpi, prefix="+r"), + ], F=Alias(box), G=Alias(bitcolor), M=Alias(monochrome), diff --git a/pygmt/tests/test_image.py b/pygmt/tests/test_image.py index e69a8d71938..184a60224c3 100644 --- a/pygmt/tests/test_image.py +++ b/pygmt/tests/test_image.py @@ -13,5 +13,11 @@ def test_image(): Place images on map. """ fig = Figure() - fig.image(imagefile="@circuit.png", position="x0/0+w2c", box=Box(pen="thin,blue")) + fig.image( + imagefile="@circuit.png", + position=(0, 0), + position_type="x", + dimension="2c", + box=Box(pen="thin,blue"), + ) return fig From 86414302e1133c11c205dbaead2f7176f0868b89 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 1 Apr 2025 13:12:05 +0800 Subject: [PATCH 34/53] Update Box --- pygmt/params/box.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pygmt/params/box.py b/pygmt/params/box.py index b84ef5f2475..60c7c183a6a 100644 --- a/pygmt/params/box.py +++ b/pygmt/params/box.py @@ -72,15 +72,13 @@ class Box(BaseParam): shading_offset: Sequence[float | str] | None = None shading_fill: str | None = None - @property - def innerborder(self) -> str | None: + def _innerborder(self) -> str | None: """ innerborder="{inner_gap}/{inner_pen}" """ return [v for v in (self.inner_gap, self.inner_pen) if v is not None] or None - @property - def shading(self) -> str | None: + def _shading(self) -> str | None: """ shading="{shading_offset}/{shading_fill}" """ @@ -93,6 +91,8 @@ def shading(self) -> str | None: @property def _aliases(self): + self.innerborder = self._innerborder() + self.shading = self._shading() return [ Alias(self.clearance, prefix="+c", separator="/"), Alias(self.fill, prefix="+g"), From 121e33d55635ae9e01c7a066d6b51d2ccf91b993 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 1 Apr 2025 13:15:03 +0800 Subject: [PATCH 35/53] Update BaseParam --- pygmt/params/base.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pygmt/params/base.py b/pygmt/params/base.py index dba049b0e27..a613eed8761 100644 --- a/pygmt/params/base.py +++ b/pygmt/params/base.py @@ -46,9 +46,5 @@ def __repr__(self): """ String representation of the object. """ - params = ", ".join( - f"{k}={v!r}" - for k, v in vars(self).items() - if v is not None and v is not False - ) + params = ", ".join(f"{k}={v!r}" for k, v in vars(self).items() if v is not None) return f"{self.__class__.__name__}({params})" From f95d8076ae492b34f6ebeb0b177d6a0e806d913d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 1 Apr 2025 13:29:44 +0800 Subject: [PATCH 36/53] Update AliasSystem --- pygmt/alias.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index 4e3c7683a4b..6f33fd1b2f6 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -114,8 +114,8 @@ class Alias: Attributes ---------- - name - Parameter name. + value + Value of the parameter. prefix String to add at the beginning of the value. separator @@ -123,8 +123,6 @@ class Alias: mapping Map long-form arguments to GMT's short-form arguments. If ``True``, will use the first letter of the long-form arguments. - value - Value of the parameter. Examples -------- @@ -210,8 +208,6 @@ def __init__(self, **kwargs): match aliases: case list(): self.options[option] = aliases - case str(): # Support shorthand like 'J="projection"' - self.options[option] = [Alias(aliases)] case _: self.options[option] = [aliases] From b1e0d71e0d76cb3a820c674116632f68b894a855 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 2 Apr 2025 06:11:33 +0800 Subject: [PATCH 37/53] Update the release checklist post v0.15.0 release (#3887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/4-release_checklist.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/4-release_checklist.md b/.github/ISSUE_TEMPLATE/4-release_checklist.md index 87a164b15c8..0c2a8dc29b2 100644 --- a/.github/ISSUE_TEMPLATE/4-release_checklist.md +++ b/.github/ISSUE_TEMPLATE/4-release_checklist.md @@ -11,6 +11,7 @@ assignees: '' **Scheduled Date**: 20YY/MM/DD **Pull request due date**: 20YY/MM/DD **DOI**: `10.5281/zenodo.XXXXXXX` +**Announcement draft**: https://hackmd.io/@pygmt/xxxxxxxx **Priority PRs/issues to complete prior to release** @@ -39,11 +40,15 @@ assignees: '' - [ ] Edit the draft release notes with the finalized changelog - [ ] Set the tag version and release title to vX.Y.Z - [ ] Make a release by clicking the 'Publish Release' button, this will automatically create a tag too +- [ ] Verify that [all workflows triggered by the release](https://github.com/GenericMappingTools/pygmt/actions?query=event%3Arelease) pass + - [ ] The latest version is correct on [PyPI](https://pypi.org/project/pygmt/) + - [ ] The latest version is correct on https://www.pygmt.org/latest/ + - [ ] The [release page](https://github.com/GenericMappingTools/pygmt/releases) has five assets, including `baseline-images.zip`, `pygmt-docs.zip` and `pygmt-docs.pdf` - [ ] Download pygmt-X.Y.Z.zip (rename to pygmt-vX.Y.Z.zip) and baseline-images.zip from the release page, and upload the two zip files to https://zenodo.org/deposit, ensure that they are filed under the correct reserved DOI **After release**: -- [ ] Update conda-forge [pygmt-feedstock](https://github.com/conda-forge/pygmt-feedstock) [Done automatically by conda-forge's bot. Remember to pin GMT, Python and SPEC0 versions] +- [ ] Update conda-forge [pygmt-feedstock](https://github.com/conda-forge/pygmt-feedstock) (Done automatically by conda-forge's bot. If you don't want to wait, open a new issue in the `conda-forge/pygmt-feedstock` repository with the title `@conda-forge-admin, please update version`. This will trigger the bot immediately. Remember to pin GMT, Python and SPEC0 versions) - [ ] Bump PyGMT version on https://github.com/GenericMappingTools/try-gmt (after conda-forge update) - [ ] Announce the release on: - [ ] GMT [forum](https://forum.generic-mapping-tools.org/c/news/) (do this announcement first! Requires moderator status) From c36df9de8b264f0151f657306bcc6b4f2c3fa403 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 06:23:48 +0800 Subject: [PATCH 38/53] Build(deps): Bump astral-sh/setup-uv from 5.4.0 to 5.4.1 (#3890) Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 5.4.0 to 5.4.1. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v5.4.0...v5.4.1) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 5.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml index 43716266506..e2524889b1c 100644 --- a/.github/workflows/ci_tests.yaml +++ b/.github/workflows/ci_tests.yaml @@ -152,7 +152,7 @@ jobs: GH_TOKEN: ${{ github.token }} - name: Install uv - uses: astral-sh/setup-uv@v5.4.0 + uses: astral-sh/setup-uv@v5.4.1 with: python-version: ${{ matrix.python-version }} From 3f03ff3bef6c33fc2ae8ae3f722ac1a1241d51ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 06:24:18 +0800 Subject: [PATCH 39/53] Build(deps): Bump actions/create-github-app-token from 1.11.6 to 1.12.0 (#3889) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.11.6 to 1.12.0. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/v1.11.6...v1.12.0) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 1.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/format-command.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format-command.yml b/.github/workflows/format-command.yml index f727cb63150..b3a00d66947 100644 --- a/.github/workflows/format-command.yml +++ b/.github/workflows/format-command.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: # Generate token from GenericMappingTools bot - - uses: actions/create-github-app-token@v1.11.6 + - uses: actions/create-github-app-token@v1.12.0 id: generate-token with: app-id: ${{ secrets.APP_ID }} From 3b706723535654b71a11928ab8c8745d1bd11f34 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 3 Apr 2025 22:26:14 +0800 Subject: [PATCH 40/53] Rename value_to_string to to_string --- pygmt/alias.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index 6f33fd1b2f6..d7d4615772a 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -10,7 +10,7 @@ from pygmt.helpers.utils import is_nonstr_iter -def value_to_string( +def to_string( value: Any, prefix: str = "", # Default to an empty string to simplify the code logic. separator: Literal["/", ","] | None = None, @@ -59,29 +59,29 @@ def value_to_string( Examples -------- - >>> value_to_string("text") + >>> to_string("text") 'text' - >>> value_to_string(12) + >>> to_string(12) '12' - >>> value_to_string((12, 34), separator="/") + >>> to_string((12, 34), separator="/") '12/34' - >>> value_to_string(("12p", "34p"), separator=",") + >>> to_string(("12p", "34p"), separator=",") '12p,34p' - >>> value_to_string(("12p", "34p"), prefix="+o", separator="/") + >>> to_string(("12p", "34p"), prefix="+o", separator="/") '+o12p/34p' - >>> value_to_string(True) + >>> to_string(True) '' - >>> value_to_string(True, prefix="+a") + >>> to_string(True, prefix="+a") '+a' - >>> value_to_string(False) - >>> value_to_string(None) - >>> value_to_string(["xaf", "yaf", "WSen"]) + >>> to_string(False) + >>> to_string(None) + >>> to_string(["xaf", "yaf", "WSen"]) ['xaf', 'yaf', 'WSen'] - >>> value_to_string("high", mapping=True) + >>> to_string("high", mapping=True) 'h' - >>> value_to_string("mean", mapping={"mean": "a", "mad": "d", "full": "g"}) + >>> to_string("mean", mapping={"mean": "a", "mad": "d", "full": "g"}) 'a' - >>> value_to_string("invalid", mapping={"mean": "a", "mad": "d", "full": "g"}) + >>> to_string("invalid", mapping={"mean": "a", "mad": "d", "full": "g"}) 'invalid' """ # Return None if the value is None or False. @@ -142,7 +142,7 @@ def __init__( separator: Literal["/", ","] | None = None, mapping: bool | Mapping = False, ): - self.value = value_to_string( + self.value = to_string( value=value, prefix=prefix, separator=separator, mapping=mapping ) From b6707b18d133be55016bf90bdb35d8867c7c3661 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 5 Apr 2025 23:23:55 +0800 Subject: [PATCH 41/53] Refactor the Alias class --- pygmt/alias.py | 36 +++++++++++++++++++++--------------- pygmt/params/base.py | 2 +- pygmt/params/frame.py | 2 +- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index d7d4615772a..d98a3c01c38 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -127,23 +127,29 @@ class Alias: Examples -------- >>> par = Alias((3.0, 3.0), prefix="+o", separator="/") - >>> par.value + >>> par._value '+o3.0/3.0' >>> par = Alias(["xaf", "yaf", "WSen"]) - >>> par.value + >>> par._value ['xaf', 'yaf', 'WSen'] """ - def __init__( - self, - value: Any, - prefix: str = "", - separator: Literal["/", ","] | None = None, - mapping: bool | Mapping = False, - ): - self.value = to_string( - value=value, prefix=prefix, separator=separator, mapping=mapping + value: Any + prefix: str = "" + separator: Literal["/", ","] | None = None + mapping: bool | Mapping = False + + @property + def _value(self) -> str | Sequence[str] | None: + """ + The value of the alias as a string, a sequence of strings or None. + """ + return to_string( + value=self.value, + prefix=self.prefix, + separator=self.separator, + mapping=self.mapping, ) @@ -221,13 +227,13 @@ def kwdict(self): for option, aliases in self.options.items(): for alias in aliases: # value can be a string, a sequence of strings or None. - if alias.value is None: + if alias._value is None: continue # Special handing of repeatable parameter like -B/frame. - if is_nonstr_iter(alias.value): - kwdict[option] = alias.value + if is_nonstr_iter(alias._value): + kwdict[option] = alias._value # A repeatable option should have only one alias, so break. break - kwdict[option] += alias.value + kwdict[option] += alias._value return kwdict diff --git a/pygmt/params/base.py b/pygmt/params/base.py index a613eed8761..0bb8252aefa 100644 --- a/pygmt/params/base.py +++ b/pygmt/params/base.py @@ -39,7 +39,7 @@ def __str__(self): String representation of the object that can be passed to GMT directly. """ return "".join( - [alias.value for alias in self._aliases if alias.value is not None] + [alias._value for alias in self._aliases if alias._value is not None] ) def __repr__(self): diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py index 6613959ca12..1459de41871 100644 --- a/pygmt/params/frame.py +++ b/pygmt/params/frame.py @@ -93,4 +93,4 @@ def __iter__(self): ------ The value of each alias in the class. None are excluded. """ - yield from (alias.value for alias in self._aliases if alias.value is not None) + yield from (alias._value for alias in self._aliases if alias._value is not None) From 3aaf44a4376e3c89f2029c226bb264b75082ace8 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 7 Apr 2025 18:49:14 +0800 Subject: [PATCH 42/53] Improve Figure.timestamp --- pygmt/src/timestamp.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pygmt/src/timestamp.py b/pygmt/src/timestamp.py index cf9eeb3ea31..14d291a976e 100644 --- a/pygmt/src/timestamp.py +++ b/pygmt/src/timestamp.py @@ -14,7 +14,6 @@ __doctest_skip__ = ["timestamp"] -# ruff: noqa: ARG001 def timestamp( self, text: str | None = None, @@ -79,6 +78,14 @@ def timestamp( """ self._preprocess() + if text is not None and len(text) > 64: + msg = ( + "Argument of 'text' must be no longer than 64 characters. " + "The given text string will be truncated to 64 characters." + ) + warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) + text = text[:64] + # TODO(GMT>=6.5.0): Remove the patch for upstream bug fixed in GMT 6.5.0. if Version(__gmt_version__) < Version("6.5.0"): # Giving a single offset doesn't work. @@ -89,16 +96,7 @@ def timestamp( # See https://github.com/GenericMappingTools/gmt/pull/7127. if text is not None: # Overriding the 'timefmt' parameter and set 'text' to None - timefmt = text[:64] - text = None - - if text is not None and len(text) > 64: - msg = ( - "Argument of 'text' must be no longer than 64 characters. " - "The given text string will be truncated to 64 characters." - ) - warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) - text = text[:64] + timefmt, text = text, None kwdict = AliasSystem( U=[ From 00d17f4f762657247c44eb821af8112b8b552d03 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 7 Apr 2025 19:01:57 +0800 Subject: [PATCH 43/53] Improve the code structure --- pygmt/src/basemap.py | 13 +++++++---- pygmt/src/binstats.py | 51 ++++++++++++++++++++++-------------------- pygmt/src/coast.py | 13 +++++++---- pygmt/src/dimfilter.py | 25 ++++++++++++--------- pygmt/src/image.py | 44 +++++++++++++++++++----------------- pygmt/src/logo.py | 25 ++++++++++++--------- pygmt/src/scalebar.py | 3 ++- 7 files changed, 99 insertions(+), 75 deletions(-) diff --git a/pygmt/src/basemap.py b/pygmt/src/basemap.py index 4beb0bb323f..7876d4455a4 100644 --- a/pygmt/src/basemap.py +++ b/pygmt/src/basemap.py @@ -82,9 +82,14 @@ def basemap(self, frame=None, **kwargs): {perspective} {transparency} """ - alias = AliasSystem( - B=Alias(frame), - ) kwargs = self._preprocess(**kwargs) + + kwdict = ( + AliasSystem( + B=Alias(frame), + ).kwdict + | kwargs + ) + with Session() as lib: - lib.call_module(module="basemap", args=build_arg_list(alias.kwdict | kwargs)) + lib.call_module(module="basemap", args=build_arg_list(kwdict)) diff --git a/pygmt/src/binstats.py b/pygmt/src/binstats.py index 61b0633e684..6c11eec125d 100644 --- a/pygmt/src/binstats.py +++ b/pygmt/src/binstats.py @@ -109,34 +109,37 @@ def binstats( - ``None`` if ``outgrid`` is set (grid output will be stored in the file set by ``outgrid``) """ - alias = AliasSystem( - C=Alias( - statistic, - mapping={ - "mean": "a", - "mad": "d", - "full": "g", - "interquartile": "i", - "min": "l", - "minpos": "L", - "median": "m", - "number": "n", - "lms": "o", - "mode": "p", - "quantile": "q", - "rms": "r", - "stddev": "s", - "max": "u", - "maxneg": "U", - "sum": "z", - }, - ), - G=Alias(outgrid), + kwdict = ( + AliasSystem( + C=Alias( + statistic, + mapping={ + "mean": "a", + "mad": "d", + "full": "g", + "interquartile": "i", + "min": "l", + "minpos": "L", + "median": "m", + "number": "n", + "lms": "o", + "mode": "p", + "quantile": "q", + "rms": "r", + "stddev": "s", + "max": "u", + "maxneg": "U", + "sum": "z", + }, + ), + G=Alias(outgrid), + ).kwdict + | kwargs ) + if statistic == "quantile": statistic += str(quantile_value) - kwdict = alias.kwdict | kwargs with Session() as lib: with ( lib.virtualfile_in(check_kind="vector", data=data) as vintbl, diff --git a/pygmt/src/coast.py b/pygmt/src/coast.py index f812b1bca35..71a3e418a3f 100644 --- a/pygmt/src/coast.py +++ b/pygmt/src/coast.py @@ -210,9 +210,6 @@ def coast( >>> # Show the plot >>> fig.show() """ - alias = AliasSystem( - D=Alias(resolution, mapping=True), - ) kwargs = self._preprocess(**kwargs) if not args_in_kwargs(args=["C", "G", "S", "I", "N", "E", "Q", "W"], kwargs=kwargs): msg = ( @@ -220,5 +217,13 @@ def coast( "lakes, land, water, rivers, borders, dcw, Q, or shorelines." ) raise GMTInvalidInput(msg) + + kwdict = ( + AliasSystem( + D=Alias(resolution, mapping=True), + ).kwdict + | kwargs + ) + with Session() as lib: - lib.call_module(module="coast", args=build_arg_list(alias.kwdict | kwargs)) + lib.call_module(module="coast", args=build_arg_list(kwdict)) diff --git a/pygmt/src/dimfilter.py b/pygmt/src/dimfilter.py index c31abdca469..9512b00ac0e 100644 --- a/pygmt/src/dimfilter.py +++ b/pygmt/src/dimfilter.py @@ -134,16 +134,6 @@ def dimfilter( ... region=[-55, -51, -24, -19], ... ) """ - alias = AliasSystem( - D=Alias(distance), - G=Alias(outgrid), - F=Alias(filter), - I=Alias(spacing, separator="/"), - N=Alias(sectors), - R=Alias(region, separator="/"), - V=Alias(verbose), - ) - if ( not all(v is not None for v in [distance, filter, sectors]) and "Q" not in kwargs @@ -153,7 +143,20 @@ def dimfilter( "distance, filters, or sectors." ) raise GMTInvalidInput(msg) - kwdict = alias.kwdict | kwargs + + kwdict = ( + AliasSystem( + D=Alias(distance), + G=Alias(outgrid), + F=Alias(filter), + I=Alias(spacing, separator="/"), + N=Alias(sectors), + R=Alias(region, separator="/"), + V=Alias(verbose), + ).kwdict + | kwargs + ) + with Session() as lib: with ( lib.virtualfile_in(check_kind="raster", data=grid) as vingrd, diff --git a/pygmt/src/image.py b/pygmt/src/image.py index 730bedfce90..244be3a8b70 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -72,27 +72,29 @@ def image( # noqa: PLR0913 {perspective} {transparency} """ - alias = AliasSystem( - R=Alias(region, separator="/"), - J=Alias(projection), - D=[ - Alias(position, separator="/", prefix=position_type), - Alias(dimension, prefix="+w", separator="/"), - Alias(repeat, prefix="+n", separator="/"), - Alias(offset, prefix="+o", separator="/"), - Alias(dpi, prefix="+r"), - ], - F=Alias(box), - G=Alias(bitcolor), - M=Alias(monochrome), - V=Alias(verbose), - c=Alias(panel, separator=","), - p=Alias(perspective, separator="/"), - t=Alias(transparency), + kwargs = self._preprocess(**kwargs) + + kwdict = ( + AliasSystem( + R=Alias(region, separator="/"), + J=Alias(projection), + D=[ + Alias(position, separator="/", prefix=position_type), + Alias(dimension, prefix="+w", separator="/"), + Alias(repeat, prefix="+n", separator="/"), + Alias(offset, prefix="+o", separator="/"), + Alias(dpi, prefix="+r"), + ], + F=Alias(box), + G=Alias(bitcolor), + M=Alias(monochrome), + V=Alias(verbose), + c=Alias(panel, separator=","), + p=Alias(perspective, separator="/"), + t=Alias(transparency), + ).kwdict + | kwargs ) - kwargs = self._preprocess(**kwargs) with Session() as lib: - lib.call_module( - module="image", args=build_arg_list(alias.kwdict | kwargs, infile=imagefile) - ) + lib.call_module(module="image", args=build_arg_list(kwdict, infile=imagefile)) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index b2ad7609594..5bd20265f49 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -63,15 +63,20 @@ def logo( {panel} {transparency} """ - alias = AliasSystem( - D=[ - Alias(position, separator="/", prefix=position_type), - Alias(length, prefix="+w"), - Alias(height, prefix="+h"), - Alias(offset, prefix="+o", separator="/"), - ], - F=Alias(box), - ) kwargs = self._preprocess(**kwargs) + + kwdict = ( + AliasSystem( + D=[ + Alias(position, separator="/", prefix=position_type), + Alias(length, prefix="+w"), + Alias(height, prefix="+h"), + Alias(offset, prefix="+o", separator="/"), + ], + F=Alias(box), + ).kwdict + | kwargs + ) + with Session() as lib: - lib.call_module(module="logo", args=build_arg_list(alias.kwdict | kwargs)) + lib.call_module(module="logo", args=build_arg_list(kwdict)) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 1be826f8f96..3c763fd84fb 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -49,6 +49,8 @@ def scalebar( # noqa: PLR0913 ... ) >>> fig.show() """ + self._preprocess() + kwdict = AliasSystem( L=[ Alias(position, separator="/", prefix=position_type), @@ -65,6 +67,5 @@ def scalebar( # noqa: PLR0913 F=Alias(box), ).kwdict - self._preprocess() with Session() as lib: lib.call_module(module="basemap", args=build_arg_list(kwdict)) From a75f22c7181ea482d31d5cd754948225bcda1e27 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 7 Apr 2025 19:04:18 +0800 Subject: [PATCH 44/53] Improve docstring of frame --- pygmt/params/frame.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py index 1459de41871..79d54a5ccc2 100644 --- a/pygmt/params/frame.py +++ b/pygmt/params/frame.py @@ -12,6 +12,8 @@ @dataclass(repr=False) class Axes(BaseParam): """ + Class for setting up the axes, title, and fill of a plot. + Examples -------- >>> from pygmt.params import Axes @@ -35,6 +37,10 @@ def _aliases(self): @dataclass(repr=False) class Axis(BaseParam): """ + Class for setting up one axis of a plot. + + Examples + -------- >>> from pygmt.params import Axis >>> str(Axis(10, angle=30, label="X axis", unit="km")) '10+a30+lX axis+ukm' @@ -58,6 +64,8 @@ def _aliases(self): @dataclass(repr=False) class Frame(BaseParam): """ + Class for setting up the frame of a plot. + >>> from pygmt.alias import AliasSystem, Alias >>> from pygmt.params import Frame, Axes, Axis >>> frame = Frame( From fb916cff28a7ef9678f6bc5647b4cbeea4ce4411 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 7 Apr 2025 19:11:38 +0800 Subject: [PATCH 45/53] Improve the docstring of logo box --- pygmt/src/logo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 5bd20265f49..4e14a402b13 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -5,6 +5,7 @@ from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias +from pygmt.params import Box @fmt_docstring @@ -25,7 +26,7 @@ def logo( length=None, height=None, offset=None, - box=None, + box: Box | str | None = None, **kwargs, ): r""" @@ -48,7 +49,7 @@ def logo( [**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\ **+w**\ *width*\ [**+j**\ *justify*]\ [**+o**\ *dx*\ [/*dy*]]. Set reference point on the map for the image. - box : bool or str + box If set to ``True``, draw a rectangular border around the GMT logo. style : str From 1ca7382877f1c2fbda4eaba5477558fdc74046e6 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 7 Apr 2025 19:16:25 +0800 Subject: [PATCH 46/53] Revert "Improve the code structure" This reverts commit 00d17f4f762657247c44eb821af8112b8b552d03. --- pygmt/src/basemap.py | 13 ++++------- pygmt/src/binstats.py | 51 ++++++++++++++++++++---------------------- pygmt/src/coast.py | 13 ++++------- pygmt/src/dimfilter.py | 25 +++++++++------------ pygmt/src/image.py | 44 +++++++++++++++++------------------- pygmt/src/logo.py | 25 +++++++++------------ pygmt/src/scalebar.py | 3 +-- 7 files changed, 75 insertions(+), 99 deletions(-) diff --git a/pygmt/src/basemap.py b/pygmt/src/basemap.py index 7876d4455a4..4beb0bb323f 100644 --- a/pygmt/src/basemap.py +++ b/pygmt/src/basemap.py @@ -82,14 +82,9 @@ def basemap(self, frame=None, **kwargs): {perspective} {transparency} """ - kwargs = self._preprocess(**kwargs) - - kwdict = ( - AliasSystem( - B=Alias(frame), - ).kwdict - | kwargs + alias = AliasSystem( + B=Alias(frame), ) - + kwargs = self._preprocess(**kwargs) with Session() as lib: - lib.call_module(module="basemap", args=build_arg_list(kwdict)) + lib.call_module(module="basemap", args=build_arg_list(alias.kwdict | kwargs)) diff --git a/pygmt/src/binstats.py b/pygmt/src/binstats.py index 6c11eec125d..61b0633e684 100644 --- a/pygmt/src/binstats.py +++ b/pygmt/src/binstats.py @@ -109,37 +109,34 @@ def binstats( - ``None`` if ``outgrid`` is set (grid output will be stored in the file set by ``outgrid``) """ - kwdict = ( - AliasSystem( - C=Alias( - statistic, - mapping={ - "mean": "a", - "mad": "d", - "full": "g", - "interquartile": "i", - "min": "l", - "minpos": "L", - "median": "m", - "number": "n", - "lms": "o", - "mode": "p", - "quantile": "q", - "rms": "r", - "stddev": "s", - "max": "u", - "maxneg": "U", - "sum": "z", - }, - ), - G=Alias(outgrid), - ).kwdict - | kwargs + alias = AliasSystem( + C=Alias( + statistic, + mapping={ + "mean": "a", + "mad": "d", + "full": "g", + "interquartile": "i", + "min": "l", + "minpos": "L", + "median": "m", + "number": "n", + "lms": "o", + "mode": "p", + "quantile": "q", + "rms": "r", + "stddev": "s", + "max": "u", + "maxneg": "U", + "sum": "z", + }, + ), + G=Alias(outgrid), ) - if statistic == "quantile": statistic += str(quantile_value) + kwdict = alias.kwdict | kwargs with Session() as lib: with ( lib.virtualfile_in(check_kind="vector", data=data) as vintbl, diff --git a/pygmt/src/coast.py b/pygmt/src/coast.py index 71a3e418a3f..f812b1bca35 100644 --- a/pygmt/src/coast.py +++ b/pygmt/src/coast.py @@ -210,6 +210,9 @@ def coast( >>> # Show the plot >>> fig.show() """ + alias = AliasSystem( + D=Alias(resolution, mapping=True), + ) kwargs = self._preprocess(**kwargs) if not args_in_kwargs(args=["C", "G", "S", "I", "N", "E", "Q", "W"], kwargs=kwargs): msg = ( @@ -217,13 +220,5 @@ def coast( "lakes, land, water, rivers, borders, dcw, Q, or shorelines." ) raise GMTInvalidInput(msg) - - kwdict = ( - AliasSystem( - D=Alias(resolution, mapping=True), - ).kwdict - | kwargs - ) - with Session() as lib: - lib.call_module(module="coast", args=build_arg_list(kwdict)) + lib.call_module(module="coast", args=build_arg_list(alias.kwdict | kwargs)) diff --git a/pygmt/src/dimfilter.py b/pygmt/src/dimfilter.py index 9512b00ac0e..c31abdca469 100644 --- a/pygmt/src/dimfilter.py +++ b/pygmt/src/dimfilter.py @@ -134,6 +134,16 @@ def dimfilter( ... region=[-55, -51, -24, -19], ... ) """ + alias = AliasSystem( + D=Alias(distance), + G=Alias(outgrid), + F=Alias(filter), + I=Alias(spacing, separator="/"), + N=Alias(sectors), + R=Alias(region, separator="/"), + V=Alias(verbose), + ) + if ( not all(v is not None for v in [distance, filter, sectors]) and "Q" not in kwargs @@ -143,20 +153,7 @@ def dimfilter( "distance, filters, or sectors." ) raise GMTInvalidInput(msg) - - kwdict = ( - AliasSystem( - D=Alias(distance), - G=Alias(outgrid), - F=Alias(filter), - I=Alias(spacing, separator="/"), - N=Alias(sectors), - R=Alias(region, separator="/"), - V=Alias(verbose), - ).kwdict - | kwargs - ) - + kwdict = alias.kwdict | kwargs with Session() as lib: with ( lib.virtualfile_in(check_kind="raster", data=grid) as vingrd, diff --git a/pygmt/src/image.py b/pygmt/src/image.py index 244be3a8b70..730bedfce90 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -72,29 +72,27 @@ def image( # noqa: PLR0913 {perspective} {transparency} """ - kwargs = self._preprocess(**kwargs) - - kwdict = ( - AliasSystem( - R=Alias(region, separator="/"), - J=Alias(projection), - D=[ - Alias(position, separator="/", prefix=position_type), - Alias(dimension, prefix="+w", separator="/"), - Alias(repeat, prefix="+n", separator="/"), - Alias(offset, prefix="+o", separator="/"), - Alias(dpi, prefix="+r"), - ], - F=Alias(box), - G=Alias(bitcolor), - M=Alias(monochrome), - V=Alias(verbose), - c=Alias(panel, separator=","), - p=Alias(perspective, separator="/"), - t=Alias(transparency), - ).kwdict - | kwargs + alias = AliasSystem( + R=Alias(region, separator="/"), + J=Alias(projection), + D=[ + Alias(position, separator="/", prefix=position_type), + Alias(dimension, prefix="+w", separator="/"), + Alias(repeat, prefix="+n", separator="/"), + Alias(offset, prefix="+o", separator="/"), + Alias(dpi, prefix="+r"), + ], + F=Alias(box), + G=Alias(bitcolor), + M=Alias(monochrome), + V=Alias(verbose), + c=Alias(panel, separator=","), + p=Alias(perspective, separator="/"), + t=Alias(transparency), ) + kwargs = self._preprocess(**kwargs) with Session() as lib: - lib.call_module(module="image", args=build_arg_list(kwdict, infile=imagefile)) + lib.call_module( + module="image", args=build_arg_list(alias.kwdict | kwargs, infile=imagefile) + ) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 4e14a402b13..acf09a963ef 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -64,20 +64,15 @@ def logo( {panel} {transparency} """ - kwargs = self._preprocess(**kwargs) - - kwdict = ( - AliasSystem( - D=[ - Alias(position, separator="/", prefix=position_type), - Alias(length, prefix="+w"), - Alias(height, prefix="+h"), - Alias(offset, prefix="+o", separator="/"), - ], - F=Alias(box), - ).kwdict - | kwargs + alias = AliasSystem( + D=[ + Alias(position, separator="/", prefix=position_type), + Alias(length, prefix="+w"), + Alias(height, prefix="+h"), + Alias(offset, prefix="+o", separator="/"), + ], + F=Alias(box), ) - + kwargs = self._preprocess(**kwargs) with Session() as lib: - lib.call_module(module="logo", args=build_arg_list(kwdict)) + lib.call_module(module="logo", args=build_arg_list(alias.kwdict | kwargs)) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 3c763fd84fb..1be826f8f96 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -49,8 +49,6 @@ def scalebar( # noqa: PLR0913 ... ) >>> fig.show() """ - self._preprocess() - kwdict = AliasSystem( L=[ Alias(position, separator="/", prefix=position_type), @@ -67,5 +65,6 @@ def scalebar( # noqa: PLR0913 F=Alias(box), ).kwdict + self._preprocess() with Session() as lib: lib.call_module(module="basemap", args=build_arg_list(kwdict)) From 2e7339d7a5bce613c17eeea3af07071532f7c25b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 7 Apr 2025 19:21:51 +0800 Subject: [PATCH 47/53] Improve code structure --- pygmt/src/basemap.py | 7 +++++-- pygmt/src/binstats.py | 2 +- pygmt/src/coast.py | 11 +++++++---- pygmt/src/dimfilter.py | 21 +++++++++++---------- pygmt/src/image.py | 8 ++++---- pygmt/src/logo.py | 7 +++++-- pygmt/src/scalebar.py | 3 ++- 7 files changed, 35 insertions(+), 24 deletions(-) diff --git a/pygmt/src/basemap.py b/pygmt/src/basemap.py index 4beb0bb323f..173905d8e17 100644 --- a/pygmt/src/basemap.py +++ b/pygmt/src/basemap.py @@ -82,9 +82,12 @@ def basemap(self, frame=None, **kwargs): {perspective} {transparency} """ + kwargs = self._preprocess(**kwargs) + alias = AliasSystem( B=Alias(frame), ) - kwargs = self._preprocess(**kwargs) + kwdict = alias.kwdict | kwargs + with Session() as lib: - lib.call_module(module="basemap", args=build_arg_list(alias.kwdict | kwargs)) + lib.call_module(module="basemap", args=build_arg_list(kwdict)) diff --git a/pygmt/src/binstats.py b/pygmt/src/binstats.py index 61b0633e684..bd89802e968 100644 --- a/pygmt/src/binstats.py +++ b/pygmt/src/binstats.py @@ -135,8 +135,8 @@ def binstats( ) if statistic == "quantile": statistic += str(quantile_value) - kwdict = alias.kwdict | kwargs + with Session() as lib: with ( lib.virtualfile_in(check_kind="vector", data=data) as vintbl, diff --git a/pygmt/src/coast.py b/pygmt/src/coast.py index f812b1bca35..b47bac674ce 100644 --- a/pygmt/src/coast.py +++ b/pygmt/src/coast.py @@ -210,9 +210,6 @@ def coast( >>> # Show the plot >>> fig.show() """ - alias = AliasSystem( - D=Alias(resolution, mapping=True), - ) kwargs = self._preprocess(**kwargs) if not args_in_kwargs(args=["C", "G", "S", "I", "N", "E", "Q", "W"], kwargs=kwargs): msg = ( @@ -220,5 +217,11 @@ def coast( "lakes, land, water, rivers, borders, dcw, Q, or shorelines." ) raise GMTInvalidInput(msg) + + alias = AliasSystem( + D=Alias(resolution, mapping=True), + ) + kwdict = alias.kwdict | kwargs + with Session() as lib: - lib.call_module(module="coast", args=build_arg_list(alias.kwdict | kwargs)) + lib.call_module(module="coast", args=build_arg_list(kwdict)) diff --git a/pygmt/src/dimfilter.py b/pygmt/src/dimfilter.py index c31abdca469..d57876b5e5e 100644 --- a/pygmt/src/dimfilter.py +++ b/pygmt/src/dimfilter.py @@ -134,16 +134,6 @@ def dimfilter( ... region=[-55, -51, -24, -19], ... ) """ - alias = AliasSystem( - D=Alias(distance), - G=Alias(outgrid), - F=Alias(filter), - I=Alias(spacing, separator="/"), - N=Alias(sectors), - R=Alias(region, separator="/"), - V=Alias(verbose), - ) - if ( not all(v is not None for v in [distance, filter, sectors]) and "Q" not in kwargs @@ -153,7 +143,18 @@ def dimfilter( "distance, filters, or sectors." ) raise GMTInvalidInput(msg) + + alias = AliasSystem( + D=Alias(distance), + G=Alias(outgrid), + F=Alias(filter), + I=Alias(spacing, separator="/"), + N=Alias(sectors), + R=Alias(region, separator="/"), + V=Alias(verbose), + ) kwdict = alias.kwdict | kwargs + with Session() as lib: with ( lib.virtualfile_in(check_kind="raster", data=grid) as vingrd, diff --git a/pygmt/src/image.py b/pygmt/src/image.py index 730bedfce90..a9df532588d 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -72,6 +72,8 @@ def image( # noqa: PLR0913 {perspective} {transparency} """ + kwargs = self._preprocess(**kwargs) + alias = AliasSystem( R=Alias(region, separator="/"), J=Alias(projection), @@ -90,9 +92,7 @@ def image( # noqa: PLR0913 p=Alias(perspective, separator="/"), t=Alias(transparency), ) + kwdict = alias.kwdict | kwargs - kwargs = self._preprocess(**kwargs) with Session() as lib: - lib.call_module( - module="image", args=build_arg_list(alias.kwdict | kwargs, infile=imagefile) - ) + lib.call_module(module="image", args=build_arg_list(kwdict, infile=imagefile)) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index acf09a963ef..8f0bba848f3 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -64,6 +64,8 @@ def logo( {panel} {transparency} """ + kwargs = self._preprocess(**kwargs) + alias = AliasSystem( D=[ Alias(position, separator="/", prefix=position_type), @@ -73,6 +75,7 @@ def logo( ], F=Alias(box), ) - kwargs = self._preprocess(**kwargs) + kwdict = alias.kwdict | kwargs + with Session() as lib: - lib.call_module(module="logo", args=build_arg_list(alias.kwdict | kwargs)) + lib.call_module(module="logo", args=build_arg_list(kwdict)) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 1be826f8f96..3c763fd84fb 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -49,6 +49,8 @@ def scalebar( # noqa: PLR0913 ... ) >>> fig.show() """ + self._preprocess() + kwdict = AliasSystem( L=[ Alias(position, separator="/", prefix=position_type), @@ -65,6 +67,5 @@ def scalebar( # noqa: PLR0913 F=Alias(box), ).kwdict - self._preprocess() with Session() as lib: lib.call_module(module="basemap", args=build_arg_list(kwdict)) From dea66ead5d7a2d6001e8e16e368b5108e18877c0 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 8 Apr 2025 00:33:19 +0800 Subject: [PATCH 48/53] Refactor the to_string function to make it more readable --- pygmt/alias.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index d98a3c01c38..9af3119113f 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -84,27 +84,25 @@ def to_string( >>> to_string("invalid", mapping={"mean": "a", "mad": "d", "full": "g"}) 'invalid' """ - # Return None if the value is None or False. - if value is None or value is False: + if value is None or value is False: # None and False are converted to None. return None - # Return an empty string if the value is True. We don't have to check 'prefix' since - # it defaults to an empty string! - if value is True: + if value is True: # True is converted to an empty string with the optional prefix. return f"{prefix}" - # Convert any value to a string or a sequence of strings. - if is_nonstr_iter(value): # Is a sequence. - value = [str(item) for item in value] # Convert to a sequence of strings - if separator is None: - # A sequence is given but separator is not specified. Return a sequence of - # strings, to support repeated GMT options like '-B'. 'prefix' makes no - # sense and is ignored. - return value - value = separator.join(value) # Join the sequence by the separator. - elif mapping: # Mapping long-form arguments to short-form arguments. - value = value[0] if mapping is True else mapping.get(value, value) - # Return the final string with the optional prefix. - return f"{prefix}{value}" + # Convert a non-sequence value to a string. + if not is_nonstr_iter(value): + if mapping: # Mapping long-form arguments to short-form arguments. + value = value[0] if mapping is True else mapping.get(value, value) + return f"{prefix}{value}" + + # Convert a sequence of values to a sequence of strings. + # In some cases, "prefix" and "mapping" are ignored. We can enable them when needed. + _values = [str(item) for item in value] + if separator is None: + # Sequence is given but separator is not specified. Return a sequence of strings + # for repeatable GMT options like '-B'. + return _values + return f"{prefix}{separator.join(_values)}" @dataclasses.dataclass From c893755798cb4647c932b339c779c56fdff2fba5 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 8 Apr 2025 11:24:36 +0800 Subject: [PATCH 49/53] Improve comment in to_string --- pygmt/alias.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index 9af3119113f..be19b0dd956 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -19,12 +19,14 @@ def to_string( """ Convert any value to a string, a sequence of strings or None. - - ``None`` or ``False`` will be converted to ``None``. + The general rules are: + + - ``None``/``False`` will be converted to ``None``. - ``True`` will be converted to an empty string. - A sequence will be joined by the separator if a separator is provided. Otherwise, each item in the sequence will be converted to a string and a sequence of strings will be returned. - - Any other value will be converted to a string if possible. + - Any other type of values will be converted to a string if possible. If a mapping dictionary is provided, the value will be converted to the short-form string that GMT accepts (e.g., mapping PyGMT long-form argument ``"high"`` to GMT's @@ -35,10 +37,10 @@ def to_string( An optional prefix (e.g., `"+o"`) can be added to the beginning of the converted string. - Need to note that this function doesn't check if the given parameters are valid, to - avoid the overhead of checking. For example, if ``value`` is a sequence but - ``separator`` is not specified, a sequence of strings will be returned. ``prefix`` - makes no sense here, but this function won't check it. + To avoid extra overhead, this function does not validate parameter combinations. For + example, if ``value`` is a sequence but ``separator`` is not specified, the function + will return a sequence of strings. In this case, ``prefix`` has no effect, but the + function does not check for such inconsistencies. Parameters ---------- From 7ae8500cc4214ec01d20753da6e5ca9b9ed37485 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 8 Apr 2025 13:09:17 +0800 Subject: [PATCH 50/53] Simplify the AliasSystem class --- pygmt/alias.py | 65 ++++++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index be19b0dd956..0ecc05ad664 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -155,19 +155,15 @@ def _value(self) -> str | Sequence[str] | None: class AliasSystem: """ - Alias system to convert PyGMT parameter into a keyword dictionary for GMT options. + Alias system for converting PyGMT parameters to GMT options. - The AliasSystem class is initialized by keyword arguments where the key is the GMT - single-letter option flag and the value is one or a list of ``Alias`` objects. + The AliasSystem class is initialized with keyword arguments, where each key is a GMT + option flag, and the corresponding value is an ``Alias`` object or a list of + ``Alias`` objects. - The ``kwdict`` property is a keyword dictionary that stores the current parameter - values. The key of the dictionary is the GMT single-letter option flag, and the - value is the corresponding value of the option. The value can be a string or a - sequence of strings, or None. The keyword dictionary can be passed to the - ``build_arg_list`` function. - - Need to note that the ``kwdict`` property is dynamically computed from the current - values of parameters. So, don't change it and avoid accessing it multiple times. + The class provides the ``kwdict`` attribute, which is a dictionary mapping each GMT + option flag to its current value. The value can be a string or a list of strings. + This keyword dictionary can then be passed to the ``build_arg_list`` function. Examples -------- @@ -207,33 +203,24 @@ class AliasSystem: def __init__(self, **kwargs): """ - Initialize as a dictionary of GMT options and their aliases. + Initialize the alias system and create the keyword dictionary that stores the + current parameter values. """ - self.options = {} - for option, aliases in kwargs.items(): - match aliases: - case list(): - self.options[option] = aliases - case _: - self.options[option] = [aliases] + # Keyword dictionary with an empty string as default value. + self.kwdict = defaultdict(str) - @property - def kwdict(self): - """ - A keyword dictionary that stores the current parameter values. - """ - # Default value is an empty string to simplify code logic. - kwdict = defaultdict(str) - for option, aliases in self.options.items(): - for alias in aliases: - # value can be a string, a sequence of strings or None. - if alias._value is None: - continue - # Special handing of repeatable parameter like -B/frame. - if is_nonstr_iter(alias._value): - kwdict[option] = alias._value - # A repeatable option should have only one alias, so break. - break - - kwdict[option] += alias._value - return kwdict + for option, aliases in kwargs.items(): + if not is_nonstr_iter(aliases): # Single alias. + self.kwdict[option] = aliases._value + continue + + for alias in aliases: # List of aliases. + match alias._value: + case None: + continue + case str(): + self.kwdict[option] += alias._value + case list(): + # A repeatable option should have only one alias, so break. + self.kwdict[option] = alias._value + break From 95e6c63de94dd2238494c4b9f08d10ce2383c8e2 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 8 Apr 2025 13:14:12 +0800 Subject: [PATCH 51/53] Simplify to_string --- pygmt/alias.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index 0ecc05ad664..20aa99aeea1 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -15,7 +15,7 @@ def to_string( prefix: str = "", # Default to an empty string to simplify the code logic. separator: Literal["/", ","] | None = None, mapping: bool | Mapping = False, -) -> str | Sequence[str] | None: +) -> str | list[str] | None: """ Convert any value to a string, a sequence of strings or None. @@ -100,11 +100,9 @@ def to_string( # Convert a sequence of values to a sequence of strings. # In some cases, "prefix" and "mapping" are ignored. We can enable them when needed. _values = [str(item) for item in value] - if separator is None: - # Sequence is given but separator is not specified. Return a sequence of strings - # for repeatable GMT options like '-B'. - return _values - return f"{prefix}{separator.join(_values)}" + # When separator is not specified, return a sequence of strings for repeatable GMT + # options like '-B'. Otherwise, join the sequence of strings with the separator. + return _values if separator is None else f"{prefix}{separator.join(_values)}" @dataclasses.dataclass From 38e2c7315a26240a7ad767edc025be8d6edd3ad2 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 8 Apr 2025 13:15:28 +0800 Subject: [PATCH 52/53] Add more doctests to Alias --- pygmt/alias.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pygmt/alias.py b/pygmt/alias.py index 20aa99aeea1..aa8e6536125 100644 --- a/pygmt/alias.py +++ b/pygmt/alias.py @@ -4,7 +4,7 @@ import dataclasses from collections import defaultdict -from collections.abc import Mapping, Sequence +from collections.abc import Mapping from typing import Any, Literal from pygmt.helpers.utils import is_nonstr_iter @@ -131,6 +131,18 @@ class Alias: >>> par = Alias(["xaf", "yaf", "WSen"]) >>> par._value ['xaf', 'yaf', 'WSen'] + + >>> par = Alias("high", mapping=True) + >>> par._value + 'h' + + >>> par = Alias("mean", mapping={"mean": "a", "mad": "d", "full": "g"}) + >>> par._value + 'a' + + >>> par = Alias("invalid", mapping={"mean": "a", "mad": "d", "full": "g"}) + >>> par._value + 'invalid' """ value: Any @@ -139,7 +151,7 @@ class Alias: mapping: bool | Mapping = False @property - def _value(self) -> str | Sequence[str] | None: + def _value(self) -> str | list[str] | None: """ The value of the alias as a string, a sequence of strings or None. """ From b2c12c88b3d984d6e752e0bda5e542e94f1c01c1 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 8 Apr 2025 13:25:26 +0800 Subject: [PATCH 53/53] Fix static typing --- pygmt/params/box.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/params/box.py b/pygmt/params/box.py index 60c7c183a6a..554f3cd0d14 100644 --- a/pygmt/params/box.py +++ b/pygmt/params/box.py @@ -72,13 +72,13 @@ class Box(BaseParam): shading_offset: Sequence[float | str] | None = None shading_fill: str | None = None - def _innerborder(self) -> str | None: + def _innerborder(self) -> list[str | float] | None: """ innerborder="{inner_gap}/{inner_pen}" """ return [v for v in (self.inner_gap, self.inner_pen) if v is not None] or None - def _shading(self) -> str | None: + def _shading(self) -> list[str | float] | None: """ shading="{shading_offset}/{shading_fill}" """