qemu-devel.nongnu.org archive mirror
 help / color / mirror / Atom feed
From: "Alex Bennée" <alex.bennee@linaro.org>
To: qemu-devel@nongnu.org
Cc: "Cleber Rosa" <crosa@redhat.com>,
	"Thomas Huth" <thuth@redhat.com>,
	"Alex Bennée" <alex.bennee@linaro.org>,
	"Philippe Mathieu-Daudé" <philmd@linaro.org>,
	"John Snow" <jsnow@redhat.com>
Subject: [RFC PATCH 6/9] scripts/get_maintainer.py: initial parsing of MAINTAINERS
Date: Thu, 11 Dec 2025 18:01:29 +0000	[thread overview]
Message-ID: <20251211180132.3186564-7-alex.bennee@linaro.org> (raw)
In-Reply-To: <20251211180132.3186564-1-alex.bennee@linaro.org>

Add the basic infrastructure to parse MAINTAINERS and generate a list
of MaintainerSection objects we can use later.

Add a --validate argument so we can use the script to ensure
MAINTAINERS is always parse-able in our CI.

Signed-off-by: Alex Bennée <alex.bennee@linaro.org>
---
 scripts/get_maintainer.py | 165 +++++++++++++++++++++++++++++++++++++-
 1 file changed, 164 insertions(+), 1 deletion(-)

diff --git a/scripts/get_maintainer.py b/scripts/get_maintainer.py
index c713f290cc7..7b8ce2b65e3 100755
--- a/scripts/get_maintainer.py
+++ b/scripts/get_maintainer.py
@@ -10,9 +10,156 @@
 #
 # SPDX-License-Identifier: GPL-2.0-or-later
 
-from argparse import ArgumentParser, ArgumentTypeError
+from argparse import ArgumentParser, ArgumentTypeError, BooleanOptionalAction
 from os import path
 from pathlib import Path
+from enum import StrEnum, auto
+from re import compile as re_compile
+
+#
+# Subsystem MAINTAINER entries
+#
+# The MAINTAINERS file is an unstructured text file where the
+# important information is in lines that follow the form:
+#
+# X: some data
+#
+# where X is a documented tag and the data is variously an email,
+# path, regex or link. Other lines should be ignored except the
+# preceding non-blank or underlined line which represents the name of
+# the "subsystem" or general area of the project.
+#
+# A blank line denominates the end of a section.
+#
+
+tag_re = re_compile(r"^([A-Z]):")
+
+
+class UnhandledTag(Exception):
+    "Exception for unhandled tags"
+
+
+class BadStatus(Exception):
+    "Exception for unknown status"
+
+
+class Status(StrEnum):
+    "Maintenance status"
+
+    UNKNOWN = auto()
+    SUPPORTED = 'Supported'
+    MAINTAINED = 'Maintained'
+    ODD_FIXES = 'Odd Fixes'
+    ORPHAN = 'Orphan'
+    OBSOLETE = 'Obsolete'
+
+    @classmethod
+    def _missing_(cls, value):
+        # _missing_ is only invoked by the enum machinery if 'value' does not
+        # match any existing enum member's value.
+        # So, if we reach this point, 'value' is inherently invalid for this enum.
+        raise BadStatus(f"'{value}' is not a valid maintenance status.")
+
+
+person_re = re_compile(r"^(?P<name>[^<]+?)\s*<(?P<email>[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>\s*(?:@(?P<handle>\w+))?$")
+
+
+class BadPerson(Exception):
+    "Exception for un-parsable person"
+
+
+class Person:
+    "Class representing a maintainer or reviewer and their details"
+
+    def __init__(self, info):
+        match = person_re.search(info)
+
+        if match is None:
+            raise BadPerson(f"Failed to parse {info}")
+
+        self.name = match.group('name')
+        self.email = match.group('email')
+
+
+class MaintainerSection:
+    "Class representing a section of MAINTAINERS"
+
+    def _expand(self, pattern):
+        if pattern.endswith("/"):
+            return f"{pattern}*"
+        return pattern
+
+    def __init__(self, section, entries):
+        self.section = section
+        self.status = Status.UNKNOWN
+        self.maintainers = []
+        self.reviewers = []
+        self.files = []
+        self.files_exclude = []
+        self.trees = []
+        self.lists = []
+        self.web = []
+        self.keywords = []
+
+        for e in entries:
+            (tag, data) = e.split(": ", 2)
+
+            if tag == "M":
+                person = Person(data)
+                self.maintainers.append(person)
+            elif tag == "R":
+                person = Person(data)
+                self.reviewers.append(person)
+            elif tag == "S":
+                self.status = Status(data)
+            elif tag == "L":
+                self.lists.append(data)
+            elif tag == 'F':
+                pat = self._expand(data)
+                self.files.append(pat)
+            elif tag == 'W':
+                self.web.append(data)
+            elif tag == 'K':
+                self.keywords.append(data)
+            elif tag == 'T':
+                self.trees.append(data)
+            elif tag == 'X':
+                pat = self._expand(data)
+                self.files_exclude.append(pat)
+            else:
+                raise UnhandledTag(f"'{tag}' is not understood.")
+
+
+
+def read_maintainers(src):
+    """
+    Read the MAINTAINERS file, return a list of MaintainerSection objects.
+    """
+
+    mfile = path.join(src, 'MAINTAINERS')
+    entries = []
+
+    section = None
+    fields = []
+
+    with open(mfile, 'r', encoding='utf-8') as f:
+        for line in f:
+            if not line.strip():  # Blank line found, potential end of a section
+                if section:
+                    new_section = MaintainerSection(section, fields)
+                    entries.append(new_section)
+                    # reset for next section
+                    section = None
+                    fields = []
+            elif tag_re.match(line):
+                fields.append(line.strip())
+            else:
+                if line.startswith("-") or line.startswith("="):
+                    continue
+
+                section = line.strip()
+
+    return entries
 
 
 #
@@ -103,6 +250,12 @@ def main():
     group.add_argument('-f', '--file', type=valid_file_path,
                        help='path to source file')
 
+    # Validate MAINTAINERS
+    parser.add_argument('--validate',
+                        action=BooleanOptionalAction,
+                        default=None,
+                        help="Just validate MAINTAINERS file")
+
     # We need to know or be told where the root of the source tree is
     src = find_src_root()
 
@@ -115,6 +268,16 @@ def main():
 
     args = parser.parse_args()
 
+    try:
+        # Now we start by reading the MAINTAINERS file
+        maint_sections = read_maintainers(args.src)
+    except Exception as e:
+        print(f"Error: {e}")
+        exit(-1)
+
+    if args.validate:
+        print(f"loaded {len(maint_sections)} from MAINTAINERS")
+        exit(0)
 
 
 if __name__ == '__main__':
-- 
2.47.3



  parent reply	other threads:[~2025-12-11 18:02 UTC|newest]

Thread overview: 19+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-12-11 18:01 [RFC PATCH 0/9] for 11.0 conversion* of get_maintainers.pl to python Alex Bennée
2025-12-11 18:01 ` [RFC PATCH 1/9] MAINTAINERS: fix missing names Alex Bennée
2025-12-11 18:01 ` [RFC PATCH 2/9] MAINTAINERS: fix libvirt entry Alex Bennée
2025-12-12  6:45   ` Philippe Mathieu-Daudé
2025-12-12 11:00     ` Alex Bennée
2025-12-12 11:12       ` Ján Tomko
2025-12-11 18:01 ` [RFC PATCH 3/9] MAINTAINERS: regularise the status fields Alex Bennée
2025-12-12  6:43   ` Philippe Mathieu-Daudé
2025-12-11 18:01 ` [RFC PATCH 4/9] scripts/get_maintainer.py: minimal argument parsing Alex Bennée
2025-12-11 18:01 ` [RFC PATCH 5/9] scripts/get_maintainer.py: resolve the source path Alex Bennée
2025-12-11 18:01 ` Alex Bennée [this message]
2025-12-11 18:01 ` [RFC PATCH 7/9] scripts/get_maintainer.py: add support for -f Alex Bennée
2025-12-11 18:01 ` [RFC PATCH 8/9] scripts/get_maintainer.py: add support reading patch files Alex Bennée
2025-12-11 18:01 ` [RFC PATCH 9/9] gitlab: add a check-maintainers task Alex Bennée
2025-12-12  8:15 ` [RFC PATCH 0/9] for 11.0 conversion* of get_maintainers.pl to python Daniel P. Berrangé
2025-12-12 11:00   ` Alex Bennée
2025-12-12 11:25     ` Peter Maydell
2025-12-12 11:27 ` Peter Maydell
2025-12-12 14:38 ` Markus Armbruster

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251211180132.3186564-7-alex.bennee@linaro.org \
    --to=alex.bennee@linaro.org \
    --cc=crosa@redhat.com \
    --cc=jsnow@redhat.com \
    --cc=philmd@linaro.org \
    --cc=qemu-devel@nongnu.org \
    --cc=thuth@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).