From b0abbfecb8595c8901855e679be0bbea64089fe8 Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Wed, 24 Jul 2019 00:17:06 -0700 Subject: new command: `spack maintainers` queries package maintainers - We don't currently make enough use of the maintainers field on packages, though we could use it to assign reviews. - add a command that allows maintainers to be queried - can ask who is maintaining a package or packages - can ask what packages users are maintaining - can list all maintained or unmaintained packages - add tests for the command --- lib/spack/spack/cmd/maintainers.py | 134 +++++++++++++++++++++ lib/spack/spack/test/cmd/maintainers.py | 117 ++++++++++++++++++ share/spack/spack-completion.bash | 10 ++ .../builtin.mock/packages/maintainers-1/package.py | 20 +++ .../builtin.mock/packages/maintainers-2/package.py | 20 +++ 5 files changed, 301 insertions(+) create mode 100644 lib/spack/spack/cmd/maintainers.py create mode 100644 lib/spack/spack/test/cmd/maintainers.py create mode 100644 var/spack/repos/builtin.mock/packages/maintainers-1/package.py create mode 100644 var/spack/repos/builtin.mock/packages/maintainers-2/package.py diff --git a/lib/spack/spack/cmd/maintainers.py b/lib/spack/spack/cmd/maintainers.py new file mode 100644 index 0000000000..361437ece5 --- /dev/null +++ b/lib/spack/spack/cmd/maintainers.py @@ -0,0 +1,134 @@ +# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other +# Spack Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from __future__ import print_function + +import argparse +from collections import defaultdict + +import llnl.util.tty as tty +import llnl.util.tty.color as color +from llnl.util.tty.colify import colify + + +import spack.repo + +description = "get information about package maintainers" +section = "developer" +level = "long" + + +def setup_parser(subparser): + maintained_group = subparser.add_mutually_exclusive_group() + maintained_group.add_argument( + '--maintained', action='store_true', default=False, + help='show names of maintained packages') + + maintained_group.add_argument( + '--unmaintained', action='store_true', default=False, + help='show names of unmaintained packages') + + subparser.add_argument( + '-a', '--all', action='store_true', default=False, + help='show maintainers for all packages') + + subparser.add_argument( + '--by-user', action='store_true', default=False, + help='show packages for users instead of users for packages') + + # options for commands that take package arguments + subparser.add_argument( + 'pkg_or_user', nargs=argparse.REMAINDER, + help='names of packages or users to get info for') + + +def packages_to_maintainers(package_names=None): + if not package_names: + package_names = spack.repo.path.all_package_names() + + pkg_to_users = defaultdict(lambda: set()) + for name in package_names: + cls = spack.repo.path.get_pkg_class(name) + for user in cls.maintainers: + pkg_to_users[name].add(user) + + return pkg_to_users + + +def maintainers_to_packages(users=None): + user_to_pkgs = defaultdict(lambda: []) + for name in spack.repo.path.all_package_names(): + cls = spack.repo.path.get_pkg_class(name) + for user in cls.maintainers: + lower_users = [u.lower() for u in users] + if not users or user.lower() in lower_users: + user_to_pkgs[user].append(cls.name) + + return user_to_pkgs + + +def maintained_packages(): + maintained = [] + unmaintained = [] + for name in spack.repo.path.all_package_names(): + cls = spack.repo.path.get_pkg_class(name) + if cls.maintainers: + maintained.append(name) + else: + unmaintained.append(name) + + return maintained, unmaintained + + +def union_values(dictionary): + """Given a dictionary with values that are Collections, return their union. + + Arguments: + dictionary (dict): dictionary whose values are all collections. + + Return: + (set): the union of all collections in the dictionary's values. + """ + sets = [set(p) for p in dictionary.values()] + return sorted(set.union(*sets)) if sets else set() + + +def maintainers(parser, args): + if args.maintained or args.unmaintained: + maintained, unmaintained = maintained_packages() + pkgs = maintained if args.maintained else unmaintained + colify(pkgs) + return 0 if pkgs else 1 + + if args.all: + if args.by_user: + maintainers = maintainers_to_packages(args.pkg_or_user) + for user, packages in sorted(maintainers.items()): + color.cprint('@c{%s}: %s' + % (user, ', '.join(sorted(packages)))) + return 0 if maintainers else 1 + + else: + packages = packages_to_maintainers(args.pkg_or_user) + for pkg, maintainers in sorted(packages.items()): + color.cprint('@c{%s}: %s' + % (pkg, ', '.join(sorted(maintainers)))) + return 0 if packages else 1 + + if args.by_user: + if not args.pkg_or_user: + tty.die('spack maintainers --by-user requires a user or --all') + + packages = union_values(maintainers_to_packages(args.pkg_or_user)) + colify(packages) + return 0 if packages else 1 + + else: + if not args.pkg_or_user: + tty.die('spack maintainers requires a package or --all') + + users = union_values(packages_to_maintainers(args.pkg_or_user)) + colify(users) + return 0 if users else 1 diff --git a/lib/spack/spack/test/cmd/maintainers.py b/lib/spack/spack/test/cmd/maintainers.py new file mode 100644 index 0000000000..5ddf176c39 --- /dev/null +++ b/lib/spack/spack/test/cmd/maintainers.py @@ -0,0 +1,117 @@ +# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other +# Spack Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from __future__ import print_function + +import pytest +import re + +import spack.main +import spack.repo + +maintainers = spack.main.SpackCommand('maintainers') + + +def split(output): + """Split command line output into an array.""" + output = output.strip() + return re.split(r'\s+', output) if output else [] + + +def test_maintained(mock_packages): + out = split(maintainers('--maintained')) + assert out == ['maintainers-1', 'maintainers-2'] + + +def test_unmaintained(mock_packages): + out = split(maintainers('--unmaintained')) + assert out == sorted( + set(spack.repo.all_package_names()) - + set(['maintainers-1', 'maintainers-2'])) + + +def test_all(mock_packages, capfd): + with capfd.disabled(): + out = split(maintainers('--all')) + assert out == [ + 'maintainers-1:', 'user1,', 'user2', + 'maintainers-2:', 'user2,', 'user3', + ] + + with capfd.disabled(): + out = split(maintainers('--all', 'maintainers-1')) + assert out == [ + 'maintainers-1:', 'user1,', 'user2', + ] + + +def test_all_by_user(mock_packages, capfd): + with capfd.disabled(): + out = split(maintainers('--all', '--by-user')) + assert out == [ + 'user1:', 'maintainers-1', + 'user2:', 'maintainers-1,', 'maintainers-2', + 'user3:', 'maintainers-2', + ] + + with capfd.disabled(): + out = split(maintainers('--all', '--by-user', 'user1', 'user2')) + assert out == [ + 'user1:', 'maintainers-1', + 'user2:', 'maintainers-1,', 'maintainers-2', + ] + + +def test_no_args(mock_packages): + with pytest.raises(spack.main.SpackCommandError): + maintainers() + + +def test_no_args_by_user(mock_packages): + with pytest.raises(spack.main.SpackCommandError): + maintainers('--by-user') + + +def test_mutex_args_fail(mock_packages): + with pytest.raises(SystemExit): + maintainers('--maintained', '--unmaintained') + + +def test_maintainers_list_packages(mock_packages, capfd): + with capfd.disabled(): + out = split(maintainers('maintainers-1')) + assert out == ['user1', 'user2'] + + with capfd.disabled(): + out = split(maintainers('maintainers-1', 'maintainers-2')) + assert out == ['user1', 'user2', 'user3'] + + with capfd.disabled(): + out = split(maintainers('maintainers-2')) + assert out == ['user2', 'user3'] + + +def test_maintainers_list_fails(mock_packages, capfd): + out = maintainers('a', fail_on_error=False) + assert not out + assert maintainers.returncode == 1 + + +def test_maintainers_list_by_user(mock_packages, capfd): + with capfd.disabled(): + out = split(maintainers('--by-user', 'user1')) + assert out == ['maintainers-1'] + + with capfd.disabled(): + out = split(maintainers('--by-user', 'user1', 'user2')) + assert out == ['maintainers-1', 'maintainers-2'] + + with capfd.disabled(): + out = split(maintainers('--by-user', 'user2')) + assert out == ['maintainers-1', 'maintainers-2'] + + with capfd.disabled(): + out = split(maintainers('--by-user', 'user3')) + assert out == ['maintainers-2'] diff --git a/share/spack/spack-completion.bash b/share/spack/spack-completion.bash index 362fe1e0bf..1edc262d0a 100755 --- a/share/spack/spack-completion.bash +++ b/share/spack/spack-completion.bash @@ -700,6 +700,16 @@ function _spack_log_parse { fi } +function _spack_maintainers { + if $list_options + then + compgen -W "-h --help -a --all --maintained --unmaintained + --by-user" -- "$cur" + else + compgen -W "$(_all_packages)" -- "$cur" + fi +} + function _spack_mirror { if $list_options then diff --git a/var/spack/repos/builtin.mock/packages/maintainers-1/package.py b/var/spack/repos/builtin.mock/packages/maintainers-1/package.py new file mode 100644 index 0000000000..75917da005 --- /dev/null +++ b/var/spack/repos/builtin.mock/packages/maintainers-1/package.py @@ -0,0 +1,20 @@ +# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other +# Spack Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack import * + + +class Maintainers1(Package): + """Package with a maintainers field.""" + + homepage = "http://www.example.com" + url = "http://www.example.com/maintainers-1.0.tar.gz" + + maintainers = ['user1', 'user2'] + + version('1.0', '0123456789abcdef0123456789abcdef') + + def install(self, spec, prefix): + pass diff --git a/var/spack/repos/builtin.mock/packages/maintainers-2/package.py b/var/spack/repos/builtin.mock/packages/maintainers-2/package.py new file mode 100644 index 0000000000..a198bde1e3 --- /dev/null +++ b/var/spack/repos/builtin.mock/packages/maintainers-2/package.py @@ -0,0 +1,20 @@ +# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other +# Spack Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack import * + + +class Maintainers2(Package): + """A second package with a maintainers field.""" + + homepage = "http://www.example.com" + url = "http://www.example.com/maintainers2-1.0.tar.gz" + + maintainers = ['user2', 'user3'] + + version('1.0', '0123456789abcdef0123456789abcdef') + + def install(self, spec, prefix): + pass -- cgit v1.2.3-60-g2f50