From cc9f3649a744e8e2526f86b69cb427ba5fb719dc Mon Sep 17 00:00:00 2001 From: Andreas Sandberg Date: Wed, 4 Nov 2020 19:07:08 +0000 Subject: [PATCH] util: Add a library to parse MAINTAINERS.yaml Add a very simple library to parse MAINTAINERS.yaml. There are currently no tools that use the library, but it can be tested using `python3 -m "maint.lib.maintainers"` from within the util directory. Change-Id: Id2edff94451f27e0b601994d198d0647325e4b35 Signed-off-by: Andreas Sandberg Reviewed-on: https://gem5-review.googlesource.com/c/public/gem5/+/37036 Maintainer: Gabe Black Reviewed-by: Hoa Nguyen Tested-by: kokoro --- util/maint/lib/__init__.py | 1 + util/maint/lib/maintainers.py | 180 ++++++++++++++++++++++++++++ util/maint/lib/tests/__init__.py | 1 + util/maint/lib/tests/maintainers.py | 117 ++++++++++++++++++ 4 files changed, 299 insertions(+) create mode 100644 util/maint/lib/__init__.py create mode 100644 util/maint/lib/maintainers.py create mode 100644 util/maint/lib/tests/__init__.py create mode 100644 util/maint/lib/tests/maintainers.py diff --git a/util/maint/lib/__init__.py b/util/maint/lib/__init__.py new file mode 100644 index 000000000..e5a0d9b48 --- /dev/null +++ b/util/maint/lib/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/util/maint/lib/maintainers.py b/util/maint/lib/maintainers.py new file mode 100644 index 000000000..072c9b477 --- /dev/null +++ b/util/maint/lib/maintainers.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2020 Arm Limited +# All rights reserved +# +# The license below extends only to copyright in the software and shall +# not be construed as granting a license to any other intellectual +# property including but not limited to intellectual property relating +# to a hardware implementation of the functionality of the software +# licensed hereunder. You may use the software subject to the license +# terms below provided that you ensure that this notice is replicated +# unmodified and in its entirety in all distributions of the software, +# modified or unmodified, in source code or in binary form. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer; +# redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution; +# neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import email.utils +import enum +import os +from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence, \ + TextIO, Tuple, Union + +import yaml + +PathOrFile = Union[TextIO, str] + +class FileFormatException(Exception): + pass + +class MissingFieldException(FileFormatException): + pass + +class IllegalValueException(FileFormatException): + pass + +class Status(enum.Enum): + MAINTAINED = enum.auto() + ORPHANED = enum.auto() + + @classmethod + def from_str(cls, key: str) -> 'Status': + _status_dict = { + 'maintained': cls.MAINTAINED, + 'orphaned': cls.ORPHANED, + } + return _status_dict[key] + + def __str__(self) -> str: + return { + Status.MAINTAINED: 'maintained', + Status.ORPHANED: 'orphaned', + }[self] + +class Subsystem(object): + tag: str + status: Status + maintainers: List[Tuple[str, str]] # Name, email + description: str + + def __init__(self, tag: str, + maintainers: Optional[Sequence[Tuple[str, str]]], + description: str = '', + status: Status = Status.ORPHANED): + self.tag = tag + self.status = status + self.maintainers = list(maintainers) if maintainers is not None else [] + self.description = description if description is not None else '' + +class Maintainers(object): + DEFAULT_MAINTAINERS = os.path.join(os.path.dirname(__file__), + '../../../MAINTAINERS.yaml') + + _subsystems: Dict[str, Subsystem] # tag -> Subsystem + + def __init__(self, ydict: Mapping[str, Any]): + self._subsystems = {} + for tag, maint in ydict.items(): + self._subsystems[tag] = Maintainers._parse_subsystem(tag, maint) + + @classmethod + def from_file(cls, path_or_file: Optional[PathOrFile] = None) \ + -> "Maintainers": + + return cls(Maintainers._load_maintainers_file(path_or_file)) + + @classmethod + def from_yaml(cls, yaml_str: str) -> "Maintainers": + return cls(yaml.load(yaml_str)) + + @classmethod + def _load_maintainers_file(cls, + path_or_file: Optional[PathOrFile] = None) \ + -> Mapping[str, Any]: + if path_or_file is None: + path_or_file = cls.DEFAULT_MAINTAINERS + + if isinstance(path_or_file, str): + with open(path_or_file, 'r') as fin: + return yaml.load(fin) + else: + return yaml.load(path_or_file) + + @classmethod + def _parse_subsystem(cls, tag: str, ydict: Mapping[str, Any]) -> Subsystem: + def required_field(name): + try: + return ydict[name] + except KeyError: + raise MissingFieldException( + f"{tag}: Required field '{name}' is missing") + + maintainers: List[Tuple[str, str]] = [] + raw_maintainers = ydict.get('maintainers', []) + if not isinstance(raw_maintainers, Sequence): + raise IllegalValueException( + f"{tag}: Illegal field 'maintainers' isn't a list.") + for maintainer in raw_maintainers: + name, address = email.utils.parseaddr(maintainer) + if name == '' and address == '': + raise IllegalValueException( + f"{tag}: Illegal maintainer field: '{maintainer}'") + maintainers.append((name, address)) + + try: + status = Status.from_str(required_field('status')) + except KeyError: + raise IllegalValueException( + f"{tag}: Invalid status '{ydict['status']}'") + + return Subsystem(tag, maintainers=maintainers, status=status, + description=ydict.get('desc', '')) + + def __iter__(self) -> Iterator[Tuple[str, Subsystem]]: + return iter(self._subsystems.items()) + + def __getitem__(self, key: str) -> Subsystem: + return self._subsystems[key] + +def _main(): + maintainers = Maintainers.from_file() + for tag, subsys in maintainers: + print(f'{tag}: {subsys.description}') + print(f' Status: {subsys.status}') + print(f' Maintainers:') + for maint in subsys.maintainers: + print(f' - {maint[0]} <{maint[1]}>') + print() + +if __name__ == '__main__': + _main() + +__all__ = [ + "FileFormatException", + "MissingFieldException", + "IllegalValueException", + "Status", + "Subsystem", + "Maintainers", +] diff --git a/util/maint/lib/tests/__init__.py b/util/maint/lib/tests/__init__.py new file mode 100644 index 000000000..e5a0d9b48 --- /dev/null +++ b/util/maint/lib/tests/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/util/maint/lib/tests/maintainers.py b/util/maint/lib/tests/maintainers.py new file mode 100644 index 000000000..cc71f2162 --- /dev/null +++ b/util/maint/lib/tests/maintainers.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2020 Arm Limited +# All rights reserved +# +# The license below extends only to copyright in the software and shall +# not be construed as granting a license to any other intellectual +# property including but not limited to intellectual property relating +# to a hardware implementation of the functionality of the software +# licensed hereunder. You may use the software subject to the license +# terms below provided that you ensure that this notice is replicated +# unmodified and in its entirety in all distributions of the software, +# modified or unmodified, in source code or in binary form. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer; +# redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution; +# neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unittest + +from ..maintainers import * + +YAML_VALID = r""" +maintained: + status: maintained + maintainers: + - John Doe + - Jane Doe + +# Test that we can handle a subsystem without maintainers +orphaned: + desc: Abandoned + status: orphaned +""" + +YAML_MISSING_STATUS = r""" +key: + maintainers: + - John Doe +""" + +YAML_INVALID_STATUS = r""" +key: + status: invalid_status_name + maintainers: + - John Doe +""" + +YAML_MAINTAINERS_NOT_LIST = r""" +key: + status: maintained + maintainers: +""" + +class StatusTestSuite(unittest.TestCase): + """Test cases for maintainers.Status""" + + def test_str_conv(self): + pairs = [ + ("maintained", Status.MAINTAINED), + ("orphaned", Status.ORPHANED), + ] + + for name, value in pairs: + assert value == Status.from_str(name) + assert str(value) == name + +class MaintainersTestSuite(unittest.TestCase): + """Test cases for Maintainers""" + + def test_parser_valid(self): + maint = Maintainers.from_yaml(YAML_VALID) + + subsys = maint['maintained'] + self.assertEqual(subsys.status, Status.MAINTAINED) + self.assertEqual(subsys.description, '') + self.assertEqual(subsys.maintainers, [ + ('John Doe', 'john.doe@test.gem5.org'), + ('Jane Doe', 'jane.doe@test.gem5.org'), + ]) + + subsys = maint['orphaned'] + self.assertEqual(subsys.status, Status.ORPHANED) + self.assertEqual(subsys.description, 'Abandoned') + self.assertEqual(subsys.maintainers, []) + + def test_parser_invalid(self): + with self.assertRaises(MissingFieldException): + Maintainers.from_yaml(YAML_MISSING_STATUS) + + with self.assertRaises(IllegalValueException): + Maintainers.from_yaml(YAML_INVALID_STATUS) + + with self.assertRaises(IllegalValueException): + Maintainers.from_yaml(YAML_MAINTAINERS_NOT_LIST) + +if __name__ == '__main__': + unittest.main() -- 2.30.2