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 5ABD7440B7; Fri, 31 May 2024 17:19:35 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 001AA402C9; Fri, 31 May 2024 17:19:34 +0200 (CEST) Received: from mail-lf1-f45.google.com (mail-lf1-f45.google.com [209.85.167.45]) by mails.dpdk.org (Postfix) with ESMTP id 890A940265 for ; Fri, 31 May 2024 17:19:34 +0200 (CEST) Received: by mail-lf1-f45.google.com with SMTP id 2adb3069b0e04-52b8aae138fso105246e87.2 for ; Fri, 31 May 2024 08:19:34 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1717168774; x=1717773574; 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=hyy3Fcvql5bALM8dDDYNTPjWUY0RapeeSV5gFKjwPHI=; b=F5fuZn5eVA4PL3/zIIpysq3T2xKcfqn1+fm9cvgZGDGfYCaHIPBETLoa4Z2ELbYtdC WqWiOyYZFB9sF+saSPRt7e7t8T6oKOIT7J2ITMsxLDWiqvRQd3Pm6C4MSdvxe4uf9Hm7 zeX1PcJCqFFILpbXaqoJHCBKh7+ndloGsvobg= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1717168774; x=1717773574; 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=hyy3Fcvql5bALM8dDDYNTPjWUY0RapeeSV5gFKjwPHI=; b=cB2CLwQvkMK9tK786LKp8XM1MtkJSFjpgyIpC2zfHwia5nYyWafh73A0nq4cmchOpG zvjglkJe5K8k3ivw5T9vjVxEutp2/l/pdVVuTvUiGNjz5K8fIdF1WV6PJCOCH7oaeW23 ffqD5xXUqoAk6c6X4h+dHoLAUBy03Nvr12VgxNoxADqsU1qam1sYhw7Fe3Kqw6VjC7tT tKYRv29BSKSlLT7nsL5FXCSmL6pfCxsr8UtUxztyjDUY6jkUM4hzPEo+8ouKeSmZPsdV T3sHSht0s7bnJBHW/gzeGL/0GTQsPdEGUWmG+JPnnxpAbeHeJXgoM3bwqD0TMrZxqxPy 9p6g== X-Gm-Message-State: AOJu0YwbVS1EC/PjMCt39VxxiC79+oVtJiudfwPvUySThs5v/2xcDgDq WeXmASZJqMDuR8p/HnCUMQNyqDF9hUewyUcT+ccivF/C6gny8gNbOShkd2WOy9WeXSmBkYh34mk VkOZtG4VpPxmLunef0/JGUPffbA3ca9bEyvKA4Q== X-Google-Smtp-Source: AGHT+IEDLac/1bWjLiZnmm8toOdutm6Hg9/IbQH7QrzfHd7xdVeraI1kO369WXi99bjPif7hraNx1rxN9TaINjWAJRM= X-Received: by 2002:a2e:be0f:0:b0:2ea:806a:d701 with SMTP id 38308e7fff4ca-2ea95242b93mr14818661fa.3.1717168773773; Fri, 31 May 2024 08:19:33 -0700 (PDT) MIME-Version: 1.0 References: <20240326190422.577028-1-luca.vizzarro@arm.com> <20240530152505.389167-1-luca.vizzarro@arm.com> <20240530152505.389167-2-luca.vizzarro@arm.com> In-Reply-To: <20240530152505.389167-2-luca.vizzarro@arm.com> From: Nicholas Pratte Date: Fri, 31 May 2024 11:19:22 -0400 Message-ID: Subject: Re: [PATCH v3 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 30, 2024 at 11:25=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..18fedcf1ff > --- /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 into 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 >