Repository: repobee/repobee

Issue:
Extension command callbacks should just return a plug.Result, not a mapping
Currently, the extension command callbacks have the following signature:

```
def callback(
    args: argparse.Namespace, api: Optional[plug.API]
) -> Optional[Mapping[str, List[plug.Result]]]:
```
The return value makes absolutely no sense, as when you run an extension command, only one command can be run! So the mapping will always only contain a single key/value pair. It also doesn't make sense to return a list of `plug.Result`, as the extension command should do one thing.

The signature should really be this:

```
def callback(
    args: argparse.Namespace, api: Optional[plug.API]
) -> Optional[plug.Result]:
```

Discussion:
slarse: Travis build is not triggering. Guess it's just one of those days.
codecov[bot]: # [Codecov](https://codecov.io/gh/repobee/repobee/pull/482?src=pr&el=h1) Report
> Merging [#482](https://codecov.io/gh/repobee/repobee/pull/482?src=pr&el=desc) into [master](https://codecov.io/gh/repobee/repobee/commit/c184fc066c6f7ed27e7fea84ac5f126c3420a0a4&el=desc) will **decrease** coverage by `3.78%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/repobee/repobee/pull/482/graphs/tree.svg?width=650&height=150&src=pr&token=5vOKgkphZe)](https://codecov.io/gh/repobee/repobee/pull/482?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #482      +/-   ##
==========================================
- Coverage   92.46%   88.67%   -3.79%     
==========================================
  Files          42       42              
  Lines        2215     2226      +11     
  Branches      380      379       -1     
==========================================
- Hits         2048     1974      -74     
- Misses        134      216      +82     
- Partials       33       36       +3     
```


| [Impacted Files](https://codecov.io/gh/repobee/repobee/pull/482?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/repobee\_plug/\_containers.py](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree#diff-c3JjL3JlcG9iZWVfcGx1Zy9fY29udGFpbmVycy5weQ==) | `100.00% <ø> (ø)` | |
| [src/\_repobee/cli/dispatch.py](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NsaS9kaXNwYXRjaC5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/\_repobee/ext/gitlab.py](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2V4dC9naXRsYWIucHk=) | `64.25% <0.00%> (-28.08%)` | :arrow_down: |
| [src/\_repobee/git.py](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2dpdC5weQ==) | `96.62% <0.00%> (-3.38%)` | :arrow_down: |
| [src/\_repobee/command/repos.py](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NvbW1hbmQvcmVwb3MucHk=) | `91.80% <0.00%> (-3.28%)` | :arrow_down: |
| [src/\_repobee/cli/parsing.py](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2NsaS9wYXJzaW5nLnB5) | `92.26% <0.00%> (-1.20%)` | :arrow_down: |
| [src/repobee\_plug/\_corehooks.py](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree#diff-c3JjL3JlcG9iZWVfcGx1Zy9fY29yZWhvb2tzLnB5) | `100.00% <0.00%> (ø)` | |
| [src/repobee\_plug/\_exceptions.py](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree#diff-c3JjL3JlcG9iZWVfcGx1Zy9fZXhjZXB0aW9ucy5weQ==) | `100.00% <0.00%> (ø)` | |
| [src/repobee\_plug/\_pluginmeta.py](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree#diff-c3JjL3JlcG9iZWVfcGx1Zy9fcGx1Z2lubWV0YS5weQ==) | `100.00% <0.00%> (ø)` | |
| [src/\_repobee/ext/defaults/configwizard.py](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree#diff-c3JjL19yZXBvYmVlL2V4dC9kZWZhdWx0cy9jb25maWd3aXphcmQucHk=) | `100.00% <0.00%> (ø)` | |
| ... and [4 more](https://codecov.io/gh/repobee/repobee/pull/482/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/repobee/repobee/pull/482?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/repobee/repobee/pull/482?src=pr&el=footer). Last update [c184fc0...3d775be](https://codecov.io/gh/repobee/repobee/pull/482?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/repobee_plug/creating_plugins.rst b/docs/repobee_plug/creating_plugins.rst
index 8a98f4a..8321f9c 100644
--- a/docs/repobee_plug/creating_plugins.rst
+++ b/docs/repobee_plug/creating_plugins.rst
@@ -444,12 +444,11 @@ will look something like this instead:
 
     def callback(
         args: argparse.Namespace, api: Optional[plug.API]
-    ) -> Optional[Mapping[str, List[plug.Result]]]:
+    ) -> Optional[plug.Result]:
         # do whatever you want to do!
-        return {
-            PLUGIN_NAME: [plug.Result(
+        return plug.Result(
                 name=PLUGIN_NAME, status=plug.Status.SUCCESS, msg="Hello, world!"
-            )]
+            )
         }
 
     @plug.repobee_hook
diff --git a/src/_repobee/cli/dispatch.py b/src/_repobee/cli/dispatch.py
index bcf99e8..7d56c15 100644
--- a/src/_repobee/cli/dispatch.py
+++ b/src/_repobee/cli/dispatch.py
@@ -56,7 +56,7 @@ def dispatch_command(
     if is_ext_command:
         ext_cmd = ext_commands[ext_command_names.index(args.subparser)]
         res = ext_cmd.callback(args, api)
-        hook_results = res if res else hook_results
+        hook_results = {ext_cmd.name: [res]} if res else hook_results
     elif args.subparser == SETUP_PARSER:
         hook_results = command.setup_student_repos(
             args.master_repo_urls, args.students, api
diff --git a/src/repobee_plug/_containers.py b/src/repobee_plug/_containers.py
index 7d9c444..c750f23 100644
--- a/src/repobee_plug/_containers.py
+++ b/src/repobee_plug/_containers.py
@@ -184,8 +184,7 @@ class ExtensionCommand(
         help: str,
         description: str,
         callback: Callable[
-            [argparse.Namespace, Optional[_apimeta.API]],
-            Optional[Mapping[str, Result]],
+            [argparse.Namespace, Optional[_apimeta.API]], Optional[Result],
         ],
         requires_api: bool = False,
         requires_base_parsers: Optional[Iterable[BaseParser]] = None,
@@ -200,9 +199,8 @@ class ExtensionCommand(
                 describing the usage of the command.
             callback: A callback function that is called if this command is
                 used on the CLI. It is passed the parsed namespace and the
-                platform API. It may optionally return a result mapping on
-                the form (name: str -> List[Result]) that's reported by
-                RepoBee.
+                platform API. It may optionally return a plugin result that's
+                reported by RepoBee's CLI.
             requires_api: If True, the base arguments required for the platform
                 API are added as options to the extension command, and the
                 platform API is then passed to the callback function. It is


Test patch:
diff --git a/tests/unit_tests/repobee/test_cli.py b/tests/unit_tests/repobee/test_cli.py
index f8479f3..7e34e21 100644
--- a/tests/unit_tests/repobee/test_cli.py
+++ b/tests/unit_tests/repobee/test_cli.py
@@ -17,6 +17,7 @@ import _repobee.ext.defaults.github
 import _repobee.ext.query
 import _repobee.constants
 import _repobee.plugin
+import _repobee.ext.defaults.configwizard
 from _repobee.cli import mainparser
 from _repobee import exception
 
@@ -709,8 +710,12 @@ class TestExtensionCommands:
         """Return a mock callback function for use with an extension
         command.
         """
+        result = plug.Result(
+            name="mock", msg="Hello!", status=plug.Status.SUCCESS
+        )
         yield mock.MagicMock(
-            spec=_repobee.ext.defaults.configwizard.create_extension_command
+            spec=_repobee.ext.defaults.configwizard.callback,
+            return_value=result,
         )
 
     @pytest.fixture
@@ -805,13 +810,7 @@ class TestExtensionCommands:
             parsed_args, None, EMPTY_PATH, [ext_command]
         )
 
-        # for some reason, this completely sane assertion fails ALWAYS
-        # mock_callback.assert_called_once_with(
-        #   parsed_args, None)
-
-        # this is a workaround
-        assert len(mock_callback.call_args_list) == 1
-        assert mock_callback.call_args_list[0] == mock.call(parsed_args, None)
+        mock_callback.assert_called_once_with(parsed_args, None)
 
     def test_dispatch_ext_command_that_requires_api(
         self,
@@ -841,15 +840,7 @@ class TestExtensionCommands:
             parsed_args, api_instance_mock, EMPTY_PATH, [ext_command]
         )
 
-        # for some reason, this completely sane assertion fails ALWAYS
-        # mock_callback.assert_called_once_with(
-        #   parsed_args, api_instance_mock)
-
-        # this is a workaround
-        assert len(mock_callback.call_args_list) == 1
-        assert mock_callback.call_args_list[0] == mock.call(
-            parsed_args, api_instance_mock
-        )
+        mock_callback.assert_called_once_with(parsed_args, api_instance_mock)
 
     def test_parse_ext_command_that_requires_base_parsers(self):
         """The query command requires the students and repo names parsers."""