From b73a6e261b28959ba4318de77c0e2e6c7c2c39b4 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 14 Jul 2025 12:03:17 +0800 Subject: [PATCH 1/9] Add the Box class for specifying box properties of embellishments --- doc/api/index.rst | 10 ++++ pygmt/params/__init__.py | 5 ++ pygmt/params/base.py | 63 ++++++++++++++++++++ pygmt/params/box.py | 122 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 pygmt/params/__init__.py create mode 100644 pygmt/params/base.py create mode 100644 pygmt/params/box.py diff --git a/doc/api/index.rst b/doc/api/index.rst index 7828a225652..61979f0756f 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -194,6 +194,16 @@ Getting metadata from tabular or grid data: info grdinfo +Common Parameters +----------------- + +.. currentmodule:: pygmt.params + +.. autosummary:: + :toctree: generated + + Box + Xarray Integration ------------------ diff --git a/pygmt/params/__init__.py b/pygmt/params/__init__.py new file mode 100644 index 00000000000..f2904afba94 --- /dev/null +++ b/pygmt/params/__init__.py @@ -0,0 +1,5 @@ +""" +Classes for common parameters in PyGMT. +""" + +from pygmt.params.box import Box diff --git a/pygmt/params/base.py b/pygmt/params/base.py new file mode 100644 index 00000000000..af1b17a10ed --- /dev/null +++ b/pygmt/params/base.py @@ -0,0 +1,63 @@ +""" +Base class for common parameters shared in PyGMT. +""" + + +class BaseParam: + """ + Base class for parameters in PyGMT. + + To define a new parameter class, inherit from this class and define the attributes + that correspond to the parameters you want to include. The class should also + implement the ``_aliases`` property, which returns a list of ``Alias`` objects. Each + ``Alias`` object represents a parameter and its value, and the ``__str__`` method + will concatenate these values into a single string that can be passed to GMT. + + 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 + ... + ... @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' + >>> repr(var) + "Test(par1='val1')" + + >>> var = Test(par1="val1", par2="val2", par3=("val3a", "val3b")) + >>> str(var) + 'val1+aval2+bval3a/val3b' + >>> repr(var) + "Test(par1='val1', par2='val2', par3=('val3a', 'val3b'))" + """ + + 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] + ) + + 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) + return f"{self.__class__.__name__}({params})" diff --git a/pygmt/params/box.py b/pygmt/params/box.py new file mode 100644 index 00000000000..cdc4691bf32 --- /dev/null +++ b/pygmt/params/box.py @@ -0,0 +1,122 @@ +""" +The Box class for specifying the box around GMT embellishments. +""" + +import dataclasses +from collections.abc import Sequence + +from pygmt.alias import Alias +from pygmt.exceptions import GMTValueError +from pygmt.params.base import BaseParam + + +@dataclasses.dataclass(repr=False) +class Box(BaseParam): + """ + Class for specifying the box around GMT embellishments. + + Attributes + ---------- + clearance + Set clearances between the embellishment and the box border. It can be either a + scalar value or a sequence of two/four values. + + - a scalar value means a uniform clearance in all four directions. + - a sequence of two values means separate clearances in x- and y- directions. + - a sequence of four values means separate clearances for left/right/bottom/top. + fill + Fill for the box. Default is no fill. + pen + Pen attributes for the box outline. + radius + Draw a rounded rectangular borders instead of sharp. Passing a value with unit + to control the corner radius (default is ``"6p"``). + inner_gap + Gap between the outer and inner border. Default is ``"2p"``. + inner_pen + Pen attributes for the inner border. Default to :gmt-term:`MAP_DEFAULT_PEN`. + shading_offset + Place an offset background shaded region behind the box. A sequence of two + values (dx, dy) indicates the shift relative to the foreground frame. Default is + ``("4p", "-4p")``. + shading_fill + Fill for the shading region. Default is ``"gray50"``. + + 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_offset=("5p", "5p"), + ... shading_fill="lightred", + ... ) + ... ) + '+c0.2+pblue+r10p+s5p/5p/lightred' + >>> 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 CLI syntax is: + # + # -F[+cclearances][+gfill][+i[[gap/]pen]][+p[pen]][+r[radius]][+s[[dx/dy/][shade]]] + clearance: float | str | Sequence[float | str] | None = None + fill: str | None = None + inner_gap: float | str | None = None + inner_pen: str | None = None + pen: str | None = None + radius: str | bool = False + shading_offset: Sequence[float | str] | None = None + shading_fill: str | None = None + + @property + def _innerborder(self) -> list[str | float] | None: + """ + Inner border of the box, formatted as a list of 1-2 values, or None. + """ + return [v for v in (self.inner_gap, self.inner_pen) if v is not None] or None + + @property + def _shading(self) -> list[str | float] | None: + """ + Shading for the box, formatted as a list of 1-3 values, or None. + """ + # Local variable to simplify the code. + _shading_offset = [] if self.shading_offset is None else self.shading_offset + + # shading_offset must be a sequence of two values (dx, dy) or None. + if len(_shading_offset) not in {0, 2}: + raise GMTValueError( + self.shading_offset, + description="value for parameter 'shading_offset'", + reason="Must be a sequence of two values (dx, dy) or None.", + ) + return [ + v for v in (*_shading_offset, self.shading_fill) if v is not None + ] or None + + @property + def _aliases(self): + """ + Aliases for the parameter. + """ + return [ + Alias(self.clearance, prefix="+c", separator="/", size=[1, 2, 4]), + Alias(self.fill, prefix="+g"), + Alias(self._innerborder, prefix="+i", separator="/", size=[1, 2]), + Alias(self.pen, prefix="+p"), + Alias(self.radius, prefix="+r"), + Alias(self._shading, prefix="+s", separator="/", size=[1, 2, 3]), + ] From 9f08d1b1c364e77fa4187c6252e199d3027bdd4d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 16 Jul 2025 09:10:18 +0800 Subject: [PATCH 2/9] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Grund <23025878+michaelgrund@users.noreply.github.com> Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- 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 cdc4691bf32..ada6298b7a0 100644 --- a/pygmt/params/box.py +++ b/pygmt/params/box.py @@ -22,17 +22,17 @@ class Box(BaseParam): scalar value or a sequence of two/four values. - a scalar value means a uniform clearance in all four directions. - - a sequence of two values means separate clearances in x- and y- directions. + - a sequence of two values means separate clearances in x- and y-directions. - a sequence of four values means separate clearances for left/right/bottom/top. fill Fill for the box. Default is no fill. pen Pen attributes for the box outline. radius - Draw a rounded rectangular borders instead of sharp. Passing a value with unit - to control the corner radius (default is ``"6p"``). + Draw a rounded rectangular border instead of sharp. Passing a value with unit + to control the corner radius [Default is ``"6p"``]. inner_gap - Gap between the outer and inner border. Default is ``"2p"``. + Gap between the outer and inner borders. Default is ``"2p"``. inner_pen Pen attributes for the inner border. Default to :gmt-term:`MAP_DEFAULT_PEN`. shading_offset From 01b563cb04a8a7d106cd59fb8a5fd8353e8f4a88 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 16 Jul 2025 14:55:20 +0800 Subject: [PATCH 3/9] Use Box in examples and tests --- examples/gallery/images/cross_section.py | 7 ++++--- examples/gallery/lines/hlines_vlines.py | 3 ++- examples/tutorials/advanced/insets.py | 7 ++++--- pygmt/src/inset.py | 3 ++- pygmt/tests/test_image.py | 3 ++- pygmt/tests/test_inset.py | 5 +++-- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/examples/gallery/images/cross_section.py b/examples/gallery/images/cross_section.py index 816f7133c52..beda4cfecae 100644 --- a/examples/gallery/images/cross_section.py +++ b/examples/gallery/images/cross_section.py @@ -14,6 +14,7 @@ # %% import pygmt +from pygmt.params import Box # Define region of study area # lon_min, lon_max, lat_min, lat_max in degrees East and North @@ -44,9 +45,9 @@ # corner with an offset ("+o") of 0.7 centimeters and 0.3 centimeters in x- or y- # directions, respectively; move the x-label above the horizontal colorbar ("+ml") position="jBR+o0.7c/0.8c+h+w5c/0.3c+ml", - # Add a box around the colobar with a fill ("+g") in "white" color and a - # transparency ("@") of 30 % and with a 0.8-points thick, black, outline ("+p") - box="+gwhite@30+p0.8p,black", + # Add a box around the colobar, filled in "white" and a 30% transparency, with a + # 0.8-points thick, black, outline. + box=Box(pen="0.8p,black", fill="white@30"), # Add x- and y-labels ("+l") frame=["x+lElevation", "y+lm"], ) diff --git a/examples/gallery/lines/hlines_vlines.py b/examples/gallery/lines/hlines_vlines.py index ec0d73dddab..19049c71d5a 100644 --- a/examples/gallery/lines/hlines_vlines.py +++ b/examples/gallery/lines/hlines_vlines.py @@ -12,6 +12,7 @@ # In Cartesian coordinate systems lines are plotted as straight lines. import pygmt +from pygmt.params import Box fig = pygmt.Figure() @@ -31,7 +32,7 @@ fig.hlines( y=[2, 3], xmin=[0, 1], xmax=[7, 7.5], pen="1.5p,dodgerblue3", label="Lines 7 & 8" ) -fig.legend(position="JBR+jBR+o0.2c", box="+gwhite+p1p") +fig.legend(position="JBR+jBR+o0.2c", box=Box(pen="1p", fill="white")) fig.shift_origin(xshift="w+2c") diff --git a/examples/tutorials/advanced/insets.py b/examples/tutorials/advanced/insets.py index 49d6ba2e631..18aa618e882 100644 --- a/examples/tutorials/advanced/insets.py +++ b/examples/tutorials/advanced/insets.py @@ -10,6 +10,7 @@ # %% import pygmt +from pygmt.params import Box # %% # Prior to creating an inset figure, a larger figure must first be plotted. In @@ -48,7 +49,7 @@ water="lightblue", frame="a", ) -with fig.inset(position="jBL+w3c", box="+pblack+glightred"): +with fig.inset(position="jBL+w3c", box=Box(pen="black", fill="lightred")): # pass is used to exit the with statement as no plotting methods are # called pass @@ -72,7 +73,7 @@ water="lightblue", frame="a", ) -with fig.inset(position="jBL+w3c+o0.5c/0.2c", box="+pblack+glightred"): +with fig.inset(position="jBL+w3c+o0.5c/0.2c", box=Box(pen="black", fill="lightred")): pass fig.show() @@ -97,7 +98,7 @@ # parameters. with fig.inset( position="jBL+o0.5c/0.2c", - box="+pblack", + box=Box(pen="black"), region=[-80, -65, 35, 50], projection="M3c", ): diff --git a/pygmt/src/inset.py b/pygmt/src/inset.py index 76f577b285a..895e7aab8cc 100644 --- a/pygmt/src/inset.py +++ b/pygmt/src/inset.py @@ -113,13 +113,14 @@ def inset(self, **kwargs): Examples -------- >>> import pygmt + >>> from pygmt.params import Box >>> >>> # Create the larger figure >>> fig = pygmt.Figure() >>> fig.coast(region="MG+r2", water="lightblue", shorelines="thin") >>> # Use a "with" statement to initialize the inset context manager >>> # Setting the position to top left and a width of 3.5 centimeters - >>> with fig.inset(position="jTL+w3.5c+o0.2c", margin=0, box="+pgreen"): + >>> with fig.inset(position="jTL+w3.5c+o0.2c", margin=0, box=Box(pen="green")): ... # Map elements under the "with" statement are plotted in the inset ... fig.coast( ... region="g", 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 diff --git a/pygmt/tests/test_inset.py b/pygmt/tests/test_inset.py index 992ba9e48c0..ca5b411fe68 100644 --- a/pygmt/tests/test_inset.py +++ b/pygmt/tests/test_inset.py @@ -4,6 +4,7 @@ import pytest from pygmt import Figure +from pygmt.params import Box @pytest.mark.benchmark @@ -14,7 +15,7 @@ def test_inset_aliases(): """ fig = Figure() fig.basemap(region="MG+r2", frame="afg") - with fig.inset(position="jTL+w3.5c+o0.2c", margin=0, box="+pgreen"): + with fig.inset(position="jTL+w3.5c+o0.2c", margin=0, box=Box(pen="green")): fig.basemap(region="g", projection="G47/-20/4c", frame="afg") return fig @@ -27,7 +28,7 @@ def test_inset_context_manager(): """ fig = Figure() fig.basemap(region=[-74, -69.5, 41, 43], projection="M9c", frame=True) - with fig.inset(position="jBL+w3c+o0.2c", margin=0, box="+pblack"): + with fig.inset(position="jBL+w3c+o0.2c", margin=0, box=Box(pen="black")): fig.basemap(region=[-80, -65, 35, 50], projection="M3c", frame="afg") fig.basemap(rose="jTR+w3c") # Pass rose argument with basemap after the inset return fig From d2ae10c8efe5a137b2e1e41ce43674aca75e21ed Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 16 Jul 2025 15:39:21 +0800 Subject: [PATCH 4/9] Use Box in examples and tests --- examples/gallery/embellishments/inset.py | 3 ++- examples/gallery/embellishments/inset_rectangle_region.py | 3 ++- examples/gallery/embellishments/scalebar.py | 3 ++- examples/tutorials/advanced/legends.py | 5 +++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/gallery/embellishments/inset.py b/examples/gallery/embellishments/inset.py index dd2ba4f7631..51d98b3a07a 100644 --- a/examples/gallery/embellishments/inset.py +++ b/examples/gallery/embellishments/inset.py @@ -10,6 +10,7 @@ # %% import pygmt +from pygmt.params import Box fig = pygmt.Figure() # Create the primary figure, setting the region to Madagascar, the land color @@ -19,7 +20,7 @@ # Create an inset, placing it in the Top Left (TL) corner with a width of 3.5 cm and # x- and y-offsets of 0.2 cm. The margin is set to 0, and the border is "gold" with a # pen size of 1.5 points. -with fig.inset(position="jTL+w3.5c+o0.2c", margin=0, box="+p1.5p,gold"): +with fig.inset(position="jTL+w3.5c+o0.2c", margin=0, box=Box(pen="1.5p,gold")): # Create a figure in the inset using coast. This example uses the azimuthal # orthogonal projection centered at 47E, 20S. The land color is set to # "gray" and Madagascar is highlighted in "red3". diff --git a/examples/gallery/embellishments/inset_rectangle_region.py b/examples/gallery/embellishments/inset_rectangle_region.py index e646182e6b4..9f374836771 100644 --- a/examples/gallery/embellishments/inset_rectangle_region.py +++ b/examples/gallery/embellishments/inset_rectangle_region.py @@ -10,6 +10,7 @@ # %% import pygmt +from pygmt.params import Box # Set the region of the main figure region = [137.5, 141, 34, 37] @@ -34,7 +35,7 @@ # a pen of "1p". with fig.inset( position="jBR+o0.1c", - box="+gwhite+p1p", + box=Box(fill="white", pen="1p"), region=[129, 146, 30, 46], projection="U54S/3c", ): diff --git a/examples/gallery/embellishments/scalebar.py b/examples/gallery/embellishments/scalebar.py index 88794cb5c95..71820e70ad8 100644 --- a/examples/gallery/embellishments/scalebar.py +++ b/examples/gallery/embellishments/scalebar.py @@ -44,6 +44,7 @@ # %% import pygmt +from pygmt.params import Box # Create a new Figure instance fig = pygmt.Figure() @@ -105,7 +106,7 @@ # Fill the box in white with a transparency of 30 percent, add a solid # outline in darkgray (gray30) with a thickness of 0.5 points, and use # rounded edges with a radius of 3 points - box="+gwhite@30+p0.5p,gray30,solid+r3p", + box=Box(fill="white@30", pen="0.5p,gray30,solid", radius="3p"), ) fig.show() diff --git a/examples/tutorials/advanced/legends.py b/examples/tutorials/advanced/legends.py index 15598bc7343..7ab44b9be6e 100644 --- a/examples/tutorials/advanced/legends.py +++ b/examples/tutorials/advanced/legends.py @@ -10,6 +10,7 @@ import io import pygmt +from pygmt.params import Box # %% # Create an auto-legend @@ -90,7 +91,7 @@ # Add a box with a 2-points thick blue, solid outline and a white fill with a # transparency of 70 percentage ("@30"). -fig.legend(position="jTL+o0.3c/0.2c", box="+p2p,blue+gwhite@30") +fig.legend(position="jTL+o0.3c/0.2c", box=Box(pen="2p,blue", fill="white@30")) fig.show() @@ -152,7 +153,7 @@ fig.basemap(region=[-5, 5, -5, 5], projection="M10c", frame=True) # Pass the io.StringIO object to the "spec" parameter -fig.legend(spec=spec_io, position="jMC+w9c", box="+p1p,gray50+ggray95") +fig.legend(spec=spec_io, position="jMC+w9c", box=Box(pen="1p,gray50", fill="gray95")) fig.show() From ec1af1db2ff59220f7284252d02e7e3ca99b6680 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 26 Jul 2025 16:48:39 +0800 Subject: [PATCH 5/9] Fix API doc --- doc/api/index.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/api/index.rst b/doc/api/index.rst index 61979f0756f..ac7b257a658 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -194,24 +194,24 @@ Getting metadata from tabular or grid data: info grdinfo -Common Parameters ------------------ - -.. currentmodule:: pygmt.params +Xarray Integration +------------------ .. autosummary:: :toctree: generated - Box + GMTBackendEntrypoint + GMTDataArrayAccessor -Xarray Integration ------------------- +Common Parameters +----------------- + +.. currentmodule:: pygmt.params .. autosummary:: :toctree: generated - GMTBackendEntrypoint - GMTDataArrayAccessor + Box Enums ----- From 48702b53f366d1825700c8fa04496fab92babed5 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 27 Jul 2025 18:02:08 +0800 Subject: [PATCH 6/9] Clarify that subclass must implement the _aliases property --- pygmt/params/base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pygmt/params/base.py b/pygmt/params/base.py index af1b17a10ed..6a48c1c98d5 100644 --- a/pygmt/params/base.py +++ b/pygmt/params/base.py @@ -61,3 +61,14 @@ def __repr__(self): """ params = ", ".join(f"{k}={v!r}" for k, v in vars(self).items() if v is not None) return f"{self.__class__.__name__}({params})" + + @property + def _aliases(self): + """ + List of Alias objects representing the parameters of this class. + + This property must be implemented in subclasses to define the parameters + and their aliases. + """ + msg = "The _aliases property must be implemented in subclasses." + raise NotImplementedError(msg) From 210292fb15366fb0650eadd6dc98bcde990e8712 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 27 Jul 2025 20:12:54 +0800 Subject: [PATCH 7/9] Add validate method --- pygmt/params/base.py | 32 +++++++++++++++++++++++--------- pygmt/params/box.py | 25 +++++++++++++++---------- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/pygmt/params/base.py b/pygmt/params/base.py index 6a48c1c98d5..1f2d6c6af9f 100644 --- a/pygmt/params/base.py +++ b/pygmt/params/base.py @@ -47,20 +47,19 @@ class BaseParam: "Test(par1='val1', par2='val2', par3=('val3a', 'val3b'))" """ - def __str__(self): + def __post_init__(self): """ - String representation of the object that can be passed to GMT directly. + Post-initialization method to _validate the _aliases property. """ - return "".join( - [alias._value for alias in self._aliases if alias._value is not None] - ) + self.validate() - def __repr__(self): + def validate(self): """ - String representation of the object. + Validate the parameters of the object. + + This method should be overridden in subclasses to perform any necessary + validation on the parameters. """ - params = ", ".join(f"{k}={v!r}" for k, v in vars(self).items() if v is not None) - return f"{self.__class__.__name__}({params})" @property def _aliases(self): @@ -72,3 +71,18 @@ def _aliases(self): """ msg = "The _aliases property must be implemented in subclasses." raise NotImplementedError(msg) + + 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] + ) + + 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) + return f"{self.__class__.__name__}({params})" diff --git a/pygmt/params/box.py b/pygmt/params/box.py index ada6298b7a0..0a8bd017fb8 100644 --- a/pygmt/params/box.py +++ b/pygmt/params/box.py @@ -81,6 +81,20 @@ class Box(BaseParam): shading_offset: Sequence[float | str] | None = None shading_fill: str | None = None + def validate(self): + """ + Validate the parameters. + """ + # shading_offset must be a sequence of two values or None. + if ( + self.shading_offset and not isinstance(self.shading_offset, Sequence) + ) or len(self.shading_offset) != 2: + raise GMTValueError( + self.shading_offset, + description="value for parameter 'shading_offset'", + reason="Must be a sequence of two values (dx, dy) or None.", + ) + @property def _innerborder(self) -> list[str | float] | None: """ @@ -93,16 +107,7 @@ def _shading(self) -> list[str | float] | None: """ Shading for the box, formatted as a list of 1-3 values, or None. """ - # Local variable to simplify the code. - _shading_offset = [] if self.shading_offset is None else self.shading_offset - - # shading_offset must be a sequence of two values (dx, dy) or None. - if len(_shading_offset) not in {0, 2}: - raise GMTValueError( - self.shading_offset, - description="value for parameter 'shading_offset'", - reason="Must be a sequence of two values (dx, dy) or None.", - ) + _shading_offset = self.shading_offset or [] return [ v for v in (*_shading_offset, self.shading_fill) if v is not None ] or None From 8e5321e54bd47f1fd84576a321e04675a1726773 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 27 Jul 2025 20:30:32 +0800 Subject: [PATCH 8/9] Fix the check logic --- pygmt/params/box.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pygmt/params/box.py b/pygmt/params/box.py index 0a8bd017fb8..7abc43e42db 100644 --- a/pygmt/params/box.py +++ b/pygmt/params/box.py @@ -86,9 +86,10 @@ def validate(self): Validate the parameters. """ # shading_offset must be a sequence of two values or None. - if ( - self.shading_offset and not isinstance(self.shading_offset, Sequence) - ) or len(self.shading_offset) != 2: + if self.shading_offset and ( + not isinstance(self.shading_offset, Sequence) + or len(self.shading_offset) != 2 + ): raise GMTValueError( self.shading_offset, description="value for parameter 'shading_offset'", From 926a6903e67c95eb534adf8be950d77141a5c1c1 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 27 Jul 2025 21:07:01 +0800 Subject: [PATCH 9/9] Change to _validate --- pygmt/params/base.py | 4 ++-- pygmt/params/box.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/params/base.py b/pygmt/params/base.py index 1f2d6c6af9f..1092836e62b 100644 --- a/pygmt/params/base.py +++ b/pygmt/params/base.py @@ -51,9 +51,9 @@ def __post_init__(self): """ Post-initialization method to _validate the _aliases property. """ - self.validate() + self._validate() - def validate(self): + def _validate(self): """ Validate the parameters of the object. diff --git a/pygmt/params/box.py b/pygmt/params/box.py index 7abc43e42db..e3ee6520426 100644 --- a/pygmt/params/box.py +++ b/pygmt/params/box.py @@ -81,7 +81,7 @@ class Box(BaseParam): shading_offset: Sequence[float | str] | None = None shading_fill: str | None = None - def validate(self): + def _validate(self): """ Validate the parameters. """