Skip to content

Commit 5fac150

Browse files
committed
Implement the new alias system
1 parent 4fbf32f commit 5fac150

File tree

1 file changed

+245
-0
lines changed

1 file changed

+245
-0
lines changed

pygmt/alias.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
"""
2+
Alias system that converts PyGMT parameters to GMT short-form options.
3+
"""
4+
5+
import dataclasses
6+
import inspect
7+
from collections import defaultdict
8+
from collections.abc import Sequence
9+
from typing import Any
10+
11+
from pygmt.helpers.utils import is_nonstr_iter
12+
13+
14+
def value_to_string(
15+
value: Any,
16+
prefix: str = "", # Default to an empty string to simplify the code logic.
17+
separator: str | None = None,
18+
) -> str | Sequence[str] | None:
19+
"""
20+
Convert any value to a string, a sequence of strings or None.
21+
22+
``None`` or ``False`` will be converted to ``None``.
23+
24+
``True`` will be converted to an empty string. If the value is a sequence and a
25+
separator is provided, the sequence will be joined by the separator. Otherwise, each
26+
item in the sequence will be converted to a string and a sequence of strings will be
27+
returned. Any other value will be converted to a string if possible.
28+
29+
An optional prefix (e.g., `"+o"`) can be added to the beginning of the converted
30+
string.
31+
32+
Parameters
33+
----------
34+
value
35+
The value to convert.
36+
prefix
37+
The string to add as a prefix to the value.
38+
separator
39+
The separator to use if the value is a sequence.
40+
41+
Examples
42+
--------
43+
>>> value_to_string("text")
44+
'text'
45+
>>> value_to_string(12)
46+
'12'
47+
>>> value_to_string((12, 34), separator="/")
48+
'12/34'
49+
>>> value_to_string(("12p", "34p"), separator=",")
50+
'12p,34p'
51+
>>> value_to_string(("12p", "34p"), prefix="+o", separator="/")
52+
'+o12p/34p'
53+
>>> value_to_string(True)
54+
''
55+
>>> value_to_string(True, prefix="+a")
56+
'+a'
57+
>>> value_to_string(False)
58+
>>> value_to_string(None)
59+
>>> value_to_string(["xaf", "yaf", "WSen"])
60+
['xaf', 'yaf', 'WSen']
61+
"""
62+
# None or False means the parameter is not specified, returns None.
63+
if value is None or value is False:
64+
return None
65+
# True means the parameter is specified, returns an empty string with the optional
66+
# prefix ('prefix' defaults to an empty string!).
67+
if value is True:
68+
return f"{prefix}"
69+
70+
# Convert any value to a string or a sequence of strings
71+
if is_nonstr_iter(value): # Is a sequence
72+
value = [str(item) for item in value] # Convert to a sequence of strings
73+
if separator is None:
74+
# A sequence is given but separator is not specified. In this case, return
75+
# a sequence of strings, which is used to support repeated GMT options like
76+
# '-B'. 'prefix' makes no sense here, so ignored.
77+
return value
78+
value = separator.join(value) # Join the sequence by the specified separator.
79+
return f"{prefix}{value}"
80+
81+
82+
@dataclasses.dataclass
83+
class Alias:
84+
"""
85+
Class for aliasing a PyGMT parameter to a GMT option or a modifier.
86+
87+
Attributes
88+
----------
89+
name
90+
Parameter name.
91+
prefix
92+
String to add at the beginning of the value.
93+
separator
94+
Separator to use if the value is a sequence.
95+
value
96+
Value of the parameter.
97+
98+
Examples
99+
--------
100+
>>> par = Alias("offset", prefix="+o", separator="/")
101+
>>> par.value = (2.0, 2.0)
102+
>>> par.value
103+
'+o2.0/2.0'
104+
>>> par = Alias("frame")
105+
>>> par.value = ("xaf", "yaf", "WSen")
106+
>>> par.value
107+
['xaf', 'yaf', 'WSen']
108+
"""
109+
110+
name: str
111+
prefix: str = "" # Default to an empty string to simplify code logic.
112+
separator: str | None = None
113+
_value: Any = None
114+
115+
@property
116+
def value(self) -> str | Sequence[str] | None:
117+
"""
118+
Get the value of the parameter.
119+
"""
120+
return self._value
121+
122+
@value.setter
123+
def value(self, new_value: Any):
124+
"""
125+
Set the value of the parameter.
126+
127+
Internally, the value is converted to a string, a sequence of strings or None.
128+
"""
129+
self._value = value_to_string(new_value, self.prefix, self.separator)
130+
131+
132+
class AliasSystem:
133+
"""
134+
Alias system to convert PyGMT parameter into a keyword dictionary for GMT options.
135+
136+
The AliasSystem class is initialized by keyword arguments where the key is the GMT
137+
single-letter option flag and the value is one or a list of ``Alias`` objects.
138+
139+
The ``kwdict`` property is a keyword dictionary that stores the current parameter
140+
values. The key of the dictionary is the GMT single-letter option flag, and the
141+
value is the corresponding value of the option. The value can be a string or a
142+
sequence of strings, or None. The keyword dictionary can be passed to the
143+
``build_arg_list`` function.
144+
145+
Need to note that the ``kwdict`` property is dynamically computed from the current
146+
values of parameters. So, don't change it and avoid accessing it multiple times.
147+
148+
Examples
149+
--------
150+
>>> from pygmt.alias import Alias, AliasSystem
151+
>>> from pygmt.helpers import build_arg_list
152+
>>>
153+
>>> def func(
154+
... par0,
155+
... par1=None,
156+
... par2=None,
157+
... par3=None,
158+
... par4=None,
159+
... frame=False,
160+
... panel=None,
161+
... **kwargs,
162+
... ):
163+
... alias = AliasSystem(
164+
... A=[
165+
... Alias("par1"),
166+
... Alias("par2", prefix="+j"),
167+
... Alias("par3", prefix="+o", separator="/"),
168+
... ],
169+
... B=Alias("frame"),
170+
... c=Alias("panel", separator=","),
171+
... )
172+
... return build_arg_list(alias.kwdict)
173+
>>> func(
174+
... "infile",
175+
... par1="mytext",
176+
... par3=(12, 12),
177+
... frame=True,
178+
... panel=(1, 2),
179+
... J="X10c/10c",
180+
... )
181+
['-Amytext+o12/12', '-B', '-JX10c/10c', '-c1,2']
182+
"""
183+
184+
def __init__(self, **kwargs):
185+
"""
186+
Initialize as a dictionary of GMT options and their aliases.
187+
"""
188+
self.options = {}
189+
for option, aliases in kwargs.items():
190+
if isinstance(aliases, list):
191+
self.options[option] = aliases
192+
elif isinstance(aliases, str): # Support shorthand like 'J="projection"'
193+
self.options[option] = [Alias(aliases)]
194+
else:
195+
self.options[option] = [aliases]
196+
197+
@property
198+
def kwdict(self):
199+
"""
200+
A keyword dictionary that stores the current parameter values.
201+
"""
202+
# Get the local variables from the calling function.
203+
p_locals = inspect.currentframe().f_back.f_locals
204+
# Get parameters/arguments from **kwargs of the calling function.
205+
p_kwargs = p_locals.pop("kwargs", {})
206+
207+
params = p_locals | p_kwargs
208+
# Default value is an empty string to simplify code logic.
209+
kwdict = defaultdict(str)
210+
for option, aliases in self.options.items():
211+
for alias in aliases:
212+
alias.value = params.get(alias.name)
213+
# value can be a string, a sequence of strings or None.
214+
if alias.value is None:
215+
continue
216+
217+
# Special handing of repeatable parameter like -B/frame.
218+
if is_nonstr_iter(alias.value):
219+
kwdict[option] = alias.value
220+
# A repeatable option should have only one alias, so break.
221+
break
222+
223+
kwdict[option] += alias.value
224+
225+
# Support short-form parameter names specified in kwargs.
226+
# Short-form parameters can be either one-letter (e.g., '-B'), or two-letters
227+
# (e.g., '-Td').
228+
for option, value in p_kwargs.items():
229+
# Here, we assume that long-form parameters specified in kwargs are longer
230+
# than two characters. Sometimes, we may use parameter like 'az', but it's
231+
# not specified in kwargs. So, the assumption is still valid.
232+
if len(option) > 2:
233+
continue
234+
235+
# Two cases for short-form parameters:
236+
#
237+
# If it has an alias and the long-form parameter is also specified, (e.g.,
238+
# 'projection="X10c", J="X10c"'), then we silently ignore the short-form
239+
# parameter.
240+
#
241+
# If it has an alias but the long-form parameter is not specified, or it
242+
# doesn't has an alias, then we use the value of the short-form parameter.
243+
if option not in self.options or option not in kwdict:
244+
kwdict[option] = value
245+
return kwdict

0 commit comments

Comments
 (0)