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