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 <andreas.sandberg@arm.com> Reviewed-on: https://gem5-review.googlesource.com/c/public/gem5/+/37036 Maintainer: Gabe Black <gabe.black@gmail.com> Reviewed-by: Hoa Nguyen <hoanguyen@ucdavis.edu> Tested-by: kokoro <noreply+kokoro@google.com>
This commit is contained in:
1
util/maint/lib/__init__.py
Normal file
1
util/maint/lib/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#!/usr/bin/env python3
|
||||
180
util/maint/lib/maintainers.py
Normal file
180
util/maint/lib/maintainers.py
Normal file
@@ -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",
|
||||
]
|
||||
1
util/maint/lib/tests/__init__.py
Normal file
1
util/maint/lib/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#!/usr/bin/env python3
|
||||
117
util/maint/lib/tests/maintainers.py
Normal file
117
util/maint/lib/tests/maintainers.py
Normal file
@@ -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 <john.doe@test.gem5.org>
|
||||
- Jane Doe <jane.doe@test.gem5.org>
|
||||
|
||||
# Test that we can handle a subsystem without maintainers
|
||||
orphaned:
|
||||
desc: Abandoned
|
||||
status: orphaned
|
||||
"""
|
||||
|
||||
YAML_MISSING_STATUS = r"""
|
||||
key:
|
||||
maintainers:
|
||||
- John Doe <john.doe@test.gem5.org>
|
||||
"""
|
||||
|
||||
YAML_INVALID_STATUS = r"""
|
||||
key:
|
||||
status: invalid_status_name
|
||||
maintainers:
|
||||
- John Doe <john.doe@test.gem5.org>
|
||||
"""
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user