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 0468F43E51; Fri, 12 Apr 2024 13:12:10 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id EEE8B40A72; Fri, 12 Apr 2024 13:11:57 +0200 (CEST) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mails.dpdk.org (Postfix) with ESMTP id 6623A40A4B for ; Fri, 12 Apr 2024 13:11:56 +0200 (CEST) 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 34C5B339; Fri, 12 Apr 2024 04:12:25 -0700 (PDT) Received: from localhost.localdomain (unknown [10.57.19.212]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id CEC4E3F766; Fri, 12 Apr 2024 04:11:54 -0700 (PDT) From: Luca Vizzarro To: dev@dpdk.org Cc: =?UTF-8?q?Juraj=20Linke=C5=A1?= , Jeremy Spewock , Luca Vizzarro , Paul Szczepanek Subject: [PATCH 3/5] dts: add parsing utility module Date: Fri, 12 Apr 2024 12:11:34 +0100 Message-Id: <20240412111136.3470304-4-luca.vizzarro@arm.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20240412111136.3470304-1-luca.vizzarro@arm.com> References: <20240412111136.3470304-1-luca.vizzarro@arm.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit 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 Adds parsing text into a custom data structure. It provides a new `TextParser` dataclass to be inherited. This implements the `parse` method, which combined with the parser functions, it can automatically parse the value for each field. Signed-off-by: Luca Vizzarro Reviewed-by: Paul Szczepanek --- dts/framework/parser.py | 147 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 dts/framework/parser.py diff --git a/dts/framework/parser.py b/dts/framework/parser.py new file mode 100644 index 0000000000..5a2ba0c93a --- /dev/null +++ b/dts/framework/parser.py @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2024 Arm Limited + +"""Parsing utility module. + +This module provides :class:`~TextParser` which can be used to model any data structure +that can parse a block of text. +""" + +from dataclasses import dataclass, fields, MISSING +import re +from typing import TypeVar +from typing_extensions import Self + +T = TypeVar("T") + + +META_PARSERS = "parsers" + + +def chain(parser, metadata): + """Chain a parser function. + + The parser function can take and return a single argument of any type. It is + up to the user to ensure that the chained functions have compatible types. + + Args: + parser: the parser function pointer + metadata: pre-existing metadata to chain if any + """ + parsers = metadata.get(META_PARSERS) or [] + parsers.append(parser) + return {**metadata, META_PARSERS: parsers} + + +def to_int(metadata={}, base=0): + """Converts a string to an integer. + + Args: + metadata: pre-existing metadata to chain if any + base: argument passed to the constructor of ``int`` + """ + return chain(lambda v: int(v, base), metadata) + + +def eq(v2, metadata={}): + """Compares two values and returns a boolean. + + Args: + v2: value to compare with the incoming value + metadata: pre-existing metadata to chain if any + """ + return chain(lambda v1: v1 == v2, metadata) + + +def to_bool(metadata={}): + """Evaluates a string into a boolean. + + The following case-insensitive words yield ``True``: on, yes, enabled, true. + + Args: + metadata: pre-existing metadata to chain if any + """ + return chain(lambda s: s.lower() in ["on", "yes", "enabled", "true"], metadata) + + +def regex( + pattern: str | re.Pattern[str], + flags: re.RegexFlag = re.RegexFlag(0), + named: bool = False, + metadata={}, +): + """Searches for a regular expression in a text. + + If there is only one capture group, its value is returned, otherwise a tuple containing all the + capture groups values is returned instead. + + Args: + pattern: the regular expression pattern + flags: the regular expression flags + named: if set to True only the named capture groups will be returned as a dictionary + metadata: pre-existing metadata to chain if any + """ + pattern = re.compile(pattern, flags) + + def regex_parser(text: str): + m = pattern.search(text) + if m is None: + return m + + if named: + return m.groupdict() + + matches = m.groups() + if len(matches) == 1: + return matches[0] + + return matches + + return chain(regex_parser, metadata) + + +@dataclass +class TextParser: + """Helper abstract dataclass that parses a text according to the fields' rules. + + This class is accompanied by a selection of parser functions and a generic chaining function, + that are to be set to the fields' metadata, to enable parsing. If a field metadata is not set with + any parser function, this is skipped. + """ + + @classmethod + def parse(cls, text: str) -> Self: + """The parsing class method. + + This function loops through every field that has any parser function associated with it and runs + each parser chain to the supplied text. If a parser function returns None, it expects that parsing + has failed and continues to the next field. + + Args: + text: the text to parse + Raises: + RuntimeError: if the parser did not find a match and the field does not have a default value + or default factory. + """ + fields_values = {} + for field in fields(cls): + parsers = field.metadata.get(META_PARSERS) + if parsers is None: + continue + + field_value = text + for parser_fn in parsers: + field_value = parser_fn(field_value) + if field_value is None: + # nothing was actually parsed, move on + break + + if field_value is None: + if field.default is MISSING and field.default_factory is MISSING: + raise RuntimeError( + f"parsers for field {field.name} returned None, but the field has no default" + ) + else: + fields_values[field.name] = field_value + + return cls(**fields_values) -- 2.34.1