Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC: Implement the new alias system, general parameter classes and some refactors #3238

Draft
wants to merge 81 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
cd1ffcd
Implement the new alias system
seisman May 10, 2024
492f2c2
Implement the BaseParam class as a base class for PyGMT class-like pa…
seisman May 10, 2024
ae9006d
Add the Box class for specifying the box parameter
seisman May 10, 2024
34c3dd7
Add the Frame/Axes/Axis class for the frame parameter
seisman May 10, 2024
85edc00
Add Figure.scalebar for plotting a scale bar
seisman May 10, 2024
b712840
Figure.basemap: Refactor to use the new alias system
seisman May 10, 2024
1b480a0
pygmt.dimfilter: Refactor to use the new alias system
seisman May 10, 2024
075d759
Figure.image: Refactor to use the new alias system
seisman May 10, 2024
0b0d5b9
Figure.logo: Refactor to use the new alias system
seisman May 10, 2024
c25ef4a
Figure.timestamp: Refactor to use the new alias system
seisman May 10, 2024
ce74b85
Figure.coast: Make the 'resolution' parameter more Pythonic
seisman Jan 18, 2024
c6ea4b8
pygmt.binstats: Make the 'statistic' parameter more Pythonic
seisman May 10, 2024
ecb5201
Merge branch 'main' into alias-system
seisman Jul 2, 2024
1c08e31
Merge branch 'main' into alias-system
seisman Jul 12, 2024
8fd16d2
Merge branch 'main' into alias-system
seisman Jul 17, 2024
478213d
Merge branch 'main' into alias-system
seisman Jul 28, 2024
07c4b4b
Merge branch 'main' into alias-system
seisman Aug 25, 2024
88a216c
Merge branch 'main' into alias-system
seisman Sep 24, 2024
b9a3267
Merge branch 'main' into alias-system
seisman Sep 27, 2024
477e7cb
Fix styling issues
seisman Sep 27, 2024
1f41808
Merge branch 'main' into alias-system
seisman Nov 22, 2024
a318d4b
Merge branch 'main' into alias-system
seisman Nov 26, 2024
d5a671a
Merge branch 'main' into alias-system
seisman Dec 3, 2024
484ecc0
Merge branch 'main' into alias-system
seisman Dec 5, 2024
03e2269
Merge branch 'main' into alias-system
seisman Dec 30, 2024
0a14135
Merge branch 'main' into alias-system
seisman Jan 3, 2025
a13182f
Improve the docstrings of the value_to_string function
seisman Jan 3, 2025
b305969
Merge branch 'main' into alias-system
seisman Feb 11, 2025
2058373
Merge branch 'main' into alias-system
seisman Feb 20, 2025
93c051b
Merge branch 'main' into alias-system
seisman Feb 25, 2025
e5bfa58
Merge branch 'main' into alias-system
seisman Mar 7, 2025
bdba2fc
Merge branch 'main' into alias-system
seisman Mar 10, 2025
1787cdf
Merge branch 'main' into alias-system
seisman Mar 10, 2025
4480581
Add more comments to the value_to_string function
seisman Mar 10, 2025
da7cea4
Refactor to allow passing an initial value to an alias
seisman Mar 10, 2025
a854e0d
Merge branch 'main' into alias-system
seisman Mar 12, 2025
bf56228
Fix for Python 3.13
seisman Mar 12, 2025
03d4bfd
Merge branch 'main' into alias-system
seisman Mar 28, 2025
e73cbf7
Update docstrings
seisman Mar 28, 2025
ee072df
Improve the Box class
seisman Mar 29, 2025
6aca13d
Avoid using inspect
seisman Mar 29, 2025
005c981
Merge branch 'main' into alias-system
seisman Mar 30, 2025
0655efe
Fix
seisman Mar 30, 2025
5067a3a
Add to documentation
seisman Mar 30, 2025
38bc807
Remove commented codes
seisman Mar 30, 2025
ee6ba40
Fix styling
seisman Mar 30, 2025
c02fdef
pygmt.grdclip: Deprecate parameter 'new' to 'replace' (remove in v0.1…
seisman Mar 31, 2025
a240fd4
Changelog entry for v0.15.0 (#3866)
seisman Mar 31, 2025
397ac10
DOC: Update URLs (Link Checker Report on 2025-03-30) (#3886)
yvonnefroehlich Mar 31, 2025
4100ace
Some fixes
seisman Mar 31, 2025
d048953
Merge branch 'main' into alias-system
seisman Mar 31, 2025
165412a
Remove the 'name' attribute from the Alias class
seisman Apr 1, 2025
79a0d91
Simplify Figure.timestamp
seisman Apr 1, 2025
a2a3e3d
Improve Figure.scalebar
seisman Apr 1, 2025
e2b448c
Improve Figure.logo
seisman Apr 1, 2025
86fb3af
Update Figure.image
seisman Apr 1, 2025
8641430
Update Box
seisman Apr 1, 2025
121e33d
Update BaseParam
seisman Apr 1, 2025
f95d807
Update AliasSystem
seisman Apr 1, 2025
b1e0d71
Update the release checklist post v0.15.0 release (#3887)
seisman Apr 1, 2025
c36df9d
Build(deps): Bump astral-sh/setup-uv from 5.4.0 to 5.4.1 (#3890)
dependabot[bot] Apr 1, 2025
3f03ff3
Build(deps): Bump actions/create-github-app-token from 1.11.6 to 1.12…
dependabot[bot] Apr 1, 2025
6499b84
Merge branch 'main' into alias-system
seisman Apr 2, 2025
3b70672
Rename value_to_string to to_string
seisman Apr 3, 2025
66bb37c
Merge branch 'main' into alias-system
seisman Apr 3, 2025
145dd79
Merge branch 'main' into alias-system
seisman Apr 5, 2025
b6707b1
Refactor the Alias class
seisman Apr 5, 2025
8d7fb60
Merge branch 'main' into alias-system
seisman Apr 7, 2025
3aaf44a
Improve Figure.timestamp
seisman Apr 7, 2025
00d17f4
Improve the code structure
seisman Apr 7, 2025
a75f22c
Improve docstring of frame
seisman Apr 7, 2025
fb916cf
Improve the docstring of logo box
seisman Apr 7, 2025
1ca7382
Revert "Improve the code structure"
seisman Apr 7, 2025
2e7339d
Improve code structure
seisman Apr 7, 2025
dea66ea
Refactor the to_string function to make it more readable
seisman Apr 7, 2025
c893755
Improve comment in to_string
seisman Apr 8, 2025
7ae8500
Simplify the AliasSystem class
seisman Apr 8, 2025
95e6c63
Simplify to_string
seisman Apr 8, 2025
38e2c73
Add more doctests to Alias
seisman Apr 8, 2025
b2c12c8
Fix static typing
seisman Apr 8, 2025
9da950c
Merge branch 'main' into alias-system
seisman Apr 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,17 @@ Getting metadata from tabular or grid data:
info
grdinfo

Common Parameters
-----------------

.. currentmodule:: pygmt.params

.. autosummary::
:toctree: generated

Box
Frame

Enums
-----

Expand Down
236 changes: 236 additions & 0 deletions pygmt/alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
"""
PyGMT's alias system for converting PyGMT parameters to GMT short-form options.
"""

import dataclasses
from collections import defaultdict
from collections.abc import Mapping
from typing import Any, Literal

from pygmt.helpers.utils import is_nonstr_iter


def to_string(
value: Any,
prefix: str = "", # Default to an empty string to simplify the code logic.
separator: Literal["/", ","] | None = None,
mapping: bool | Mapping = False,
) -> str | list[str] | None:
"""
Convert any value to a string, a sequence of strings or 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 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
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.
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
----------
value
The value to convert.
prefix
The string to add as a prefix to the returned value.
separator
The separator to use if the value is a sequence.
mapping
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
--------
>>> to_string("text")
'text'
>>> to_string(12)
'12'
>>> to_string((12, 34), separator="/")
'12/34'
>>> to_string(("12p", "34p"), separator=",")
'12p,34p'
>>> to_string(("12p", "34p"), prefix="+o", separator="/")
'+o12p/34p'
>>> to_string(True)
''
>>> to_string(True, prefix="+a")
'+a'
>>> to_string(False)
>>> to_string(None)
>>> to_string(["xaf", "yaf", "WSen"])
['xaf', 'yaf', 'WSen']
>>> to_string("high", mapping=True)
'h'
>>> to_string("mean", mapping={"mean": "a", "mad": "d", "full": "g"})
'a'
>>> to_string("invalid", mapping={"mean": "a", "mad": "d", "full": "g"})
'invalid'
"""
if value is None or value is False: # None and False are converted to None.
return None
if value is True: # True is converted to an empty string with the optional prefix.
return f"{prefix}"

# 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]
# 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
class Alias:
"""
Class for aliasing a PyGMT parameter to a GMT option or a modifier.
Attributes
----------
value
Value of the parameter.
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.
Examples
--------
>>> par = Alias((3.0, 3.0), prefix="+o", separator="/")
>>> par._value
'+o3.0/3.0'
>>> 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
prefix: str = ""
separator: Literal["/", ","] | None = None
mapping: bool | Mapping = False

@property
def _value(self) -> str | list[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,
)


class AliasSystem:
"""
Alias system for converting PyGMT parameters to GMT options.
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 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
--------
>>> 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 | kwargs)
>>> 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 the alias system and create the keyword dictionary that stores the
current parameter values.
"""
# Keyword dictionary with an empty string as default value.
self.kwdict = defaultdict(str)

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
1 change: 1 addition & 0 deletions pygmt/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ def _repr_html_(self) -> str:
plot3d,
psconvert,
rose,
scalebar,
set_panel,
shift_origin,
solar,
Expand Down
6 changes: 6 additions & 0 deletions pygmt/params/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Classes for PyGMT common parameters.
"""

from pygmt.params.box import Box
from pygmt.params.frame import Axes, Axis, Frame
50 changes: 50 additions & 0 deletions pygmt/params/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
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
...
... @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')"
"""

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})"
Loading