From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id F01B2440EC; Tue, 28 May 2024 17:41:04 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 7232A402E8; Tue, 28 May 2024 17:41:04 +0200 (CEST) Received: from mail-lj1-f174.google.com (mail-lj1-f174.google.com [209.85.208.174]) by mails.dpdk.org (Postfix) with ESMTP id 48453402E4 for ; Tue, 28 May 2024 17:41:02 +0200 (CEST) Received: by mail-lj1-f174.google.com with SMTP id 38308e7fff4ca-2e952657b74so831541fa.3 for ; Tue, 28 May 2024 08:41:02 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1716910861; x=1717515661; darn=dpdk.org; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=2wK4wtXElx5OTWqdTDQmeDVG2ZzWiujFZ3Nmp4Gkb3s=; b=VxVLnr3InXqvCRzuaIialTGUdmnhHQiVlkeezI+O9VCOO296wBDbVtfBHjqoW7tpI1 eEi3m3K/Tgvc0OwwdUu+LAX4sh+7wqhRAaWCKVTBqfmedreIrrKZVm831S0qYprG/gmE PJN+Nyt6uIrtQNxvQdvO7Lxqh4eiAR+3UBhGA= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1716910861; x=1717515661; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=2wK4wtXElx5OTWqdTDQmeDVG2ZzWiujFZ3Nmp4Gkb3s=; b=wxHqLPL7NawtIif0H74zZ3NpqB4R6rk6KmtgXfywV/eHUkCmuWqjCEHrYpFweoI/ro 1y0MJESs0lHGMuduBxoZWvkLxWCRke1Ag/546znzxt9qoYHclGkqo3JnSG/zq8GTQ1Iu Uk5yohtT9eagrrSWhjLTSEDARsR7QvDAr62Py92h5PnCsTQ8PDrIDu0sm648KtsJ4wM7 FY2V4gvOIhpkIatqHXGVYvcHObZxsPljwW0is3SHOFjoXHa0YnlUhJ5Rfh1fP/Up8Fz2 s9b4F4WVJdvtSmc+nOq893w9UKAZ2wqffa2vzjT92O0yK+cJI+fUDKB5gMA8m0otahdl /PSw== X-Gm-Message-State: AOJu0YxJS/2eRld0GpAC5IK65HVGRd4Z+osnS5BZx03wShCmJtb/PnvL Hk8W574m5oOnI9Gr4Ulgcp9FWrRzI3bB7fT5ONJWs4lt2cXnPUOmtoD3vEj5vBAlzIM6wzr+Tg2 xpeSX5mkq9Z2J6LL9ZEzlqgxzkWuZu9qd0uN4lw== X-Google-Smtp-Source: AGHT+IFWPeP5dYuMfcoy6zveb0jJaMWWU5ff3rJdSN+nK0bthaloTJiK043d1XRhZJDf4LLoB9VfjplaZfOWFuDEABM= X-Received: by 2002:a2e:95da:0:b0:2e9:8197:ec9f with SMTP id 38308e7fff4ca-2e98197ee3cmr28278841fa.0.1716910861483; Tue, 28 May 2024 08:41:01 -0700 (PDT) MIME-Version: 1.0 References: <20240326190422.577028-1-luca.vizzarro@arm.com> <20240509112057.1167947-1-luca.vizzarro@arm.com> <20240509112057.1167947-2-luca.vizzarro@arm.com> In-Reply-To: <20240509112057.1167947-2-luca.vizzarro@arm.com> From: Nicholas Pratte Date: Tue, 28 May 2024 11:40:50 -0400 Message-ID: Subject: Re: [PATCH v2 1/8] dts: add params manipulation module To: Luca Vizzarro Cc: dev@dpdk.org, =?UTF-8?Q?Juraj_Linke=C5=A1?= , Jeremy Spewock , Paul Szczepanek Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable 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 Tested-by: Nicholas Pratte Reviewed-by: Nicholas Pratte On Thu, May 9, 2024 at 7:21=E2=80=AFAM Luca Vizzarro 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 > Reviewed-by: Paul Szczepanek > --- > dts/framework/params/__init__.py | 274 +++++++++++++++++++++++++++++++ > 1 file changed, 274 insertions(+) > create mode 100644 dts/framework/params/__init__.py > > diff --git a/dts/framework/params/__init__.py b/dts/framework/params/__in= it__.py > new file mode 100644 > index 0000000000..aa27e34357 > --- /dev/null > +++ b/dts/framework/params/__init__.py > @@ -0,0 +1,274 @@ > +# 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, fields > +from enum import Flag > +from typing import Any, Callable, Iterable, Literal, Reversible, TypedDi= ct, cast > + > +from typing_extensions import Self > + > +#: Type for a function taking one argument. > +FnPtr =3D Callable[[Any], Any] > +#: Type for a switch parameter. > +Switch =3D Literal[True, None] > +#: Type for a yes/no switch parameter. > +YesNoSwitch =3D Literal[True, False, None] > + > + > +def _reduce_functions(funcs: Reversible[FnPtr]) -> FnPtr: > + """Reduces an iterable of :attr:`FnPtr` from end to start to a compo= site function. > + > + If the iterable is empty, the created function just returns its fed = value back. > + """ > + > + def composite_function(value: Any): > + for fn in reversed(funcs): > + value =3D fn(value) > + return value > + > + return composite_function > + > + > +def convert_str(*funcs: FnPtr): > + """Decorator that makes the ``__str__`` method a composite function = created from its arguments. > + > + The :attr:`FnPtr`s fed to the decorator are executed from right to l= eft > + in the arguments list order. > + > + Example: > + .. code:: python > + > + @convert_str(hex_from_flag_value) > + class BitMask(enum.Flag): > + A =3D auto() > + B =3D auto() > + > + will allow ``BitMask`` to render as a hexadecimal value. > + """ > + > + def _class_decorator(original_class): > + original_class.__str__ =3D _reduce_functions(funcs) > + return original_class > + > + return _class_decorator > + > + > +def comma_separated(values: Iterable[Any]) -> str: > + """Converts 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: > + """Adds round brackets to the input.""" > + return f"({value})" > + > + > +def str_from_flag_value(flag: Flag) -> str: > + """Returns the value from a :class:`enum.Flag` as a string.""" > + return str(flag.value) > + > + > +def hex_from_flag_value(flag: Flag) -> str: > + """Returns the value from a :class:`enum.Flag` converted to hexadeci= mal.""" > + return hex(flag.value) > + > + > +class ParamsModifier(TypedDict, total=3DFalse): > + """Params modifiers dict compatible with the :func:`dataclasses.fiel= d` metadata parameter.""" > + > + #: > + Params_value_only: bool > + #: > + 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 foll= owing: > + > + .. code:: python > + > + name: str | None =3D "value" > + > + is rendered as ``--name=3Dvalue``. > + Through :func:`dataclasses.field` the resulting parameter can be man= ipulated by applying > + this class' metadata modifier functions. > + > + 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 =3D True # renders --interactive > + numa: YesNoSwitch =3D False # renders --no-numa > + > + Setting ``None`` will prevent it from being rendered. The :attr:`~Sw= itch` type alias is provided > + for regular switches, whereas :attr:`~YesNoSwitch` is offered for ye= s/no ones. > + > + An instance of a dataclass inheriting ``Params`` can also be assigne= d to an attribute, > + this helps with grouping parameters together. > + The attribute holding the dataclass will be ignored and the latter w= ill just be rendered as > + expected. > + """ > + > + _suffix =3D "" > + """Holder of the plain text value of Params when called directly. A = suffix for child classes.""" > + > + """=3D=3D=3D=3D=3D=3D=3D=3D=3D BEGIN FIELD METADATA MODIFIER FUNCTIO= NS =3D=3D=3D=3D=3D=3D=3D=3D""" > + > + @staticmethod > + def value_only() -> ParamsModifier: > + """Injects the value of the attribute as-is without flag. > + > + Metadata modifier for :func:`dataclasses.field`. > + """ > + return ParamsModifier(Params_value_only=3DTrue) > + > + @staticmethod > + def short(name: str) -> ParamsModifier: > + """Overrides any parameter name with the given short option. > + > + Metadata modifier for :func:`dataclasses.field`. > + > + Example: > + .. code:: python > + > + logical_cores: str | None =3D field(default=3D"1-4", metadat= a=3DParams.short("l")) > + > + will render as ``-l=3D1-4`` instead of ``--logical-cores=3D1-4``= . > + """ > + return ParamsModifier(Params_short=3Dname) > + > + @staticmethod > + def long(name: str) -> ParamsModifier: > + """Overrides the inferred parameter name to the specified one. > + > + Metadata modifier for :func:`dataclasses.field`. > + > + Example: > + .. code:: python > + > + x_name: str | None =3D field(default=3D"y", metadata=3DParam= s.long("x")) > + > + will render as ``--x=3Dy``, but the field is accessed and modifi= ed through ``x_name``. > + """ > + return ParamsModifier(Params_long=3Dname) > + > + @staticmethod > + def multiple() -> ParamsModifier: > + """Specifies that this parameter is set multiple times. Must be = a list. > + > + Metadata modifier for :func:`dataclasses.field`. > + > + Example: > + .. code:: python > + > + ports: list[int] | None =3D field( > + default_factory=3Dlambda: [0, 1, 2], > + metadata=3DParams.multiple() | Params.long("port") > + ) > + > + will render as ``--port=3D0 --port=3D1 --port=3D2``. Note that m= odifiers can be chained like > + in this example. > + """ > + return ParamsModifier(Params_multiple=3DTrue) > + > + @classmethod > + def convert_value(cls, *funcs: FnPtr) -> ParamsModifier: > + """Takes in a variable number of functions to convert the value = text representation. > + > + Metadata modifier for :func:`dataclasses.field`. > + > + The ``metadata`` keyword argument can be used to chain metadata = modifiers together. > + > + Functions can be chained together, executed from right to left i= n the arguments list order. > + > + Example: > + .. code:: python > + > + hex_bitmask: int | None =3D field( > + default=3D0b1101, > + metadata=3DParams.convert_value(hex) | Params.long("mask= ") > + ) > + > + will render as ``--mask=3D0xd``. > + """ > + return ParamsModifier(Params_convert_value=3Dfuncs) > + > + """=3D=3D=3D=3D=3D=3D=3D=3D=3D END FIELD METADATA MODIFIER FUNCTIONS= =3D=3D=3D=3D=3D=3D=3D=3D""" > + > + def append_str(self, text: str) -> None: > + """Appends a string at the end of the string representation.""" > + self._suffix +=3D text > + > + def __iadd__(self, text: str) -> Self: > + """Appends a string at the end of the string representation.""" > + self.append_str(text) > + return self > + > + @classmethod > + def from_str(cls, text: str) -> Self: > + """Creates a plain Params object from a string.""" > + obj =3D cls() > + obj.append_str(text) > + return obj > + > + @staticmethod > + def _make_switch( > + name: str, is_short: bool =3D False, is_no: bool =3D False, valu= e: str | None =3D None > + ) -> str: > + prefix =3D f"{'-' if is_short else '--'}{'no-' if is_no else ''}= " > + name =3D name.replace("_", "-") > + value =3D f"{' ' if is_short else '=3D'}{value}" if value else "= " > + return f"{prefix}{name}{value}" > + > + def __str__(self) -> str: > + """Returns a string of command-line-ready arguments from the cla= ss fields.""" > + arguments: list[str] =3D [] > + > + for field in fields(self): > + value =3D getattr(self, field.name) > + modifiers =3D cast(ParamsModifier, field.metadata) > + > + if value is None: > + continue > + > + value_only =3D modifiers.get("Params_value_only", False) > + if isinstance(value, Params) or value_only: > + arguments.append(str(value)) > + continue > + > + # take the short modifier, or the long modifier, or infer fr= om field name > + switch_name =3D modifiers.get("Params_short", modifiers.get(= "Params_long", field.name)) > + is_short =3D "Params_short" in modifiers > + > + if isinstance(value, bool): > + arguments.append(self._make_switch(switch_name, is_short= , is_no=3D(not value))) > + continue > + > + convert =3D _reduce_functions(modifiers.get("Params_convert_= value", [])) > + multiple =3D modifiers.get("Params_multiple", False) > + > + values =3D value if multiple else [value] > + for value in values: > + arguments.append(self._make_switch(switch_name, is_short= , value=3Dconvert(value))) > + > + if self._suffix: > + arguments.append(self._suffix) > + > + return " ".join(arguments) > -- > 2.34.1 >