Repository: pallets/click

Issue:
group return value is ignored
Calling a command defined as `@click.group()` with `standalone_mode=False` does not propagate the return value as expected.

For instance:
```python
import click
import sys

@click.group(invoke_without_command=True)
@click.option("-l", "--local-configuration", type=str)
def main(local_configuration):
    print('Inside main')
    return 1

if __name__ == '__main__':
    print(main(sys.argv[1:], standalone_mode=False))
```

```
$ python test.py
Inside main
None
```

It does work when the command is defined with `@click.command()`, hence I suppose this is a bug.

Environment:

- Python version: 3.7.5
- Click version: 8.0.3


Discussion:
Thanks for opening the issue, I ran a git bisect to see where this behavior changed. 
It showed https://github.com/pallets/click/commit/079f91a2396c5c9264f34f95894259b7b15eb89c as when the behavior changed I'm not sure why moving to a src directory would cause this. 
It seems related to
https://github.com/pallets/click/blob/d5932d674d804cf67b20e4f1c3546abf5aa65494/src/click/core.py#L1633-L1638
where return value of `super().invoke(ctx)` is replaced with `[]` or `None` before passed to `_process_result`.  Return value propagation (without subcommand invocation) stops here with `invoke_without_command=True`, regardless of `standalone_mode`.
Not sure what to do with this one. The difference in behavior between groups and commands wasn't documented, and it's not clear why group returns weren't used when adding support for command return values. I think it could make sense to use the group's return value as well though. The problem is making sure existing code continues to work after the change, at least for a deprecation period.

The return value of a command is the return value, if any. The return value of a *group* is the result of calling its `result_callback` on the return value(s) of it subcommand(s). `result_callback` only gets the subcommand result, it won't get a value returned by the group itself.

* A group can be invoked without a subcommand, and returns a value. Would the value be returned directly, or still passed through the result callback? What if the callback only expects values returned by subcommands?
* A subcommand is required, and only the subcommand returns a value. This is the current expected behavior.
* A subcommand is required, and both the group and the subcommand return values. This is tricky because the result callback would only be expecting one value based on current behavior. Should the subcommand value overwrite the group value?
* The group is in chain mode, the return value of each subcommand is collected in a list. Should the return value of the group be the first item in the list? What if the group doesn't return anything, and the result callback expects that every item is not None (that's how the example in the docs works)?

A possible solution that sidesteps the questions above: only use the group return value if invoked without subcommand. That is, if the group is treated like a command itself, return its value as if it were a command.

Regardless of the solution, the behavior of return values needs to be much better documented.

For now, you could add a result callback to the group that returns what you would have returned from the group if it was used without a subcommand.

```python
@main.result_callback
@click.pass_context
def group_result(ctx, value):
    if ctx.invoked_subcommand is None:
        return 1
```
For #1178, in 8.0 we changed the behavior so that the result callback was called even if there was no subcommand, passing `None`.

Previously, the group return value *was* returned, as long as the group wasn't in chain mode and was called without a subcommand. In chain mode, the group return was always ignored since the intention in that mode was to have a pipeline of return values.

The behavior before 8.0 was basically what I just described as a possible solution to this issue. Now that I've looked over this more, I think that makes more sense, I think the change should be reverted in 8.1.
A compromise that should satisfy that issue and this one would be to pass the group return value to the result callback if it's invoked directly. As shown in the workaround I posted above, you can tell whether the value is from a subcommand or not based on `ctx.invoked_subcommand`.

Resolving patch:
diff --git a/src/click/core.py b/src/click/core.py
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -1626,11 +1626,11 @@ def _process_result(value: t.Any) -> t.Any:
         if not ctx.protected_args:
             if self.invoke_without_command:
                 # No subcommand was invoked, so the result callback is
-                # invoked with None for regular groups, or an empty list
-                # for chained groups.
+                # invoked with the group return value for regular
+                # groups, or an empty list for chained groups.
                 with ctx:
-                    super().invoke(ctx)
-                    return _process_result([] if self.chain else None)
+                    rv = super().invoke(ctx)
+                    return _process_result([] if self.chain else rv)
             ctx.fail(_("Missing command."))
 
         # Fetch args back out


Test patch:
diff --git a/tests/test_chain.py b/tests/test_chain.py
--- a/tests/test_chain.py
+++ b/tests/test_chain.py
@@ -85,20 +85,20 @@ def bdist(format):
     assert result.output.splitlines() == ["bdist called 1", "sdist called 2"]
 
 
-@pytest.mark.parametrize(("chain", "expect"), [(False, "None"), (True, "[]")])
+@pytest.mark.parametrize(("chain", "expect"), [(False, "1"), (True, "[]")])
 def test_no_command_result_callback(runner, chain, expect):
     """When a group has ``invoke_without_command=True``, the result
     callback is always invoked. A regular group invokes it with
-    ``None``, a chained group with ``[]``.
+    its return value, a chained group with ``[]``.
     """
 
     @click.group(invoke_without_command=True, chain=chain)
     def cli():
-        pass
+        return 1
 
     @cli.result_callback()
     def process_result(result):
-        click.echo(str(result), nl=False)
+        click.echo(result, nl=False)
 
     result = runner.invoke(cli, [])
     assert result.output == expect