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[[T], T]
#: 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: T) -> T:
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: T) -> T:
setattr(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)
|