Skip to content

Commit 7c26a8d

Browse files
committed
Implement the new alias system
1 parent 4fbf32f commit 7c26a8d

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed

pygmt/alias.py

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

0 commit comments

Comments
 (0)