|
| 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