From patchwork Tue Mar 26 19:04:17 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Vizzarro X-Patchwork-Id: 138827 X-Patchwork-Delegate: thomas@monjalon.net Return-Path: X-Original-To: patchwork@inbox.dpdk.org Delivered-To: patchwork@inbox.dpdk.org Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id A91C443D5B; Tue, 26 Mar 2024 20:04:39 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id DFECB40EE5; Tue, 26 Mar 2024 20:04:33 +0100 (CET) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mails.dpdk.org (Postfix) with ESMTP id E848040EA5 for ; Tue, 26 Mar 2024 20:04:32 +0100 (CET) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id 11313339; Tue, 26 Mar 2024 12:05:06 -0700 (PDT) Received: from localhost.localdomain (unknown [10.57.16.115]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id 2F2113F64C; Tue, 26 Mar 2024 12:04:30 -0700 (PDT) From: Luca Vizzarro To: dev@dpdk.org Cc: =?utf-8?q?Juraj_Linke=C5=A1?= , Luca Vizzarro , Jack Bond-Preston , Honnappa Nagarahalli Subject: [PATCH 1/6] dts: add parameters data structure Date: Tue, 26 Mar 2024 19:04:17 +0000 Message-Id: <20240326190422.577028-2-luca.vizzarro@arm.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20240326190422.577028-1-luca.vizzarro@arm.com> References: <20240326190422.577028-1-luca.vizzarro@arm.com> MIME-Version: 1.0 X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org 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 representation can then manipulated by using the dataclass field metadata in conjunction with the provided functions: * value_only, used to supply a value without forming an option/flag * options_end, used to prefix with an options end delimiter (`--`) * short, used to define a short parameter name, e.g. `-p` * long, used to define a long parameter name, e.g. `--parameter` * multiple, used to turn a list into repeating parameters * field_mixins, used to manipulate the string representation of the value Signed-off-by: Luca Vizzarro Reviewed-by: Jack Bond-Preston Reviewed-by: Honnappa Nagarahalli --- dts/framework/params.py | 232 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 dts/framework/params.py diff --git a/dts/framework/params.py b/dts/framework/params.py new file mode 100644 index 0000000000..6b48c8353e --- /dev/null +++ b/dts/framework/params.py @@ -0,0 +1,232 @@ +# 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 parameters. +""" + +from dataclasses import dataclass, field, fields +from typing import Any, Callable, Literal, Reversible, TypeVar, Iterable +from enum import Flag + + +T = TypeVar("T") +#: Type for a Mixin. +Mixin = Callable[[Any], str] +#: Type for an option parameter. +Option = Literal[True, None] +#: Type for a yes/no option parameter. +BooleanOption = Literal[True, False, None] + +META_VALUE_ONLY = "value_only" +META_OPTIONS_END = "options_end" +META_SHORT_NAME = "short_name" +META_LONG_NAME = "long_name" +META_MULTIPLE = "multiple" +META_MIXINS = "mixins" + + +def value_only(metadata: dict[str, Any] = {}) -> dict[str, Any]: + """Injects the value of the attribute as-is without flag. Metadata modifier for :func:`dataclasses.field`.""" + return {**metadata, META_VALUE_ONLY: True} + + +def short(name: str, metadata: dict[str, Any] = {}) -> dict[str, Any]: + """Overrides any parameter name with the given short option. Metadata modifier for :func:`dataclasses.field`. + + .. code:: python + + logical_cores: str | None = field(default="1-4", metadata=short("l")) + + will render as ``-l=1-4`` instead of ``--logical-cores=1-4``. + """ + return {**metadata, META_SHORT_NAME: name} + + +def long(name: str, metadata: dict[str, Any] = {}) -> dict[str, Any]: + """Overrides the inferred parameter name to the specified one. Metadata modifier for :func:`dataclasses.field`. + + .. code:: python + + x_name: str | None = field(default="y", metadata=long("x")) + + will render as ``--x=y``, but the field is accessed and modified through ``x_name``. + """ + return {**metadata, META_LONG_NAME: name} + + +def options_end(metadata: dict[str, Any] = {}) -> dict[str, Any]: + """Precedes the value with an options end delimiter (``--``). Metadata modifier for :func:`dataclasses.field`.""" + return {**metadata, META_OPTIONS_END: True} + + +def multiple(metadata: dict[str, Any] = {}) -> dict[str, Any]: + """Specifies that this parameter is set multiple times. Must be a list. Metadata modifier for :func:`dataclasses.field`. + + .. code:: python + + ports: list[int] | None = field(default_factory=lambda: [0, 1, 2], metadata=multiple(param_name("port"))) + + will render as ``--port=0 --port=1 --port=2``. Note that modifiers can be chained like in this example. + """ + return {**metadata, META_MULTIPLE: True} + + +def field_mixins(*mixins: Mixin, metadata: dict[str, Any] = {}) -> dict[str, Any]: + """Takes in a variable number of mixins to manipulate the value's rendering. Metadata modifier for :func:`dataclasses.field`. + + The ``metadata`` keyword argument can be used to chain metadata modifiers together. + + Mixins can be chained together, executed from right to left in the arguments list order. + + Example: + + .. code:: python + + hex_bitmask: int | None = field(default=0b1101, metadata=field_mixins(hex, metadata=param_name("mask"))) + + will render as ``--mask=0xd``. The :func:`hex` built-in can be used as a mixin turning a valid integer into a hexadecimal representation. + """ + return {**metadata, META_MIXINS: mixins} + + +def _reduce_mixins(mixins: Reversible[Mixin], value: Any) -> str: + for mixin in reversed(mixins): + value = mixin(value) + return value + + +def str_mixins(*mixins: Mixin): + """Decorator which modifies the ``__str__`` method, enabling support for mixins. + + Mixins can be chained together, executed from right to left in the arguments list order. + + Example: + + .. code:: python + + @str_mixins(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__ = lambda self: _reduce_mixins(mixins, self) + return original_class + + return _class_decorator + + +def comma_separated(values: Iterable[T]) -> str: + """Mixin which renders an iterable in a comma-separated string.""" + return ",".join([str(value).strip() for value in values if value is not None]) + + +def bracketed(value: str) -> str: + """Mixin which adds round brackets to the input.""" + return f"({value})" + + +def str_from_flag_value(flag: Flag) -> str: + """Mixin which returns the value from a :class:`enum.Flag` as a string.""" + return str(flag.value) + + +def hex_from_flag_value(flag: Flag) -> str: + """Mixin which turns a :class:`enum.Flag` value into hexadecimal.""" + return hex(flag.value) + + +def _make_option(param_name: str, short: bool = False, no: bool = False) -> str: + param_name = param_name.replace("_", "-") + return f"{'-' if short else '--'}{'no-' if no else ''}{param_name}" + + +@dataclass +class Params: + """Helper abstract 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 + appropriate metadata. This class can be used with the following metadata modifiers: + + * :func:`value_only` + * :func:`options_end` + * :func:`short` + * :func:`long` + * :func:`multiple` + * :func:`field_mixins` + + To use fields as option switches set the value to ``True`` to enable them. If you + use a yes/no option switch you can also set ``False`` which would enable an option + prefixed with ``--no-``. Examples: + + .. code:: python + + interactive: Option = True # renders --interactive + numa: BooleanOption = False # renders --no-numa + + Setting ``None`` will disable any option. The :attr:`~Option` type alias is provided for + regular option switches, whereas :attr:`~BooleanOption` 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. + """ + + def __str__(self) -> str: + arguments: list[str] = [] + + for field in fields(self): + value = getattr(self, field.name) + + if value is None: + continue + + options_end = field.metadata.get(META_OPTIONS_END, False) + if options_end: + arguments.append("--") + + value_only = field.metadata.get(META_VALUE_ONLY, False) + if isinstance(value, Params) or value_only or options_end: + arguments.append(str(value)) + continue + + # take "short_name" metadata, or "long_name" metadata, or infer from field name + option_name = field.metadata.get( + META_SHORT_NAME, field.metadata.get(META_LONG_NAME, field.name) + ) + is_short = META_SHORT_NAME in field.metadata + + if isinstance(value, bool): + arguments.append(_make_option(option_name, short=is_short, no=(not value))) + continue + + option = _make_option(option_name, short=is_short) + separator = " " if is_short else "=" + str_mixins = field.metadata.get(META_MIXINS, []) + multiple = field.metadata.get(META_MULTIPLE, False) + + values = value if multiple else [value] + for entry_value in values: + entry_value = _reduce_mixins(str_mixins, entry_value) + arguments.append(f"{option}{separator}{entry_value}") + + return " ".join(arguments) + + +@dataclass +class StrParams(Params): + """A drop-in replacement for parameters passed as a string.""" + + value: str = field(metadata=value_only())