1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
|
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2024 Arm Limited
"""Parameter manipulation module.
This module provides :class:`Params` which can be used to model any data structure
that is meant to represent any command line parameters.
"""
from dataclasses import dataclass, fields
from enum import Flag
from typing import (
Any,
Callable,
Iterable,
Literal,
Reversible,
TypedDict,
TypeVar,
cast,
)
from typing_extensions import Self
T = TypeVar("T")
#: Type for a function taking one argument.
FnPtr = Callable[[Any], Any]
#: Type for a switch parameter.
Switch = Literal[True, None]
#: Type for a yes/no switch parameter.
YesNoSwitch = Literal[True, False, None]
def _reduce_functions(funcs: Iterable[FnPtr]) -> FnPtr:
"""Reduces an iterable of :attr:`FnPtr` from left to right to a single function.
If the iterable is empty, the created function just returns its fed value back.
Args:
funcs: An iterable containing the functions to be chained from left to right.
Returns:
FnPtr: A function that calls the given functions from left to right.
"""
def reduced_fn(value):
for fn in funcs:
value = fn(value)
return value
return reduced_fn
def modify_str(*funcs: FnPtr) -> Callable[[T], T]:
r"""Class decorator modifying the ``__str__`` method with a function created from its arguments.
The :attr:`FnPtr`\s fed to the decorator are executed from left to right in the arguments list
order.
Args:
*funcs: The functions to chain from left to right.
Returns:
The decorator.
Example:
.. code:: python
@convert_str(hex_from_flag_value)
class BitMask(enum.Flag):
A = auto()
B = auto()
will allow ``BitMask`` to render as a hexadecimal value.
"""
def _class_decorator(original_class):
original_class.__str__ = _reduce_functions(funcs)
return original_class
return _class_decorator
def comma_separated(values: Iterable[Any]) -> str:
"""Converts an iterable into a comma-separated string.
Args:
values: An iterable of objects.
Returns:
A comma-separated list of stringified values.
"""
return ",".join([str(value).strip() for value in values if value is not None])
def bracketed(value: str) -> str:
"""Adds round brackets to the input.
Args:
value: Any string.
Returns:
A string surrounded by round brackets.
"""
return f"({value})"
def str_from_flag_value(flag: Flag) -> str:
"""Returns the value from a :class:`enum.Flag` as a string.
Args:
flag: An instance of :class:`Flag`.
Returns:
The stringified value of the given flag.
"""
return str(flag.value)
def hex_from_flag_value(flag: Flag) -> str:
"""Returns the value from a :class:`enum.Flag` converted to hexadecimal.
Args:
flag: An instance of :class:`Flag`.
Returns:
The value of the given flag in hexadecimal representation.
"""
return hex(flag.value)
class ParamsModifier(TypedDict, total=False):
"""Params modifiers dict compatible with the :func:`dataclasses.field` metadata parameter."""
#:
Params_short: str
#:
Params_long: str
#:
Params_multiple: bool
#:
Params_convert_value: Reversible[FnPtr]
@dataclass
class Params:
"""Dataclass that renders its fields into command line arguments.
The parameter name is taken from the field name by default. The following:
.. code:: python
name: str | None = "value"
is rendered as ``--name=value``.
Through :func:`dataclasses.field` the resulting parameter can be manipulated by applying
this class' metadata modifier functions. These return regular dictionaries which can be combined
together using the pipe (OR) operator, as used in the example for :meth:`~Params.multiple`.
To use fields as switches, set the value to ``True`` to render them. If you
use a yes/no switch you can also set ``False`` which would render a switch
prefixed with ``--no-``. Examples:
.. code:: python
interactive: Switch = True # renders --interactive
numa: YesNoSwitch = False # renders --no-numa
Setting ``None`` will prevent it from being rendered. The :attr:`~Switch` type alias is provided
for regular switches, whereas :attr:`~YesNoSwitch` is offered for yes/no ones.
An instance of a dataclass inheriting ``Params`` can also be assigned to an attribute,
this helps with grouping parameters together.
The attribute holding the dataclass will be ignored and the latter will just be rendered as
expected.
"""
_suffix = ""
"""Holder of the plain text value of Params when called directly. A suffix for child classes."""
"""========= BEGIN FIELD METADATA MODIFIER FUNCTIONS ========"""
@staticmethod
def short(name: str) -> ParamsModifier:
"""Overrides any parameter name with the given short option.
Args:
name: The short parameter name.
Returns:
ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
the parameter short name modifier.
Example:
.. code:: python
logical_cores: str | None = field(default="1-4", metadata=Params.short("l"))
will render as ``-l=1-4`` instead of ``--logical-cores=1-4``.
"""
return ParamsModifier(Params_short=name)
@staticmethod
def long(name: str) -> ParamsModifier:
"""Overrides the inferred parameter name to the specified one.
Args:
name: The long parameter name.
Returns:
ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
the parameter long name modifier.
Example:
.. code:: python
x_name: str | None = field(default="y", metadata=Params.long("x"))
will render as ``--x=y``, but the field is accessed and modified through ``x_name``.
"""
return ParamsModifier(Params_long=name)
@staticmethod
def multiple() -> ParamsModifier:
"""Specifies that this parameter is set multiple times. The parameter type must be a list.
Returns:
ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
the multiple parameters modifier.
Example:
.. code:: python
ports: list[int] | None = field(
default_factory=lambda: [0, 1, 2],
metadata=Params.multiple() | Params.long("port")
)
will render as ``--port=0 --port=1 --port=2``.
"""
return ParamsModifier(Params_multiple=True)
@staticmethod
def convert_value(*funcs: FnPtr) -> ParamsModifier:
"""Takes in a variable number of functions to convert the value text representation.
Functions can be chained together, executed from left to right in the arguments list order.
Args:
*funcs: The functions to chain from left to right.
Returns:
ParamsModifier: A dictionary for the `dataclasses.field` metadata argument containing
the convert value modifier.
Example:
.. code:: python
hex_bitmask: int | None = field(
default=0b1101,
metadata=Params.convert_value(hex) | Params.long("mask")
)
will render as ``--mask=0xd``.
"""
return ParamsModifier(Params_convert_value=funcs)
"""========= END FIELD METADATA MODIFIER FUNCTIONS ========"""
def append_str(self, text: str) -> None:
"""Appends a string at the end of the string representation.
Args:
text: Any text to append at the end of the parameters string representation.
"""
self._suffix += text
def __iadd__(self, text: str) -> Self:
"""Appends a string at the end of the string representation.
Args:
text: Any text to append at the end of the parameters string representation.
Returns:
The given instance back.
"""
self.append_str(text)
return self
@classmethod
def from_str(cls, text: str) -> Self:
"""Creates a plain Params object from a string.
Args:
text: The string parameters.
Returns:
A new plain instance of :class:`Params`.
"""
obj = cls()
obj.append_str(text)
return obj
@staticmethod
def _make_switch(
name: str, is_short: bool = False, is_no: bool = False, value: str | None = None
) -> str:
"""Make the string representation of the parameter.
Args:
name: The name of the parameters.
is_short: If the parameters is short or not.
is_no: If the parameter is negated or not.
value: The value of the parameter.
Returns:
The complete command line parameter.
"""
prefix = f"{'-' if is_short else '--'}{'no-' if is_no else ''}"
name = name.replace("_", "-")
value = f"{' ' if is_short else '='}{value}" if value else ""
return f"{prefix}{name}{value}"
def __str__(self) -> str:
"""Returns a string of command-line-ready arguments from the class fields."""
arguments: list[str] = []
for field in fields(self):
value = getattr(self, field.name)
modifiers = cast(ParamsModifier, field.metadata)
if value is None:
continue
if isinstance(value, Params):
arguments.append(str(value))
continue
# take the short modifier, or the long modifier, or infer from field name
switch_name = modifiers.get("Params_short", modifiers.get("Params_long", field.name))
is_short = "Params_short" in modifiers
if isinstance(value, bool):
arguments.append(self._make_switch(switch_name, is_short, is_no=(not value)))
continue
convert = _reduce_functions(modifiers.get("Params_convert_value", []))
multiple = modifiers.get("Params_multiple", False)
values = value if multiple else [value]
for value in values:
arguments.append(self._make_switch(switch_name, is_short, value=convert(value)))
if self._suffix:
arguments.append(self._suffix)
return " ".join(arguments)
|