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
next prev 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).