Repository: pallets/click
Pull request #1405 (2019-10-08 22:36:07)

Add support for calling ``click.Group(commands=[...])`` with a list
Resolves https://github.com/pallets/click/issues/1339.



Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 1bb2d6c..d731f9f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -82,6 +82,8 @@ Unreleased
 -   When using ``CliRunner.invoke()``, the replaced ``stdin`` file has
     ``name`` and ``mode`` attributes. This lets ``File`` options with
     the ``-`` value match non-testing behavior. :issue:`1064`
+-   When creating a ``Group``, allow passing a list of commands instead
+    of a dict. :issue:`1339`
 
 
 Version 7.1.2
diff --git a/src/click/core.py b/src/click/core.py
index a678bd7..2df179f 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -1430,10 +1430,19 @@ class MultiCommand(Command):
 
 
 class Group(MultiCommand):
-    """A group allows a command to have subcommands attached.  This is the
-    most common way to implement nesting in Click.
+    """A group allows a command to have subcommands attached. This is
+    the most common way to implement nesting in Click.
 
-    :param commands: a dictionary of commands.
+    :param name: The name of the group command.
+    :param commands: A dict mapping names to :class:`Command` objects.
+        Can also be a list of :class:`Command`, which will use
+        :attr:`Command.name` to create the dict.
+    :param attrs: Other command arguments described in
+        :class:`MultiCommand`, :class:`Command`, and
+        :class:`BaseCommand`.
+
+    .. versionchanged:: 8.0
+        The ``commmands`` argument can be a list of command objects.
     """
 
     #: If set, this is used by the group's :meth:`command` decorator
@@ -1457,8 +1466,14 @@ class Group(MultiCommand):
 
     def __init__(self, name=None, commands=None, **attrs):
         super().__init__(name, **attrs)
-        #: the registered subcommands by their exported names.
-        self.commands = commands or {}
+
+        if commands is None:
+            commands = {}
+        elif isinstance(commands, (list, tuple)):
+            commands = {c.name: c for c in commands}
+
+        #: The registered subcommands by their exported names.
+        self.commands = commands
 
     def add_command(self, cmd, name=None):
         """Registers another :class:`Command` with this group.  If the name


Test patch:
diff --git a/tests/test_basic.py b/tests/test_basic.py
index 8ab94dc..cc0d0f2 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -80,6 +80,30 @@ def test_basic_group(runner):
     assert "SUBCOMMAND EXECUTED" in result.output
 
 
+def test_group_commands_dict(runner):
+    """A Group can be built with a dict of commands."""
+
+    @click.command()
+    def sub():
+        click.echo("sub", nl=False)
+
+    cli = click.Group(commands={"other": sub})
+    result = runner.invoke(cli, ["other"])
+    assert result.output == "sub"
+
+
+def test_group_from_list(runner):
+    """A Group can be built with a list of commands."""
+
+    @click.command()
+    def sub():
+        click.echo("sub", nl=False)
+
+    cli = click.Group(commands=[sub])
+    result = runner.invoke(cli, ["sub"])
+    assert result.output == "sub"
+
+
 def test_basic_option(runner):
     @click.command()
     @click.option("--foo", default="no value")