Tested-by: Nicholas Pratte <npra...@iol.unh.edu>
Reviewed-by: Nicholas Pratte <npra...@iol.unh.edu>

On Mon, Jun 17, 2024 at 10:54 AM Luca Vizzarro <luca.vizza...@arm.com> wrote:
>
> This commit introduces a new "params" module, which adds a new way
> to manage command line parameters. The provided Params dataclass
> is able to read the fields of its child class and produce a string
> representation to supply to the command line. Any data structure
> that is intended to represent command line parameters can inherit it.
>
> The main purpose is to make it easier to represent data structures that
> map to parameters. Aiding quicker development, while minimising code
> bloat.
>
> Signed-off-by: Luca Vizzarro <luca.vizza...@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepa...@arm.com>
> ---
>  dts/framework/params/__init__.py | 358 +++++++++++++++++++++++++++++++
>  1 file changed, 358 insertions(+)
>  create mode 100644 dts/framework/params/__init__.py
>
> diff --git a/dts/framework/params/__init__.py 
> b/dts/framework/params/__init__.py
> new file mode 100644
> index 0000000000..107b070ed2
> --- /dev/null
> +++ b/dts/framework/params/__init__.py
> @@ -0,0 +1,358 @@
> +# 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]:
> +    """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.
> +
> +    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)
> --
> 2.34.1
>

Reply via email to